mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-09 16:05:07 +00:00
General maintenance for vending machines (#91987)
## About The Pull Request **1. Code Improvements** - Removed unused vars `coin`, `bill` & other stuff - Removed duplicate definition of `on_deconstruction()` - Autodoc for a lot of procs - Merged smaller procs into larger ones to avoid scrolling in the code editor & reduced overhead - Split the vending machine file into several smaller files for easy code management **2. Qol** - Implemented vending machine ads. They now display random stuff on the UI https://github.com/user-attachments/assets/9720ea60-f268-4ca2-940d-243e3d0ac75f - More error messages for custom & normal vendors as to why an item could not be loaded - Custom vending machines can be deconstructed safely via crowbar without any explosion only after unlinking your account from the machine else you get the same explosion. Upon deconstruction all loaded items are moved into its restock canister meaning the machine can be safely moved with all its products just like a regular vending machine to a new location **3. Fixes** - Fixes #81917. Any returned items in the vending machine now show up as free in the UI & won't cost credits to buy them - Fixes #87416. Custom & normal vendors now keep track of products removed via `Exited()` so the UI gets always updated - Fixes #83151. Items with different names & custom prices now show up in unique rows - Fixes #92170 Custom vendors now show the correct icon for inserted items - Closes #80010. From the above fix this situation is impossible so it's safe to close this as a duplicate - Closes #78016 same problem as above with `Exited()` duplicate - Custom vendors can now actually be used by players who are not the owner instead of locking down the UI - Vending machines keep track of `max_amount` of stocked items by hand as well & not just RPED **4. Refactor** - Separates custom vending machine code from normal vending machine code. This prime Marely focus on the `vending_machine_input` list which now only exists inside the custom vending machine - Compressed the UI code for vending machine so both custom & normal vending machines can send the same data instead of separating the 2. Overall less code - Moved attack chain from `attackby()` to `item_interaction()` for loading items ## Changelog 🆑 code: cleaned up vending machine code qol: vending machines now have more product slogans you never heard before qol: custom & normal vending machines now have more feedback on why an item could not be loaded qol: vending machines now display random ads on the UI qol: custom vending machines can be deconstructed via crowbar safely only after unlinking your account from the machine. qol: upon deconstructing a custom vendor all its products are moved into its refill canister & it will be restored when reconstructing the machine elsewhere fix: Returned items to the vending machine now show up as free in the UI and won't be greyed out if you don't have credits to get them back fix: items that leave the vending machine by any means will update the UI in all cases fix: loading items by hand to the vending machine now respects the max_amount for that category fix: custom vendors can now actually be used by players who are not the owner thus enabling them to transfer credits to the owner during purchases & basically they do their job again fix: custom vendors now show the correct icon for inserted items fix: Items with different names & custom prices now show up in unique rows in custom vendors refactor: separated custom & normal vending machine code. Reduced UI code & improved attack chain /🆑 --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
This commit is contained in:
@@ -247,6 +247,32 @@ SUBSYSTEM_DEF(economy)
|
||||
var/obj/machinery/vending/vending = prices_to_update[i]
|
||||
vending.reset_prices(vending.product_records, vending.coin_records + vending.hidden_records)
|
||||
|
||||
/**
|
||||
* 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/inflation_value = HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING) ? SSeconomy.inflation_value() : 1
|
||||
default_price = round(initial(default_price) * inflation_value)
|
||||
extra_price = round(initial(extra_price) * inflation_value)
|
||||
|
||||
for(var/datum/data/vending_product/record as anything in recordlist)
|
||||
var/obj/item/potential_product = record.product_path
|
||||
var/custom_price = round(initial(potential_product.custom_price) * inflation_value)
|
||||
record.price = custom_price | default_price
|
||||
for(var/datum/data/vending_product/premium_record as anything in premiumlist)
|
||||
var/obj/item/potential_product = premium_record.product_path
|
||||
var/premium_custom_price = round(initial(potential_product.custom_premium_price) * inflation_value)
|
||||
var/custom_price = initial(potential_product.custom_price)
|
||||
if(!premium_custom_price && custom_price) //For some ungodly reason, some premium only items only have a custom_price
|
||||
premium_record.price = extra_price + round(custom_price * inflation_value)
|
||||
else
|
||||
premium_record.price = premium_custom_price || extra_price
|
||||
|
||||
/datum/controller/subsystem/economy/proc/inflict_moneybags(datum/bank_account/moneybags)
|
||||
if(!moneybags)
|
||||
return FALSE
|
||||
|
||||
@@ -93,3 +93,24 @@
|
||||
vending_machine.scan_id = mend
|
||||
if(WIRE_SPEAKER)
|
||||
vending_machine.shut_up = mend
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
return FALSE
|
||||
|
||||
@@ -161,11 +161,11 @@
|
||||
acid = 70
|
||||
|
||||
///Needed by machine frame & flatpacker i.e the named arg board
|
||||
/obj/machinery/New(loc, obj/item/circuitboard/board, ...)
|
||||
/obj/machinery/New(location, obj/item/circuitboard/board, ...)
|
||||
if(istype(board))
|
||||
circuit = board
|
||||
//we don't want machines that override Initialize() have the board passed as a param e.g. atmos
|
||||
return ..(loc)
|
||||
return ..(location)
|
||||
|
||||
return ..()
|
||||
|
||||
|
||||
@@ -672,14 +672,21 @@
|
||||
valid_vendor_names_paths[vendor_type::name] = vendor_type
|
||||
|
||||
/obj/item/circuitboard/machine/vendor/screwdriver_act(mob/living/user, obj/item/tool)
|
||||
. = ITEM_INTERACT_FAILURE
|
||||
var/choice = tgui_input_list(user, "Choose a new brand", "Select an Item", sort_list(valid_vendor_names_paths))
|
||||
if(isnull(choice))
|
||||
return
|
||||
if(isnull(valid_vendor_names_paths[choice]))
|
||||
if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
|
||||
return
|
||||
set_type(valid_vendor_names_paths[choice])
|
||||
return TRUE
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
|
||||
/**
|
||||
* Sets circuitboard details based on the vending machine type to create
|
||||
*
|
||||
* Arguments
|
||||
* * obj/machinery/vending/typepath - the vending machine type to create
|
||||
*/
|
||||
/obj/item/circuitboard/machine/vendor/proc/set_type(obj/machinery/vending/typepath)
|
||||
build_path = typepath
|
||||
name = "[typepath::name] Vendor"
|
||||
@@ -687,11 +694,7 @@
|
||||
flatpack_components = list(initial(typepath.refill_canister))
|
||||
|
||||
/obj/item/circuitboard/machine/vendor/apply_default_parts(obj/machinery/machine)
|
||||
for(var/key in valid_vendor_names_paths)
|
||||
// == instead of istype so subtypes don't pass check for their supertypes
|
||||
if(machine.type == valid_vendor_names_paths[key])
|
||||
set_type(valid_vendor_names_paths[key])
|
||||
break
|
||||
set_type(machine.type)
|
||||
return ..()
|
||||
|
||||
/obj/item/circuitboard/machine/vending/donksofttoyvendor
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
*/
|
||||
/obj/item/vending_refill
|
||||
name = "resupply canister"
|
||||
var/machine_name = "Generic"
|
||||
|
||||
icon = 'icons/obj/vending_restock.dmi'
|
||||
icon_state = "refill_snack"
|
||||
inhand_icon_state = "restock_unit"
|
||||
@@ -19,13 +17,14 @@
|
||||
w_class = WEIGHT_CLASS_BULKY
|
||||
armor_type = /datum/armor/item_vending_refill
|
||||
|
||||
/**
|
||||
* Built automatically from the corresponding vending machine.
|
||||
* If null, considered to be full. Otherwise, is list(/typepath = amount).
|
||||
*/
|
||||
///Name of the vending machine this canister is associated with
|
||||
var/machine_name = "Generic"
|
||||
|
||||
///corresponds to /obj/machinery/vending::list/products
|
||||
var/list/products
|
||||
var/list/product_categories
|
||||
///corresponds to /obj/machinery/vending::list/contraband
|
||||
var/list/contraband
|
||||
///corresponds to /obj/machinery/vending::list/premium
|
||||
var/list/premium
|
||||
|
||||
/datum/armor/item_vending_refill
|
||||
@@ -36,19 +35,25 @@
|
||||
. = ..()
|
||||
name = "\improper [machine_name] restocking unit"
|
||||
|
||||
if(istype(loc, /obj/machinery/vending))
|
||||
var/obj/machinery/vending/vendor = loc
|
||||
products = vendor.products.Copy()
|
||||
contraband = vendor.contraband.Copy()
|
||||
premium = vendor.premium.Copy()
|
||||
else
|
||||
products = list()
|
||||
contraband = list()
|
||||
premium = list()
|
||||
|
||||
/obj/item/vending_refill/examine(mob/user)
|
||||
. = ..()
|
||||
var/num = get_part_rating()
|
||||
if (num == INFINITY)
|
||||
. += "It's sealed tight, completely full of supplies."
|
||||
else if (num == 0)
|
||||
if (!num)
|
||||
. += "It's empty!"
|
||||
else
|
||||
. += "It can restock [num] item\s."
|
||||
|
||||
/obj/item/vending_refill/get_part_rating()
|
||||
if (!products || !product_categories || !contraband || !premium)
|
||||
return INFINITY
|
||||
. = 0
|
||||
for(var/key in products)
|
||||
. += products[key]
|
||||
@@ -57,9 +62,3 @@
|
||||
for(var/key in premium)
|
||||
. += premium[key]
|
||||
|
||||
for (var/list/category as anything in product_categories)
|
||||
var/list/products = category["products"]
|
||||
for (var/product_key in products)
|
||||
. += products[product_key]
|
||||
|
||||
return .
|
||||
|
||||
@@ -385,19 +385,24 @@
|
||||
/datum/fish_source/vending/custom
|
||||
catalog_description = null //no duplicate entries on autowiki or catalog
|
||||
|
||||
/datum/fish_source/vending/custom/get_vending_table(obj/item/fishing_rod/rod, mob/fisherman, obj/machinery/vending/location)
|
||||
/datum/fish_source/vending/custom/get_vending_table(obj/item/fishing_rod/rod, mob/fisherman, obj/machinery/vending/custom/location)
|
||||
var/list/table = list()
|
||||
///Create a list of products, ordered by price from highest to lowest
|
||||
var/list/products = location.vending_machine_input.Copy()
|
||||
var/list/products = location.contents - location.component_parts
|
||||
sortTim(products, GLOBAL_PROC_REF(cmp_item_vending_prices))
|
||||
|
||||
var/bait_value = rod.bait?.get_item_credit_value() || 1
|
||||
|
||||
var/highest_record_price = 0
|
||||
for(var/obj/item/stocked as anything in products)
|
||||
if(location.vending_machine_input[stocked] <= 0)
|
||||
products -= stocked
|
||||
table[FISHING_DUD] += PAYCHECK_LOWER //it gets harder the emptier the machine is
|
||||
//count how many items of this type are in the machine
|
||||
var/item_count = 1
|
||||
for(var/obj/item/thing as anything in products)
|
||||
if(stocked.type == thing.type)
|
||||
item_count += 1
|
||||
//find what percentage of the total storage space this item occupies.
|
||||
if(ROUND_UP((item_count / location.max_loaded_items) * 100) <= 20)
|
||||
table[FISHING_DUD] += PAYCHECK_LOWER //it gets harder if it occupies less than 20% of the available space i.e the more free space is inside this machine
|
||||
continue
|
||||
if(!highest_record_price)
|
||||
highest_record_price = stocked.custom_price
|
||||
@@ -405,7 +410,7 @@
|
||||
var/low = min(highest_record_price, bait_value)
|
||||
|
||||
//the smaller the difference between product price and bait value, the more likely you're to get it.
|
||||
table[stocked] = low/high * 1000 //multiply the value by 1000 for accuracy. pick_weight() doesn't work with zero decimals yet.
|
||||
table[stocked] = (low / high) * 1000 //multiply the value by 1000 for accuracy. pick_weight() doesn't work with zero decimals yet.
|
||||
|
||||
add_risks(table, bait_value, highest_record_price, length(products) * 0.5)
|
||||
return table
|
||||
|
||||
@@ -1,25 +1,4 @@
|
||||
//lavaland_surface_syndicate_base1.dmm and it's modules
|
||||
|
||||
/obj/machinery/vending/syndichem
|
||||
name = "\improper SyndiChem"
|
||||
desc = "A vending machine full of grenades and grenade accessories. Sponsored by Donk Co."
|
||||
products = list(/obj/item/stack/cable_coil = 5,
|
||||
/obj/item/assembly/igniter = 20,
|
||||
/obj/item/assembly/prox_sensor = 5,
|
||||
/obj/item/assembly/signaler = 5,
|
||||
/obj/item/assembly/timer = 5,
|
||||
/obj/item/assembly/voice = 5,
|
||||
/obj/item/assembly/health = 5,
|
||||
/obj/item/assembly/infra = 5,
|
||||
/obj/item/grenade/chem_grenade = 5,
|
||||
/obj/item/grenade/chem_grenade/large = 5,
|
||||
/obj/item/grenade/chem_grenade/pyro = 5,
|
||||
/obj/item/grenade/chem_grenade/cryo = 5,
|
||||
/obj/item/grenade/chem_grenade/adv_release = 5,
|
||||
/obj/item/reagent_containers/cup/glass/bottle/holywater = 1)
|
||||
product_slogans = "It's not pyromania if you're getting paid!;You smell that? Plasma, son. Nothing else in the world smells like that.;I love the smell of Plasma in the morning."
|
||||
resistance_flags = FIRE_PROOF
|
||||
|
||||
/obj/modular_map_root/syndicatebase
|
||||
config_file = "strings/modular_maps/syndicatebase.toml"
|
||||
|
||||
|
||||
@@ -14,16 +14,17 @@
|
||||
var/list/data = list()
|
||||
var/list/vending_list = list()
|
||||
var/id_increment = 1
|
||||
for(var/obj/machinery/vending/vendor as anything in GLOB.vending_machines_to_restock)
|
||||
var/stock = vendor.total_loaded_stock()
|
||||
var/max_stock = vendor.total_max_stock()
|
||||
if((max_stock == 0 || (stock >= max_stock)) && vendor.credits_contained == 0)
|
||||
for(var/obj/machinery/vending/vendor as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/vending))
|
||||
if(vendor.all_products_free)
|
||||
continue
|
||||
var/list/total_legal_stock = vendor.total_stock(contrabrand = FALSE)
|
||||
if((!total_legal_stock[2] || (total_legal_stock[1] >= total_legal_stock[2])) && !vendor.credits_contained)
|
||||
continue
|
||||
vending_list += list(list(
|
||||
"name" = vendor.name,
|
||||
"location" = get_area_name(vendor),
|
||||
"credits" = vendor.credits_contained,
|
||||
"percentage" = (stock / max_stock) * 100,
|
||||
"percentage" = (total_legal_stock[1] / total_legal_stock[2]) * 100,
|
||||
"id" = id_increment,
|
||||
))
|
||||
id_increment++
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
354
code/modules/vending/custom.dm
Normal file
354
code/modules/vending/custom.dm
Normal file
@@ -0,0 +1,354 @@
|
||||
///This unique key decides how items are stacked on the UI. We separate them based on name,price & type
|
||||
#define ITEM_HASH(item)("[item.name][item.custom_price][item.type]")
|
||||
|
||||
/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"
|
||||
panel_type = "panel20"
|
||||
allow_custom = TRUE
|
||||
refill_canister = /obj/item/vending_refill/custom
|
||||
fish_source_path = /datum/fish_source/vending/custom
|
||||
|
||||
/// max number of items that the custom vendor can hold
|
||||
var/max_loaded_items = 20
|
||||
/// where the money is sent
|
||||
VAR_PRIVATE/datum/bank_account/linked_account
|
||||
/// Base64 cache of custom icons.
|
||||
VAR_PRIVATE/static/list/base64_cache = list()
|
||||
|
||||
/obj/machinery/vending/custom/on_deconstruction(disassembled)
|
||||
var/obj/item/vending_refill/custom/installed_refill = locate() in component_parts
|
||||
|
||||
if(linked_account)
|
||||
//we delete the canister so players don't resell our products as their own
|
||||
component_parts -= installed_refill
|
||||
qdel(installed_refill)
|
||||
|
||||
//self destruct protocol for unauthorized destruction
|
||||
explosion(get_turf(src), devastation_range = -1, light_impact_range = 3)
|
||||
|
||||
return
|
||||
|
||||
//copy product hash keys
|
||||
installed_refill.products.Cut()
|
||||
installed_refill.products += products
|
||||
|
||||
//move products to canister
|
||||
for(var/obj/item/stored_item in contents - component_parts)
|
||||
stored_item.forceMove(installed_refill)
|
||||
|
||||
/obj/machinery/vending/custom/add_context(atom/source, list/context, obj/item/held_item, mob/user)
|
||||
if(panel_open && istype(held_item, refill_canister))
|
||||
context[SCREENTIP_CONTEXT_LMB] = "Restock vending machine"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
|
||||
if(isliving(user) && istype(held_item, /obj/item/card/id))
|
||||
var/obj/item/card/id/card_used = held_item
|
||||
if(card_used?.registered_account)
|
||||
if(!linked_account)
|
||||
context[SCREENTIP_CONTEXT_LMB] = "Link account"
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
else if(linked_account == card_used.registered_account)
|
||||
context[SCREENTIP_CONTEXT_LMB] = "Unlink account"
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
|
||||
return ..()
|
||||
|
||||
/obj/machinery/vending/custom/examine(mob/user)
|
||||
. = ..()
|
||||
if(linked_account)
|
||||
. += span_warning("Machine is ID locked. Be sure to unlink before deconstructing the machine.")
|
||||
|
||||
/obj/machinery/vending/custom/Exited(obj/item/gone, direction)
|
||||
. = ..()
|
||||
|
||||
var/hash_key = ITEM_HASH(gone)
|
||||
if(products[hash_key])
|
||||
var/new_amount = products[hash_key] - 1
|
||||
if(!new_amount)
|
||||
products -= hash_key
|
||||
update_static_data_for_all_viewers()
|
||||
else
|
||||
products[hash_key] = new_amount
|
||||
|
||||
///Returns the number of products loaded in this machine
|
||||
/obj/machinery/vending/custom/proc/loaded_items()
|
||||
PRIVATE_PROC(TRUE)
|
||||
SHOULD_BE_PURE(TRUE)
|
||||
|
||||
. = 0
|
||||
for(var/product_hash in products)
|
||||
. += products[product_hash]
|
||||
|
||||
/obj/machinery/vending/custom/canLoadItem(obj/item/loaded_item, mob/user, send_message = TRUE)
|
||||
if(loaded_item.flags_1 & HOLOGRAM_1)
|
||||
if(send_message)
|
||||
speak("This vendor cannot accept nonexistent items.")
|
||||
return FALSE
|
||||
if(isstack(loaded_item))
|
||||
if(send_message)
|
||||
speak("Loose items may cause problems, try to use it inside wrapping paper.")
|
||||
return FALSE
|
||||
if(!loaded_item.custom_price)
|
||||
if(send_message)
|
||||
speak("Item needs to have a custom price set.")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/obj/machinery/vending/custom/loadingAttempt(obj/item/inserted_item, mob/user)
|
||||
if(!canLoadItem(inserted_item, user))
|
||||
return FALSE
|
||||
|
||||
if(loaded_items() == max_loaded_items)
|
||||
speak("There are too many items in stock.")
|
||||
return FALSE
|
||||
|
||||
if(!user.transferItemToLoc(inserted_item, src))
|
||||
to_chat(user, span_warning("[inserted_item] is stuck in your hand!"))
|
||||
return FALSE
|
||||
|
||||
//the hash key decides how items stack in the UI. We diffrentiate them based on name & price
|
||||
var/hash_key = ITEM_HASH(inserted_item)
|
||||
if(products[hash_key])
|
||||
products[hash_key]++
|
||||
else
|
||||
products[hash_key] = 1
|
||||
update_static_data_for_all_viewers()
|
||||
return TRUE
|
||||
|
||||
/obj/machinery/vending/custom/RefreshParts()
|
||||
SHOULD_CALL_PARENT(FALSE)
|
||||
|
||||
restock(locate(refill_canister) in component_parts)
|
||||
|
||||
/obj/machinery/vending/custom/restock(obj/item/vending_refill/canister)
|
||||
. = 0
|
||||
if(!canister.products?.len)
|
||||
return
|
||||
|
||||
var/update_static_data = FALSE
|
||||
var/available_load = max_loaded_items - loaded_items()
|
||||
for(var/product_hash in canister.products)
|
||||
//get available space
|
||||
var/load_count = min(canister.products[product_hash], available_load)
|
||||
if(!load_count)
|
||||
break
|
||||
//update canister record
|
||||
canister.products[product_hash] -= load_count
|
||||
if(!canister.products[product_hash])
|
||||
canister.products -= product_hash
|
||||
//update vendor record
|
||||
products[product_hash] += load_count
|
||||
//reduce from available space
|
||||
available_load -= load_count
|
||||
|
||||
//update product
|
||||
for(var/obj/item/product in canister)
|
||||
if(!load_count)
|
||||
break
|
||||
if(ITEM_HASH(product) == product_hash)
|
||||
. += 1
|
||||
product.forceMove(src)
|
||||
load_count--
|
||||
|
||||
if(update_static_data)
|
||||
update_static_data_for_all_viewers()
|
||||
|
||||
|
||||
/obj/machinery/vending/custom/post_restock(mob/living/user, restocked)
|
||||
if(!restocked)
|
||||
to_chat(user, span_warning("There's nothing to restock!"))
|
||||
return
|
||||
|
||||
to_chat(user, span_notice("You loaded [restocked] items in [src]"))
|
||||
|
||||
/obj/machinery/vending/custom/crowbar_act(mob/living/user, obj/item/attack_item)
|
||||
if(linked_account)
|
||||
visible_message(
|
||||
span_warning("Security warning"),
|
||||
span_warning("Unauthorized deconstruction of vending machine is prohibited. Please read the warning alert")
|
||||
)
|
||||
if(tgui_alert(user, "Vending machine is ID locked.\
|
||||
Deconstruction will result in an catrostrophic self destruct.\
|
||||
If you are the owner of this machine please unlink your account with an ID swipe before proceeding.\
|
||||
Still proceed?",
|
||||
"Vandalism protection protocol",
|
||||
list("Yes", "No")) == "No")
|
||||
return ITEM_INTERACT_FAILURE
|
||||
|
||||
return ..()
|
||||
|
||||
/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/item_interaction(mob/living/user, obj/item/attack_item, list/modifiers)
|
||||
if(isliving(user) && istype(attack_item, /obj/item/card/id))
|
||||
var/obj/item/card/id/card_used = attack_item
|
||||
if(card_used?.registered_account)
|
||||
if(!linked_account)
|
||||
linked_account = card_used.registered_account
|
||||
speak("\The [src] has been linked to [card_used].")
|
||||
else if(linked_account == card_used.registered_account)
|
||||
linked_account = null
|
||||
speak("account unlinked.")
|
||||
else
|
||||
to_chat(user, "verification failed. unlinking process has been cancelled.")
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
|
||||
if(!compartmentLoadAccessCheck(user) || !IS_WRITING_UTENSIL(attack_item))
|
||||
return ..()
|
||||
|
||||
. ITEM_INTERACT_FAILURE
|
||||
var/new_name = reject_bad_name(tgui_input_text(user, "Set name", "Name", name, max_length = 20), allow_numbers = TRUE, strict = TRUE, cap_after_symbols = FALSE)
|
||||
if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
|
||||
return
|
||||
if (new_name)
|
||||
name = new_name
|
||||
var/new_desc = reject_bad_text(tgui_input_text(user, "Set description", "Description", desc, max_length = 60))
|
||||
if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
|
||||
return
|
||||
if (new_desc)
|
||||
desc = new_desc
|
||||
var/new_slogan = reject_bad_text(tgui_input_text(user, "Set slogan", "Slogan", "Epic", max_length = 60))
|
||||
if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
|
||||
return
|
||||
if (new_slogan)
|
||||
slogan_list += new_slogan
|
||||
last_slogan = world.time + rand(0, slogan_delay)
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
|
||||
/obj/machinery/vending/custom/collect_records_for_static_data(list/records, list/categories, premium)
|
||||
. = list()
|
||||
if(records != product_records) //no coin or hidden stuff only product records
|
||||
return
|
||||
|
||||
categories["Products"] = list("icon" = "cart-shopping")
|
||||
for(var/stocked_hash in products)
|
||||
var/base64 = ""
|
||||
var/obj/item/target = null
|
||||
for(var/obj/item/stored_item in contents - component_parts)
|
||||
if(ITEM_HASH(stored_item) == stocked_hash)
|
||||
base64 = base64_cache[stocked_hash]
|
||||
if(!base64) //generate an icon of the item to use in UI
|
||||
base64 = icon2base64(getFlatIcon(stored_item, no_anim = TRUE))
|
||||
base64_cache[stocked_hash] = base64
|
||||
target = stored_item
|
||||
break
|
||||
|
||||
. += list(list(
|
||||
path = stocked_hash,
|
||||
name = target.name,
|
||||
price = target.custom_price,
|
||||
category = "Products",
|
||||
ref = stocked_hash,
|
||||
colorable = FALSE,
|
||||
image = base64
|
||||
))
|
||||
|
||||
|
||||
/obj/machinery/vending/custom/ui_interact(mob/user, datum/tgui/ui)
|
||||
if(!linked_account)
|
||||
balloon_alert(user, "no registered owner!")
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/obj/machinery/vending/custom/ui_data(mob/user)
|
||||
. = ..()
|
||||
|
||||
var/is_owner = compartmentLoadAccessCheck(user)
|
||||
|
||||
.["stock"] = list()
|
||||
for(var/stocked_hash in products)
|
||||
.["stock"][stocked_hash] = list(
|
||||
amount = products[stocked_hash],
|
||||
free = is_owner
|
||||
)
|
||||
|
||||
/obj/machinery/vending/custom/vend(list/params, mob/living/user, list/greyscale_colors)
|
||||
. = FALSE
|
||||
if(!isliving(user))
|
||||
return
|
||||
var/obj/item/dispensed_item = params["ref"]
|
||||
for(var/obj/item/product in contents - component_parts)
|
||||
if(ITEM_HASH(product) == dispensed_item)
|
||||
dispensed_item = product
|
||||
break
|
||||
if(QDELETED(dispensed_item))
|
||||
return
|
||||
|
||||
var/obj/item/card/id/id_card = user.get_idcard(TRUE)
|
||||
if(!id_card || !id_card.registered_account || !id_card.registered_account.account_job)
|
||||
balloon_alert(user, "no card found!")
|
||||
flick(icon_deny, src)
|
||||
return
|
||||
|
||||
/// Charges the user if its not the owner
|
||||
var/datum/bank_account/payee = id_card.registered_account
|
||||
if(!compartmentLoadAccessCheck(user))
|
||||
if(!payee.has_money(dispensed_item.custom_price))
|
||||
balloon_alert(user, "insufficient funds!")
|
||||
return
|
||||
/// 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
|
||||
var/ref = REF(user)
|
||||
if(last_shopper != ref || purchase_message_cooldown < world.time)
|
||||
speak("Thank you for your patronage [user]!")
|
||||
purchase_message_cooldown = world.time + 5 SECONDS
|
||||
last_shopper = ref
|
||||
|
||||
/// Remove the item
|
||||
use_energy(active_power_usage)
|
||||
try_put_in_hand(dispensed_item, user)
|
||||
return TRUE
|
||||
|
||||
/obj/item/vending_refill/custom
|
||||
machine_name = "Custom Vendor"
|
||||
icon_state = "refill_custom"
|
||||
custom_premium_price = PAYCHECK_CREW
|
||||
|
||||
/obj/machinery/vending/custom/unbreakable
|
||||
name = "Indestructible Vendor"
|
||||
resistance_flags = INDESTRUCTIBLE
|
||||
allow_custom = FALSE
|
||||
|
||||
/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"
|
||||
allow_custom = FALSE
|
||||
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_anchored(FALSE)
|
||||
set_panel_open(TRUE)
|
||||
//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 ITEM_HASH
|
||||
@@ -15,19 +15,18 @@
|
||||
var/type_to_vend = /obj/item/food/grown/citrus
|
||||
|
||||
/obj/machinery/vending/subtype_vendor/Initialize(mapload, type_to_vend)
|
||||
. = ..()
|
||||
if(type_to_vend)
|
||||
src.type_to_vend = type_to_vend
|
||||
load_subtypes()
|
||||
|
||||
/obj/machinery/vending/subtype_vendor/proc/load_subtypes()
|
||||
products = list()
|
||||
product_records = list()
|
||||
return ..()
|
||||
|
||||
///Adds the subtype to the product list
|
||||
/obj/machinery/vending/subtype_vendor/RefreshParts()
|
||||
products.Cut()
|
||||
for(var/type in typesof(type_to_vend))
|
||||
LAZYADDASSOC(products, type, 50)
|
||||
|
||||
build_inventories()
|
||||
//no refill canister so we fill the records with their max amounts directly
|
||||
build_inventories(start_empty = FALSE)
|
||||
|
||||
/obj/machinery/vending/subtype_vendor/attack_hand_secondary(mob/user, list/modifiers)
|
||||
. = ..()
|
||||
@@ -48,5 +47,5 @@
|
||||
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
|
||||
|
||||
type_to_vend = type_to_vend_now
|
||||
load_subtypes()
|
||||
RefreshParts()
|
||||
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
|
||||
|
||||
@@ -27,6 +27,18 @@
|
||||
payment_department = NO_FREEBIES
|
||||
allow_custom = TRUE
|
||||
|
||||
/obj/machinery/vending/sustenance/interact(mob/living/living_user)
|
||||
if(!isliving(living_user))
|
||||
return
|
||||
if(!istype(living_user.get_idcard(TRUE), /obj/item/card/id/advanced/prisoner))
|
||||
if(!req_access)
|
||||
speak("No valid prisoner account found. Vending is not permitted.")
|
||||
return
|
||||
if(!allowed(living_user))
|
||||
speak("No valid permissions. Vending is not permitted.")
|
||||
return
|
||||
return ..()
|
||||
|
||||
/obj/item/vending_refill/sustenance
|
||||
machine_name = "Sustenance Vendor"
|
||||
icon_state = "refill_snack"
|
||||
@@ -42,33 +54,15 @@
|
||||
displayed_currency_name = " LP"
|
||||
allow_custom = FALSE
|
||||
|
||||
/obj/machinery/vending/sustenance/interact(mob/user)
|
||||
if(!isliving(user))
|
||||
return ..()
|
||||
var/mob/living/living_user = user
|
||||
if(!is_operational)
|
||||
to_chat(user, span_warning("Machine does not respond to your ID swipe"))
|
||||
return
|
||||
if(!istype(living_user.get_idcard(TRUE), /obj/item/card/id/advanced/prisoner))
|
||||
if(!req_access)
|
||||
speak("No valid prisoner account found. Vending is not permitted.")
|
||||
return
|
||||
if(!allowed(user))
|
||||
speak("No valid permissions. Vending is not permitted.")
|
||||
return
|
||||
return ..()
|
||||
|
||||
/obj/machinery/vending/sustenance/labor_camp/proceed_payment(obj/item/card/id/paying_id_card, mob/living/mob_paying, datum/data/vending_product/product_to_vend, price_to_use)
|
||||
if(!istype(paying_id_card, /obj/item/card/id/advanced/prisoner))
|
||||
/obj/machinery/vending/sustenance/labor_camp/proceed_payment(obj/item/card/id/advanced/prisoner/paying_scum_id, mob/living/mob_paying, datum/data/vending_product/product_to_vend, price_to_use)
|
||||
if(!istype(paying_scum_id))
|
||||
speak("I don't take bribes! Pay with labor points!")
|
||||
return FALSE
|
||||
var/obj/item/card/id/advanced/prisoner/paying_scum_id = paying_id_card
|
||||
if(LAZYLEN(product_to_vend.returned_products))
|
||||
price_to_use = 0 //returned items are free
|
||||
if(price_to_use && !(paying_scum_id.points >= price_to_use)) //not enough good prisoner points
|
||||
speak("You do not possess enough points to purchase [product_to_vend.name].")
|
||||
flick(icon_deny, src)
|
||||
vend_ready = TRUE
|
||||
return FALSE
|
||||
|
||||
paying_scum_id.points -= price_to_use
|
||||
|
||||
21
code/modules/vending/syndichem.dm
Normal file
21
code/modules/vending/syndichem.dm
Normal file
@@ -0,0 +1,21 @@
|
||||
/obj/machinery/vending/syndichem
|
||||
name = "\improper SyndiChem"
|
||||
desc = "A vending machine full of grenades and grenade accessories. Sponsored by Donk Co."
|
||||
products = list(
|
||||
/obj/item/stack/cable_coil = 5,
|
||||
/obj/item/assembly/igniter = 20,
|
||||
/obj/item/assembly/prox_sensor = 5,
|
||||
/obj/item/assembly/signaler = 5,
|
||||
/obj/item/assembly/timer = 5,
|
||||
/obj/item/assembly/voice = 5,
|
||||
/obj/item/assembly/health = 5,
|
||||
/obj/item/assembly/infra = 5,
|
||||
/obj/item/grenade/chem_grenade = 5,
|
||||
/obj/item/grenade/chem_grenade/large = 5,
|
||||
/obj/item/grenade/chem_grenade/pyro = 5,
|
||||
/obj/item/grenade/chem_grenade/cryo = 5,
|
||||
/obj/item/grenade/chem_grenade/adv_release = 5,
|
||||
/obj/item/reagent_containers/cup/glass/bottle/holywater = 1
|
||||
)
|
||||
product_slogans = "It's not pyromania if you're getting paid!;You smell that? Plasma, son. Nothing else in the world smells like that.;I love the smell of Plasma in the morning."
|
||||
resistance_flags = FIRE_PROOF
|
||||
477
code/modules/vending/vendor/_vending.dm
vendored
Normal file
477
code/modules/vending/vendor/_vending.dm
vendored
Normal file
@@ -0,0 +1,477 @@
|
||||
///Maximum credits dump threshold
|
||||
#define CREDITS_DUMP_THRESHOLD 50
|
||||
/**
|
||||
* # 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
|
||||
///The price of the item
|
||||
var/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
|
||||
|
||||
/datum/data/vending_product/Destroy(force)
|
||||
returned_products = null
|
||||
return ..()
|
||||
|
||||
/**
|
||||
* # 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"
|
||||
|
||||
///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
|
||||
///List of mobs stuck under the vendor
|
||||
var/list/pinned_mobs = list()
|
||||
///Icon for the maintenance panel overlay
|
||||
var/panel_type = "panel1"
|
||||
///Whether this vendor can be selected when building a custom vending machine
|
||||
var/allow_custom = FALSE
|
||||
|
||||
/**
|
||||
* 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/datum/data/vending_product/product_records = list()
|
||||
///List of contraband product records
|
||||
var/list/datum/data/vending_product/hidden_records = list()
|
||||
///List of premium product records
|
||||
var/list/datum/data/vending_product/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()
|
||||
///List of ads built from product_ads upon Iniitialize()
|
||||
var/list/ad_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
|
||||
///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
|
||||
/// How many credits does this vending machine have? 20% of all sales go to this pool, and are given freely when the machine is restocked, or successfully tilted. Lost on deconstruction.
|
||||
var/credits_contained = 0
|
||||
/**
|
||||
* Is this item on station or not
|
||||
*
|
||||
* if it doesn't originate from off-station during mapload, all_products_free gets automatically set to TRUE if it was unset previously.
|
||||
* if it's off-station during mapload, it's also safe from the brand intelligence event
|
||||
*/
|
||||
var/onstation = TRUE
|
||||
/**
|
||||
* DO NOT APPLY THIS GLOBALLY. For mapping var edits only.
|
||||
* A variable to change on a per instance basis that allows the instance to avoid having onstation set for them during mapload.
|
||||
* Setting this to TRUE means that the vending machine is treated as if it were still onstation if it spawns off-station during mapload.
|
||||
* Useful to specify an off-station machine that will be affected by machine-brand intelligence for whatever reason.
|
||||
*/
|
||||
var/onstation_override = FALSE
|
||||
/**
|
||||
* If this is set to TRUE, all products sold by the vending machine are free (cost nothing).
|
||||
* If unset, this will get automatically set to TRUE during init if the machine originates from off-station during mapload.
|
||||
* Defaults to null, set it to TRUE or FALSE explicitly on a per-machine basis if you want to force it to be a certain value.
|
||||
*/
|
||||
var/all_products_free
|
||||
|
||||
//The type of refill canisters used by this machine.
|
||||
var/obj/item/vending_refill/refill_canister = null
|
||||
|
||||
///Name of lighting mask for the vending machine
|
||||
var/light_mask
|
||||
|
||||
//the path of the fish_source datum to use for the fishing_spot component
|
||||
var/fish_source_path = /datum/fish_source/vending
|
||||
|
||||
/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)
|
||||
//means we produce products with fixed amounts
|
||||
if(!refill_canister)
|
||||
circuit = null
|
||||
RefreshParts()
|
||||
|
||||
. = ..()
|
||||
|
||||
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]
|
||||
|
||||
slogan_list = splittext(product_slogans, ";")
|
||||
ad_list = splittext(product_ads, ";")
|
||||
// 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(isnull(all_products_free)) // Only auto-set the free products var if we haven't explicitly assigned a value to it yet.
|
||||
all_products_free = TRUE
|
||||
if(circuit)
|
||||
circuit.all_products_free = all_products_free //sync up the circuit so the pricing schema is carried over if it's reconstructed.
|
||||
|
||||
else if(circuit)
|
||||
all_products_free = circuit.all_products_free //if it was constructed outside mapload, sync the vendor up with the circuit's var so you can't bypass price requirements by moving / reconstructing it off station.
|
||||
if(!all_products_free)
|
||||
AddComponent(/datum/component/payment, 0, SSeconomy.get_dep_account(payment_department), PAYMENT_VENDING)
|
||||
register_context()
|
||||
|
||||
if(fish_source_path)
|
||||
AddComponent(/datum/component/fishing_spot, fish_source_path)
|
||||
|
||||
/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/i in 1 to LAZYLEN(record.returned_products))
|
||||
var/obj/item/returned_obj_to_dump = dispense(record, get_turf(src), dispense_returned = TRUE)
|
||||
step(returned_obj_to_dump, pick(GLOB.alldirs))
|
||||
|
||||
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
|
||||
// busting open a vendor will destroy some of the contents
|
||||
if(found_anything && prob(80))
|
||||
record.amount--
|
||||
continue
|
||||
|
||||
var/obj/obj_to_dump = dispense(record, loc)
|
||||
step(obj_to_dump, pick(GLOB.alldirs))
|
||||
found_anything = TRUE
|
||||
dump_amount++
|
||||
if (dump_amount >= 16)
|
||||
return
|
||||
|
||||
/obj/machinery/vending/on_deconstruction(disassembled)
|
||||
var/obj/item/vending_refill/installed_refill = locate() in component_parts
|
||||
if(!installed_refill)
|
||||
return
|
||||
|
||||
var/list/datum/data/vending_product/record_list
|
||||
var/list/canister_list
|
||||
for(var/i in 1 to 3)
|
||||
switch(i)
|
||||
if (1)
|
||||
record_list = product_records
|
||||
canister_list = installed_refill.products
|
||||
if (2)
|
||||
record_list = hidden_records
|
||||
canister_list = installed_refill.contraband
|
||||
else
|
||||
record_list = coin_records
|
||||
canister_list = installed_refill.premium
|
||||
|
||||
canister_list.Cut()
|
||||
for(var/datum/data/vending_product/record as anything in record_list)
|
||||
var/stock = record.amount - LAZYLEN(record.returned_products)
|
||||
if(stock)
|
||||
canister_list[record.product_path] = stock
|
||||
|
||||
/obj/machinery/vending/Destroy()
|
||||
QDEL_LIST(product_records)
|
||||
QDEL_LIST(hidden_records)
|
||||
QDEL_LIST(coin_records)
|
||||
return ..()
|
||||
|
||||
/obj/machinery/vending/add_context(atom/source, list/context, obj/item/held_item, mob/user)
|
||||
. = NONE
|
||||
if(tilted && !held_item)
|
||||
context[SCREENTIP_CONTEXT_LMB] = "Right machine"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
|
||||
if(held_item?.tool_behaviour == TOOL_SCREWDRIVER)
|
||||
context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] Panel"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
|
||||
if(panel_open && held_item?.tool_behaviour == TOOL_WRENCH)
|
||||
context[SCREENTIP_CONTEXT_LMB] = anchored ? "Unsecure" : "Secure"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
|
||||
if(panel_open && held_item?.tool_behaviour == TOOL_CROWBAR)
|
||||
context[SCREENTIP_CONTEXT_LMB] = "Deconstruct"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
|
||||
if(!isnull(held_item) && canLoadItem(held_item, user, send_message = FALSE))
|
||||
context[SCREENTIP_CONTEXT_LMB] = "Load item"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
|
||||
if(panel_open && istype(held_item, refill_canister))
|
||||
context[SCREENTIP_CONTEXT_LMB] = "Restock vending machine[credits_contained ? " and collect credits" : null]"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
|
||||
/**
|
||||
* Returns the total loaded & max amount of items i.e list(total_loaded, total_maximum) in the vending machine based on the product records and premium records
|
||||
*
|
||||
* Arguments
|
||||
* * contraband - should we count contrabrand as well
|
||||
*/
|
||||
/obj/machinery/vending/proc/total_stock(contrabrand = TRUE)
|
||||
SHOULD_BE_PURE(TRUE)
|
||||
SHOULD_NOT_OVERRIDE(TRUE)
|
||||
RETURN_TYPE(/list)
|
||||
|
||||
var/total_loaded = 0
|
||||
var/total_max = 0
|
||||
var/list/stock = product_records + coin_records
|
||||
if(contrabrand)
|
||||
stock += hidden_records
|
||||
for(var/datum/data/vending_product/record as anything in stock)
|
||||
total_loaded += record.amount
|
||||
total_max += record.max_amount
|
||||
return list(total_loaded, total_max)
|
||||
|
||||
/obj/machinery/vending/examine(mob/user)
|
||||
. = ..()
|
||||
if(isnull(refill_canister))
|
||||
return // you can add the comment here instead
|
||||
|
||||
. += span_notice("Its maintainence panel can be [EXAMINE_HINT("screwed")] [panel_open ? "closed" : "open"]")
|
||||
if(panel_open)
|
||||
. += span_notice("The machine may be [EXAMINE_HINT("pried")] apart.")
|
||||
|
||||
var/list/total_stock = total_stock()
|
||||
if(total_stock[2])
|
||||
if(total_stock[1] < total_stock[2])
|
||||
. += span_notice("\The [src] can be restocked with [span_boldnotice("\a [initial(refill_canister.machine_name)] [initial(refill_canister.name)]")] with the panel open.")
|
||||
else
|
||||
. += span_notice("\The [src] is fully stocked.")
|
||||
if(credits_contained < CREDITS_DUMP_THRESHOLD && credits_contained > 0)
|
||||
. += span_notice("It should have a handfull of credits stored based on the missing items.")
|
||||
else if (credits_contained > PAYCHECK_CREW)
|
||||
. += span_notice("It should have at least a full paycheck worth of credits inside!")
|
||||
|
||||
/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/vv_edit_var(vname, vval)
|
||||
. = ..()
|
||||
if (vname == NAMEOF(src, all_products_free))
|
||||
if (all_products_free)
|
||||
RemoveComponentSource(src, /datum/component/payment)
|
||||
else
|
||||
AddComponent(/datum/component/payment, 0, SSeconomy.get_dep_account(payment_department), PAYMENT_VENDING)
|
||||
|
||||
/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]
|
||||
|
||||
/obj/machinery/vending/unbuckle_mob(mob/living/buckled_mob, force = FALSE, can_fall = TRUE)
|
||||
if(!force)
|
||||
return
|
||||
. = ..()
|
||||
|
||||
/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/power_change()
|
||||
. = ..()
|
||||
if(powered())
|
||||
START_PROCESSING(SSmachines, src)
|
||||
|
||||
/obj/machinery/vending/process(seconds_per_tick)
|
||||
if(!is_operational)
|
||||
return PROCESS_KILL
|
||||
|
||||
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 && !shut_up && SPT_PROB(2.5, seconds_per_tick))
|
||||
say(pick(slogan_list))
|
||||
last_slogan = world.time
|
||||
|
||||
if(shoot_inventory && SPT_PROB(shoot_inventory_chance, seconds_per_tick))
|
||||
throw_item()
|
||||
|
||||
|
||||
//===============================SPEACH===================================================
|
||||
/obj/machinery/vending/can_speak(allow_mimes)
|
||||
return is_operational && !shut_up && ..()
|
||||
|
||||
|
||||
/**
|
||||
* 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(!is_operational)
|
||||
return
|
||||
if(!message)
|
||||
return
|
||||
|
||||
say(message)
|
||||
|
||||
/datum/aas_config_entry/vendomat_age_control
|
||||
name = "Security Alert: Underaged Substance Abuse"
|
||||
announcement_lines_map = list(
|
||||
"Message" = "SECURITY ALERT: Underaged crewmember %PERSON recorded attempting to purchase %PRODUCT in %LOCATION by %VENDOR. Please watch for substance abuse."
|
||||
)
|
||||
vars_and_tooltips_map = list(
|
||||
"PERSON" = "will be replaced with the name of the crewmember",
|
||||
"PRODUCT" = "with the product, he attempted to purchase",
|
||||
"LOCATION" = "with place of purchase",
|
||||
"VENDOR" = "with the vending machine"
|
||||
)
|
||||
//=============================================================================
|
||||
235
code/modules/vending/vendor/interaction.dm
vendored
Normal file
235
code/modules/vending/vendor/interaction.dm
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
/// Maximum amount of items in a storage bag that we're transferring items to the vendor from.
|
||||
#define MAX_VENDING_INPUT_AMOUNT 30
|
||||
|
||||
//================================TOOL ACTS==============================================
|
||||
/obj/machinery/vending/crowbar_act(mob/living/user, obj/item/attack_item)
|
||||
if(!component_parts)
|
||||
return ITEM_INTERACT_FAILURE
|
||||
default_deconstruction_crowbar(attack_item)
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
|
||||
/obj/machinery/vending/wrench_act(mob/living/user, obj/item/tool)
|
||||
. = NONE
|
||||
if(!panel_open)
|
||||
return ITEM_INTERACT_FAILURE
|
||||
if(default_unfasten_wrench(user, tool, time = 6 SECONDS))
|
||||
unbuckle_all_mobs(TRUE)
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
|
||||
/obj/machinery/vending/screwdriver_act(mob/living/user, obj/item/attack_item)
|
||||
if(anchored)
|
||||
default_deconstruction_screwdriver(user, icon_state, icon_state, attack_item)
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
else
|
||||
to_chat(user, span_warning("You must first secure [src]."))
|
||||
return ITEM_INTERACT_FAILURE
|
||||
|
||||
/obj/machinery/vending/on_set_panel_open(old_value)
|
||||
update_appearance(UPDATE_OVERLAYS)
|
||||
|
||||
//=======================================RESTOCKING==========================================
|
||||
/**
|
||||
* 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)
|
||||
PROTECTED_PROC(TRUE)
|
||||
|
||||
return !req_access || allowed(user) || (obj_flags & EMAGGED) || !scan_id
|
||||
|
||||
/**
|
||||
* 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, send_message = TRUE)
|
||||
PROTECTED_PROC(TRUE)
|
||||
|
||||
if(!length(loaded_item.contents) && (products[loaded_item.type] || premium[loaded_item.type] || contraband[loaded_item.type]))
|
||||
return TRUE
|
||||
if(send_message)
|
||||
to_chat(user, span_warning("[src] does not accept [loaded_item]!"))
|
||||
return FALSE
|
||||
|
||||
|
||||
/**
|
||||
* 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)
|
||||
PROTECTED_PROC(TRUE)
|
||||
|
||||
. = TRUE
|
||||
if(!canLoadItem(inserted_item, user))
|
||||
to_chat(user, span_warning("[src] does not accept [inserted_item]!"))
|
||||
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(inserted_item.type == product_datum.product_path)
|
||||
if(product_datum.amount == product_datum.max_amount)
|
||||
to_chat(user, span_warning("no space for any more [product_datum.category || "Products"]!"))
|
||||
return FALSE
|
||||
|
||||
if(!user.transferItemToLoc(inserted_item, src))
|
||||
to_chat(user, span_warning("[inserted_item] is stuck in your hand!"))
|
||||
return FALSE
|
||||
|
||||
product_datum.amount++
|
||||
LAZYADD(product_datum.returned_products, inserted_item)
|
||||
break
|
||||
|
||||
/obj/machinery/vending/item_interaction(mob/living/user, obj/item/attack_item, list/modifiers)
|
||||
. = NONE
|
||||
if(panel_open && is_wire_tool(attack_item))
|
||||
wires.interact(user)
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
|
||||
if(refill_canister && istype(attack_item, refill_canister))
|
||||
. = ITEM_INTERACT_FAILURE
|
||||
if (!panel_open)
|
||||
to_chat(user, span_warning("You should probably unscrew the service panel first!"))
|
||||
else if (!is_operational)
|
||||
to_chat(user, span_warning("[src] does not respond."))
|
||||
else
|
||||
var/obj/item/vending_refill/canister = attack_item
|
||||
if(canister.get_part_rating() == 0)
|
||||
to_chat(user, span_warning("[canister] is empty!"))
|
||||
else
|
||||
post_restock(user, restock(canister))
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
|
||||
if(compartmentLoadAccessCheck(user) && !user.combat_mode)
|
||||
. = ITEM_INTERACT_FAILURE
|
||||
if (!is_operational)
|
||||
to_chat(user, span_warning("[src] does not respond."))
|
||||
else 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(loadingAttempt(the_item, user))
|
||||
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."))
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
else
|
||||
return loadingAttempt(attack_item, user) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_FAILURE
|
||||
|
||||
/**
|
||||
* After-effects of refilling a vending machine from a refill canister
|
||||
*
|
||||
* This takes the amount of products restocked and gives the user our contained credits if needed,
|
||||
* sending the user a fitting message.
|
||||
*
|
||||
* Arguments:
|
||||
* * user - the user restocking us
|
||||
* * restocked - the amount of items we've been refilled with
|
||||
*/
|
||||
/obj/machinery/vending/proc/post_restock(mob/living/user, restocked)
|
||||
PROTECTED_PROC(TRUE)
|
||||
|
||||
if(!restocked)
|
||||
to_chat(user, span_warning("There's nothing to restock!"))
|
||||
return
|
||||
|
||||
to_chat(user, span_notice("You loaded [restocked] items in [src][credits_contained > 0 ? ", and are rewarded [credits_contained] credits." : "."]"))
|
||||
var/datum/bank_account/cargo_account = SSeconomy.get_dep_account(ACCOUNT_CAR)
|
||||
cargo_account.adjust_money(round(credits_contained * 0.5), "Vending: Restock")
|
||||
var/obj/item/holochip/payday = new(src, credits_contained)
|
||||
try_put_in_hand(payday, user)
|
||||
credits_contained = 0
|
||||
|
||||
/obj/machinery/vending/exchange_parts(mob/user, obj/item/storage/part_replacer/replacer)
|
||||
if(!istype(replacer) || !component_parts || !refill_canister)
|
||||
return FALSE
|
||||
|
||||
var/works_from_distance = istype(replacer, /obj/item/storage/part_replacer/bluespace)
|
||||
|
||||
if(!panel_open || works_from_distance)
|
||||
to_chat(user, display_parts(user))
|
||||
|
||||
if(!panel_open && !works_from_distance)
|
||||
return FALSE
|
||||
|
||||
var/restocked = 0
|
||||
for(var/replacer_item in replacer)
|
||||
if(istype(replacer_item, refill_canister))
|
||||
restocked += restock(replacer_item)
|
||||
post_restock(user, restocked)
|
||||
if(restocked > 0)
|
||||
replacer.play_rped_effect()
|
||||
return TRUE
|
||||
|
||||
//=======================================ATTACKS================================================
|
||||
/**
|
||||
* Dispenses free items from the standard stock.
|
||||
*
|
||||
* Arguments:
|
||||
* freebies - number of free items to vend
|
||||
*/
|
||||
/obj/machinery/vending/proc/freebie(freebies)
|
||||
PRIVATE_PROC(TRUE)
|
||||
|
||||
visible_message(span_notice("[src] yields [freebies > 1 ? "several free goodies" : "a free goody"][credits_contained > 0 ? " and some credits" : ""]!"))
|
||||
|
||||
for(var/i in 1 to freebies)
|
||||
playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3)
|
||||
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
|
||||
// Always give out new stuff that costs before free returned stuff, because of the risk getting gibbed involved
|
||||
var/only_returned_left = (record.amount <= LAZYLEN(record.returned_products))
|
||||
dispense(record, get_turf(src), silent = TRUE, dispense_returned = only_returned_left)
|
||||
break
|
||||
|
||||
if(credits_contained > 0)
|
||||
var/credits_to_remove = min(CREDITS_DUMP_THRESHOLD, round(credits_contained))
|
||||
var/obj/item/holochip/holochip = new(loc, credits_to_remove)
|
||||
playsound(src, 'sound/effects/cashregister.ogg', 40, TRUE)
|
||||
credits_contained = max(0, credits_contained - credits_to_remove)
|
||||
SSblackbox.record_feedback("amount", "vending machine looted", holochip.credits)
|
||||
|
||||
/obj/machinery/vending/attackby(obj/item/weapon, mob/user, list/modifiers, list/attack_modifiers)
|
||||
if(tiltable && !tilted && weapon.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.
|
||||
tilt(get_turf(get_step(src, REVERSE_DIR(get_dir(src, user)))))
|
||||
return TRUE
|
||||
|
||||
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)
|
||||
return TRUE
|
||||
return ..()
|
||||
|
||||
/obj/machinery/vending/attack_tk_grab(mob/user)
|
||||
to_chat(user, span_warning("[src] seems to resist your mental grasp!"))
|
||||
|
||||
/obj/machinery/vending/attack_robot_secondary(mob/user, list/modifiers)
|
||||
. = ..()
|
||||
if (!Adjacent(user, src))
|
||||
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
|
||||
|
||||
#undef MAX_VENDING_INPUT_AMOUNT
|
||||
277
code/modules/vending/vendor/inventory.dm
vendored
Normal file
277
code/modules/vending/vendor/inventory.dm
vendored
Normal file
@@ -0,0 +1,277 @@
|
||||
//================================STOCKING IN ITEMS=================================
|
||||
|
||||
/**
|
||||
* Build the inventory of the vending machine from its 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)
|
||||
* * premium - Whether the ending products shall have premium or default prices
|
||||
*/
|
||||
/obj/machinery/vending/proc/build_inventory(list/productlist, list/recordlist, list/categories, start_empty = FALSE, premium = FALSE)
|
||||
PRIVATE_PROC(TRUE)
|
||||
|
||||
var/inflation_value = HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING) ? SSeconomy.inflation_value() : 1
|
||||
default_price = round(initial(default_price) * inflation_value)
|
||||
extra_price = round(initial(extra_price) * inflation_value)
|
||||
|
||||
QDEL_LIST(recordlist)
|
||||
|
||||
var/list/product_to_category = list()
|
||||
for (var/list/category as anything in categories)
|
||||
for (var/product_key in category["products"])
|
||||
product_to_category[product_key] = category
|
||||
|
||||
for(var/typepath in productlist)
|
||||
var/amount = productlist[typepath]
|
||||
|
||||
var/obj/item/temp = typepath
|
||||
var/datum/data/vending_product/new_record = new
|
||||
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.
|
||||
var/custom_price = round(initial(temp.custom_price) * inflation_value)
|
||||
if(!premium)
|
||||
new_record.price = custom_price || default_price
|
||||
else
|
||||
var/premium_custom_price = round(initial(temp.custom_premium_price) * inflation_value)
|
||||
if(!premium_custom_price && custom_price) //For some ungodly reason, some premium only items only have a custom_price
|
||||
new_record.price = extra_price + custom_price
|
||||
else
|
||||
new_record.price = premium_custom_price || extra_price
|
||||
|
||||
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 = FALSE)
|
||||
build_inventory(products, product_records, product_categories, start_empty)
|
||||
build_inventory(contraband, hidden_records, list(list("name" = "Contraband", "icon" = "mask", "products" = contraband)), start_empty, premium = TRUE)
|
||||
build_inventory(premium, coin_records, list(list("name" = "Premium", "icon" = "coins", "products" = premium)), start_empty, premium = TRUE)
|
||||
|
||||
//Better would be to make constructable child
|
||||
/obj/machinery/vending/RefreshParts()
|
||||
SHOULD_CALL_PARENT(FALSE)
|
||||
|
||||
//compress all product categories into an linear list
|
||||
if(product_categories)
|
||||
products.Cut()
|
||||
for(var/list/category as anything in product_categories)
|
||||
products |= category["products"]
|
||||
|
||||
//locate canister
|
||||
var/obj/item/vending_refill/canister = refill_canister ? locate(refill_canister) in component_parts : null
|
||||
|
||||
//build the records, if we have a canister make the records empty so we can refill it from the canister else make it max amount
|
||||
build_inventories(start_empty = !isnull(canister))
|
||||
|
||||
//fill the records if we have an canister
|
||||
if(canister)
|
||||
restock(canister)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
. = 0
|
||||
var/list/datum/data/vending_product/record_list
|
||||
var/list/canister_list
|
||||
|
||||
for(var/i in 1 to 3)
|
||||
switch(i)
|
||||
if (1)
|
||||
record_list = product_records
|
||||
canister_list = canister.products
|
||||
if (2)
|
||||
record_list = hidden_records
|
||||
canister_list = canister.contraband
|
||||
else
|
||||
record_list = coin_records
|
||||
canister_list = canister.premium
|
||||
if(!record_list.len || !canister_list.len)
|
||||
continue
|
||||
|
||||
for(var/datum/data/vending_product/record as anything in record_list)
|
||||
var/diff = min(record.max_amount - record.amount, canister_list[record.product_path] || 0)
|
||||
if (diff)
|
||||
canister_list[record.product_path] -= diff
|
||||
if(!canister_list[record.product_path])
|
||||
canister_list -= record.product_path
|
||||
record.amount += diff
|
||||
. += diff
|
||||
|
||||
//===========================VENDING OUT ITEMS================================
|
||||
/obj/machinery/vending/Exited(atom/movable/gone, direction)
|
||||
. = ..()
|
||||
for(var/datum/data/vending_product/record in product_records + coin_records + hidden_records)
|
||||
if(gone in record.returned_products)
|
||||
record.returned_products -= gone
|
||||
record.amount -= 1
|
||||
break
|
||||
|
||||
/**
|
||||
* 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, mob/user, list/greyscale_colors)
|
||||
PROTECTED_PROC(TRUE)
|
||||
|
||||
. = TRUE
|
||||
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)
|
||||
return
|
||||
var/price_to_use = item_record.price
|
||||
if(item_record in hidden_records)
|
||||
if(!extended_inventory)
|
||||
return
|
||||
else if (!(item_record in record_to_check))
|
||||
message_admins("Vending machine exploit attempted by [ADMIN_LOOKUPFLW(user)]!")
|
||||
return
|
||||
if (item_record.amount <= 0)
|
||||
speak("Sold out of [item_record.name].")
|
||||
flick(icon_deny, src)
|
||||
return
|
||||
if(onstation)
|
||||
// Here we do additional handing ahead of the payment component's logic, such as age restrictions and additional logging
|
||||
var/obj/item/card/id/card_used
|
||||
var/mob/living/living_user
|
||||
if(isliving(user))
|
||||
living_user = user
|
||||
card_used = living_user.get_idcard(TRUE)
|
||||
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(!(user in GLOB.narcd_underages))
|
||||
aas_config_announce(/datum/aas_config_entry/vendomat_age_control, list(
|
||||
"PERSON" = usr.name,
|
||||
"LOCATION" = get_area_name(src),
|
||||
"VENDOR" = name,
|
||||
"PRODUCT" = item_record.name
|
||||
), src, list(RADIO_CHANNEL_SECURITY))
|
||||
GLOB.narcd_underages += user
|
||||
flick(icon_deny, src)
|
||||
return
|
||||
|
||||
if(!proceed_payment(card_used, living_user, item_record, price_to_use, params["discountless"]))
|
||||
return
|
||||
|
||||
if(last_shopper != REF(user) || 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(user)
|
||||
use_energy(active_power_usage)
|
||||
if(icon_vend) //Show the vending animation if needed
|
||||
flick(icon_vend, src)
|
||||
|
||||
// Always give out free returned stuff first, e.g. to avoid walling a traitor objective in a bag behind paid items
|
||||
var/obj/item/vended_item = dispense(item_record, get_turf(src), dispense_returned = LAZYLEN(item_record.returned_products))
|
||||
if(!vended_item)
|
||||
return
|
||||
|
||||
if(greyscale_colors)
|
||||
vended_item.set_greyscale(colors=greyscale_colors)
|
||||
if(user.CanReach(src) && user.put_in_hands(vended_item))
|
||||
to_chat(user, span_notice("You take [item_record.name] out of the slot."))
|
||||
vended_item.do_pickup_animation(user, src)
|
||||
else
|
||||
to_chat(user, span_warning("[capitalize(format_text(item_record.name))] falls onto the floor!"))
|
||||
SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[item_record.product_path]"))
|
||||
|
||||
/**
|
||||
* Common proc that dispenses an item. Called when the item is vended, or gotten some other way.
|
||||
*
|
||||
* Arguments
|
||||
* * datum/data/vending_product/item_record - the vending record which contains the information of the item to dispense
|
||||
* * atom/spawn_location - location to dispense the item to
|
||||
* * silent - should we play the vending sound
|
||||
* * dispense_returned - are we vending out an returned item
|
||||
*/
|
||||
/obj/machinery/vending/proc/dispense(datum/data/vending_product/item_record, atom/spawn_location, silent = FALSE, dispense_returned = FALSE)
|
||||
SHOULD_NOT_OVERRIDE(TRUE)
|
||||
|
||||
if(!silent)
|
||||
playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3)
|
||||
|
||||
var/obj/item/vended_item = null
|
||||
if(dispense_returned)
|
||||
vended_item = LAZYACCESS(item_record.returned_products, LAZYLEN(item_record.returned_products)) //first in, last out
|
||||
if(!QDELETED(vended_item))
|
||||
vended_item.forceMove(spawn_location)
|
||||
else if(item_record.amount)
|
||||
vended_item = new item_record.product_path(spawn_location)
|
||||
if(vended_item.type in contraband)
|
||||
ADD_TRAIT(vended_item, TRAIT_CONTRABAND, INNATE_TRAIT)
|
||||
item_record.amount--
|
||||
|
||||
if(!QDELETED(vended_item))
|
||||
on_dispense(vended_item, dispense_returned)
|
||||
return vended_item
|
||||
|
||||
/**
|
||||
* A proc meant to perform custom behavior on newly dispensed items.
|
||||
*
|
||||
* Arguments
|
||||
* * obj/item/vended_item - the item that has just been dispensed
|
||||
* * dispense_returned - is this item an returned product
|
||||
*/
|
||||
/obj/machinery/vending/proc/on_dispense(obj/item/vended_item, dispense_returned = FALSE)
|
||||
PROTECTED_PROC(TRUE)
|
||||
|
||||
return
|
||||
|
||||
/**
|
||||
* Handles payment processing: discounts, logging, balance change etc.
|
||||
* arguments:
|
||||
* paying_id_card - the id card that will be billed for the product.
|
||||
* mob_paying - the mob that is trying to purchase the item.
|
||||
* product_to_vend - the product record of the item we're trying to vend.
|
||||
* price_to_use - price of the item we're trying to vend.
|
||||
* discountless - whether or not to apply discounts
|
||||
*/
|
||||
/obj/machinery/vending/proc/proceed_payment(obj/item/card/id/paying_id_card, mob/living/mob_paying, datum/data/vending_product/product_to_vend, price_to_use, discountless)
|
||||
PROTECTED_PROC(TRUE)
|
||||
|
||||
if(QDELETED(paying_id_card)) //not available(null) or somehow is getting destroyed
|
||||
speak("You do not possess an ID to purchase [product_to_vend.name].")
|
||||
return FALSE
|
||||
var/datum/bank_account/account = paying_id_card.registered_account
|
||||
if(account.account_job && account.account_job.paycheck_department == payment_department && !discountless)
|
||||
price_to_use = max(round(price_to_use * DEPARTMENT_DISCOUNT), 1) //No longer free, but signifigantly cheaper.
|
||||
if(LAZYLEN(product_to_vend.returned_products))
|
||||
price_to_use = 0 //returned items are free
|
||||
if(price_to_use && (attempt_charge(src, mob_paying, price_to_use) & COMPONENT_OBJ_CANCEL_CHARGE))
|
||||
speak("You do not possess the funds to purchase [product_to_vend.name].")
|
||||
flick(icon_deny,src)
|
||||
return FALSE
|
||||
//actual payment here
|
||||
var/datum/bank_account/paying_id_account = SSeconomy.get_dep_account(payment_department)
|
||||
if(paying_id_account)
|
||||
SSblackbox.record_feedback("amount", "vending_spent", price_to_use)
|
||||
SSeconomy.track_purchase(account, price_to_use, name)
|
||||
log_econ("[price_to_use] credits were inserted into [src] by [account.account_holder] to buy [product_to_vend].")
|
||||
credits_contained += round(price_to_use * VENDING_CREDITS_COLLECTION_AMOUNT)
|
||||
return TRUE
|
||||
57
code/modules/vending/vendor/throwing.dm
vendored
Normal file
57
code/modules/vending/vendor/throwing.dm
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
/**
|
||||
* 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/mob/living/target = locate() in view(7,src)
|
||||
if(!target)
|
||||
return FALSE
|
||||
|
||||
var/obj/thrown_item
|
||||
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
|
||||
// Always throw new stuff that costs before free returned stuff, because of the hacking effort and time between throws involved
|
||||
var/only_returned_left = (record.amount <= LAZYLEN(record.returned_products))
|
||||
thrown_item = dispense(record, get_turf(src), silent = TRUE, dispense_returned = only_returned_left)
|
||||
break
|
||||
if(isnull(thrown_item))
|
||||
return FALSE
|
||||
|
||||
pre_throw(thrown_item)
|
||||
|
||||
thrown_item.throw_at(target, 16, 3)
|
||||
visible_message(span_danger("[src] launches [thrown_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
|
||||
|
||||
|
||||
///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/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)
|
||||
312
code/modules/vending/vendor/tilting.dm
vendored
Normal file
312
code/modules/vending/vendor/tilting.dm
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* Can this atom be curshed by the vending machine
|
||||
* Arguments
|
||||
*
|
||||
* * atom/atom_target - the atom we are checking for
|
||||
*/
|
||||
/proc/check_atom_crushable(atom/atom_target)
|
||||
/// Contains structures and items that vendors shouldn't crush when we land on them.
|
||||
var/static/list/vendor_uncrushable_objects = list(
|
||||
/obj/structure/chair,
|
||||
/obj/machinery/conveyor,
|
||||
) + GLOB.WALLITEMS_INTERIOR + GLOB.WALLITEMS_EXTERIOR
|
||||
|
||||
//make sure its not in the list of "uncrushable" stuff
|
||||
if(is_type_in_list(atom_target, vendor_uncrushable_objects))
|
||||
return FALSE
|
||||
|
||||
//check if it has integrity + allow ninjas, etc to be crushed in cloak
|
||||
if (atom_target.uses_integrity && !(atom_target.invisibility > SEE_INVISIBLE_LIVING))
|
||||
return TRUE //SMUSH IT
|
||||
|
||||
return 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 occurring. Default: 0
|
||||
* * forced_crit_case: If given a value from crushing.dm, [target] and its 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/blob/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(check_atom_crushable(atom_target))
|
||||
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
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
return list(
|
||||
CRUSH_CRIT_SHATTER_LEGS = 100,
|
||||
CRUSH_CRIT_PARAPLEGIC = 80,
|
||||
CRUSH_CRIT_HEADGIB = 20,
|
||||
CRUSH_CRIT_SQUISH_LIMB = 100
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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_PARAPLEGIC) // 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)
|
||||
var/severity = pick(WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_SEVERE, WOUND_SEVERITY_CRITICAL)
|
||||
if (!carbon_target.cause_wound_of_type_and_severity(WOUND_BLUNT, squish_part, severity, wound_source = "crushed by [src]"))
|
||||
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), carbon_target)
|
||||
return TRUE
|
||||
|
||||
return FALSE
|
||||
|
||||
/**
|
||||
* 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
|
||||
* range - the range of the machine when thrown if not adjacent
|
||||
*/
|
||||
/obj/machinery/vending/proc/tilt(atom/fatty, local_crit_chance = crit_chance, forced_crit, range = 1)
|
||||
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
|
||||
|
||||
if(get_turf(fatty) != get_turf(src))
|
||||
throw_at(get_turf(fatty), range, 1, spin = FALSE, quickstart = FALSE)
|
||||
|
||||
/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)
|
||||
return crit_case == VENDOR_CRUSH_CRIT_GLASSCANDY ? 0.33 : ..()
|
||||
|
||||
/obj/machinery/vending/get_crit_crush_chances()
|
||||
return list(
|
||||
VENDOR_CRUSH_CRIT_GLASSCANDY = 100,
|
||||
VENDOR_CRUSH_CRIT_PIN = 100
|
||||
)
|
||||
|
||||
/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 7)
|
||||
var/obj/item/shard/shard = new /obj/item/shard(get_turf(carbon_target))
|
||||
var/datum/embedding/embed = shard.get_embed()
|
||||
embed.embed_chance = 100
|
||||
embed.ignore_throwspeed_threshold = TRUE
|
||||
embed.impact_pain_mult = 1
|
||||
carbon_target.hitby(shard, skipcatch = TRUE, hitpush = FALSE)
|
||||
embed.embed_chance = initial(embed.embed_chance)
|
||||
embed.ignore_throwspeed_threshold = initial(embed.ignore_throwspeed_threshold)
|
||||
embed.impact_pain_mult = initial(embed.impact_pain_mult)
|
||||
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)
|
||||
|
||||
var/matrix/to_turn = turn(transform, -tilted_rotation)
|
||||
animate(src, transform = to_turn, 0.2 SECONDS)
|
||||
tilted_rotation = 0
|
||||
181
code/modules/vending/vendor/ui_data.dm
vendored
Normal file
181
code/modules/vending/vendor/ui_data.dm
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
///Helper to create a typepath to be used in the UI
|
||||
#define SANITIZED_PATH(path)(replacetext(replacetext("[path]", "/obj/item/", ""), "/", "-"))
|
||||
|
||||
/obj/machinery/vending/ui_assets(mob/user)
|
||||
return list(
|
||||
get_asset_datum(/datum/asset/spritesheet_batched/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()
|
||||
|
||||
|
||||
/**
|
||||
* 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)
|
||||
PROTECTED_PROC(TRUE)
|
||||
|
||||
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 = SANITIZED_PATH(record.product_path),
|
||||
name = record.name,
|
||||
price = record.price,
|
||||
ref = REF(record),
|
||||
colorable = record.colorable,
|
||||
)
|
||||
|
||||
var/atom/printed = record.product_path
|
||||
// If it's not GAGS and has no innate colors we have to care about, we use DMIcon
|
||||
if(ispath(printed, /atom) \
|
||||
&& (!initial(printed.greyscale_config) || !initial(printed.greyscale_colors)) \
|
||||
&& !initial(printed.color) \
|
||||
)
|
||||
static_record["icon"] = initial(printed.icon)
|
||||
static_record["icon_state"] = initial(printed.icon_state)
|
||||
|
||||
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_static_data(mob/user)
|
||||
var/list/data = list()
|
||||
data["onstation"] = onstation
|
||||
if(ad_list.len)
|
||||
data["ad"] = ad_list[rand(1, ad_list.len)]
|
||||
data["all_products_free"] = all_products_free
|
||||
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 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)
|
||||
PROTECTED_PROC(TRUE)
|
||||
|
||||
return passed_id.registered_account.account_balance
|
||||
|
||||
/obj/machinery/vending/ui_data(mob/user)
|
||||
. = list()
|
||||
|
||||
var/obj/item/card/id/card_used
|
||||
var/held_cash = 0
|
||||
if(isliving(user))
|
||||
var/mob/living/living_user = user
|
||||
card_used = living_user.get_idcard(TRUE)
|
||||
held_cash = living_user.tally_physical_credits()
|
||||
|
||||
var/list/user_data = null
|
||||
if(card_used?.registered_account)
|
||||
user_data = list()
|
||||
user_data["name"] = card_used.registered_account.account_holder
|
||||
user_data["cash"] = fetch_balance_to_use(card_used) + held_cash
|
||||
if(card_used.registered_account.account_job)
|
||||
user_data["job"] = card_used.registered_account.account_job.title
|
||||
user_data["department"] = card_used.registered_account.account_job.paycheck_department
|
||||
else
|
||||
user_data["job"] = "No Job"
|
||||
user_data["department"] = DEPARTMENT_UNASSIGNED
|
||||
.["user"] = user_data
|
||||
|
||||
.["stock"] = list()
|
||||
for (var/datum/data/vending_product/product_record as anything in product_records + coin_records + hidden_records)
|
||||
.["stock"][SANITIZED_PATH(product_record.product_path)] = list(
|
||||
amount = product_record.amount,
|
||||
free = length(product_record.returned_products)
|
||||
)
|
||||
|
||||
if(prob(10) && ad_list.len)
|
||||
.["ad"] = ad_list[rand(1, ad_list.len)]
|
||||
|
||||
.["extended_inventory"] = extended_inventory
|
||||
|
||||
/obj/machinery/vending/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("vend")
|
||||
. = vend(params, ui.user)
|
||||
if("select_colors")
|
||||
var/datum/data/vending_product/product = locate(params["ref"])
|
||||
if(!istype(product))
|
||||
return FALSE
|
||||
var/atom/fake_atom = product.product_path
|
||||
var/config = initial(fake_atom.greyscale_config)
|
||||
if(!config)
|
||||
return FALSE
|
||||
|
||||
var/list/allowed_configs = list("[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, ui.user, allowed_configs, CALLBACK(src, PROC_REF(_vend_greyscale), params, ui.user),
|
||||
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(ui.user)
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* 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, mob/user, datum/greyscale_modify_menu/menu)
|
||||
PRIVATE_PROC(TRUE)
|
||||
|
||||
if(user != menu.user)
|
||||
return
|
||||
vend(params, user, menu.split_colors)
|
||||
|
||||
#undef SANITIZED_PATH
|
||||
@@ -158,7 +158,7 @@ GLOBAL_VAR_INIT(roaches_deployed, FALSE)
|
||||
name = "AtmosDrobe"
|
||||
desc = "This relatively unknown vending machine delivers clothing for Atmospherics Technicians, an equally unknown job."
|
||||
icon_state = "atmosdrobe"
|
||||
product_ads = "Get your inflammable clothing right here!!!"
|
||||
product_slogans = "Get your inflammable clothing right here!!!"
|
||||
vend_reply = "Thank you for using the AtmosDrobe!"
|
||||
products = list(
|
||||
/obj/item/clothing/accessory/pocketprotector = 3,
|
||||
@@ -322,7 +322,7 @@ GLOBAL_VAR_INIT(roaches_deployed, FALSE)
|
||||
name = "CuraDrobe"
|
||||
desc = "A low-stock vendor only capable of vending clothing for curators and librarians."
|
||||
icon_state = "curadrobe"
|
||||
product_ads = "Glasses for your eyes and literature for your soul, Curadrobe has it all!; Impress & enthrall your library guests with Curadrobe's extended line of pens!"
|
||||
product_slogans = "Glasses for your eyes and literature for your soul, Curadrobe has it all!; Impress & enthrall your library guests with Curadrobe's extended line of pens!"
|
||||
vend_reply = "Thank you for using the CuraDrobe!"
|
||||
products = list(
|
||||
/obj/item/clothing/accessory/pocketprotector = 2,
|
||||
@@ -663,7 +663,7 @@ GLOBAL_VAR_INIT(roaches_deployed, FALSE)
|
||||
name = "ViroDrobe"
|
||||
desc = "An unsterilized machine for dispensing virology related clothing."
|
||||
icon_state = "virodrobe"
|
||||
product_ads = " Viruses getting you down? Then upgrade to sterilized clothing today!"
|
||||
product_slogans = " Viruses getting you down? Then upgrade to sterilized clothing today!"
|
||||
vend_reply = "Thank you for using the ViroDrobe"
|
||||
products = list(
|
||||
/obj/item/clothing/mask/surgical = 2,
|
||||
|
||||
@@ -6475,7 +6475,6 @@
|
||||
#include "code\modules\vehicles\mecha\medical\odysseus.dm"
|
||||
#include "code\modules\vehicles\mecha\working\clarke.dm"
|
||||
#include "code\modules\vehicles\mecha\working\ripley.dm"
|
||||
#include "code\modules\vending\_vending.dm"
|
||||
#include "code\modules\vending\assist.dm"
|
||||
#include "code\modules\vending\autodrobe.dm"
|
||||
#include "code\modules\vending\boozeomat.dm"
|
||||
@@ -6484,6 +6483,7 @@
|
||||
#include "code\modules\vending\clothesmate.dm"
|
||||
#include "code\modules\vending\coffee.dm"
|
||||
#include "code\modules\vending\cola.dm"
|
||||
#include "code\modules\vending\custom.dm"
|
||||
#include "code\modules\vending\cytopro.dm"
|
||||
#include "code\modules\vending\donk.dm"
|
||||
#include "code\modules\vending\drinnerware.dm"
|
||||
@@ -6508,9 +6508,16 @@
|
||||
#include "code\modules\vending\sovietsoda.dm"
|
||||
#include "code\modules\vending\subtype.dm"
|
||||
#include "code\modules\vending\sustenance.dm"
|
||||
#include "code\modules\vending\syndichem.dm"
|
||||
#include "code\modules\vending\toys.dm"
|
||||
#include "code\modules\vending\wardrobes.dm"
|
||||
#include "code\modules\vending\youtool.dm"
|
||||
#include "code\modules\vending\vendor\_vending.dm"
|
||||
#include "code\modules\vending\vendor\interaction.dm"
|
||||
#include "code\modules\vending\vendor\inventory.dm"
|
||||
#include "code\modules\vending\vendor\throwing.dm"
|
||||
#include "code\modules\vending\vendor\tilting.dm"
|
||||
#include "code\modules\vending\vendor\ui_data.dm"
|
||||
#include "code\modules\visuals\render_steps.dm"
|
||||
#include "code\modules\wiremod\components\abstract\assoc_list_variable.dm"
|
||||
#include "code\modules\wiremod\components\abstract\compare.dm"
|
||||
|
||||
@@ -14,47 +14,24 @@ import { useBackend } from '../backend';
|
||||
import { Window } from '../layouts';
|
||||
import { getLayoutState, LAYOUT, LayoutToggle } from './common/LayoutToggle';
|
||||
|
||||
type VendingData = {
|
||||
all_products_free: boolean;
|
||||
onstation: boolean;
|
||||
department: string;
|
||||
jobDiscount: number;
|
||||
displayed_currency_icon: string;
|
||||
displayed_currency_name: string;
|
||||
product_records: ProductRecord[];
|
||||
coin_records: CoinRecord[];
|
||||
hidden_records: HiddenRecord[];
|
||||
user: UserData;
|
||||
stock: Record<string, StockItem>[];
|
||||
extended_inventory: boolean;
|
||||
access: boolean;
|
||||
vending_machine_input: CustomInput[];
|
||||
categories: Record<string, Category>;
|
||||
};
|
||||
|
||||
type Category = {
|
||||
icon: string;
|
||||
type StockItem = {
|
||||
amount: number;
|
||||
free: boolean;
|
||||
};
|
||||
|
||||
type ProductRecord = {
|
||||
path: string;
|
||||
name: string;
|
||||
price: number;
|
||||
max_amount: number;
|
||||
ref: string;
|
||||
category: string;
|
||||
colorable: boolean;
|
||||
premium: boolean;
|
||||
image?: string;
|
||||
icon?: string;
|
||||
icon_state?: string;
|
||||
};
|
||||
|
||||
type CoinRecord = ProductRecord & {
|
||||
premium: boolean;
|
||||
};
|
||||
|
||||
type HiddenRecord = ProductRecord & {
|
||||
premium: boolean;
|
||||
};
|
||||
|
||||
type UserData = {
|
||||
name: string;
|
||||
cash: number;
|
||||
@@ -62,25 +39,34 @@ type UserData = {
|
||||
department: string;
|
||||
};
|
||||
|
||||
type StockItem = {
|
||||
name: string;
|
||||
path: string;
|
||||
amount: number;
|
||||
colorable: boolean;
|
||||
type Category = {
|
||||
icon: string;
|
||||
};
|
||||
|
||||
type CustomInput = {
|
||||
path: string;
|
||||
name: string;
|
||||
price: number;
|
||||
img: string;
|
||||
type VendingData = {
|
||||
all_products_free: boolean;
|
||||
onstation: boolean;
|
||||
ad: string;
|
||||
department: string;
|
||||
jobDiscount: number;
|
||||
displayed_currency_icon: string;
|
||||
displayed_currency_name: string;
|
||||
product_records: ProductRecord[];
|
||||
coin_records: ProductRecord[];
|
||||
hidden_records: ProductRecord[];
|
||||
user: UserData;
|
||||
stock: Record<string, StockItem>[];
|
||||
extended_inventory: boolean;
|
||||
access: boolean;
|
||||
categories: Record<string, Category>;
|
||||
};
|
||||
|
||||
export const Vending = (props) => {
|
||||
export const Vending = () => {
|
||||
const { data } = useBackend<VendingData>();
|
||||
|
||||
const {
|
||||
onstation,
|
||||
ad,
|
||||
product_records = [],
|
||||
coin_records = [],
|
||||
hidden_records = [],
|
||||
@@ -94,19 +80,12 @@ export const Vending = (props) => {
|
||||
const [stockSearch, setStockSearch] = useState('');
|
||||
const stockSearchFn = createSearch(
|
||||
stockSearch,
|
||||
(item: ProductRecord | CustomInput) => item.name,
|
||||
(item: ProductRecord) => item.name,
|
||||
);
|
||||
|
||||
let inventory: (ProductRecord | CustomInput)[];
|
||||
let custom = false;
|
||||
if (data.vending_machine_input) {
|
||||
inventory = data.vending_machine_input;
|
||||
custom = true;
|
||||
} else {
|
||||
inventory = [...product_records, ...coin_records];
|
||||
if (data.extended_inventory) {
|
||||
inventory = [...inventory, ...hidden_records];
|
||||
}
|
||||
let inventory: ProductRecord[] = [...product_records, ...coin_records];
|
||||
if (data.extended_inventory) {
|
||||
inventory = [...inventory, ...hidden_records];
|
||||
}
|
||||
|
||||
// Just in case we still have undefined values in the list
|
||||
@@ -137,9 +116,13 @@ export const Vending = (props) => {
|
||||
<UserDetails />
|
||||
</Stack.Item>
|
||||
)}
|
||||
{ad && (
|
||||
<Stack.Item>
|
||||
<AdSection AdDisplay={ad} />
|
||||
</Stack.Item>
|
||||
)}
|
||||
<Stack.Item grow>
|
||||
<ProductDisplay
|
||||
custom={custom}
|
||||
inventory={inventory}
|
||||
stockSearch={stockSearch}
|
||||
setStockSearch={setStockSearch}
|
||||
@@ -164,7 +147,7 @@ export const Vending = (props) => {
|
||||
};
|
||||
|
||||
/** Displays user details if an ID is present and the user is on the station */
|
||||
export const UserDetails = (props) => {
|
||||
export const UserDetails = () => {
|
||||
const { data } = useBackend<VendingData>();
|
||||
const { user } = data;
|
||||
|
||||
@@ -184,17 +167,27 @@ export const UserDetails = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const AdSection = (props: { AdDisplay: string }) => {
|
||||
const { AdDisplay } = props;
|
||||
|
||||
return (
|
||||
<NoticeBox m={0} color={'yellow'}>
|
||||
<Stack align="center">
|
||||
<Stack.Item>{AdDisplay}</Stack.Item>
|
||||
</Stack>
|
||||
</NoticeBox>
|
||||
);
|
||||
};
|
||||
|
||||
/** Displays products in a section, with user balance at top */
|
||||
const ProductDisplay = (props: {
|
||||
custom: boolean;
|
||||
inventory: (ProductRecord | CustomInput)[];
|
||||
inventory: ProductRecord[];
|
||||
stockSearch: string;
|
||||
setStockSearch: (search: string) => void;
|
||||
selectedCategory: string | null;
|
||||
}) => {
|
||||
const { data } = useBackend<VendingData>();
|
||||
const { custom, inventory, stockSearch, setStockSearch, selectedCategory } =
|
||||
props;
|
||||
const { inventory, stockSearch, setStockSearch, selectedCategory } = props;
|
||||
const {
|
||||
stock,
|
||||
all_products_free,
|
||||
@@ -242,7 +235,6 @@ const ProductDisplay = (props: {
|
||||
<Product
|
||||
key={product.path}
|
||||
fluid={toggleLayout === LAYOUT.List}
|
||||
custom={custom}
|
||||
product={product}
|
||||
productStock={stock[product.path]}
|
||||
/>
|
||||
@@ -251,25 +243,29 @@ const ProductDisplay = (props: {
|
||||
);
|
||||
};
|
||||
|
||||
type ProductProps = {
|
||||
product: ProductRecord;
|
||||
productStock: StockItem;
|
||||
fluid: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* An individual listing for an item.
|
||||
*/
|
||||
const Product = (props) => {
|
||||
const Product = (props: ProductProps) => {
|
||||
const { act, data } = useBackend<VendingData>();
|
||||
const { custom, product, productStock, fluid } = props;
|
||||
const { access, department, jobDiscount, all_products_free, user } = data;
|
||||
const { product, productStock, fluid } = props;
|
||||
const { department, jobDiscount, all_products_free, user } = data;
|
||||
|
||||
const colorable = !!productStock?.colorable;
|
||||
const free = all_products_free || product.price === 0;
|
||||
const colorable = !!product.colorable;
|
||||
const free = all_products_free || productStock.free || product.price === 0;
|
||||
const discount = !product.premium && department === user?.department;
|
||||
const remaining = custom ? product.amount : productStock.amount;
|
||||
const remaining = productStock.amount;
|
||||
const redPrice = Math.round(product.price * jobDiscount);
|
||||
const disabled =
|
||||
remaining === 0 ||
|
||||
(!all_products_free && !user) ||
|
||||
(!all_products_free &&
|
||||
!access &&
|
||||
(discount ? redPrice : product.price) > user?.cash);
|
||||
(!free && (discount ? redPrice : product.price) > user?.cash);
|
||||
|
||||
const baseProps = {
|
||||
base64: product.image,
|
||||
@@ -285,19 +281,14 @@ const Product = (props) => {
|
||||
colorable: colorable,
|
||||
remaining: remaining,
|
||||
onClick: () => {
|
||||
custom
|
||||
? act('dispense', {
|
||||
item: product.path,
|
||||
})
|
||||
: act('vend', {
|
||||
ref: product.ref,
|
||||
discountless: !!product.premium,
|
||||
});
|
||||
act('vend', {
|
||||
ref: product.ref,
|
||||
discountless: !!product.premium,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const priceProps = {
|
||||
custom: custom,
|
||||
discount: discount,
|
||||
free: free,
|
||||
product: product,
|
||||
@@ -311,7 +302,7 @@ const Product = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ProductGrid = (props) => {
|
||||
const ProductGrid = (props: any) => {
|
||||
const { product, remaining, ...baseProps } = props;
|
||||
const { ...priceProps } = props;
|
||||
|
||||
@@ -333,7 +324,7 @@ const ProductGrid = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ProductList = (props) => {
|
||||
const ProductList = (props: any) => {
|
||||
const { colorable, product, remaining, ...baseProps } = props;
|
||||
const { ...priceProps } = props;
|
||||
|
||||
@@ -365,7 +356,14 @@ const ProductList = (props) => {
|
||||
* In the case of customizable items, ie: shoes,
|
||||
* this displays a color wheel button that opens another window.
|
||||
*/
|
||||
const ProductColorSelect = (props) => {
|
||||
|
||||
type ProductColorSelectProps = {
|
||||
disabled: boolean;
|
||||
product: ProductRecord;
|
||||
fluid: boolean;
|
||||
};
|
||||
|
||||
const ProductColorSelect = (props: ProductColorSelectProps) => {
|
||||
const { act } = useBackend<VendingData>();
|
||||
const { disabled, product, fluid } = props;
|
||||
|
||||
@@ -375,37 +373,34 @@ const ProductColorSelect = (props) => {
|
||||
icon={'palette'}
|
||||
color={'transparent'}
|
||||
tooltip={'Change color'}
|
||||
style={disabled && { pointerEvents: 'none', opacity: 0.5 }}
|
||||
style={disabled ? { pointerEvents: 'none', opacity: 0.5 } : {}}
|
||||
onClick={() => act('select_colors', { ref: product.ref })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ProductPriceProps = {
|
||||
discount: boolean;
|
||||
free: boolean;
|
||||
product: ProductRecord;
|
||||
redPrice: number;
|
||||
};
|
||||
|
||||
/** The main button to purchase an item. */
|
||||
const ProductPrice = (props) => {
|
||||
const { act, data } = useBackend<VendingData>();
|
||||
const { access, displayed_currency_name } = data;
|
||||
const { custom, discount, free, product, redPrice } = props;
|
||||
const customPrice = access ? 'Free' : product.price;
|
||||
let standardPrice = product.price;
|
||||
const ProductPrice = (props: ProductPriceProps) => {
|
||||
const { data } = useBackend<VendingData>();
|
||||
const { displayed_currency_name } = data;
|
||||
const { discount, free, product, redPrice } = props;
|
||||
let standardPrice = product.price + '';
|
||||
if (free) {
|
||||
standardPrice = 'Free';
|
||||
standardPrice = 'FREE';
|
||||
} else if (discount) {
|
||||
standardPrice = redPrice;
|
||||
standardPrice = redPrice + '';
|
||||
}
|
||||
return (
|
||||
<Stack.Item fontSize={0.85} color={'gold'}>
|
||||
{custom ? (
|
||||
<>
|
||||
{customPrice}
|
||||
{!access && displayed_currency_name}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{standardPrice}
|
||||
{!free && displayed_currency_name}
|
||||
</>
|
||||
)}
|
||||
{standardPrice}
|
||||
{!free && displayed_currency_name}
|
||||
</Stack.Item>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user