/*
* 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
/**
* # vending record datum
*
* A datum that represents a product that is vendable
*/
/datum/data/vending_product
name = "generic"
///Typepath of the product that is created when this record "sells"
var/product_path = null
///How many of this product we currently have
var/amount = 0
///How many we can store at maximum
var/max_amount = 0
///Does the item have a custom price override
var/custom_price
///Does the item have a custom premium price override
var/custom_premium_price
/**
* # vending machines
*
* Captalism in the year 2525, everything in a vending machine, even love
*/
/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
/// Is the machine active (No sales pitches if off)!
var/active = 1
///Are we ready to vend?? Is it time??
var/vend_ready = 1
///Next world time to send a purchase message
var/purchase_message_cooldown
///Last mob to shop with us
var/last_shopper
/**
* List of products this machine sells
*
* form should be list(/type/path = amount, /type/path2 = amount2)
*/
var/list/products = list()
/**
* List of products this machine sells when you hack it
*
* form should be list(/type/path = amount, /type/path2 = amount2)
*/
var/list/contraband = list()
/**
* List of premium products this machine sells
*
* form should be list(/type/path, /type/path2) as there is only ever one in stock
*/
var/list/premium = list()
///String of slogans separated by semicolons, optional
var/product_slogans = ""
///String of small ad messages in the vending screen - random chance
var/product_ads = ""
var/list/product_records = list()
var/list/hidden_records = list()
var/list/coin_records = list()
var/list/slogan_list = list()
///Small ad messages in the vending screen - random chance of popping up whenever you open it
var/list/small_ads = list()
///Message sent post vend (Thank you for shopping!)
var/vend_reply
///Last world tick we sent a vent reply
var/last_reply = 0
///Last world tick we sent a slogan message out
var/last_slogan = 0
///How many ticks until we can send another
var/slogan_delay = 6000
///Icon when vending an item to the user
var/icon_vend
///Icon to flash when user is denied a vend
var/icon_deny
///World ticks the machine is electified for
var/seconds_electrified = MACHINE_NOT_ELECTRIFIED
///When this is TRUE, we fire items at customers! We're broken!
var/shoot_inventory = 0
///How likely this is to happen (prob 100)
var/shoot_inventory_chance = 2
//Stop spouting those godawful pitches!
var/shut_up = 0
///can we access the hidden inventory?
var/extended_inventory = 0
///Are we checking the users ID
var/scan_id = 1
///Coins that we accept?
var/obj/item/coin/coin
///Bills we accept?
var/obj/item/stack/spacecash/bill
var/chef_price = 10
///Default price of items if not overridden
var/default_price = 25
///Default price of premium items if not overridden
var/extra_price = 50
/**
* Is this item on station or not
*
* if it doesn't originate from off-station during mapload, everything is free
*/
var/onstation = TRUE //if it doesn't originate from off-station during mapload, everything is free
///A variable to change on a per instance basis on the map that allows the instance to force cost and ID requirements
var/onstation_override = FALSE //change this on the object on the map to override the onstation check. DO NOT APPLY THIS GLOBALLY.
///ID's that can load this vending machine wtih refills
var/list/canload_access_list
var/list/vending_machine_input = list()
///Display header on the input view
var/input_display_header = "Custom Compartment"
//The type of refill canisters used by this machine.
var/obj/item/vending_refill/refill_canister = null
/obj/item/circuitboard
///determines if the circuit board originated from a vendor off station or not.
var/onstation = TRUE
/**
* Initialize the vending machine
*
* Builds the vending machine inventory, sets up slogans and other such misc work
*
* This also sets the onstation var to:
* * FALSE - if the machine was maploaded on a zlevel that doesn't pass the is_station_level check
* * TRUE - all other cases
*/
/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(onstation_override) //overrides the checks if true.
onstation = TRUE
return
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)
/**
* Build the inventory of the vending machine from it's product and record lists
*
* This builds up a full set of /datum/data/vending_products from the product list of the vending machine type
* Arguments:
* * productlist - the list of products that need to be converted
* * recordlist - the list containing /datum/data/vending_product datums
* * startempty - should we set vending_product record amount from the product list (so it's prefilled at roundstart)
*/
/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
/**
* Refill a vending machine from a refill canister
*
* This takes the products from the refill canister and then fills the products,contraband and premium product categories
*
* Arguments:
* * canister - the vending canister we are refilling from
*/
/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)
/**
* Refill our inventory from the passed in product list into the record list
*
* Arguments:
* * productlist - list of types -> amount
* * recordlist - existing record datums
*/
/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
/**
* Set up a refill canister that matches this machines products
*
* This is used when the machine is deconstructed, so the items aren't "lost"
*/
/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)
/**
* Given a record list, go through and and return a list of type -> amount
*/
/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, "You must first secure [src].")
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, "You should probably unscrew the service panel first!")
else if (stat & (BROKEN|NOPOWER))
to_chat(user, "[src] does not respond.")
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, "[canister] is empty!")
else
// instantiate canister if needed
var/transferred = restock(canister)
if(transferred)
to_chat(user, "You loaded [transferred] items in [src].")
else
to_chat(user, "There's nothing to restock!")
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, "[src]'s chef compartment is full.")
break
if(canLoadItem(the_item) && 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, "[src] refuses some items!")
if(loaded)
to_chat(user, "You insert [loaded] dishes into [src]'s chef compartment.")
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, "You insert [I] into [src]'s input compartment.")
/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, "You short out the product lock on [src].")
/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 += "No ID Card detected!
"
else if (!C.registered_account)
dat += "No account on registered ID card!
"
if(onstation && C && C.registered_account)
account = C.registered_account
if(vending_machine_input.len)
dat += "
| [sanitize(R.name)] ([price_listed]) | "} if(R.amount > 0 && ((C && C.registered_account && onstation) || (!onstation && isliving(user)))) dat += "[R.amount] Vend | " else dat += "Not Available | " dat += "