/* * 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 /** * # 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 = 0.33 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 light_power = 0.5 light_range = MINIMUM_USEFUL_LIGHT_RANGE /// Is the machine active (No sales pitches if off)! var/active = TRUE ///Are we ready to vend?? Is it time?? var/vend_ready = TRUE ///Next world time to send a purchase message var/purchase_message_cooldown ///Last mob to shop with us /** * 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() var/dish_quants = list() //used by the snack machine's custom compartment to count dishes. ///Message sent post vend (Thank you for shopping!) var/vend_reply ///Last world tick we sent a vent reply var/last_reply ///Last world tick we sent a slogan message out var/last_slogan var/last_shopper ///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 ///How likely this is to happen (prob 100) var/shoot_inventory_chance = 2 //Stop spouting those godawful pitches! var/shut_up ///can we access the hidden inventory? var/extended_inventory ///Are we checking the users ID var/scan_id = TRUE ///Coins that we accept? var/obj/item/coin/coin ///Default price of items if not overridden /** * 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 Vendor" //The type of refill canisters used by this machine. var/obj/item/vending_refill/refill_canister = null /// how many items have been inserted in a vendor var/loaded_items ///Name of lighting mask for the vending machine var/light_mask /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() /obj/machinery/vending/Destroy() QDEL_NULL(wires) QDEL_NULL(coin) 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/update_icon_state() if(stat & BROKEN) icon_state = "[initial(icon_state)]-broken" set_light(0) else if(powered()) icon_state = initial(icon_state) set_light(1.4) else icon_state = "[initial(icon_state)]-off" set_light(0) /obj/machinery/vending/update_overlays() . = ..() if(!light_mask) return SSvis_overlays.remove_vis_overlay(src, managed_vis_overlays) if(!(stat & BROKEN) && powered()) SSvis_overlays.add_vis_overlay(src, icon, light_mask, EMISSIVE_LAYER, EMISSIVE_PLANE) /obj/machinery/vending/obj_break(damage_flag) . = ..() if(!.) return 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 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") 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 else if(istype(I, /obj/item/coin)) if(coin) to_chat(user, "[src] already has [coin] inserted") return if(!premium.len) to_chat(user, "[src] doesn't have a coin slot.") return if(!user.transferItemToLoc(I, src)) return coin = I to_chat(user, "You insert [I] into [src].") return else 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) && user.a_intent != INTENT_HARM) 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 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 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[format_text(I.name)]) vending_machine_input[format_text(I.name)]++ else vending_machine_input[format_text(I.name)] = 1 to_chat(user, "You insert [I] into [src]'s input compartment.") loaded_items++ /** * Is the passed in user allowed to load this vending machines compartments * * Arguments: * * user - mob that is doing the loading of the vending machine */ /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, "[src]'s input compartment blinks red: Access denied.") return FALSE /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].") return TRUE /obj/machinery/vending/_try_interact(mob/user) if(seconds_electrified && !(stat & NOPOWER)) if(shock(user, 100)) return return ..() /obj/machinery/vending/ui_base_html(html) var/datum/asset/spritesheet/assets = get_asset_datum(/datum/asset/spritesheet/vending) . = replacetext(html, "", assets.css_tag()) /obj/machinery/vending/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state) ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) if(!ui) var/datum/asset/assets = get_asset_datum(/datum/asset/spritesheet/vending) assets.send(user) ui = new(user, src, ui_key, "vending", name, 450, 600, master_ui, state) ui.open() /obj/machinery/vending/ui_static_data(mob/user) . = list() .["product_records"] = list() for (var/datum/data/vending_product/R in product_records) var/list/data = list( path = replacetext(replacetext("[R.product_path]", "/obj/item/", ""), "/", "-"), name = R.name, max_amount = R.max_amount, ref = REF(R) ) .["product_records"] += list(data) .["coin_records"] = list() for (var/datum/data/vending_product/R in coin_records) var/list/data = list( path = replacetext(replacetext("[R.product_path]", "/obj/item/", ""), "/", "-"), name = R.name, max_amount = R.max_amount, ref = REF(R), premium = TRUE ) .["coin_records"] += list(data) .["hidden_records"] = list() for (var/datum/data/vending_product/R in hidden_records) var/list/data = list( path = replacetext(replacetext("[R.product_path]", "/obj/item/", ""), "/", "-"), name = R.name, max_amount = R.max_amount, ref = REF(R), premium = TRUE ) .["hidden_records"] += list(data) /obj/machinery/vending/ui_data(mob/user) . = list() .["stock"] = list() for(var/datum/data/vending_product/R in product_records + coin_records + hidden_records) .["stock"][R.name] = R.amount .["extended_inventory"] = extended_inventory .["coin"] = coin /obj/machinery/vending/ui_act(action, params) . = ..() if(.) return switch(action) if("vend") . = TRUE if(!vend_ready) return if(panel_open) to_chat(usr, "The vending machine cannot dispense products while its service panel is open!") return if((!allowed(usr)) && !(obj_flags & EMAGGED) && scan_id) //For SECURE VENDING MACHINES YEAH to_chat(usr, "Access denied." ) flick(icon_deny,src) return vend_ready = FALSE //One thing at a time!! var/datum/data/vending_product/R = locate(params["ref"]) if(!R || !istype(R) || !R.product_path) vend_ready = TRUE return if(R in hidden_records) if(!extended_inventory) vend_ready = TRUE return else if(R in coin_records) if(!(coin)) to_chat(usr, "You need to a coin to get this item!") vend_ready = TRUE return if(coin && coin.string_attached) if(!prob(50)) to_chat(usr, "You weren't able to pull [coin] out fast enough, the machine ate it, string and all!") QDEL_NULL(coin) return if(!usr.CanReach(src)) to_chat(usr, "You successfully pull [coin] out of [src] to the floor.") coin = null if(!usr.put_in_hands(coin)) to_chat(usr, "You couldn't pull [coin] out because your hands are full!") QDEL_NULL(coin) to_chat(usr, "You successfully pull [coin] out before [src] could swallow it.") coin = null QDEL_NULL(coin) else if(!(R in product_records)) vend_ready = TRUE message_admins("Vending machine exploit attempted by [ADMIN_LOOKUPFLW(usr)]!") return if(R.amount <= 0) to_chat(usr, "Sold out.") vend_ready = TRUE return else R.amount-- if(((last_reply + 200) <= world.time) && vend_reply) speak(vend_reply) last_reply = world.time use_power(5) if(icon_vend) //Show the vending animation if needed flick(icon_vend,src) var/vended = new R.product_path(get_turf(src)) if(usr.CanReach(src) && usr.put_in_hands(vended)) to_chat(usr, "You take [R.name] out of the slot.") else to_chat(usr, "[capitalize(R.name)] falls onto the floor!") SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[R.product_path]")) vend_ready = TRUE return if("takeoutcoin") usr.put_in_hands(coin) to_chat(usr, "You remove [coin] from [src].") coin = null return if("togglevoice") if(panel_open) shut_up = !shut_up /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() /** * Speak the given message verbally * * Checks if the machine is powered and the message exists * * Arguments: * * message - the message to speak */ /obj/machinery/vending/proc/speak(message) if(stat & (BROKEN|NOPOWER)) return if(!message) return say(message) /obj/machinery/vending/power_change() . = ..() if(powered()) START_PROCESSING(SSmachines, src) //Somebody cut an important wire and now we're following a new definition of "pitch." /** * Throw an item from our internal inventory out in front of us * * This is called when we are hacked, it selects a random product from the records that has an amount > 0 * This item is then created and tossed out in front of us with a visible message */ /obj/machinery/vending/proc/throw_item() var/obj/throw_item = null var/mob/living/target = locate() in view(7, src) if(!target) return FALSE 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 FALSE pre_throw(throw_item) throw_item.throw_at(target, 16, 3) visible_message("[src] launches [throw_item] at [target]!") return TRUE /** * A callback called before an item is tossed out * * Override this if you need to do any special case handling * * Arguments: * * I - obj/item being thrown */ /obj/machinery/vending/proc/pre_throw(obj/item/I) return /** * Shock the passed in user * * This checks we have power and that the passed in prob is passed, then generates some sparks * and calls electrocute_mob on the user * * Arguments: * * user - the user to shock * * prb - probability the shock happens */ /obj/machinery/vending/proc/shock(mob/living/user, prb) if(!istype(user) || 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 /** * Are we able to load the item passed in * * Arguments: * * I - the item being loaded * * user - the user doing the loading */ /obj/machinery/vending/proc/canLoadItem(obj/item/I, mob/user) return FALSE /obj/machinery/vending/onTransitZ() return