Files
nevimer 6e9f2ccfc0 Merge remote-tracking branch 'tgstation/master' into upstream-12-15
# Conflicts:
#	.github/workflows/compile_all_maps.yml
#	.github/workflows/run_integration_tests.yml
#	_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm
#	code/_onclick/hud/credits.dm
#	code/controllers/subsystem/networks/id_access.dm
#	code/datums/diseases/advance/advance.dm
#	code/datums/diseases/advance/symptoms/heal.dm
#	code/game/machinery/doors/door.dm
#	code/game/objects/structures/crates_lockers/closets/secure/medical.dm
#	code/game/objects/structures/crates_lockers/closets/secure/security.dm
#	code/modules/antagonists/malf_ai/malf_ai_modules.dm
#	code/modules/jobs/job_types/_job.dm
#	code/modules/loadout/categories/accessories.dm
#	code/modules/loadout/loadout_helpers.dm
#	code/modules/loadout/loadout_items.dm
#	code/modules/loadout/loadout_preference.dm
#	code/modules/mob/living/silicon/robot/robot_defense.dm
#	code/modules/mod/mod_theme.dm
#	code/modules/projectiles/ammunition/energy/laser.dm
#	code/modules/reagents/reagent_containers/cups/drinks.dm
#	code/modules/shuttle/mobile_port/variants/supply.dm
#	code/modules/surgery/organs/internal/eyes/_eyes.dm
#	code/modules/unit_tests/screenshots/screenshot_antag_icons_heretic.png
#	icons/hud/screen_full.dmi
2025-12-15 18:12:29 -05:00

296 lines
14 KiB
Plaintext

