From fc94ed2f531a446374f6272e351d5ea4e5f9c163 Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Sun, 28 Jul 2024 10:19:35 +0200 Subject: [PATCH] [MIRROR] (Black)market Telepad (LTSRBT) Update: Restocking Edition (#29094) * (Black)market Telepad (LTSRBT) Update: Restocking Edition (#85066) ## About The Pull Request This is a suggestion that was to me several months ago to add the ability to pay credits to restock the black market. I liked the idea because it'd add anothe small reason to buy the board, though I had forgotten about it shortly after and just happened to remember it now. So, yeah, you can swat the LTSRBT with a holochip to restock the market(s). The price is shared amongst all pads and starts at 675 credits, but it doubles up everytime this is done, for obvious balance purposes. I've also updated included this new feature in the unit test, given the LTSRBT new sprites and renamed the Blackmarket subsystem to Market, because of how it can support different types of market datums, not just the blackmarket ## Why It's Good For The Game This adds one more reason to buy and build the LTSRBT and make markets less dependant on the bad side of RNG, if you have the credits. It's a bit of a money sink. ## Changelog :cl: add: You can now restock the black market by hitting the LTSRBT with enough credits. The price doubles each time this is done. imageadd: Updated the LTSRBT sprites. balance: Reintroduced the LTSRBT to cargo for 2000 credits vs the original 4000 (the ansible and crystals to build it are included btw), and slightly lowered the average blackmarket price for the same item to account for shipping costs. /:cl: * (Black)market Telepad (LTSRBT) Update: Restocking Edition --------- Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com> --- ...gnals_blackmarket.dm => signals_market.dm} | 0 code/__DEFINES/{blackmarket.dm => market.dm} | 0 .../subsystem/{blackmarket.dm => market.dm} | 68 +++++++++++------ code/modules/antagonists/spy/spy_bounty.dm | 2 +- code/modules/cargo/markets/_market.dm | 16 +++- code/modules/cargo/markets/market_item.dm | 15 ++-- .../cargo/markets/market_items/hostages.dm | 1 + .../markets/market_items/stolen_goods.dm | 1 + .../cargo/markets/market_items/tools.dm | 2 +- code/modules/cargo/markets/market_telepad.dm | 69 ++++++++++++++++-- code/modules/cargo/markets/market_uplink.dm | 18 ++--- code/modules/cargo/packs/imports.dm | 17 +++++ code/modules/mob/living/living.dm | 2 +- code/modules/unit_tests/_unit_tests.dm | 2 +- code/modules/unit_tests/blackmarket.dm | 23 ------ code/modules/unit_tests/market.dm | 59 +++++++++++++++ icons/obj/machines/telecomms.dmi | Bin 30341 -> 30043 bytes tgstation.dme | 6 +- 18 files changed, 227 insertions(+), 74 deletions(-) rename code/__DEFINES/dcs/signals/{signals_blackmarket.dm => signals_market.dm} (100%) rename code/__DEFINES/{blackmarket.dm => market.dm} (100%) rename code/controllers/subsystem/{blackmarket.dm => market.dm} (68%) delete mode 100644 code/modules/unit_tests/blackmarket.dm create mode 100644 code/modules/unit_tests/market.dm diff --git a/code/__DEFINES/dcs/signals/signals_blackmarket.dm b/code/__DEFINES/dcs/signals/signals_market.dm similarity index 100% rename from code/__DEFINES/dcs/signals/signals_blackmarket.dm rename to code/__DEFINES/dcs/signals/signals_market.dm diff --git a/code/__DEFINES/blackmarket.dm b/code/__DEFINES/market.dm similarity index 100% rename from code/__DEFINES/blackmarket.dm rename to code/__DEFINES/market.dm diff --git a/code/controllers/subsystem/blackmarket.dm b/code/controllers/subsystem/market.dm similarity index 68% rename from code/controllers/subsystem/blackmarket.dm rename to code/controllers/subsystem/market.dm index 5c88177583b..81d96d331c7 100644 --- a/code/controllers/subsystem/blackmarket.dm +++ b/code/controllers/subsystem/market.dm @@ -1,5 +1,5 @@ -SUBSYSTEM_DEF(blackmarket) - name = "Blackmarket" +SUBSYSTEM_DEF(market) + name = "Market" flags = SS_BACKGROUND init_order = INIT_ORDER_DEFAULT @@ -18,27 +18,27 @@ SUBSYSTEM_DEF(blackmarket) /// Currently queued purchases. var/list/queued_purchases = list() -/datum/controller/subsystem/blackmarket/Initialize() +/datum/controller/subsystem/market/Initialize() for(var/market in subtypesof(/datum/market)) markets[market] += new market - for(var/datum/market_item/item as anything in subtypesof(/datum/market_item)) - if(!initial(item.item)) - continue - if(!prob(initial(item.availability_prob))) - continue - - var/datum/market_item/item_instance = new item() - for(var/potential_market in item_instance.markets) - if(!markets[potential_market]) - stack_trace("SSblackmarket: Item [item_instance] available in market that does not exist.") - continue - // If this fails the market item will just be GC'd - markets[potential_market].add_item(item_instance) + for(var/path in subtypesof(/datum/market_item)) + initialize_item(path) return SS_INIT_SUCCESS -/datum/controller/subsystem/blackmarket/fire(resumed) +/datum/controller/subsystem/market/proc/initialize_item(datum/market_item/path, list/market_whitelist) + if(!path::item || !prob(path::availability_prob)) + return + var/datum/market_item/item_instance = new path() + for(var/potential_market in item_instance.markets) + if(!markets[potential_market]) + stack_trace("SSmarket: Item [item_instance] available in market that does not exist.") + continue + if(isnull(market_whitelist) || (potential_market in market_whitelist)) + markets[potential_market].add_item(item_instance) + +/datum/controller/subsystem/market/fire(resumed) while(length(queued_purchases)) var/datum/market_purchase/purchase = queued_purchases[1] queued_purchases.Cut(1,2) @@ -55,9 +55,9 @@ SUBSYSTEM_DEF(blackmarket) // The time left of the shortest cooldown amongst all telepads. var/lowest_timeleft = INFINITY for(var/obj/machinery/ltsrbt/pad as anything in telepads) - if(!COOLDOWN_FINISHED(pad, recharge_cooldown)) - var/timeleft = COOLDOWN_TIMELEFT(pad, recharge_cooldown) - if(timeleft < lowest_timeleft) + if(!COOLDOWN_FINISHED(pad, recharge_cooldown) || (pad.machine_stat & NOPOWER)) + var/timeleft = pad.machine_stat & NOPOWER ? INFINITY - 1 : COOLDOWN_TIMELEFT(pad, recharge_cooldown) + if(timeleft <= lowest_timeleft) lowest_cd_pad = pad lowest_timeleft = timeleft continue @@ -79,7 +79,7 @@ SUBSYSTEM_DEF(blackmarket) to_chat(buyer, span_notice("[purchase.uplink] flashes a message noting that the order is being teleported to [get_area(targetturf)] in 60 seconds.")) // do_teleport does not want to teleport items from nullspace, so it just forceMoves and does sparks. - addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/controller/subsystem/blackmarket, fake_teleport), purchase, targetturf), 60 SECONDS) + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/controller/subsystem/market, fake_teleport), purchase, targetturf), 60 SECONDS) // Get the current location of the uplink if it exists, then throws the item from space at the station from a random direction. if(SHIPPING_METHOD_LAUNCH) @@ -106,7 +106,7 @@ SUBSYSTEM_DEF(blackmarket) break /// Used to make a teleportation effect as do_teleport does not like moving items from nullspace. -/datum/controller/subsystem/blackmarket/proc/fake_teleport(datum/market_purchase/purchase, turf/target) +/datum/controller/subsystem/market/proc/fake_teleport(datum/market_purchase/purchase, turf/target) // Oopsie, whoopsie, the item is gone. So long, and thanks for all the money. if(QDELETED(purchase)) return @@ -119,9 +119,31 @@ SUBSYSTEM_DEF(blackmarket) qdel(purchase) /// Used to add /datum/market_purchase to queued_purchases var. Returns TRUE when queued. -/datum/controller/subsystem/blackmarket/proc/queue_item(datum/market_purchase/purchase) +/datum/controller/subsystem/market/proc/queue_item(datum/market_purchase/purchase) if((purchase.method == SHIPPING_METHOD_LTSRBT && !telepads.len) || isnull(purchase.uplink)) qdel(purchase) return FALSE queued_purchases += purchase return TRUE + +///A proc that restocks one or more markets, or all if the market_whitelist is null. +/datum/controller/subsystem/market/proc/restock(list/market_whitelist) + var/market_name = "Markets" + if(market_whitelist && !islist(market_whitelist)) + var/datum/market/market_path = market_whitelist + market_name = market_path::name + market_whitelist = list(market_path) + + var/list/existing_types = list() + for(var/path in markets) + if(isnull(market_whitelist) || (path in market_whitelist)) + markets[path].restock(existing_types) + + for(var/datum/market_item/path as anything in (subtypesof(/datum/market_item) - existing_types)) + if(!path::restockable) + continue + initialize_item(path, market_whitelist) + + for(var/obj/machinery/ltsrbt/pad as anything in telepads) + pad.say("[market_name] restocked!") + playsound(src, 'sound/effects/cashregister.ogg', 40, FALSE) diff --git a/code/modules/antagonists/spy/spy_bounty.dm b/code/modules/antagonists/spy/spy_bounty.dm index 28984ce2272..01a1a1baf7b 100644 --- a/code/modules/antagonists/spy/spy_bounty.dm +++ b/code/modules/antagonists/spy/spy_bounty.dm @@ -186,7 +186,7 @@ var/datum/market_item/stolen_good/new_item = new(thing, item_price) - return SSblackmarket.markets[/datum/market/blackmarket].add_item(new_item) + return SSmarket.markets[/datum/market/blackmarket].add_item(new_item) /// Steal an item /datum/spy_bounty/objective_item diff --git a/code/modules/cargo/markets/_market.dm b/code/modules/cargo/markets/_market.dm index 0a93469411c..4696d3007a7 100644 --- a/code/modules/cargo/markets/_market.dm +++ b/code/modules/cargo/markets/_market.dm @@ -6,7 +6,7 @@ var/list/shipping // Automatic vars, do not touch these. - /// Items available from this market, populated by SSblackmarket on initialization. Automatically assigned, so don't manually adjust. + /// Items available from this market, populated by SSmarket on initialization. Automatically assigned, so don't manually adjust. var/list/available_items = list() /// Item categories available from this market, only items which are in these categories can be gotten from this market. Automatically assigned, so don't manually adjust. var/list/categories = list() @@ -73,6 +73,20 @@ return FALSE +/** + * A proc that restocks only the EXISTING items of this market. + * If you want to selectively restock markets, call SSmarket.restock(market_or_list_of_markets) instead. + */ +/datum/market/proc/restock(list/existing_items) + for(var/category in available_items) + var/category_list = available_items[category] + for(var/identifier in category_list) + var/datum/market_item/item = category_list[identifier] + existing_items |= item.type + if(!item.restockable || item.stock >= item.stock_max || !prob(item.availability_prob)) + continue + item.stock += rand(1, item.stock_max - item.stock) + /datum/market/blackmarket name = "Black Market" shipping = list( diff --git a/code/modules/cargo/markets/market_item.dm b/code/modules/cargo/markets/market_item.dm index 5e3ce4efb6c..d7a4dd4c0ee 100644 --- a/code/modules/cargo/markets/market_item.dm +++ b/code/modules/cargo/markets/market_item.dm @@ -5,7 +5,7 @@ var/desc /// The category this item belongs to, should be already declared in the market that this item is accessible in. var/category - /// "/datum/market"s that this item should be in, used by SSblackmarket on init. + /// "/datum/market"s that this item should be in, used by SSmarket on init. var/list/markets = list(/datum/market/blackmarket) /// Price for the item, if not set creates a price according to the *_min and *_max vars. @@ -27,7 +27,7 @@ var/stock_min = 1 /// Maximum amount that there should be of this item in the market if generated randomly. var/stock_max = 0 - /// Probability for this item to be available. Used by SSblackmarket on init. + /// Probability for this item to be available. Used by SSmarket on init. var/availability_prob ///The identifier for the market item, generated on runtime and used to access them in the market categories. @@ -36,6 +36,9 @@ ///If set, these will override the shipment methods set by the market var/list/shipping_override + /// Can this item be restocked + var/restockable = TRUE + /datum/market_item/New() if(isnull(price)) price = rand(price_min, price_max) @@ -82,7 +85,7 @@ CRASH("Invalid item type for market item [item || "null"]") /** - * Buys the item and makes SSblackmarket handle it. + * Buys the item and makes SSmarket handle it. * * @param uplink The uplink that is buying the item. * @param buyer The mob that is buying the item. @@ -102,8 +105,8 @@ // Alright, the item has been purchased. var/datum/market_purchase/purchase = new(src, uplink, shipping_method, legal_status) - // SSblackmarket takes care of the shipping. - if(SSblackmarket.queue_item(purchase)) + // SSmarket takes care of the shipping. + if(SSmarket.queue_item(purchase)) stock-- buyer.log_message("has succesfully purchased [name] using [shipping_method] for shipping.", LOG_ECON) return TRUE @@ -139,7 +142,7 @@ /datum/market_purchase/Destroy() entry = null uplink = null - SSblackmarket.queued_purchases -= src + SSmarket.queued_purchases -= src return ..() /datum/market_purchase/proc/on_instance_del(datum/source) diff --git a/code/modules/cargo/markets/market_items/hostages.dm b/code/modules/cargo/markets/market_items/hostages.dm index ed5b1f10a7f..702cea907bd 100644 --- a/code/modules/cargo/markets/market_items/hostages.dm +++ b/code/modules/cargo/markets/market_items/hostages.dm @@ -5,6 +5,7 @@ stock = 1 availability_prob = 100 shipping_override = list(SHIPPING_METHOD_LTSRBT = 0, SHIPPING_METHOD_SUPPLYPOD = 350) + restockable = FALSE /// temporary reference to the 4 in 7 chances of signaler and electropack. var/obj/item/assembly/signaler/signaler diff --git a/code/modules/cargo/markets/market_items/stolen_goods.dm b/code/modules/cargo/markets/market_items/stolen_goods.dm index 02a72f05d26..cb1932673e0 100644 --- a/code/modules/cargo/markets/market_items/stolen_goods.dm +++ b/code/modules/cargo/markets/market_items/stolen_goods.dm @@ -4,6 +4,7 @@ abstract_path = /datum/market_item/stolen_good stock = 1 availability_prob = 100 + restockable = FALSE /datum/market_item/stolen_good/New(atom/movable/thing, thing_price) ..() diff --git a/code/modules/cargo/markets/market_items/tools.dm b/code/modules/cargo/markets/market_items/tools.dm index 963d7fbaeb0..0c9969756d3 100644 --- a/code/modules/cargo/markets/market_items/tools.dm +++ b/code/modules/cargo/markets/market_items/tools.dm @@ -11,7 +11,7 @@ stock_min = 2 stock_max = 4 price_min = CARGO_CRATE_VALUE * 2.5 - price_max = CARGO_CRATE_VALUE * 3.75 + price_max = CARGO_CRATE_VALUE * 3.25 availability_prob = 100 /datum/market_item/tool/caravan_wrench diff --git a/code/modules/cargo/markets/market_telepad.dm b/code/modules/cargo/markets/market_telepad.dm index 7c5b509a942..799395f30d1 100644 --- a/code/modules/cargo/markets/market_telepad.dm +++ b/code/modules/cargo/markets/market_telepad.dm @@ -1,3 +1,5 @@ +#define DEFAULT_RESTOCK_COST 675 + /obj/item/circuitboard/machine/ltsrbt name = "LTSRBT (Machine Board)" icon_state = "bluespacearray" @@ -13,7 +15,8 @@ name = "Long-To-Short-Range-Bluespace-Transceiver" desc = "The LTSRBT is a compact teleportation machine for receiving and sending items outside the station and inside the station.\nUsing teleportation frequencies stolen from NT it is near undetectable.\nEssential for any illegal market operations on NT stations.\n" icon = 'icons/obj/machines/telecomms.dmi' - icon_state = "exonet_node" + icon_state = "exonet_node_idle" + base_icon_state = "exonet_node" circuit = /obj/item/circuitboard/machine/ltsrbt density = TRUE @@ -35,19 +38,43 @@ var/datum/market_purchase/transmitting /// Queue for purchases that the machine should receive and send. var/list/datum/market_purchase/queue = list() + /** + * Attacking the machinery with enough credits will restock the markets, allowing for more/better items. + * The cost doubles each time this is done. + */ + var/static/restock_cost = DEFAULT_RESTOCK_COST /obj/machinery/ltsrbt/Initialize(mapload) . = ..() - SSblackmarket.telepads += src + register_context() + SSmarket.telepads += src /obj/machinery/ltsrbt/Destroy() - SSblackmarket.telepads -= src + SSmarket.telepads -= src // Bye bye orders. - if(length(SSblackmarket.telepads)) + if(length(SSmarket.telepads)) for(var/datum/market_purchase/P in queue) - SSblackmarket.queue_item(P) + SSmarket.queue_item(P) . = ..() +/obj/machinery/ltsrbt/add_context(atom/source, list/context, obj/item/held_item, mob/user) + if(held_item && held_item.get_item_credit_value()) + context[SCREENTIP_CONTEXT_LMB] = "Restock" + return CONTEXTUAL_SCREENTIP_SET + return NONE + +/obj/machinery/ltsrbt/examine(mob/user) + . = ..() + if(machine_stat & NOPOWER) + . += span_info("A display reads: \"Current market restock price: [EXAMINE_HINT("[restock_cost] cr")]\".") + +/obj/machinery/ltsrbt/update_icon_state() + . = ..() + if(machine_stat & NOPOWER) + icon_state = "[base_icon_state]_off" + else + icon_state = "[base_icon_state][(receiving || length(queue)) ? "" : "_idle"]" + /obj/machinery/ltsrbt/RefreshParts() . = ..() recharge_time = base_recharge_time @@ -67,6 +94,7 @@ /obj/machinery/ltsrbt/proc/add_to_queue(datum/market_purchase/purchase) if(!recharge_cooldown && !receiving && !transmitting) receiving = purchase + update_appearance(UPDATE_ICON_STATE) else queue += purchase @@ -80,6 +108,8 @@ if(transmitting == purchase) transmitting = null + update_appearance(UPDATE_ICON_STATE) + /obj/machinery/ltsrbt/process(seconds_per_tick) if(machine_stat & NOPOWER) return @@ -113,3 +143,32 @@ if(length(queue)) receiving = pick_n_take(queue) + +/obj/machinery/ltsrbt/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + var/creds_value = tool.get_item_credit_value() + if(!creds_value) + return NONE + + . = ITEM_INTERACT_SUCCESS + + if(machine_stat & NOPOWER) + return + + if(creds_value < restock_cost) + say("Insufficient credits!") + playsound(src, 'sound/machines/buzz-sigh.ogg', 40, FALSE) + return + + if(istype(tool, /obj/item/holochip)) + var/obj/item/holochip/chip = tool + chip.spend(restock_cost) + else + qdel(tool) + if(creds_value != restock_cost) + var/obj/item/holochip/change = new(creds_value - restock_cost) + user.put_in_hands(change) + + SSmarket.restock() + restock_cost *= 2 + +#undef DEFAULT_RESTOCK_COST diff --git a/code/modules/cargo/markets/market_uplink.dm b/code/modules/cargo/markets/market_uplink.dm index d13f59937b6..a324a2f0409 100644 --- a/code/modules/cargo/markets/market_uplink.dm +++ b/code/modules/cargo/markets/market_uplink.dm @@ -20,7 +20,7 @@ /obj/item/market_uplink/Initialize(mapload) . = ..() - // We don't want to go through this at mapload because the SSblackmarket isn't initialized yet. + // We don't want to go through this at mapload because the SSmarket isn't initialized yet. if(mapload) return @@ -30,7 +30,7 @@ /obj/item/market_uplink/proc/update_viewing_category() if(accessible_markets.len) viewing_market = accessible_markets[1] - var/list/categories = SSblackmarket.markets[viewing_market].categories + var/list/categories = SSmarket.markets[viewing_market].categories if(categories?.len) viewing_category = categories[1] @@ -45,7 +45,7 @@ /obj/item/market_uplink/ui_data(mob/user) var/list/data = list() - var/datum/market/market = viewing_market ? SSblackmarket.markets[viewing_market] : null + var/datum/market/market = viewing_market ? SSmarket.markets[viewing_market] : null var/obj/item/card/id/id_card if(isliving(user)) var/mob/living/livin = user @@ -86,11 +86,11 @@ /obj/item/market_uplink/ui_static_data(mob/user) var/list/data = list() - data["delivery_method_description"] = SSblackmarket.shipping_method_descriptions - data["ltsrbt_built"] = SSblackmarket.telepads.len + data["delivery_method_description"] = SSmarket.shipping_method_descriptions + data["ltsrbt_built"] = SSmarket.telepads.len data["markets"] = list() for(var/M in accessible_markets) - var/datum/market/BM = SSblackmarket.markets[M] + var/datum/market/BM = SSmarket.markets[M] data["markets"] += list(list( "id" = M, "name" = BM.name @@ -107,7 +107,7 @@ return if(isnull(viewing_market)) return - if(!(params["category"] in SSblackmarket.markets[viewing_market].categories)) + if(!(params["category"] in SSmarket.markets[viewing_market].categories)) return viewing_category = params["category"] . = TRUE @@ -120,7 +120,7 @@ viewing_market = market - var/list/categories = SSblackmarket.markets[viewing_market].categories + var/list/categories = SSmarket.markets[viewing_market].categories if(categories?.len) viewing_category = categories[1] else @@ -142,7 +142,7 @@ if(isnull(selected_item)) buying = FALSE return - var/datum/market/market = SSblackmarket.markets[viewing_market] + var/datum/market/market = SSmarket.markets[viewing_market] market.purchase(selected_item, viewing_category, params["method"], src, usr) buying = FALSE diff --git a/code/modules/cargo/packs/imports.dm b/code/modules/cargo/packs/imports.dm index cbab2fe70fa..a7853ad55ab 100644 --- a/code/modules/cargo/packs/imports.dm +++ b/code/modules/cargo/packs/imports.dm @@ -321,3 +321,20 @@ ) crate_name = "floortile camouflauge crate" crate_type = /obj/structure/closet/crate/secure/weapon + +/** + * The Long To Short Range Bluespace Teleporter, used to deliver (black) market purchases more effiiently + * It can also be used to restock it, if you hit it with enough credits. + */ +/datum/supply_pack/imports/blackmarket_telepad + name = "Black Market LTSRBT" + desc = "Need a faster and better way of transporting your illegal goods from and to the \ + station? Fear not, the Long-To-Short-Range-Bluespace-Transceiver (LTSRBT for short) \ + is here to help. Contains a LTSRBT circuit, two bluespace crystals, and one ansible." + cost = CARGO_CRATE_VALUE * 10 + contraband = TRUE + contains = list( + /obj/item/circuitboard/machine/ltsrbt, + /obj/item/stack/ore/bluespace_crystal/artificial = 2, + /obj/item/stock_parts/subspace/ansible, + ) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 76623c19a87..66deda41155 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -2740,7 +2740,7 @@ GLOBAL_LIST_EMPTY(fire_appearances) ///The price should be high enough that the contractor can't just buy 'em back with their cut alone. var/datum/market_item/hostage/market_item = new(src, black_market_price || ransom_price) - SSblackmarket.markets[/datum/market/blackmarket].add_item(market_item) + SSmarket.markets[/datum/market/blackmarket].add_item(market_item) if(mind) ADD_TRAIT(mind, TRAIT_HAS_BEEN_KIDNAPPED, TRAIT_GENERIC) diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index a3e1680a0a6..b2080c2bb55 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -102,7 +102,6 @@ #include "bespoke_id.dm" #include "binary_insert.dm" #include "bitrunning.dm" -#include "blackmarket.dm" #include "blindness.dm" #include "bloody_footprints.dm" #include "breath.dm" @@ -183,6 +182,7 @@ #include "mapload_space_verification.dm" #include "mapping.dm" #include "mapping_nearstation_test.dm" +#include "market.dm" #include "mecha_damage.dm" #include "medical_wounds.dm" #include "merge_type.dm" diff --git a/code/modules/unit_tests/blackmarket.dm b/code/modules/unit_tests/blackmarket.dm deleted file mode 100644 index 984e2ea8155..00000000000 --- a/code/modules/unit_tests/blackmarket.dm +++ /dev/null @@ -1,23 +0,0 @@ -/// Ensures black market items have acceptable variable values. -/datum/unit_test/blackmarket - -/datum/unit_test/blackmarket/Run() - for(var/datum/market_item/prototype as anything in subtypesof(/datum/market_item)) - if(prototype::abstract_path == prototype) //skip abstract paths - continue - if(!prototype::category) - TEST_FAIL("[prototype] doesn't have a set category (or the abstract path var isn't correctly set)") - continue - if(!prototype::item) - TEST_FAIL("[prototype] doesn't have a set item (or the abstract path var isn't correctly set)") - continue - if(isnull(prototype::price) && prototype::price_max <= prototype::price_min) - TEST_FAIL("[prototype] doesn't have a correctly set random price (price_max should be higher than price_min)") - if(isnull(prototype::stock) && prototype::stock_max < prototype::stock_min) - TEST_FAIL("[prototype] doesn't have a correctly set random stock (stock_max shouldn't be lower than stock_min)") - if(!isnum(prototype::availability_prob)) - TEST_FAIL("[prototype] doesn't have a set availability_prob (must be a number)") - if(!prototype::name) - TEST_FAIL("[prototype] doesn't have a set name") - if(!prototype::desc) - TEST_FAIL("[prototype] doesn't have a set desc") diff --git a/code/modules/unit_tests/market.dm b/code/modules/unit_tests/market.dm new file mode 100644 index 00000000000..022f277d879 --- /dev/null +++ b/code/modules/unit_tests/market.dm @@ -0,0 +1,59 @@ +#define CATEGORY_CODERBUS "Coderbus" +/// Ensures market items have acceptable variable values and restocking works. +/datum/unit_test/market + +/datum/unit_test/market/Run() + for(var/datum/market_item/prototype as anything in subtypesof(/datum/market_item)) + if(prototype::abstract_path == prototype) //skip abstract paths + continue + if(!prototype::category) + TEST_FAIL("[prototype] doesn't have a set category (or the abstract path var isn't correctly set)") + continue + if(!prototype::item) + TEST_FAIL("[prototype] doesn't have a set item (or the abstract path var isn't correctly set)") + continue + if(isnull(prototype::price) && prototype::price_max <= prototype::price_min) + TEST_FAIL("[prototype] doesn't have a correctly set random price (price_max should be higher than price_min)") + if(isnull(prototype::stock) && prototype::stock_max < prototype::stock_min) + TEST_FAIL("[prototype] doesn't have a correctly set random stock (stock_max shouldn't be lower than stock_min)") + if(!isnum(prototype::availability_prob)) + TEST_FAIL("[prototype] doesn't have a set availability_prob (must be a number)") + if(!prototype::name) + TEST_FAIL("[prototype] doesn't have a set name") + if(!prototype::desc) + TEST_FAIL("[prototype] doesn't have a set desc") + + + var/datum/market/unit_test/market = SSmarket.markets[/datum/market/unit_test] + TEST_ASSERT(market, "Couldn't find the unit test market") + var/list/category_items = market.available_items[CATEGORY_CODERBUS] + var/datum/market_item/unit_test/item = category_items[category_items[1]] + TEST_ASSERT(item, "Couldn't find the unit test market item") + TEST_ASSERT_EQUAL(item.stock, 1, "The unit test market item is incorrectly stocked. Only one should be in stock") + + var/mob/living/user = allocate(/mob/living) + var/obj/item/holochip/chip = allocate(/obj/item/holochip, run_loc_floor_bottom_left, INFINITY) + var/obj/machinery/ltsrbt/pad = allocate(/obj/machinery/ltsrbt) + + pad.item_interaction(user, chip) + + TEST_ASSERT_EQUAL(item, category_items[category_items[1]], "The unit test market item has been replaced during restock") + TEST_ASSERT_EQUAL(item.stock, 2, "The unit test market item is incorrectly stocked after restock. There should be two in stock") + +/datum/market/unit_test + name = "Unit Test Market" + shipping = list(SHIPPING_METHOD_TELEPORT = 0) + +/datum/market_item/unit_test + name = "Your Own Special Singularity" + desc = "ALL HAIL LORD SINGULOTH!!!" + category = CATEGORY_CODERBUS + markets = list(/datum/market/unit_test) + item = /obj/singularity + price = 42069 + stock_min = 1 + stock = 1 + stock_max = 2 + availability_prob = 100 + +#undef CATEGORY_CODERBUS diff --git a/icons/obj/machines/telecomms.dmi b/icons/obj/machines/telecomms.dmi index 1af082171ac16bc9c3826cf3a1922094ab612506..f1380268c29cf09a2be28fc154b26bd6cd6e3de6 100644 GIT binary patch delta 6111 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

)Kf@3Gx8W|l*91;{B zOID^NV}t2z2R4f$Kxjs8k;my8^RlzyAfRG3*$ZR`ZvKt5E)i^p*&-Xgd_%nar)%Dtly@5tJFMDeJ zYufqivKV|O>Uq}Y9DeCbjl9U?v9&V)HLvm)wH%`0U7BeNQkcu~@tYNnb!sGE&DfIz zzFh7)&}m=#|ATh&&^cCkpIuWn3D1q%6)6-5fe53Fs`+&TrgQvXnXzkE*_KFr&sepW zh!;M$ClY!$wnC#=EZRx5=|9%37XQ4SsnOR%qq8}f$;RPLlElPY%Rhh&{vl-El1-u+ z_86;rW2t2o(owv1Ho(-B-_g<0mj%KS%>F?^Ds=2@-+lNracl-UYEmZ!c*JAJFLJVt z|H>w=T`vzunJn1d7{oqr9%5Tp+T;&ljv+$-w^OBg2P6(kS8nv#+d5RKQ_+Yj^+ z2tM8tTm+$#hMQoK>%yxSFH)4es&ggBgcdnq%TE_!%^HBc%B>g=;pB{=;Z_;JuHo|8 zj7<+mN+*4;7nd93JGxdeN}TQJ6?(FX1?W|tM=6HyxM4fOLe(5qL5Navb~-aS2IIEF zih(_rapQ(*suwv@ic?n147ryOq5d=JC0tp*)yYz1a4ue-PZ!1C9Ny@-cUo07{)F8J z3&Us_*ZdcOzS@dBHfH$h<#WNb_FbPW8!4%kYfq9xS|~)rtbTj|DoR{)w}At%ayZG_ zG)Kw6&-k~L^p@N`Uq_nF4o(~`KweR~4|wbXKl}R+C8}6N*nT-!P*BjnHkkX`NEmgg zp5r>W?rnFjGyMM`E);?q5}5x&>-|iG-Mfr^#n^|dA$9^6Az|)v!2G1pq^ahivhl;k=qi>guSoJijadX$I8(%>>OvVj@BN&bV~Hm)L;_E^cpWeBcK8DZW)>Xzi`v+GB%NJ zQYxq`8m<0CXB6Gt-EEwn{r>&?hI@Rk2UGquldi9un)n6=TG9xmX&3X$*SVwF^lES4 z93Ri!qyOmugWfpb)g{&Cu#T@IE^cK$cNI7Ratd=#4Kk*pL@9iS?!I{eI85`y{PG>y zn9262Z*Y*SNF*C9UFQZG7;4Ht8y*=f#h=3V+}Pe6J!lsciB5bm^0p}M%W6?mp}WlRawjOk zJb!l|5IFt&%BKiUYaDu_+Ex-keYW%Pk2l2+ zQ}X45gM+7Nt*u7Wcy5ToEF7F<7sR{i(KqJGTJ)f?TzVeyzWbXkP94jo?-K^wK|J`O z?E44j`csjN@G4SjUKkiB}e=Sdge!{ z#qm!EG_|~curHEvT`wx>Pje%P8xGiI=i-*g#rM^G7G@>ctBOcJeNeV#_sNqdILlUl z_2J>+DZz)8^Fb|)t71(r$L*8ov-tLpP2r4qDEIYP82M4yQQb1MYgtM2xpS(j@(1?+ z`F9Y3I`gMJgqQA!&=yu!LMj%L&o&OkFp?JZbh+e>oFK*{Yb6C067o6cy*v%04F8+_ zl(Hfl4V7msgSQ_T?G|KhzJhgMbb<6~Fp6b&Rwr`EG9dTS`)uy)FK_;n_oEcF#T1;^ z-B>J|ATK;dz&SBzY@Sa;nCd@>jryar;%&~Nd`s{G(H@-2i!%z1MEGS3* zXTp2k?)6E)v%1l&Av(ma*|yNt55XJTvHu})?NZT5&MmD&=v>X@djs#uyWa6C!GHY4 z+G|AY_}=jLew^J@-MqBz*vQZ2CTYd5YvF!yc>bq!)vGtE&eNu7^0fsOqj_i+EWtNf z5m?A=eLHvD&+*NPzc1#}6GnZV-rXyk