diff --git a/code/__DEFINES/dynamic.dm b/code/__DEFINES/dynamic.dm
new file mode 100644
index 00000000000..a5b1c15be5f
--- /dev/null
+++ b/code/__DEFINES/dynamic.dm
@@ -0,0 +1,8 @@
+/// This is the only ruleset that should be picked this round, used by admins and should not be on rulesets in code.
+#define ONLY_RULESET 1
+
+/// Only one ruleset with this flag will be picked.
+#define HIGH_IMPACT_RULESET 2
+
+/// This ruleset can only be picked once. Anything that does not have a scaling_cost MUST have this.
+#define LONE_RULESET 4
diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm
index 6ea04b8ed1f..3834e756fed 100644
--- a/code/__HELPERS/mobs.dm
+++ b/code/__HELPERS/mobs.dm
@@ -646,3 +646,7 @@ GLOBAL_LIST_EMPTY(species_list)
return log(temp_diff * change_rate + 1) * BODYTEMP_AUTORECOVERY_DIVISOR
#define ISADVANCEDTOOLUSER(mob) (HAS_TRAIT(mob, TRAIT_ADVANCEDTOOLUSER) && !HAS_TRAIT(mob, TRAIT_MONKEYLIKE))
+
+/// Gets the client of the mob, allowing for mocking of the client.
+/// You only need to use this if you know you're going to be mocking clients somewhere else.
+#define GET_CLIENT(mob) (##mob.client || ##mob.mock_client)
diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm
index fd17823a5d1..7edb324b400 100644
--- a/code/__HELPERS/roundend.dm
+++ b/code/__HELPERS/roundend.dm
@@ -358,7 +358,7 @@
if(istype(SSticker.mode, /datum/game_mode/dynamic))
var/datum/game_mode/dynamic/mode = SSticker.mode
parts += "[FOURSPACES]Threat level: [mode.threat_level]"
- parts += "[FOURSPACES]Threat left: [mode.threat]"
+ parts += "[FOURSPACES]Threat left: [mode.mid_round_budget]"
parts += "[FOURSPACES]Executed rules:"
for(var/datum/dynamic_ruleset/rule in mode.executed_rules)
parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat"
diff --git a/code/datums/mocking/client.dm b/code/datums/mocking/client.dm
new file mode 100644
index 00000000000..913c47e2a31
--- /dev/null
+++ b/code/datums/mocking/client.dm
@@ -0,0 +1,4 @@
+/// This should match the interface of /client wherever necessary.
+/datum/client_interface
+ /// Player preferences datum for the client
+ var/datum/preferences/prefs
diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm
index 1506f46f728..e3bd3457cae 100644
--- a/code/game/gamemodes/dynamic/dynamic.dm
+++ b/code/game/gamemodes/dynamic/dynamic.dm
@@ -1,36 +1,14 @@
-#define ONLY_RULESET 1
-#define HIGHLANDER_RULESET 2
-#define TRAITOR_RULESET 4
-#define MINOR_RULESET 8
-
#define RULESET_STOP_PROCESSING 1
#define FAKE_REPORT_CHANCE 8
#define REPORT_NEG_DIVERGENCE -15
#define REPORT_POS_DIVERGENCE 15
-// -- Injection delays
-GLOBAL_VAR_INIT(dynamic_latejoin_delay_min, (5 MINUTES))
-GLOBAL_VAR_INIT(dynamic_latejoin_delay_max, (25 MINUTES))
-
-GLOBAL_VAR_INIT(dynamic_midround_delay_min, (15 MINUTES))
-GLOBAL_VAR_INIT(dynamic_midround_delay_max, (35 MINUTES))
-
-// Are HIGHLANDER_RULESETs allowed to stack?
+// Are HIGH_IMPACT_RULESETs allowed to stack?
GLOBAL_VAR_INIT(dynamic_no_stacking, TRUE)
-// 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.
-GLOBAL_VAR_INIT(dynamic_curve_centre, 0)
-// A number between 0.5 and 4.
-// Higher value will favour extreme rounds and
-// lower value rounds closer to the average.
-GLOBAL_VAR_INIT(dynamic_curve_width, 1.8)
-// If enabled only picks a single starting rule and executes only autotraitor midround ruleset.
-GLOBAL_VAR_INIT(dynamic_classic_secret, FALSE)
// If enabled does not accept or execute any rulesets.
GLOBAL_VAR_INIT(dynamic_forced_extended, FALSE)
-// How high threat is required for HIGHLANDER_RULESETs stacking.
+// How high threat is required for HIGH_IMPACT_RULESETs stacking.
// This is independent of dynamic_no_stacking.
GLOBAL_VAR_INIT(dynamic_stacking_limit, 90)
// List of forced roundstart rulesets.
@@ -46,13 +24,21 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
announce_span = "danger"
announce_text = "Dynamic mode!" // This needs to be changed maybe
- reroll_friendly = FALSE;
+ reroll_friendly = FALSE
// Threat logging vars
/// The "threat cap", threat shouldn't normally go above this and is used in ruleset calculations
var/threat_level = 0
- /// Set at the beginning of the round. Spent by the mode to "purchase" rules.
- var/threat = 0
+
+ /// Set at the beginning of the round. Spent by the mode to "purchase" rules. Everything else goes in the postround budget.
+ var/round_start_budget = 0
+
+ /// Set at the beginning of the round. Spent by midrounds and latejoins.
+ var/mid_round_budget = 0
+
+ /// The initial round start budget for logging purposes, set once at the beginning of the round.
+ var/initial_round_start_budget = 0
+
/// Running information about the threat. Can store text or datum entries.
var/list/threat_log = list()
/// List of roundstart rules used for selecting the rules.
@@ -70,16 +56,6 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
* 0-6, 7-13, 14-20, 21-27, 28-34, 35-41, 42-48, 49-55, 56-62, 63+
*/
var/pop_per_requirement = 6
- /// The requirement used for checking if a second rule should be selected. Index based on pop_per_requirement.
- var/list/second_rule_req = list(100, 100, 80, 70, 60, 50, 30, 20, 10, 0)
- /// The probability for a second ruleset with index being every ten threat.
- var/list/second_rule_prob = list(0,0,60,80,80,80,100,100,100,100)
- /// The requirement used for checking if a third rule should be selected. Index based on pop_per_requirement.
- var/list/third_rule_req = list(100, 100, 100, 90, 80, 70, 60, 50, 40, 30)
- /// The probability for a third ruleset with index being every ten threat.
- var/list/third_rule_prob = list(0,0,0,0,60,60,80,90,100,100)
- /// The amount of additional rulesets waiting to be picked.
- var/extra_rulesets_amount = 0
/// Number of players who were ready on roundstart.
var/roundstart_pop_ready = 0
/// List of candidates used on roundstart rulesets.
@@ -88,34 +64,86 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
var/list/current_rules = list()
/// List of executed rulesets.
var/list/executed_rules = list()
- /// When world.time is over this number the mode tries to inject a latejoin ruleset.
- var/latejoin_injection_cooldown = 0
- /// When world.time is over this number the mode tries to inject a midround ruleset.
- var/midround_injection_cooldown = 0
/// When TRUE GetInjectionChance returns 100.
var/forced_injection = FALSE
/// Forced ruleset to be executed for the next latejoin.
var/datum/dynamic_ruleset/latejoin/forced_latejoin_rule = null
/// How many percent of the rounds are more peaceful.
var/peaceful_percentage = 50
- /// If a highlander executed.
- var/highlander_executed = FALSE
+ /// If a high impact ruleset was executed. Only one will run at a time in most circumstances.
+ var/high_impact_ruleset_executed = FALSE
/// If a only ruleset has been executed.
var/only_ruleset_executed = FALSE
/// Dynamic configuration, loaded on pre_setup
var/list/configuration = null
+ /// When world.time is over this number the mode tries to inject a latejoin ruleset.
+ var/latejoin_injection_cooldown = 0
+
+ /// The minimum time the recurring latejoin ruleset timer is allowed to be.
+ var/latejoin_delay_min = (5 MINUTES)
+
+ /// 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 minimum time the recurring midround ruleset timer is allowed to be.
+ var/midround_delay_min = (15 MINUTES)
+
+ /// The maximum time the recurring midround ruleset timer is allowed to be.
+ var/midround_delay_max = (35 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
+
+ /// 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.
+ var/threat_curve_centre = 0
+
+ /// A number between 0.5 and 4.
+ /// Higher value will favour extreme rounds and
+ /// lower value rounds closer to the average.
+ var/threat_curve_width = 1.8
+
+ /// 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.
+ var/roundstart_split_curve_centre = 1
+
+ /// A number between 0.5 and 4.
+ /// Equivalent to threat_curve_width, but for the budget split.
+ /// Higher value will favour more variance in splits and
+ /// lower value rounds closer to the average.
+ var/roundstart_split_curve_width = 1.8
+
+ /// A list of recorded "snapshots" of the round, stored in the dynamic.json log
+ var/list/datum/dynamic_snapshot/snapshots
+
/datum/game_mode/dynamic/admin_panel()
var/list/dat = list("
Game Mode PanelGame Mode Panel
")
dat += "Dynamic Mode \[VV\] \[Refresh\]
"
dat += "Threat Level: [threat_level]
"
+ dat += "Budgets (Roundstart/Midrounds): [initial_round_start_budget]/[threat_level - initial_round_start_budget]
"
- dat += "Threat to Spend: [threat] \[Adjust\] \[View Log\]
"
+ dat += "Midround budget to spend: [mid_round_budget] \[Adjust\] \[View Log\]
"
dat += "
"
- dat += "Parameters: centre = [GLOB.dynamic_curve_centre] ; width = [GLOB.dynamic_curve_width].
"
+ dat += "Parameters: centre = [threat_curve_centre] ; width = [threat_curve_width].
"
+ dat += "Split parameters: centre = [roundstart_split_curve_centre] ; width = [roundstart_split_curve_width].
"
dat += "On average, [peaceful_percentage]% of the rounds are more peaceful.
"
dat += "Forced extended: [GLOB.dynamic_forced_extended ? "On" : "Off"]
"
- dat += "Classic secret (only autotraitor): [GLOB.dynamic_classic_secret ? "On" : "Off"]
"
dat += "No stacking (only one round-ender): [GLOB.dynamic_no_stacking ? "On" : "Off"]
"
dat += "Stacking limit: [GLOB.dynamic_stacking_limit] \[Adjust\]"
dat += "
"
@@ -147,8 +175,6 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
GLOB.dynamic_forced_extended = !GLOB.dynamic_forced_extended
else if (href_list["no_stacking"])
GLOB.dynamic_no_stacking = !GLOB.dynamic_no_stacking
- else if (href_list["classic_secret"])
- GLOB.dynamic_classic_secret = !GLOB.dynamic_classic_secret
else if (href_list["adjustthreat"])
var/threatadd = input("Specify how much threat to add (negative to subtract). This can inflate the threat level.", "Adjust Threat", 0) as null|num
if(!threatadd)
@@ -157,7 +183,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
create_threat(threatadd)
threat_log += "[worldtime2text()]: [key_name(usr)] increased threat by [threatadd] threat."
else
- spend_threat(-threatadd)
+ spend_midround_budget(-threatadd)
threat_log += "[worldtime2text()]: [key_name(usr)] decreased threat by [-threatadd] threat."
else if (href_list["injectlate"])
latejoin_injection_cooldown = 0
@@ -192,11 +218,11 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
admin_panel() // Refreshes the window
-// Checks if there are HIGHLANDER_RULESETs and calls the rule's round_result() proc
+// Checks if there are HIGH_IMPACT_RULESETs and calls the rule's round_result() proc
/datum/game_mode/dynamic/set_round_result()
- // If it got to this part, just pick one highlander if it exists
+ // If it got to this part, just pick one high impact ruleset if it exists
for(var/datum/dynamic_ruleset/rule in executed_rules)
- if(rule.flags & HIGHLANDER_RULESET)
+ if(rule.flags & HIGH_IMPACT_RULESET)
return rule.round_result()
return ..()
@@ -270,40 +296,44 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
if(istext(entry))
out += "[entry]
"
- out += "Remaining threat/threat_level: [threat]/[threat_level]"
+ out += "Remaining threat/threat_level: [mid_round_budget]/[threat_level]"
usr << browse(out.Join(), "window=threatlog;size=700x500")
/// Generates the threat level using lorentz distribution and assigns peaceful_percentage.
/datum/game_mode/dynamic/proc/generate_threat()
- var/relative_threat = LORENTZ_DISTRIBUTION(GLOB.dynamic_curve_centre, GLOB.dynamic_curve_width)
- threat_level = round(lorentz_to_threat(relative_threat), 0.1)
+ var/relative_threat = LORENTZ_DISTRIBUTION(threat_curve_centre, threat_curve_width)
+ threat_level = round(lorentz_to_amount(relative_threat), 0.1)
- peaceful_percentage = round(LORENTZ_CUMULATIVE_DISTRIBUTION(relative_threat, GLOB.dynamic_curve_centre, GLOB.dynamic_curve_width), 0.01)*100
+ peaceful_percentage = round(LORENTZ_CUMULATIVE_DISTRIBUTION(relative_threat, threat_curve_centre, threat_curve_width), 0.01)*100
- threat = threat_level
+/// Generates the midround and roundstart budgets
+/datum/game_mode/dynamic/proc/generate_budgets()
+ var/relative_round_start_budget_scale = LORENTZ_DISTRIBUTION(roundstart_split_curve_centre, roundstart_split_curve_width)
+ round_start_budget = round((lorentz_to_amount(relative_round_start_budget_scale) / 100) * threat_level, 0.1)
+ initial_round_start_budget = round_start_budget
+ mid_round_budget = threat_level - round_start_budget
/datum/game_mode/dynamic/can_start()
- message_admins("Dynamic mode parameters for the round:")
- message_admins("Centre is [GLOB.dynamic_curve_centre], Width is [GLOB.dynamic_curve_width], Forced extended is [GLOB.dynamic_forced_extended ? "Enabled" : "Disabled"], No stacking is [GLOB.dynamic_no_stacking ? "Enabled" : "Disabled"].")
- message_admins("Stacking limit is [GLOB.dynamic_stacking_limit], Classic secret is [GLOB.dynamic_classic_secret ? "Enabled" : "Disabled"].")
log_game("DYNAMIC: Dynamic mode parameters for the round:")
- log_game("DYNAMIC: Centre is [GLOB.dynamic_curve_centre], Width is [GLOB.dynamic_curve_width], Forced extended is [GLOB.dynamic_forced_extended ? "Enabled" : "Disabled"], No stacking is [GLOB.dynamic_no_stacking ? "Enabled" : "Disabled"].")
- log_game("DYNAMIC: Stacking limit is [GLOB.dynamic_stacking_limit], Classic secret is [GLOB.dynamic_classic_secret ? "Enabled" : "Disabled"].")
+ log_game("DYNAMIC: Centre is [threat_curve_centre], Width is [threat_curve_width], Forced extended is [GLOB.dynamic_forced_extended ? "Enabled" : "Disabled"], No stacking is [GLOB.dynamic_no_stacking ? "Enabled" : "Disabled"].")
+ log_game("DYNAMIC: Stacking limit is [GLOB.dynamic_stacking_limit].")
if(GLOB.dynamic_forced_threat_level >= 0)
threat_level = round(GLOB.dynamic_forced_threat_level, 0.1)
- threat = threat_level
else
generate_threat()
-
- var/latejoin_injection_cooldown_middle = 0.5*(GLOB.dynamic_latejoin_delay_max + GLOB.dynamic_latejoin_delay_min)
- latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), GLOB.dynamic_latejoin_delay_min, GLOB.dynamic_latejoin_delay_max)) + world.time
-
- var/midround_injection_cooldown_middle = 0.5*(GLOB.dynamic_midround_delay_max + GLOB.dynamic_midround_delay_min)
- midround_injection_cooldown = round(clamp(EXP_DISTRIBUTION(midround_injection_cooldown_middle), GLOB.dynamic_midround_delay_min, GLOB.dynamic_midround_delay_max)) + world.time
- log_game("DYNAMIC: Dynamic Mode initialized with a Threat Level of... [threat_level]!")
+ generate_budgets()
+ set_cooldowns()
+ log_game("DYNAMIC: Dynamic Mode initialized with a Threat Level of... [threat_level]! ([round_start_budget] round start budget)")
return TRUE
+/datum/game_mode/dynamic/proc/set_cooldowns()
+ 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")
@@ -314,7 +344,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
if(!(variable in vars))
stack_trace("Invalid dynamic configuration variable [variable] in game mode variable changes.")
continue
- vars[variable] = configuration["dynamic"][variable]
+ vars[variable] = configuration["Dynamic"][variable]
var/valid_roundstart_ruleset = 0
for (var/rule in subtypesof(/datum/dynamic_ruleset))
@@ -350,6 +380,10 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
else
roundstart()
+ log_game("DYNAMIC: [round_start_budget] round start budget was left, donating it to midrounds.")
+ threat_log += "[worldtime2text()]: [round_start_budget] round start budget was left, donating it to midrounds."
+ mid_round_budget += round_start_budget
+
var/starting_rulesets = ""
for (var/datum/dynamic_ruleset/roundstart/DR in executed_rules)
starting_rulesets += "[DR.name], "
@@ -375,8 +409,14 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
rule.acceptable(roundstart_pop_ready, threat_level) // Assigns some vars in the modes, running it here for consistency
rule.candidates = candidates.Copy()
rule.trim_candidates()
- if (rule.ready(TRUE))
- picking_roundstart_rule(list(rule), forced = TRUE)
+ if (rule.ready(roundstart_pop_ready, TRUE))
+ var/cost = rule.cost
+ var/scaled_times = 0
+ if (!(rule.flags & LONE_RULESET))
+ scaled_times = round(max(round_start_budget - cost, 0) / rule.scaling_cost)
+ cost += rule.scaling_cost * scaled_times
+
+ spend_roundstart_budget(picking_roundstart_rule(rule, scaled_times, forced = TRUE))
/datum/game_mode/dynamic/proc/roundstart()
if (GLOB.dynamic_forced_extended)
@@ -386,104 +426,71 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
for (var/datum/dynamic_ruleset/roundstart/rule in roundstart_rules)
if (!rule.weight)
continue
- if (rule.acceptable(roundstart_pop_ready, threat_level) && threat >= rule.cost) // If we got the population and threat required
+ if (rule.acceptable(roundstart_pop_ready, threat_level) && round_start_budget >= rule.cost) // If we got the population and threat required
rule.candidates = candidates.Copy()
rule.trim_candidates()
- if (rule.ready() && rule.candidates.len > 0)
+ if (rule.ready(roundstart_pop_ready) && rule.candidates.len > 0)
drafted_rules[rule] = rule.weight
- var/indice_pop = min(10,round(roundstart_pop_ready/pop_per_requirement)+1)
- extra_rulesets_amount = 0
- if (GLOB.dynamic_classic_secret)
- extra_rulesets_amount = 0
- else
- var/threat_indice = min(10, max(round(threat_level ? threat_level/10 : 1), 1)) // 0-9 threat = 1, 10-19 threat = 2 ...
- if (threat_level >= second_rule_req[indice_pop] && prob(second_rule_prob[threat_indice]))
- extra_rulesets_amount++
- if (threat_level >= third_rule_req[indice_pop] && prob(third_rule_prob[threat_indice]))
- extra_rulesets_amount++
- log_game("DYNAMIC: Trying to roll [extra_rulesets_amount + 1] roundstart rulesets. Picking from [drafted_rules.len] eligible rulesets.")
+ var/list/rulesets_picked = list()
- if (drafted_rules.len > 0 && picking_roundstart_rule(drafted_rules))
- log_game("DYNAMIC: First ruleset picked successfully. [extra_rulesets_amount] remaining.")
- while(extra_rulesets_amount > 0 && drafted_rules.len > 0) // We had enough threat for one or two more rulesets
- for (var/datum/dynamic_ruleset/roundstart/rule in drafted_rules)
- if (rule.cost > threat)
- drafted_rules -= rule
- if(drafted_rules.len)
- picking_roundstart_rule(drafted_rules)
- extra_rulesets_amount--
- log_game("DYNAMIC: Additional ruleset picked successfully, now [executed_rules.len] picked. [extra_rulesets_amount] remaining.")
- else
- if(threat >= 10)
- message_admins("DYNAMIC: Picking first roundstart ruleset failed. You should report this.")
- log_game("DYNAMIC: Picking first roundstart ruleset failed. drafted_rules.len = [drafted_rules.len] and threat = [threat]/[threat_level]")
- return FALSE
- return TRUE
+ // Kept in case a ruleset can't be initialized for whatever reason, we want to be able to only spend what we can use.
+ var/round_start_budget_left = round_start_budget
-/// Picks a random roundstart rule from the list given as an argument and executes it.
-/datum/game_mode/dynamic/proc/picking_roundstart_rule(list/drafted_rules = list(), forced = FALSE)
- var/datum/dynamic_ruleset/roundstart/starting_rule = pickweight(drafted_rules)
- if(!starting_rule)
- log_game("DYNAMIC: Couldn't pick a starting ruleset. No rulesets available")
- return FALSE
+ while (round_start_budget_left > 0)
+ var/datum/dynamic_ruleset/roundstart/ruleset = pickweightAllowZero(drafted_rules)
+ if (isnull(ruleset))
+ log_game("DYNAMIC: No more rules can be applied, stopping with [round_start_budget] left.")
+ break
- if(!forced)
- if(only_ruleset_executed)
- log_game("DYNAMIC: Picking [starting_rule.name] failed due to only_ruleset_executed.")
- return FALSE
- // Check if a blocking ruleset has been executed.
- else if(check_blocking(starting_rule.blocking_rules, executed_rules)) // Should already be filtered out, but making sure. Check filtering at end of proc if reported.
- drafted_rules -= starting_rule
- if(drafted_rules.len <= 0)
- log_game("DYNAMIC: Picking [starting_rule.name] failed due to blocking_rules and no more rulesets available. Report this.")
- return FALSE
- starting_rule = pickweight(drafted_rules)
- // Check if the ruleset is highlander and if a highlander ruleset has been executed
- else if(starting_rule.flags & HIGHLANDER_RULESET) // Should already be filtered out, but making sure. Check filtering at end of proc if reported.
- if(threat_level > GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking)
- if(highlander_executed)
- drafted_rules -= starting_rule
- if(drafted_rules.len <= 0)
- log_game("DYNAMIC: Picking [starting_rule.name] failed due to no highlander stacking and no more rulesets available. Report this.")
- return FALSE
- starting_rule = pickweight(drafted_rules)
- // With low pop and high threat there might be rulesets that get executed with no valid candidates.
- else if(!starting_rule.ready()) // Should already be filtered out, but making sure. Check filtering at end of proc if reported.
- drafted_rules -= starting_rule
- if(drafted_rules.len <= 0)
- log_game("DYNAMIC: Picking [starting_rule.name] failed because there were not enough candidates and no more rulesets available. Report this.")
- return FALSE
- starting_rule = pickweight(drafted_rules)
+ var/cost = (ruleset in rulesets_picked) ? ruleset.scaling_cost : ruleset.cost
+ if (cost == 0)
+ stack_trace("[ruleset] cost 0, this is going to result in an infinite loop.")
+ drafted_rules[ruleset] = null
+ continue
- log_game("DYNAMIC: Picked a ruleset: [starting_rule.name]")
+ if (cost > round_start_budget_left)
+ drafted_rules[ruleset] = null
+ continue
- roundstart_rules -= starting_rule
- drafted_rules -= starting_rule
+ if (check_blocking(ruleset.blocking_rules, rulesets_picked))
+ drafted_rules[ruleset] = null
+ continue
- starting_rule.trim_candidates()
+ round_start_budget_left -= cost
- var/added_threat = starting_rule.scale_up(extra_rulesets_amount, threat)
- if(starting_rule.pre_execute())
- spend_threat(starting_rule.cost + added_threat)
- threat_log += "[worldtime2text()]: Roundstart [starting_rule.name] spent [starting_rule.cost + added_threat]. [starting_rule.scaling_cost ? "Scaled up[starting_rule.scaled_times]/3 times." : ""]"
- if(starting_rule.flags & HIGHLANDER_RULESET)
- highlander_executed = TRUE
- else if(starting_rule.flags & ONLY_RULESET)
+ rulesets_picked[ruleset] += 1
+
+ if (ruleset.flags & HIGH_IMPACT_RULESET)
+ for (var/_other_ruleset in drafted_rules)
+ var/datum/dynamic_ruleset/other_ruleset = _other_ruleset
+ if (other_ruleset.flags & HIGH_IMPACT_RULESET)
+ drafted_rules[other_ruleset] = null
+
+ if (ruleset.flags & LONE_RULESET)
+ drafted_rules[ruleset] = null
+
+ for (var/ruleset in rulesets_picked)
+ spend_roundstart_budget(picking_roundstart_rule(ruleset, rulesets_picked[ruleset] - 1))
+
+/// Initializes the round start ruleset provided to it. Returns how much threat to spend.
+/datum/game_mode/dynamic/proc/picking_roundstart_rule(datum/dynamic_ruleset/roundstart/ruleset, scaled_times = 0, forced = FALSE)
+ log_game("DYNAMIC: Picked a ruleset: [ruleset.name], scaled [scaled_times] times")
+
+ ruleset.trim_candidates()
+ var/added_threat = ruleset.scale_up(roundstart_pop_ready, scaled_times)
+
+ if(ruleset.pre_execute(roundstart_pop_ready))
+ threat_log += "[worldtime2text()]: Roundstart [ruleset.name] spent [ruleset.cost + added_threat]. [ruleset.scaling_cost ? "Scaled up [ruleset.scaled_times]/[scaled_times] times." : ""]"
+ if(ruleset.flags & ONLY_RULESET)
only_ruleset_executed = TRUE
- executed_rules += starting_rule
- for(var/datum/dynamic_ruleset/roundstart/rule in drafted_rules)
- if(check_blocking(rule.blocking_rules, executed_rules))
- drafted_rules -= rule
- if(highlander_executed && rule.flags & HIGHLANDER_RULESET)
- drafted_rules -= rule
- if(!rule.ready())
- drafted_rules -= rule // And removing rules that are no longer eligible
-
- return TRUE
+ if(ruleset.flags & HIGH_IMPACT_RULESET)
+ high_impact_ruleset_executed = TRUE
+ executed_rules += ruleset
+ return ruleset.cost + added_threat
else
- stack_trace("The starting rule \"[starting_rule.name]\" failed to pre_execute.")
- return FALSE
+ stack_trace("The starting rule \"[ruleset.name]\" failed to pre_execute.")
+ return 0
/// Mainly here to facilitate delayed rulesets. All roundstart rulesets are executed with a timered callback to this proc.
/datum/game_mode/dynamic/proc/execute_roundstart_rule(sent_rule)
@@ -491,6 +498,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
if(rule.execute())
if(rule.persistent)
current_rules += rule
+ new_snapshot(rule)
return TRUE
rule.clean_up() // Refund threat, delete teams and so on.
executed_rules -= rule
@@ -513,10 +521,10 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
if(drafted_rules.len <= 0)
return FALSE
rule = pickweight(drafted_rules)
- // Check if the ruleset is highlander and if a highlander ruleset has been executed
- else if(rule.flags & HIGHLANDER_RULESET)
+ // Check if the ruleset is high impact and if a high impact ruleset has been executed
+ else if(rule.flags & HIGH_IMPACT_RULESET)
if(threat_level > GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking)
- if(highlander_executed)
+ if(high_impact_ruleset_executed)
drafted_rules -= rule
if(drafted_rules.len <= 0)
return FALSE
@@ -551,21 +559,22 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
// Check if a blocking ruleset has been executed.
else if(check_blocking(new_rule.blocking_rules, executed_rules))
return FALSE
- // Check if the ruleset is highlander and if a highlander ruleset has been executed
- else if(new_rule.flags & HIGHLANDER_RULESET)
+ // Check if the ruleset is high impact and if a high impact ruleset has been executed
+ else if(new_rule.flags & HIGH_IMPACT_RULESET)
if(threat_level > GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking)
- if(highlander_executed)
+ if(high_impact_ruleset_executed)
return FALSE
- if((new_rule.acceptable(current_players[CURRENT_LIVING_PLAYERS].len, threat_level) && new_rule.cost <= threat) || forced)
+ var/population = current_players[CURRENT_LIVING_PLAYERS].len
+ if((new_rule.acceptable(population, threat_level) && new_rule.cost <= mid_round_budget) || forced)
new_rule.trim_candidates()
if (new_rule.ready(forced))
- spend_threat(new_rule.cost)
+ spend_midround_budget(new_rule.cost)
threat_log += "[worldtime2text()]: Forced rule [new_rule.name] spent [new_rule.cost]"
- new_rule.pre_execute()
+ new_rule.pre_execute(population)
if (new_rule.execute()) // This should never fail since ready() returned 1
- if(new_rule.flags & HIGHLANDER_RULESET)
- highlander_executed = TRUE
+ if(new_rule.flags & HIGH_IMPACT_RULESET)
+ high_impact_ruleset_executed = TRUE
else if(new_rule.flags & ONLY_RULESET)
only_ruleset_executed = TRUE
log_game("DYNAMIC: Making a call to a specific ruleset...[new_rule.name]!")
@@ -580,13 +589,13 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// Mainly here to facilitate delayed rulesets. All midround/latejoin rulesets are executed with a timered callback to this proc.
/datum/game_mode/dynamic/proc/execute_midround_latejoin_rule(sent_rule)
var/datum/dynamic_ruleset/rule = sent_rule
- spend_threat(rule.cost)
+ spend_midround_budget(rule.cost)
threat_log += "[worldtime2text()]: [rule.ruletype] [rule.name] spent [rule.cost]"
- rule.pre_execute()
+ rule.pre_execute(current_players[CURRENT_LIVING_PLAYERS].len)
if (rule.execute())
log_game("DYNAMIC: Injected a [rule.ruletype == "latejoin" ? "latejoin" : "midround"] ruleset [rule.name].")
- if(rule.flags & HIGHLANDER_RULESET)
- highlander_executed = TRUE
+ if(rule.flags & HIGH_IMPACT_RULESET)
+ high_impact_ruleset_executed = TRUE
else if(rule.flags & ONLY_RULESET)
only_ruleset_executed = TRUE
if(rule.ruletype == "Latejoin")
@@ -597,6 +606,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
rule.candidates.Cut()
if (rule.persistent)
current_rules += rule
+ new_snapshot(rule)
return TRUE
rule.clean_up()
stack_trace("The [rule.ruletype] rule \"[rule.name]\" failed to execute.")
@@ -613,8 +623,8 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
// 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*(GLOB.dynamic_midround_delay_max + GLOB.dynamic_midround_delay_min)
- midround_injection_cooldown = (round(clamp(EXP_DISTRIBUTION(midround_injection_cooldown_middle), GLOB.dynamic_midround_delay_min, GLOB.dynamic_midround_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)
// Time to inject some threat into the round
if(EMERGENCY_ESCAPED_OR_ENDGAMED) // Unless the shuttle is gone
@@ -628,10 +638,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
for (var/datum/dynamic_ruleset/midround/rule in midround_rules)
if (!rule.weight)
continue
- if (rule.acceptable(current_players[CURRENT_LIVING_PLAYERS].len, threat_level) && threat >= rule.cost)
- // Classic secret : only autotraitor/minor roles
- if (GLOB.dynamic_classic_secret && !((rule.flags & TRAITOR_RULESET) || (rule.flags & MINOR_RULESET)))
- continue
+ if (rule.acceptable(current_players[CURRENT_LIVING_PLAYERS].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
@@ -658,10 +665,10 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
chance += 25-10*(max_pop_per_antag-current_pop_per_antag)
if (current_players[CURRENT_DEAD_PLAYERS].len > current_players[CURRENT_LIVING_PLAYERS].len)
chance -= 30 // More than half the crew died? ew, let's calm down on antags
- if (threat > 70)
- chance += 15
- if (threat < 30)
- chance -= 15
+ 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))
/// Removes type from the list
@@ -675,7 +682,8 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/datum/game_mode/dynamic/proc/check_blocking(list/blocking_list, list/rule_list)
if(blocking_list.len > 0)
for(var/blocking in blocking_list)
- for(var/datum/executed in rule_list)
+ for(var/_executed in rule_list)
+ var/datum/executed = _executed
if(blocking == executed.type)
return TRUE
return FALSE
@@ -708,13 +716,10 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
for (var/datum/dynamic_ruleset/latejoin/rule in latejoin_rules)
if (!rule.weight)
continue
- if (rule.acceptable(current_players[CURRENT_LIVING_PLAYERS].len, threat_level) && threat >= rule.cost)
- // Classic secret : only autotraitor/minor roles
- if (GLOB.dynamic_classic_secret && !((rule.flags & TRAITOR_RULESET) || (rule.flags & MINOR_RULESET)))
- continue
+ if (rule.acceptable(current_players[CURRENT_LIVING_PLAYERS].len, threat_level) && mid_round_budget >= rule.cost)
// No stacking : only one round-ender, unless threat level > stacking_limit.
if (threat_level > GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking)
- if(rule.flags & HIGHLANDER_RULESET && highlander_executed)
+ if(rule.flags & HIGH_IMPACT_RULESET && high_impact_ruleset_executed)
continue
rule.candidates = list(newPlayer)
@@ -723,8 +728,8 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
drafted_rules[rule] = rule.get_weight()
if (drafted_rules.len > 0 && picking_midround_latejoin_rule(drafted_rules))
- var/latejoin_injection_cooldown_middle = 0.5*(GLOB.dynamic_latejoin_delay_max + GLOB.dynamic_latejoin_delay_min)
- latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), GLOB.dynamic_latejoin_delay_min, GLOB.dynamic_latejoin_delay_max)) + world.time
+ 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
/// Apply configurations to rule.
/datum/game_mode/dynamic/proc/configure_ruleset(datum/dynamic_ruleset/ruleset)
@@ -741,20 +746,25 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// Refund threat, but no more than threat_level.
/datum/game_mode/dynamic/proc/refund_threat(regain)
- threat = min(threat_level,threat+regain)
+ mid_round_budget = min(threat_level, mid_round_budget + regain)
/// Generate threat and increase the threat_level if it goes beyond, capped at 100
/datum/game_mode/dynamic/proc/create_threat(gain)
- threat = min(100, threat+gain)
- if(threat > threat_level)
- threat_level = threat
+ mid_round_budget = min(100, mid_round_budget + gain)
+ if(mid_round_budget > threat_level)
+ threat_level = mid_round_budget
-/// Expend threat, can't fall under 0.
-/datum/game_mode/dynamic/proc/spend_threat(cost)
- threat = max(threat-cost,0)
+/// Expend round start threat, can't fall under 0.
+/datum/game_mode/dynamic/proc/spend_roundstart_budget(cost)
+ round_start_budget = max(round_start_budget - cost,0)
-/// Turns the value generated by lorentz distribution to threat value between 0 and 100.
-/datum/game_mode/dynamic/proc/lorentz_to_threat(x)
+/// Expend midround threat, can't fall under 0.
+/datum/game_mode/dynamic/proc/spend_midround_budget(cost)
+ mid_round_budget = max(mid_round_budget - cost,0)
+
+/// Turns the value generated by lorentz distribution to number between 0 and 100.
+/// Used for threat level and splitting the budgets.
+/datum/game_mode/dynamic/proc/lorentz_to_amount(x)
switch (x)
if (-INFINITY to -20)
return rand(0, 10)
diff --git a/code/game/gamemodes/dynamic/dynamic_logging.dm b/code/game/gamemodes/dynamic/dynamic_logging.dm
new file mode 100644
index 00000000000..08eb1330550
--- /dev/null
+++ b/code/game/gamemodes/dynamic/dynamic_logging.dm
@@ -0,0 +1,97 @@
+/// A "snapshot" of dynamic at an important point in time.
+/// Exported to JSON in the dynamic.json log file.
+/datum/dynamic_snapshot
+ /// The remaining midround threat
+ var/remaining_threat
+
+ /// The world.time when the snapshot was taken
+ var/time
+
+ /// The total number of players in the server
+ var/total_players
+
+ /// The number of alive players
+ var/alive_players
+
+ /// The number of dead players
+ var/dead_players
+
+ /// The number of observers
+ var/observers
+
+ /// The number of alive antags
+ var/alive_antags
+
+ /// The rulesets chosen this snapshot
+ var/datum/dynamic_snapshot_ruleset/ruleset_chosen
+
+ /// The cached serialization of this snapshot
+ var/serialization
+
+/// A ruleset chosen during a snapshot
+/datum/dynamic_snapshot_ruleset
+ /// The name of the ruleset chosen
+ var/name
+
+ /// If it is a round start ruleset, how much it was scaled by
+ var/scaled
+
+ /// The number of assigned antags
+ var/assigned
+
+/datum/dynamic_snapshot_ruleset/New(datum/dynamic_ruleset/ruleset)
+ name = ruleset.name
+ assigned = ruleset.assigned.len
+
+ if (istype(ruleset, /datum/dynamic_ruleset/roundstart))
+ scaled = ruleset.scaled_times
+
+/// Convert the snapshot to an associative list
+/datum/dynamic_snapshot/proc/to_list()
+ if (!isnull(serialization))
+ return serialization
+
+ serialization = list(
+ "remaining_threat" = remaining_threat,
+ "time" = time,
+ "total_players" = total_players,
+ "alive_players" = alive_players,
+ "dead_players" = dead_players,
+ "observers" = observers,
+ "alive_antags" = alive_antags,
+ "ruleset_chosen" = list(
+ "name" = ruleset_chosen.name,
+ "scaled" = ruleset_chosen.scaled,
+ "assigned" = ruleset_chosen.assigned,
+ ),
+ )
+
+ return serialization
+
+/// Creates a new snapshot with the given rulesets chosen, and writes to the JSON output.
+/datum/game_mode/dynamic/proc/new_snapshot(datum/dynamic_ruleset/ruleset_chosen)
+ var/datum/dynamic_snapshot/new_snapshot = new
+
+ new_snapshot.remaining_threat = mid_round_budget
+ new_snapshot.time = world.time
+ new_snapshot.alive_players = current_players[CURRENT_LIVING_PLAYERS].len
+ new_snapshot.dead_players = current_players[CURRENT_DEAD_PLAYERS].len
+ new_snapshot.observers = current_players[CURRENT_OBSERVERS].len
+ new_snapshot.total_players = new_snapshot.alive_players + new_snapshot.dead_players + new_snapshot.observers
+ new_snapshot.alive_antags = current_players[CURRENT_LIVING_ANTAGS].len
+ new_snapshot.ruleset_chosen = new /datum/dynamic_snapshot_ruleset(ruleset_chosen)
+
+ LAZYADD(snapshots, new_snapshot)
+
+ var/list/serialized = list()
+ serialized["threat_level"] = threat_level
+ serialized["round_start_budget"] = initial_round_start_budget
+ serialized["mid_round_budget"] = threat_level - initial_round_start_budget
+
+ var/list/serialized_snapshots = list()
+ for (var/_snapshot in snapshots)
+ var/datum/dynamic_snapshot/snapshot = _snapshot
+ serialized_snapshots += list(snapshot.to_list())
+ serialized["snapshots"] = serialized_snapshots
+
+ rustg_file_write(json_encode(serialized), "[GLOB.log_directory]/dynamic.json")
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm
index 665ee273a42..17da0a2b3fa 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm
@@ -1,6 +1,3 @@
-#define EXTRA_RULESET_PENALTY 20 // Changes how likely a gamemode is to scale based on how many other roundstart rulesets are waiting to be rolled.
-#define POP_SCALING_PENALTY 50 // Discourages scaling up rulesets if ratio of antags to crew is high.
-
#define REVOLUTION_VICTORY 1
#define STATION_VICTORY 2
@@ -47,9 +44,7 @@
var/scaled_times = 0
/// Used for the roundend report
var/total_cost = 0
- /// A flag that determines how the ruleset is handled
- /// HIGHLANDER_RULESET are rulesets can end the round.
- /// TRAITOR_RULESET and MINOR_RULESET can't end the round and have no difference right now.
+ /// A flag that determines how the ruleset is handled. Check __DEFINES/dynamic.dm for an explanation of the accepted values.
var/flags = NONE
/// Pop range per requirement. If zero defaults to mode's pop_per_requirement.
var/pop_per_requirement = 0
@@ -69,14 +64,18 @@
var/maximum_players = 0
/// Calculated during acceptable(), used in scaling and team sizes.
var/indice_pop = 0
- /// Population scaling. Used by team antags and scaling for solo antags.
- var/list/antag_cap = list()
/// Base probability used in scaling. The higher it is, the more likely to scale. Kept as a var to allow for config editing._SendSignal(sigtype, list/arguments)
var/base_prob = 60
/// Delay for when execute will get called from the time of post_setup (roundstart) or process (midround/latejoin).
/// Make sure your ruleset works with execute being called during the game when using this, and that the clean_up proc reverts it properly in case of faliure.
var/delay = 0
+ /// Judges the amount of antagonists to apply, for both solo and teams.
+ /// Note that some antagonists (such as traitors, lings, heretics, etc) will add more based on how many times they've been scaled.
+ /// Written as a linear equation--ceil(x/denominator) + offset, or as a fixed constant.
+ /// If written as a linear equation, will be in the form of `list("denominator" = denominator, "offset" = offset).
+ var/antag_cap = 0
+
/datum/dynamic_ruleset/New()
..()
@@ -96,9 +95,6 @@
/// If your rule has extra checks, such as counting security officers, do that in ready() instead
/datum/dynamic_ruleset/proc/acceptable(population = 0, threat_level = 0)
pop_per_requirement = pop_per_requirement > 0 ? pop_per_requirement : mode.pop_per_requirement
- if(antag_cap.len && requirements.len != antag_cap.len)
- message_admins("DYNAMIC: requirements and antag_cap lists have different lengths in ruleset [name]. Likely config issue, report this.")
- log_game("DYNAMIC: requirements and antag_cap lists have different lengths in ruleset [name]. Likely config issue, report this.")
indice_pop = min(requirements.len,round(population/pop_per_requirement)+1)
if(minimum_players > population)
@@ -107,28 +103,32 @@
return FALSE
return (threat_level >= requirements[indice_pop])
-/// Called when a suitable rule is picked during roundstart(). Will some times attempt to scale a rule up when there is threat remaining. Returns the additional threat from scaling up.
-/datum/dynamic_ruleset/proc/scale_up(extra_rulesets = 0, remaining_threat_level = 0)
- remaining_threat_level -= cost
- if(scaling_cost && scaling_cost <= remaining_threat_level) // Only attempts to scale the modes with a scaling cost explicitly set.
- var/antag_fraction = 0
- var/new_prob = 0
- for(var/R in (mode.executed_rules + list(src))) // we care about the antags we *will* assign, too
- var/datum/dynamic_ruleset/ruleset = R
- antag_fraction += ((1 + ruleset.scaled_times) * ruleset.antag_cap[indice_pop]) / mode.roundstart_pop_ready
+/// When picking rulesets, if dynamic picks the same one multiple times, it will "scale up".
+/// However, doing this blindly would result in lowpop rounds (think under 10 people) where over 80% of the crew is antags!
+/// This function is here to ensure the antag ratio is kept under control while scaling up.
+/// Returns how much threat to actually spend in the end.
+/datum/dynamic_ruleset/proc/scale_up(population, max_scale)
+ if (!scaling_cost)
+ return 0
- log_game("DYNAMIC: [name] roundstart ruleset attempting to scale up with [extra_rulesets] rulesets waiting and [remaining_threat_level] threat remaining.")
- for(var/i in 1 to 3) //Can scale a max of 3 times
- if(remaining_threat_level >= scaling_cost && antag_fraction < 0.25)
- new_prob = base_prob + (remaining_threat_level) - (scaled_times * scaling_cost) - (extra_rulesets * EXTRA_RULESET_PENALTY) - (antag_fraction * POP_SCALING_PENALTY)
- if (!prob(new_prob))
- break
- remaining_threat_level -= scaling_cost
- scaled_times++
- antag_fraction += antag_cap[indice_pop] / mode.roundstart_pop_ready // we added new antags, gotta update the %
+ var/antag_fraction = 0
+ for(var/_ruleset in (mode.executed_rules + list(src))) // we care about the antags we *will* assign, too
+ var/datum/dynamic_ruleset/ruleset = _ruleset
+ antag_fraction += ((1 + ruleset.scaled_times) * ruleset.get_antag_cap(population)) / mode.roundstart_pop_ready
- log_game("DYNAMIC: [name] roundstart ruleset failed scaling up at [new_prob]% chance after [scaled_times]/3 successful scaleups. [remaining_threat_level] threat remaining, % of players that are antags: [antag_fraction*100]%.")
- return scaled_times * scaling_cost
+ for(var/i in 1 to max_scale)
+ if(antag_fraction < 0.25)
+ scaled_times += 1
+ antag_fraction += get_antag_cap(population) / mode.roundstart_pop_ready // we added new antags, gotta update the %
+
+ return scaled_times * scaling_cost
+
+/// Returns what the antag cap with the given population is.
+/datum/dynamic_ruleset/proc/get_antag_cap(population)
+ if (isnum(antag_cap))
+ return antag_cap
+
+ return CEILING(population / antag_cap["denominator"], 1) + (antag_cap["offset"] || 0)
/// This is called if persistent variable is true everytime SSTicker ticks.
/datum/dynamic_ruleset/proc/rule_process()
@@ -179,7 +179,7 @@
return
/// Set mode result and news report here.
-/// Only called if ruleset is flagged as HIGHLANDER_RULESET
+/// Only called if ruleset is flagged as HIGH_IMPACT_RULESET
/datum/dynamic_ruleset/proc/round_result()
//////////////////////////////////////////////
@@ -191,20 +191,21 @@
/// Checks if candidates are connected and if they are banned or don't want to be the antagonist.
/datum/dynamic_ruleset/roundstart/trim_candidates()
for(var/mob/dead/new_player/P in candidates)
- if (!P.client || !P.mind) // Are they connected?
+ var/client/client = GET_CLIENT(P)
+ if (!client || !P.mind) // Are they connected?
candidates.Remove(P)
- else if(!mode.check_age(P.client, minimum_required_age))
+ else if(!mode.check_age(client, minimum_required_age))
candidates.Remove(P)
else if(P.mind.special_role) // We really don't want to give antag to an antag.
candidates.Remove(P)
else if(antag_flag_override)
- if(!(antag_flag_override in P.client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag_override, ROLE_SYNDICATE)))
+ if(!(antag_flag_override in client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag_override, ROLE_SYNDICATE)))
candidates.Remove(P)
else
- if(!(antag_flag in P.client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag, ROLE_SYNDICATE)))
+ if(!(antag_flag in client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag, ROLE_SYNDICATE)))
candidates.Remove(P)
/// Do your checks if the ruleset is ready to be executed here.
/// Should ignore certain checks if forced is TRUE
-/datum/dynamic_ruleset/roundstart/ready(forced = FALSE)
+/datum/dynamic_ruleset/roundstart/ready(population, forced = FALSE)
return ..()
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
index 3c933aa275a..c425c722043 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
@@ -60,7 +60,6 @@
cost = 5
requirements = list(40,30,20,10,10,10,10,10,10,10)
repeatable = TRUE
- flags = TRAITOR_RULESET
//////////////////////////////////////////////
// //
@@ -82,7 +81,7 @@
delay = 1 MINUTES // Prevents rule start while head is offstation.
cost = 20
requirements = list(101,101,70,40,30,20,20,20,20,20)
- flags = HIGHLANDER_RULESET
+ flags = HIGH_IMPACT_RULESET
blocking_rules = list(/datum/dynamic_ruleset/roundstart/revs)
var/required_heads_of_staff = 3
var/finished = FALSE
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index 80bfe6483ec..242c9c146e4 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -171,17 +171,24 @@
cost = 10
requirements = list(50,40,30,20,10,10,10,10,10,10)
repeatable = TRUE
- flags = TRAITOR_RULESET
/datum/dynamic_ruleset/midround/autotraitor/acceptable(population = 0, threat = 0)
var/player_count = mode.current_players[CURRENT_LIVING_PLAYERS].len
var/antag_count = mode.current_players[CURRENT_LIVING_ANTAGS].len
var/max_traitors = round(player_count / 10) + 1
- if ((antag_count < max_traitors) && prob(mode.threat_level))//adding traitors if the antag population is getting low
- return ..()
- else
+
+ // 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 (!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)
@@ -221,10 +228,10 @@
weight = 1
cost = 25
requirements = list(101,101,101,101,101,80,50,30,10,10)
- flags = HIGHLANDER_RULESET
+ flags = HIGH_IMPACT_RULESET
blocking_rules = list(/datum/dynamic_ruleset/roundstart/families)
minimum_players = 36
- antag_cap = list(6,6,6,6,6,6,6,6,6,6)
+ antag_cap = 6
/// A reference to the handler that is used to run pre_execute(), execute(), etc..
var/datum/gang_handler/handler
@@ -371,7 +378,7 @@
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
- flags = HIGHLANDER_RULESET
+ flags = HIGH_IMPACT_RULESET
/datum/dynamic_ruleset/midround/from_ghosts/nuclear/acceptable(population=0, threat=0)
if (locate(/datum/dynamic_ruleset/roundstart/nuclear) in mode.executed_rules)
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
index 357ecc26077..bd2c77a7865 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
@@ -9,21 +9,23 @@
name = "Traitors"
persistent = TRUE
antag_flag = ROLE_TRAITOR
- antag_datum = /datum/antagonist/traitor/
+ antag_datum = /datum/antagonist/traitor
minimum_required_age = 0
protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain")
restricted_roles = list("Cyborg")
required_candidates = 1
weight = 5
- cost = 10 // Avoid raising traitor threat above 10, as it is the default low cost ruleset.
- scaling_cost = 10
+ cost = 8 // Avoid raising traitor threat above 10, as it is the default low cost ruleset.
+ scaling_cost = 9
requirements = list(10,10,10,10,10,10,10,10,10,10)
- antag_cap = list(1,1,1,1,2,2,2,2,3,3)
- var/autotraitor_cooldown = 450 // 15 minutes (ticks once per 2 sec)
+ antag_cap = list("denominator" = 24)
+ var/autotraitor_cooldown = (15 MINUTES)
+ COOLDOWN_DECLARE(autotraitor_cooldown_check)
-/datum/dynamic_ruleset/roundstart/traitor/pre_execute()
+/datum/dynamic_ruleset/roundstart/traitor/pre_execute(population)
. = ..()
- var/num_traitors = antag_cap[indice_pop] * (scaled_times + 1)
+ 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)
var/mob/M = pick_n_take(candidates)
assigned += M.mind
@@ -33,11 +35,8 @@
return TRUE
/datum/dynamic_ruleset/roundstart/traitor/rule_process()
- if (autotraitor_cooldown > 0)
- autotraitor_cooldown--
- else
- autotraitor_cooldown = 450 // 15 minutes
- message_admins("Checking if we can turn someone into a traitor.")
+ 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)
@@ -58,13 +57,13 @@
cost = 15
scaling_cost = 15
requirements = list(40,30,30,20,20,15,15,15,10,10)
- antag_cap = list(2,2,2,2,2,2,2,2,2,2) // Can pick 3 per team, but rare enough it doesn't matter.
+ antag_cap = 2 // Can pick 3 per team, but rare enough it doesn't matter.
var/list/datum/team/brother_team/pre_brother_teams = list()
var/const/min_team_size = 2
-/datum/dynamic_ruleset/roundstart/traitorbro/pre_execute()
+/datum/dynamic_ruleset/roundstart/traitorbro/pre_execute(population)
. = ..()
- var/num_teams = (antag_cap[indice_pop]/min_team_size) * (scaled_times + 1) // 1 team per scaling
+ var/num_teams = (get_antag_cap(population)/min_team_size) * (scaled_times + 1) // 1 team per scaling
for(var/j = 1 to num_teams)
if(candidates.len < min_team_size || candidates.len < required_candidates)
break
@@ -105,14 +104,14 @@
restricted_roles = list("AI", "Cyborg")
required_candidates = 1
weight = 3
- cost = 15
- scaling_cost = 15
+ cost = 16
+ scaling_cost = 10
requirements = list(70,70,60,50,40,20,20,10,10,10)
- antag_cap = list(1,1,1,1,1,2,2,2,2,3)
+ antag_cap = list("denominator" = 29)
-/datum/dynamic_ruleset/roundstart/changeling/pre_execute()
+/datum/dynamic_ruleset/roundstart/changeling/pre_execute(population)
. = ..()
- var/num_changelings = antag_cap[indice_pop] * (scaled_times + 1)
+ var/num_changelings = get_antag_cap(population) * (scaled_times + 1)
for (var/i = 1 to num_changelings)
var/mob/M = pick_n_take(candidates)
assigned += M.mind
@@ -142,15 +141,15 @@
restricted_roles = list("AI", "Cyborg")
required_candidates = 1
weight = 3
- cost = 20
- scaling_cost = 15
+ cost = 15
+ scaling_cost = 9
requirements = list(50,45,45,40,35,20,20,15,10,10)
- antag_cap = list(1,1,1,1,2,2,2,2,3,3)
+ antag_cap = list("denominator" = 24)
-/datum/dynamic_ruleset/roundstart/heretics/pre_execute()
+/datum/dynamic_ruleset/roundstart/heretics/pre_execute(population)
. = ..()
- var/num_ecult = antag_cap[indice_pop] * (scaled_times + 1)
+ var/num_ecult = get_antag_cap(population) * (scaled_times + 1)
for (var/i = 1 to num_ecult)
var/mob/picked_candidate = pick_n_take(candidates)
@@ -182,11 +181,12 @@
name = "Wizard"
antag_flag = ROLE_WIZARD
antag_datum = /datum/antagonist/wizard
+ flags = LONE_RULESET
minimum_required_age = 14
restricted_roles = list("Head of Security", "Captain") // Just to be sure that a wizard getting picked won't ever imply a Captain or HoS not getting drafted
required_candidates = 1
weight = 2
- cost = 30
+ cost = 20
requirements = list(90,90,70,40,30,20,10,10,10,10)
var/list/roundstart_wizards = list()
@@ -229,19 +229,19 @@
restricted_roles = list("AI", "Cyborg", "Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Chaplain", "Head of Personnel")
required_candidates = 2
weight = 3
- cost = 35
+ cost = 20
requirements = list(100,90,80,60,40,30,10,10,10,10)
- flags = HIGHLANDER_RULESET
- antag_cap = list(2,2,2,3,3,4,4,4,4,4)
+ flags = HIGH_IMPACT_RULESET
+ antag_cap = list("denominator" = 20, "offset" = 1)
var/datum/team/cult/main_cult
-/datum/dynamic_ruleset/roundstart/bloodcult/ready(forced = FALSE)
- required_candidates = antag_cap[indice_pop]
+/datum/dynamic_ruleset/roundstart/bloodcult/ready(population, forced = FALSE)
+ required_candidates = get_antag_cap(population)
. = ..()
-/datum/dynamic_ruleset/roundstart/bloodcult/pre_execute()
+/datum/dynamic_ruleset/roundstart/bloodcult/pre_execute(population)
. = ..()
- var/cultists = antag_cap[indice_pop]
+ var/cultists = get_antag_cap(population)
for(var/cultists_number = 1 to cultists)
if(candidates.len <= 0)
break
@@ -287,20 +287,20 @@
restricted_roles = list("Head of Security", "Captain") // Just to be sure that a nukie getting picked won't ever imply a Captain or HoS not getting drafted
required_candidates = 5
weight = 3
- cost = 40
+ cost = 20
requirements = list(90,90,90,80,60,40,30,20,10,10)
- flags = HIGHLANDER_RULESET
- antag_cap = list(2,2,2,3,3,3,4,4,5,5)
+ flags = HIGH_IMPACT_RULESET
+ antag_cap = list("denominator" = 18, "offset" = 1)
var/datum/team/nuclear/nuke_team
-/datum/dynamic_ruleset/roundstart/nuclear/ready(forced = FALSE)
- required_candidates = antag_cap[indice_pop]
+/datum/dynamic_ruleset/roundstart/nuclear/ready(population, forced = FALSE)
+ required_candidates = get_antag_cap(population)
. = ..()
-/datum/dynamic_ruleset/roundstart/nuclear/pre_execute()
+/datum/dynamic_ruleset/roundstart/nuclear/pre_execute(population)
. = ..()
// If ready() did its job, candidates should have 5 or more members in it
- var/operatives = antag_cap[indice_pop]
+ var/operatives = get_antag_cap(population)
for(var/operatives_number = 1 to operatives)
if(candidates.len <= 0)
break
@@ -371,12 +371,12 @@
minimum_required_age = 14
restricted_roles = list("AI", "Cyborg", "Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director")
required_candidates = 3
- weight = 2
+ weight = 3
delay = 7 MINUTES
- cost = 35
+ cost = 20
requirements = list(101,101,70,40,30,20,10,10,10,10)
- antag_cap = list(3,3,3,3,3,3,3,3,3,3)
- flags = HIGHLANDER_RULESET
+ antag_cap = 3
+ flags = HIGH_IMPACT_RULESET
blocking_rules = list(/datum/dynamic_ruleset/latejoin/provocateur)
// I give up, just there should be enough heads with 35 players...
minimum_players = 35
@@ -385,9 +385,9 @@
var/datum/team/revolution/revolution
var/finished = FALSE
-/datum/dynamic_ruleset/roundstart/revs/pre_execute()
+/datum/dynamic_ruleset/roundstart/revs/pre_execute(population)
. = ..()
- var/max_candidates = antag_cap[indice_pop]
+ var/max_candidates = get_antag_cap(population)
for(var/i = 1 to max_candidates)
if(candidates.len <= 0)
break
@@ -457,16 +457,16 @@
weight = 2
cost = 30
requirements = list(101,101,101,101,101,70,40,10,10,10)
- flags = HIGHLANDER_RULESET
+ flags = HIGH_IMPACT_RULESET
minimum_players = 36
- antag_cap = list(6,6,6,6,6,6,6,6,6,6)
+ antag_cap = 6
/// A reference to the handler that is used to run pre_execute(), execute(), etc..
var/datum/gang_handler/handler
-/datum/dynamic_ruleset/roundstart/families/pre_execute()
+/datum/dynamic_ruleset/roundstart/families/pre_execute(population)
..()
handler = new /datum/gang_handler(candidates,restricted_roles)
- handler.gangs_to_generate = (antag_cap[indice_pop] / 2)
+ handler.gangs_to_generate = (get_antag_cap(population) / 2)
handler.gang_balance_cap = clamp((indice_pop - 3), 2, 5) // gang_balance_cap by indice_pop: (2,2,2,2,2,3,4,5,5,5)
handler.use_dynamic_timing = TRUE
return handler.pre_setup_analogue()
@@ -501,12 +501,14 @@
weight = 3
cost = 0
requirements = list(101,101,101,101,101,101,101,101,101,101)
+ flags = LONE_RULESET
/datum/dynamic_ruleset/roundstart/extended/pre_execute()
. = ..()
message_admins("Starting a round of extended.")
log_game("Starting a round of extended.")
- mode.spend_threat(mode.threat)
+ mode.spend_roundstart_budget(mode.round_start_budget)
+ mode.spend_midround_budget(mode.mid_round_budget)
mode.threat_log += "[worldtime2text()]: Extended ruleset set threat to 0."
return TRUE
@@ -550,6 +552,7 @@
weight = 3
cost = 0
requirements = list(101,101,101,101,101,101,101,101,101,101)
+ flags = LONE_RULESET
var/players_per_carrier = 30
var/monkeys_to_win = 1
var/escaped_monkeys = 0
@@ -611,6 +614,7 @@
weight = 3
cost = 0
requirements = list(101,101,101,101,101,101,101,101,101,101)
+ flags = LONE_RULESET
var/meteordelay = 2000
var/nometeors = FALSE
var/rampupdelta = 5
diff --git a/code/game/gamemodes/dynamic/dynamic_simulations.dm b/code/game/gamemodes/dynamic/dynamic_simulations.dm
new file mode 100644
index 00000000000..3e312b65700
--- /dev/null
+++ b/code/game/gamemodes/dynamic/dynamic_simulations.dm
@@ -0,0 +1,137 @@
+#ifdef TESTING
+/datum/dynamic_simulation
+ var/datum/game_mode/dynamic/gamemode
+ var/datum/dynamic_simulation_config/config
+ var/list/mock_candidates = list()
+
+/datum/dynamic_simulation/proc/initialize_gamemode(forced_threat)
+ gamemode = new
+
+ if (forced_threat)
+ gamemode.create_threat(forced_threat)
+ else
+ gamemode.generate_threat()
+
+ gamemode.generate_budgets()
+ gamemode.set_cooldowns()
+
+/datum/dynamic_simulation/proc/create_candidates(players)
+ GLOB.new_player_list.Cut()
+
+ for (var/_ in 1 to players)
+ var/mob/dead/new_player/mock_new_player = new
+ mock_new_player.ready = PLAYER_READY_TO_PLAY
+
+ var/datum/mind/mock_mind = new
+ mock_new_player.mind = mock_mind
+
+ var/datum/client_interface/mock_client = new
+
+ var/datum/preferences/prefs = new
+ var/list/be_special = list()
+ for (var/special_role in GLOB.special_roles)
+ be_special += special_role
+
+ prefs.be_special = be_special
+ mock_client.prefs = prefs
+
+ mock_new_player.mock_client = mock_client
+
+ mock_candidates += mock_new_player
+
+/datum/dynamic_simulation/proc/simulate(datum/dynamic_simulation_config/config)
+ src.config = config
+
+ initialize_gamemode(config.forced_threat_level)
+ create_candidates(config.roundstart_players)
+ gamemode.pre_setup()
+
+ var/total_antags = 0
+ for (var/_ruleset in gamemode.executed_rules)
+ var/datum/dynamic_ruleset/ruleset = _ruleset
+ total_antags += ruleset.assigned.len
+
+ return list(
+ "roundstart_players" = config.roundstart_players,
+ "threat_level" = gamemode.threat_level,
+ "snapshot" = list(
+ "antag_percent" = total_antags / config.roundstart_players,
+ "remaining_threat" = gamemode.mid_round_budget,
+ "rulesets" = gamemode.executed_rules.Copy(),
+ ),
+ )
+
+/datum/dynamic_simulation_config
+ /// How many players round start should there be?
+ var/roundstart_players
+
+ /// Optional, force this threat level instead of picking randomly through the lorentz distribution
+ var/forced_threat_level
+
+/client/proc/run_dynamic_simulations()
+ set name = "Run Dynamic Simulations"
+ set category = "Debug"
+
+ var/simulations = input(usr, "Enter number of simulations") as num
+ var/roundstart_players = input(usr, "Enter number of round start players") as num
+ var/forced_threat_level = input(usr, "Enter forced threat level, if you want one") as num | null
+
+ SSticker.mode = config.pick_mode("dynamic")
+ message_admins("Running dynamic simulations...")
+
+ var/list/outputs = list()
+
+ var/datum/dynamic_simulation_config/dynamic_config = new
+
+ if (roundstart_players)
+ dynamic_config.roundstart_players = roundstart_players
+
+ if (forced_threat_level)
+ dynamic_config.forced_threat_level = forced_threat_level
+
+ for (var/count in 1 to simulations)
+ var/datum/dynamic_simulation/simulator = new
+ var/output = simulator.simulate(dynamic_config)
+ outputs += list(output)
+
+ if (CHECK_TICK)
+ log_world("[count]/[simulations]")
+
+ message_admins("Writing file...")
+ WRITE_FILE(file("[GLOB.log_directory]/dynamic_simulations.json"), json_encode(outputs))
+ message_admins("Writing complete.")
+
+/proc/export_dynamic_json_of(ruleset_list)
+ var/list/export = list()
+
+ for (var/_ruleset in ruleset_list)
+ var/datum/dynamic_ruleset/ruleset = _ruleset
+ export[ruleset.name] = list(
+ "repeatable_weight_decrease" = ruleset.repeatable_weight_decrease,
+ "weight" = ruleset.weight,
+ "cost" = ruleset.cost,
+ "scaling_cost" = ruleset.scaling_cost,
+ "antag_cap" = ruleset.antag_cap,
+ "pop_per_requirement" = ruleset.pop_per_requirement,
+ "requirements" = ruleset.requirements,
+ "base_prob" = ruleset.base_prob,
+ )
+
+ return export
+
+/client/proc/export_dynamic_json()
+ set name = "Export dynamic.json"
+ set category = "Debug"
+
+ var/datum/game_mode/dynamic/dynamic = SSticker.mode
+
+ var/list/export = list()
+ export["Roundstart"] = export_dynamic_json_of(dynamic.roundstart_rules)
+ export["Midround"] = export_dynamic_json_of(dynamic.midround_rules)
+ export["Latejoin"] = export_dynamic_json_of(dynamic.latejoin_rules)
+
+ message_admins("Writing file...")
+ WRITE_FILE(file("[GLOB.log_directory]/dynamic.json"), json_encode(export))
+ message_admins("Writing complete.")
+
+#endif
diff --git a/code/game/gamemodes/dynamic/readme.md b/code/game/gamemodes/dynamic/readme.md
index e2bae07a450..645ab3ac8d5 100644
--- a/code/game/gamemodes/dynamic/readme.md
+++ b/code/game/gamemodes/dynamic/readme.md
@@ -6,10 +6,32 @@ Dynamic rolls threat based on a special sauce formula:
> [dynamic_curve_width][/datum/controller/global_vars/var/dynamic_curve_width] \* tan((3.1416 \* (rand() - 0.5) \* 57.2957795)) + [dynamic_curve_centre][/datum/controller/global_vars/var/dynamic_curve_centre]
+This threat is split into two separate budgets--`round_start_budget` and `mid_round_budget`. For example, a round with 50 threat might be split into a 30 roundstart budget, and a 20 midround budget. The roundstart budget is used to apply antagonists applied on readied players when the roundstarts (`/datum/dynamic_ruleset/roundstart`). The midround budget is used for two types of rulesets:
+- `/datum/dynamic_ruleset/midround` - Rulesets that apply to either existing alive players, or to ghosts. Think Blob or Space Ninja, which poll ghosts asking if they want to play as these roles.
+- `/datum/dynamic_ruleset/latejoin` - Rulesets that apply to the next player that joins. Think Syndicate Infiltrator, which converts a player just joining an existing round into traitor.
+
+This split is done with a similar method, known as the ["lorentz distribution"](https://en.wikipedia.org/wiki/Cauchy_distribution), exists to create a bell curve that ensures that while most rounds will have a threat level around ~50, chaotic and tame rounds still exist for variety.
+
+The process of creating these numbers occurs in `/datum/game_mode/dynamic/proc/generate_threat` (for creating the threat level) and `/datum/game_mode/dynamic/proc/generate_budgets` (for splitting the threat level into budgets).
+
+## Deciding roundstart threats
+In `/datum/game_mode/dynamic/proc/roundstart()` (called when no admin chooses the rulesets explicitly), Dynamic uses the available roundstart budget to pick threats. This is done through the following system:
+
+- All roundstart rulesets (remember, `/datum/dynamic_ruleset/roundstart`) are put into an associative list with their weight as the values (`drafted_rules`).
+- Until there is either no roundstart budget left, or until there is no ruleset we can choose from with the available threat, a `pickweight` is done based on the drafted_rules. If the same threat is picked twice, it will "scale up". The meaning of this depends on the ruleset itself, using the `scaled_times` variable; traitors for instance will create more the higher they scale.
+ - If a ruleset is chosen with the `HIGH_IMPACT_RULESET` in its `flags`, then all other `HIGH_IMPACT_RULESET`s will be removed from `drafted_rules`. This is so that only one can ever be chosen.
+ - If a ruleset has `LONE_RULESET` in its `flags`, then it will be removed from `drafted_rules`. This is to ensure it will only ever be picked once. An example of this in use is Wizard, to avoid creating multiple wizards.
+- After all roundstart threats are chosen, `/datum/dynamic_ruleset/proc/picking_roundstart_rule` is called for each, passing in the ruleset and the number of times it is scaled.
+ - In this stage, `pre_execute` is called, which is the function that will determine what players get what antagonists. If this function returns FALSE for whatever reason (in the case of an error), then its threat is refunded.
+
+After this process is done, any leftover roundstart threat will be given to the existing midround budget (done in `/datum/game_mode/dynamic/pre_setup()`).
+
+## Deciding midround threats
+
Latejoin and midround injection cooldowns are set using exponential distribution between
-- 5 minutes and 25 for latejoin
-- 15 minutes and 35 for midround
+- 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)
this value is then added to `world.time` and assigned to the injection cooldown variables.
@@ -21,7 +43,7 @@ this value is then added to `world.time` and assigned to the injection cooldown
4. [picking_roundstart_rule][/datum/game_mode/dynamic/proc/picking_roundstart_rule]\(drafted_rules)
5. [post_setup][/datum/game_mode/proc/post_setup]\()
-## Process
+## Rule Processing
Calls [rule_process][/datum/dynamic_ruleset/proc/rule_process] on every rule which is in the current_rules list.
Every sixty seconds, update_playercounts()
@@ -72,3 +94,77 @@ Midround: Instead of building a single list candidates, candidates contains four
Midround - Rulesets have additional types
/from_ghosts: execute() -> send_applications() -> review_applications() -> finish_setup(mob/newcharacter, index) -> setup_role(role)
**NOTE: execute() here adds dead players and observers to candidates list
+
+## Configuration and variables
+
+### Configuration
+Configuration can be done through a `config/dynamic.json` file. One is provided as example in the codebase. This config file, loaded in `/datum/game_mode/dynamic/pre_setup()`, directly overrides the values in the codebase, and so is perfect for making some rulesets harder/easier to get, turning them off completely, changing how much they cost, etc.
+
+The format of this file is:
+```json
+{
+ "Dynamic": {
+ /* Configuration in here will directly override `/datum/game_mode/dynamic` itself. */
+ /* Keys are variable names, values are their new values. */
+ },
+
+ "Roundstart": {
+ /* Configuration in here will apply to `/datum/dynamic_ruleset/roundstart` instances. */
+ /* Keys are the ruleset names, values are another associative list with keys being variable names and values being new values. */
+ "Wizard": {
+ /* I, a head admin, have died to wizard, and so I made it cost a lot more threat than it does in the codebase. */
+ "cost": 80
+ }
+ },
+
+ "Midround": {
+ /* Same as "Roundstart", but for `/datum/dynamic_ruleset/midround` instead. */
+ },
+
+ "Latejoin": {
+ /* Same as "Roundstart", but for `/datum/dynamic_ruleset/latejoin` instead. */
+ }
+}
+```
+
+Note: Comments are not possible in this format, and are just in this document for the sake of readability.
+
+### Rulesets
+Rulesets have the following variables notable to developers and those interested in tuning.
+
+- `required_candidates` - The number of people that *must be willing* (in their preferences) to be an antagonist with this ruleset. If the candidates do not meet this requirement, then the ruleset will not bother to be drafted.
+- `antag_cap` - Judges the amount of antagonists to apply, for both solo and teams. Note that some antagonists (such as traitors, lings, heretics, etc) will add more based on how many times they've been scaled. Written as a linear equation--ceil(x/denominator) + offset, or as a fixed constant. If written as a linear equation, will be in the form of `list("denominator" = denominator, "offset" = offset)`.
+ - Examples include:
+ - Traitor: `antag_cap = list("denominator" = 24)`. This means that for every 24 players, 1 traitor will be added (assuming no scaling).
+ - Nuclear Emergency: `antag_cap = list("denominator" = 18, "offset" = 1)`. For every 18 players, 1 nuke op will be added. Starts at 1, meaning at 30 players, 3 nuke ops will be created, rather than 2.
+ - Revolution: `antag_cap = 3`. There will always be 3 rev-heads, no matter what.
+- `minimum_required_age` - The minimum age in order to apply for the ruleset.
+- `weight` - How likely this ruleset is to be picked. A higher weight results in a higher chance of drafting.
+- `cost` - The initial cost of the ruleset. This cost is taken from either the roundstart or midround budget, depending on the ruleset.
+- `scaling_cost` - Cost for every *additional* application of this ruleset.
+ - Suppose traitors has a `cost` of 8, and a `scaling_cost` of 5. This means that buying 1 application of the traitor ruleset costs 8 threat, but buying two costs 13 (8 + 5). Buying it a third time is 18 (8 + 5 + 5), etc.
+- `pop_per_requirement` - The range of population each value in `requirements` represents. By default, this is 6.
+ - If the value is five the range is 0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30-34, 35-39, 40-54, 45+.
+ - If it is six the range is 0-5, 6-11, 12-17, 18-23, 24-29, 30-35, 36-41, 42-47, 48-53, 54+.
+ - If it is seven the range is 0-6, 7-13, 14-20, 21-27, 28-34, 35-41, 42-48, 49-55, 56-62, 63+.
+- `requirements` - A list that represents, per population range (see: `pop_per_requirement`), how much threat is required to *consider* this ruleset. This is independent of how much it'll actually cost. This uses *threat level*, not the budget--meaning if a round has 50 threat level, but only 10 points of round start threat, a ruleset with a requirement of 40 can still be picked if it can be bought.
+ - Suppose wizard has a `requirements` of `list(90,90,70,40,30,20,10,10,10,10)`. This means that, at 0-5 and 6-11 players, A station must have 90 threat in order for a wizard to be possible. At 12-17, 70 threat is required instead, etc.
+- `restricted_roles` - A list of jobs that *can't* be drafted by this ruleset. For example, cyborgs cannot be changelings, and so are in the `restricted_roles`.
+- `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.
+
+### 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.
+- `roundstart_split_curve_width` - A number between 0.5 and 4. Equivalent to threat_curve_width, but for the budget split. Higher value will favour more variance in splits and lower value rounds closer to the average.
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index 86e57987130..99037b17091 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -875,25 +875,12 @@
No stacking: - Option is [GLOB.dynamic_no_stacking ? "ON" : "OFF"].
Unless the threat goes above [GLOB.dynamic_stacking_limit], only one "round-ender" ruleset will be drafted.
- Classic secret mode: - Option is [GLOB.dynamic_classic_secret ? "ON" : "OFF"].
-
Only one roundstart ruleset will be drafted. Only traitors and minor roles will latespawn.
-
-
Forced threat level: Current value : [GLOB.dynamic_forced_threat_level].
The value threat is set to if it is higher than -1.
Stacking threeshold: Current value : [GLOB.dynamic_stacking_limit].
The threshold at which "round-ender" rulesets will stack. A value higher than 100 ensure this never happens.
- Advanced parameters
- Curve centre: -> [GLOB.dynamic_curve_centre] <-
- Curve width: -> [GLOB.dynamic_curve_width] <-
- Latejoin injection delay:
- Minimum: -> [GLOB.dynamic_latejoin_delay_min / 60 / 10] <- Minutes
- Maximum: -> [GLOB.dynamic_latejoin_delay_max / 60 / 10] <- Minutes
- Midround injection delay:
- Minimum: -> [GLOB.dynamic_midround_delay_min / 60 / 10] <- Minutes
- Maximum: -> [GLOB.dynamic_midround_delay_max / 60 / 10] <- Minutes
"}
user << browse(dat, "window=dyn_mode_options;size=900x650")
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 7e5cafbb99d..195aaca9aa0 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -184,6 +184,8 @@ GLOBAL_PROTECT(admin_verbs_debug)
/client/proc/print_cards,
#ifdef TESTING
/client/proc/check_missing_sprites,
+ /client/proc/export_dynamic_json,
+ /client/proc/run_dynamic_simulations,
#endif
/datum/admins/proc/create_or_modify_area,
#ifdef REFERENCE_TRACKING
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 04eb044ffaa..0f72b38451b 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -681,113 +681,6 @@
return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null)
dynamic_mode_options(usr)
-
- else if(href_list["f_dynamic_roundstart_centre"])
- if(!check_rights(R_ADMIN))
- return
- if(SSticker?.mode)
- return alert(usr, "The game has already started.", null, null, null, null)
- if(!SSticker.is_mode("dynamic"))
- return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null)
-
- var/new_centre = input(usr,"Change the centre of the dynamic mode threat curve. A negative value will give a more peaceful round ; a positive value, a round with higher threat. Any number between -5 and +5 is allowed.", "Change curve centre", null) as num
- if (new_centre < -5 || new_centre > 5)
- return alert(usr, "Only values between -5 and +5 are allowed.", null, null, null, null)
-
- log_admin("[key_name(usr)] changed the distribution curve center to [new_centre].")
- message_admins("[key_name(usr)] changed the distribution curve center to [new_centre]", 1)
- GLOB.dynamic_curve_centre = new_centre
- dynamic_mode_options(usr)
-
- else if(href_list["f_dynamic_roundstart_width"])
- if(!check_rights(R_ADMIN))
- return
- if(SSticker?.mode)
- return alert(usr, "The game has already started.", null, null, null, null)
- if(!SSticker.is_mode("dynamic"))
- return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null)
-
- var/new_width = input(usr,"Change the width of the dynamic mode threat curve. A higher value will favour extreme rounds ; a lower value, a round closer to the average. Any Number between 0.5 and 4 are allowed.", "Change curve width", null) as num
- if (new_width < 0.5 || new_width > 4)
- return alert(usr, "Only values between 0.5 and +2.5 are allowed.", null, null, null, null)
-
- log_admin("[key_name(usr)] changed the distribution curve width to [new_width].")
- message_admins("[key_name(usr)] changed the distribution curve width to [new_width]", 1)
- GLOB.dynamic_curve_width = new_width
- dynamic_mode_options(usr)
-
- else if(href_list["f_dynamic_roundstart_latejoin_min"])
- if(!check_rights(R_ADMIN))
- return
- if(SSticker?.mode)
- return alert(usr, "The game has already started.", null, null, null, null)
- if(!SSticker.is_mode("dynamic"))
- return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null)
- var/new_min = input(usr,"Change the minimum delay of latejoin injection in minutes.", "Change latejoin injection delay minimum", null) as num
- if(new_min <= 0)
- return alert(usr, "The minimum can't be zero or lower.", null, null, null, null)
- if((new_min MINUTES) > GLOB.dynamic_latejoin_delay_max)
- return alert(usr, "The minimum must be lower than the maximum.", null, null, null, null)
-
- log_admin("[key_name(usr)] changed the latejoin injection minimum delay to [new_min] minutes.")
- message_admins("[key_name(usr)] changed the latejoin injection minimum delay to [new_min] minutes", 1)
- GLOB.dynamic_latejoin_delay_min = (new_min MINUTES)
- dynamic_mode_options(usr)
-
- else if(href_list["f_dynamic_roundstart_latejoin_max"])
- if(!check_rights(R_ADMIN))
- return
- if(SSticker?.mode)
- return alert(usr, "The game has already started.", null, null, null, null)
- if(!SSticker.is_mode("dynamic"))
- return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null)
- var/new_max = input(usr,"Change the maximum delay of latejoin injection in minutes.", "Change latejoin injection delay maximum", null) as num
- if(new_max <= 0)
- return alert(usr, "The maximum can't be zero or lower.", null, null, null, null)
- if((new_max MINUTES) < GLOB.dynamic_latejoin_delay_min)
- return alert(usr, "The maximum must be higher than the minimum.", null, null, null, null)
-
- log_admin("[key_name(usr)] changed the latejoin injection maximum delay to [new_max] minutes.")
- message_admins("[key_name(usr)] changed the latejoin injection maximum delay to [new_max] minutes", 1)
- GLOB.dynamic_latejoin_delay_max = (new_max MINUTES)
- dynamic_mode_options(usr)
-
- else if(href_list["f_dynamic_roundstart_midround_min"])
- if(!check_rights(R_ADMIN))
- return
- if(SSticker?.mode)
- return alert(usr, "The game has already started.", null, null, null, null)
- if(!SSticker.is_mode("dynamic"))
- return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null)
- var/new_min = input(usr,"Change the minimum delay of midround injection in minutes.", "Change midround injection delay minimum", null) as num
- if(new_min <= 0)
- return alert(usr, "The minimum can't be zero or lower.", null, null, null, null)
- if((new_min MINUTES) > GLOB.dynamic_midround_delay_max)
- return alert(usr, "The minimum must be lower than the maximum.", null, null, null, null)
-
- log_admin("[key_name(usr)] changed the midround injection minimum delay to [new_min] minutes.")
- message_admins("[key_name(usr)] changed the midround injection minimum delay to [new_min] minutes", 1)
- GLOB.dynamic_midround_delay_min = (new_min MINUTES)
- dynamic_mode_options(usr)
-
- else if(href_list["f_dynamic_roundstart_midround_max"])
- if(!check_rights(R_ADMIN))
- return
- if(SSticker?.mode)
- return alert(usr, "The game has already started.", null, null, null, null)
- if(!SSticker.is_mode("dynamic"))
- return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null)
- var/new_max = input(usr,"Change the maximum delay of midround injection in minutes.", "Change midround injection delay maximum", null) as num
- if(new_max <= 0)
- return alert(usr, "The maximum can't be zero or lower.", null, null, null, null)
- if((new_max MINUTES) > GLOB.dynamic_midround_delay_max)
- return alert(usr, "The maximum must be higher than the minimum.", null, null, null, null)
-
- log_admin("[key_name(usr)] changed the midround injection maximum delay to [new_max] minutes.")
- message_admins("[key_name(usr)] changed the midround injection maximum delay to [new_max] minutes", 1)
- GLOB.dynamic_midround_delay_max = (new_max MINUTES)
- dynamic_mode_options(usr)
-
else if(href_list["f_dynamic_force_extended"])
if(!check_rights(R_ADMIN))
return
@@ -811,19 +704,6 @@
log_admin("[key_name(usr)] set 'no_stacking' to [GLOB.dynamic_no_stacking].")
message_admins("[key_name(usr)] set 'no_stacking' to [GLOB.dynamic_no_stacking].")
dynamic_mode_options(usr)
-
- else if(href_list["f_dynamic_classic_secret"])
- if(!check_rights(R_ADMIN))
- return
-
- if(!SSticker.is_mode("dynamic"))
- return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null)
-
- GLOB.dynamic_classic_secret = !GLOB.dynamic_classic_secret
- log_admin("[key_name(usr)] set 'classic_secret' to [GLOB.dynamic_classic_secret].")
- message_admins("[key_name(usr)] set 'classic_secret' to [GLOB.dynamic_classic_secret].")
- dynamic_mode_options(usr)
-
else if(href_list["f_dynamic_stacking_limit"])
if(!check_rights(R_ADMIN))
return
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index 3cd864f05a7..8f9359dfa3d 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -228,3 +228,6 @@
///Override for sound_environments. If this is set the user will always hear a specific type of reverb (Instead of the area defined reverb)
var/sound_environment_override = SOUND_ENVIRONMENT_NONE
+
+ /// A mock client, provided by tests and friends
+ var/datum/client_interface/mock_client
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index fc7ebbb707c..d4fb7eb0f8b 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -46,6 +46,7 @@
#include "component_tests.dm"
#include "confusion.dm"
#include "designs.dm"
+#include "dynamic_ruleset_sanity.dm"
#include "emoting.dm"
#include "heretic_knowledge.dm"
#include "holidays.dm"
diff --git a/code/modules/unit_tests/dynamic_ruleset_sanity.dm b/code/modules/unit_tests/dynamic_ruleset_sanity.dm
new file mode 100644
index 00000000000..9fff5daf9b2
--- /dev/null
+++ b/code/modules/unit_tests/dynamic_ruleset_sanity.dm
@@ -0,0 +1,14 @@
+/// Verifies that roundstart dynamic rulesets are setup properly without external configuration.
+/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
+
+ var/has_scaling_cost = initial(ruleset.scaling_cost)
+ var/is_lone = initial(ruleset.flags) & (LONE_RULESET | HIGH_IMPACT_RULESET)
+
+ if (has_scaling_cost && is_lone)
+ Fail("[ruleset] has a scaling_cost, but is also a lone/highlander ruleset.")
+ else if (!has_scaling_cost && !is_lone)
+ Fail("[ruleset] has no scaling cost, but is also not a lone/highlander ruleset.")
diff --git a/config/dynamic.json b/config/dynamic.json
index 7d2f8cd30f7..f0961cbe2d7 100644
--- a/config/dynamic.json
+++ b/config/dynamic.json
@@ -2,8 +2,8 @@
"Dynamic": {},
"Roundstart": {
"Traitors": {
- "cost": 10,
- "scaling_cost": 10,
+ "cost": 8,
+ "scaling_cost": 9,
"weight": 5,
"required_candidates": 1,
"minimum_required_age": 0,
@@ -19,18 +19,9 @@
10,
10
],
- "antag_cap": [
- 1,
- 1,
- 1,
- 2,
- 2,
- 2,
- 3,
- 3,
- 3,
- 3
- ],
+ "antag_cap": {
+ "denominator": 24
+ },
"protected_roles": [
"Prisoner",
"Security Officer",
@@ -41,8 +32,7 @@
],
"restricted_roles": [
"Cyborg"
- ],
- "autotraitor_cooldown": 450
+ ]
}
},
"Midround": {},
diff --git a/tgstation.dme b/tgstation.dme
index b14eb9018bd..035e17e9bee 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -50,6 +50,7 @@
#include "code\__DEFINES\DNA.dm"
#include "code\__DEFINES\do_afters.dm"
#include "code\__DEFINES\dye_keys.dm"
+#include "code\__DEFINES\dynamic.dm"
#include "code\__DEFINES\economy.dm"
#include "code\__DEFINES\events.dm"
#include "code\__DEFINES\exosuit_fab.dm"
@@ -700,6 +701,7 @@
#include "code\datums\materials\hauntium.dm"
#include "code\datums\materials\meat.dm"
#include "code\datums\materials\pizza.dm"
+#include "code\datums\mocking\client.dm"
#include "code\datums\mood_events\_mood_event.dm"
#include "code\datums\mood_events\beauty_events.dm"
#include "code\datums\mood_events\drink_events.dm"
@@ -806,10 +808,12 @@
#include "code\game\gamemodes\clown_ops\clown_weapons.dm"
#include "code\game\gamemodes\cult\cult.dm"
#include "code\game\gamemodes\dynamic\dynamic.dm"
+#include "code\game\gamemodes\dynamic\dynamic_logging.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"
#include "code\game\gamemodes\dynamic\dynamic_rulesets_roundstart.dm"
+#include "code\game\gamemodes\dynamic\dynamic_simulations.dm"
#include "code\game\gamemodes\eldritch_cult\eldritch_cult.dm"
#include "code\game\gamemodes\extended\extended.dm"
#include "code\game\gamemodes\gang\gang.dm"