SUBSYSTEM_DEF(economy)
name = "Economy"
wait = 5 MINUTES
runlevels = RUNLEVEL_GAME
///How many paychecks should players start out the round with?
var/roundstart_paychecks = 5
///How many credits does the in-game economy have in circulation at round start? Divided up by 6 of the 7 department budgets evenly, where cargo starts with nothing.
var/budget_pool = 35000
var/list/department_accounts = list(ACCOUNT_CIV = ACCOUNT_CIV_NAME,
ACCOUNT_ENG = ACCOUNT_ENG_NAME,
ACCOUNT_SCI = ACCOUNT_SCI_NAME,
ACCOUNT_MED = ACCOUNT_MED_NAME,
ACCOUNT_SRV = ACCOUNT_SRV_NAME,
ACCOUNT_CAR = ACCOUNT_CAR_NAME,
ACCOUNT_CMD = ACCOUNT_CMD_NAME, // SKYRAT EDIT
ACCOUNT_INT = ACCOUNT_INT_NAME, // BUBBER EDIT
ACCOUNT_TAR = ACCOUNT_TAR_NAME, // END BUBBER EDIT
ACCOUNT_SEC = ACCOUNT_SEC_NAME)
var/list/departmental_accounts = list()
/**
* Enables extra money charges for things that normally would be free, such as sleepers/cryo/beepsky.
* Take care when enabling, as players will NOT respond well if the economy is set up for low cash flows.
*/
var/full_ancap = FALSE
/// Departmental cash provided to science when a node is researched in specific configs.
var/techweb_bounty = 250
/**
* List of normal (no department ones) accounts' identifiers with associated datum accounts, for big O performance.
* A list of sole account datums can be obtained with assoc_to_values(), another variable would be redundant rn.
*/
var/list/bank_accounts_by_id = list()
/// A list of bank accounts indexed by their assigned job typepath.
var/list/bank_accounts_by_job = list()
///List of the departmental budget cards in existence.
var/list/dep_cards = list()
/// A var that collects the total amount of credits owned in player accounts on station, reset and recounted on fire()
var/station_total = 0
/// A var that tracks how much money is expected to be on station at a given time. If less than station_total prices go up in vendors.
var/station_target = 1
/// A passively increasing buffer to help alliviate inflation later into the shift, but to a lesser degree.
var/station_target_buffer = 0
/// A var that displays the result of inflation_value for easier debugging and tracking.
var/inflation_value = 1
/// How many civilain bounties have been completed so far this shift? Affects civilian budget payout values.
var/civ_bounty_tracker = 0
/// Contains the message to send to newscasters about price inflation and earnings, updated on price_update()
var/earning_report
///The modifier multiplied to the value of bounties paid out.
var/bounty_modifier = 1
///The modifier multiplied to the value of cargo pack prices.
var/pack_price_modifier = 1
/**
* A list of strings containing a basic transaction history of purchases on the station.
* Added to any time when player accounts purchase something.
*/
var/list/audit_log = list()
/// Number of mail items generated.
var/mail_waiting = 0
/// Mail Holiday: AKA does mail arrive today? Always blocked on Sundays.
var/mail_blocked = FALSE
/// List used to track partially completed processing steps
/// Allows for proper yielding
var/list/cached_processing
/// Tracks what bit of processing we're on, so we can resume post yield in the right place
var/processing_part
/// Tracks a temporary sum of all money in the system
/// We need this on the subsystem because of yielding and such
var/temporary_total = 0
/// Determines how many ticks it takes to restock mail
var/ticks_per_mail = 2
/datum/controller/subsystem/economy/Initialize()
//removes cargo from the split
var/budget_to_hand_out = round(budget_pool / department_accounts.len -1)
if(time2text(world.timeofday, "DDD") == SUNDAY)
mail_blocked = TRUE
for(var/dep_id in department_accounts)
if(dep_id == ACCOUNT_CAR) //cargo starts with NOTHING
new /datum/bank_account/department(dep_id, 0, player_account = FALSE)
continue
new /datum/bank_account/department(dep_id, budget_to_hand_out, player_account = FALSE)
return SS_INIT_SUCCESS
/datum/controller/subsystem/economy/Recover()
departmental_accounts = SSeconomy.departmental_accounts
bank_accounts_by_id = SSeconomy.bank_accounts_by_id
dep_cards = SSeconomy.dep_cards
/// Processing step defines, to track what we've done so far
#define ECON_DEPARTMENT_STEP "econ_dpt_stp"
#define ECON_ACCOUNT_STEP "econ_act_stp"
#define ECON_PRICE_UPDATE_STEP "econ_prc_stp"
/datum/controller/subsystem/economy/fire(resumed = 0)
var/seconds_per_tick = wait / (5 MINUTES)
if(!resumed)
temporary_total = 0
processing_part = ECON_DEPARTMENT_STEP
cached_processing = department_accounts.Copy()
if(processing_part == ECON_DEPARTMENT_STEP)
if(!departmental_payouts())
return
processing_part = ECON_ACCOUNT_STEP
cached_processing = bank_accounts_by_id.Copy()
station_total = 0
station_target_buffer += STATION_TARGET_BUFFER
if(processing_part == ECON_ACCOUNT_STEP)
if(!issue_paydays())
return
processing_part = ECON_PRICE_UPDATE_STEP
station_target = max(round(temporary_total / max(bank_accounts_by_id.len * 2, 1)) + station_target_buffer, 1)
if(processing_part == ECON_PRICE_UPDATE_STEP)
if(!HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING) && !price_update())
return
if(times_fired % ticks_per_mail == 0)
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, ticks_per_mail * 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.
*/
/datum/controller/subsystem/economy/proc/get_dep_account(dep_id)
for(var/datum/bank_account/department/D in departmental_accounts)
if(D.department_id == dep_id)
return D
/**
* Departmental income payments are kept static and linear for every department, and paid out once every 5 minutes, as determined by MAX_GRANT_DPT.
* Iterates over every department account for the same payment.
*/
/datum/controller/subsystem/economy/proc/departmental_payouts()
// son sonic speed? cache? hot over in cold food why? (datum var accesses are slow, cache lists for sonic speed)
var/list/cached_processing = src.cached_processing
for(var/i in 1 to length(cached_processing))
var/datum/bank_account/dept_account = get_dep_account(cached_processing[i])
if(!dept_account)
continue
dept_account.adjust_money(MAX_GRANT_DPT)
if(MC_TICK_CHECK)
cached_processing.Cut(1, i + 1)
return FALSE
return TRUE
/**
* Issues all our bank-accounts paydays, and gets an idea of how much money is in circulation
*/
/datum/controller/subsystem/economy/proc/issue_paydays()
var/list/cached_processing = src.cached_processing
for(var/i in 1 to length(cached_processing))
var/datum/bank_account/bank_account = cached_processing[cached_processing[i]]
if(bank_account?.account_job && !ispath(bank_account.account_job))
temporary_total += (bank_account.account_job.paycheck * STARTING_PAYCHECKS)
bank_account.payday(1, skippable = TRUE)
SEND_SIGNAL(bank_account, COMSIG_ON_BANK_ACCOUNT_PAYOUT) // BUBBER EDIT
station_total += bank_account.account_balance
if(MC_TICK_CHECK)
cached_processing.Cut(1, i + 1)
return FALSE
return TRUE
/**
* Updates the the inflation_value, effecting newscaster alerts and the mail system.
**/
/datum/controller/subsystem/economy/proc/price_update()
var/fluff_string = ""
if(!HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING))
fluff_string = ", but company countermeasures protect <b>YOU</b> from being affected!"
else
fluff_string = ", and company countermeasures are failing to protect <b>YOU</b> from being affected. We're all doomed!"
earning_report = "<b>Sector Economic Report</b><br><br> Sector vendor prices is currently at <b>[SSeconomy.inflation_value()*100]%</b>[fluff_string]<br><br> The station spending power is currently <b>[station_total] [MONEY_NAME_CAPITALIZED]</b>, and the crew's targeted allowance is at <b>[station_target] [MONEY_NAME_CAPITALIZED]</b>.<br><br>[SSstock_market.news_string]"
var/update_alerts = FALSE
if(HAS_TRAIT(SSstation, STATION_TRAIT_ECONOMY_ALERTS) && (living_player_count() > 1))
var/datum/bank_account/moneybags
var/static/list/typecache_bank = typecacheof(list(/datum/bank_account/department, /datum/bank_account/remote))
for(var/i in bank_accounts_by_id)
var/datum/bank_account/current_acc = bank_accounts_by_id[i]
if(typecache_bank[current_acc.type])
continue
if(!moneybags || moneybags.account_balance < current_acc.account_balance)
moneybags = current_acc
if (moneybags)
earning_report += "Our GMM Spotlight would like to alert you that <b>[moneybags.account_holder]</b> is your station's most affulent crewmate! They've hit it big with [moneybags.account_balance] [MONEY_NAME] saved. "
update_alerts = TRUE
inflict_moneybags(moneybags)
earning_report += "That's all from the <i>Nanotrasen Economist Division</i>."
GLOB.news_network.submit_article(earning_report, "Station Earnings Report", NEWSCASTER_STATION_ANNOUNCEMENTS, null, update_alert = update_alerts)
return TRUE
/**
* Proc that returns a value meant to shift inflation values in vendors, based on how much money exists on the station.
*
* If crew are somehow aquiring far too much money, this value will dynamically cause vendables across the station to skyrocket in price until some money is spent.
* Additionally, civilain bounties will cost less, and cargo goodies will increase in price as well.
* The goal here is that if you want to spend money, you'll have to get it, and the most efficient method is typically from other players.
**/
/datum/controller/subsystem/economy/proc/inflation_value()
if(!bank_accounts_by_id.len)
return 1
if(HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING))
return inflation_value //early return instead of the actual check
inflation_value = max(round(((station_total / bank_accounts_by_id.len) / station_target), 0.1), 1.0)
return inflation_value
/**
* Proc that adds a set of strings and ints to the audit log, tracked by the economy SS.
*
* * account: The bank account of the person purchasing the item.
* * price_to_use: The cost of the purchase made for this transaction.
* * vendor: The object or structure medium that is charging the user. For Vending machines that's the machine, for payment component that's the parent, cargo that's the crate, etc.
*/
/datum/controller/subsystem/economy/proc/add_audit_entry(datum/bank_account/account, price_to_use, vendor)
if(isnull(account) || isnull(price_to_use) || !vendor)
CRASH("Track purchases was missing an argument! (Account, Price, or Vendor.)")
audit_log += list(list(
"account" = "[account.account_holder]",
"cost" = price_to_use,
"vendor" = "[vendor]",
"stationtime" = station_time_timestamp("hh:mm"),
))
/**
* Iterates over the machines list for vending machines, resets their regular and premium product prices (Not contraband), and sends a message to the newscaster network.
*/
/datum/controller/subsystem/economy/proc/update_vending_prices()
var/list/obj/machinery/vending/prices_to_update = list()
// Assoc list of "z level" -> if it's on the station
// Hack, is station z level is too expensive to do for each machine, I hate this place
var/list/station_z_status = list()
for(var/obj/machinery/vending/vending_lad as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/vending))
if(istype(vending_lad, /obj/machinery/vending/custom))
continue
var/vending_level = vending_lad.z
var/station_status = station_z_status["[vending_level]"]
if(station_status == null)
station_status = is_station_level(vending_level)
station_z_status["[vending_level]"] = station_status
if(!station_status)
continue
prices_to_update += vending_lad
for(var/i in 1 to length(prices_to_update))
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
var/mob/living/card_holder
for(var/obj/card in moneybags?.bank_cards)
if(isidcard(card))
card_holder = recursive_loc_check(card, /mob/living)
if(!isliving(card_holder)) //If on a living mob
return FALSE
card_holder.adjust_timed_status_effect(wait, /datum/status_effect/spotlight_light)
return TRUE
#undef ECON_DEPARTMENT_STEP
#undef ECON_ACCOUNT_STEP
#undef ECON_PRICE_UPDATE_STEP