Files
Bubberstation/code/modules/vending/_vending.dm
Sealed101 f21e88c225 Makes labor camp sustenance vendor cost labor points (#76976)
## About The Pull Request

On the tin.
Labor camp vendor is a subtype of the sustenance vendor, same stock,
different currency.
If a user doesn't have a prisoner ID, they won't be able to buy anything
from that vendor.

![изображение](https://github.com/tgstation/tgstation/assets/75863639/e266e168-0c3c-4caf-ab05-879c4a18323c)

## Why It's Good For The Game

Brings to a close the inconsistency of permabrig vendor costing money
but the labor camp vendor being free...
...with the caveat that now it actually costs your freedom points. Which
has been agreed upon to be equal parts fun and evil.
Closes #40889 

![изображение](https://github.com/tgstation/tgstation/assets/75863639/a10241ca-f7e4-4a62-a945-569c2f8be534)
Keeping it as a fix but lmk

## Changelog

🆑
fix: Labor Camp Sustenance vendor is no longer free; instead, it takes
your labor points that you'd otherwise use for paying off your point
goal.
/🆑
2023-08-12 14:15:20 -04:00

1755 lines
63 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()
*/
/// Maximum amount of items in a storage bag that we're transferring items to the vendor from.
#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
///Whether spessmen with an ID with an age below AGE_MINOR (20 by default) can buy this item
var/age_restricted = FALSE
///Whether the product can be recolored by the GAGS system
var/colorable
/// The category the product was in, if any.
/// Sourced directly from product_categories.
var/category
///List of items that have been returned to the vending machine.
var/list/returned_products
/**
* # 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/machines/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_type = /datum/armor/machinery_vending
circuit = /obj/item/circuitboard/machine/vendor
payment_department = ACCOUNT_SRV
light_power = 0.7
light_range = MINIMUM_USEFUL_LIGHT_RANGE
voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000"
/// Is the machine active (No sales pitches if off)!
var/active = 1
///Are we ready to vend?? Is it time??
var/vend_ready = TRUE
///Next world time to send a purchase message
var/purchase_message_cooldown
///The ref of the last mob to shop with us
var/last_shopper
///Whether the vendor is tilted or not
var/tilted = FALSE
/// If tilted, this variable should always be the rotation that was applied when we were tilted. Stored for the purposes of unapplying it.
var/tilted_rotation = 0
///Whether this vendor can be tilted over or not
var/tiltable = TRUE
///Damage this vendor does when tilting onto an atom
var/squish_damage = 75
/// The chance, in percent, of this vendor performing a critical hit on anything it crushes via [tilt].
var/crit_chance = 15
/// If set to a critical define in crushing.dm, anything this vendor crushes will always be hit with that effect.
var/forcecrit = null
///Number of glass shards the vendor creates and tries to embed into an atom it tilted onto
var/num_shards = 7
///List of mobs stuck under the vendor
var/list/pinned_mobs = list()
///Icon for the maintenance panel overlay
var/panel_type = "panel1"
/**
* 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, categorized.
* Can only be used as an alternative to `products`, not alongside it.
*
* Form should be list(
* "name" = "Category Name",
* "icon" = "UI Icon (Font Awesome or tgfont)",
* "products" = list(/type/path = amount, ...),
* )
*/
var/list/product_categories = null
/**
* 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 = amount, /type/path2 = amount2)
*/
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 = ""
///List of standard product records
var/list/product_records = list()
///List of contraband product records
var/list/hidden_records = list()
///List of premium product records
var/list/coin_records = list()
///List of slogans to scream at potential customers; built upon Iniitialize() of the vendor from product_slogans
var/list/slogan_list = 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 = 10 MINUTES
///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 = FALSE
///How likely this is to happen (prob 100) per second
var/shoot_inventory_chance = 1
//Stop spouting those godawful pitches!
var/shut_up = FALSE
///can we access the hidden inventory?
var/extended_inventory = FALSE
///Are we checking the users ID
var/scan_id = TRUE
///Coins that we accept?
var/obj/item/coin/coin
///Bills we accept?
var/obj/item/stack/spacecash/bill
///Default price of items if not overridden
var/default_price = 25
///Default price of premium items if not overridden
var/extra_price = 50
///fontawesome icon name to use in to display the user's balance in the vendor UI
var/displayed_currency_icon = "coins"
///String of the used currency to display in the vendor UI
var/displayed_currency_name = " cr"
///Whether our age check is currently functional
var/age_restrictions = TRUE
/**
* Is this item on station or not
*
* if it doesn't originate from off-station during mapload, everything is free
* if it's off-station during mapload, it's also safe from the brand intelligence event
*/
var/onstation = TRUE
/**
* A variable to change on a per instance basis on the map that allows the instance
* to ignore whether it's on the station or not.
* Useful to force cost and ID requirements. DO NOT APPLY THIS GLOBALLY.
*/
var/onstation_override = FALSE
///Items that the players have loaded into the vendor
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 = 0
///Name of lighting mask for the vending machine
var/light_mask
/// used for narcing on underages
var/obj/item/radio/sec_radio
/datum/armor/machinery_vending
melee = 20
fire = 50
acid = 70
/**
* 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
. = ..()
set_wires(new /datum/wires/vending(src))
if(SStts.tts_enabled)
var/static/vendor_voice_by_type = list()
if(!vendor_voice_by_type[type])
vendor_voice_by_type[type] = pick(SStts.available_speakers)
voice = vendor_voice_by_type[type]
if(build_inv) //non-constructable vending machine
build_inventories()
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))
if(!onstation_override)
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)
/**
* in average, it should be 37.5% of the max amount, rounded up to the nearest int,
* tho the max boundary can be as low/high as 50%/100%
*/
var/max_amount = rand(CEILING(product_record.amount * 0.5, 1), product_record.amount)
product_record.amount = rand(0, max_amount)
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)
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)
QDEL_NULL(sec_radio)
return ..()
/obj/machinery/vending/can_speak()
return !shut_up
/obj/machinery/vending/emp_act(severity)
. = ..()
var/datum/language_holder/vending_languages = get_language_holder()
var/datum/wires/vending/vending_wires = wires
// if the language wire got pulsed during an EMP, this will make sure the language_iterator is synched correctly
vending_languages.selected_language = vending_languages.spoken_languages[vending_wires.language_iterator]
//Better would be to make constructable child
/obj/machinery/vending/RefreshParts()
SHOULD_CALL_PARENT(FALSE)
if(!component_parts)
return
build_products_from_categories()
product_records = list()
hidden_records = list()
coin_records = list()
build_inventories(start_empty = TRUE)
for(var/obj/item/vending_refill/installed_refill in component_parts)
restock(installed_refill)
/obj/machinery/vending/deconstruct(disassembled = TRUE)
if(refill_canister)
return ..()
if(!(flags_1 & NODECONSTRUCT_1)) //the non constructable vendors drop metal instead of a machine frame.
new /obj/item/stack/sheet/iron(loc, 3)
qdel(src)
/obj/machinery/vending/update_appearance(updates=ALL)
. = ..()
if(machine_stat & BROKEN)
set_light(0)
return
set_light(powered() ? MINIMUM_USEFUL_LIGHT_RANGE : 0)
/obj/machinery/vending/update_icon_state()
if(machine_stat & BROKEN)
icon_state = "[initial(icon_state)]-broken"
return ..()
icon_state = "[initial(icon_state)][powered() ? null : "-off"]"
return ..()
/obj/machinery/vending/update_overlays()
. = ..()
if(panel_open)
. += panel_type
if(light_mask && !(machine_stat & BROKEN) && powered())
. += emissive_appearance(icon, light_mask, src)
/obj/machinery/vending/atom_break(damage_flag)
. = ..()
if(!.)
return
var/dump_amount = 0
var/found_anything = TRUE
while (found_anything)
found_anything = FALSE
for(var/datum/data/vending_product/record as anything in shuffle(product_records))
//first dump any of the items that have been returned, in case they contain the nuke disk or something
for(var/obj/returned_obj_to_dump in record.returned_products)
LAZYREMOVE(record.returned_products, returned_obj_to_dump)
returned_obj_to_dump.forceMove(get_turf(src))
step(returned_obj_to_dump, pick(GLOB.alldirs))
record.amount--
if(record.amount <= 0) //Try to use a record that actually has something to dump.
continue
var/dump_path = record.product_path
if(!dump_path)
continue
record.amount--
// busting open a vendor will destroy some of the contents
if(found_anything && prob(80))
continue
var/obj/obj_to_dump = new dump_path(loc)
step(obj_to_dump, pick(GLOB.alldirs))
found_anything = TRUE
dump_amount++
if (dump_amount >= 16)
return
/**
* 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
* * categories - A list in the format of product_categories to source category from
* * 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, list/categories, start_empty = FALSE)
default_price = round(initial(default_price))
extra_price = round(initial(extra_price))
if(HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING))
default_price *= SSeconomy.inflation_value()
extra_price *= SSeconomy.inflation_value()
var/list/product_to_category = list()
for (var/list/category as anything in categories)
var/list/products = category["products"]
for (var/product_key in products)
product_to_category[product_key] = category
for(var/typepath in productlist)
var/amount = productlist[typepath]
if(isnull(amount))
amount = 0
var/obj/item/temp = typepath
var/datum/data/vending_product/new_record = new /datum/data/vending_product()
new_record.name = initial(temp.name)
new_record.product_path = typepath
if(!start_empty)
new_record.amount = amount
new_record.max_amount = amount
///Prices of vending machines are all increased uniformly.
new_record.custom_price = round(initial(temp.custom_price))
new_record.custom_premium_price = round(initial(temp.custom_premium_price))
if(HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING))
new_record.custom_price = round(initial(temp.custom_price) * SSeconomy.inflation_value())
new_record.custom_premium_price = round(initial(temp.custom_premium_price) * SSeconomy.inflation_value())
new_record.age_restricted = initial(temp.age_restricted)
new_record.colorable = !!(initial(temp.greyscale_config) && initial(temp.greyscale_colors) && (initial(temp.flags_1) & IS_PLAYER_COLORABLE_1))
new_record.category = product_to_category[typepath]
recordlist += new_record
/**Builds all available inventories for the vendor - standard, contraband and premium
* Arguments:
* start_empty - bool to pass into build_inventory that determines whether a product entry starts with available stock or not
*/
/obj/machinery/vending/proc/build_inventories(start_empty)
build_inventory(products, product_records, product_categories, start_empty)
build_inventory(contraband, hidden_records, create_categories_from("Contraband", "mask", contraband), start_empty)
build_inventory(premium, coin_records, create_categories_from("Premium", "coins", premium), start_empty)
/**
* Returns a list of data about the category
* Arguments:
* name - string for the name of the category
* icon - string for the fontawesome icon to use in the UI for the category
* products - list of products available in the category
*/
/obj/machinery/vending/proc/create_categories_from(name, icon, products)
return list(list(
"name" = name,
"icon" = icon,
"products" = products,
))
///Populates list of products with categorized products
/obj/machinery/vending/proc/build_products_from_categories()
if (isnull(product_categories))
return
products = list()
for (var/list/category in product_categories)
var/list/category_products = category["products"]
for (var/product_key in category_products)
products[product_key] += category_products[product_key]
/**
* Reassign the prices of the vending machine as a result of the inflation value, as provided by SSeconomy
*
* This rebuilds both /datum/data/vending_products lists for premium and standard products based on their most relevant pricing values.
* Arguments:
* * recordlist - the list of standard product datums in the vendor to refresh their prices.
* * premiumlist - the list of premium product datums in the vendor to refresh their prices.
*/
/obj/machinery/vending/proc/reset_prices(list/recordlist, list/premiumlist)
var/crash_status = HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING)
default_price = round(initial(default_price))
extra_price = round(initial(extra_price))
if(crash_status)
default_price *= SSeconomy.inflation_value()
extra_price *= SSeconomy.inflation_value()
for(var/datum/data/vending_product/record as anything in recordlist)
var/obj/item/potential_product = record.product_path
record.custom_price = round(initial(potential_product.custom_price))
if(crash_status)
record.custom_price = round(initial(potential_product.custom_price) * SSeconomy.inflation_value())
for(var/datum/data/vending_product/premium_record as anything in premiumlist)
var/obj/item/potential_product = premium_record.product_path
var/premium_sanity = round(initial(potential_product.custom_premium_price))
if(premium_sanity)
premium_record.custom_premium_price = round(premium_sanity)
if(crash_status)
premium_record.custom_premium_price = round(premium_sanity * SSeconomy.inflation_value())
continue
//For some ungodly reason, some premium only items only have a custom_price
premium_record.custom_premium_price = round(extra_price + (initial(potential_product.custom_price)))
if(crash_status)
premium_record.custom_premium_price = round(extra_price + (initial(potential_product.custom_price) * (SSeconomy.inflation_value() - 1)))
/**
* 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
if (isnull(canister.product_categories) && !isnull(product_categories))
canister.product_categories = product_categories.Copy()
if (!isnull(canister.product_categories))
var/list/products_unwrapped = list()
for (var/list/category as anything in canister.product_categories)
var/list/products = category["products"]
for (var/product_key in products)
products_unwrapped[product_key] += products[product_key]
. += refill_inventory(products_unwrapped, product_records)
else
. += refill_inventory(canister.products, product_records)
. += refill_inventory(canister.contraband, hidden_records)
. += refill_inventory(canister.premium, coin_records)
return .
/**
* 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/datum/data/vending_product/record as anything in recordlist)
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 machine's 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/installed_refill = locate() in component_parts
if (!installed_refill)
CRASH("Constructible vending machine did not have a refill canister")
unbuild_inventory_into(product_records, installed_refill.products, installed_refill.product_categories)
installed_refill.contraband = unbuild_inventory(hidden_records)
installed_refill.premium = unbuild_inventory(coin_records)
/**
* Given a record list, go through and return a list of products in format of type -> amount
* Arguments:
* recordlist - list of records to unbuild products from
*/
/obj/machinery/vending/proc/unbuild_inventory(list/recordlist)
. = list()
for(var/datum/data/vending_product/record as anything in recordlist)
.[record.product_path] += record.amount
/**
* Unbuild product_records into categorized product lists to the machine's refill canister.
* Does not handle contraband/premium products, only standard stock and any other categories used by the vendor(see: ClothesMate).
* If a product has no category, puts it into standard stock category.
* Arguments:
* product_records - list of products of the vendor
* products - list of products of the refill canister
* product_categories - list of product categories of the refill canister
*/
/obj/machinery/vending/proc/unbuild_inventory_into(list/product_records, list/products, list/product_categories)
products?.Cut()
product_categories?.Cut()
var/others_have_category = null
var/list/categories_to_index = list()
for (var/datum/data/vending_product/record as anything in product_records)
var/list/category = record.category
var/has_category = !isnull(category)
//check if there're any uncategorized products
if (isnull(others_have_category))
others_have_category = has_category
else if (others_have_category != has_category)
if (has_category)
WARNING("[record.product_path] in [type] has a category, but other products don't")
else
WARNING("[record.product_path] in [type] does not have a category, but other products do")
continue
if (has_category)
var/index = categories_to_index.Find(category)
if (index) //if we've already established a category, add the product there
var/list/category_in_list = product_categories[index]
var/list/products_in_category = category_in_list["products"]
products_in_category[record.product_path] += record.amount
else //create a category that the product is supposed to have and put it there
categories_to_index += list(category)
index = categories_to_index.len
var/list/category_clone = category.Copy()
var/list/initial_product_list = list()
initial_product_list[record.product_path] = record.amount
category_clone["products"] = initial_product_list
product_categories += list(category_clone)
else //no category found - dump it into standard stock
products[record.product_path] = record.amount
/obj/machinery/vending/crowbar_act(mob/living/user, obj/item/attack_item)
if(!component_parts)
return FALSE
default_deconstruction_crowbar(attack_item)
return TRUE
/obj/machinery/vending/wrench_act(mob/living/user, obj/item/tool)
. = ..()
if(!panel_open)
return FALSE
if(default_unfasten_wrench(user, tool, time = 6 SECONDS))
unbuckle_all_mobs(TRUE)
return TOOL_ACT_TOOLTYPE_SUCCESS
return FALSE
/obj/machinery/vending/screwdriver_act(mob/living/user, obj/item/attack_item)
if(..())
return TRUE
if(anchored)
default_deconstruction_screwdriver(user, icon_state, icon_state, attack_item)
update_appearance()
else
to_chat(user, span_warning("You must first secure [src]."))
return TRUE
/obj/machinery/vending/attackby(obj/item/attack_item, mob/living/user, params)
if(panel_open && is_wire_tool(attack_item))
wires.interact(user)
return
if(refill_canister && istype(attack_item, refill_canister))
if (!panel_open)
to_chat(user, span_warning("You should probably unscrew the service panel first!"))
else if (machine_stat & (BROKEN|NOPOWER))
to_chat(user, span_notice("[src] does not respond."))
else
//if the panel is open we attempt to refill the machine
var/obj/item/vending_refill/canister = attack_item
if(canister.get_part_rating() == 0)
to_chat(user, span_warning("[canister] is empty!"))
else
// instantiate canister if needed
var/transferred = restock(canister)
if(transferred)
to_chat(user, span_notice("You loaded [transferred] items in [src]."))
else
to_chat(user, span_warning("There's nothing to restock!"))
return
if(compartmentLoadAccessCheck(user) && !user.combat_mode)
if(canLoadItem(attack_item))
loadingAttempt(attack_item, user)
if(istype(attack_item, /obj/item/storage/bag)) //trays USUALLY
var/obj/item/storage/storage_item = attack_item
var/loaded = 0
var/denied_items = 0
for(var/obj/item/the_item in storage_item.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_warning("[src]'s compartment is full."))
break
if(canLoadItem(the_item) && loadingAttempt(the_item, user))
storage_item.atom_storage?.attempt_remove(the_item, src)
loaded++
else
denied_items++
if(denied_items)
to_chat(user, span_warning("[src] refuses some items!"))
if(loaded)
to_chat(user, span_notice("You insert [loaded] dishes into [src]'s compartment."))
else
. = ..()
if(tiltable && !tilted && attack_item.force)
if(isclosedturf(get_turf(user))) //If the attacker is inside of a wall, immediately fall in the other direction, with no chance for goodies.
var/opposite_direction = REVERSE_DIR(get_dir(src, user))
var/target = get_step(src, opposite_direction)
tilt(get_turf(target))
return
switch(rand(1, 100))
if(1 to 5)
freebie(3)
if(6 to 15)
freebie(2)
if(16 to 25)
freebie(1)
if(26 to 75)
return
if(76 to 100)
tilt(user)
/**
* Dispenses free items from the standard stock.
* Arguments:
* 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"]!"))
for(var/i in 1 to freebies)
playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3)
for(var/datum/data/vending_product/record in shuffle(product_records))
if(record.amount <= 0) //Try to use a record that actually has something to dump.
continue
var/dump_path = record.product_path
if(!dump_path)
continue
if(record.amount > LAZYLEN(record.returned_products)) //always give out new stuff that costs before free returned stuff, because of the risk getting gibbed involved
new dump_path(get_turf(src))
else
var/obj/returned_obj_to_dump = LAZYACCESS(record.returned_products, LAZYLEN(record.returned_products)) //first in, last out
LAZYREMOVE(record.returned_products, returned_obj_to_dump)
returned_obj_to_dump.forceMove(get_turf(src))
record.amount--
break
/**
* Tilts ontop of the atom supplied, if crit is true some extra shit can happen. See [fall_and_crush] for return values.
* Arguments:
* fatty - atom to tilt the vendor onto
* local_crit_chance - percent chance of a critical hit
* forced_crit - specific critical hit case to use, if any
*/
/obj/machinery/vending/proc/tilt(atom/fatty, local_crit_chance = crit_chance, forced_crit = forcecrit)
if(QDELETED(src) || !has_gravity(src))
return
. = NONE
var/picked_rotation = pick(90, 270)
if(Adjacent(fatty))
. = fall_and_crush(get_turf(fatty), squish_damage, local_crit_chance, forced_crit, 6 SECONDS, rotation = picked_rotation)
if (. & SUCCESSFULLY_FELL_OVER)
visible_message(span_danger("[src] tips over!"))
tilted = TRUE
tilted_rotation = picked_rotation
layer = ABOVE_MOB_LAYER
SET_PLANE_IMPLICIT(src, GAME_PLANE_UPPER)
if(get_turf(fatty) != get_turf(src))
throw_at(get_turf(fatty), 1, 1, spin = FALSE, quickstart = FALSE)
/**
* Causes src to fall onto [target], crushing everything on it (including itself) with [damage]
* and a small chance to do a spectacular effect per entity (if a chance above 0 is provided).
*
* Args:
* * turf/target: The turf to fall onto. Cannot be null.
* * damage: The raw numerical damage to do by default.
* * chance_to_crit: The percent chance of a critical hit occuring. Default: 0
* * forced_crit_case: If given a value from crushing.dm, [target] and it's contents will always be hit with that specific critical hit. Default: null
* * paralyze_time: The time, in deciseconds, a given mob/living will be paralyzed for if crushed.
* * crush_dir: The direction the crush is coming from. Default: dir of src to [target].
* * damage_type: The type of damage to do. Default: BRUTE
* * damage_flag: The attack flag for armor purposes. Default: MELEE
* * rotation: The angle of which to rotate src's transform by on a successful tilt. Default: 90.
*
* Returns: A collection of bitflags defined in crushing.dm. Read that file's documentation for info.
*/
/atom/movable/proc/fall_and_crush(turf/target, damage, chance_to_crit = 0, forced_crit_case = null, paralyze_time, crush_dir = get_dir(get_turf(src), target), damage_type = BRUTE, damage_flag = MELEE, rotation = 90)
ASSERT(!isnull(target))
var/flags_to_return = NONE
if (!target.is_blocked_turf(TRUE, src, list(src)))
for(var/atom/atom_target in (target.contents) + target)
if (isarea(atom_target))
continue
if (SEND_SIGNAL(atom_target, COMSIG_PRE_TILT_AND_CRUSH, src) & COMPONENT_IMMUNE_TO_TILT_AND_CRUSH)
continue
var/crit_case = forced_crit_case
if (isnull(crit_case) && chance_to_crit > 0)
if (prob(chance_to_crit))
crit_case = pick_weight(get_crit_crush_chances())
var/crit_rebate_mult = 1 // lessen the normal damage we deal for some of the crits
if (!isnull(crit_case))
crit_rebate_mult = fall_and_crush_crit_rebate_table(crit_case)
apply_crit_crush(crit_case, atom_target)
var/adjusted_damage = damage * crit_rebate_mult
var/crushed
if (isliving(atom_target))
crushed = TRUE
var/mob/living/carbon/living_target = atom_target
var/was_alive = (living_target.stat != DEAD)
var/blocked = living_target.run_armor_check(attack_flag = damage_flag)
if (iscarbon(living_target))
var/mob/living/carbon/carbon_target = living_target
if(prob(30))
carbon_target.apply_damage(max(0, adjusted_damage), damage_type, blocked = blocked, forced = TRUE, spread_damage = TRUE, attack_direction = crush_dir) // the 30% chance to spread the damage means you escape breaking any bones
else
var/brute = (damage_type == BRUTE ? damage : 0) * 0.5
var/burn = (damage_type == BURN ? damage : 0) * 0.5
carbon_target.take_bodypart_damage(brute, burn, check_armor = TRUE, wound_bonus = 5) // otherwise, deal it to 2 random limbs (or the same one) which will likely shatter something
carbon_target.take_bodypart_damage(brute, burn, check_armor = TRUE, wound_bonus = 5)
carbon_target.AddElement(/datum/element/squish, 80 SECONDS)
else
living_target.apply_damage(adjusted_damage, damage_type, blocked = blocked, forced = TRUE, attack_direction = crush_dir)
living_target.Paralyze(paralyze_time)
living_target.emote("scream")
playsound(living_target, 'sound/effects/blobattack.ogg', 40, TRUE)
playsound(living_target, 'sound/effects/splat.ogg', 50, TRUE)
post_crush_living(living_target, was_alive)
flags_to_return |= (SUCCESSFULLY_CRUSHED_MOB|SUCCESSFULLY_CRUSHED_ATOM)
else if (atom_target.uses_integrity && !(atom_target.invisibility > SEE_INVISIBLE_LIVING) && !(is_type_in_typecache(atom_target, GLOB.WALLITEMS_INTERIOR) || is_type_in_typecache(atom_target, GLOB.WALLITEMS_EXTERIOR)))
atom_target.take_damage(adjusted_damage, damage_type, damage_flag, FALSE, crush_dir)
crushed = TRUE
flags_to_return |= SUCCESSFULLY_CRUSHED_ATOM
if (crushed)
atom_target.visible_message(span_danger("[atom_target] is crushed by [src]!"), span_userdanger("You are crushed by [src]!"))
SEND_SIGNAL(atom_target, COMSIG_POST_TILT_AND_CRUSH, src)
var/matrix/to_turn = turn(transform, rotation)
animate(src, transform = to_turn, 0.2 SECONDS)
playsound(src, 'sound/effects/bang.ogg', 40)
visible_message(span_danger("[src] tips over, slamming hard onto [target]!"))
flags_to_return |= SUCCESSFULLY_FELL_OVER
post_tilt()
else
visible_message(span_danger("[src] rebounds comically as it fails to slam onto [target]!"))
Move(target, crush_dir) // we still TRY to move onto it for shit like teleporters
return flags_to_return
/**
* Exists for the purposes of custom behavior.
* Called directly after [crushed] is crushed.
*
* Args:
* * mob/living/crushed: The mob that was crushed.
* * was_alive: Boolean. True if the mob was alive before the crushing.
*/
/atom/movable/proc/post_crush_living(mob/living/crushed, was_alive)
return
/**
* Exists for the purposes of custom behavior.
* Called directly after src actually rotates and falls over.
*/
/atom/movable/proc/post_tilt()
return
/obj/machinery/vending/post_crush_living(mob/living/crushed, was_alive)
if(was_alive && crushed.stat == DEAD && crushed.client)
crushed.client.give_award(/datum/award/achievement/misc/vendor_squish, crushed) // good job losing a fight with an inanimate object idiot
add_memory_in_range(crushed, 7, /datum/memory/witness_vendor_crush, protagonist = crushed, antagonist = src)
return ..()
/**
* Allows damage to be reduced on certain crit cases.
* Args:
* * crit_case: The critical case chosen.
*/
/atom/movable/proc/fall_and_crush_crit_rebate_table(crit_case)
ASSERT(!isnull(crit_case))
switch(crit_case)
if (CRUSH_CRIT_SHATTER_LEGS)
return 0.2
else
return 1
/obj/machinery/vending/fall_and_crush_crit_rebate_table(crit_case)
if (crit_case == VENDOR_CRUSH_CRIT_GLASSCANDY)
return 0.33
return ..()
/**
* Returns a assoc list of (critcase -> num), where critcase is a critical define in crushing.dm and num is a weight.
* Use with pickweight to acquire a random critcase.
*/
/atom/movable/proc/get_crit_crush_chances()
RETURN_TYPE(/list)
var/list/weighted_crits = list()
weighted_crits[CRUSH_CRIT_SHATTER_LEGS] = 100
weighted_crits[CRUSH_CRIT_PARAPALEGIC] = 80
weighted_crits[CRUSH_CRIT_HEADGIB] = 20
weighted_crits[CRUSH_CRIT_SQUISH_LIMB] = 100
return weighted_crits
/obj/machinery/vending/get_crit_crush_chances()
var/list/weighted_crits = ..()
weighted_crits[VENDOR_CRUSH_CRIT_GLASSCANDY] = 100
weighted_crits[VENDOR_CRUSH_CRIT_PIN] = 100
return weighted_crits
/**
* Should be where critcase effects are actually implemented. Use this to apply critcases.
* Args:
* * crit_case: The chosen critcase, defined in crushing.dm.
* * atom/atom_target: The target to apply the critical hit to. Cannot be null. Can be anything except /area.
*
* Returns:
* TRUE if a crit case is successfully applied, FALSE otherwise.
*/
/atom/movable/proc/apply_crit_crush(crit_case, atom/atom_target)
switch (crit_case)
if(CRUSH_CRIT_SHATTER_LEGS) // shatter their legs and bleed 'em
if (!iscarbon(atom_target))
return FALSE
var/mob/living/carbon/carbon_target = atom_target
carbon_target.bleed(150)
var/obj/item/bodypart/leg/left/left_leg = carbon_target.get_bodypart(BODY_ZONE_L_LEG)
if(left_leg)
left_leg.receive_damage(brute = 200)
var/obj/item/bodypart/leg/right/right_leg = carbon_target.get_bodypart(BODY_ZONE_R_LEG)
if(right_leg)
right_leg.receive_damage(brute = 200)
if(left_leg || right_leg)
carbon_target.visible_message(span_danger("[carbon_target]'s legs shatter with a sickening crunch!"), span_userdanger("Your legs shatter with a sickening crunch!"))
return TRUE
if(CRUSH_CRIT_PARAPALEGIC) // paralyze this binch
// the new paraplegic gets like 4 lines of losing their legs so skip them
if (!iscarbon(atom_target))
return FALSE
var/mob/living/carbon/carbon_target = atom_target
visible_message(span_danger("[carbon_target]'s spinal cord is obliterated with a sickening crunch!"), ignored_mobs = list(carbon_target))
carbon_target.gain_trauma(/datum/brain_trauma/severe/paralysis/paraplegic)
return TRUE
if(CRUSH_CRIT_SQUISH_LIMB) // limb squish!
if (!iscarbon(atom_target))
return FALSE
var/mob/living/carbon/carbon_target = atom_target
for(var/obj/item/bodypart/squish_part in carbon_target.bodyparts)
if(IS_ORGANIC_LIMB(squish_part))
var/type_wound = pick(list(/datum/wound/blunt/critical, /datum/wound/blunt/severe, /datum/wound/blunt/moderate))
squish_part.force_wound_upwards(type_wound, wound_source = "crushed by [src]")
else
squish_part.receive_damage(brute=30)
carbon_target.visible_message(span_danger("[carbon_target]'s body is maimed underneath the mass of [src]!"), span_userdanger("Your body is maimed underneath the mass of [src]!"))
return TRUE
if(CRUSH_CRIT_HEADGIB) // skull squish!
if (!iscarbon(atom_target))
return FALSE
var/mob/living/carbon/carbon_target = atom_target
var/obj/item/bodypart/head/carbon_head = carbon_target.get_bodypart(BODY_ZONE_HEAD)
if(carbon_head)
if(carbon_head.dismember())
carbon_target.visible_message(span_danger("[carbon_head] explodes in a shower of gore beneath [src]!"), span_userdanger("Oh f-"))
carbon_head.drop_organs()
qdel(carbon_head)
new /obj/effect/gibspawner/human/bodypartless(get_turf(carbon_target))
return TRUE
return FALSE
/obj/machinery/vending/apply_crit_crush(crit_case, atom_target)
. = ..()
if (.)
return TRUE
switch (crit_case)
if (VENDOR_CRUSH_CRIT_GLASSCANDY)
if (!iscarbon(atom_target))
return FALSE
var/mob/living/carbon/carbon_target = atom_target
for(var/i in 1 to num_shards)
var/obj/item/shard/shard = new /obj/item/shard(get_turf(carbon_target))
shard.embedding = list(embed_chance = 100, ignore_throwspeed_threshold = TRUE, impact_pain_mult = 1, pain_chance = 5)
shard.updateEmbedding()
carbon_target.hitby(shard, skipcatch = TRUE, hitpush = FALSE)
shard.embedding = list()
shard.updateEmbedding()
return TRUE
if (VENDOR_CRUSH_CRIT_PIN) // pin them beneath the machine until someone untilts it
if (!isliving(atom_target))
return FALSE
var/mob/living/living_target = atom_target
forceMove(get_turf(living_target))
buckle_mob(living_target, force=TRUE)
living_target.visible_message(span_danger("[living_target] is pinned underneath [src]!"), span_userdanger("You are pinned down by [src]!"))
return TRUE
return FALSE
/**
* Rights the vendor up, unpinning mobs under it, if any.
* Arguments:
* user - mob that has untilted the vendor
*/
/obj/machinery/vending/proc/untilt(mob/user)
if(user)
user.visible_message(span_notice("[user] rights [src]."), \
span_notice("You right [src]."))
unbuckle_all_mobs(TRUE)
tilted = FALSE
layer = initial(layer)
SET_PLANE_IMPLICIT(src, initial(plane))
var/matrix/to_turn = turn(transform, -tilted_rotation)
animate(src, transform = to_turn, 0.2 SECONDS)
tilted_rotation = 0
/**
* Tries to insert the item into the vendor, and depending on whether the product is a part of the vendor's
* stock or not, increments an already present product entry's available amount or creates a new entry.
* arguments:
* inserted_item - the item we're trying to insert
* user - mob who's trying to insert the item
*/
/obj/machinery/vending/proc/loadingAttempt(obj/item/inserted_item, mob/user)
. = TRUE
if(!user.transferItemToLoc(inserted_item, src))
return FALSE
to_chat(user, span_notice("You insert [inserted_item] into [src]'s input compartment."))
for(var/datum/data/vending_product/product_datum in product_records + coin_records + hidden_records)
if(ispath(inserted_item.type, product_datum.product_path))
product_datum.amount++
LAZYADD(product_datum.returned_products, inserted_item)
return
if(vending_machine_input[format_text(inserted_item.name)])
vending_machine_input[format_text(inserted_item.name)]++
else
vending_machine_input[format_text(inserted_item.name)] = 1
loaded_items++
/obj/machinery/vending/unbuckle_mob(mob/living/buckled_mob, force = FALSE, can_fall = TRUE)
if(!force)
return
. = ..()
/**
* Is the passed in user allowed to load this vending machines compartments? This only is ran if we are using a /obj/item/storage/bag to load the vending machine, and not a dedicated restocker.
*
* Arguments:
* * user - mob that is doing the loading of the vending machine
*/
/obj/machinery/vending/proc/compartmentLoadAccessCheck(mob/user)
if(!req_access || allowed(user) || (obj_flags & EMAGGED) || !scan_id)
return TRUE
to_chat(user, span_warning("[src]'s input compartment blinks red: Access denied."))
return FALSE
/obj/machinery/vending/exchange_parts(mob/user, obj/item/storage/part_replacer/replacer)
if(!istype(replacer))
return FALSE
if((flags_1 & NODECONSTRUCT_1) && !replacer.works_from_distance)
return FALSE
if(!component_parts || !refill_canister)
return FALSE
var/moved = 0
if(panel_open || replacer.works_from_distance)
if(replacer.works_from_distance)
display_parts(user)
for(var/replacer_item in replacer)
if(istype(replacer, refill_canister))
moved += restock(replacer_item)
else
display_parts(user)
if(moved)
to_chat(user, span_notice("[moved] items restocked."))
replacer.play_rped_sound()
return TRUE
/obj/machinery/vending/on_deconstruction()
update_canister()
. = ..()
/obj/machinery/vending/emag_act(mob/user, obj/item/card/emag/emag_card)
if(obj_flags & EMAGGED)
return FALSE
obj_flags |= EMAGGED
balloon_alert(user, "product lock disabled")
return TRUE
/obj/machinery/vending/interact(mob/user)
if (!isAI(user))
if(seconds_electrified && !(machine_stat & NOPOWER))
if(shock(user, 100))
return
if(tilted && !user.buckled && !isAdminGhostAI(user))
to_chat(user, span_notice("You begin righting [src]."))
if(do_after(user, 50, target=src))
untilt(user)
return
return ..()
/obj/machinery/vending/attack_robot_secondary(mob/user, list/modifiers)
. = ..()
if (!Adjacent(user, src))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/machinery/vending/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/spritesheet/vending),
)
/obj/machinery/vending/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Vending", name)
ui.open()
/obj/machinery/vending/ui_static_data(mob/user)
var/list/data = list()
data["onstation"] = onstation
data["department"] = payment_department
data["jobDiscount"] = DEPARTMENT_DISCOUNT
data["product_records"] = list()
data["displayed_currency_icon"] = displayed_currency_icon
data["displayed_currency_name"] = displayed_currency_name
var/list/categories = list()
data["product_records"] = collect_records_for_static_data(product_records, categories)
data["coin_records"] = collect_records_for_static_data(coin_records, categories, premium = TRUE)
data["hidden_records"] = collect_records_for_static_data(hidden_records, categories, premium = TRUE)
data["categories"] = categories
return data
/**
* Returns a list of given product records of the vendor to be used in UI.
* arguments:
* records - list of records available
* categories - list of categories available
* premium - bool of whether a record should be priced by a custom/premium price or not
*/
/obj/machinery/vending/proc/collect_records_for_static_data(list/records, list/categories, premium)
var/static/list/default_category = list(
"name" = "Products",
"icon" = "cart-shopping",
)
var/list/out_records = list()
for (var/datum/data/vending_product/record as anything in records)
var/list/static_record = list(
path = replacetext(replacetext("[record.product_path]", "/obj/item/", ""), "/", "-"),
name = record.name,
price = premium ? (record.custom_premium_price || extra_price) : (record.custom_price || default_price),
max_amount = record.max_amount,
ref = REF(record),
)
var/list/category = record.category || default_category
if (!isnull(category))
if (!(category["name"] in categories))
categories[category["name"]] = list(
"icon" = category["icon"],
)
static_record["category"] = category["name"]
if (premium)
static_record["premium"] = TRUE
out_records += list(static_record)
return out_records
/obj/machinery/vending/ui_data(mob/user)
. = list()
var/obj/item/card/id/card_used
if(isliving(user))
var/mob/living/living_user = user
card_used = living_user.get_idcard(TRUE)
if(card_used?.registered_account)
.["user"] = list()
.["user"]["name"] = card_used.registered_account.account_holder
.["user"]["cash"] = fetch_balance_to_use(card_used)
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
else
.["user"]["job"] = "No Job"
.["user"]["department"] = DEPARTMENT_UNASSIGNED
.["stock"] = list()
for (var/datum/data/vending_product/product_record as anything in product_records + coin_records + hidden_records)
var/list/product_data = list(
name = product_record.name,
amount = product_record.amount,
colorable = product_record.colorable,
)
.["stock"][product_record.name] = product_data
.["extended_inventory"] = extended_inventory
/obj/machinery/vending/ui_act(action, params)
. = ..()
if(.)
return
switch(action)
if("vend")
. = vend(params)
if("select_colors")
. = select_colors(params)
/**
* Whether this vendor can vend items or not.
* arguments:
* user - current customer
*/
/obj/machinery/vending/proc/can_vend(user)
. = FALSE
if(!vend_ready)
return
if(panel_open)
to_chat(user, span_warning("The vending machine cannot dispense products while its service panel is open!"))
return
return TRUE
/**
* Brings up a color config menu for the picked greyscaled item
*/
/obj/machinery/vending/proc/select_colors(list/params)
. = TRUE
if(!can_vend(usr))
return
var/datum/data/vending_product/product = locate(params["ref"])
var/atom/fake_atom = product.product_path
var/list/allowed_configs = list()
var/config = initial(fake_atom.greyscale_config)
if(!config)
return
allowed_configs += "[config]"
if(ispath(fake_atom, /obj/item))
var/obj/item/item = fake_atom
if(initial(item.greyscale_config_worn))
allowed_configs += "[initial(item.greyscale_config_worn)]"
if(initial(item.greyscale_config_inhand_left))
allowed_configs += "[initial(item.greyscale_config_inhand_left)]"
if(initial(item.greyscale_config_inhand_right))
allowed_configs += "[initial(item.greyscale_config_inhand_right)]"
var/datum/greyscale_modify_menu/menu = new(
src, usr, allowed_configs, CALLBACK(src, PROC_REF(vend_greyscale), params),
starting_icon_state=initial(fake_atom.icon_state),
starting_config=initial(fake_atom.greyscale_config),
starting_colors=initial(fake_atom.greyscale_colors)
)
menu.ui_interact(usr)
/**
* Vends a greyscale modified item.
* arguments:
* menu - greyscale config menu that has been used to vend the item
*/
/obj/machinery/vending/proc/vend_greyscale(list/params, datum/greyscale_modify_menu/menu)
if(usr != menu.user)
return
vend(params, menu.split_colors)
/**
* The entire shebang of vending the picked item. Processes the vending and initiates the payment for the item.
* arguments:
* greyscale_colors - greyscale config for the item we're about to vend, if any
*/
/obj/machinery/vending/proc/vend(list/params, list/greyscale_colors)
. = TRUE
if(!can_vend(usr))
return
vend_ready = FALSE //One thing at a time!!
var/datum/data/vending_product/item_record = locate(params["ref"])
var/list/record_to_check = product_records + coin_records
if(extended_inventory)
record_to_check = product_records + coin_records + hidden_records
if(!item_record || !istype(item_record) || !item_record.product_path)
vend_ready = TRUE
return
var/price_to_use = default_price
if(item_record.custom_price)
price_to_use = item_record.custom_price
if(item_record in hidden_records)
if(!extended_inventory)
vend_ready = TRUE
return
else if (!(item_record in record_to_check))
vend_ready = TRUE
message_admins("Vending machine exploit attempted by [ADMIN_LOOKUPFLW(usr)]!")
return
if (item_record.amount <= 0)
speak("Sold out of [item_record.name].")
flick(icon_deny,src)
vend_ready = TRUE
return
if(onstation)
var/obj/item/card/id/card_used
if(isliving(usr))
var/mob/living/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))
if (isnull(sec_radio))
sec_radio = new (src)
sec_radio.set_listening(FALSE)
sec_radio.set_frequency(FREQ_SECURITY)
sec_radio.talk_into(src, "SECURITY ALERT: Underaged crewmember [usr] recorded attempting to purchase [item_record.name] in [get_area(src)]. Please watch for substance abuse.", FREQ_SECURITY)
GLOB.narcd_underages += usr
flick(icon_deny,src)
vend_ready = TRUE
return
if(!proceed_payment(card_used, item_record, price_to_use))
return
if(last_shopper != REF(usr) || purchase_message_cooldown < world.time)
var/vend_response = vend_reply || "Thank you for shopping with [src]!"
speak(vend_response)
purchase_message_cooldown = world.time + 5 SECONDS
//This is not the best practice, but it's safe enough here since the chances of two people using a machine with the same ref in 5 seconds is fuck low
last_shopper = REF(usr)
use_power(active_power_usage)
if(icon_vend) //Show the vending animation if needed
flick(icon_vend,src)
playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3)
var/obj/item/vended_item
if(!LAZYLEN(item_record.returned_products)) //always give out free returned stuff first, e.g. to avoid walling a traitor objective in a bag behind paid items
vended_item = new item_record.product_path(get_turf(src))
else
vended_item = LAZYACCESS(item_record.returned_products, LAZYLEN(item_record.returned_products)) //first in, last out
LAZYREMOVE(item_record.returned_products, vended_item)
vended_item.forceMove(get_turf(src))
if(greyscale_colors)
vended_item.set_greyscale(colors=greyscale_colors)
item_record.amount--
if(usr.CanReach(src) && usr.put_in_hands(vended_item))
to_chat(usr, span_notice("You take [item_record.name] out of the slot."))
else
to_chat(usr, span_warning("[capitalize(item_record.name)] falls onto the floor!"))
SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[item_record.product_path]"))
vend_ready = TRUE
/**
* Returns the balance that the vendor will use for proceeding payment. Most vendors would want to use the user's
* card's account credits balance.
* arguments:
* passed_id - the id card that will be billed for the product
*/
/obj/machinery/vending/proc/fetch_balance_to_use(obj/item/card/id/passed_id)
return passed_id.registered_account.account_balance
/**
* 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
*/
/obj/machinery/vending/proc/proceed_payment(obj/item/card/id/paying_id_card, 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.
if(coin_records.Find(product_to_vend) || hidden_records.Find(product_to_vend))
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]"))
speak("You do not possess the funds to purchase [product_to_vend.name].")
flick(icon_deny,src)
vend_ready = TRUE
return FALSE
//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].")
return TRUE
/obj/machinery/vending/process(seconds_per_tick)
if(machine_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 && SPT_PROB(2.5, seconds_per_tick))
var/slogan = pick(slogan_list)
speak(slogan)
last_slogan = world.time
if(shoot_inventory && SPT_PROB(shoot_inventory_chance, seconds_per_tick))
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(machine_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/record in shuffle(product_records))
if(record.amount <= 0) //Try to use a record that actually has something to dump.
continue
var/dump_path = record.product_path
if(!dump_path)
continue
if(record.amount > LAZYLEN(record.returned_products)) //always throw new stuff that costs before free returned stuff, because of the hacking effort and time between throws involved
throw_item = new dump_path(loc)
else
throw_item = LAZYACCESS(record.returned_products, LAZYLEN(record.returned_products)) //first in, last out
throw_item.forceMove(loc)
LAZYREMOVE(record.returned_products, throw_item)
record.amount--
break
if(!throw_item)
return FALSE
pre_throw(throw_item)
throw_item.throw_at(target, 16, 3)
visible_message(span_danger("[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:
* * thrown_item - obj/item being thrown
*/
/obj/machinery/vending/proc/pre_throw(obj/item/thrown_item)
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
* * shock_chance - probability the shock happens
*/
/obj/machinery/vending/proc/shock(mob/living/user, shock_chance)
if(!istype(user) || machine_stat & (BROKEN|NOPOWER)) // unpowered, no shock
return FALSE
if(!prob(shock_chance))
return FALSE
do_sparks(5, TRUE, src)
if(electrocute_mob(user, get_area(src), src, 0.7, dist_check = TRUE))
return TRUE
else
return FALSE
/**
* Are we able to load the item passed in
*
* Arguments:
* * loaded_item - the item being loaded
* * user - the user doing the loading
*/
/obj/machinery/vending/proc/canLoadItem(obj/item/loaded_item, mob/user)
if((loaded_item.type in products) || (loaded_item.type in premium) || (loaded_item.type in contraband))
return TRUE
to_chat(user, span_warning("[src] does not accept [loaded_item]!"))
return FALSE
/obj/machinery/vending/hitby(atom/movable/hitting_atom, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
. = ..()
var/mob/living/living_mob = hitting_atom
if(tilted || !istype(living_mob) || !prob(20 * (throwingdatum.speed - living_mob.throw_speed))) // hulk throw = +20%, neckgrab throw = +20%
return
tilt(living_mob)
/obj/machinery/vending/attack_tk_grab(mob/user)
to_chat(user, span_warning("[src] seems to resist your mental grasp!"))
///Crush the mob that the vending machine got thrown at
/obj/machinery/vending/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
if(isliving(hit_atom))
tilt(fatty=hit_atom)
return ..()
/obj/machinery/vending/custom
name = "Custom Vendor"
icon_state = "custom"
icon_deny = "custom-deny"
max_integrity = 400
payment_department = NO_FREEBIES
light_mask = "custom-light-mask"
refill_canister = /obj/item/vending_refill/custom
/// where the money is sent
var/datum/bank_account/linked_account
/// max number of items that the custom vendor can hold
var/max_loaded_items = 20
/// Base64 cache of custom icons.
var/list/base64_cache = list()
panel_type = "panel20"
/obj/machinery/vending/custom/compartmentLoadAccessCheck(mob/user)
. = FALSE
if(!isliving(user))
return FALSE
var/mob/living/living_user = user
var/obj/item/card/id/id_card = living_user.get_idcard(FALSE)
if(id_card?.registered_account && id_card.registered_account == linked_account)
return TRUE
/obj/machinery/vending/custom/canLoadItem(obj/item/loaded_item, mob/user)
. = FALSE
if(loaded_item.flags_1 & HOLOGRAM_1)
speak("This vendor cannot accept nonexistent items.")
return
if(loaded_items >= max_loaded_items)
speak("There are too many items in stock.")
return
if(isstack(loaded_item))
speak("Loose items may cause problems, try to use it inside wrapping paper.")
return
if(loaded_item.custom_price)
return TRUE
/obj/machinery/vending/custom/ui_interact(mob/user)
if(!linked_account)
balloon_alert(user, "no registered owner!")
return FALSE
return ..()
/obj/machinery/vending/custom/ui_data(mob/user)
. = ..()
.["access"] = compartmentLoadAccessCheck(user)
.["vending_machine_input"] = list()
for (var/stocked_item in vending_machine_input)
if(vending_machine_input[stocked_item] > 0)
var/base64
var/price = 0
for(var/obj/item/stored_item in contents)
if(format_text(stored_item.name) == stocked_item)
price = stored_item.custom_price
if(!base64) //generate an icon of the item to use in UI
if(base64_cache[stored_item.type])
base64 = base64_cache[stored_item.type]
else
base64 = icon2base64(getFlatIcon(stored_item, no_anim=TRUE))
base64_cache[stored_item.type] = base64
break
var/list/data = list(
name = stocked_item,
price = price,
img = base64,
amount = vending_machine_input[stocked_item],
colorable = FALSE
)
.["vending_machine_input"] += list(data)
/obj/machinery/vending/custom/ui_act(action, params)
. = ..()
if(.)
return
switch(action)
if("dispense")
if(isliving(usr))
vend_act(usr, params["item"])
vend_ready = TRUE
return TRUE
/obj/machinery/vending/custom/attackby(obj/item/attack_item, mob/user, params)
if(!linked_account && isliving(user))
var/mob/living/living_user = user
var/obj/item/card/id/card_used = living_user.get_idcard(TRUE)
if(card_used?.registered_account)
linked_account = card_used.registered_account
speak("\The [src] has been linked to [card_used].")
if(compartmentLoadAccessCheck(user))
if(istype(attack_item, /obj/item/pen))
name = tgui_input_text(user, "Set name", "Name", name, 20)
desc = tgui_input_text(user, "Set description", "Description", desc, 60)
slogan_list += tgui_input_text(user, "Set slogan", "Slogan", "Epic", 60)
last_slogan = world.time + rand(0, slogan_delay)
return
return ..()
/obj/machinery/vending/custom/crowbar_act(mob/living/user, obj/item/attack_item)
return FALSE
/obj/machinery/vending/custom/deconstruct(disassembled)
unbuckle_all_mobs(TRUE)
var/turf/current_turf = get_turf(src)
if(current_turf)
for(var/obj/item/stored_item in contents)
stored_item.forceMove(current_turf)
explosion(src, devastation_range = -1, light_impact_range = 3)
return ..()
/**
* Vends an item to the user. Handles all the logic:
* Updating stock, account transactions, alerting users.
* @return -- TRUE if a valid condition was met, FALSE otherwise.
*/
/obj/machinery/vending/custom/proc/vend_act(mob/living/user, choice)
if(!vend_ready)
return
var/obj/item/dispensed_item
var/obj/item/card/id/id_card = user.get_idcard(TRUE)
vend_ready = FALSE
if(!id_card || !id_card.registered_account || !id_card.registered_account.account_job)
balloon_alert(usr, "no card found!")
flick(icon_deny, src)
return TRUE
var/datum/bank_account/payee = id_card.registered_account
for(var/obj/stock in contents)
if(format_text(stock.name) == choice)
dispensed_item = stock
break
if(!dispensed_item)
return FALSE
/// Charges the user if its not the owner
if(!compartmentLoadAccessCheck(user))
if(!payee.has_money(dispensed_item.custom_price))
balloon_alert(user, "insufficient funds!")
return TRUE
/// Make the transaction
payee.adjust_money(-dispensed_item.custom_price, , "Vending: [dispensed_item]")
linked_account.adjust_money(dispensed_item.custom_price, "Vending: [dispensed_item] Bought")
linked_account.bank_card_talk("[payee.account_holder] made a [dispensed_item.custom_price] \
cr purchase at your custom vendor.")
/// Log the transaction
SSblackbox.record_feedback("amount", "vending_spent", dispensed_item.custom_price)
log_econ("[dispensed_item.custom_price] credits were spent on [src] buying a \
[dispensed_item] by [payee.account_holder], owned by [linked_account.account_holder].")
/// Make an alert
if(last_shopper != REF(usr) || purchase_message_cooldown < world.time)
speak("Thank you for your patronage [user]!")
purchase_message_cooldown = world.time + 5 SECONDS
last_shopper = REF(usr)
/// Remove the item
loaded_items--
use_power(active_power_usage)
vending_machine_input[choice] = max(vending_machine_input[choice] - 1, 0)
if(user.CanReach(src) && user.put_in_hands(dispensed_item))
to_chat(user, span_notice("You take [dispensed_item.name] out of the slot."))
else
to_chat(user, span_warning("[capitalize(dispensed_item.name)] falls onto the floor!"))
return TRUE
/obj/machinery/vending/custom/unbreakable
name = "Indestructible Vendor"
resistance_flags = INDESTRUCTIBLE
/obj/item/vending_refill/custom
machine_name = "Custom Vendor"
icon_state = "refill_custom"
custom_premium_price = PAYCHECK_CREW
/obj/machinery/vending/custom/greed //name and like decided by the spawn
icon_state = "greed"
icon_deny = "greed-deny"
panel_type = "panel4"
max_integrity = 700
max_loaded_items = 40
light_mask = "greed-light-mask"
custom_materials = list(/datum/material/gold = SHEET_MATERIAL_AMOUNT * 5)
/obj/machinery/vending/custom/greed/Initialize(mapload)
. = ..()
//starts in a state where you can move it
set_panel_open(TRUE)
set_anchored(FALSE)
add_overlay(panel_type)
//and references the deity
name = "[GLOB.deity]'s Consecrated Vendor"
desc = "A vending machine created by [GLOB.deity]."
slogan_list = list("[GLOB.deity] says: It's your divine right to buy!")
add_filter("vending_outline", 9, list("type" = "outline", "color" = COLOR_VERY_SOFT_YELLOW))
add_filter("vending_rays", 10, list("type" = "rays", "size" = 35, "color" = COLOR_VIVID_YELLOW))
#undef MAX_VENDING_INPUT_AMOUNT