diff --git a/code/__DEFINES/economy.dm b/code/__DEFINES/economy.dm index 7e12f3904b6..9d6841b56fd 100644 --- a/code/__DEFINES/economy.dm +++ b/code/__DEFINES/economy.dm @@ -73,3 +73,30 @@ #define PAYMENT_CLINICAL "clinical" #define PAYMENT_FRIENDLY "friendly" #define PAYMENT_ANGRY "angry" + +#define MARKET_TREND_UPWARD 1 +#define MARKET_TREND_DOWNWARD -1 +#define MARKET_TREND_STABLE 0 + +#define MARKET_EVENT_PROBABILITY 1 //Probability of a market event firing, in percent. Fires once per material, every 20 seconds. + +#define MARKET_PROFIT_MODIFIER 0.8 //We don't make every sale a 1-1 of the actual buy price value, like with real life taxes and to encourage more smart trades + +/// Create quantity subtypes for stock market datums. +#define MARKET_QUANTITY_HELPERS(path) ##path/one {\ + amount = 1; \ +} \ +##path/five {\ + amount = 5; \ +} \ +##path/ten {\ + amount = 10; \ +} \ +##path/twenty_five {\ + amount = 25; \ +} \ +##path/fifty {\ + amount = 50; \ +} + + diff --git a/code/__DEFINES/materials.dm b/code/__DEFINES/materials.dm index 08b88324826..5fc5cc08ea2 100644 --- a/code/__DEFINES/materials.dm +++ b/code/__DEFINES/materials.dm @@ -75,3 +75,13 @@ #define MATERIAL_SLOWDOWN_PLASTEEL (0.05) /// The slowdown value of one [SHEET_MATERIAL_AMOUNT] of alien alloy. #define MATERIAL_SLOWDOWN_ALIEN_ALLOY (0.1) + +//Stock market stock values. +/// How much quantity of a material stock exists for common materials like iron & glass. +#define MATERIAL_QUANTITY_COMMON 25000 +/// How much quantity of a material stock exists for uncommon materials like silver & titanium. +#define MATERIAL_QUANTITY_UNCOMMON 10000 +/// How much quantity of a material stock exists for rare materials like gold, uranium, & diamond. +#define MATERIAL_QUANTITY_RARE 2500 +/// How much quantity of a material stock exists for exotic materials like diamond & bluespace crystals. +#define MATERIAL_QUANTITY_EXOTIC 500 diff --git a/code/controllers/subsystem/economy.dm b/code/controllers/subsystem/economy.dm index b560f12ee20..68fb43d7c2e 100644 --- a/code/controllers/subsystem/economy.dm +++ b/code/controllers/subsystem/economy.dm @@ -122,6 +122,8 @@ SUBSYSTEM_DEF(economy) var/effective_mailcount = round(living_player_count()/(inflation_value - 0.5)) //More mail at low inflation, and vis versa. mail_waiting += clamp(effective_mailcount, 1, MAX_MAIL_PER_MINUTE * seconds_per_tick) + SSstock_market.news_string = "" + /** * Handy proc for obtaining a department's bank account, given the department ID, AKA the define assigned for what department they're under. */ @@ -172,7 +174,7 @@ SUBSYSTEM_DEF(economy) fluff_string = ", but company countermeasures protect YOU from being affected!" else fluff_string = ", and company countermeasures are failing to protect YOU from being affected. We're all doomed!" - earning_report = "Sector Economic Report

Sector vendor prices is currently at [SSeconomy.inflation_value()*100]%[fluff_string]

The station spending power is currently [station_total] Credits, and the crew's targeted allowance is at [station_target] Credits.

That's all from the Nanotrasen Economist Division." + earning_report = "Sector Economic Report

Sector vendor prices is currently at [SSeconomy.inflation_value()*100]%[fluff_string]

The station spending power is currently [station_total] Credits, and the crew's targeted allowance is at [station_target] Credits.

