mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 04:30:44 +01:00
## About The Pull Request Picking up where I left off on #81216. * Stock market stocks have had their market quantity drastically reduced, as on both the more common and more rare material sides of things these materials traditionally have been traded in quantities that it would prevent bicycle like quantities of materials entering the game while still allowing for access to rare materials at rare material rates of credits. * The stock market subsystem now fires once every 60 seconds, as opposed to once every 20 seconds. To compensate for this, the subsystem now makes much wider price changes every update, to disincentivize players from just camping out at the stock market console all game, as this is behavior we typically discourage (genetics/virology/etc). Material tending times are similarly decreased to make up for that, while noting that stock market events will still enable for a material to change directions at any point as well. * Material prices can drop below their minimum trading threshold, resulting in them gaining protected purchasing status, and resulting in them being unavailable for purchase. This means if you're watching a price drop, and it's still trending lower, there's a distinct chance you might want to buy before it drops below the threshold, or risk it, buy later, and avoid the material getting locked out for another minute or more. * Adds 2 new stock market events to help add additional variety to the stock market's variability, while adjusting the probability of a market event occurring per stock market event. This should average to ~4 events every minute, keeping things somewhat interesting if you're watching the prices of items, but without requiring second to second updates to keep things engaging. * These two events include one that blocks off all material quantity from a material for the duration of the event and resets prices when complete, and another one that maximizes the profitability of a material, but leaves it's market quantity up in the air. * Stock blocks have had their freeze timer decreased from 5 minutes, down to 3, with the warning now at 1.5 minutes. This is to encourage players not to sit on their resources for longer periods of time if their goal is just to sell at a specific price point and to keep items going through the shuttle, which _also_ encourages players to receive mail/receive regular orders from the rest of the crew. * The UI has a number of improvements, those being: * The time until the next stock market update is listed on the UI as an active timer. * The materials listed in the UI are now sorted by the value of that resource per unit. * The instructions are now kept within a collapsible component to cut down on wasted space within the UI. * A few elements are moved over to % width as opposed to a hardset x pixels width for screen size compatibility purposes.  ## Why It's Good For The Game Stock market has been known to create bike levels of wealth with near negligible amounts of effort and was going to need a balance pass eventually. This is being accomplished by slowing down the system, but also making it more unpredictable by expanding on the stock market event system a bit further. Naturally, it could use a few more wacky events to keep the system fresh and active, but for now this helps to keep the system from being a screen simulator while also making game-health changes like lowering material quantities that were capable of allowing the player to double, quadruple, octuple, etc. their wealth every few minutes by just buying low and selling high. Makes a few QOL changes to the UI to compensate for a few of these changes, like the new update timer on the UI in the case we change the time per update any further, as well as to give incentive to players to not just camp the console for new updates, just to glance at how their investments are doing. These tweaks also keep cargo moving as opposed to just trying to power game iron and glass for maximum returns, while giving them extra opportunities to send the shuttle to keep packages flowing for other purchases/getting mail. This has a chance to stop #79978, but I'll edit this appropriately after a TM has confirmed if it was effective or not. ## Changelog 🆑 balance: The stock market now fires slower, has stock market events occur more often, and the stock market has fewer minerals that are available to buy in a single purchase before restocking. balance: Materials sold on the stock market may be protected from being bought if their prices drop too low, so make sure you watch your prices before they run the risk of getting shut out! balance: Stock blocks now freeze the price of materials for 3 minutes, down from 5. qol: Tweaks to the Galactic Material Market UI, with materials sorted based on their rarity and a timer to show how long until it updates. add: New Stock market events, one locks a material from being purchased, the other maximizes the value and quantity of a material for sale. /🆑 --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
382 lines
14 KiB
Plaintext
382 lines
14 KiB
Plaintext
/// The maximum number of stacks you can place in 1 order
|
|
#define MAX_STACK_LIMIT 10
|
|
/// The order rank for all galactic material market orders
|
|
#define GALATIC_MATERIAL_ORDER "Galactic Materials Market"
|
|
|
|
/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/static/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
|
|
|
|
/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)
|
|
. = ..()
|
|
if(default_unfasten_wrench(user, tool, time = 1.5 SECONDS) == SUCCESSFUL_UNFASTEN)
|
|
return ITEM_INTERACT_SUCCESS
|
|
|
|
/obj/machinery/materials_market/screwdriver_act(mob/living/user, obj/item/tool)
|
|
. = ..()
|
|
if(default_deconstruction_screwdriver(user, "[base_icon_state]_open", "[base_icon_state]", tool))
|
|
return ITEM_INTERACT_SUCCESS
|
|
|
|
/obj/machinery/materials_market/crowbar_act(mob/living/user, obj/item/tool)
|
|
. = ..()
|
|
if(default_deconstruction_crowbar(tool))
|
|
return ITEM_INTERACT_SUCCESS
|
|
|
|
/obj/machinery/materials_market/attackby(obj/item/O, mob/user, params)
|
|
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 ..()
|
|
|
|
/**
|
|
* Find the order purchased either privately or by cargo budget
|
|
* Arguments
|
|
* * [user][mob] - the user who placed this order
|
|
* * is_ordering_private - is the player ordering privatly. If FALSE it means they are using cargo budget
|
|
*/
|
|
/obj/machinery/materials_market/proc/find_order(mob/user, is_ordering_private)
|
|
for(var/datum/supply_order/order in SSshuttle.shopping_list)
|
|
// Must be a Galactic Materials Market order and payed by the null account(if ordered via cargo budget) or by correct user for private purchase
|
|
if(order.orderer_rank == GALATIC_MATERIAL_ORDER && ( \
|
|
(!is_ordering_private && isnull(order.paying_account)) || \
|
|
(is_ordering_private && !isnull(order.paying_account) && order.orderer == user) \
|
|
))
|
|
return order
|
|
return null
|
|
|
|
/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_static_data(mob/user)
|
|
. = list()
|
|
.["CARGO_CRATE_VALUE"] = CARGO_CRATE_VALUE
|
|
|
|
/obj/machinery/materials_market/ui_data(mob/user)
|
|
. = list()
|
|
|
|
//can this player use cargo budget
|
|
var/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())
|
|
|
|
//if no cargo access then force private purchase
|
|
var/is_ordering_private = ordering_private || !can_buy_via_budget
|
|
|
|
//find current order based on ordering mode & player
|
|
var/datum/supply_order/current_order = find_order(user, is_ordering_private)
|
|
|
|
var/material_data
|
|
var/trend_string
|
|
var/color_string
|
|
var/sheet_to_buy
|
|
var/requested_amount
|
|
var/minimum_value_threshold = 0
|
|
for(var/datum/material/traded_mat as anything in SSstock_market.materials_prices)
|
|
//convert trend into text
|
|
switch(SSstock_market.materials_trends[traded_mat])
|
|
if(0)
|
|
trend_string = "neutral"
|
|
if(1)
|
|
trend_string = "up"
|
|
else
|
|
trend_string = "down"
|
|
|
|
//get mat color
|
|
var/initial_colors = initial(traded_mat.greyscale_colors)
|
|
if(initial_colors)
|
|
color_string = splicetext(initial_colors, 7, length(initial_colors), "") //slice it to a standard 6 char hex
|
|
else
|
|
initial_colors = initial(traded_mat.color)
|
|
if(initial_colors)
|
|
color_string = initial_colors
|
|
else
|
|
color_string = COLOR_CYAN
|
|
|
|
//get sheet type from material
|
|
sheet_to_buy = initial(traded_mat.sheet_type)
|
|
if(!sheet_to_buy)
|
|
CRASH("Material with no sheet type being sold on materials market!")
|
|
|
|
//get the ordered amount from the order
|
|
requested_amount = 0
|
|
if(!isnull(current_order))
|
|
requested_amount = current_order.pack.contains[sheet_to_buy]
|
|
|
|
var/min_value_override = initial(traded_mat.minimum_value_override)
|
|
if(min_value_override)
|
|
minimum_value_threshold = min_value_override
|
|
else
|
|
minimum_value_threshold = round(initial(traded_mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 0.5)
|
|
|
|
//send data
|
|
material_data += list(list(
|
|
"name" = initial(traded_mat.name),
|
|
"price" = SSstock_market.materials_prices[traded_mat],
|
|
"rarity" = initial(traded_mat.value_per_unit),
|
|
"threshold" = minimum_value_threshold,
|
|
"quantity" = SSstock_market.materials_quantity[traded_mat],
|
|
"trend" = trend_string,
|
|
"color" = color_string,
|
|
"requested" = requested_amount
|
|
))
|
|
|
|
//get account balance
|
|
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
|
|
|
|
//is market crashing
|
|
var/market_crashing = FALSE
|
|
if(HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING))
|
|
market_crashing = TRUE
|
|
|
|
//get final order cost
|
|
var/current_cost = 0
|
|
if(!isnull(current_order))
|
|
current_cost = current_order.get_final_cost()
|
|
|
|
//pack data
|
|
.["catastrophe"] = market_crashing
|
|
.["materials"] = material_data
|
|
.["creditBalance"] = balance
|
|
.["orderBalance"] = current_cost
|
|
.["orderingPrive"] = ordering_private
|
|
.["canOrderCargo"] = can_buy_via_budget
|
|
.["updateTime"] = SSstock_market.next_fire - world.time
|
|
|
|
/obj/machinery/materials_market/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
|
|
. = ..()
|
|
if(.)
|
|
return
|
|
|
|
//You must have an ID to be able to do something
|
|
var/mob/living/living_user = ui.user
|
|
var/obj/item/card/id/used_id_card = living_user.get_idcard(TRUE)
|
|
if(isnull(used_id_card))
|
|
say("No ID Found")
|
|
return
|
|
var/can_buy_via_budget = (ACCESS_CARGO in used_id_card?.GetAccess())
|
|
|
|
//if multiple users open the UI some of them may not have the required access so we recheck
|
|
var/is_ordering_private = ordering_private
|
|
if(!can_buy_via_budget) //no cargo access then force private purchase
|
|
is_ordering_private = TRUE
|
|
|
|
switch(action)
|
|
if("buy")
|
|
var/material_str = params["material"]
|
|
var/quantity = text2num(params["quantity"])
|
|
|
|
//find material from it's name
|
|
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(initial(mat.name) == material_str)
|
|
material_bought = mat
|
|
break
|
|
if(!material_bought)
|
|
CRASH("Invalid material name passed to materials market!")
|
|
sheet_to_buy = initial(material_bought.sheet_type)
|
|
if(!sheet_to_buy)
|
|
CRASH("Material with no sheet type being sold on materials market!")
|
|
|
|
//get available bank account for purchasing
|
|
var/datum/bank_account/account_payable
|
|
if(is_ordering_private)
|
|
account_payable = used_id_card.registered_account
|
|
else if(can_buy_via_budget)
|
|
account_payable = SSeconomy.get_dep_account(ACCOUNT_CAR)
|
|
if(!account_payable)
|
|
say("No bank account detected!")
|
|
return
|
|
|
|
//sanity checks for available quantity & budget
|
|
if(quantity > SSstock_market.materials_quantity[material_bought])
|
|
say("Not enough materials on the market to purchase!")
|
|
return
|
|
|
|
var/cost = SSstock_market.materials_prices[material_bought] * quantity
|
|
|
|
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
|
|
var/datum/supply_order/current_order = find_order(living_user, is_ordering_private)
|
|
if(!isnull(current_order))
|
|
// Check if this order exceeded the market limit
|
|
var/prior_sheets = current_order.pack.contains[sheet_to_buy]
|
|
if(prior_sheets + quantity > SSstock_market.materials_quantity[material_bought] )
|
|
say("There aren't enough sheets on the market! Please wait for more sheets to be traded before adding more.")
|
|
playsound(usr, 'sound/machines/synth_no.ogg', 35, FALSE)
|
|
return
|
|
|
|
// Check if the order exceeded the purchase limit
|
|
var/prior_stacks = ROUND_UP(prior_sheets / MAX_STACK_SIZE)
|
|
if(prior_stacks >= MAX_STACK_LIMIT)
|
|
say("There are already 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
|
|
|
|
// Prevents you from ordering more than the available budget
|
|
var/datum/bank_account/paying_account = account_payable
|
|
if(!isnull(current_order.paying_account)) //order is already being paid by another account
|
|
paying_account = current_order.paying_account
|
|
if(current_order.get_final_cost() + cost > paying_account.account_balance)
|
|
say("Order exceeds available budget! Please send it before purchasing more.")
|
|
return
|
|
|
|
// Finally Append to this order
|
|
current_order.append_order(things_to_order, cost)
|
|
return TRUE
|
|
|
|
|
|
//Place a new order
|
|
var/datum/supply_pack/custom/minerals/mineral_pack = new(
|
|
purchaser = is_ordering_private ? living_user : "Cargo", \
|
|
cost = cost, \
|
|
contains = things_to_order, \
|
|
)
|
|
var/datum/supply_order/disposable/materials/new_order = new(
|
|
pack = mineral_pack,
|
|
orderer = living_user,
|
|
orderer_rank = GALATIC_MATERIAL_ORDER,
|
|
orderer_ckey = living_user.ckey,
|
|
paying_account = is_ordering_private ? account_payable : null,
|
|
cost_type = "cr",
|
|
can_be_cancelled = FALSE
|
|
)
|
|
//first time order compute the correct cost and compare
|
|
if(new_order.get_final_cost() > account_payable.account_balance)
|
|
say("Not enough money to start purchase!")
|
|
qdel(new_order)
|
|
return
|
|
|
|
say("Thank you for your purchase! It will arrive on the next cargo shuttle!")
|
|
SSshuttle.shopping_list += new_order
|
|
return TRUE
|
|
|
|
if("toggle_budget")
|
|
if(!can_buy_via_budget)
|
|
return
|
|
ordering_private = !ordering_private
|
|
return TRUE
|
|
|
|
if("clear")
|
|
var/datum/supply_order/current_order = find_order(living_user, is_ordering_private)
|
|
if(!isnull(current_order))
|
|
SSshuttle.shopping_list -= current_order
|
|
qdel(current_order)
|
|
return TRUE
|
|
|
|
/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/Initialize(mapload)
|
|
. = ..()
|
|
addtimer(CALLBACK(src, PROC_REF(value_warning)), 1.5 MINUTES, TIMER_DELETE_ME)
|
|
addtimer(CALLBACK(src, PROC_REF(update_value)), 3 MINUTES, TIMER_DELETE_ME)
|
|
|
|
/obj/item/stock_block/examine(mob/user)
|
|
. = ..()
|
|
. += span_notice("\The [src] is worth [export_value] cr, from selling [quantity] sheets of [initial(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/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!"))
|
|
|
|
#undef MAX_STACK_LIMIT
|
|
#undef GALATIC_MATERIAL_ORDER
|