Files
Bubberstation/code/datums/components/payment.dm
SkyratBot 6221130798 [MIRROR] Arcargo: Vendor Cargo and Vending Machine Update (#26782)
* Arcargo: Vendor Cargo and Vending Machine Update (#81582)

Another one.

## About The Pull Request
This pull request originally had a design doc that @ Fikou and I worked
on, but that was never really polished up for publishing quality so I'll
forgo it for now and be as descript as possible here.

### Core changes
- This pull request adds a new NTOS app to the game, the restock
tracker. The restock tracker shows a comprehensive list of vending
machines across the station, as long as there is a need for that vending
machine to get restocked.
- This has also been pre-installed into the cargo data disks.
(`/obj/item/computer_disk/quartermaster`)
- Vending machines now store a total of 20% of the cost of any purchase
made within themselves into a small pool of cash. This only applies to
premium and normal purchases, not to contraband, as they're technically
not sanctioned by the company.
- The restock tracker app will also track which vending machines have
the most credits stored internally inside them.
- By refilling a vending machine, the stored credits within are paid out
to any crewmember who goes and restocks the station, while also paying
out *half that amount to the cargo budget*, serving as a basic but
otherwise easy tertiary money making method on the same level of
complexity as doing bounties, with the added benefit of actually helping
to assist the station for jobs like... assistant.

![image](https://github.com/tgstation/tgstation/assets/41715314/59cee2d9-7e60-4733-8a76-d88fe5b8c3f2)

### Break Stuff
- Anyway, when you try and smash a vending machine open with a melee
weapon of choice, it can now pay out 50 credits at a time as a way to
make money at zero risk to yourself.
- ~~Except for the horrible risk to yourself.~~

![image](https://github.com/tgstation/tgstation/assets/41715314/23208bf0-8484-40b9-b753-0ffdb57d770f)

### Cargo Specific Changes
- Restock units may now be sold for a small profit as well, to
incentivize cargo to keep the station stocked further.
- The `STATION_TRAIT_VENDING_SHORTAGE` trait will now add a small amount
of existing credits into the vending machines on station, to incentivize
cargo to fix the issue during the round and not just push for an early
shuttle call. Or, more accurately, provide the crew with a money making
scheme to engage better with the station trait as it stands.

### This also refactors behavior on vending machines
- This pull request also finally changes it so that vending machines now
use the payment component, which as a consequence allows for the
following improvements:
* Vending machines may now pull from physical credits on your person,
not just requiring you to have money on your ID card.
* Vending machines may also use credits being pulled by the player
interacting with the vending machine, allowing for handless mobs to be
able to purchase items from a vending machine.
* Finally makes the "use-for-everything buying things component" used by
the most utilized component of the in-game economy, to reduce the
quantity of unique implementations of purchasing things in the code.
- Existing vending specific checks are retained on before handing off
behavior to the payment component, for behavior such as purchasing
cigarettes/alcohol under the age of 18/21.

Notes:
- Vending machines will lose their internal credits stored when
deconstructed, as a security measure.
- Vending machines will now show the total amount of credits that a mob
has on their person, combining physical credits as well as credits held
in their ID card to accurately portray their total wealth across the mob
in question.

## Why It's Good For The Game

First off, this is largely an excuse to move vending machine behavior
over to the payment component for the purposes to less code copy-paste,
and to try and make the implementation more wide-spread.

Second, this implements a new tertiary economy method to the game, in
the same design space as bounties, which serve as common methods of
making money without necessarily being specific to their job in
question, with the primary goal of providing small amounts of work to
the crew and a basic interaction with the economy system.

Additionally, it gives cargo more things they can do to assist the
station, and a way to know which parts of the station need support as a
result.
It improves the interaction between the vending shortage station trait
as well, making it a challenge with depth as opposed to a more
oppressive round change that players would rather reroll the game over.

Additionally, this makes a few price tweaks to vending restock modules
as well to help incentivize buying some of the more minor restock kits,
and a few select bumps on restocks that cover wide enough territory to
necessitate fewer restocks.

* Arcargo: Vendor Cargo and Vending Machine Update

---------

Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
2024-03-06 22:29:57 -05:00

182 lines
7.3 KiB
Plaintext

/**
* Handles simple payment operations where the cost of the object in question doesn't change.
*
* What this is useful for:
* Basic forms of vending.
* Objects that can drain the owner's money linearly.
* What this is not useful for:
* Things where the seller may want to fluxuate the price of the object.
* Improving standardizing every form of payment handing, as some custom handling is specific to that object.
**/
/datum/component/payment
dupe_mode = COMPONENT_DUPE_UNIQUE ///NO OVERRIDING TO CHEESE BOUNTIES
///Standardized of operation.
var/cost = 10
///Flavor style for handling cash (Friendly? Hostile? etc.)
var/transaction_style = "Clinical"
///Who's getting paid?
var/datum/bank_account/target_acc
///Does this payment component respect same-department-discount?
var/department_discount = FALSE
/datum/component/payment/Initialize(_cost, _target, _style)
target_acc = _target
if(!target_acc)
target_acc = SSeconomy.get_dep_account(ACCOUNT_CIV)
cost = _cost
transaction_style = _style
/datum/component/payment/RegisterWithParent()
RegisterSignal(parent, COMSIG_OBJ_ATTEMPT_CHARGE, PROC_REF(attempt_charge))
RegisterSignal(parent, COMSIG_OBJ_ATTEMPT_CHARGE_CHANGE, PROC_REF(change_cost))
/datum/component/payment/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_OBJ_ATTEMPT_CHARGE, COMSIG_OBJ_ATTEMPT_CHARGE_CHANGE))
/datum/component/payment/proc/attempt_charge(datum/source, atom/movable/target, extra_fees = 0)
SIGNAL_HANDLER
if(!cost && !extra_fees) //In case a free variant of anything is made it'll skip charging anyone.
return
var/total_cost = cost + extra_fees
if(!ismob(target))
return COMPONENT_OBJ_CANCEL_CHARGE
var/mob/living/user = target
if(issilicon(user) || isdrone(user) || isAdminGhostAI(user)) //They have evolved beyond the need for mere credits
return
var/obj/item/card/id/card
if(istype(user))
card = user.get_idcard(TRUE)
if(!card && istype(user.pulling, /obj/item/card/id))
card = user.pulling
if(handle_card(user, card, total_cost))
return //Only breaks here if the card can handle the cost of purchasing with someone's ID.
if(handle_cardless(user, total_cost)) //Here we attempt to handle the purchase physically, with held money first. Otherwise we default to below.
return
return COMPONENT_OBJ_CANCEL_CHARGE
/**
* Proc that changes the base cost of the interaction.
*
* * source: Datum source of the thing changing the cost.
* * new_cost: the int value of the attempted new_cost to replace the cost value.
*/
/datum/component/payment/proc/change_cost(datum/source, new_cost)
SIGNAL_HANDLER
if(!isnum(new_cost))
CRASH("change_cost called with variable new_cost as not a number.")
cost = new_cost
/**
* Attempts to charge the mob, user, an integer number of credits, total_cost, without the use of an ID card to directly draw upon.
*/
/datum/component/payment/proc/handle_cardless(mob/living/user, total_cost)
//Here is all the possible non-ID payment methods.
var/list/counted_money = list()
var/physical_cash_total = 0
for(var/obj/item/credit in typecache_filter_list(user.get_all_contents(), GLOB.allowed_money)) //Coins, cash, and credits.
if(physical_cash_total > total_cost)
break
physical_cash_total += credit.get_item_credit_value()
counted_money += credit
if(is_type_in_typecache(user.pulling, GLOB.allowed_money) && (physical_cash_total < total_cost)) //Coins(Pulled).
var/obj/item/counted_credit = user.pulling
physical_cash_total += counted_credit.get_item_credit_value()
counted_money += counted_credit
if(physical_cash_total < total_cost)
var/armless //Suggestions for those with no arms/simple animals.
if(!ishuman(user) && !isslime(user))
armless = TRUE
else
var/mob/living/carbon/human/harmless_armless = user
if(!harmless_armless.get_bodypart(BODY_ZONE_L_ARM) && !harmless_armless.get_bodypart(BODY_ZONE_R_ARM))
armless = TRUE
if(armless)
if(!user.pulling || !iscash(user.pulling) && !istype(user.pulling, /obj/item/card/id))
to_chat(user, span_notice("Try pulling a valid ID, space cash, holochip or coin while using \the [parent]!"))
return FALSE
return FALSE
if(physical_cash_total < total_cost)
to_chat(user, span_notice("Insufficient funds. Aborting."))
return FALSE
for(var/obj/cash_object in counted_money)
qdel(cash_object)
physical_cash_total -= total_cost
if(physical_cash_total > 0)
var/obj/item/holochip/holochange = new /obj/item/holochip(user.loc, physical_cash_total) //Change is made in holocredits exclusively.
holochange.name = "[holochange.credits] credit holochip"
if(ishuman(user))
var/mob/living/carbon/human/paying_customer = user
var/successfully_put_in_hands
ASYNC //Put_in_hands can sleep, we don't want that to block this proc.
successfully_put_in_hands = paying_customer.put_in_hands(holochange)
if(!successfully_put_in_hands)
user.pulling = holochange
else
user.pulling = holochange
log_econ("[total_cost] credits were spent on [parent] by [user].")
to_chat(user, span_notice("Purchase completed with held credits."))
playsound(user, 'sound/effects/cashregister.ogg', 20, TRUE)
return TRUE
/**
* Attempts to charge a mob, user, an integer number of credits, total_cost, directly from an ID card/bank account.
*/
/datum/component/payment/proc/handle_card(mob/living/user, obj/item/card/id/idcard, total_cost)
var/atom/movable/atom_parent = parent
if(!idcard)
if(transaction_style == PAYMENT_VENDING)
to_chat(user, span_warning("No card found."))
return FALSE
if(!idcard?.registered_account)
switch(transaction_style)
if(PAYMENT_FRIENDLY)
to_chat(user, span_warning("There's no account detected on your ID, how mysterious!"))
if(PAYMENT_ANGRY)
to_chat(user, span_warning("ARE YOU JOKING. YOU DON'T HAVE A BANK ACCOUNT ON YOUR ID YOU IDIOT."))
if(PAYMENT_CLINICAL)
to_chat(user, span_warning("ID Card lacks a bank account. Advancing."))
if(PAYMENT_VENDING)
to_chat(user, span_warning("No account found."))
return FALSE
if(!idcard.registered_account.account_job)
atom_parent.say("Departmental accounts have been blacklisted from personal expenses due to embezzlement.")
return FALSE
if(!(idcard.registered_account.has_money(total_cost)))
switch(transaction_style)
if(PAYMENT_FRIENDLY)
to_chat(user, span_warning("I'm so sorry... You don't seem to have enough money."))
if(PAYMENT_ANGRY)
to_chat(user, span_warning("YOU MORON. YOU ABSOLUTE BAFOON. YOU INSUFFERABLE TOOL. YOU ARE POOR."))
if(PAYMENT_CLINICAL)
to_chat(user, span_warning("ID Card lacks funds. Aborting."))
if(PAYMENT_VENDING)
to_chat(user, span_warning("You do not possess the funds to purchase that."))
atom_parent.balloon_alert(user, "needs [total_cost] credit\s!")
return FALSE
target_acc.transfer_money(idcard.registered_account, total_cost, "Nanotrasen: Usage of Corporate Machinery")
log_econ("[total_cost] credits were spent on [parent] by [user] via [idcard.registered_account.account_holder]'s card.")
idcard.registered_account.bank_card_talk("[total_cost] credits deducted from your account.")
playsound(src, 'sound/effects/cashregister.ogg', 20, TRUE)
SSeconomy.track_purchase(idcard.registered_account, total_cost, parent)
return TRUE
/**
* Attempts to remove the payment component, currently when the crew wins a revolution.
* * datum/source: source of the signal.
*/
/datum/component/payment/proc/clean_up(datum/source)
SIGNAL_HANDLER
target_acc = null
qdel(src)