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

🆑
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.
/🆑
This commit is contained in:
Jacquerel
2025-02-22 22:38:40 +00:00
committed by Roxy
parent 6237696d31
commit 53248581a3
15 changed files with 76 additions and 71 deletions

View File

@@ -42,3 +42,6 @@
/// Minimal cost for an item to be eligible for a discount /// Minimal cost for an item to be eligible for a discount
#define TRAITOR_DISCOUNT_MIN_PRICE 4 #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

View File

@@ -62,11 +62,6 @@
integer = FALSE integer = FALSE
min_val = 0 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. /// Determines how fast traitors scale in general.
/datum/config_entry/number/traitor_scaling_multiplier /datum/config_entry/number/traitor_scaling_multiplier
default = 1 default = 1

View File

@@ -11,48 +11,33 @@ SUBSYSTEM_DEF(traitor)
/// The coefficient multiplied by the current_global_progression for new joining traitors to calculate their progression /// The coefficient multiplied by the current_global_progression for new joining traitors to calculate their progression
var/newjoin_progression_coeff = 1 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 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 /// The current uplink handlers being managed
var/list/datum/uplink_handler/uplink_handlers = list() 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 var/current_progression_scaling = 1 MINUTES
/datum/controller/subsystem/traitor/Initialize() /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)) for(var/theft_item in subtypesof(/datum/objective_item/steal))
new theft_item new theft_item
return SS_INIT_SUCCESS return SS_INIT_SUCCESS
/datum/controller/subsystem/traitor/fire(resumed) /datum/controller/subsystem/traitor/fire(resumed)
var/player_count = length(GLOB.alive_player_list) var/previous_progression = current_global_progression
// Has a maximum of 1 minute, however the value can be lower if there are lower players than the ideal current_global_progression = (STATION_TIME_PASSED()) * CONFIG_GET(number/traitor_scaling_multiplier)
// player count for a traitor to be threatening. Rounds to the nearest 10% of a minute to prevent weird var/progression_increment = current_global_progression - previous_progression
// 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/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) for(var/datum/uplink_handler/handler in uplink_handlers)
if(!handler.has_progression || QDELETED(handler)) if(!handler.has_progression || QDELETED(handler))
uplink_handlers -= handler uplink_handlers -= handler
var/deviance = (previous_global_progression - handler.progression_points) / progression_scaling_deviance if(handler.progression_points < current_global_progression)
if(abs(deviance) < 0.01) // If we got unsynced somehow, just set them to the current global progression
// If deviance is less than 1%, just set them to the current global progression
// Prevents problems with precision errors. // Prevents problems with precision errors.
handler.progression_points = current_global_progression handler.progression_points = current_global_progression
else else
var/amount_to_give = progression_scaling_delta + (progression_scaling_delta * deviance) handler.progression_points += progression_increment // Should only really happen if an admin is messing with an individual's progression value
amount_to_give = clamp(amount_to_give, 0, progression_scaling_delta * 2)
handler.progression_points += amount_to_give
handler.on_update() handler.on_update()
/datum/controller/subsystem/traitor/proc/register_uplink_handler(datum/uplink_handler/uplink_handler) /datum/controller/subsystem/traitor/proc/register_uplink_handler(datum/uplink_handler/uplink_handler)

View File

@@ -168,8 +168,7 @@
var/list/data = list() var/list/data = list()
data["telecrystals"] = uplink_handler.telecrystals data["telecrystals"] = uplink_handler.telecrystals
data["progression_points"] = uplink_handler.progression_points data["progression_points"] = uplink_handler.progression_points
data["current_expected_progression"] = SStraitor.current_global_progression data["joined_population"] = length(GLOB.joined_player_list)
data["progression_scaling_deviance"] = SStraitor.progression_scaling_deviance
data["current_progression_scaling"] = SStraitor.current_progression_scaling data["current_progression_scaling"] = SStraitor.current_progression_scaling
if(uplink_handler.primary_objectives) if(uplink_handler.primary_objectives)
@@ -206,6 +205,7 @@
"restricted_roles" = item.restricted_roles, "restricted_roles" = item.restricted_roles,
"restricted_species" = item.restricted_species, "restricted_species" = item.restricted_species,
"progression_minimum" = item.progression_minimum, "progression_minimum" = item.progression_minimum,
"population_minimum" = item.population_minimum,
"ref" = REF(item), "ref" = REF(item),
)) ))

View File