[SSstock_market.news_string] That's all from the Nanotrasen Economist Division." GLOB.news_network.submit_article(earning_report, "Station Earnings Report", "Station Announcements", null, update_alert = FALSE) return TRUE diff --git a/code/controllers/subsystem/stock_market.dm b/code/controllers/subsystem/stock_market.dm new file mode 100644 index 00000000000..7c2cb71dc49 --- /dev/null +++ b/code/controllers/subsystem/stock_market.dm @@ -0,0 +1,154 @@ + +SUBSYSTEM_DEF(stock_market) + name = "Stock Market" + wait = 20 SECONDS + init_order = INIT_ORDER_DEFAULT + runlevels = RUNLEVEL_GAME + + /// Associated list of materials and their prices at the given time. + var/list/materials_prices = list() + /// Associated list of materials alongside their market trends. 1 is up, 0 is stable, -1 is down. + var/list/materials_trends = list() + /// Associated list of materials alongside the life of it's current trend. After it's life is up, it will change to a new trend. + var/list/materials_trend_life = list() + /// Associated list of materials alongside their available quantity. This is used to determine how much of a material is available to buy, and how much buying and selling affects the price. + var/list/materials_quantity = list() + /// HTML string that is used to display the market events to the player. + var/news_string = "" + +/datum/controller/subsystem/stock_market/Initialize() + for(var/datum/material/possible_market as anything in subtypesof(/datum/material)) // I need to make this work like this, but lets hardcode it for now + if(initial(possible_market.tradable)) + materials_prices += possible_market + materials_prices[possible_market] = initial(possible_market.value_per_unit) * SHEET_MATERIAL_AMOUNT + + materials_trends += possible_market + materials_trends[possible_market] = rand(MARKET_TREND_DOWNWARD,MARKET_TREND_UPWARD) //aka -1 to 1 + + materials_trend_life += possible_market + materials_trend_life[possible_market] = rand(1,10) + + materials_quantity += possible_market + materials_quantity[possible_market] = initial(possible_market.tradable_base_quantity) + (rand(-initial(possible_market.tradable_base_quantity) * 0.5, initial(possible_market.tradable_base_quantity) * 0.5)) + return SS_INIT_SUCCESS +/datum/controller/subsystem/stock_market/fire(resumed) + for(var/datum/material/market as anything in materials_prices) + handle_trends_and_price(market) + +/** + * Handles shifts in the cost of materials, and in what direction the material is most likely to move. + */ +/datum/controller/subsystem/stock_market/proc/handle_trends_and_price(datum/material/mat) + if(prob(MARKET_EVENT_PROBABILITY)) + handle_market_event(mat) + return + var/trend = materials_trends[mat] + var/trend_life = materials_trend_life[mat] + + var/price_units = materials_prices[mat] + var/price_minimum = round(initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 0.5) + if(!isnull(initial(mat.minimum_value_override))) + price_minimum = round(initial(mat.minimum_value_override) * SHEET_MATERIAL_AMOUNT) + var/price_maximum = round(initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 3) + var/price_baseline = initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT + + var/stock_quantity = materials_quantity[mat] + + if(HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING)) //We hardset to the worst possible price and lowest possible impact if sold + materials_prices[mat] = price_minimum + materials_quantity[mat] = stock_quantity * 2 + materials_trends[mat] = MARKET_TREND_DOWNWARD + trend_life = materials_trend_life[mat] = 1 + return + + if(trend_life == 0) + ///We want to scale our trend so that if we're closer to our minimum or maximum price, we're more likely to trend the other way. + if((price_units < price_baseline)) + var/chance_swap = 100 - ((clamp((price_units - price_minimum), 1, 1000) / (price_baseline - price_minimum))*100) + if(prob(chance_swap)) + materials_trends[mat] = MARKET_TREND_UPWARD + else + materials_trends[mat] = MARKET_TREND_STABLE + else if((price_units > price_baseline)) + var/chance_swap = 100 - ((clamp((price_units - price_maximum), 1, 1000) / (price_maximum - price_baseline))*100) + if(prob(chance_swap)) + materials_trends[mat] = MARKET_TREND_DOWNWARD + else + materials_trends[mat] = MARKET_TREND_STABLE + materials_trend_life[mat] = rand(3,10) // Change our trend life for x number of cycles + else + materials_trend_life[mat] -= 1 + + var/price_change = 0 + var/quantity_change = 0 + switch(trend) + if(MARKET_TREND_UPWARD) + price_change = ROUND_UP(gaussian(price_units * 0.1, price_baseline * 0.05)) //If we don't ceil, small numbers will get trapped at low values + quantity_change = -round(gaussian(stock_quantity * 0.1, stock_quantity * 0.05)) + if(MARKET_TREND_STABLE) + price_change = round(gaussian(0, price_baseline * 0.01)) + quantity_change = round(gaussian(0, stock_quantity * 0.01)) + if(MARKET_TREND_DOWNWARD) + price_change = -ROUND_UP(gaussian(price_units * 0.1, price_baseline * 0.05)) + quantity_change = round(gaussian(stock_quantity * 0.1, stock_quantity * 0.05)) + materials_prices[mat] = round(clamp(price_units + price_change, price_minimum, price_maximum)) + materials_quantity[mat] = round(clamp(stock_quantity + quantity_change, 0, initial(mat.tradable_base_quantity) * 2)) + +/** + * Market events are a way to spice up the market and make it more interesting. + * Randomly one will occur to a random material, and it will change the price of that material more drastically, or reset it to a stable price. + * Events are also broadcast to the newscaster as a fun little fluff piece. Good way to tell some lore as well, or just make a joke. + */ +/datum/controller/subsystem/stock_market/proc/handle_market_event(datum/material/mat) + + var/company_name = list( // Pick a random company name from the list, I let copilot make a few up for me which is why some suck + "Nakamura Engineering", + "Robust Industries, LLC", + "MODular Solutions", + "SolGov", + "Australicus Industrial Mining", + "Vey-Medical", + "Aussec Armory", + "Dreamland Robotics" + ) + var/circumstance + var/event = rand(1,3) + + var/price_units = materials_prices[mat] + var/price_minimum = round(initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 0.5) + if(!isnull(initial(mat.minimum_value_override))) + price_minimum = round(initial(mat.minimum_value_override) * SHEET_MATERIAL_AMOUNT) + var/price_maximum = round(initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 3) + var/price_baseline = initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT + + switch(event) + if(1) //Reset to stable + materials_prices[mat] = price_baseline + materials_trends[mat] = MARKET_TREND_STABLE + materials_trend_life[mat] = 1 + circumstance = pick(list( + "[pick(company_name)] has been bought out by a private investment firm. As a result, [initial(mat.name)] is now stable at [materials_prices[mat]] cr.", + "Due to a corporate restructuring, the largest supplier of [initial(mat.name)] has had the price changed to [materials_prices[mat]] cr.", + "[initial(mat.name)] is now under a monopoly by [pick(company_name)]. The price has been changed to [materials_prices[mat]] cr accordingly." + )) + if(2) //Big boost + materials_prices[mat] += round(gaussian(price_units * 0.5, price_units * 0.1)) + materials_prices[mat] = clamp(materials_prices[mat], price_minimum, price_maximum) + materials_trends[mat] = MARKET_TREND_UPWARD + materials_trend_life[mat] = rand(1,5) + circumstance = pick(list( + "[pick(company_name)] has just released a new product that uses [initial(mat.name)]! As a result, the price has been raised to [materials_prices[mat]] cr.", + "Due to [pick(company_name)] finding a new property of [initial(mat.name)], its price has been raised to [materials_prices[mat]] cr.", + "A study has found that [initial(mat.name)] may run out within the next 100 years. The price has raised to [materials_prices[mat]] cr due to panic." + )) + if(3) //Big drop + materials_prices[mat] -= round(gaussian(price_units * 1.5, price_units * 0.1)) + materials_prices[mat] = clamp(materials_prices[mat], price_minimum, price_maximum) + materials_trends[mat] = MARKET_TREND_DOWNWARD + materials_trend_life[mat] = rand(1,5) + circumstance = pick(list( + "[pick(company_name)]'s latest product has seen major controversy, and as a result, the price of [initial(mat.name)] has dropped to [materials_prices[mat]] cr.", + "Due to a new competitor, the price of [initial(mat.name)] has dropped to [materials_prices[mat]] cr.", + "[initial(mat.name)] has been found to be a carcinogen. The price has dropped to [materials_prices[mat]] cr due to panic." + )) + news_string += circumstance + "
" // Add the event to the news_string, formatted for newscasters. diff --git a/code/datums/materials/_material.dm b/code/datums/materials/_material.dm index d91884d972e..06d26f31ea3 100644 --- a/code/datums/materials/_material.dm +++ b/code/datums/materials/_material.dm @@ -33,8 +33,16 @@ Simple datum which is instanced once per type and is used for every object of sa var/strength_modifier = 1 ///This is a modifier for integrity, and resembles the strength of the material var/integrity_modifier = 1 + ///This is the amount of value per 1 unit of the material var/value_per_unit = 0 + ///This is the minimum value of the material, used in the stock market for any mat that isn't set to null + var/minimum_value_override = null + ///Is this material traded on the stock market? + var/tradable = FALSE + ///If this material is tradable, what is the base quantity of the material on the stock market? + var/tradable_base_quantity = 0 + ///Armor modifiers, multiplies an items normal armor vars by these amounts. var/armor_modifiers = list(MELEE = 1, BULLET = 1, LASER = 1, ENERGY = 1, BOMB = 1, BIO = 1, FIRE = 1, ACID = 1) ///How beautiful is this material per unit. diff --git a/code/datums/materials/basemats.dm b/code/datums/materials/basemats.dm index fcef1b5d3d8..f79b9f7e422 100644 --- a/code/datums/materials/basemats.dm +++ b/code/datums/materials/basemats.dm @@ -7,6 +7,9 @@ categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE, MAT_CATEGORY_ITEM_MATERIAL=TRUE) sheet_type = /obj/item/stack/sheet/iron value_per_unit = 5 / SHEET_MATERIAL_AMOUNT + minimum_value_override = 0 + tradable = TRUE + tradable_base_quantity = MATERIAL_QUANTITY_COMMON /datum/material/iron/on_accidental_mat_consumption(mob/living/carbon/victim, obj/item/source_item) victim.apply_damage(10, BRUTE, BODY_ZONE_HEAD, wound_bonus = 5) @@ -25,6 +28,9 @@ shard_type = /obj/item/shard debris_type = /obj/effect/decal/cleanable/glass value_per_unit = 5 / SHEET_MATERIAL_AMOUNT + minimum_value_override = 0 + tradable = TRUE + tradable_base_quantity = MATERIAL_QUANTITY_COMMON beauty_modifier = 0.05 armor_modifiers = list(MELEE = 0.2, BULLET = 0.2, ENERGY = 1, BIO = 0.2, FIRE = 1, ACID = 0.2) @@ -56,6 +62,8 @@ Unless you know what you're doing, only use the first three numbers. They're in categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE, MAT_CATEGORY_ITEM_MATERIAL=TRUE) sheet_type = /obj/item/stack/sheet/mineral/silver value_per_unit = 50 / SHEET_MATERIAL_AMOUNT + tradable = TRUE + tradable_base_quantity = MATERIAL_QUANTITY_UNCOMMON beauty_modifier = 0.075 /datum/material/silver/on_accidental_mat_consumption(mob/living/carbon/victim, obj/item/source_item) @@ -72,6 +80,8 @@ Unless you know what you're doing, only use the first three numbers. They're in categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE, MAT_CATEGORY_ITEM_MATERIAL=TRUE) sheet_type = /obj/item/stack/sheet/mineral/gold value_per_unit = 125 / SHEET_MATERIAL_AMOUNT + tradable = TRUE + tradable_base_quantity = MATERIAL_QUANTITY_RARE beauty_modifier = 0.15 armor_modifiers = list(MELEE = 1.1, BULLET = 1.1, LASER = 1.15, ENERGY = 1.15, BOMB = 1, BIO = 1, FIRE = 0.7, ACID = 1.1) @@ -90,6 +100,8 @@ Unless you know what you're doing, only use the first three numbers. They're in alpha = 132 starlight_color = COLOR_BLUE_LIGHT value_per_unit = 500 / SHEET_MATERIAL_AMOUNT + tradable = TRUE + tradable_base_quantity = MATERIAL_QUANTITY_EXOTIC beauty_modifier = 0.3 armor_modifiers = list(MELEE = 1.3, BULLET = 1.3, LASER = 0.6, ENERGY = 1, BOMB = 1.2, BIO = 1, FIRE = 1, ACID = 1) @@ -106,6 +118,8 @@ Unless you know what you're doing, only use the first three numbers. They're in categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE, MAT_CATEGORY_ITEM_MATERIAL=TRUE) sheet_type = /obj/item/stack/sheet/mineral/uranium value_per_unit = 100 / SHEET_MATERIAL_AMOUNT + tradable = TRUE + tradable_base_quantity = MATERIAL_QUANTITY_RARE beauty_modifier = 0.3 //It shines so beautiful armor_modifiers = list(MELEE = 1.5, BULLET = 1.4, LASER = 0.5, ENERGY = 0.5, FIRE = 1, ACID = 1) @@ -173,6 +187,8 @@ Unless you know what you're doing, only use the first three numbers. They're in beauty_modifier = 0.5 sheet_type = /obj/item/stack/sheet/bluespace_crystal value_per_unit = 300 / SHEET_MATERIAL_AMOUNT + tradable = TRUE + tradable_base_quantity = MATERIAL_QUANTITY_EXOTIC /datum/material/bluespace/on_accidental_mat_consumption(mob/living/carbon/victim, obj/item/source_item) victim.reagents.add_reagent(/datum/reagent/bluespace, rand(5, 8)) @@ -216,6 +232,8 @@ Unless you know what you're doing, only use the first three numbers. They're in categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE, MAT_CATEGORY_ITEM_MATERIAL=TRUE) sheet_type = /obj/item/stack/sheet/mineral/titanium value_per_unit = 125 / SHEET_MATERIAL_AMOUNT + tradable = TRUE + tradable_base_quantity = MATERIAL_QUANTITY_UNCOMMON beauty_modifier = 0.05 armor_modifiers = list(MELEE = 1.35, BULLET = 1.3, LASER = 1.3, ENERGY = 1.25, BOMB = 1.25, BIO = 1, FIRE = 0.7, ACID = 1) diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm index 95a95aa4bda..faa48559100 100644 --- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm +++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm @@ -1351,6 +1351,15 @@ greyscale_colors = CIRCUIT_COLOR_SUPPLY build_path = /obj/machinery/rnd/production/techfab/department/cargo +/obj/item/circuitboard/machine/materials_market + name = "Galactic Materials Market" + greyscale_colors = CIRCUIT_COLOR_SUPPLY + build_path = /obj/machinery/materials_market + req_components = list( + /obj/item/stack/cable_coil = 5, + /datum/stock_part/scanning_module = 1, + /datum/stock_part/card_reader = 1) + //Misc /obj/item/circuitboard/machine/sheetifier name = "Sheet-meister 2000" diff --git a/code/modules/cargo/exports/materials.dm b/code/modules/cargo/exports/materials.dm index 06c52305f51..46d089b5ac5 100644 --- a/code/modules/cargo/exports/materials.dm +++ b/code/modules/cargo/exports/materials.dm @@ -2,10 +2,13 @@ cost = 5 // Cost per SHEET_MATERIAL_AMOUNT, which is 100cm3 as of May 2023. message = "cm3 of developer's tears. Please, report this on github" amount_report_multiplier = SHEET_MATERIAL_AMOUNT - var/material_id = null + var/datum/material/material_id = null export_types = list( - /obj/item/stack/sheet/mineral, /obj/item/stack/tile/mineral, - /obj/item/stack/ore, /obj/item/coin) + /obj/item/stack/sheet/mineral, + /obj/item/stack/tile/mineral, + /obj/item/stack/ore, + /obj/item/coin + ) // Yes, it's a base type containing export_types. // But it has no material_id, so any applies_to check will return false, and these types reduce amount of copypasta a lot @@ -27,7 +30,14 @@ return round(amount / SHEET_MATERIAL_AMOUNT) -// Materials. Nothing but plasma is really worth selling. Better leave it all to RnD and sell some plasma instead. +// Materials. Static materials exist as parent types, while materials subject to the stock market have a fluid cost as determined by material/market types +// If you're adding a new material to the stock market, make sure its export type is added here. + +/datum/export/material/plasma + cost = CARGO_CRATE_VALUE * 0.4 + k_elasticity = 0 + material_id = /datum/material/plasma + message = "cm3 of plasma" /datum/export/material/bananium cost = CARGO_CRATE_VALUE * 2 @@ -35,37 +45,6 @@ message = "cm3 of bananium" /datum/export/material/diamond - cost = CARGO_CRATE_VALUE - material_id = /datum/material/diamond - message = "cm3 of diamonds" - -/datum/export/material/plasma - cost = CARGO_CRATE_VALUE * 0.4 - k_elasticity = 0 - material_id = /datum/material/plasma - message = "cm3 of plasma" - -/datum/export/material/uranium - cost = CARGO_CRATE_VALUE * 0.2 - material_id = /datum/material/uranium - message = "cm3 of uranium" - -/datum/export/material/gold - cost = CARGO_CRATE_VALUE * 0.25 - material_id = /datum/material/gold - message = "cm3 of gold" - -/datum/export/material/silver - cost = CARGO_CRATE_VALUE * 0.1 - material_id = /datum/material/silver - message = "cm3 of silver" - -/datum/export/material/titanium - cost = CARGO_CRATE_VALUE * 0.25 - material_id = /datum/material/titanium - message = "cm3 of titanium" - -/datum/export/material/adamantine cost = CARGO_CRATE_VALUE material_id = /datum/material/adamantine message = "cm3 of adamantine" @@ -75,11 +54,6 @@ material_id = /datum/material/mythril message = "cm3 of mythril" -/datum/export/material/bscrystal - cost = CARGO_CRATE_VALUE * 0.6 - message = "of bluespace crystals" - material_id = /datum/material/bluespace - /datum/export/material/plastic cost = CARGO_CRATE_VALUE * 0.05 message = "cm3 of plastic" @@ -90,21 +64,6 @@ message = "cm3 of runite" material_id = /datum/material/runite -/datum/export/material/iron - cost = CARGO_CRATE_VALUE * 0.01 - message = "cm3 of iron" - material_id = /datum/material/iron - export_types = list( - /obj/item/stack/sheet/iron, /obj/item/stack/tile/iron, - /obj/item/stack/rods, /obj/item/stack/ore, /obj/item/coin) - -/datum/export/material/glass - cost = CARGO_CRATE_VALUE * 0.01 - message = "cm3 of glass" - material_id = /datum/material/glass - export_types = list(/obj/item/stack/sheet/glass, /obj/item/stack/ore, - /obj/item/shard) - /datum/export/material/hot_ice cost = CARGO_CRATE_VALUE * 0.8 message = "cm3 of Hot Ice" @@ -116,3 +75,90 @@ message = "cm3 of metallic hydrogen" material_id = /datum/material/metalhydrogen export_types = /obj/item/stack/sheet/mineral/metal_hydrogen + +/datum/export/material/market + +/datum/export/material/market/diamond + material_id = /datum/material/diamond + message = "cm3 of diamonds" + +/datum/export/material/market/uranium + material_id = /datum/material/uranium + message = "cm3 of uranium" + +/datum/export/material/market/gold + material_id = /datum/material/gold + message = "cm3 of gold" + +/datum/export/material/market/silver + material_id = /datum/material/silver + message = "cm3 of silver" + +/datum/export/material/market/titanium + material_id = /datum/material/titanium + message = "cm3 of titanium" + +/datum/export/material/market/bscrystal + message = "of bluespace crystals" + material_id = /datum/material/bluespace + export_types = list(/obj/item/stack/sheet/bluespace_crystal, /obj/item/stack/ore) //For whatever reason, bluespace crystals are not a mineral + +/datum/export/material/market/iron + message = "cm3 of iron" + material_id = /datum/material/iron + export_types = list( + /obj/item/stack/sheet/iron, + /obj/item/stack/tile/iron, + /obj/item/stack/rods, + /obj/item/stack/ore, + /obj/item/coin + ) + +/datum/export/material/market/glass + message = "cm3 of glass" + material_id = /datum/material/glass + export_types = list( + /obj/item/stack/sheet/glass, + /obj/item/stack/ore, + /obj/item/shard + ) + +/datum/export/material/market/get_cost(obj/O, apply_elastic = FALSE) + var/obj/item/I = O + var/amount = get_amount(I) + if(!amount) + return 0 + var/material_value = (SSstock_market.materials_prices[material_id]) * amount * MARKET_PROFIT_MODIFIER + return round(material_value) + +/datum/export/material/market/sell_object(obj/sold_item, datum/export_report/report, dry_run, apply_elastic) + . = ..() + var/amount = get_amount(sold_item) + var/price = get_cost(sold_item) + if(!amount) + return + if(!dry_run) + SSstock_market.materials_quantity[material_id] += amount + SSstock_market.materials_prices[material_id] -= round((price) * (amount / (amount + SSstock_market.materials_quantity[material_id]))) + //This formula should impact lower quantity materials greater, and higher quantity materials less. Still, it's a bit rough. Tweaking may be needed. + + +// Stock blocks are a special type of export that can be used to sell a quantity of materials at a specific price on the market. +/datum/export/stock_block + cost = 0 + message = "stock block" + export_types = list(/obj/item/stock_block) + +/datum/export/stock_block/get_cost(obj/O, apply_elastic = FALSE) + var/obj/item/stock_block/block = O + return block.export_value + +/datum/export/stock_block/sell_object(obj/sold_item, datum/export_report/report, dry_run, apply_elastic) + . = ..() + if(dry_run) + return + var/obj/item/stock_block/sold_block = sold_item + var/sale_value = sold_block.export_value + SSstock_market.materials_quantity[sold_block.export_mat] += sold_block.quantity + SSstock_market.materials_prices[sold_block.export_mat] -= round((sale_value) * (sold_block.quantity / (sold_block.quantity + SSstock_market.materials_quantity[sold_block.export_mat]))) + SSstock_market.materials_prices[sold_block.export_mat] = round(clamp(SSstock_market.materials_prices[sold_block.export_mat], sold_block.export_mat.value_per_unit * SHEET_MATERIAL_AMOUNT * 0.5 , sold_block.export_mat.value_per_unit * SHEET_MATERIAL_AMOUNT * 3)) diff --git a/code/modules/cargo/materials_market.dm b/code/modules/cargo/materials_market.dm new file mode 100644 index 00000000000..d211df7debd --- /dev/null +++ b/code/modules/cargo/materials_market.dm @@ -0,0 +1,259 @@ +/obj/machinery/materials_market + name = "galactic materials market" + desc = "This machine allows the user to buy and sell sheets of minerals \ + across the system. Prices are known to fluxuate quite often,\ + sometimes even within the same minute. All transactions are final." + circuit = /obj/item/circuitboard/machine/materials_market + req_access = list(ACCESS_CARGO) + density = TRUE + icon = 'icons/obj/economy.dmi' + icon_state = "mat_market" + base_icon_state = "mat_market" + idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION + /// What items can be converted into a stock block? Must be a stack subtype based on current implementation. + var/list/exportable_material_items = list( + /obj/item/stack/sheet/iron, //God why are we like this + /obj/item/stack/sheet/glass, //No really, God why are we like this + /obj/item/stack/sheet/mineral, + /obj/item/stack/tile/mineral, + /obj/item/stack/ore, + /obj/item/stack/sheet/bluespace_crystal, + /obj/item/stack/rods + ) + /// Are we ordering sheets from our own card balance or the cargo budget? + var/ordering_private = TRUE + /// Currently, can we order sheets from our own card balance or the cargo budget? + var/can_buy_via_budget = FALSE + +/obj/machinery/materials_market/update_icon_state() + if(panel_open) + icon_state = "[base_icon_state]_open" + return ..() + if(!is_operational || !anchored) + icon_state = "[base_icon_state]_off" + return ..() + icon_state = "[base_icon_state]" + return ..() + +/obj/machinery/materials_market/wrench_act(mob/living/user, obj/item/tool) + ..() + default_unfasten_wrench(user, tool, time = 1.5 SECONDS) + return TOOL_ACT_TOOLTYPE_SUCCESS + +/obj/machinery/materials_market/attackby(obj/item/O, mob/user, params) + if(default_deconstruction_screwdriver(user, "[base_icon_state]_open", "[base_icon_state]", O)) + return + else if(default_deconstruction_crowbar(O)) + return + if(is_type_in_list(O, exportable_material_items)) + var/amount = 0 + var/value = 0 + var/material_to_export + var/obj/item/stack/exportable = O + for(var/datum/material/mat as anything in SSstock_market.materials_prices) + if(exportable.has_material_type(mat)) + amount = exportable.amount + value = SSstock_market.materials_prices[mat] + material_to_export = mat + break //This is only for trading non-alloys, so we can break here + + if(!amount) + say("Not enough material. Aborting.") + playsound(src, 'sound/machines/scanbuzz.ogg', 25, FALSE) + return TRUE + qdel(exportable) + var/obj/item/stock_block/new_block = new /obj/item/stock_block(drop_location()) + new_block.export_value = amount * value * MARKET_PROFIT_MODIFIER + new_block.export_mat = material_to_export + new_block.quantity = amount + to_chat(user, span_notice("You have created a stock block worth [new_block.export_value] cr! Sell it before it becomes liquid!")) + playsound(src, 'sound/machines/synth_yes.ogg', 50, FALSE) + return TRUE + return ..() + + +/obj/machinery/materials_market/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!anchored) + return + if(!ui) + ui = new(user, src, "MatMarket", name) + ui.open() + +/obj/machinery/materials_market/ui_data(mob/user) + var/data = list() + var/material_data + for(var/datum/material/traded_mat as anything in SSstock_market.materials_prices) + var/trend_string = "" + if(SSstock_market.materials_trends[traded_mat] == 0) + trend_string = "neutral" + else if(SSstock_market.materials_trends[traded_mat] == 1) + trend_string = "up" + else if(SSstock_market.materials_trends[traded_mat] == -1) + trend_string = "down" + var/color_string = "" + if(traded_mat.color) + color_string = traded_mat.color + else if (traded_mat.greyscale_colors) + color_string = splicetext(traded_mat.greyscale_colors, 6, length(traded_mat.greyscale_colors), "") //slice it to a standard 6 char hex + material_data += list(list( + "name" = traded_mat.name, + "price" = SSstock_market.materials_prices[traded_mat], + "quantity" = SSstock_market.materials_quantity[traded_mat], + "trend" = trend_string, + "color" = color_string, + )) + + can_buy_via_budget = FALSE + var/obj/item/card/id/used_id_card + if(isliving(user)) + var/mob/living/living_user = user + used_id_card = living_user.get_idcard(TRUE) + can_buy_via_budget = (ACCESS_CARGO in used_id_card?.GetAccess()) + + var/balance = 0 + if(!ordering_private) + var/datum/bank_account/dept = SSeconomy.get_dep_account(ACCOUNT_CAR) + if(dept) + balance = dept.account_balance + else + balance = used_id_card?.registered_account?.account_balance + + var/market_crashing = FALSE + if(HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING)) + market_crashing = TRUE + + data["catastrophe"] = market_crashing + data["materials"] = material_data + data["creditBalance"] = balance + data["orderingPrive"] = ordering_private + data["canOrderCargo"] = can_buy_via_budget + return data + +/obj/machinery/materials_market/ui_act(action, params) + . = ..() + if(.) + return + if(!isliving(usr)) + return + switch(action) + if("buy") + var/material_str = params["material"] + var/quantity = text2num(params["quantity"]) + + var/datum/material/material_bought + var/obj/item/stack/sheet/sheet_to_buy + for(var/datum/material/mat as anything in SSstock_market.materials_prices) + if(mat.name == material_str) + material_bought = mat + break + if(!material_bought) + CRASH("Invalid material name passed to materials market!") + var/mob/living/living_user = usr + var/datum/bank_account/account_payable = SSeconomy.get_dep_account(ACCOUNT_CAR) + if(ordering_private) + var/obj/item/card/id/used_id_card = living_user.get_idcard(TRUE) + account_payable = used_id_card.registered_account + else if(can_buy_via_budget) + account_payable = SSeconomy.get_dep_account(ACCOUNT_CAR) + + var/cost = SSstock_market.materials_prices[material_bought] * quantity + + sheet_to_buy = material_bought.sheet_type + if(!sheet_to_buy) + CRASH("Material with no sheet type being sold on materials market!") + if(!account_payable) + say("No bank account detected!") + return + if(cost > account_payable.account_balance) + to_chat(living_user, span_warning("You don't have enough money to buy that!")) + return + var/list/things_to_order = list() + things_to_order += (sheet_to_buy) + things_to_order[sheet_to_buy] = quantity + // We want to count how many stacks of all sheets we're ordering to make sure they don't exceed the limit of 10 + //If we already have a custom order on SSshuttle, we should add the things to order to that order + for(var/datum/supply_order/order in SSshuttle.shopping_list) + if(order.orderer == living_user && order.orderer_rank == "Galactic Materials Market") + var/prior_stacks = 0 + for(var/obj/item/stack/sheet/sheet as anything in order.pack.contains) + prior_stacks += ROUND_UP(order.pack.contains[sheet] / 50) + if(prior_stacks >= 10) + to_chat(usr, span_notice("You already have 10 stacks of sheets on order! Please wait for them to arrive before ordering more.")) + playsound(usr, 'sound/machines/synth_no.ogg', 35, FALSE) + return + order.append_order(things_to_order, cost) + account_payable.adjust_money(-(cost) , "Materials Market Purchase") //Add the extra price to the total + return + account_payable.adjust_money(-(CARGO_CRATE_VALUE) , "Materials Market Purchase") //Here is where we factor in the base cost of a crate + //Now we need to add a cargo order for quantity sheets of material_bought.sheet_type + var/datum/supply_pack/custom/minerals/mineral_pack = new( + purchaser = living_user, \ + cost = SSstock_market.materials_prices[material_bought] * quantity, \ + contains = things_to_order, \ + ) + var/datum/supply_order/new_order = new( + pack = mineral_pack, + orderer = living_user, + orderer_rank = "Galactic Materials Market", + orderer_ckey = living_user.ckey, + reason = "", + paying_account = account_payable, + department_destination = null, + coupon = null, + charge_on_purchase = FALSE, + manifest_can_fail = FALSE, + cost_type = "credit", + can_be_cancelled = FALSE, + ) + say("Thank you for your purchase! It will arrive on the next cargo shuttle!") + SSshuttle.shopping_list += new_order + return + if("toggle_budget") + if(!can_buy_via_budget) + return + ordering_private = !ordering_private + + +/obj/item/stock_block + name = "stock block" + desc = "A block of stock. It's worth a certain amount of money, based on a sale on the materials market. Ship it on the cargo shuttle to claim your money." + icon = 'icons/obj/economy.dmi' + icon_state = "stock_block" + /// How many credits was this worth when created? + var/export_value = 0 + /// What is the name of the material this was made from? + var/datum/material/export_mat + /// Quantity of export material + var/quantity = 0 + /// Is this stock block currently updating it's value with the market (aka fluid)? + var/fluid = FALSE + +/obj/item/stock_block/examine(mob/user) + . = ..() + . += span_notice("\The [src] is worth [export_value] cr, from selling [quantity] sheets of [export_mat?.name].") + if(fluid) + . += span_warning("\The [src] is currently liquid! It's value is based on the market price.") + else + . += span_notice("\The [src]'s value is still [span_boldnotice("locked in")]. [span_boldnotice("Sell it")] before it's value becomes liquid!") + +/obj/item/stock_block/Initialize(mapload) + . = ..() + addtimer(CALLBACK(src, PROC_REF(value_warning)), 2.5 MINUTES) + addtimer(CALLBACK(src, PROC_REF(update_value)), 5 MINUTES) + +/obj/item/stock_block/proc/value_warning() + visible_message(span_warning("\The [src] is starting to become liquid!")) + icon_state = "stock_block_fluid" + update_appearance(UPDATE_ICON_STATE) + +/obj/item/stock_block/proc/update_value() + if(!export_mat) + return + if(!SSstock_market.materials_prices[export_mat]) + return + export_value = quantity * SSstock_market.materials_prices[export_mat] * MARKET_PROFIT_MODIFIER + icon_state = "stock_block_liquid" + update_appearance(UPDATE_ICON_STATE) + visible_message(span_warning("\The [src] becomes liquid!")) + diff --git a/code/modules/cargo/order.dm b/code/modules/cargo/order.dm index 6c1f5e1d839..2707719c170 100644 --- a/code/modules/cargo/order.dm +++ b/code/modules/cargo/order.dm @@ -188,6 +188,15 @@ generateManifest(miscbox, misc_own, "", misc_cost) return +/datum/supply_order/proc/append_order(list/new_contents, cost_increase) + for(var/i as anything in new_contents) + if(pack.contains[i]) + pack.contains[i] += new_contents[i] + else + pack.contains += i + pack.contains[i] = new_contents[i] + pack.cost += cost_increase + #undef MANIFEST_ERROR_CHANCE #undef MANIFEST_ERROR_NAME #undef MANIFEST_ERROR_CONTENTS diff --git a/code/modules/cargo/packs/_packs.dm b/code/modules/cargo/packs/_packs.dm index 4d7e5066a4a..aaeb55f2533 100644 --- a/code/modules/cargo/packs/_packs.dm +++ b/code/modules/cargo/packs/_packs.dm @@ -110,3 +110,15 @@ name = "[purchaser]'s Mining Order" src.cost = cost src.contains = contains + +/datum/supply_pack/custom/minerals + name = "materials order" + crate_name = "galactic materials market delivery crate" + access = list() + crate_type = /obj/structure/closet/crate/cardboard + +/datum/supply_pack/custom/minerals/New(purchaser, cost, list/contains) + . = ..() + name = "[purchaser]'s Materials Order" + src.cost = cost + src.contains = contains diff --git a/code/modules/cargo/packs/imports.dm b/code/modules/cargo/packs/imports.dm index 31e7c2b7f9e..6569321a164 100644 --- a/code/modules/cargo/packs/imports.dm +++ b/code/modules/cargo/packs/imports.dm @@ -299,3 +299,17 @@ contraband = TRUE contains = list(/obj/item/weaponcrafting/giant_wrench) crate_name = "unknown parts crate" + +/datum/supply_pack/imports/materials_market + name = "Galactic Materials Market Crate" + desc = "A circuit board to build your own materials market for use by certified market traders. Warning: Losses are not covered by insurance." + cost = CARGO_CRATE_VALUE * 3 + contains = list( + /obj/item/circuitboard/machine/materials_market = 1, + /obj/item/stack/sheet/iron = 5, + /obj/item/stack/cable_coil/five = 2, + /obj/item/stock_parts/scanning_module = 1, + /obj/item/stock_parts/card_reader = 1 + ) + crate_name = "materials market crate" + crate_type = /obj/structure/closet/crate diff --git a/code/modules/cargo/packs/materials.dm b/code/modules/cargo/packs/materials.dm index 68dacd730be..ba9a162698b 100644 --- a/code/modules/cargo/packs/materials.dm +++ b/code/modules/cargo/packs/materials.dm @@ -16,34 +16,6 @@ contains = list(/obj/item/stack/license_plates/empty/fifty) crate_name = "empty license plate crate" -/datum/supply_pack/materials/glass50 - name = "50 Glass Sheets" - desc = "Let some nice light in with fifty glass sheets!" - cost = CARGO_CRATE_VALUE * 2 - contains = list(/obj/item/stack/sheet/glass/fifty) - crate_name = "glass sheets crate" - -/datum/supply_pack/materials/iron50 - name = "50 Iron Sheets" - desc = "Any construction project begins with a good stack of fifty iron sheets!" - cost = CARGO_CRATE_VALUE * 2 - contains = list(/obj/item/stack/sheet/iron/fifty) - crate_name = "iron sheets crate" - -/datum/supply_pack/materials/plasteel20 - name = "20 Plasteel Sheets" - desc = "Reinforce the station's integrity with twenty plasteel sheets!" - cost = CARGO_CRATE_VALUE * 15 - contains = list(/obj/item/stack/sheet/plasteel/twenty) - crate_name = "plasteel sheets crate" - -/datum/supply_pack/materials/plasteel50 - name = "50 Plasteel Sheets" - desc = "For when you REALLY have to reinforce something." - cost = CARGO_CRATE_VALUE * 33 - contains = list(/obj/item/stack/sheet/plasteel/fifty) - crate_name = "plasteel sheets crate" - /datum/supply_pack/materials/plastic50 name = "50 Plastic Sheets" desc = "Build a limitless amount of toys with fifty plastic sheets!" diff --git a/code/modules/cargo/packs/stock_market_items.dm b/code/modules/cargo/packs/stock_market_items.dm new file mode 100644 index 00000000000..04b2eac4acf --- /dev/null +++ b/code/modules/cargo/packs/stock_market_items.dm @@ -0,0 +1,36 @@ +/** + * todo: make this a supply_pack/custom. Drop pog? ohoho yes. Would be VERY fun. + */ +/datum/supply_pack/market_materials + name = "A Single Sheet of Bananium" + desc = "Going market price for this kind of sheet, by Australicus Industrial Mining." + cost = CARGO_CRATE_VALUE * 2 + // contains = list(/obj/item/stack/sheet/mineral/bananium) + crate_name = "mineral stock sheet crate" + group = "Canisters & Materials" + /// What material we are trying to buy sheets of? + var/datum/material/material + /// How many sheets of the material we are trying to buy at once? + var/amount + +/datum/supply_pack/market_materials/get_cost() + for(var/datum/material/mat in SSstock_market.materials_prices) + if(material == mat) + return SSstock_market.materials_prices[mat] * amount + +/datum/supply_pack/market_materials/fill(obj/structure/closet/crate/C) + . = ..() + new material.sheet_type(C, amount) + +/datum/supply_pack/market_materials/iron + name = "Iron Sheets" + crate_name = "iron stock crate" + material = /datum/material/iron +MARKET_QUANTITY_HELPERS(/datum/supply_pack/market_materials/iron) + + +/datum/supply_pack/market_materials/gold + name = "Gold Sheets" + crate_name = "gold stock crate" + material = /datum/material/gold +MARKET_QUANTITY_HELPERS(/datum/supply_pack/market_materials/gold) diff --git a/icons/obj/economy.dmi b/icons/obj/economy.dmi index dc90265b6e9..04abc41cae1 100644 Binary files a/icons/obj/economy.dmi and b/icons/obj/economy.dmi differ diff --git a/tgstation.dme b/tgstation.dme index bc2c5de3e86..35b9c7c2dcc 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -646,6 +646,7 @@ #include "code\controllers\subsystem\speech_controller.dm" #include "code\controllers\subsystem\statpanel.dm" #include "code\controllers\subsystem\stickyban.dm" +#include "code\controllers\subsystem\stock_market.dm" #include "code\controllers\subsystem\sun.dm" #include "code\controllers\subsystem\tcgsetup.dm" #include "code\controllers\subsystem\tgui.dm" @@ -3317,6 +3318,7 @@ #include "code\modules\cargo\expressconsole.dm" #include "code\modules\cargo\gondolapod.dm" #include "code\modules\cargo\goodies.dm" +#include "code\modules\cargo\materials_market.dm" #include "code\modules\cargo\order.dm" #include "code\modules\cargo\orderconsole.dm" #include "code\modules\cargo\supplypod.dm" @@ -3376,6 +3378,7 @@ #include "code\modules\cargo\packs\science.dm" #include "code\modules\cargo\packs\security.dm" #include "code\modules\cargo\packs\service.dm" +#include "code\modules\cargo\packs\stock_market_items.dm" #include "code\modules\cargo\packs\vending_restock.dm" #include "code\modules\chatter\chatter.dm" #include "code\modules\client\client_colour.dm" diff --git a/tgui/packages/tgui/interfaces/MatMarket.tsx b/tgui/packages/tgui/interfaces/MatMarket.tsx new file mode 100644 index 00000000000..86f44462cb1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/MatMarket.tsx @@ -0,0 +1,180 @@ +import { useBackend } from '../backend'; +import { Section, Stack, Button, Modal } from '../components'; +import { Window } from '../layouts'; +import { BooleanLike } from 'common/react'; +import { toTitleCase } from 'common/string'; + +type Data = { + orderingPrive: BooleanLike; // you will need to import this + canOrderCargo: BooleanLike; + creditBalance: number; + materials: Material[]; + catastrophe: BooleanLike; +}; + +type Material = { + name: string; + quantity: number; + id: string; // correct this if its a number + trend: string; + price: number; + color: string; +}; + +export const MatMarket = (props, context) => { + const { act, data } = useBackend(context); // this will tell your editor that data is the type listed above + + const { + orderingPrive, + canOrderCargo, + creditBalance, + materials = [], + catastrophe, + } = data; // better to destructure here (style nit) + return ( + + + {!!catastrophe && } +
act('toggle_budget')} + /> + }> + Buy orders for material sheets placed here will be ordered on the next + cargo shipment. +

+ To sell materials, please insert sheets or similar stacks of + materials. All minerals sold on the market directly are subject to an + 20% market fee. To prevent market manipulation, all registered traders + can buy a total of 10 full stacks of materials at a time. +

+ All new purchases will include the cost of the shipped crate, + which may be recycled afterwards. +
+ Current credit balance: {creditBalance || 'zero'} cr. +
+
+ {materials.map((material) => ( +
+ + + + + {toTitleCase(material.name)} + + + + Trading at {material.price} cr. + + + + {material.quantity} sheets of {material.name}{' '} + trading. + + + {toTitleCase(material.name)} is trending{' '} + {material.trend}. + + + + + + + + + + + +
+ ))} +
+
+ ); +}; + +const MarketCrashModal = (props, context) => { + const { act, data } = useBackend(context); + return ( + + ATTENTION! THE MARKET HAS CRASHED +

+ ALL MATERIALS ARE NOW WORTHLESS +

+ TRADING CIRCUIT BREAKER HAS BEEN ENGAGED FOR ALL TRADERS +

+ DO NOT PANIC, WE ARE FIXING THIS +
+ ); +};