Files
749643cddb Reworks cargo market elasticity (#93235)
## About The Pull Request
What it says on the tin. Cargo now has new math for market elasticity,
each parameter and what it does is explained below

**1. Refactor**
1. **k_elasticity**: This is the elasticity as before applied on all
exports except now this is a floating-point value that swings from
`0->1` instead of 10e-10 like is the current case. This in laymen terms
is the percentile of an items actual cost that is sold on cargo [0 means
you get no credits, 0.5 means you get 50% of the items cost, 1 means you
get 100% of the items cost]. Whenever an item is sold on cargo this
value decreases by an amount that is determined by the next variable

2. **k_hit_percentile(default 5%)**: This is the value by which an
export `k_elasticity` decreases whenever an export is successful for
every unit of an item sold. The real formulae by which the exports
elasticity decreases is dependent on the total amount sold and is as
follows
<pre>k_elasticity -= amount sold (1 for most cases except stacks) *
k_hit_percentile</pre>
So subsequent exports yield lesser profits cause the elasticity
decreases. Now the rate at which the elasticity recovers is as follows

3. **k_recovery_time(default 10 minutes)**: This is the time (minimum
unit should be seconds) it takes for the elasticity to rebound back to
100% after it has reached full 0 but recovery process starts immediately
if it decreases at any point. So if elasticity becomes say 50% it means
it would take 5 minutes to reach 100% again

**2. Some Balance changes**
1. Profits earned from exporting gas is linear per mole sold so the more
gas you put in the more profits you get HOWEVER the max number of
credits you can make per canister is 15000 cr
2. Selling fish yields higher prices because it's no longer subject to
the old elasticity formula
3. Selling 50 sheets of anything will decrease future sale price by 10%
and will take 8 minutes to rebound back to 100% if it reaches 0

**3. Improvements:**
- `SSProcessing` subsystem no longer processes more than 180+ export
datums from round start itself but now starts out empty. It instead
processes only those exports whose elasticity has been impacted and
later cancels it after elasticity has reached 100% so performance of
this subsystem has been drastically improved
- export datums now respect `abstract_type` meaning they won't be
created and can be used as a skeleton body for subtypes. So datums like
`datum/material` & `datum/material/market` are not created anymore but
only their subtypes are so we save processing power & memory
 - Shaved of a lot of dead exports that went unused

## Changelog
🆑
balance: cargo exports now have different prices with applied elasticity
code: Improved performance of export code
qol: stock blocks can be recycled for materials & show up as stock
blocks in order console sold items
refactor: refactored cargo export code in whole. Report bugs on GitHub
/🆑

---------

Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
2025-10-18 04:36:09 -04:00

135 lines
6.4 KiB
Plaintext

SUBSYSTEM_DEF(stock_market)
name = "Stock Market"
wait = 60 SECONDS
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 its current trend. After its 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()
/// A list of all currently active stock market events.
var/list/active_events = 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(possible_market.tradable)
materials_prices[possible_market] = possible_market.value_per_unit * SHEET_MATERIAL_AMOUNT
materials_trends[possible_market] = rand(MARKET_TREND_DOWNWARD,MARKET_TREND_UPWARD) //aka -1 to 1
materials_trend_life[possible_market] = rand(1,3)
materials_quantity[possible_market] = possible_market.tradable_base_quantity + (rand(-(possible_market.tradable_base_quantity) * 0.5, 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)
for(var/datum/stock_market_event/event as anything in active_events)
event.handle()
///Adjust the price of a material(either through buying or selling) ensuring it stays within limits
/datum/controller/subsystem/stock_market/proc/adjust_material_price(datum/material/mat, delta)
mat = GET_MATERIAL_REF(mat)
//adjust the price
var/new_price = materials_prices[mat.type] + delta
//get the limits
var/price_minimum = round(mat.value_per_unit * SHEET_MATERIAL_AMOUNT * 0.5)
if(!isnull(mat.minimum_value_override))
price_minimum = round(mat.minimum_value_override * SHEET_MATERIAL_AMOUNT)
var/price_maximum = round(mat.value_per_unit * SHEET_MATERIAL_AMOUNT * 3)
//clamp it down
new_price = round(clamp(new_price, price_minimum, price_maximum))
materials_prices[mat.type] = new_price
///Adjust the amount of material(either through buying or selling) ensuring it stays within limits
/datum/controller/subsystem/stock_market/proc/adjust_material_quantity(datum/material/mat, delta)
mat = GET_MATERIAL_REF(mat)
//adjust the quantity
var/new_quantity = materials_quantity[mat.type] + delta
//get the upper limit
var/quantity_baseline = mat.tradable_base_quantity
//clamp it down
new_quantity = round(clamp(new_quantity, 0, quantity_baseline * 2))
materials_quantity[mat.type] = new_quantity
/**
* 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)
var/trend = materials_trends[mat]
var/trend_life = materials_trend_life[mat]
var/price_units = materials_prices[mat]
var/price_minimum = round(mat.value_per_unit * SHEET_MATERIAL_AMOUNT * 0.5)
if(!isnull(mat.minimum_value_override))
price_minimum = round(mat.minimum_value_override * SHEET_MATERIAL_AMOUNT)
var/price_maximum = round(mat.value_per_unit * SHEET_MATERIAL_AMOUNT * 3)
var/price_baseline = mat.value_per_unit * SHEET_MATERIAL_AMOUNT
var/quantity_baseline = mat.tradable_base_quantity
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(1,3) // Change our trend life for x number of fires of the subsystem
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.30, price_baseline * 0.15)) //If we don't ceil, small numbers will get trapped at low values
quantity_change = -round(gaussian(quantity_baseline * 0.15, quantity_baseline * 0.15))
if(MARKET_TREND_STABLE)
price_change = round(gaussian(0, price_baseline * 0.01))
quantity_change = round(gaussian(0, quantity_baseline * 0.5))
if(MARKET_TREND_DOWNWARD)
price_change = -ROUND_UP(gaussian(price_units * 0.3, price_baseline * 0.15))
quantity_change = round(gaussian(quantity_baseline * 0.15, quantity_baseline * 0.15))
materials_prices[mat] = round(clamp(price_units + price_change, price_minimum, price_maximum))
materials_quantity[mat] = round(clamp(stock_quantity + quantity_change, 0, quantity_baseline * 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/datum/stock_market_event/event = pick(subtypesof(/datum/stock_market_event))
event = new event
if(event.start_event(mat))
active_events += event