From 53248581a32d3f2ffc598ef31b5112055001c955 Mon Sep 17 00:00:00 2001 From: Jacquerel Date: Sat, 22 Feb 2025 22:38:40 +0000 Subject: [PATCH] Traitor Reputation does not scale with population & reintroduces population locked items (#89617) ## About The Pull Request Closes #89617 Prior to progression traitor some items were only available with a minimum number of (normally 25) players in the round. These items were: - The dual esword - Noslip shoes - The ebow - Holoparasite - Sleeping Carp - Contractor Kit - Maybe a couple of others that I forgot to write down When we moved to a progression system this concept was merged with reputation; under 20 players your reputation would advance more slowly thus making these "dangerous" items less obtainable and also serving as a sort of scaling factor on rewards (with fewer players the secondary objectives are easier to complete, so the reward is commesurately lower). Now that we have removed secondary objectives this doesn't really make sense any more, so now reputation simply advances at a rate of one second per second all the time, but that leaves the old population locks in question. So... I just recoded items that are only available when there are enough players ![image](https://github.com/user-attachments/assets/206577f0-dfdb-4b53-a00f-36e39b2a7f44) ![image](https://github.com/user-attachments/assets/8f840168-9550-4c77-aad0-cb87beb20499) (This iconography simply vanishes once the pop level is reached). Note that this is based on "players who have joined" (roundstart + latejoin), not "players who are online" or "players who are alive". Once an item becomes available it will never stop being available, but it only becomes available based on people who are playing and not watching. Currently the only items I applied this to (with a value of 20 players) are: - Dual esword - Sleeping Carp - Spider Extract (the spider antagonist usually requires like 27 players) - Romerol It isn't applied to anything else. ## Why It's Good For The Game Reputation isn't really a tool used to designate how dangerous an item is any more (if it ever was) and resultingly it doesn't make any sense to slow its gain based on population. Some items though we maybe still don't want to show up in a "low pop" round because they'll create an overall unsatisfying experience, so we should be able to remove those items from play. ## Changelog :cl: balance: Traitor reputation now advances at a fixed rate, not dependent on current server population. balance: The dual esword, sleeping carp scroll, spider extract, and romerol vial cannot be purchased if fewer than 20 players have joined the game. /:cl: --- code/__DEFINES/uplink.dm | 3 ++ .../configuration/entries/game_options.dm | 5 -- code/controllers/subsystem/traitor.dm | 33 ++++-------- code/datums/components/uplink.dm | 4 +- .../antagonists/traitor/uplink_handler.dm | 12 ++++- code/modules/asset_cache/assets/uplink.dm | 1 + code/modules/uplink/uplink_items.dm | 2 + code/modules/uplink/uplink_items/dangerous.dm | 1 + code/modules/uplink/uplink_items/job.dm | 1 + code/modules/uplink/uplink_items/nukeops.dm | 1 + code/modules/uplink/uplink_items/stealthy.dm | 1 + .../tgui/interfaces/AbductorConsole.tsx | 2 + .../tgui/interfaces/Uplink/GenericUplink.tsx | 29 ++++++++++- .../packages/tgui/interfaces/Uplink/index.tsx | 50 +++++-------------- .../tgui/interfaces/common/MalfAiModules.tsx | 2 + 15 files changed, 76 insertions(+), 71 deletions(-) diff --git a/code/__DEFINES/uplink.dm b/code/__DEFINES/uplink.dm index 3b3621f24c5..3e75278132a 100644 --- a/code/__DEFINES/uplink.dm +++ b/code/__DEFINES/uplink.dm @@ -42,3 +42,6 @@ /// Minimal cost for an item to be eligible for a discount #define TRAITOR_DISCOUNT_MIN_PRICE 4 + +/// The standard minimum player count for "don't spawn this item on low population rounds" +#define TRAITOR_POPULATION_LOWPOP 20 diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index a81bc480f97..e2dd97935e0 100644 --- a/code/controllers/configuration/entries/game_options.dm +++ b/code/controllers/configuration/entries/game_options.dm @@ -62,11 +62,6 @@ integer = FALSE min_val = 0 -/// Determines the ideal player count for maximum progression per minute. -/datum/config_entry/number/traitor_ideal_player_count - default = 20 - min_val = 1 - /// Determines how fast traitors scale in general. /datum/config_entry/number/traitor_scaling_multiplier default = 1 diff --git a/code/controllers/subsystem/traitor.dm b/code/controllers/subsystem/traitor.dm index 53cf90ecbd0..afa15e7606c 100644 --- a/code/controllers/subsystem/traitor.dm +++ b/code/controllers/subsystem/traitor.dm @@ -11,48 +11,33 @@ SUBSYSTEM_DEF(traitor) /// The coefficient multiplied by the current_global_progression for new joining traitors to calculate their progression var/newjoin_progression_coeff = 1 - /// The current progression that all traitors should be at in the round + /// The current progression that all traitors should be at in the round, you can't have less than this var/current_global_progression = 0 - /// The amount of deviance from the current global progression before you start getting 2x the current scaling or no scaling at all - /// Also affects objectives, so -50% progress reduction or 50% progress boost. - var/progression_scaling_deviance = 20 MINUTES /// The current uplink handlers being managed var/list/datum/uplink_handler/uplink_handlers = list() - /// The current scaling per minute of progression. Has a maximum value of 1 MINUTES. + /// The current scaling per minute of progression. var/current_progression_scaling = 1 MINUTES /datum/controller/subsystem/traitor/Initialize() + current_progression_scaling = 1 MINUTES * CONFIG_GET(number/traitor_scaling_multiplier) for(var/theft_item in subtypesof(/datum/objective_item/steal)) new theft_item return SS_INIT_SUCCESS /datum/controller/subsystem/traitor/fire(resumed) - var/player_count = length(GLOB.alive_player_list) - // Has a maximum of 1 minute, however the value can be lower if there are lower players than the ideal - // player count for a traitor to be threatening. Rounds to the nearest 10% of a minute to prevent weird - // values from appearing in the UI. Traitor scaling multiplier bypasses the limit and only multiplies the end value. - // from all of our calculations. - current_progression_scaling = max(min( - (player_count / CONFIG_GET(number/traitor_ideal_player_count)) * 1 MINUTES, - 1 MINUTES - ), 0.1 MINUTES) * CONFIG_GET(number/traitor_scaling_multiplier) + var/previous_progression = current_global_progression + current_global_progression = (STATION_TIME_PASSED()) * CONFIG_GET(number/traitor_scaling_multiplier) + var/progression_increment = current_global_progression - previous_progression - var/progression_scaling_delta = (wait / (1 MINUTES)) * current_progression_scaling - var/previous_global_progression = current_global_progression - - current_global_progression += progression_scaling_delta for(var/datum/uplink_handler/handler in uplink_handlers) if(!handler.has_progression || QDELETED(handler)) uplink_handlers -= handler - var/deviance = (previous_global_progression - handler.progression_points) / progression_scaling_deviance - if(abs(deviance) < 0.01) - // If deviance is less than 1%, just set them to the current global progression + if(handler.progression_points < current_global_progression) + // If we got unsynced somehow, just set them to the current global progression // Prevents problems with precision errors. handler.progression_points = current_global_progression else - var/amount_to_give = progression_scaling_delta + (progression_scaling_delta * deviance) - amount_to_give = clamp(amount_to_give, 0, progression_scaling_delta * 2) - handler.progression_points += amount_to_give + handler.progression_points += progression_increment // Should only really happen if an admin is messing with an individual's progression value handler.on_update() /datum/controller/subsystem/traitor/proc/register_uplink_handler(datum/uplink_handler/uplink_handler) diff --git a/code/datums/components/uplink.dm b/code/datums/components/uplink.dm index 98faa95b667..31294d586a8 100644 --- a/code/datums/components/uplink.dm +++ b/code/datums/components/uplink.dm @@ -168,8 +168,7 @@ var/list/data = list() data["telecrystals"] = uplink_handler.telecrystals data["progression_points"] = uplink_handler.progression_points - data["current_expected_progression"] = SStraitor.current_global_progression - data["progression_scaling_deviance"] = SStraitor.progression_scaling_deviance + data["joined_population"] = length(GLOB.joined_player_list) data["current_progression_scaling"] = SStraitor.current_progression_scaling if(uplink_handler.primary_objectives) @@ -206,6 +205,7 @@ "restricted_roles" = item.restricted_roles, "restricted_species" = item.restricted_species, "progression_minimum" = item.progression_minimum, + "population_minimum" = item.population_minimum, "ref" = REF(item), )) diff --git a/code/modules/antagonists/traitor/uplink_handler.dm b/code/modules/antagonists/traitor/uplink_handler.dm index 2801ef29aad..84e186bc116 100644 --- a/code/modules/antagonists/traitor/uplink_handler.dm +++ b/code/modules/antagonists/traitor/uplink_handler.dm @@ -51,6 +51,10 @@ /datum/uplink_handler/proc/not_enough_reputation(datum/uplink_item/to_purchase) return has_progression && progression_points < to_purchase.progression_minimum +/// Checks if there are enough joined players to purchase an item +/datum/uplink_handler/proc/not_enough_population(datum/uplink_item/to_purchase) + return length(GLOB.joined_player_list) < to_purchase.population_minimum + /// Checks for uplink flags as well as items restricted to roles and species /datum/uplink_handler/proc/check_if_restricted(datum/uplink_item/to_purchase) if(!to_purchase.can_be_bought(src)) @@ -80,9 +84,15 @@ if(!check_if_restricted(to_purchase)) return FALSE + if(not_enough_reputation(to_purchase) || not_enough_population(to_purchase)) + return FALSE + + if(telecrystals < to_purchase.cost) + return FALSE + var/current_stock = item_stock[to_purchase.stock_key] var/stock = current_stock != null ? current_stock : INFINITY - if(telecrystals < to_purchase.cost || stock <= 0 || not_enough_reputation(to_purchase)) + if(stock <= 0) return FALSE return TRUE diff --git a/code/modules/asset_cache/assets/uplink.dm b/code/modules/asset_cache/assets/uplink.dm index 35a907a234d..04971c99195 100644 --- a/code/modules/asset_cache/assets/uplink.dm +++ b/code/modules/asset_cache/assets/uplink.dm @@ -35,6 +35,7 @@ "restricted_roles" = item.restricted_roles, "restricted_species" = item.restricted_species, "progression_minimum" = item.progression_minimum, + "population_minimum" = item.population_minimum, "cost_override_string" = item.cost_override_string, "lock_other_purchases" = item.lock_other_purchases )) diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm index 1782f836bea..f0889770f44 100644 --- a/code/modules/uplink/uplink_items.dm +++ b/code/modules/uplink/uplink_items.dm @@ -82,6 +82,8 @@ var/list/restricted_species = list() /// The minimum amount of progression needed for this item to be added to uplinks. var/progression_minimum = 0 + /// The minimum number of joined players (so not observers) needed for this item to be added to uplinks. + var/population_minimum = 0 /// Whether this purchase is visible in the purchase log. var/purchase_log_vis = TRUE // Visible in the purchase log? /// Whether this purchase is restricted or not (VR/Events related) diff --git a/code/modules/uplink/uplink_items/dangerous.dm b/code/modules/uplink/uplink_items/dangerous.dm index 0c348d810cc..8cc8b8b6fca 100644 --- a/code/modules/uplink/uplink_items/dangerous.dm +++ b/code/modules/uplink/uplink_items/dangerous.dm @@ -63,6 +63,7 @@ desc = "The double-bladed energy sword does slightly more damage than a standard energy sword and will deflect \ energy projectiles it blocks, but requires two hands to wield. It also struggles to protect you from tackles." progression_minimum = 30 MINUTES + population_minimum = TRAITOR_POPULATION_LOWPOP item = /obj/item/dualsaber cost = 13 diff --git a/code/modules/uplink/uplink_items/job.dm b/code/modules/uplink/uplink_items/job.dm index 9d357cebfa4..cb10a3aa39f 100644 --- a/code/modules/uplink/uplink_items/job.dm +++ b/code/modules/uplink/uplink_items/job.dm @@ -340,6 +340,7 @@ also give them a bit of sentience though." progression_minimum = 30 MINUTES item = /obj/item/reagent_containers/syringe/spider_extract + population_minimum = TRAITOR_POPULATION_LOWPOP cost = 10 restricted_roles = list(JOB_RESEARCH_DIRECTOR, JOB_SCIENTIST, JOB_ROBOTICIST) surplus = 10 diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm index c7f0bc82a72..677b13dd08f 100644 --- a/code/modules/uplink/uplink_items/nukeops.dm +++ b/code/modules/uplink/uplink_items/nukeops.dm @@ -604,6 +604,7 @@ along with slurred speech, aggression, and the ability to infect others with this agent." item = /obj/item/storage/box/syndie_kit/romerol cost = 25 + population_minimum = TRAITOR_POPULATION_LOWPOP progression_minimum = 30 MINUTES purchasable_from = UPLINK_ALL_SYNDIE_OPS | UPLINK_TRAITORS // Don't give this to spies cant_discount = TRUE diff --git a/code/modules/uplink/uplink_items/stealthy.dm b/code/modules/uplink/uplink_items/stealthy.dm index 6d8b6b44b9b..53bc37f59e6 100644 --- a/code/modules/uplink/uplink_items/stealthy.dm +++ b/code/modules/uplink/uplink_items/stealthy.dm @@ -85,6 +85,7 @@ and gain the ability to swat bullets from the air, but you will also refuse to use dishonorable ranged weaponry." item = /obj/item/book/granter/martial/carp progression_minimum = 30 MINUTES + population_minimum = TRAITOR_POPULATION_LOWPOP cost = 17 surplus = 0 purchasable_from = ~UPLINK_ALL_SYNDIE_OPS diff --git a/tgui/packages/tgui/interfaces/AbductorConsole.tsx b/tgui/packages/tgui/interfaces/AbductorConsole.tsx index d8d33053627..79063265551 100644 --- a/tgui/packages/tgui/interfaces/AbductorConsole.tsx +++ b/tgui/packages/tgui/interfaces/AbductorConsole.tsx @@ -93,6 +93,8 @@ const Abductsoft = (props) => { disabled: (credits || 0) < item.cost, icon: item.icon, icon_state: item.icon_state, + population_tooltip: '', + insufficient_population: false, }); } } diff --git a/tgui/packages/tgui/interfaces/Uplink/GenericUplink.tsx b/tgui/packages/tgui/interfaces/Uplink/GenericUplink.tsx index 4165c1049f3..ce460599022 100644 --- a/tgui/packages/tgui/interfaces/Uplink/GenericUplink.tsx +++ b/tgui/packages/tgui/interfaces/Uplink/GenericUplink.tsx @@ -133,6 +133,8 @@ export type Item = { category: string; cost: JSX.Element | string; desc: JSX.Element | string; + population_tooltip: string; + insufficient_population: BooleanLike; disabled: BooleanLike; }; @@ -184,9 +186,19 @@ const ItemList = (props: ItemListProps) => { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', + opacity: item.insufficient_population ? '0.5' : '1', }} > - {item.name} + {item.insufficient_population ? ( + + + + {item.name} + + + ) : ( + item.name + )} @@ -215,6 +227,21 @@ const ItemList = (props: ItemListProps) => { } > + {item.insufficient_population ? ( + + {' '} + {item.population_tooltip} + + ) : ( + '' + )} + { const { telecrystals, progression_points, + joined_population, primary_objectives, can_renegotiate, has_progression, - current_expected_progression, - progression_scaling_deviance, current_progression_scaling, extra_purchasable, extra_purchasable_stock, @@ -202,6 +201,8 @@ export class Uplink extends Component<{}, UplinkState> { const item = itemsToAdd[i]; const hasEnoughProgression = progression_points >= item.progression_minimum; + const hasEnoughPop = + !joined_population || joined_population >= item.population_minimum; let stock: number | null = current_stock[item.stock_key]; if (item.ref) { @@ -245,8 +246,14 @@ export class Uplink extends Component<{}, UplinkState> { )} ), + population_tooltip: + 'This item is not cleared for operations performed against stations crewed by fewer than ' + + item.population_minimum + + ' people.', + insufficient_population: !hasEnoughPop, disabled: !canBuy || + !hasEnoughPop || (has_progression && !hasEnoughProgression) || (item.lock_other_purchases && purchased_items > 0), extraData: { @@ -256,17 +263,7 @@ export class Uplink extends Component<{}, UplinkState> { }, }); } - // Get the difference between the current progression and - // expected progression - let progressionPercentage = - current_expected_progression - progression_points; - // Clamp it down between 0 and 2 - progressionPercentage = Math.min( - Math.max(progressionPercentage / progression_scaling_deviance, -1), - 1, - ); - // Round it and convert it into a percentage - progressionPercentage = Math.round(progressionPercentage * 1000) / 10; + return ( @@ -292,29 +289,6 @@ export class Uplink extends Component<{}, UplinkState> {  every minute - {Math.abs(progressionPercentage) > 0 && ( - - Because your threat level is - {progressionPercentage < 0 - ? ' ahead ' - : ' behind '} - of where it should be, you are getting - - {progressionPercentage}% - - {progressionPercentage < 0 ? 'less' : 'more'}{' '} - threat every minute - - )} {dangerLevelsTooltip} diff --git a/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx b/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx index 971dfc3ef17..039b292aa6f 100644 --- a/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx +++ b/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx @@ -34,6 +34,8 @@ export function MalfAiModules(props) { icon: item.icon, id: item.name, name: item.name, + population_tooltip: '', + insufficient_population: false, }); } }