@@ -51,6 +51,10 @@
/datum/uplink_handler/proc/not_enough_reputation(datum/uplink_item/to_purchase) /datum/uplink_handler/proc/not_enough_reputation(datum/uplink_item/to_purchase)
return has_progression && progression_points < to_purchase.progression_minimum 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 /// 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) /datum/uplink_handler/proc/check_if_restricted(datum/uplink_item/to_purchase)
if(!to_purchase.can_be_bought(src)) if(!to_purchase.can_be_bought(src))
@@ -80,9 +84,15 @@
if(!check_if_restricted(to_purchase)) if(!check_if_restricted(to_purchase))
return FALSE 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/current_stock = item_stock[to_purchase.stock_key]
var/stock = current_stock != null ? current_stock : INFINITY 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 FALSE
return TRUE return TRUE

View File

@@ -35,6 +35,7 @@
"restricted_roles" = item.restricted_roles, "restricted_roles" = item.restricted_roles,
"restricted_species" = item.restricted_species, "restricted_species" = item.restricted_species,
"progression_minimum" = item.progression_minimum, "progression_minimum" = item.progression_minimum,
"population_minimum" = item.population_minimum,
"cost_override_string" = item.cost_override_string, "cost_override_string" = item.cost_override_string,
"lock_other_purchases" = item.lock_other_purchases "lock_other_purchases" = item.lock_other_purchases
)) ))

View File

@@ -82,6 +82,8 @@
var/list/restricted_species = list() var/list/restricted_species = list()
/// The minimum amount of progression needed for this item to be added to uplinks. /// The minimum amount of progression needed for this item to be added to uplinks.
var/progression_minimum = 0 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. /// Whether this purchase is visible in the purchase log.
var/purchase_log_vis = TRUE // 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) /// Whether this purchase is restricted or not (VR/Events related)

View File

@@ -63,6 +63,7 @@
desc = "The double-bladed energy sword does slightly more damage than a standard energy sword and will deflect \ 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." energy projectiles it blocks, but requires two hands to wield. It also struggles to protect you from tackles."
progression_minimum = 30 MINUTES progression_minimum = 30 MINUTES
population_minimum = TRAITOR_POPULATION_LOWPOP
item = /obj/item/dualsaber item = /obj/item/dualsaber
cost = 13 cost = 13

View File

@@ -340,6 +340,7 @@
also give them a bit of sentience though." also give them a bit of sentience though."
progression_minimum = 30 MINUTES progression_minimum = 30 MINUTES
item = /obj/item/reagent_containers/syringe/spider_extract item = /obj/item/reagent_containers/syringe/spider_extract
population_minimum = TRAITOR_POPULATION_LOWPOP
cost = 10 cost = 10
restricted_roles = list(JOB_RESEARCH_DIRECTOR, JOB_SCIENTIST, JOB_ROBOTICIST) restricted_roles = list(JOB_RESEARCH_DIRECTOR, JOB_SCIENTIST, JOB_ROBOTICIST)
surplus = 10 surplus = 10

View File

@@ -604,6 +604,7 @@
along with slurred speech, aggression, and the ability to infect others with this agent." along with slurred speech, aggression, and the ability to infect others with this agent."
item = /obj/item/storage/box/syndie_kit/romerol item = /obj/item/storage/box/syndie_kit/romerol
cost = 25 cost = 25
population_minimum = TRAITOR_POPULATION_LOWPOP
progression_minimum = 30 MINUTES progression_minimum = 30 MINUTES
purchasable_from = UPLINK_ALL_SYNDIE_OPS | UPLINK_TRAITORS // Don't give this to spies purchasable_from = UPLINK_ALL_SYNDIE_OPS | UPLINK_TRAITORS // Don't give this to spies
cant_discount = TRUE cant_discount = TRUE

View File

@@ -85,6 +85,7 @@
and gain the ability to swat bullets from the air, but you will also refuse to use dishonorable ranged weaponry." 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 item = /obj/item/book/granter/martial/carp
progression_minimum = 30 MINUTES progression_minimum = 30 MINUTES
population_minimum = TRAITOR_POPULATION_LOWPOP
cost = 17 cost = 17
surplus = 0 surplus = 0
purchasable_from = ~UPLINK_ALL_SYNDIE_OPS purchasable_from = ~UPLINK_ALL_SYNDIE_OPS

View File

@@ -93,6 +93,8 @@ const Abductsoft = (props) => {
disabled: (credits || 0) < item.cost, disabled: (credits || 0) < item.cost,
icon: item.icon, icon: item.icon,
icon_state: item.icon_state, icon_state: item.icon_state,
population_tooltip: '',
insufficient_population: false,
}); });
} }
} }

View File

