Refactors pay stands + custom vendors (#63889)

I am disgruntled by the way pay stations work. They're not intuitive, they're a pain to build and have no interface. Basically: They don't get made, and the potential is lost.

Pay stands => Holopay

    Summoned by right clicking your ID
    Disappears if the card is out of range.
    New TGUI window that offers more customization

Other bundled fixes:

    Custom vendors become more user friendly
    Code improvement
    Lots of documentation + refactoring
    New bundled number input will likely take place of animated number in tgui input number

Why It's Good For The Game

More RP opportunity for players, plus bug fixes. It's now much easier for players to start their own in game business selling substances clown shoes.
Changelog

cl
code: Created a new input component that accepts only integers. More usage to come.
refactor: Pay stands are now holographic. It's 2562! Create one by right-clicking your ID.
del: Circuit boards for pay stands.
refactor: Pay stands now have their own TGUI.
fix: Custom vendors now alert you when someone makes a purchase.
fix: Custom vendors now place items in your hand when you make a purchase.
/cl
This commit is contained in:
Jeremiah
2022-02-01 09:47:28 +13:00
committed by GitHub
parent 631fefb946
commit cdfd5df7be
18 changed files with 853 additions and 245 deletions
+1 -1
View File
@@ -173,7 +173,7 @@
to_chat(user,span_notice("You don't even have a id and you want to be an art patron?"))
return
if(!id_card.registered_account || !id_card.registered_account.account_job)
to_chat(user,span_notice("No valid non-departamental account found."))
to_chat(user,span_notice("No valid non-departmental account found."))
return
var/datum/bank_account/account = id_card.registered_account
if(account.account_balance < painting_metadata.credit_value)
+268
View File
@@ -0,0 +1,268 @@
/obj/structure/holopay
name = "holographic pay stand"
desc = "an unregistered pay stand"
icon = 'icons/obj/economy.dmi'
icon_state = "card_scanner"
alpha = 150
anchored = TRUE
armor = list(MELEE = 0, BULLET = 50, LASER = 50, ENERGY = 50, BOMB = 0, BIO = 0, FIRE = 20, ACID = 20)
max_integrity = 15
layer = FLY_LAYER
/// ID linked to the holopay
var/datum/weakref/card_ref
/// Max range at which the hologram can be projected before it deletes
var/max_holo_range = 4
/// The holopay shop icon displayed in the UI
var/shop_logo = "donate"
/// Replaces the "pay whatever" functionality with a set amount when non-zero.
var/force_fee = 0
/obj/structure/holopay/examine(mob/user)
. = ..()
if(force_fee)
. += span_boldnotice("This holopay forces a payment of <b>[force_fee]</b> credit\s per swipe instead of a variable amount.")
/obj/structure/holopay/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(.)
return
if(!user.combat_mode)
ui_interact(user)
return .
user.do_attack_animation(src, ATTACK_EFFECT_PUNCH)
user.changeNext_move(CLICK_CD_MELEE)
take_damage(5, BRUTE, MELEE, 1)
/obj/structure/holopay/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
switch(damage_type)
if(BRUTE)
playsound(loc, 'sound/weapons/egloves.ogg', 80, TRUE)
if(BURN)
playsound(loc, 'sound/weapons/egloves.ogg', 80, TRUE)
/obj/structure/holopay/deconstruct()
dissapate()
return ..()
/obj/structure/holopay/attackby(obj/item/held_item, mob/holder, params)
var/mob/living/user = holder
if(!isliving(user))
return ..()
/// Users can pay with an ID to skip the UI
if(istype(held_item, /obj/item/card/id))
if(force_fee && tgui_alert(holder, "This holopay has a [force_fee] cr fee. Confirm?", "Holopay Fee", list("Pay", "Cancel")) != "Pay")
return TRUE
process_payment(user)
return TRUE
/// Users can also pay by holochip
if(istype(held_item, /obj/item/holochip))
/// Account checks
var/obj/item/holochip/chip = held_item
if(!chip.credits)
balloon_alert(user, "holochip is empty")
to_chat(user, span_warning("There doesn't seem to be any credits here."))
return FALSE
/// Charges force fee or uses pay what you want
var/cash_deposit = force_fee || tgui_input_number(user, "How much? (Max: [chip.credits])", "Patronage", max_value = chip.credits)
/// Exit sanity checks
if(!cash_deposit)
return TRUE
if(QDELETED(held_item) || QDELETED(user) || QDELETED(src) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
return FALSE
if(!chip.spend(cash_deposit, FALSE))
balloon_alert(user, "insufficient credits")
to_chat(user, span_warning("You don't have enough credits to pay with this chip."))
return FALSE
/// Success: Alert buyer
alert_buyer(user, cash_deposit)
return TRUE
/// Throws errors if they try to use space cash
if(istype(held_item, /obj/item/stack/spacecash))
to_chat(user, "What is this, the 2000s? We only take card here.")
return TRUE
if(istype(held_item, /obj/item/coin))
to_chat(user, "What is this, the 1800s? We only take card here.")
return TRUE
return ..()
/obj/structure/holopay/ui_interact(mob/user, datum/tgui/ui)
. = ..()
if(.)
return FALSE
var/mob/living/interactor = user
if(isliving(interactor) && interactor.combat_mode)
return FALSE
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "HoloPay")
ui.open()
/obj/structure/holopay/ui_status(mob/user)
. = ..()
if(!in_range(user, src) && !isobserver(user))
return UI_CLOSE
/obj/structure/holopay/ui_static_data(mob/user)
. = list()
/// Sanity checks
var/obj/item/card/id/linked_card = get_card()
.["available_logos"] = linked_card.available_logos
.["description"] = desc
.["max_fee"] = linked_card.holopay_max_fee
.["owner"] = linked_card.registered_account?.account_holder || null
.["shop_logo"] = shop_logo
/obj/structure/holopay/ui_data(mob/user)
. = list()
.["force_fee"] = force_fee
.["name"] = name
if(!isliving(user))
return .
var/mob/living/card_holder = user
var/obj/item/card/id/id_card = card_holder.get_idcard(TRUE)
var/datum/bank_account/account = id_card?.registered_account || null
if(account)
.["user"] = list()
.["user"]["name"] = account.account_holder
.["user"]["balance"] = account.account_balance
/obj/structure/holopay/ui_act(action, list/params, datum/tgui/ui)
. = ..()
if(.)
return FALSE
var/obj/item/card/id/linked_card = get_card()
switch(action)
if("done")
ui.send_full_update()
return TRUE
if("fee")
linked_card.set_holopay_fee(params["amount"])
force_fee = linked_card.holopay_fee
if("logo")
linked_card.set_holopay_logo(params["logo"])
shop_logo = linked_card.holopay_logo
if("pay")
ui.close()
process_payment(usr)
return TRUE
if("rename")
linked_card.set_holopay_name(params["name"])
name = linked_card.holopay_name
return FALSE
/**
* Links the source card to the holopay. Begins checking if its in range.
*
* Parameters:
* * turf/target - The tile to project the holopay onto
* * obj/item/card/id/card - The card to link to the holopay
* Returns:
* * TRUE - the card was linked
*/
/obj/structure/holopay/proc/assign_card(turf/target, obj/item/card/id/card)
card_ref = WEAKREF(card)
desc = "Pays directly into [card.registered_account.account_holder]'s bank account."
force_fee = card.holopay_fee
shop_logo = card.holopay_logo
name = card.holopay_name
add_atom_colour("#77abff", FIXED_COLOUR_PRIORITY)
set_light(2)
visible_message(span_notice("A holographic pay stand appears."))
/// Start checking if the source projection is in range
RegisterSignal(card, COMSIG_MOVABLE_MOVED, .proc/check_operation)
if(card.loc)
RegisterSignal(card.loc, COMSIG_MOVABLE_MOVED, .proc/check_operation)
return TRUE
/**
* A periodic check to see if the projecting card is nearby.
* Deletes the holopay if true.
*/
/obj/structure/holopay/proc/check_operation()
SIGNAL_HANDLER
var/obj/item/card/id/linked_card = get_card()
if(!IN_GIVEN_RANGE(src, linked_card, max_holo_range) || !IN_GIVEN_RANGE(src, linked_card.loc, max_holo_range))
dissapate()
/**
* Creates holopay vanishing effects.
* Deletes the holopay thereafter.
*/
/obj/structure/holopay/proc/dissapate()
var/obj/item/card/id/linked_card = get_card()
playsound(loc, "sound/effects/empulse.ogg", 40, TRUE)
visible_message(span_notice("The pay stand vanishes."))
QDEL_NULL(linked_card.holopay_ref)
/**
* Checks that the card is still linked.
* Deletes the holopay if not.
*
* Returns:
* * /obj/item/card/id/card - The card that is linked to the holopay
*/
/obj/structure/holopay/proc/get_card()
var/obj/item/card/id/linked_card = card_ref?.resolve()
if(!linked_card || !istype(linked_card, /obj/item/card/id))
stack_trace("Could not link a holopay to a valid card.")
qdel(src)
return linked_card
/**
* Initiates a transaction between accounts.
*
* Parameters:
* * mob/living/user - The user who initiated the transaction.
* Returns:
* * TRUE - transaction was successful
*/
/obj/structure/holopay/proc/process_payment(mob/living/user)
// Preliminary sanity checks
var/obj/item/card/id/linked_card = get_card()
/// Account checks
var/obj/item/card/id/id_card
id_card = user.get_idcard(TRUE)
if(!id_card || !id_card.registered_account || !id_card.registered_account.account_job)
balloon_alert(user, "invalid account")
to_chat(user, span_warning("You don't have a valid account."))
return FALSE
var/datum/bank_account/payee = id_card.registered_account
if(payee == linked_card.registered_account)
balloon_alert(user, "invalid transaction")
to_chat(user, span_warning("You can't pay yourself."))
return FALSE
/// If the user has enough money, ask them the amount or charge the force fee
var/amount = force_fee || tgui_input_number(user, "How much? (Max: [payee.account_balance])", "Patronage", max_value = payee.account_balance)
/// Exit checks in case the user cancelled or entered an invalid amount
if(!amount || QDELETED(user) || QDELETED(src) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
return FALSE
if(!payee.adjust_money(-amount))
balloon_alert(user, "insufficient credits")
to_chat(user, span_warning("You don't have the money to pay for this."))
return FALSE
/// Success: Alert the buyer
alert_buyer(user, amount)
return TRUE
/**
* Alerts the owner of the transaction.
*
* Parameters:
* * payee - The user who initiated the transaction.
* * amount - The amount of money that was paid.
* Returns:
* * TRUE - alert was successful.
*/
/obj/structure/holopay/proc/alert_buyer(payee, amount)
/// Sanity checks
var/obj/item/card/id/linked_card = get_card()
/// Pay the owner
linked_card.registered_account.adjust_money(amount)
/// Make alerts
linked_card.registered_account.bank_card_talk("[payee] has deposited [amount] cr at your holographic pay stand.")
say("Thank you for your patronage, [payee]!")
playsound(src, 'sound/effects/cashregister.ogg', 20, TRUE)
/// Log the event
log_econ("[amount] credits were transferred from [payee]'s transaction to [linked_card.registered_account.account_holder]")
SSblackbox.record_feedback("amount", "credits_transferred", amount)
return TRUE
-152
View File
@@ -1,152 +0,0 @@
/obj/machinery/paystand
name = "unregistered pay stand"
desc = "See title."
icon = 'icons/obj/economy.dmi'
icon_state = "card_scanner"
density = TRUE
anchored = TRUE
var/locked = FALSE
var/obj/item/card/id/my_card
var/obj/item/assembly/signaler/signaler //attached signaler, let people attach signalers that get activated if the user's transaction limit is achieved.
var/signaler_threshold = 0 //signaler threshold amount
var/amount_deposited = 0 //keep track of the amount deposited over time so you can pay multiple times to reach the signaler threshold
var/force_fee = 0 //replaces the "pay whatever" functionality with a set amount when non-zero.
/obj/machinery/paystand/attackby(obj/item/W, mob/user, params)
if(istype(W, /obj/item/card/id))
if(W == my_card)
var/list/items = list(
"Rename" = image(icon = 'icons/obj/economy.dmi', icon_state = "name"),
"Set the fee" = image(icon = 'icons/obj/economy.dmi', icon_state = "fee")
)
var/choice = show_radial_menu(user, src, items, null, require_near = TRUE, tooltips = TRUE)
if(choice == "Rename")
var/rename_msg = tgui_input_text(user, "Rename the Paystand", "Paystand Name", name)
if(!rename_msg || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
return
name = rename_msg
return
else if(choice == "Set the fee")
var/force_fee_input = tgui_input_number(user, "Set the fee", "Fee", max_value = 10000)
if(isnull(force_fee_input) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
return
force_fee = round(force_fee_input)
return
locked = !locked
to_chat(user, span_notice("You [src.locked ? "lock" : "unlock"] the paystand, protecting the bolts from [anchored ? "loosening" : "tightening"]."))
return
if(!my_card)
var/obj/item/card/id/new_card = W
if(!new_card.registered_account)
return
var/msg = tgui_input_text(user, "Name of pay stand", "Paystand Naming", "Paystand (owned by [new_card.registered_account.account_holder])")
if(!msg)
return
name = msg
desc = "Owned by [new_card.registered_account.account_holder], pays directly into [user.p_their()] account."
my_card = new_card
to_chat(user, "You link the stand to your account.")
return
var/obj/item/card/id/pay_card = W
if(pay_card.registered_account)
if(!pay_card.registered_account.account_job)//Departmental budget cards like cargo's fall under this
to_chat(user, span_warning("ERROR: Personal use of department budgets is not authorized."))
return
var/credit_amount = 0
if(!force_fee)
credit_amount = tgui_input_number(user, "How much would you like to deposit?", "Money Deposit")
if(isnull(credit_amount))
return
credit_amount = round(credit_amount)
else
credit_amount = force_fee
if(!user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
return
if(credit_amount < 1)
to_chat(user, span_warning("ERROR: Invalid amount designated."))
return
if(pay_card.registered_account.adjust_money(-credit_amount))
purchase(pay_card.registered_account.account_holder, credit_amount)
say("Thank you for your patronage, [pay_card.registered_account.account_holder]!")
playsound(src, 'sound/effects/cashregister.ogg', 20, TRUE)
return
else
to_chat(user, span_warning("ERROR: Account has insufficient funds to make transaction."))
return
else
to_chat(user, span_warning("ERROR: No bank account assigned to identification card."))
return
if(istype(W, /obj/item/holochip))
var/obj/item/holochip/H = W
var/cashmoney = round(tgui_input_number(user, "How much would you like to deposit?", "Money Deposit"))
if(isnull(cashmoney))
return
cashmoney = round(cashmoney)
if(H.spend(cashmoney, FALSE))
purchase(user, cashmoney)
to_chat(user, "Thanks for purchasing! The vendor has been informed.")
return
else
to_chat(user, span_warning("ERROR: Insufficient funds to make transaction."))
return
if(istype(W, /obj/item/stack/spacecash))
to_chat(user, "What is this, the 2000s? We only take card here.")
return
if(istype(W, /obj/item/coin))
to_chat(user, "What is this, the 1800s? We only take card here.")
return
if(istype(W, /obj/item/assembly/signaler))
var/obj/item/assembly/signaler/S = W
if(S.secured)
to_chat(user, span_warning("The signaler needs to be in attachable mode to add it to the paystand!"))
return
if(!my_card)
to_chat(user, span_warning("ERROR: No identification card has been assigned to this paystand yet!"))
return
if(!isnull(signaler))
to_chat(user, span_warning("A signaler is already attached to this unit!"))
return
var/cash_limit = tgui_input_number(user, "Enter the minimum amount of cash needed to deposit before the signaler is activated.", "Signaler Activation Threshold", 1, min_value = 1)
if(isnull(cash_limit))
return
cash_limit = round(cash_limit)
S.forceMove(src)
signaler = S
signaler_threshold = cash_limit
to_chat(user, "You attach the signaler to the paystand.")
desc += " A signaler appears to be attached to the scanner."
if(default_deconstruction_screwdriver(user, "card_scanner", "card_scanner", W))
return
else if(default_pry_open(W))
return
else if(default_unfasten_wrench(user, W))
return
else if(default_deconstruction_crowbar(W))
return
else
return ..()
/obj/machinery/paystand/proc/purchase(buyer, price)
my_card.registered_account.adjust_money(price)
my_card.registered_account.bank_card_talk("Purchase made at your vendor by [buyer] for [price] credits.")
amount_deposited = amount_deposited + price
if(signaler && amount_deposited >= signaler_threshold)
signaler.signal()
amount_deposited = 0
/obj/machinery/paystand/default_unfasten_wrench(mob/user, obj/item/I, time = 20)
if(locked)
to_chat(user, span_warning("The bolts on this paystand are currently covered!"))
return FALSE
. = ..()
/obj/machinery/paystand/examine(mob/user)
. = ..()
if(force_fee)
. += span_warning("This paystand forces a payment of <b>[force_fee]</b> credit\s per swipe instead of a variable amount.")
if(user.get_active_held_item() == my_card)
. += span_notice("Paystands can be edited through swiping your card.")
@@ -572,14 +572,6 @@
category = list ("Research Machinery")
departmental_flags = DEPARTMENTAL_FLAG_CARGO
/datum/design/board/paystand
name = "Machine Design (Pay Stand)"
desc = "The circuit board for a paystand."
id = "paystand"
build_path = /obj/item/circuitboard/machine/paystand
category = list ("Misc. Machinery")
/datum/design/board/fat_sucker
name = "Machine Design (Lipid Extractor)"
desc = "The circuit board for a lipid extractor."
@@ -35,7 +35,6 @@
"micro_mani",
"oven_tray",
"packagewrap",
"paystand",
"plasmaglass",
"plasmareinforcedglass",
"plasteel",
+66 -59
View File
@@ -1128,7 +1128,7 @@ GLOBAL_LIST_EMPTY(vending_products)
light_mask = "custom-light-mask"
refill_canister = /obj/item/vending_refill/custom
/// where the money is sent
var/datum/bank_account/private_a
var/datum/bank_account/linked_account
/// max number of items that the custom vendor can hold
var/max_loaded_items = 20
/// Base64 cache of custom icons.
@@ -1139,9 +1139,9 @@ GLOBAL_LIST_EMPTY(vending_products)
. = FALSE
if(!isliving(user))
return FALSE
var/mob/living/L = user
var/obj/item/card/id/C = L.get_idcard(FALSE)
if(C?.registered_account && C.registered_account == private_a)
var/mob/living/living_user = user
var/obj/item/card/id/id_card = living_user.get_idcard(FALSE)
if(id_card?.registered_account && id_card.registered_account == linked_account)
return TRUE
/obj/machinery/vending/custom/canLoadItem(obj/item/I, mob/user)
@@ -1158,6 +1158,12 @@ GLOBAL_LIST_EMPTY(vending_products)
if(I.custom_price)
return TRUE
/obj/machinery/vending/custom/ui_interact(mob/user)
if(!linked_account)
balloon_alert(user, "no registered owner")
return FALSE
return ..()
/obj/machinery/vending/custom/ui_data(mob/user)
. = ..()
.["access"] = compartmentLoadAccessCheck(user)
@@ -1191,68 +1197,18 @@ GLOBAL_LIST_EMPTY(vending_products)
return
switch(action)
if("dispense")
. = TRUE
if(!vend_ready)
return
var/N = params["item"]
var/obj/item/S
vend_ready = FALSE
var/obj/item/card/id/C
if(isliving(usr))
var/mob/living/L = usr
C = L.get_idcard(TRUE)
if(!C)
say("No card found.")
flick(icon_deny,src)
vend_ready = TRUE
return
else if (!C.registered_account)
say("No account found.")
flick(icon_deny,src)
vend_ready = TRUE
return
var/datum/bank_account/account = C.registered_account
for(var/obj/O in contents)
if(format_text(O.name) == N)
S = O
break
if(S)
if(compartmentLoadAccessCheck(usr))
vending_machine_input[N] = max(vending_machine_input[N] - 1, 0)
S.forceMove(drop_location())
loaded_items--
use_power(5)
vend_ready = TRUE
updateUsrDialog()
return
if(account.has_money(S.custom_price))
account.adjust_money(-S.custom_price)
var/datum/bank_account/owner = private_a
if(owner)
owner.adjust_money(S.custom_price)
SSblackbox.record_feedback("amount", "vending_spent", S.custom_price)
log_econ("[S.custom_price] credits were spent on [src] buying a [S] by [owner.account_holder], owned by [private_a.account_holder].")
vending_machine_input[N] = max(vending_machine_input[N] - 1, 0)
S.forceMove(drop_location())
loaded_items--
use_power(5)
if(last_shopper != REF(usr) || purchase_message_cooldown < world.time)
say("Thank you for buying local and purchasing [S]!")
purchase_message_cooldown = world.time + 5 SECONDS
last_shopper = REF(usr)
vend_ready = TRUE
updateUsrDialog()
return
else
say("You do not possess the funds to purchase this.")
vend_act(usr, params["item"])
vend_ready = TRUE
updateUsrDialog()
return TRUE
/obj/machinery/vending/custom/attackby(obj/item/I, mob/user, params)
if(!private_a && isliving(user))
if(!linked_account && isliving(user))
var/mob/living/L = user
var/obj/item/card/id/C = L.get_idcard(TRUE)
if(C?.registered_account)
private_a = C.registered_account
linked_account = C.registered_account
say("\The [src] has been linked to [C].")
if(compartmentLoadAccessCheck(user))
@@ -1277,6 +1233,57 @@ GLOBAL_LIST_EMPTY(vending_products)
explosion(src, devastation_range = -1, light_impact_range = 3)
return ..()
/**
* Vends an item to the user. Handles all the logic:
* Updating stock, account transactions, alerting users.
* @return -- TRUE if a valid condition was met, FALSE otherwise.
*/
/obj/machinery/vending/custom/proc/vend_act(mob/living/user, choice)
if(!vend_ready)
return
var/obj/item/dispensed_item
var/obj/item/card/id/id_card = user.get_idcard(TRUE)
vend_ready = FALSE
if(!id_card || !id_card.registered_account || !id_card.registered_account.account_job)
balloon_alert(usr, "no card found")
flick(icon_deny, src)
return TRUE
var/datum/bank_account/payee = id_card.registered_account
for(var/obj/stock in contents)
if(format_text(stock.name) == choice)
dispensed_item = stock
break
if(!dispensed_item)
return FALSE
/// Charges the user if its not the owner
if(!compartmentLoadAccessCheck(user))
if(!payee.has_money(dispensed_item.custom_price))
balloon_alert(user, "insufficient funds")
return TRUE
/// Make the transaction
payee.adjust_money(-dispensed_item.custom_price)
linked_account.adjust_money(dispensed_item.custom_price)
linked_account.bank_card_talk("[payee.account_holder] made a [dispensed_item.custom_price] \
cr purchase at your custom vendor.")
/// Log the transaction
SSblackbox.record_feedback("amount", "vending_spent", dispensed_item.custom_price)
log_econ("[dispensed_item.custom_price] credits were spent on [src] buying a \
[dispensed_item] by [payee.account_holder], owned by [linked_account.account_holder].")
/// Make an alert
if(last_shopper != REF(usr) || purchase_message_cooldown < world.time)
say("Thank you for your patronage [user]!")
purchase_message_cooldown = world.time + 5 SECONDS
last_shopper = REF(usr)
/// Remove the item
loaded_items--
use_power(5)
vending_machine_input[choice] = max(vending_machine_input[choice] - 1, 0)
if(user.CanReach(src) && user.put_in_hands(dispensed_item))
to_chat(user, span_notice("You take [dispensed_item.name] out of the slot."))
else
to_chat(user, span_warning("[capitalize(dispensed_item.name)] falls onto the floor!"))
return TRUE
/obj/machinery/vending/custom/unbreakable
name = "Indestructible Vendor"
resistance_flags = INDESTRUCTIBLE