From 09a6cdb4e01941867d30b77cd9404ffab0275e1a Mon Sep 17 00:00:00 2001
From: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
Date: Wed, 18 May 2022 20:16:46 -0700
Subject: [PATCH] Dynamic 2022, Part 1 - Redesigning midround rolls (Midrounds
happen far more frequently now, and scale their power over time) (#65997)
Implements the new midround roll changes.
1. Split midround rulesets into heavy/light impact
First, midround rulesets will be split into light/heavy impact categories. For example, sleeper agent and midround thief would be classified as light impact, while blob/ninja would be classified as heavy impact.
This split will then be used to spawn lighter impact antagonists earlier into the round, and higher impact antagonists later into the round.
Midrounds before 25 minutes = 100% light impact / 0% heavy impact
Midrounds from 25-60 minutes = Varying chances, increasing in favor of heavy impact after enough time
Midrounds from 60+ minutes = 0% light impact / 100% heavy impact
Low impact threat rulesets are guaranteed to roll if there is enough threat. High impact threat rulesets will carry the same % chance as they do now. If a heavy impact threat ruleset cannot be rolled, but there is enough threat, a light impact threat ruleset will roll in its place. In the future, this can potentially be changed into not spawning any ruleset if the station is deemed extremely hostile, or even be tweaked into spawning protagonists like ERTs.
Alongside this, rulesets should be able to determine their minimum time required. For example, nuclear assault has terrible numbers--it's very high cost, and very low weight, which are basically the only variables it can configure. In Dynamic 2022, it should be able to have low cost, still low weight (though maybe nto as much), while only being rollable after 70 minutes or so.
2. Midround antagonists now roll as intervals, determined by midround threat
In order to make sure 50 midround threat spawns more antagonists than 10 midround threat, the intervals at which midround threats are spawned will change from 15-45 minutes to increments determined by midround threat. Larger midround threat = more, close intervals, smaller midround threat = less, farther intervals.
Any threat not spent on midrounds will still be carried into latejoins, which as before will still consume midround threat, and are unchanged by this document.
---
code/__DEFINES/dynamic.dm | 9 +-
code/game/gamemodes/dynamic/dynamic.dm | 191 +++++++-----------
.../gamemodes/dynamic/dynamic_hijacking.dm | 8 +-
.../dynamic/dynamic_midround_rolling.dm | 90 +++++++++
.../dynamic/dynamic_rulesets_latejoin.dm | 4 +-
.../dynamic/dynamic_rulesets_midround.dm | 90 +++++----
.../dynamic/dynamic_rulesets_roundstart.dm | 9 -
code/game/gamemodes/dynamic/readme.md | 38 ++--
.../game/gamemodes/dynamic/ruleset_picking.dm | 4 +-
.../antagonists/_common/antag_datum.dm | 10 +-
.../antagonists/ashwalker/ashwalker.dm | 1 +
.../antagonists/brainwashing/brainwashing.dm | 1 +
.../antagonists/changeling/changeling.dm | 2 +-
code/modules/antagonists/creep/creep.dm | 1 +
code/modules/antagonists/ert/ert.dm | 1 +
code/modules/antagonists/fugitive/fugitive.dm | 1 +
code/modules/antagonists/fugitive/hunter.dm | 1 +
.../antagonists/greentext/greentext.dm | 1 +
.../antagonists/highlander/highlander.dm | 1 +
.../antagonists/hypnotized/hypnotized.dm | 1 +
code/modules/antagonists/thief/thief.dm | 1 +
.../antagonists/valentines/valentine.dm | 2 +-
code/modules/mob/mob_lists.dm | 9 +-
.../unit_tests/dynamic_ruleset_sanity.dm | 9 +-
tgstation.dme | 1 +
25 files changed, 281 insertions(+), 205 deletions(-)
create mode 100644 code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
diff --git a/code/__DEFINES/dynamic.dm b/code/__DEFINES/dynamic.dm
index dedf05396c5..ae32d40a930 100644
--- a/code/__DEFINES/dynamic.dm
+++ b/code/__DEFINES/dynamic.dm
@@ -7,11 +7,14 @@
/// This ruleset can only be picked once. Anything that does not have a scaling_cost MUST have this.
#define LONE_RULESET (1 << 2)
+/// This is a "heavy" midround ruleset, and should be run later into the round
+#define MIDROUND_RULESET_STYLE_HEAVY "Heavy"
+
+/// This is a "light" midround ruleset, and should be run early into the round
+#define MIDROUND_RULESET_STYLE_LIGHT "Light"
+
/// No round event was hijacked this cycle
#define HIJACKED_NOTHING "HIJACKED_NOTHING"
/// This cycle, a round event was hijacked when the last midround event was too recent.
#define HIJACKED_TOO_RECENT "HIJACKED_TOO_RECENT"
-
-/// This cycle, a round event was hijacked when the next midround event is too soon.
-#define HIJACKED_TOO_SOON "HIJACKED_TOO_SOON"
diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm
index bcde127b473..b05802b5362 100644
--- a/code/game/gamemodes/dynamic/dynamic.dm
+++ b/code/game/gamemodes/dynamic/dynamic.dm
@@ -78,26 +78,29 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// The maximum time the recurring latejoin ruleset timer is allowed to be.
var/latejoin_delay_max = (25 MINUTES)
- /// When world.time is over this number the mode tries to inject a midround ruleset.
- var/midround_injection_cooldown = 0
+ /// The low bound for the midround roll time splits.
+ /// This number influences where to place midround rolls, making this smaller
+ /// will make midround rolls more frequent, and vice versa.
+ /// A midround will never be able to roll before this.
+ var/midround_lower_bound = 10 MINUTES
- /// The minimum time the recurring midround ruleset timer is allowed to be.
- var/midround_delay_min = (15 MINUTES)
+ /// The upper bound for the midround roll time splits.
+ /// This number influences where to place midround rolls, making this larger
+ /// will make midround rolls less frequent, and vice versa.
+ /// A midround will never be able to roll farther than this.
+ var/midround_upper_bound = 100 MINUTES
- /// The maximum time the recurring midround ruleset timer is allowed to be.
- var/midround_delay_max = (35 MINUTES)
+ /// The distance between the chosen midround roll point (which is deterministic),
+ /// and when it can actually roll.
+ /// Basically, if this is set to 5 minutes, and a midround roll point is decided to be at 20 minutes,
+ /// then it can roll anywhere between 15 and 25 minutes.
+ var/midround_roll_distance = 3 MINUTES
- /// If above this threat, increase the chance of injection
- var/higher_injection_chance_minimum_threat = 70
-
- /// The chance of injection increase when above higher_injection_chance_minimum_threat
- var/higher_injection_chance = 15
-
- /// If below this threat, decrease the chance of injection
- var/lower_injection_chance_minimum_threat = 10
-
- /// The chance of injection decrease when above lower_injection_chance_minimum_threat
- var/lower_injection_chance = 15
+ /// The amount of threat per midround roll.
+ /// Basically, if this is set to 5, then for every 5 threat, one midround roll will be added.
+ /// The equation this is used in rounds up, meaning that if this is set to 5, and you have 6
+ /// threat, then you will get 2 midround rolls.
+ var/threat_per_midround_roll = 6.5
/// A number between -5 and +5.
/// A negative value will give a more peaceful round and
@@ -127,15 +130,49 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// The maximum amount of time for antag random events to be hijacked.
var/random_event_hijack_maximum = 18 MINUTES
+ /// What is the lower bound of when the roundstart annoucement is sent out?
+ var/waittime_l = 600
+
+ /// What is the higher bound of when the roundstart annoucement is sent out?
+ var/waittime_h = 1800
+
+ /// Maximum amount of threat allowed to generate.
+ var/max_threat_level = 100
+
+ /// The extra chance multiplier that a heavy impact midround ruleset will run next time.
+ /// For example, if this is set to 50, then the next heavy roll will be about 50% more likely to happen.
+ var/hijacked_random_event_injection_chance_modifier = 50
+
+ /// Any midround before this point is guaranteed to be light
+ var/midround_light_upper_bound = 25 MINUTES
+
+ /// Any midround after this point is guaranteed to be heavy
+ var/midround_heavy_lower_bound = 55 MINUTES
+
+ /// If there are less than this many players readied, threat level will be lowered.
+ /// This number should be kept fairly low, as there are other measures that population
+ /// impacts Dynamic, such as the requirements variable on rulesets.
+ var/low_pop_player_threshold = 20
+
+ /// The maximum threat that can roll with *zero* players.
+ /// As the number of players approaches `low_pop_player_threshold`, the maximum
+ /// threat level will increase.
+ /// For example, if `low_pop_minimum_threat` is 50, `low_pop_player_threshold` is 20,
+ /// and the number of readied players is 10, then the highest threat that can roll is
+ /// lerp(50, 100, 10 / 20), AKA 75.
+ var/low_pop_minimum_threat = 50
+
+ /// The chance for latejoins to roll when ready
+ var/latejoin_roll_chance = 50
+
+ // == EVERYTHING BELOW THIS POINT SHOULD NOT BE CONFIGURED ==
+
/// A list of recorded "snapshots" of the round, stored in the dynamic.json log
var/list/datum/dynamic_snapshot/snapshots
/// The time when the last midround injection was attempted, whether or not it was successful
var/last_midround_injection_attempt = 0
- /// The amount to inject when a round event is hijacked
- var/hijacked_random_event_injection_chance = 50
-
/// Whether or not a random event has been hijacked this midround cycle
var/random_event_hijacked = HIJACKED_NOTHING
@@ -150,14 +187,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// Can differ from the actual threat amount.
var/shown_threat
- /// What is the lower bound of when the roundstart annoucement is sent out?
- var/waittime_l = 600
- /// What is the higher bound of when the roundstart annoucement is sent out?
- var/waittime_h = 1800
-
- /// Maximum amount of threat allowed to generate.
- var/max_threat_level = 100
-
+ VAR_PRIVATE/next_midround_injection
/datum/game_mode/dynamic/admin_panel()
var/list/dat = list("
Game Mode PanelGame Mode Panel
")
@@ -186,9 +216,15 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
dat += "[DR.ruletype] - [DR.name]
"
else
dat += "none.
"
- dat += "
Injection Timers: ([get_injection_chance(dry_run = TRUE)]% latejoin chance, [get_midround_injection_chance(dry_run = TRUE)]% midround chance)
"
+ dat += "
Injection Timers: ([get_heavy_midround_injection_chance(dry_run = TRUE)]% heavy midround chance)
"
dat += "Latejoin: [DisplayTimeText(latejoin_injection_cooldown-world.time)] \[Now!\]
"
- dat += "Midround: [DisplayTimeText(midround_injection_cooldown-world.time)] \[Now!\]
"
+
+ var/next_injection = next_midround_injection()
+ if (next_injection == INFINITY)
+ dat += "All midrounds have been exhausted."
+ else
+ dat += "Midround: [DisplayTimeText(next_injection - world.time)] \[Now!\]
"
+
usr << browse(dat.Join(), "window=gamemode_panel;size=500x500")
/datum/game_mode/dynamic/Topic(href, href_list)
@@ -215,9 +251,9 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
forced_injection = TRUE
message_admins("[key_name(usr)] forced a latejoin injection.")
else if (href_list["injectmid"])
- midround_injection_cooldown = 0
forced_injection = TRUE
message_admins("[key_name(usr)] forced a midround injection.")
+ try_midround_roll()
else if (href_list["threatlog"])
show_threatlog(usr)
else if (href_list["stacking_limit"])
@@ -330,6 +366,9 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
var/relative_threat = LORENTZ_DISTRIBUTION(threat_curve_centre, threat_curve_width)
threat_level = clamp(round(lorentz_to_amount(relative_threat), 0.1), 0, max_threat_level)
+ if (SSticker.totalPlayersReady < low_pop_player_threshold)
+ threat_level = min(threat_level, LERP(low_pop_minimum_threat, max_threat_level, SSticker.totalPlayersReady / low_pop_player_threshold))
+
peaceful_percentage = round(LORENTZ_CUMULATIVE_DISTRIBUTION(relative_threat, threat_curve_centre, threat_curve_width), 0.01)*100
/// Generates the midround and roundstart budgets
@@ -362,9 +401,6 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
var/latejoin_injection_cooldown_middle = 0.5*(latejoin_delay_max + latejoin_delay_min)
latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), latejoin_delay_min, latejoin_delay_max)) + world.time
- var/midround_injection_cooldown_middle = 0.5*(midround_delay_max + midround_delay_min)
- midround_injection_cooldown = round(clamp(EXP_DISTRIBUTION(midround_injection_cooldown_middle), midround_delay_min, midround_delay_max)) + world.time
-
/datum/game_mode/dynamic/pre_setup()
if(CONFIG_GET(flag/dynamic_config_enabled))
var/json_file = file("[global.config.directory]/dynamic.json")
@@ -606,80 +642,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
if(rule.rule_process() == RULESET_STOP_PROCESSING) // If rule_process() returns 1 (RULESET_STOP_PROCESSING), stop processing.
current_rules -= rule
- if (midround_injection_cooldown < world.time)
- if (GLOB.dynamic_forced_extended)
- return
-
- // Somehow it managed to trigger midround multiple times so this was moved here.
- // There is no way this should be able to trigger an injection twice now.
- var/midround_injection_cooldown_middle = 0.5*(midround_delay_max + midround_delay_min)
- midround_injection_cooldown = (round(clamp(EXP_DISTRIBUTION(midround_injection_cooldown_middle), midround_delay_min, midround_delay_max)) + world.time)
-
- // Time to inject some threat into the round
- if(EMERGENCY_PAST_POINT_OF_NO_RETURN) // Unless the shuttle is past the point of no return
- return
-
- message_admins("DYNAMIC: Checking for midround injection.")
- log_game("DYNAMIC: Checking for midround injection.")
-
- last_midround_injection_attempt = world.time
-
- if (prob(get_midround_injection_chance()))
- var/list/drafted_rules = list()
- for (var/datum/dynamic_ruleset/midround/rule in midround_rules)
- if (!rule.weight)
- continue
- if (rule.acceptable(GLOB.alive_player_list.len, threat_level) && mid_round_budget >= rule.cost)
- // If admins have disabled dynamic from picking from the ghost pool
- if(rule.ruletype == "Latejoin" && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT))
- continue
- rule.trim_candidates()
- if (rule.ready())
- drafted_rules[rule] = rule.get_weight()
- if (drafted_rules.len > 0)
- pick_midround_rule(drafted_rules)
- else if (random_event_hijacked == HIJACKED_TOO_SOON)
- log_game("DYNAMIC: Midround injection failed when random event was hijacked. Spawning another random event in its place.")
-
- // A random event antag would have rolled had this injection check passed.
- // As a refund, spawn a non-ghost-role random event.
- SSevents.spawnEvent()
- SSevents.reschedule()
-
- random_event_hijacked = HIJACKED_NOTHING
-
-/// Gets the chance for latejoin injection, the dry_run argument is only used for forced injection.
-/datum/game_mode/dynamic/proc/get_injection_chance(dry_run = FALSE)
- if(forced_injection)
- forced_injection = dry_run
- return 100
- var/chance = 0
- var/max_pop_per_antag = max(5,15 - round(threat_level/10) - round(GLOB.alive_player_list.len/5))
- if (!GLOB.current_living_antags.len)
- chance += 50 // No antags at all? let's boost those odds!
- else
- var/current_pop_per_antag = GLOB.alive_player_list.len / GLOB.current_living_antags.len
- if (current_pop_per_antag > max_pop_per_antag)
- chance += min(50, 25+10*(current_pop_per_antag-max_pop_per_antag))
- else
- chance += 25-10*(max_pop_per_antag-current_pop_per_antag)
- if (GLOB.dead_player_list.len > GLOB.alive_player_list.len)
- chance -= 30 // More than half the crew died? ew, let's calm down on antags
- if (mid_round_budget > higher_injection_chance_minimum_threat)
- chance += higher_injection_chance
- if (mid_round_budget < lower_injection_chance_minimum_threat)
- chance -= lower_injection_chance
- return round(max(0,chance))
-
-/// Gets the chance for midround injection, the dry_run argument is only used for forced injection.
-/// Usually defers to the latejoin injection chance.
-/datum/game_mode/dynamic/proc/get_midround_injection_chance(dry_run)
- var/chance = get_injection_chance(dry_run)
-
- if (random_event_hijacked != HIJACKED_NOTHING)
- chance += hijacked_random_event_injection_chance
-
- return chance
+ try_midround_roll()
/// Removes type from the list
/datum/game_mode/dynamic/proc/remove_from_list(list/type_list, type)
@@ -714,7 +677,9 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
addtimer(CALLBACK(src, /datum/game_mode/dynamic/.proc/execute_midround_latejoin_rule, forced_latejoin_rule), forced_latejoin_rule.delay)
forced_latejoin_rule = null
- else if (latejoin_injection_cooldown < world.time && prob(get_injection_chance()))
+ else if (latejoin_injection_cooldown < world.time && (forced_injection || prob(latejoin_roll_chance)))
+ forced_injection = FALSE
+
var/list/drafted_rules = list()
for (var/datum/dynamic_ruleset/latejoin/rule in latejoin_rules)
if (!rule.weight)
@@ -768,14 +733,14 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// Expend round start threat, can't fall under 0.
/datum/game_mode/dynamic/proc/spend_roundstart_budget(cost, list/threat_log, reason)
round_start_budget = max(round_start_budget - cost,0)
- for(var/list/logs in threat_log)
- log_threat(cost, logs, reason)
+ if (!isnull(threat_log))
+ log_threat(-cost, threat_log, reason)
/// Expend midround threat, can't fall under 0.
/datum/game_mode/dynamic/proc/spend_midround_budget(cost, list/threat_log, reason)
mid_round_budget = max(mid_round_budget - cost,0)
- for(var/list/logs in threat_log)
- log_threat(cost, logs, reason)
+ if (!isnull(threat_log))
+ log_threat(-cost, threat_log, reason)
/// Turns the value generated by lorentz distribution to number between 0 and 100.
/// Used for threat level and splitting the budgets.
diff --git a/code/game/gamemodes/dynamic/dynamic_hijacking.dm b/code/game/gamemodes/dynamic/dynamic_hijacking.dm
index 04892ad1530..335a4246a2b 100644
--- a/code/game/gamemodes/dynamic/dynamic_hijacking.dm
+++ b/code/game/gamemodes/dynamic/dynamic_hijacking.dm
@@ -17,11 +17,9 @@
if (world.time - last_midround_injection_attempt < time_range)
random_event_hijacked = HIJACKED_TOO_RECENT
dynamic_log("Random event [round_event_control.name] tried to roll, but the last midround injection \
- was too recent. Injection chance has been raised to [get_midround_injection_chance(dry_run = TRUE)]%.")
+ was too recent. Heavy injection chance has been raised to [get_heavy_midround_injection_chance(dry_run = TRUE)]%.")
return CANCEL_PRE_RANDOM_EVENT
- if (midround_injection_cooldown - world.time < time_range)
- random_event_hijacked = HIJACKED_TOO_SOON
- dynamic_log("Random event [round_event_control.name] tried to roll, but the next midround injection \
- is too soon. Injection chance has been raised to [get_midround_injection_chance(dry_run = TRUE)]%.")
+ if (next_midround_injection() - world.time < time_range)
+ dynamic_log("Random event [round_event_control.name] tried to roll, but the next midround injection is too soon.")
return CANCEL_PRE_RANDOM_EVENT
diff --git a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
new file mode 100644
index 00000000000..96fb0d68413
--- /dev/null
+++ b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
@@ -0,0 +1,90 @@
+/// Returns the world.time of the next midround injection.
+/// Will return a cached result from `next_midround_injection`, the variable.
+/// If that variable is null, will generate a new one.
+/datum/game_mode/dynamic/proc/next_midround_injection()
+ if (!isnull(next_midround_injection))
+ return next_midround_injection
+
+ // Admins can futz around with the midround threat, and we want to be able to react to that
+ var/midround_threat = threat_level - round_start_budget
+
+ var/rolls = CEILING(midround_threat / threat_per_midround_roll, 1)
+ var/distance = ((1 / (rolls + 1)) * midround_upper_bound) + midround_lower_bound
+
+ if (last_midround_injection_attempt == 0)
+ last_midround_injection_attempt = SSticker.round_start_time
+
+ return last_midround_injection_attempt + distance
+
+/datum/game_mode/dynamic/proc/try_midround_roll()
+ if (!forced_injection && next_midround_injection() > world.time)
+ return
+
+ if (GLOB.dynamic_forced_extended)
+ return
+
+ if (EMERGENCY_PAST_POINT_OF_NO_RETURN)
+ return
+
+ last_midround_injection_attempt = world.time
+ next_midround_injection = null
+ forced_injection = FALSE
+
+ var/spawn_heavy = prob(get_heavy_midround_injection_chance())
+ dynamic_log("A midround ruleset is rolling, and will be [spawn_heavy ? "HEAVY" : "LIGHT"].")
+
+ random_event_hijacked = HIJACKED_NOTHING
+
+ var/list/drafted_heavies = list()
+ var/list/drafted_lights = list()
+
+ for (var/datum/dynamic_ruleset/midround/ruleset in midround_rules)
+ if (ruleset.weight == 0)
+ continue
+
+ if (!ruleset.acceptable(GLOB.alive_player_list.len, threat_level))
+ continue
+
+ if (mid_round_budget < ruleset.cost)
+ continue
+
+ if (ruleset.minimum_round_time > world.time - SSticker.round_start_time)
+ continue
+
+ // If admins have disabled dynamic from picking from the ghost pool
+ if(istype(ruleset, /datum/dynamic_ruleset/midround/from_ghosts) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT))
+ continue
+
+ ruleset.trim_candidates()
+ if (ruleset.ready())
+ var/ruleset_is_heavy = (ruleset.midround_ruleset_style == MIDROUND_RULESET_STYLE_HEAVY)
+ if (ruleset_is_heavy)
+ drafted_heavies[ruleset] = ruleset.get_weight()
+ else
+ drafted_lights[ruleset] = ruleset.get_weight()
+
+ if (spawn_heavy && drafted_heavies.len > 0 && pick_midround_rule(drafted_heavies))
+ return
+ else if (drafted_lights.len > 0 && pick_midround_rule(drafted_lights))
+ if (spawn_heavy)
+ dynamic_log("A heavy ruleset was intended to roll, but there weren't any available.")
+ else
+ dynamic_log("No midround rulesets could be drafted.")
+
+/// Gets the chance for a heavy ruleset midround injection, the dry_run argument is only used for forced injection.
+/datum/game_mode/dynamic/proc/get_heavy_midround_injection_chance(dry_run)
+ var/chance_modifier = 1
+ var/next_midround_roll = next_midround_injection() - SSticker.round_start_time
+
+ if (random_event_hijacked != HIJACKED_NOTHING)
+ chance_modifier += (hijacked_random_event_injection_chance_modifier / 100)
+
+ if (GLOB.current_living_antags.len == 0)
+ chance_modifier += 0.5
+
+ if (GLOB.dead_player_list.len > GLOB.alive_player_list.len)
+ chance_modifier -= 0.3
+
+ var/heavy_coefficient = CLAMP01((next_midround_roll - midround_light_upper_bound) / (midround_heavy_lower_bound - midround_light_upper_bound))
+
+ return 100 * (heavy_coefficient * max(1, chance_modifier))
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
index 9eebe23f509..2e682d7ef72 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
@@ -107,7 +107,7 @@
required_candidates = 1
weight = 2
delay = 1 MINUTES // Prevents rule start while head is offstation.
- cost = 20
+ cost = 10
requirements = list(101,101,70,40,30,20,20,20,20,20)
flags = HIGH_IMPACT_RULESET
blocking_rules = list(/datum/dynamic_ruleset/roundstart/revs)
@@ -192,7 +192,7 @@
)
required_candidates = 1
weight = 4
- cost = 10
+ cost = 7
requirements = list(101,101,101,10,10,10,10,10,10,10)
repeatable = TRUE
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index 143e7cfe483..ee5cdef6b51 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -11,6 +11,7 @@
/datum/dynamic_ruleset/midround // Can be drafted once in a while during a round
ruletype = "Midround"
+ var/midround_ruleset_style
/// If the ruleset should be restricted from ghost roles.
var/restrict_ghost_roles = TRUE
/// What mob type the ruleset is restricted to.
@@ -20,6 +21,9 @@
var/list/dead_players = list()
var/list/list_observers = list()
+ /// The minimum round time before this ruleset will show up
+ var/minimum_round_time = 0
+
/datum/dynamic_ruleset/midround/from_ghosts
weight = 0
required_type = /mob/dead/observer
@@ -163,9 +167,6 @@
/datum/dynamic_ruleset/midround/from_ghosts/proc/attempt_replacement()
var/datum/dynamic_ruleset/midround/autotraitor/sleeper_agent = new
- // Otherwise, it has a chance to fail. We don't want that, since this is already pretty unlikely.
- sleeper_agent.has_failure_chance = FALSE
-
mode.configure_ruleset(sleeper_agent)
if (!mode.picking_specific_rule(sleeper_agent))
@@ -181,6 +182,7 @@
/datum/dynamic_ruleset/midround/autotraitor
name = "Syndicate Sleeper Agent"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
antag_datum = /datum/antagonist/traitor
antag_flag = ROLE_SLEEPER_AGENT
antag_flag_override = ROLE_TRAITOR
@@ -199,32 +201,11 @@
ROLE_POSITRONIC_BRAIN,
)
required_candidates = 1
- weight = 7
- cost = 10
- requirements = list(10,10,10,10,10,10,10,10,10,10)
+ weight = 35
+ cost = 3
+ requirements = list(3,3,3,3,3,3,3,3,3,3)
repeatable = TRUE
- /// Whether or not this instance of sleeper agent should be randomly acceptable.
- /// If TRUE, then this has a threat level% chance to succeed.
- var/has_failure_chance = TRUE
-
-/datum/dynamic_ruleset/midround/autotraitor/acceptable(population = 0, threat = 0)
- var/player_count = GLOB.alive_player_list.len
- var/antag_count = GLOB.current_living_antags.len
- var/max_traitors = round(player_count / 10) + 1
-
- // adding traitors if the antag population is getting low
- var/too_little_antags = antag_count < max_traitors
- if (!too_little_antags)
- log_game("DYNAMIC: Too many living antags compared to living players ([antag_count] living antags, [player_count] living players, [max_traitors] max traitors)")
- return FALSE
-
- if (has_failure_chance && !prob(mode.threat_level))
- log_game("DYNAMIC: Random chance to roll autotraitor failed, it was a [mode.threat_level]% chance.")
- return FALSE
-
- return ..()
-
/datum/dynamic_ruleset/midround/autotraitor/trim_candidates()
..()
for(var/mob/living/player in living_players)
@@ -258,6 +239,7 @@
/datum/dynamic_ruleset/midround/families
name = "Family Head Aspirants"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
persistent = TRUE
antag_datum = /datum/antagonist/gang
antag_flag = ROLE_FAMILY_HEAD_ASPIRANT
@@ -335,6 +317,7 @@
/datum/dynamic_ruleset/midround/malf
name = "Malfunctioning AI"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_datum = /datum/antagonist/malf_ai
antag_flag = ROLE_MALF_MIDROUND
antag_flag_override = ROLE_MALF
@@ -351,7 +334,7 @@
required_enemies = list(4,4,4,4,4,4,2,2,2,0)
required_candidates = 1
weight = 3
- cost = 22
+ cost = 10
requirements = list(101,101,101,80,60,50,30,20,10,10)
required_type = /mob/living/silicon/ai
blocking_rules = list(/datum/dynamic_ruleset/roundstart/malf_ai)
@@ -395,6 +378,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/wizard
name = "Wizard"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_datum = /datum/antagonist/wizard
antag_flag = ROLE_WIZARD_MIDROUND
antag_flag_override = ROLE_WIZARD
@@ -407,8 +391,8 @@
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
weight = 1
- cost = 20
- requirements = list(90,90,90,80,60,40,30,20,10,10)
+ cost = 10
+ requirements = list(90,90,90,80,60,50,40,40,40,40)
flags = HIGH_IMPACT_RULESET
/datum/dynamic_ruleset/midround/from_ghosts/wizard/ready(forced = FALSE)
@@ -432,6 +416,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/nuclear
name = "Nuclear Assault"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_flag = ROLE_OPERATIVE_MIDROUND
antag_flag_override = ROLE_OPERATIVE
antag_datum = /datum/antagonist/nukeop
@@ -446,7 +431,8 @@
required_enemies = list(3,3,3,3,3,2,1,1,0,0)
required_candidates = 5
weight = 5
- cost = 35
+ cost = 7
+ minimum_round_time = 70 MINUTES
requirements = list(90,90,90,80,60,40,30,20,10,10)
var/list/operative_cap = list(2,2,3,3,4,5,5,5,5,5)
var/datum/team/nuclear/nuke_team
@@ -482,6 +468,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/blob
name = "Blob"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_datum = /datum/antagonist/blob
antag_flag = ROLE_BLOB
enemy_roles = list(
@@ -492,8 +479,9 @@
)
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
- weight = 2
- cost = 10
+ minimum_round_time = 35 MINUTES
+ weight = 3
+ cost = 8
requirements = list(101,101,101,80,60,50,30,20,10,10)
repeatable = TRUE
@@ -504,6 +492,7 @@
/// Infects a random player, making them explode into a blob.
/datum/dynamic_ruleset/midround/blob_infection
name = "Blob Infection"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_datum = /datum/antagonist/blob/infection
antag_flag = ROLE_BLOB_INFECTION
antag_flag_override = ROLE_BLOB
@@ -528,7 +517,8 @@
)
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
- weight = 2
+ minimum_round_time = 35 MINUTES
+ weight = 3
cost = 10
requirements = list(101,101,101,80,60,50,30,20,10,10)
repeatable = TRUE
@@ -561,6 +551,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph
name = "Alien Infestation"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_datum = /datum/antagonist/xeno
antag_flag = ROLE_ALIEN
enemy_roles = list(
@@ -571,7 +562,8 @@
)
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
- weight = 3
+ minimum_round_time = 40 MINUTES
+ weight = 5
cost = 10
requirements = list(101,101,101,70,50,40,20,15,10,10)
repeatable = TRUE
@@ -612,6 +604,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/nightmare
name = "Nightmare"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
antag_datum = /datum/antagonist/nightmare
antag_flag = ROLE_NIGHTMARE
antag_flag_override = ROLE_ALIEN
@@ -624,7 +617,7 @@
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
weight = 3
- cost = 10
+ cost = 5
requirements = list(101,101,101,70,50,40,20,15,10,10)
repeatable = TRUE
var/list/spawn_locs = list()
@@ -663,6 +656,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/space_dragon
name = "Space Dragon"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_datum = /datum/antagonist/space_dragon
antag_flag = ROLE_SPACE_DRAGON
antag_flag_override = ROLE_SPACE_DRAGON
@@ -675,7 +669,7 @@
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
weight = 4
- cost = 10
+ cost = 7
requirements = list(101,101,101,80,60,50,30,20,10,10)
repeatable = TRUE
var/list/spawn_locs = list()
@@ -713,6 +707,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/abductors
name = "Abductors"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_datum = /datum/antagonist/abductor
antag_flag = ROLE_ABDUCTOR
enemy_roles = list(
@@ -725,7 +720,7 @@
required_candidates = 2
required_applicants = 2
weight = 4
- cost = 10
+ cost = 7
requirements = list(101,101,101,80,60,50,30,20,10,10)
repeatable = TRUE
var/datum/team/abductor_team/new_team
@@ -756,6 +751,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/space_ninja
name = "Space Ninja"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_datum = /datum/antagonist/ninja
antag_flag = ROLE_NINJA
enemy_roles = list(
@@ -767,7 +763,7 @@
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
weight = 4
- cost = 10
+ cost = 8
requirements = list(101,101,101,80,60,50,30,20,10,10)
repeatable = TRUE
var/list/spawn_locs = list()
@@ -800,6 +796,7 @@
/datum/dynamic_ruleset/midround/spiders
name = "Spiders"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_flag = ROLE_SPIDER
required_type = /mob/dead/observer
enemy_roles = list(
@@ -811,7 +808,7 @@
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 0
weight = 3
- cost = 10
+ cost = 8
requirements = list(101,101,101,80,60,50,30,20,10,10)
repeatable = TRUE
var/spawncount = 2
@@ -823,6 +820,7 @@
/// Revenant ruleset
/datum/dynamic_ruleset/midround/from_ghosts/revenant
name = "Revenant"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
antag_datum = /datum/antagonist/revenant
antag_flag = ROLE_REVENANT
enemy_roles = list(
@@ -834,7 +832,7 @@
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
weight = 4
- cost = 10
+ cost = 5
requirements = list(101,101,101,70,50,40,20,15,10,10)
repeatable = TRUE
var/dead_mobs_required = 20
@@ -874,11 +872,12 @@
/// Sentient Disease ruleset
/datum/dynamic_ruleset/midround/from_ghosts/sentient_disease
name = "Sentient Disease"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_datum = /datum/antagonist/disease
antag_flag = ROLE_SENTIENT_DISEASE
required_candidates = 1
weight = 4
- cost = 10
+ cost = 8
requirements = list(101,101,101,80,60,50,30,20,10,10)
repeatable = TRUE
@@ -893,6 +892,7 @@
/// Space Pirates ruleset
/datum/dynamic_ruleset/midround/pirates
name = "Space Pirates"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
antag_flag = "Space Pirates"
required_type = /mob/dead/observer
enemy_roles = list(
@@ -904,7 +904,7 @@
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 0
weight = 4
- cost = 10
+ cost = 8
requirements = list(101,101,101,80,60,50,30,20,10,10)
repeatable = TRUE
@@ -920,6 +920,7 @@
/// Obsessed ruleset
/datum/dynamic_ruleset/midround/obsessed
name = "Obsessed"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
antag_datum = /datum/antagonist/obsessed
antag_flag = ROLE_OBSESSED
restricted_roles = list(
@@ -965,6 +966,7 @@
/// Thief ruleset
/datum/dynamic_ruleset/midround/opportunist
name = "Opportunist"
+ midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
antag_datum = /datum/antagonist/thief
antag_flag = ROLE_OPPORTUNIST
antag_flag_override = ROLE_THIEF
@@ -982,7 +984,7 @@
ROLE_POSITRONIC_BRAIN,
)
required_candidates = 1
- weight = 0 // Disabled until Dynamic midround rolling handles minor threats better
+ weight = 2
cost = 3 //Worth less than obsessed, but there's more of them.
requirements = list(10,10,10,10,10,10,10,10,10,10)
repeatable = TRUE
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
index 283ef8644c1..d632a1b56bc 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
@@ -7,7 +7,6 @@
/datum/dynamic_ruleset/roundstart/traitor
name = "Traitors"
- persistent = TRUE
antag_flag = ROLE_TRAITOR
antag_datum = /datum/antagonist/traitor
minimum_required_age = 0
@@ -30,11 +29,9 @@
requirements = list(8,8,8,8,8,8,8,8,8,8)
antag_cap = list("denominator" = 24)
var/autotraitor_cooldown = (15 MINUTES)
- COOLDOWN_DECLARE(autotraitor_cooldown_check)
/datum/dynamic_ruleset/roundstart/traitor/pre_execute(population)
. = ..()
- COOLDOWN_START(src, autotraitor_cooldown_check, autotraitor_cooldown)
var/num_traitors = get_antag_cap(population) * (scaled_times + 1)
for (var/i = 1 to num_traitors)
if(candidates.len <= 0)
@@ -46,12 +43,6 @@
GLOB.pre_setup_antags += M.mind
return TRUE
-/datum/dynamic_ruleset/roundstart/traitor/rule_process()
- if (COOLDOWN_FINISHED(src, autotraitor_cooldown_check))
- COOLDOWN_START(src, autotraitor_cooldown_check, autotraitor_cooldown)
- log_game("DYNAMIC: Checking if we can turn someone into a traitor.")
- mode.picking_specific_rule(/datum/dynamic_ruleset/midround/autotraitor)
-
//////////////////////////////////////////////
// //
// MALFUNCTIONING AI //
diff --git a/code/game/gamemodes/dynamic/readme.md b/code/game/gamemodes/dynamic/readme.md
index afc7ea5c81f..b897596fb29 100644
--- a/code/game/gamemodes/dynamic/readme.md
+++ b/code/game/gamemodes/dynamic/readme.md
@@ -28,20 +28,27 @@ After this process is done, any leftover roundstart threat will be given to the
## Deciding midround threats
-Latejoin and midround injection cooldowns are set using exponential distribution between
+### Frequency
-- 5 minutes and 25 for latejoin (configurable as latejoin_delay_min and latejoin_delay_max)
-- 15 minutes and 35 for midround (configurable as midround_delay_min and midround_delay_max)
+The frequency of midround threats is based on the midround threat of the round. The number of midround threats that will roll is `threat_level` / `threat_per_midround_roll` (configurable), rounded up. For example, if `threat_per_midround_roll` is set to 5, then for every 5 threat, one midround roll will be added. If you have 6 threat, with this configuration, you will get 2 midround rolls.
-this value is then added to `world.time` and assigned to the injection cooldown variables.
+These midround roll points are then equidistantly spaced across the round, starting from `midround_lower_bound` (configurable) to `midround_upper_bound` (configurable), with a +/- of `midround_roll_distance` (configurable).
-[rigged_roundstart][/datum/game_mode/dynamic/proc/rigged_roundstart] is called instead if there are forced rules (an admin set the mode)
+For example, if:
+1. `midround_lower_bound` is `10 MINUTES`
+2. `midround_upper_bound` is `100 MINUTES`
+3. `midround_roll_distance` is `3 MINUTES`
+4. You have 5 midround rolls for the round
-1. [setup_parameters][/datum/game_mode/proc/setup_parameters]\()
-2. [pre_setup][/datum/game_mode/proc/pre_setup]\()
-3. [roundstart][/datum/game_mode/dynamic/proc/roundstart]\() OR [rigged_roundstart][/datum/game_mode/dynamic/proc/rigged_roundstart]\()
-4. [picking_roundstart_rule][/datum/game_mode/dynamic/proc/picking_roundstart_rule]\(drafted_rules)
-5. [post_setup][/datum/game_mode/proc/post_setup]\()
+...then those 5 midround rolls will be placed equidistantly (meaning equally apart) across the first 10-100 minutes of the round. Every individual roll will then be adjusted to either be 3 minutes earlier, or 3 minutes later.
+
+### Threat variety
+
+Threats are split between **heavy** rulesets and **light** rulesets. A heavy ruleset includes major threats like space dragons or blobs, while light rulesets are ones that don't often cause shuttle calls when rolled, such as revenants or traitors (sleeper agents).
+
+When a midround roll occurs, the decision to choose between light or heavy depends on the current round time. If it is less than `midround_light_upper_bound` (configurable), then it is guaranteed to be a light ruleset. If it is more than `midround_heavy_lower_bound`, then it is guaranteed to be a heavy ruleset. If it is any point in between, it will interpolate the value between those. This means that the longer the round goes on, the more likely you are to get a heavy ruleset.
+
+If no heavy ruleset can run, such as not having enough threat, then a light ruleset is guaranteed to run.
## Rule Processing
@@ -153,17 +160,14 @@ Rulesets have the following variables notable to developers and those interested
- `protected_roles` - Serves the same purpose of `restricted_roles`, except it can be turned off through configuration (`protect_roles_from_antagonist`). For example, security officers *shouldn't* be made traitor, so they are in Traitor's `protected_roles`.
- When considering putting a role in `protected_roles` or `restricted_roles`, the rule of thumb is if it is *technically infeasible* to support that job in that role. There's no *technical* reason a security officer can't be a traitor, and so they are simply in `protected_roles`. There *are* technical reasons a cyborg can't be a changeling, so they are in `restricted_roles` instead.
+This is not a complete list--search "configurable" in this README to learn more.
+
### Dynamic
The "Dynamic" key has the following configurable values:
- `pop_per_requirement` - The default value of `pop_per_requirement` for any ruleset that does not explicitly set it. Defaults to 6.
- `latejoin_delay_min`, `latejoin_delay_max` - The time range, in deciseconds (take your seconds, and multiply by 10), for a latejoin to attempt rolling. Once this timer is finished, a new one will be created within the same range.
- Suppose you have a `latejoin_delay_min` of 600 (60 seconds, 1 minute) and a `latejoin_delay_max` of 1800 (180 seconds, 3 minutes). Once the round starts, a random number in this range will be picked--let's suppose 1.5 minutes. After 1.5 minutes, Dynamic will decide if a latejoin threat should be created (a probability of `/datum/game_mode/dynamic/proc/get_injection_chance()`). Regardless of its decision, a new timer will be started within the range of 1 to 3 minutes, repeatedly.
-- `midround_delay_min`, `midround_delay_max` - Same as `latejoin_delay_min` and `latejoin_delay_max`, except for midround threats instead of latejoin ones.
-- `higher_injection_chance`, `higher_injection_chance_minimum_threat` - Manipulates the injection chance (`/datum/game_mode/dynamic/proc/get_injection_chance()`). If the *current midround budget* is above `higher_injection_chance_minimum_threat`, then this chance will be increased by `higher_injection_chance`.
- - For example: suppose you have a `higher_injection_chance_minimum_threat` of 70, and a `higher_injection_chance` of 15. This means that, if when a midround threat is trying to roll, there is 75 midround budget left, then the injection chance will go up 15%.
-- `lower_injection_chance`, `lower_injection_chance_minimum_threat` - The inverse of the `higher_injection_chance` variables. If the *current midround budget* is *below* `lower_injection_chance`, then the chance is lowered by `lower_injection_chance_minimum_threat`.
- - For example: suppose you have a `lower_injection_chance_minimum_threat` of 30, and a `lower_injection_chance` of 15. This means if there is 20 midround budget left, then the chance will lower by 15%.
- `threat_curve_centre` - A number between -5 and +5. A negative value will give a more peaceful round and a positive value will give a round with higher threat.
- `threat_curve_width` - A number between 0.5 and 4. Higher value will favour extreme rounds and lower value rounds closer to the average.
- `roundstart_split_curve_centre` - A number between -5 and +5. Equivalent to threat_curve_centre, but for the budget split. A negative value will weigh towards midround rulesets, and a positive value will weight towards roundstart ones.
@@ -180,6 +184,6 @@ Random events have the potential to be hijacked by Dynamic to keep the pace of m
In `/datum/game_mode/dynamic/on_pre_random_event` (in `dynamic_hijacking.dm`), Dynamic hooks to random events. If the `dynamic_should_hijack` variable is TRUE, the following sequence of events occurs:
-
+
-`n` is a random value between `random_event_hijack_minimum` and `random_event_hijack_maximum`. Injection chance, should it need to be raised, is increased by `hijacked_random_event_injection_chance`.
+`n` is a random value between `random_event_hijack_minimum` and `random_event_hijack_maximum`. Heavy injection chance, should it need to be raised, is increased by `hijacked_random_event_injection_chance_modifier`.
diff --git a/code/game/gamemodes/dynamic/ruleset_picking.dm b/code/game/gamemodes/dynamic/ruleset_picking.dm
index 6324d06c282..4d240cbf02c 100644
--- a/code/game/gamemodes/dynamic/ruleset_picking.dm
+++ b/code/game/gamemodes/dynamic/ruleset_picking.dm
@@ -33,7 +33,7 @@
/datum/game_mode/dynamic/proc/pick_midround_rule(list/drafted_rules)
var/datum/dynamic_ruleset/rule = pick_ruleset(drafted_rules)
if (isnull(rule))
- return
+ return null
current_midround_rulesets = drafted_rules - rule
midround_injection_timer_id = addtimer(
@@ -47,6 +47,8 @@
CANCEL | \
SOMETHING ELSE")
+ return rule
+
/// Fired after admins do not cancel a midround injection.
/datum/game_mode/dynamic/proc/execute_midround_rule(datum/dynamic_ruleset/rule)
current_midround_rulesets = null
diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm
index 0e0e0afaad4..32fe591eac0 100644
--- a/code/modules/antagonists/_common/antag_datum.dm
+++ b/code/modules/antagonists/_common/antag_datum.dm
@@ -36,7 +36,7 @@ GLOBAL_LIST_EMPTY(antagonists)
///Name of the antag hud we provide to this mob.
var/antag_hud_name
/// If set to true, the antag will not be added to the living antag list.
- var/soft_antag = FALSE
+ var/count_against_dynamic_roll_chance = TRUE
/// The battlecry this antagonist shouts when suiciding with C4/X4.
var/suicide_cry = ""
//Antag panel properties
@@ -92,14 +92,14 @@ GLOBAL_LIST_EMPTY(antagonists)
/datum/antagonist/proc/on_body_transfer(mob/living/old_body, mob/living/new_body)
SHOULD_CALL_PARENT(TRUE)
remove_innate_effects(old_body)
- if(!soft_antag && old_body && old_body.stat != DEAD && !LAZYLEN(old_body.mind?.antag_datums))
+ if(old_body && old_body.stat != DEAD && !LAZYLEN(old_body.mind?.antag_datums))
old_body.remove_from_current_living_antags()
var/datum/action/antag_info/info_button = info_button_ref?.resolve()
if(info_button)
info_button.Remove(old_body)
info_button.Grant(new_body)
apply_innate_effects(new_body)
- if(!soft_antag && new_body.stat != DEAD)
+ if(count_against_dynamic_roll_chance && new_body.stat != DEAD)
new_body.add_to_current_living_antags()
//This handles the application of antag huds/special abilities
@@ -162,7 +162,7 @@ GLOBAL_LIST_EMPTY(antagonists)
replace_banned_player()
else if(owner.current.client?.holder && (CONFIG_GET(flag/auto_deadmin_antagonists) || owner.current.client.prefs?.toggles & DEADMIN_ANTAGONIST))
owner.current.client.holder.auto_deadmin()
- if(!soft_antag && owner.current.stat != DEAD && owner.current.client)
+ if(count_against_dynamic_roll_chance && owner.current.stat != DEAD && owner.current.client)
owner.current.add_to_current_living_antags()
SEND_SIGNAL(owner, COMSIG_ANTAGONIST_GAINED, src)
@@ -203,7 +203,7 @@ GLOBAL_LIST_EMPTY(antagonists)
remove_innate_effects()
clear_antag_moodies()
LAZYREMOVE(owner.antag_datums, src)
- if(!LAZYLEN(owner.antag_datums) && !soft_antag)
+ if(!LAZYLEN(owner.antag_datums))
owner.current.remove_from_current_living_antags()
if(info_button_ref)
QDEL_NULL(info_button_ref)
diff --git a/code/modules/antagonists/ashwalker/ashwalker.dm b/code/modules/antagonists/ashwalker/ashwalker.dm
index 88b436e22c8..cee1ba44c9b 100644
--- a/code/modules/antagonists/ashwalker/ashwalker.dm
+++ b/code/modules/antagonists/ashwalker/ashwalker.dm
@@ -11,6 +11,7 @@
prevent_roundtype_conversion = FALSE
antagpanel_category = "Ash Walkers"
suicide_cry = "I HAVE NO IDEA WHAT THIS THING DOES!!"
+ count_against_dynamic_roll_chance = FALSE
var/datum/team/ashwalkers/ashie_team
/datum/antagonist/ashwalker/create_team(datum/team/team)
diff --git a/code/modules/antagonists/brainwashing/brainwashing.dm b/code/modules/antagonists/brainwashing/brainwashing.dm
index b56f276e8df..96d807207ea 100644
--- a/code/modules/antagonists/brainwashing/brainwashing.dm
+++ b/code/modules/antagonists/brainwashing/brainwashing.dm
@@ -33,6 +33,7 @@
antag_hud_name = "brainwashed"
antagpanel_category = "Other"
show_name_in_check_antagonists = TRUE
+ count_against_dynamic_roll_chance = FALSE
ui_name = "AntagInfoBrainwashed"
suicide_cry = "FOR... SOMEONE!!"
diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm
index b74ac7ffaa2..73bb37d91f9 100644
--- a/code/modules/antagonists/changeling/changeling.dm
+++ b/code/modules/antagonists/changeling/changeling.dm
@@ -908,7 +908,7 @@
name = "\improper Headslug Changeling"
show_in_antagpanel = FALSE
give_objectives = FALSE
- soft_antag = TRUE
+ count_against_dynamic_roll_chance = FALSE
genetic_points = 5
total_genetic_points = 5
diff --git a/code/modules/antagonists/creep/creep.dm b/code/modules/antagonists/creep/creep.dm
index 5c52106442b..07618c4239d 100644
--- a/code/modules/antagonists/creep/creep.dm
+++ b/code/modules/antagonists/creep/creep.dm
@@ -6,6 +6,7 @@
antag_hud_name = "obsessed"
show_name_in_check_antagonists = TRUE
roundend_category = "obsessed"
+ count_against_dynamic_roll_chance = FALSE
silent = TRUE //not actually silent, because greet will be called by the trauma anyway.
suicide_cry = "FOR MY LOVE!!"
preview_outfit = /datum/outfit/obsessed
diff --git a/code/modules/antagonists/ert/ert.dm b/code/modules/antagonists/ert/ert.dm
index 56bae0d9485..50be074e611 100644
--- a/code/modules/antagonists/ert/ert.dm
+++ b/code/modules/antagonists/ert/ert.dm
@@ -10,6 +10,7 @@
show_to_ghosts = TRUE
antag_moodlet = /datum/mood_event/focused
suicide_cry = "FOR NANOTRASEN!!"
+ count_against_dynamic_roll_chance = FALSE
var/datum/team/ert/ert_team
var/leader = FALSE
var/datum/outfit/outfit = /datum/outfit/centcom/ert/security
diff --git a/code/modules/antagonists/fugitive/fugitive.dm b/code/modules/antagonists/fugitive/fugitive.dm
index d3d18775667..6cbc7530481 100644
--- a/code/modules/antagonists/fugitive/fugitive.dm
+++ b/code/modules/antagonists/fugitive/fugitive.dm
@@ -9,6 +9,7 @@
antag_hud_name = "fugitive"
suicide_cry = "FOR FREEDOM!!"
preview_outfit = /datum/outfit/prisoner
+ count_against_dynamic_roll_chance = FALSE
var/datum/team/fugitive/fugitive_team
var/is_captured = FALSE
var/backstory = "error"
diff --git a/code/modules/antagonists/fugitive/hunter.dm b/code/modules/antagonists/fugitive/hunter.dm
index 5e674df4957..59c8c415108 100644
--- a/code/modules/antagonists/fugitive/hunter.dm
+++ b/code/modules/antagonists/fugitive/hunter.dm
@@ -7,6 +7,7 @@
prevent_roundtype_conversion = FALSE
antag_hud_name = "fugitive_hunter"
suicide_cry = "FOR GLORY!!"
+ count_against_dynamic_roll_chance = FALSE
var/datum/team/fugitive_hunters/hunter_team
var/backstory = "error"
diff --git a/code/modules/antagonists/greentext/greentext.dm b/code/modules/antagonists/greentext/greentext.dm
index 97bd5c9e482..cfc87bdccc5 100644
--- a/code/modules/antagonists/greentext/greentext.dm
+++ b/code/modules/antagonists/greentext/greentext.dm
@@ -3,6 +3,7 @@
show_in_antagpanel = FALSE
show_name_in_check_antagonists = TRUE //Not that it will be there for long
suicide_cry = "FOR THE GREENTEXT!!" // This can never actually show up, but not including it is a missed opportunity
+ count_against_dynamic_roll_chance = FALSE
/datum/antagonist/greentext/proc/forge_objectives()
var/datum/objective/succeed_objective = new /datum/objective("Succeed")
diff --git a/code/modules/antagonists/highlander/highlander.dm b/code/modules/antagonists/highlander/highlander.dm
index 27f21456333..92a81d0775f 100644
--- a/code/modules/antagonists/highlander/highlander.dm
+++ b/code/modules/antagonists/highlander/highlander.dm
@@ -5,6 +5,7 @@
show_name_in_check_antagonists = TRUE
can_elimination_hijack = ELIMINATION_ENABLED
suicide_cry = "FOR SCOTLAND!!" // If they manage to lose their no-drop stuff somehow
+ count_against_dynamic_roll_chance = FALSE
/datum/antagonist/highlander/apply_innate_effects(mob/living/mob_override)
var/mob/living/L = owner.current || mob_override
diff --git a/code/modules/antagonists/hypnotized/hypnotized.dm b/code/modules/antagonists/hypnotized/hypnotized.dm
index 41b6559f5fd..2ee17b671aa 100644
--- a/code/modules/antagonists/hypnotized/hypnotized.dm
+++ b/code/modules/antagonists/hypnotized/hypnotized.dm
@@ -8,6 +8,7 @@
show_in_antagpanel = TRUE
antagpanel_category = "Other"
show_name_in_check_antagonists = TRUE
+ count_against_dynamic_roll_chance = FALSE
silent = TRUE //not actually silent, because greet will be called by the trauma anyway.
/// Brain trauma associated with this antag datum
diff --git a/code/modules/antagonists/thief/thief.dm b/code/modules/antagonists/thief/thief.dm
index 529bcab630b..43ee50bae70 100644
--- a/code/modules/antagonists/thief/thief.dm
+++ b/code/modules/antagonists/thief/thief.dm
@@ -9,6 +9,7 @@
preview_outfit = /datum/outfit/thief
antag_hud_name = "thief"
ui_name = "AntagInfoThief"
+ count_against_dynamic_roll_chance = FALSE
///assoc list of strings set up for the flavor of the thief.
var/list/thief_flavor
///if added by an admin, they can choose a thief flavor
diff --git a/code/modules/antagonists/valentines/valentine.dm b/code/modules/antagonists/valentines/valentine.dm
index b760b618fe3..67f824e2077 100644
--- a/code/modules/antagonists/valentines/valentine.dm
+++ b/code/modules/antagonists/valentines/valentine.dm
@@ -5,7 +5,7 @@
prevent_roundtype_conversion = FALSE
suicide_cry = "FOR MY LOVE!!"
var/datum/mind/date
- soft_antag = TRUE
+ count_against_dynamic_roll_chance = FALSE
/datum/antagonist/valentine/proc/forge_objectives()
var/datum/objective/protect/protect_objective = new /datum/objective/protect
diff --git a/code/modules/mob/mob_lists.dm b/code/modules/mob/mob_lists.dm
index aa0ce4028d2..f0a5275bf71 100644
--- a/code/modules/mob/mob_lists.dm
+++ b/code/modules/mob/mob_lists.dm
@@ -126,7 +126,14 @@
/mob/proc/add_to_current_living_antags()
if(!SSticker?.mode)
return
- GLOB.current_living_antags |= src
+
+ if (length(mind.antag_datums) == 0)
+ return
+
+ for (var/datum/antagonist/antagonist in mind.antag_datums)
+ if (antagonist.count_against_dynamic_roll_chance)
+ GLOB.current_living_antags |= src
+ return
///Removes the mob reference from the list of living antag player-mobs.
/mob/proc/remove_from_current_living_antags()
diff --git a/code/modules/unit_tests/dynamic_ruleset_sanity.dm b/code/modules/unit_tests/dynamic_ruleset_sanity.dm
index 8ec500e1a82..983a1026b53 100644
--- a/code/modules/unit_tests/dynamic_ruleset_sanity.dm
+++ b/code/modules/unit_tests/dynamic_ruleset_sanity.dm
@@ -2,9 +2,7 @@
/datum/unit_test/dynamic_roundstart_ruleset_sanity
/datum/unit_test/dynamic_roundstart_ruleset_sanity/Run()
- for (var/_ruleset in subtypesof(/datum/dynamic_ruleset/roundstart))
- var/datum/dynamic_ruleset/roundstart/ruleset = _ruleset
-
+ for (var/datum/dynamic_ruleset/roundstart/ruleset as anything in subtypesof(/datum/dynamic_ruleset/roundstart))
var/has_scaling_cost = initial(ruleset.scaling_cost)
var/is_lone = initial(ruleset.flags) & (LONE_RULESET | HIGH_IMPACT_RULESET)
@@ -13,6 +11,11 @@
else if (!has_scaling_cost && !is_lone)
TEST_FAIL("[ruleset] has no scaling cost, but is also not a lone/highlander ruleset.")
+ for (var/datum/dynamic_ruleset/midround/ruleset as anything in subtypesof(/datum/dynamic_ruleset/midround) - /datum/dynamic_ruleset/midround/from_ghosts)
+ var/midround_ruleset_style = initial(ruleset.midround_ruleset_style)
+ if (midround_ruleset_style != MIDROUND_RULESET_STYLE_HEAVY && midround_ruleset_style != MIDROUND_RULESET_STYLE_LIGHT)
+ TEST_FAIL("[ruleset] has an invalid midround_ruleset_style, it should be MIDROUND_RULESET_STYLE_HEAVY or MIDROUND_RULESET_STYLE_LIGHT")
+
/// Verifies that dynamic rulesets have unique antag_flag.
/datum/unit_test/dynamic_unique_antag_flags
diff --git a/tgstation.dme b/tgstation.dme
index 8e6a600f952..abc9c35c4b6 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1224,6 +1224,7 @@
#include "code\game\gamemodes\dynamic\dynamic.dm"
#include "code\game\gamemodes\dynamic\dynamic_hijacking.dm"
#include "code\game\gamemodes\dynamic\dynamic_logging.dm"
+#include "code\game\gamemodes\dynamic\dynamic_midround_rolling.dm"
#include "code\game\gamemodes\dynamic\dynamic_rulesets.dm"
#include "code\game\gamemodes\dynamic\dynamic_rulesets_latejoin.dm"
#include "code\game\gamemodes\dynamic\dynamic_rulesets_midround.dm"