Files
ArcaneMusic 7b8efd3daa Stock Market UI Tweaks and improvements (#95564)
## About The Pull Request

This PR pulls forward some UI tweaks that I made in #93183, but cleaned
up and with some additional adjustments.
This adjusts a bit of the GMM Ui, cutting out the horizontal quantity
measurement and rolling that into the supply/order information in the
middle.
<img width="984" height="613" alt="image"
src="https://github.com/user-attachments/assets/40661624-c88d-4899-a624-7f3b391edf27"
/>
Adds a hit of color as well to show what materials are currently being
ordered.
Adds a bit of tooltip text in order to showcase the range of prices that
a mineral can be bought and sold at, in order to showcase why lower
value minerals like glass/iron can be bought and sold from.
In addition to this, also tweaks the logic on the disabling of buttons
to better reflect these thresholds, so you can buy and sell when sitting
on those thresholds.

## Why It's Good For The Game

The market UI needs to exist in a sort of "spreadsheet" format, in order
to see all the values, all the costs, and make decisions about what you
what to buy and how many. That said, it's clunky, and there's a lot of
complexity related to buying and selling materials already that I would
love to cut down on.

This helps trim the UI, makes it a bit smaller, but adds in some missing
information that even I don't have memorized for the purposes of regular
gameplay.

## Changelog

🆑
qol: The GMM Ui can now tell you the minimum and maximum price ranges
that a material will sell for.
qol: You can now sell materials on the edge of their buying and selling
thresholds. (This really only effects iron and glass, practically)
/🆑
2026-04-23 08:50:02 +12:00

386 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
light_power = 3
light_range = MINIMUM_USEFUL_LIGHT_RANGE
/// 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)
return default_deconstruction_screwdriver(user, tool)
/obj/machinery/materials_market/crowbar_act(mob/living/user, obj/item/tool)
return default_deconstruction_crowbar(user, tool)
/obj/machinery/materials_market/item_interaction(mob/living/user, obj/item/stack/exportable, list/modifiers)
if(!isstack(exportable))
return NONE
if(!is_operational)
balloon_alert(user, "no power!")
return ITEM_INTERACT_FAILURE
var/list/datum/material/materials = exportable.custom_materials
if(materials.len != 1)
balloon_alert(user, "alloy stacks not allowed")
return ITEM_INTERACT_FAILURE
var/price = SSstock_market.materials_prices[materials[1].type]
if(!price)
balloon_alert(user, "materials in stack are worthless")
return ITEM_INTERACT_FAILURE
if(!user.transferItemToLoc(exportable, src))
to_chat(user, span_warning("[exportable] is stuck in hand!"))
return ITEM_INTERACT_FAILURE
var/obj/item/stock_block/new_block = new /obj/item/stock_block(drop_location())
new_block.export_value = price
new_block.set_custom_materials(materials)
to_chat(user, span_notice("You have created a stock block worth [new_block.export_value * exportable.amount] [MONEY_SYMBOL]! Sell it before it becomes liquid!"))
playsound(src, 'sound/machines/synth/synth_yes.ogg', 50, FALSE)
qdel(exportable)
use_energy(active_power_usage)
return ITEM_INTERACT_SUCCESS
/obj/machinery/materials_market/power_change()
. = ..()
if(!is_operational)
set_light(0, 0)
else
set_light(initial(light_range), initial(light_power))
/**
* 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
var/maximum_value_threshold = 0
var/elastic_mult = 1
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_color) || initial(traded_mat.color)
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)
maximum_value_threshold = round(initial(traded_mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 3)
//Pulling elastic modifier into data.
for(var/datum/export/material/market/export_est in GLOB.exports_list)
if(export_est.material_id == traded_mat)
elastic_mult = export_est.k_elasticity * 100
material_data += list(list(
"name" = initial(traded_mat.name),
"price" = SSstock_market.materials_prices[traded_mat],
"rarity" = initial(traded_mat.value_per_unit),
"min_threshold" = minimum_value_threshold,
"max_threshold" = maximum_value_threshold,
"quantity" = SSstock_market.materials_quantity[traded_mat],
"trend" = trend_string,
"color" = color_string,
"requested" = requested_amount,
"elastic" = elastic_mult,
))
//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 its 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] = 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(living_user, 'sound/machines/synth/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(living_user, 'sound/machines/synth/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 = MONEY_SYMBOL,
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
/// Is this stock block currently updating its 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)
. = ..()
var/datum/material/export_mat = custom_materials[1]
var/quantity = custom_materials[export_mat] / SHEET_MATERIAL_AMOUNT
. += span_notice("\The [src] is worth [quantity * export_value] [MONEY_SYMBOL], from selling [quantity] sheets of [export_mat.name].")
if(fluid)
. += span_warning("\The [src] is currently liquid! Its 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 its 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()
export_value = SSstock_market.materials_prices[custom_materials[1]]
icon_state = "stock_block_liquid"
update_appearance(UPDATE_ICON_STATE)
visible_message(span_warning("\The [src] becomes liquid!"))
fluid = TRUE
#undef MAX_STACK_LIMIT
#undef GALATIC_MATERIAL_ORDER