diff --git a/code/__DEFINES/dynamic.dm b/code/__DEFINES/dynamic.dm index f77cfcf59c292..cb6f3d4218339 100644 --- a/code/__DEFINES/dynamic.dm +++ b/code/__DEFINES/dynamic.dm @@ -17,6 +17,11 @@ /// This ruleset will be logged in persistence, to reduce the chances of it repeatedly rolling several rounds in a row. #define PERSISTENT_RULESET (1 << 5) +/// Can this ruleset be executed once we consider the round to be in the late game context? +/// We generally want lategame rulesets to be chaotic and antagonists which do not require a long setup in order to get started. +/// These are rulesets that should push the round to a close +#define LATEGAME_RULESET (1 << 6) + /// This is a "heavy" midround ruleset, and should be run later into the round #define MIDROUND_RULESET_STYLE_HEAVY "Heavy" diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index 5d795c7bf706a..68e32cdc7e40b 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -378,7 +378,10 @@ parts += "[FOURSPACES][FOURSPACES][entry]
" 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" + if (rule.lategame_spawned) + parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat (Lategame, threat level ignored)" + else + parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat" return parts.Join("
") /client/proc/roundend_report_file() @@ -726,29 +729,32 @@ /datum/controller/subsystem/ticker/proc/sendtodiscord(var/survivors, var/escapees, var/integrity) - var/discordmsg = "" - discordmsg += "--------------ROUND END--------------\n" - discordmsg += "Server: [CONFIG_GET(string/servername)]\n" - discordmsg += "Round Number: [GLOB.round_id]\n" - discordmsg += "Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]\n" - discordmsg += "Players: [GLOB.player_list.len]\n" - discordmsg += "Survivors: [survivors]\n" - discordmsg += "Escapees: [escapees]\n" - discordmsg += "Integrity: [integrity]\n" - discordmsg += "Gamemode: [SSticker.mode.name]\n" - if(istype(SSticker.mode, /datum/game_mode/dynamic)) - var/datum/game_mode/dynamic/mode = SSticker.mode - discordmsg += "Threat level: [mode.threat_level]\n" - discordmsg += "Threat left: [mode.mid_round_budget]\n" - discordmsg += "Executed rules:\n" - for(var/datum/dynamic_ruleset/rule in mode.executed_rules) - discordmsg += "[rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat\n" - var/list/ded = SSblackbox.first_death - if(ded) - discordmsg += "First Death: [ded["name"]], [ded["role"]], at [ded["area"]]\n" - var/last_words = ded["last_words"] ? "Their last words were: \"[ded["last_words"]]\"\n" : "They had no last words.\n" - discordmsg += "[last_words]\n" - else - discordmsg += "Nobody died!\n" - discordmsg += "--------------------------------------\n" - sendooc2ext(discordmsg) + var/discordmsg = "" + discordmsg += "--------------ROUND END--------------\n" + discordmsg += "Server: [CONFIG_GET(string/servername)]\n" + discordmsg += "Round Number: [GLOB.round_id]\n" + discordmsg += "Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]\n" + discordmsg += "Players: [GLOB.player_list.len]\n" + discordmsg += "Survivors: [survivors]\n" + discordmsg += "Escapees: [escapees]\n" + discordmsg += "Integrity: [integrity]\n" + discordmsg += "Gamemode: [SSticker.mode.name]\n" + if(istype(SSticker.mode, /datum/game_mode/dynamic)) + var/datum/game_mode/dynamic/mode = SSticker.mode + discordmsg += "Threat level: [mode.threat_level]\n" + discordmsg += "Threat left: [mode.mid_round_budget]\n" + discordmsg += "Executed rules:\n" + for(var/datum/dynamic_ruleset/rule in mode.executed_rules) + if (rule.lategame_spawned) + discordmsg += "[rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat (Lategame, threat level ignored)\n" + else + discordmsg += "[rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat\n" + var/list/ded = SSblackbox.first_death + if(ded) + discordmsg += "First Death: [ded["name"]], [ded["role"]], at [ded["area"]]\n" + var/last_words = ded["last_words"] ? "Their last words were: \"[ded["last_words"]]\"\n" : "They had no last words.\n" + discordmsg += "[last_words]\n" + else + discordmsg += "Nobody died!\n" + discordmsg += "--------------------------------------\n" + sendooc2ext(discordmsg) diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm index 4e35244ae9505..c030665dccf0c 100644 --- a/code/game/gamemodes/dynamic/dynamic.dm +++ b/code/game/gamemodes/dynamic/dynamic.dm @@ -95,7 +95,8 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) /// 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. + /// Once this time has passed, only midround antags with the LATEGAME_RULESET + /// flag may roll, and these will roll independent of threat requirements. var/midround_upper_bound = 100 MINUTES /// The distance between the chosen midround roll point (which is deterministic), @@ -208,6 +209,15 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) /// Cached value of is_station_intact. var/cached_station_intact = TRUE + /// If not null, use this instead of world.time + var/simulated_time = null + + /// If we are running simulations and should treat nobody signing up as successful spawns + var/simulated = FALSE + + /// Should we simulate there being more alive players than there actually are? + var/simulated_alive_players = 0 + /// When the cached station intactness will expire. COOLDOWN_DECLARE(intact_cache_expiry) @@ -429,7 +439,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) /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 + latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), latejoin_delay_min, latejoin_delay_max)) + get_time() /datum/game_mode/dynamic/pre_setup() if(CONFIG_GET(flag/dynamic_config_enabled)) @@ -604,7 +614,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) ruleset.trim_candidates() var/added_threat = ruleset.scale_up(roundstart_pop_ready, scaled_times) - if(ruleset.pre_execute(roundstart_pop_ready)) + if(simulated || 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(CHECK_BITFIELD(ruleset.flags, ONLY_RULESET)) only_ruleset_executed = TRUE @@ -654,6 +664,9 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) if(threat_level < GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking && high_impact_ruleset_active()) return FALSE + // Stop respecting cost once we reach the late game stage + ignore_cost = ignore_cost || is_lategame() + var/population = current_players[CURRENT_LIVING_PLAYERS].len if((new_rule.acceptable(population, threat_level) && (ignore_cost || new_rule.cost <= mid_round_budget)) || forced) new_rule.trim_candidates() @@ -728,7 +741,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/game_mode/dynamic, execute_midround_latejoin_rule), forced_latejoin_rule), forced_latejoin_rule.delay) forced_latejoin_rule = null - else if (latejoin_injection_cooldown < world.time && (forced_injection || prob(latejoin_roll_chance))) + else if (latejoin_injection_cooldown < get_time() && (forced_injection || prob(latejoin_roll_chance))) forced_injection = FALSE var/list/drafted_rules = list() @@ -750,7 +763,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) if (drafted_rules.len > 0 && pick_latejoin_rule(drafted_rules)) 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 + latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), latejoin_delay_min, latejoin_delay_max)) + get_time() /// Apply configurations to rule. /datum/game_mode/dynamic/proc/configure_ruleset(datum/dynamic_ruleset/ruleset) @@ -850,11 +863,19 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) if (20 to INFINITY) return rand(90, 100) +/datum/game_mode/dynamic/proc/is_lategame() + return (get_time() - SSticker.round_start_time) > midround_upper_bound + /// Log to messages and to the game /datum/game_mode/dynamic/proc/dynamic_log(text) + if (simulated) + return message_admins("DYNAMIC: [text]") log_game("DYNAMIC: [text]") +/datum/game_mode/dynamic/proc/get_time() + return simulated_time || world.time + /// This sets the "don't roll after high impact rules die" flag /// if the mode is dynamic, signalling that something major has happened /// and that dynamic should NOT try to roll new antags, even if all the diff --git a/code/game/gamemodes/dynamic/dynamic_hijacking.dm b/code/game/gamemodes/dynamic/dynamic_hijacking.dm index 7849fa7928da5..0de231ece1248 100644 --- a/code/game/gamemodes/dynamic/dynamic_hijacking.dm +++ b/code/game/gamemodes/dynamic/dynamic_hijacking.dm @@ -15,12 +15,12 @@ var/time_range = rand(random_event_hijack_minimum, random_event_hijack_maximum) - if (world.time - last_midround_injection_attempt < time_range) + if (get_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. Heavy injection chance has been raised to [get_heavy_midround_injection_chance(dry_run = TRUE)]%.") return CANCEL_PRE_RANDOM_EVENT - if (next_midround_injection() - world.time < time_range) + if (next_midround_injection() - get_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_logging.dm b/code/game/gamemodes/dynamic/dynamic_logging.dm index 08eb1330550d2..22510e9672913 100644 --- a/code/game/gamemodes/dynamic/dynamic_logging.dm +++ b/code/game/gamemodes/dynamic/dynamic_logging.dm @@ -73,7 +73,7 @@ var/datum/dynamic_snapshot/new_snapshot = new new_snapshot.remaining_threat = mid_round_budget - new_snapshot.time = world.time + new_snapshot.time = get_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 diff --git a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm index 2d631a14667cd..a31f953be4da0 100644 --- a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm +++ b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm @@ -16,19 +16,19 @@ return last_midround_injection_attempt + distance -/datum/game_mode/dynamic/proc/try_midround_roll() - if (!forced_injection && next_midround_injection() > world.time) - return +/datum/game_mode/dynamic/proc/try_midround_roll(force = FALSE) + if (!forced_injection && next_midround_injection() > get_time()) + return null if (GLOB.dynamic_forced_extended) - return + return null if (EMERGENCY_ESCAPED_OR_ENDGAMED) - return + return null var/spawn_heavy = prob(get_heavy_midround_injection_chance()) - last_midround_injection_attempt = world.time + last_midround_injection_attempt = get_time() next_midround_injection = null forced_injection = FALSE @@ -44,15 +44,15 @@ log_game("DYNAMIC: FAIL: [ruleset] has a weight of 0") continue - if (!ruleset.acceptable(SSticker.mode.current_players[CURRENT_LIVING_PLAYERS].len, threat_level)) + if (!ruleset.acceptable(simulated_alive_players || SSticker.mode.current_players[CURRENT_LIVING_PLAYERS].len, threat_level)) log_game("DYNAMIC: FAIL: [ruleset] is not acceptable with the current parameters. Alive players: [SSticker.mode.current_players[CURRENT_LIVING_PLAYERS].len], threat level: [threat_level]") continue - if (mid_round_budget < ruleset.cost) + if (mid_round_budget < ruleset.cost && !is_lategame()) log_game("DYNAMIC: FAIL: [ruleset] is too expensive, and cannot be bought. Midround budget: [mid_round_budget], ruleset cost: [ruleset.cost]") continue - if (ruleset.minimum_round_time > world.time - SSticker.round_start_time) + if (ruleset.minimum_round_time > get_time() - SSticker.round_start_time) log_game("DYNAMIC: FAIL: [ruleset] is trying to run too early. Minimum round time: [ruleset.minimum_round_time], current round time: [world.time - SSticker.round_start_time]") continue @@ -62,7 +62,7 @@ continue ruleset.trim_candidates() - if (!ruleset.ready()) + if (!ruleset.ready(force)) log_game("DYNAMIC: FAIL: [ruleset] is not ready()") continue @@ -76,13 +76,20 @@ log_game("DYNAMIC: Rolling [spawn_heavy ? "HEAVY" : "LIGHT"]... [heavy_light_log_count]") - if (spawn_heavy && drafted_heavies.len > 0 && pick_midround_rule(drafted_heavies, "heavy rulesets")) + // Attempt to draft a heavy ruleset + if (spawn_heavy && drafted_heavies.len > 0) + . = pick_midround_rule(drafted_heavies, "heavy rulesets") + if (.) + return + if (drafted_lights.len <= 0) + dynamic_log("No midround rulesets could be drafted as there were no drafted rules. ([heavy_light_log_count])") + return + . = pick_midround_rule(drafted_lights, "light rulesets") + if (!.) + dynamic_log("No midround rulesets could be drafted as pick midround rules returned nothing. ([heavy_light_log_count])") return - else if (drafted_lights.len > 0 && pick_midround_rule(drafted_lights, "light rulesets")) - if (spawn_heavy) - dynamic_log("A heavy ruleset was intended to roll, but there weren't any available. [heavy_light_log_count]") - else - dynamic_log("No midround rulesets could be drafted. ([heavy_light_log_count])") + if (spawn_heavy) + dynamic_log("A heavy ruleset was intended to roll, but there weren't any available. [heavy_light_log_count]") /// 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) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm index eb48b7164ee71..20d725fddf863 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm @@ -83,6 +83,9 @@ /// Whether repeated_mode_adjust weight changes have been logged already. var/logged_repeated_mode_adjust = FALSE + /// Was this ruleset spawned from the lategame mode? + var/lategame_spawned = FALSE + /datum/dynamic_ruleset/New(datum/game_mode/dynamic/dynamic_mode) // Rulesets can be instantiated more than once, such as when an admin clicks @@ -91,6 +94,7 @@ SHOULD_NOT_OVERRIDE(TRUE) mode = dynamic_mode + lategame_spawned = mode.is_lategame() ..() /datum/dynamic_ruleset/roundstart // One or more of those drafted at roundstart @@ -112,6 +116,9 @@ log_game("DYNAMIC: FAIL: [src] failed acceptable: maximum_players ([maximum_players]) < population ([population])") return FALSE + if (mode.is_lategame()) + return (flags & LATEGAME_RULESET) + pop_per_requirement = pop_per_requirement > 0 ? pop_per_requirement : mode.pop_per_requirement indice_pop = min(requirements.len,round(population/pop_per_requirement)+1) if (threat_level < requirements[indice_pop]) @@ -228,6 +235,9 @@ /// Checks if the ruleset is "dead", where all the antags are either dead or deconverted. /datum/dynamic_ruleset/proc/is_dead() + // Don't let dead threats affect simulation results + if (mode.simulated) + return FALSE for(var/datum/mind/mind in assigned) var/mob/living/body = mind.current // If they have no body, they're dead for realsies. @@ -248,7 +258,7 @@ if(body.soul_departed() || mind.hellbound) continue // Are they in medbay or an operating table/stasis bed, and have been dead for less than 20 minutes? If so, they're probably being revived. - if(world.time <= (mind.last_death + 15 MINUTES) && (istype(get_area(body), /area/medical) || (locate(/obj/machinery/stasis) in body.loc) || (locate(/obj/structure/table/optable) in body.loc))) + if((mode.simulated_time || world.time) <= (mind.last_death + 15 MINUTES) && (istype(get_area(body), /area/medical) || (locate(/obj/machinery/stasis) in body.loc) || (locate(/obj/structure/table/optable) in body.loc))) log_undead() return FALSE else diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm index 2e1308adad7ad..55f540c4f796c 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm @@ -189,7 +189,7 @@ // As a consequence, latejoin heretics start out at a massive // disadvantage if the round's been going on for a while. // Let's give them some influence points when they arrive. - new_heretic.knowledge_points += round((world.time - SSticker.round_start_time) / new_heretic.passive_gain_timer) + new_heretic.knowledge_points += round(((mode.simulated_time || world.time) - SSticker.round_start_time) / new_heretic.passive_gain_timer) // BUT let's not give smugglers a million points on arrival. // Limit it to four missed passive gain cycles (4 points). new_heretic.knowledge_points = min(new_heretic.knowledge_points, 5) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm index 4d4a416a68c1c..5c8d249c19b2a 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm @@ -298,7 +298,7 @@ weight = 1 cost = 15 requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED - flags = HIGH_IMPACT_RULESET|PERSISTENT_RULESET + flags = HIGH_IMPACT_RULESET|PERSISTENT_RULESET|LATEGAME_RULESET /datum/dynamic_ruleset/midround/from_ghosts/wizard/ready(forced = FALSE) if(!length(GLOB.wizardstart)) @@ -367,7 +367,7 @@ weight = 3 cost = 12 minimum_players = 25 - flags = HIGH_IMPACT_RULESET|INTACT_STATION_RULESET|PERSISTENT_RULESET + flags = HIGH_IMPACT_RULESET|INTACT_STATION_RULESET|PERSISTENT_RULESET|LATEGAME_RULESET /datum/dynamic_ruleset/midround/from_ghosts/blob/generate_ruleset_body(mob/applicant) var/body = applicant.become_overmind() @@ -391,7 +391,7 @@ weight = 3 cost = 12 minimum_players = 25 - flags = HIGH_IMPACT_RULESET|INTACT_STATION_RULESET|PERSISTENT_RULESET + flags = HIGH_IMPACT_RULESET|INTACT_STATION_RULESET|PERSISTENT_RULESET|LATEGAME_RULESET var/list/vents /datum/dynamic_ruleset/midround/from_ghosts/xenomorph/acceptable(population=0, threat=0) @@ -492,7 +492,7 @@ cost = 11 minimum_players = 25 repeatable = TRUE - flags = INTACT_STATION_RULESET|PERSISTENT_RULESET + flags = INTACT_STATION_RULESET|PERSISTENT_RULESET|LATEGAME_RULESET var/list/spawn_locs /datum/dynamic_ruleset/midround/from_ghosts/space_dragon/ready(forced = FALSE) @@ -625,6 +625,7 @@ cost = 8 minimum_players = 27 repeatable = FALSE + flags = LATEGAME_RULESET /datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat=0) if (!SSmapping.empty_space) @@ -690,7 +691,7 @@ weight = 3 cost = 11 repeatable = TRUE - flags = INTACT_STATION_RULESET|PERSISTENT_RULESET + flags = INTACT_STATION_RULESET|PERSISTENT_RULESET|LATEGAME_RULESET minimum_players = 27 var/fed = 1 var/list/vents @@ -886,6 +887,7 @@ minimum_players = 20 repeatable = TRUE blocking_rules = list(/datum/dynamic_ruleset/roundstart/nuclear, /datum/dynamic_ruleset/roundstart/clockcult) + flags = LATEGAME_RULESET var/spawn_loc /datum/dynamic_ruleset/midround/from_ghosts/ninja/ready(forced) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm index d5032b56ff746..aefd66ce22290 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm @@ -546,11 +546,11 @@ var/rampupdelta = 5 /datum/dynamic_ruleset/roundstart/meteor/rule_process() - if(nometeors || meteordelay > world.time - SSticker.round_start_time) + if(nometeors || meteordelay > (mode.simulated_time || world.time) - SSticker.round_start_time) return var/list/wavetype = GLOB.meteors_normal - var/meteorminutes = (world.time - SSticker.round_start_time - meteordelay) / 10 / 60 + var/meteorminutes = ((mode.simulated_time || world.time) - SSticker.round_start_time - meteordelay) / 10 / 60 if (prob(meteorminutes)) wavetype = GLOB.meteors_threatening diff --git a/code/game/gamemodes/dynamic/dynamic_simulations.dm b/code/game/gamemodes/dynamic/dynamic_simulations.dm index af1ed14f315b1..67b002bce5cc1 100644 --- a/code/game/gamemodes/dynamic/dynamic_simulations.dm +++ b/code/game/gamemodes/dynamic/dynamic_simulations.dm @@ -40,20 +40,55 @@ initialize_gamemode(config.forced_threat_level, config.roundstart_players) create_candidates(config.roundstart_players) gamemode.pre_setup() + gamemode.simulated = TRUE var/total_antags = 0 for (var/_ruleset in gamemode.executed_rules) var/datum/dynamic_ruleset/ruleset = _ruleset total_antags += ruleset.assigned.len + var/midround_threat = gamemode.mid_round_budget + + var/list/roundstart_rules = gamemode.executed_rules.Copy() + + var/list/midround_rules = list() + + // Generate midround threats + SSticker.round_start_time = 0 + var/simulated_time = 1 + gamemode.simulated_alive_players = config.roundstart_players + while (simulated_time < gamemode.midround_upper_bound) + // Simulate deaths and leaves + gamemode.simulated_alive_players = FLOOR(gamemode.simulated_alive_players * rand(90, 100) / 100, 1) + // Set the new world time + simulated_time = gamemode.next_midround_injection() + // Simulate an injection + gamemode.forced_injection = TRUE + // Set the simulated time + gamemode.simulated_time = simulated_time + // Run a midround injection + var/datum/dynamic_ruleset/simulated_result = gamemode.try_midround_roll(TRUE) + if (!simulated_result) + continue + midround_rules += list(list( + "ruleset" = simulated_result.name, + "weight" = simulated_result.weight, + "cost" = simulated_result.cost, + "execution_time" = simulated_time, + "remaining_threat" = gamemode.mid_round_budget, + "simulated_alive_players" = gamemode.simulated_alive_players, + "is_lategame" = gamemode.is_lategame() + )) + 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(), + "remaining_threat" = midround_threat, + "rulesets" = roundstart_rules, ), + "midround_rules" = midround_rules, ) /datum/dynamic_simulation_config @@ -96,6 +131,8 @@ 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() diff --git a/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm b/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm index 224a35c0708cf..94f364d48354d 100644 --- a/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm +++ b/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm @@ -24,13 +24,13 @@ if (ruleset.weight == 0) continue - if (ruleset.cost > max_threat_level) + if (ruleset.cost > max_threat_level && !is_lategame()) continue if (!ruleset.acceptable(SSticker.mode.current_players[CURRENT_LIVING_PLAYERS].len, threat_level)) continue - if (ruleset.minimum_round_time > world.time - SSticker.round_start_time) + if (ruleset.minimum_round_time > get_time() - SSticker.round_start_time) continue if(istype(ruleset, /datum/dynamic_ruleset/midround/from_ghosts) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT)) diff --git a/code/game/gamemodes/dynamic/ruleset_picking.dm b/code/game/gamemodes/dynamic/ruleset_picking.dm index 50d975e67cb30..6859ffa0227c5 100644 --- a/code/game/gamemodes/dynamic/ruleset_picking.dm +++ b/code/game/gamemodes/dynamic/ruleset_picking.dm @@ -47,17 +47,20 @@ current_midround_rulesets = drafted_rules - rule - midround_injection_timer_id = addtimer( - CALLBACK(src, PROC_REF(execute_midround_rule), rule), \ - ADMIN_CANCEL_MIDROUND_TIME, \ - TIMER_STOPPABLE, \ - ) - - log_game("DYNAMIC: [rule] ruleset executing...") - message_admins("DYNAMIC: Executing midround ruleset [rule] in [DisplayTimeText(ADMIN_CANCEL_MIDROUND_TIME)]. \ - CANCEL | \ - SOMETHING ELSE") - play_sound_to_all_admins('sound/effects/admin_alert.ogg') + if (!simulated) + midround_injection_timer_id = addtimer( + CALLBACK(src, PROC_REF(execute_midround_rule), rule), \ + ADMIN_CANCEL_MIDROUND_TIME, \ + TIMER_STOPPABLE, \ + ) + + log_game("DYNAMIC: [rule] ruleset executing...") + message_admins("DYNAMIC: Executing midround ruleset [rule] in [DisplayTimeText(ADMIN_CANCEL_MIDROUND_TIME)]. \ + CANCEL | \ + SOMETHING ELSE") + play_sound_to_all_admins('sound/effects/admin_alert.ogg') + else + execute_midround_rule(rule) return rule @@ -67,7 +70,10 @@ midround_injection_timer_id = null if (!rule.repeatable) midround_rules = remove_from_list(midround_rules, rule.type) - addtimer(CALLBACK(src, PROC_REF(execute_midround_latejoin_rule), rule), rule.delay) + if (!simulated) + addtimer(CALLBACK(src, PROC_REF(execute_midround_latejoin_rule), rule), rule.delay) + else + execute_midround_latejoin_rule(rule) /// Executes a random latejoin ruleset from the list of drafted rules. /datum/game_mode/dynamic/proc/pick_latejoin_rule(list/drafted_rules) @@ -83,6 +89,13 @@ /datum/game_mode/dynamic/proc/execute_midround_latejoin_rule(sent_rule) var/datum/dynamic_ruleset/rule = sent_rule spend_midround_budget(rule.cost, threat_log, "[worldtime2text()]: [rule.ruletype] [rule.name]") + if (simulated) + if(rule.flags & ONLY_RULESET) + only_ruleset_executed = TRUE + executed_rules += rule + if (rule.persistent) + current_rules += rule + return TRUE rule.pre_execute(current_players[CURRENT_LIVING_PLAYERS].len) var/execute_result = rule.execute() if(execute_result == DYNAMIC_EXECUTE_SUCCESS)