@@ -133,6 +133,8 @@ export type Item = {
category: string; category: string;
cost: JSX.Element | string; cost: JSX.Element | string;
desc: JSX.Element | string; desc: JSX.Element | string;
population_tooltip: string;
insufficient_population: BooleanLike;
disabled: BooleanLike; disabled: BooleanLike;
}; };
@@ -184,9 +186,19 @@ const ItemList = (props: ItemListProps) => {
overflow: 'hidden', overflow: 'hidden',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
opacity: item.insufficient_population ? '0.5' : '1',
}} }}
> >
{item.name} {item.insufficient_population ? (
<Tooltip content={item.population_tooltip}>
<Box>
<Icon mr="8px" name="lock" lineHeight="36px" />
{item.name}
</Box>
</Tooltip>
) : (
item.name
)}
</Stack.Item> </Stack.Item>
<Stack.Item> <Stack.Item>
<Tooltip content={item.desc}> <Tooltip content={item.desc}>
@@ -215,6 +227,21 @@ const ItemList = (props: ItemListProps) => {
</Button> </Button>
} }
> >
{item.insufficient_population ? (
<Box
mt="-12px"
mb="-6px"
style={{
opacity: '0.5',
}}
>
<Icon name="lock" lineHeight="36px" />{' '}
{item.population_tooltip}
</Box>
) : (
''
)}
<Box <Box
style={{ style={{
opacity: '0.75', opacity: '0.75',

View File

@@ -38,6 +38,7 @@ type UplinkItem = {
restricted_roles: string; restricted_roles: string;
restricted_species: string; restricted_species: string;
progression_minimum: number; progression_minimum: number;
population_minimum: number;
cost_override_string: string; cost_override_string: string;
lock_other_purchases: BooleanLike; lock_other_purchases: BooleanLike;
ref?: string; ref?: string;
@@ -46,9 +47,8 @@ type UplinkItem = {
type UplinkData = { type UplinkData = {
telecrystals: number; telecrystals: number;
progression_points: number; progression_points: number;
joined_population?: number;
lockable: BooleanLike; lockable: BooleanLike;
current_expected_progression: number;
progression_scaling_deviance: number;
current_progression_scaling: number; current_progression_scaling: number;
uplink_flag: number; uplink_flag: number;
assigned_role: string; assigned_role: string;
@@ -175,11 +175,10 @@ export class Uplink extends Component<{}, UplinkState> {
const { const {
telecrystals, telecrystals,
progression_points, progression_points,
joined_population,
primary_objectives, primary_objectives,
can_renegotiate, can_renegotiate,
has_progression, has_progression,
current_expected_progression,
progression_scaling_deviance,
current_progression_scaling, current_progression_scaling,
extra_purchasable, extra_purchasable,
extra_purchasable_stock, extra_purchasable_stock,
@@ -202,6 +201,8 @@ export class Uplink extends Component<{}, UplinkState> {
const item = itemsToAdd[i]; const item = itemsToAdd[i];
const hasEnoughProgression = const hasEnoughProgression =
progression_points >= item.progression_minimum; progression_points >= item.progression_minimum;
const hasEnoughPop =
!joined_population || joined_population >= item.population_minimum;
let stock: number | null = current_stock[item.stock_key]; let stock: number | null = current_stock[item.stock_key];
if (item.ref) { if (item.ref) {
@@ -245,8 +246,14 @@ export class Uplink extends Component<{}, UplinkState> {
)} )}
</Box> </Box>
), ),
population_tooltip:
'This item is not cleared for operations performed against stations crewed by fewer than ' +
item.population_minimum +
' people.',
insufficient_population: !hasEnoughPop,
disabled: disabled:
!canBuy || !canBuy ||
!hasEnoughPop ||
(has_progression && !hasEnoughProgression) || (has_progression && !hasEnoughProgression) ||
(item.lock_other_purchases && purchased_items > 0), (item.lock_other_purchases && purchased_items > 0),
extraData: { 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 ( return (
<Window width={700} height={600} theme="syndicate"> <Window width={700} height={600} theme="syndicate">
<Window.Content> <Window.Content>
@@ -292,29 +289,6 @@ export class Uplink extends Component<{}, UplinkState> {
</Box> </Box>
&nbsp;every minute &nbsp;every minute
</Box> </Box>
{Math.abs(progressionPercentage) > 0 && (
<Box mt={0.5}>
Because your threat level is
{progressionPercentage < 0
? ' ahead '
: ' behind '}
of where it should be, you are getting
<Box
as="span"
color={
progressionPercentage < 0
? 'red'
: 'green'
}
ml={1}
mr={1}
>
{progressionPercentage}%
</Box>
{progressionPercentage < 0 ? 'less' : 'more'}{' '}
threat every minute
</Box>
)}
{dangerLevelsTooltip} {dangerLevelsTooltip}
</Box> </Box>
</Box> </Box>

View File

@@ -34,6 +34,8 @@ export function MalfAiModules(props) {
icon: item.icon, icon: item.icon,
id: item.name, id: item.name,
name: item.name, name: item.name,
population_tooltip: '',
insufficient_population: false,
}); });
} }
} }