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.
This commit is contained in:
ArcaneMusic
2024-03-06 19:15:49 -05:00
committed by GitHub
parent dc7d3e09f6
commit d7cf8309c5
20 changed files with 257 additions and 48 deletions
+6
View File
@@ -33,3 +33,9 @@
unit_name = "data disk"
export_types = list(/obj/item/computer_disk)
include_subtypes = TRUE
/datum/export/refill_canister
cost = CARGO_CRATE_VALUE * 0.5 //If someone want to make this worth more as it empties, go ahead
unit_name = "vending refill canister"
message = "Thank you for restocking the station!"
export_types = list(/obj/item/vending_refill)
@@ -150,6 +150,7 @@
icon_state = "uplink"
//The original black market uplink
accessible_markets = list(/datum/market/blackmarket)
custom_premium_price = PAYCHECK_CREW * 2.5
/datum/crafting_recipe/blackmarket_uplink
+10 -10
View File
@@ -4,7 +4,7 @@
/datum/supply_pack/vending/bartending
name = "Booze-o-mat and Coffee Supply Crate"
desc = "Bring on the booze and coffee vending machine refills."
cost = CARGO_CRATE_VALUE * 4
cost = CARGO_CRATE_VALUE * 2
contains = list(/obj/item/vending_refill/boozeomat,
/obj/item/vending_refill/coffee,
)
@@ -14,7 +14,7 @@
name = "Cigarette Supply Crate"
desc = "Don't believe the reports - smoke today! Contains a \
cigarette vending machine refill."
cost = CARGO_CRATE_VALUE * 3
cost = CARGO_CRATE_VALUE * 2
contains = list(/obj/item/vending_refill/cigarette)
crate_name = "cigarette supply crate"
crate_type = /obj/structure/closet/crate
@@ -62,7 +62,7 @@
/datum/supply_pack/vending/imported
name = "Imported Vending Machines"
desc = "Vending machines famous in other parts of the galaxy."
cost = CARGO_CRATE_VALUE * 8
cost = CARGO_CRATE_VALUE * 5
contains = list(/obj/item/vending_refill/sustenance,
/obj/item/vending_refill/robotics,
/obj/item/vending_refill/sovietsoda,
@@ -74,7 +74,7 @@
name = "Medical Vending Crate"
desc = "Contains one NanoMed Plus refill, one NanoDrug Plus refill, \
and one wall-mounted NanoMed refill."
cost = CARGO_CRATE_VALUE * 5
cost = CARGO_CRATE_VALUE * 3.5
contains = list(/obj/item/vending_refill/medical,
/obj/item/vending_refill/drugs,
/obj/item/vending_refill/wallmed,
@@ -85,7 +85,7 @@
name = "PTech Supply Crate"
desc = "Not enough cartridges after half the crew lost their PDA \
to explosions? This may fix it."
cost = CARGO_CRATE_VALUE * 3
cost = CARGO_CRATE_VALUE * 2.5
contains = list(/obj/item/vending_refill/cart)
crate_name = "\improper PTech supply crate"
@@ -103,7 +103,7 @@
name = "Snack Supply Crate"
desc = "One vending machine refill of cavity-bringin' goodness! \
The number one dentist recommended order!"
cost = CARGO_CRATE_VALUE * 3
cost = CARGO_CRATE_VALUE * 2
contains = list(/obj/item/vending_refill/snack)
crate_name = "snacks supply crate"
@@ -111,14 +111,14 @@
name = "Softdrinks Supply Crate"
desc = "Got whacked by a toolbox, but you still have those pesky teeth? \
Get rid of those pearly whites with this soda machine refill, today!"
cost = CARGO_CRATE_VALUE * 3
cost = CARGO_CRATE_VALUE * 2
contains = list(/obj/item/vending_refill/cola)
crate_name = "soft drinks supply crate"
/datum/supply_pack/vending/vendomat
name = "Part-Mart & YouTool Supply Crate"
desc = "More tools for your IED testing facility."
cost = CARGO_CRATE_VALUE * 2
cost = CARGO_CRATE_VALUE * 3
contains = list(/obj/item/vending_refill/assist,
/obj/item/vending_refill/youtool,
)
@@ -138,7 +138,7 @@
name = "Autodrobe Supply Crate"
desc = "Autodrobe missing your favorite dress? Solve that issue today \
with this autodrobe refill."
cost = CARGO_CRATE_VALUE * 3
cost = CARGO_CRATE_VALUE * 2
contains = list(/obj/item/vending_refill/autodrobe)
crate_name = "autodrobe supply crate"
@@ -200,7 +200,7 @@
name = "Science Wardrobe Supply Crate"
desc = "This crate contains refills for the SciDrobe, \
GeneDrobe, and RoboDrobe."
cost = CARGO_CRATE_VALUE * 3
cost = CARGO_CRATE_VALUE * 4.5
contains = list(/obj/item/vending_refill/wardrobe/robo_wardrobe,
/obj/item/vending_refill/wardrobe/gene_wardrobe,
/obj/item/vending_refill/wardrobe/science_wardrobe,
+17
View File
@@ -2628,3 +2628,20 @@ GLOBAL_LIST_EMPTY(fire_appearances)
end_look_down()
else
look_down()
/**
* Totals the physical cash on the mob and returns the total.
*/
/mob/living/verb/tally_physical_credits()
//Here is all the possible non-ID payment methods.
var/list/counted_money = list()
var/physical_cash_total = 0
for(var/obj/item/credit as anything in typecache_filter_list(get_all_contents(), GLOB.allowed_money)) //Coins, cash, and credits.
physical_cash_total += credit.get_item_credit_value()
counted_money += credit
if(is_type_in_typecache(pulling, GLOB.allowed_money)) //Coins(Pulled).
var/obj/item/counted_credit = pulling
physical_cash_total += counted_credit.get_item_credit_value()
counted_money += counted_credit
return round(physical_cash_total)
@@ -98,6 +98,7 @@
starting_programs = list(
/datum/computer_file/program/shipping,
/datum/computer_file/program/budgetorders,
/datum/computer_file/program/restock_tracker,
)
/**
@@ -123,6 +124,6 @@
/datum/computer_file/program/alarm_monitor,
/datum/computer_file/program/atmosscan,
/datum/computer_file/program/supermatter_monitor,
)
@@ -114,6 +114,7 @@
/datum/computer_file/program/robocontrol,
/datum/computer_file/program/budgetorders,
/datum/computer_file/program/shipping,
/datum/computer_file/program/restock_tracker,
)
/**
@@ -264,6 +265,7 @@
/datum/computer_file/program/shipping,
/datum/computer_file/program/budgetorders,
/datum/computer_file/program/robocontrol,
/datum/computer_file/program/restock_tracker,
)
/obj/item/modular_computer/pda/shaftminer
@@ -0,0 +1,31 @@
/datum/computer_file/program/restock_tracker
filename = "restockapp"
filedesc = "NT Restock Tracker"
downloader_category = PROGRAM_CATEGORY_SUPPLY
program_open_overlay = "restock"
extended_desc = "Nanotrasen IoT network listing all the vending machines found on station, and how well stocked they are each. Profitable!"
program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
can_run_on_flags = PROGRAM_LAPTOP | PROGRAM_PDA
size = 4
program_icon = "cash-register"
tgui_id = "NtosRestock"
/datum/computer_file/program/restock_tracker/ui_data()
var/list/data = list()
var/list/vending_list = list()
var/id_increment = 1
for(var/obj/machinery/vending/vendor as anything in GLOB.vending_machines_to_restock)
var/stock = vendor.total_loaded_stock()
var/max_stock = vendor.total_max_stock()
if((max_stock == 0 || (stock >= max_stock)) && vendor.credits_contained == 0)
continue
vending_list += list(list(
"name" = vendor.name,
"location" = get_area_name(vendor),
"credits" = vendor.credits_contained,
"percentage" = (stock / max_stock) * 100,
"id" = id_increment,
))
id_increment++
data["vending_list"] = vending_list
return data
+61 -26
View File
@@ -14,8 +14,12 @@
premium = list()
*/
/// List of vending machines that players can restock, so only vending machines that are on station or don't have a unique condition.
GLOBAL_LIST_EMPTY(vending_machines_to_restock)
/// Maximum amount of items in a storage bag that we're transferring items to the vendor from.
#define MAX_VENDING_INPUT_AMOUNT 30
#define CREDITS_DUMP_THRESHOLD 50
/**
* # vending record datum
*
@@ -178,6 +182,8 @@
var/displayed_currency_name = " cr"
///Whether our age check is currently functional
var/age_restrictions = TRUE
/// How many credits does this vending machine have? 20% of all sales go to this pool, and are given freely when the machine is restocked, or successfully tilted. Lost on deconstruction.
var/credits_contained = 0
/**
* Is this item on station or not
*
@@ -256,6 +262,7 @@
onstation = FALSE
if(circuit)
circuit.onstation = onstation //sync up the circuit so the pricing schema is carried over if it's reconstructed.
else if(HAS_TRAIT(SSstation, STATION_TRAIT_VENDING_SHORTAGE))
for (var/datum/data/vending_product/product_record as anything in product_records + coin_records + hidden_records)
/**
@@ -264,16 +271,22 @@
*/
var/max_amount = rand(CEILING(product_record.amount * 0.5, 1), product_record.amount)
product_record.amount = rand(0, max_amount)
credits_contained += rand(0, 1) //randomly add a few credits to the machine to make it look like it's been used, proportional to the amount missing.
if(tiltable && prob(6)) // 1 in 17 chance to start tilted (as an additional hint to the station trait behind it)
INVOKE_ASYNC(src, PROC_REF(tilt), loc)
credits_contained = 0 // If it's tilted, it's been looted, so no credits for you.
else if(circuit && (circuit.onstation != onstation)) //check if they're not the same to minimize the amount of edited values.
onstation = circuit.onstation //if it was constructed outside mapload, sync the vendor up with the circuit's var so you can't bypass price requirements by moving / reconstructing it off station.
if(onstation && !onstation_override)
AddComponent(/datum/component/payment, 0, SSeconomy.get_dep_account(payment_department), PAYMENT_VENDING)
GLOB.vending_machines_to_restock += src //We need to keep track of the final onstation vending machines so we can keep them restocked.
/obj/machinery/vending/Destroy()
QDEL_NULL(wires)
QDEL_NULL(coin)
QDEL_NULL(bill)
QDEL_NULL(sec_radio)
GLOB.vending_machines_to_restock -= src
return ..()
/obj/machinery/vending/can_speak()
@@ -612,6 +625,24 @@
else //no category found - dump it into standard stock
products[record.product_path] = record.amount
/**
* Returns the total amount of items in the vending machine based on the product records and premium records, but not contraband
*/
/obj/machinery/vending/proc/total_loaded_stock()
var/total = 0
for(var/datum/data/vending_product/record as anything in product_records + coin_records)
total += record.amount
return total
/**
* Returns the total amount of items in the vending machine based on the product records and premium records, but not contraband
*/
/obj/machinery/vending/proc/total_max_stock()
var/total_max = 0
for(var/datum/data/vending_product/record as anything in product_records + coin_records)
total_max += record.max_amount
return total_max
/obj/machinery/vending/crowbar_act(mob/living/user, obj/item/attack_item)
if(!component_parts)
return FALSE
@@ -656,7 +687,11 @@
// instantiate canister if needed
var/transferred = restock(canister)
if(transferred)
to_chat(user, span_notice("You loaded [transferred] items in [src]."))
to_chat(user, span_notice("You loaded [transferred] items in [src][credits_contained > 0 ? ", and are rewarded [credits_contained] credits." : "."]"))
var/datum/bank_account/cargo_account = SSeconomy.get_dep_account(ACCOUNT_CAR)
cargo_account.adjust_money(round(credits_contained * 0.5), "Vending: Restock")
var/obj/item/holochip/payday = new(src, credits_contained)
try_put_in_hand(payday, user)
else
to_chat(user, span_warning("There's nothing to restock!"))
return
@@ -708,7 +743,7 @@
* freebies - number of free items to vend
*/
/obj/machinery/vending/proc/freebie(freebies)
visible_message(span_notice("[src] yields [freebies > 1 ? "several free goodies" : "a free goody"]!"))
visible_message(span_notice("[src] yields [freebies > 1 ? "several free goodies" : "a free goody"][credits_contained > 0 ? " and some credits" : ""]!"))
for(var/i in 1 to freebies)
playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3)
@@ -728,6 +763,7 @@
returned_obj_to_dump.forceMove(get_turf(src))
record.amount--
break
deploy_credits()
/**
* Tilts ontop of the atom supplied, if crit is true some extra shit can happen. See [fall_and_crush] for return values.
@@ -1208,13 +1244,15 @@
/obj/machinery/vending/ui_data(mob/user)
. = list()
var/obj/item/card/id/card_used
var/held_cash = 0
if(isliving(user))
var/mob/living/living_user = user
card_used = living_user.get_idcard(TRUE)
held_cash = living_user.tally_physical_credits()
if(card_used?.registered_account)
.["user"] = list()
.["user"]["name"] = card_used.registered_account.account_holder
.["user"]["cash"] = fetch_balance_to_use(card_used)
.["user"]["cash"] = fetch_balance_to_use(card_used) + held_cash
if(card_used.registered_account.account_job)
.["user"]["job"] = card_used.registered_account.account_job.title
.["user"]["department"] = card_used.registered_account.account_job.paycheck_department
@@ -1335,25 +1373,12 @@
vend_ready = TRUE
return
if(onstation)
// Here we do additional handing ahead of the payment component's logic, such as age restrictions and additional logging
var/obj/item/card/id/card_used
var/mob/living/living_user
if(isliving(usr))
var/mob/living/living_user = usr
living_user = usr
card_used = living_user.get_idcard(TRUE)
if(!card_used)
speak("No card found.")
flick(icon_deny,src)
vend_ready = TRUE
return
else if (!card_used.registered_account)
speak("No account found.")
flick(icon_deny,src)
vend_ready = TRUE
return
else if(!card_used.registered_account.account_job)
speak("Departmental accounts have been blacklisted from personal expenses due to embezzlement.")
flick(icon_deny, src)
vend_ready = TRUE
return
else if(age_restrictions && item_record.age_restricted && (!card_used.registered_age || card_used.registered_age < AGE_MINOR))
speak("You are not of legal age to purchase [item_record.name].")
if(!(usr in GLOB.narcd_underages))
@@ -1367,7 +1392,7 @@
vend_ready = TRUE
return
if(!proceed_payment(card_used, item_record, price_to_use))
if(!proceed_payment(card_used, living_user, item_record, price_to_use))
return
if(last_shopper != REF(usr) || purchase_message_cooldown < world.time)
@@ -1414,11 +1439,12 @@
/**
* Handles payment processing: discounts, logging, balance change etc.
* arguments:
* paying_id_card - the id card that will be billed for the product
* product_to_vend - the product record of the item we're trying to vend
* price_to_use - price of the item we're trying to vend
* paying_id_card - the id card that will be billed for the product.
* mob_paying - the mob that is trying to purchase the item.
* product_to_vend - the product record of the item we're trying to vend.
* price_to_use - price of the item we're trying to vend.
*/
/obj/machinery/vending/proc/proceed_payment(obj/item/card/id/paying_id_card, datum/data/vending_product/product_to_vend, price_to_use)
/obj/machinery/vending/proc/proceed_payment(obj/item/card/id/paying_id_card, mob/living/mob_paying, datum/data/vending_product/product_to_vend, price_to_use)
var/datum/bank_account/account = paying_id_card.registered_account
if(account.account_job && account.account_job.paycheck_department == payment_department)
price_to_use = max(round(price_to_use * DEPARTMENT_DISCOUNT), 1) //No longer free, but signifigantly cheaper.
@@ -1426,7 +1452,7 @@
price_to_use = product_to_vend.custom_premium_price ? product_to_vend.custom_premium_price : extra_price
if(LAZYLEN(product_to_vend.returned_products))
price_to_use = 0 //returned items are free
if(price_to_use && !account.adjust_money(-price_to_use, "Vending: [product_to_vend.name]"))
if(price_to_use && (attempt_charge(src, mob_paying, price_to_use) & COMPONENT_OBJ_CANCEL_CHARGE))
speak("You do not possess the funds to purchase [product_to_vend.name].")
flick(icon_deny,src)
vend_ready = TRUE
@@ -1434,10 +1460,10 @@
//actual payment here
var/datum/bank_account/paying_id_account = SSeconomy.get_dep_account(payment_department)
if(paying_id_account)
paying_id_account.adjust_money(price_to_use)
SSblackbox.record_feedback("amount", "vending_spent", price_to_use)
SSeconomy.track_purchase(account, price_to_use, name)
log_econ("[price_to_use] credits were inserted into [src] by [account.account_holder] to buy [product_to_vend].")
credits_contained += round(price_to_use * 0.2)
return TRUE
/obj/machinery/vending/process(seconds_per_tick)
@@ -1576,6 +1602,15 @@
tilt(fatty=hit_atom)
return ..()
/** Drop credits when the vendor is attacked.*/
/obj/machinery/vending/proc/deploy_credits()
if(credits_contained <= 0)
return
var/credits_to_remove = min(CREDITS_DUMP_THRESHOLD, round(credits_contained))
var/obj/item/holochip/holochip = new(loc, credits_to_remove)
credits_contained = max(0, credits_contained - credits_to_remove)
SSblackbox.record_feedback("amount", "vending machine looted", holochip.credits)
/obj/machinery/vending/custom
name = "Custom Vendor"
icon_state = "custom"
+1
View File
@@ -21,6 +21,7 @@
/obj/item/assembly/timer = 2,
/obj/item/assembly/voice = 2,
/obj/item/stock_parts/cell/high = 1,
/obj/item/market_uplink/blackmarket = 1,
)
premium = list(
/obj/item/assembly/igniter/condenser = 2,
+1 -1
View File
@@ -48,7 +48,7 @@
return
return ..()
/obj/machinery/vending/sustenance/labor_camp/proceed_payment(obj/item/card/id/paying_id_card, datum/data/vending_product/product_to_vend, price_to_use)
/obj/machinery/vending/sustenance/labor_camp/proceed_payment(obj/item/card/id/paying_id_card, mob/living/mob_paying, datum/data/vending_product/product_to_vend, price_to_use)
if(!istype(paying_id_card, /obj/item/card/id/advanced/prisoner))
speak("I don't take bribes! Pay with labor points!")
return FALSE