Files
Bubberstation/code/modules/vending/_vending.dm
moo c55dbf97f4 Vending Machine Input Framework & Clothing Vendor Inputs (#43964)
About The Pull Request

I didn't like how the wardrobe replaced lockers but you couldn't clean up after yourself say once you switched to the nurse's outfit as a doctor. Now, anyone can put clothes in a wardrobe so long as that clothing is a vendable product of said wardrobe (No engineering jumpsuits in science vendor, etc.).

Building off the previous snack machine vendor, this lays the framework for ALL vendors to allow all sorts of items to be inputted into vendors, all you have to do is change canLoadItem(obj/item/I,mob/user) to TRUE for the items you want the vendor to accept! It also has an option to restrict loading by changing canload_access_list. NOTE: having any of the access permits input instead of all access is needed to input (important distinction!)

ECONOMY: This will make it so any clothes you put in becomes a sellable product. It does NOT make it free unless you can already access the vendor's contents for free.

Code improvement + minor QoL with minute balance implications. If you want to discuss how making it easier to clean up your unused clothes makes it more difficult for antags to sneak then I'm in trouble lol.
Why It's Good For The Game
Changelog

cl ExcessiveUseOfVending
tweak: Wardrobe Vendors will now accept clothing types they sell. Now you can clean up after getting that cool alternate uniform!
code: see PR #43964 on how to easily setup a vending machine to accept items!
/cl
2019-05-27 22:24:58 +12:00

641 lines
20 KiB
Plaintext

/*
* Vending machine types - Can be found under /code/modules/vending/
*/
/*
/obj/machinery/vending/[vendors name here] // --vending machine template :)
name = ""
desc = ""
icon = ''
icon_state = ""
products = list()
contraband = list()
premium = list()
IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY CANISTER CHARGES in vending_items.dm
*/
#define MAX_VENDING_INPUT_AMOUNT 30
/datum/data/vending_product
name = "generic"
var/product_path = null
var/amount = 0
var/max_amount = 0
var/custom_price
var/custom_premium_price
/obj/machinery/vending
name = "\improper Vendomat"
desc = "A generic vending machine."
icon = 'icons/obj/vending.dmi'
icon_state = "generic"
layer = BELOW_OBJ_LAYER
density = TRUE
verb_say = "beeps"
verb_ask = "beeps"
verb_exclaim = "beeps"
max_integrity = 300
integrity_failure = 100
armor = list("melee" = 20, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 50, "acid" = 70)
circuit = /obj/item/circuitboard/machine/vendor
payment_department = ACCOUNT_SRV
var/active = 1 //No sales pitches if off!
var/vend_ready = 1 //Are we ready to vend?? Is it time??
var/purchase_message_cooldown
var/last_shopper
// To be filled out at compile time
var/list/products = list() //For each, use the following pattern:
var/list/contraband = list() //list(/type/path = amount, /type/path2 = amount2)
var/list/premium = list() //No specified amount = only one in stock
var/product_slogans = "" //String of slogans separated by semicolons, optional
var/product_ads = "" //String of small ad messages in the vending screen - random chance
var/list/product_records = list()
var/list/hidden_records = list()
var/list/coin_records = list()
var/list/slogan_list = list()
var/list/small_ads = list() //Small ad messages in the vending screen - random chance of popping up whenever you open it
var/vend_reply //Thank you for shopping!
var/last_reply = 0
var/last_slogan = 0 //When did we last pitch?
var/slogan_delay = 6000 //How long until we can pitch again?
var/icon_vend //Icon_state when vending!
var/icon_deny //Icon_state when vending!
var/seconds_electrified = MACHINE_NOT_ELECTRIFIED //Shock customers like an airlock.
var/shoot_inventory = 0 //Fire items at customers! We're broken!
var/shoot_inventory_chance = 2
var/shut_up = 0 //Stop spouting those godawful pitches!
var/extended_inventory = 0 //can we access the hidden inventory?
var/scan_id = 1
var/obj/item/coin/coin
var/obj/item/stack/spacecash/bill
var/chef_price = 10
var/default_price = 25
var/extra_price = 50
var/onstation = TRUE //if it doesn't originate from off-station during mapload, everything is free
var/list/canload_access_list
var/list/vending_machine_input = list()
var/input_display_header = "Custom Compartment"
var/obj/item/vending_refill/refill_canister = null //The type of refill canisters used by this machine.
/obj/item/circuitboard
var/onstation = TRUE //if the circuit board originated from a vendor off station or not.
/obj/machinery/vending/Initialize(mapload)
var/build_inv = FALSE
if(!refill_canister)
circuit = null
build_inv = TRUE
. = ..()
wires = new /datum/wires/vending(src)
if(build_inv) //non-constructable vending machine
build_inventory(products, product_records)
build_inventory(contraband, hidden_records)
build_inventory(premium, coin_records)
slogan_list = splittext(product_slogans, ";")
// So not all machines speak at the exact same time.
// The first time this machine says something will be at slogantime + this random value,
// so if slogantime is 10 minutes, it will say it at somewhere between 10 and 20 minutes after the machine is crated.
last_slogan = world.time + rand(0, slogan_delay)
power_change()
if(mapload) //check if it was initially created off station during mapload.
if(!is_station_level(z))
onstation = FALSE
if(circuit)
circuit.onstation = onstation //sync up the circuit so the pricing schema is carried over if it's reconstructed.
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.
/obj/machinery/vending/Destroy()
QDEL_NULL(wires)
QDEL_NULL(coin)
QDEL_NULL(bill)
return ..()
/obj/machinery/vending/can_speak()
return !shut_up
/obj/machinery/vending/RefreshParts() //Better would be to make constructable child
if(!component_parts)
return
product_records = list()
hidden_records = list()
coin_records = list()
build_inventory(products, product_records, start_empty = TRUE)
build_inventory(contraband, hidden_records, start_empty = TRUE)
build_inventory(premium, coin_records, start_empty = TRUE)
for(var/obj/item/vending_refill/VR in component_parts)
restock(VR)
/obj/machinery/vending/deconstruct(disassembled = TRUE)
if(!refill_canister) //the non constructable vendors drop metal instead of a machine frame.
if(!(flags_1 & NODECONSTRUCT_1))
new /obj/item/stack/sheet/metal(loc, 3)
qdel(src)
else
..()
/obj/machinery/vending/obj_break(damage_flag)
if(!(stat & BROKEN) && !(flags_1 & NODECONSTRUCT_1))
stat |= BROKEN
icon_state = "[initial(icon_state)]-broken"
var/dump_amount = 0
var/found_anything = TRUE
while (found_anything)
found_anything = FALSE
for(var/record in shuffle(product_records))
var/datum/data/vending_product/R = record
if(R.amount <= 0) //Try to use a record that actually has something to dump.
continue
var/dump_path = R.product_path
if(!dump_path)
continue
R.amount--
// busting open a vendor will destroy some of the contents
if(found_anything && prob(80))
continue
var/obj/O = new dump_path(loc)
step(O, pick(GLOB.alldirs))
found_anything = TRUE
dump_amount++
if (dump_amount >= 16)
return
GLOBAL_LIST_EMPTY(vending_products)
/obj/machinery/vending/proc/build_inventory(list/productlist, list/recordlist, start_empty = FALSE)
for(var/typepath in productlist)
var/amount = productlist[typepath]
if(isnull(amount))
amount = 0
var/atom/temp = typepath
var/datum/data/vending_product/R = new /datum/data/vending_product()
GLOB.vending_products[typepath] = 1
R.name = initial(temp.name)
R.product_path = typepath
if(!start_empty)
R.amount = amount
R.max_amount = amount
R.custom_price = initial(temp.custom_price)
R.custom_premium_price = initial(temp.custom_premium_price)
recordlist += R
/obj/machinery/vending/proc/restock(obj/item/vending_refill/canister)
if (!canister.products)
canister.products = products.Copy()
if (!canister.contraband)
canister.contraband = contraband.Copy()
if (!canister.premium)
canister.premium = premium.Copy()
. = 0
. += refill_inventory(canister.products, product_records)
. += refill_inventory(canister.contraband, hidden_records)
. += refill_inventory(canister.premium, coin_records)
/obj/machinery/vending/proc/refill_inventory(list/productlist, list/recordlist)
. = 0
for(var/R in recordlist)
var/datum/data/vending_product/record = R
var/diff = min(record.max_amount - record.amount, productlist[record.product_path])
if (diff)
productlist[record.product_path] -= diff
record.amount += diff
. += diff
/obj/machinery/vending/proc/update_canister()
if (!component_parts)
return
var/obj/item/vending_refill/R = locate() in component_parts
if (!R)
CRASH("Constructible vending machine did not have a refill canister")
return
R.products = unbuild_inventory(product_records)
R.contraband = unbuild_inventory(hidden_records)
R.premium = unbuild_inventory(coin_records)
/obj/machinery/vending/proc/unbuild_inventory(list/recordlist)
. = list()
for(var/R in recordlist)
var/datum/data/vending_product/record = R
.[record.product_path] += record.amount
/obj/machinery/vending/crowbar_act(mob/living/user, obj/item/I)
if(!component_parts)
return FALSE
default_deconstruction_crowbar(I)
return TRUE
/obj/machinery/vending/wrench_act(mob/living/user, obj/item/I)
if(panel_open)
default_unfasten_wrench(user, I, time = 60)
return TRUE
/obj/machinery/vending/screwdriver_act(mob/living/user, obj/item/I)
if(..())
return TRUE
if(anchored)
default_deconstruction_screwdriver(user, icon_state, icon_state, I)
cut_overlays()
if(panel_open)
add_overlay("[initial(icon_state)]-panel")
updateUsrDialog()
else
to_chat(user, "<span class='warning'>You must first secure [src].</span>")
return TRUE
/obj/machinery/vending/attackby(obj/item/I, mob/user, params)
if(panel_open && is_wire_tool(I))
wires.interact(user)
return
if(refill_canister && istype(I, refill_canister))
if (!panel_open)
to_chat(user, "<span class='notice'>You should probably unscrew the service panel first.</span>")
else if (stat & (BROKEN|NOPOWER))
to_chat(user, "<span class='notice'>[src] does not respond.</span>")
else
//if the panel is open we attempt to refill the machine
var/obj/item/vending_refill/canister = I
if(canister.get_part_rating() == 0)
to_chat(user, "<span class='notice'>[canister] is empty!</span>")
else
// instantiate canister if needed
var/transferred = restock(canister)
if(transferred)
to_chat(user, "<span class='notice'>You loaded [transferred] items in [src].</span>")
else
to_chat(user, "<span class='notice'>There's nothing to restock!</span>")
return
if(compartmentLoadAccessCheck(user))
if(canLoadItem(I))
loadingAttempt(I,user)
updateUsrDialog() //can't put this on the proc above because we spam it below
if(istype(I, /obj/item/storage/bag)) //trays USUALLY
var/obj/item/storage/T = I
var/loaded = 0
var/denied_items = 0
for(var/obj/item/the_item in T.contents)
if(contents.len >= MAX_VENDING_INPUT_AMOUNT) // no more than 30 item can fit inside, legacy from snack vending although not sure why it exists
to_chat(user, "<span class='warning'>[src]'s chef compartment is full.</span>")
break
if(loadingAttempt(the_item,user))
SEND_SIGNAL(T, COMSIG_TRY_STORAGE_TAKE, the_item, src, TRUE)
loaded++
else
denied_items++
if(denied_items)
to_chat(user, "<span class='notice'>[src] refuses some items.</span>")
if(loaded)
to_chat(user, "<span class='notice'>You insert [loaded] dishes into [src]'s chef compartment.</span>")
updateUsrDialog()
else
..()
/obj/machinery/vending/proc/loadingAttempt(obj/item/I,mob/user)
. = TRUE
if(!user.transferItemToLoc(I, src))
return FALSE
if(vending_machine_input[I.name])
vending_machine_input[I.name]++
else
vending_machine_input[I.name] = 1
to_chat(user, "<span class='notice'>You insert [I] into [src]'s input compartment.</span>")
/obj/machinery/vending/exchange_parts(mob/user, obj/item/storage/part_replacer/W)
if(!istype(W))
return FALSE
if((flags_1 & NODECONSTRUCT_1) && !W.works_from_distance)
return FALSE
if(!component_parts || !refill_canister)
return FALSE
var/moved = 0
if(panel_open || W.works_from_distance)
if(W.works_from_distance)
display_parts(user)
for(var/I in W)
if(istype(I, refill_canister))
moved += restock(I)
else
display_parts(user)
if(moved)
to_chat(user, "[moved] items restocked.")
W.play_rped_sound()
return TRUE
/obj/machinery/vending/on_deconstruction()
update_canister()
. = ..()
/obj/machinery/vending/emag_act(mob/user)
if(obj_flags & EMAGGED)
return
obj_flags |= EMAGGED
to_chat(user, "<span class='notice'>You short out the product lock on [src].</span>")
/obj/machinery/vending/_try_interact(mob/user)
if(seconds_electrified && !(stat & NOPOWER))
if(shock(user, 100))
return
return ..()
/obj/machinery/vending/ui_interact(mob/user)
var/list/dat = list()
var/datum/bank_account/account
var/mob/living/carbon/human/H
var/obj/item/card/id/C
if(ishuman(user))
H = user
C = H.get_idcard(TRUE)
if(!C)
dat += "<font color = 'red'><h3>No ID Card detected!</h3></font>"
else if (!C.registered_account)
dat += "<font color = 'red'><h3>No account on registered ID card!</h3></font>"
if(onstation && C && C.registered_account)
account = C.registered_account
dat += {"<h3>Select an item</h3>
<div class='statusDisplay'>"}
if(!product_records.len)
dat += "<font color = 'red'>No product loaded!</font>"
else
var/list/display_records = product_records + coin_records
if(extended_inventory)
display_records = product_records + coin_records + hidden_records
dat += "<table>"
for (var/datum/data/vending_product/R in display_records)
var/price_listed = "$[default_price]"
var/is_hidden = hidden_records.Find(R)
if(is_hidden && !extended_inventory)
continue
if(R.custom_price)
price_listed = "$[R.custom_price]"
if(!onstation || account && account.account_job && account.account_job.paycheck_department == payment_department)
price_listed = "FREE"
if(coin_records.Find(R) || is_hidden)
price_listed = "$[R.custom_premium_price ? R.custom_premium_price : extra_price]"
dat += {"<tr><td><span class="vending32x32 [replacetext(replacetext("[R.product_path]", "/obj/item/", ""), "/", "-")]"></td>
<td style=\"width: 100%\"><b>[sanitize(R.name)] ([price_listed])</b></td>"}
if(R.amount > 0 && ((C && C.registered_account && onstation) || (!onstation && isliving(user))))
dat += "<td align='right'><b>[R.amount]&nbsp;</b><a href='byond://?src=[REF(src)];vend=[REF(R)]'>Vend</a></td>"
else
dat += "<td align='right'><span class='linkOff'>Not&nbsp;Available</span></td>"
dat += "</tr>"
dat += "</table>"
dat += "</div>"
if(onstation && C && C.registered_account)
dat += "<b>Balance: $[account.account_balance]</b>"
if(vending_machine_input.len)
dat += "<h3>[input_display_header]</h3>"
dat += "<div class='statusDisplay'>"
for (var/O in vending_machine_input)
if(vending_machine_input[O] > 0)
var/N = vending_machine_input[O]
dat += "<a href='byond://?src=[REF(src)];dispense=[sanitize(O)]'>Dispense</A> "
dat += "<B>[capitalize(O)] ($[default_price]): [N]</B><br>"
dat += "</div>"
var/datum/browser/popup = new(user, "vending", (name))
popup.add_stylesheet(get_asset_datum(/datum/asset/spritesheet/vending))
popup.set_content(dat.Join(""))
popup.set_title_image(user.browse_rsc_icon(icon, icon_state))
popup.open()
/obj/machinery/vending/Topic(href, href_list)
if(..())
return
usr.set_machine(src)
if((href_list["dispense"]) && (vend_ready))
var/N = href_list["dispense"]
if(vending_machine_input[N] <= 0) // Sanity check, there are probably ways to press the button when it shouldn't be possible.
return
vend_ready = 0
if(ishuman(usr) && onstation)
var/mob/living/carbon/human/H = usr
var/obj/item/card/id/C = H.get_idcard(TRUE)
if(!C)
say("No card found.")
flick(icon_deny,src)
vend_ready = 1
return
else if (!C.registered_account)
say("No account found.")
flick(icon_deny,src)
vend_ready = 1
return
var/datum/bank_account/account = C.registered_account
if(!account.adjust_money(-chef_price))
say("You do not possess the funds to purchase this meal.")
var/datum/bank_account/D = SSeconomy.get_dep_account(ACCOUNT_SRV)
if(D)
D.adjust_money(chef_price)
use_power(5)
vending_machine_input[N] = max(vending_machine_input[N] - 1, 0)
for(var/obj/O in contents)
if(O.name == N)
say("Thank you for supporting your local kitchen and purchasing [O]!")
O.forceMove(drop_location())
break
vend_ready = 1
updateUsrDialog()
return
if((href_list["vend"]) && (vend_ready))
if(panel_open)
to_chat(usr, "<span class='notice'>The vending machine cannot dispense products while its service panel is open!</span>")
return
vend_ready = 0 //One thing at a time!!
var/datum/data/vending_product/R = locate(href_list["vend"])
var/list/record_to_check = product_records + coin_records
if(extended_inventory)
record_to_check = product_records + coin_records + hidden_records
if(!R || !istype(R) || !R.product_path)
vend_ready = 1
return
var/price_to_use = default_price
if(R.custom_price)
price_to_use = R.custom_price
if(R in hidden_records)
if(!extended_inventory)
vend_ready = 1
return
else if (!(R in record_to_check))
vend_ready = 1
message_admins("Vending machine exploit attempted by [ADMIN_LOOKUPFLW(usr)]!")
return
if (R.amount <= 0)
say("Sold out of [R.name].")
flick(icon_deny,src)
vend_ready = 1
return
if(onstation && ishuman(usr))
var/mob/living/carbon/human/H = usr
var/obj/item/card/id/C = H.get_idcard(TRUE)
if(!C)
say("No card found.")
flick(icon_deny,src)
vend_ready = 1
return
else if (!C.registered_account)
say("No account found.")
flick(icon_deny,src)
vend_ready = 1
return
var/datum/bank_account/account = C.registered_account
if(account.account_job && account.account_job.paycheck_department == payment_department)
price_to_use = 0
if(coin_records.Find(R) || hidden_records.Find(R))
price_to_use = R.custom_premium_price ? R.custom_premium_price : extra_price
if(price_to_use && !account.adjust_money(-price_to_use))
say("You do not possess the funds to purchase [R.name].")
flick(icon_deny,src)
vend_ready = 1
return
var/datum/bank_account/D = SSeconomy.get_dep_account(payment_department)
if(D)
D.adjust_money(price_to_use)
if(last_shopper != usr || purchase_message_cooldown < world.time)
say("Thank you for shopping with [src]!")
purchase_message_cooldown = world.time + 5 SECONDS
last_shopper = usr
use_power(5)
if(icon_vend) //Show the vending animation if needed
flick(icon_vend,src)
new R.product_path(get_turf(src))
R.amount--
SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[R.product_path]"))
vend_ready = 1
else if(href_list["togglevoice"] && panel_open)
shut_up = !shut_up
updateUsrDialog()
/obj/machinery/vending/process()
if(stat & (BROKEN|NOPOWER))
return PROCESS_KILL
if(!active)
return
if(seconds_electrified > MACHINE_NOT_ELECTRIFIED)
seconds_electrified--
//Pitch to the people! Really sell it!
if(last_slogan + slogan_delay <= world.time && slogan_list.len > 0 && !shut_up && prob(5))
var/slogan = pick(slogan_list)
speak(slogan)
last_slogan = world.time
if(shoot_inventory && prob(shoot_inventory_chance))
throw_item()
/obj/machinery/vending/proc/speak(message)
if(stat & (BROKEN|NOPOWER))
return
if(!message)
return
say(message)
/obj/machinery/vending/power_change()
if(stat & BROKEN)
icon_state = "[initial(icon_state)]-broken"
else
if(powered())
icon_state = initial(icon_state)
stat &= ~NOPOWER
START_PROCESSING(SSmachines, src)
else
icon_state = "[initial(icon_state)]-off"
stat |= NOPOWER
//Somebody cut an important wire and now we're following a new definition of "pitch."
/obj/machinery/vending/proc/throw_item()
var/obj/throw_item = null
var/mob/living/target = locate() in view(7,src)
if(!target)
return 0
for(var/datum/data/vending_product/R in shuffle(product_records))
if(R.amount <= 0) //Try to use a record that actually has something to dump.
continue
var/dump_path = R.product_path
if(!dump_path)
continue
R.amount--
throw_item = new dump_path(loc)
break
if(!throw_item)
return 0
pre_throw(throw_item)
throw_item.throw_at(target, 16, 3)
visible_message("<span class='danger'>[src] launches [throw_item] at [target]!</span>")
return 1
/obj/machinery/vending/proc/pre_throw(obj/item/I)
return
/obj/machinery/vending/proc/shock(mob/user, prb)
if(stat & (BROKEN|NOPOWER)) // unpowered, no shock
return FALSE
if(!prob(prb))
return FALSE
do_sparks(5, TRUE, src)
var/check_range = TRUE
if(electrocute_mob(user, get_area(src), src, 0.7, check_range))
return TRUE
else
return FALSE
/obj/machinery/vending/proc/canLoadItem(obj/item/I,mob/user)
return FALSE
/obj/machinery/vending/proc/compartmentLoadAccessCheck(mob/user)
if(!canload_access_list)
return TRUE
else
var/do_you_have_access = FALSE
var/req_access_txt_holder = req_access_txt
for(var/i in canload_access_list)
req_access_txt = i
if(!allowed(user) && !(obj_flags & EMAGGED) && scan_id)
continue
else
do_you_have_access = TRUE
break //you passed don't bother looping anymore
req_access_txt = req_access_txt_holder // revert to normal (before the proc ran)
if(do_you_have_access)
return TRUE
else
to_chat(user, "<span class='warning'>[src]'s input compartment blinks red: Access denied.</span>")
return FALSE
/obj/machinery/vending/onTransitZ()
return