From 7ed5f622f44016802669ccc6c8977aa083865c4d Mon Sep 17 00:00:00 2001 From: keronshb Date: Tue, 14 Sep 2021 19:33:17 -0400 Subject: [PATCH] Dynamic --- code/__DEFINES/dynamic.dm | 30 +- code/__HELPERS/mobs.dm | 4 + code/__HELPERS/roundend.dm | 12 +- code/_globalvars/lists/mobs.dm | 15 + .../configuration/configuration.dm | 47 - .../subsystem/persistence/recent_votes_etc.dm | 39 - code/controllers/subsystem/vote.dm | 22 - code/datums/mocking/client.dm | 7 + code/game/gamemodes/dynamic/dynamic.dm | 1033 ++++++++--------- .../game/gamemodes/dynamic/dynamic_logging.dm | 101 ++ .../gamemodes/dynamic/dynamic_rulesets.dm | 215 ++-- .../dynamic/dynamic_rulesets_latejoin.dm | 206 +--- .../dynamic/dynamic_rulesets_midround.dm | 631 +++++----- .../dynamic/dynamic_rulesets_minor.dm | 255 ---- .../dynamic/dynamic_rulesets_roundstart.dm | 561 +++------ .../gamemodes/dynamic/dynamic_simulations.dm | 122 ++ .../gamemodes/dynamic/dynamic_storytellers.dm | 388 ------- code/game/gamemodes/dynamic/readme.md | 166 ++- .../game/gamemodes/dynamic/ruleset_picking.dm | 121 ++ code/modules/admin/admin.dm | 26 - code/modules/admin/admin_verbs.dm | 2 + code/modules/admin/topic.dm | 242 +--- .../nukeop/equipment/nuclear_challenge.dm | 9 - .../antagonists/traitor/classes/human.dm | 4 - .../antagonists/traitor/datum_traitor.dm | 6 +- code/modules/mob/mob_defines.dm | 3 + code/modules/unit_tests/_unit_tests.dm | 1 + .../unit_tests/dynamic_ruleset_sanity.dm | 14 + config/dynamic.json | 138 +++ tgstation.dme | 5 +- 30 files changed, 1833 insertions(+), 2592 deletions(-) create mode 100644 code/datums/mocking/client.dm create mode 100644 code/game/gamemodes/dynamic/dynamic_logging.dm delete mode 100644 code/game/gamemodes/dynamic/dynamic_rulesets_minor.dm create mode 100644 code/game/gamemodes/dynamic/dynamic_simulations.dm delete mode 100644 code/game/gamemodes/dynamic/dynamic_storytellers.dm create mode 100644 code/game/gamemodes/dynamic/ruleset_picking.dm create mode 100644 code/modules/unit_tests/dynamic_ruleset_sanity.dm create mode 100644 config/dynamic.json diff --git a/code/__DEFINES/dynamic.dm b/code/__DEFINES/dynamic.dm index 6a15834ed8..dedf05396c 100644 --- a/code/__DEFINES/dynamic.dm +++ b/code/__DEFINES/dynamic.dm @@ -1,19 +1,17 @@ -#define CURRENT_LIVING_PLAYERS 1 -#define CURRENT_LIVING_ANTAGS 2 -#define CURRENT_DEAD_PLAYERS 3 -#define CURRENT_OBSERVERS 4 +/// 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 << 0) -#define NO_ASSASSIN (1<<0) -#define WAROPS_ALWAYS_ALLOWED (1<<1) -#define USE_PREF_WEIGHTS (1<<2) -#define FORCE_IF_WON (1<<3) -#define USE_PREV_ROUND_WEIGHTS (1<<4) +/// Only one ruleset with this flag will be picked. +#define HIGH_IMPACT_RULESET (1 << 1) -#define ONLY_RULESET (1<<0) -#define HIGHLANDER_RULESET (1<<1) -#define TRAITOR_RULESET (1<<2) -#define MINOR_RULESET (1<<3) -#define FAKE_ANTAG_RULESET (1<<4) -#define ALWAYS_MAX_WEIGHT_RULESET (1<<5) +/// This ruleset can only be picked once. Anything that does not have a scaling_cost MUST have this. +#define LONE_RULESET (1 << 2) -#define RULESET_STOP_PROCESSING 1 +/// No round event was hijacked this cycle +#define HIJACKED_NOTHING "HIJACKED_NOTHING" + +/// This cycle, a round event was hijacked when the last midround event was too recent. +#define HIJACKED_TOO_RECENT "HIJACKED_TOO_RECENT" + +/// This cycle, a round event was hijacked when the next midround event is too soon. +#define HIJACKED_TOO_SOON "HIJACKED_TOO_SOON" diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index 02af6fab79..4693041f18 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -457,3 +457,7 @@ GLOBAL_LIST_EMPTY(species_datums) REMOVE_TRAIT(L, TRAIT_PASSTABLE, source) if(!HAS_TRAIT(L, TRAIT_PASSTABLE)) L.pass_flags &= ~PASSTABLE + +/// 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 9a59b2b0a9..77e63a0dbe 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -390,20 +390,10 @@ parts += "[FOURSPACES]Max threat: [max_threat]" if(istype(SSticker.mode, /datum/game_mode/dynamic)) var/datum/game_mode/dynamic/mode = SSticker.mode - mode.update_playercounts() // ? - parts += "[FOURSPACES]Target threat: [mode.threat_level]" + 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" - parts += "[FOURSPACES]Other threat changes:" - for(var/str in mode.threat_log) - parts += "[FOURSPACES][FOURSPACES][str]" - for(var/entry in mode.threat_tallies) - parts += "[FOURSPACES][FOURSPACES][entry] added [mode.threat_tallies[entry]]" - SSblackbox.record_feedback("tally","threat",mode.threat_level,"Target threat") - SSblackbox.record_feedback("tally","threat",SSactivity.current_threat,"Final Threat") - SSblackbox.record_feedback("tally","threat",avg_threat,"Average Threat") - SSblackbox.record_feedback("tally","threat",max_threat,"Max Threat") return parts.Join("
") /client/proc/roundend_report_file() diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm index 64bac1f9af..85ffd99732 100644 --- a/code/_globalvars/lists/mobs.dm +++ b/code/_globalvars/lists/mobs.dm @@ -44,6 +44,21 @@ GLOBAL_LIST_EMPTY(mob_config_movespeed_type_lookup_floating) GLOBAL_LIST_EMPTY(latejoiners) //CIT CHANGE - All latejoining people, for traitor-target purposes. +/// All alive mobs with clients. +GLOBAL_LIST_EMPTY(alive_player_list) + +/// All dead mobs with clients. Does not include observers. +GLOBAL_LIST_EMPTY(dead_player_list) + +/// All alive antags with clients. +GLOBAL_LIST_EMPTY(current_living_antags) + +/// All observers with clients that joined as observers. +GLOBAL_LIST_EMPTY(current_observers_list) + +//Dynamic Port +GLOBAL_LIST_EMPTY(new_player_list) //all /mob/dead/new_player, in theory all should have clients and those that don't are in the process of spawning and get deleted when done. + /proc/update_config_movespeed_type_lookup(update_mobs = TRUE) // NOTE: This is entirely based on the fact that byond typesof/subtypesof gets longer/deeper paths before shallower ones. // If that ever breaks this entire proc breaks. diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm index 6a5a4c4610..ed3959c0cc 100644 --- a/code/controllers/configuration/configuration.dm +++ b/code/controllers/configuration/configuration.dm @@ -41,7 +41,6 @@ CRASH("/datum/controller/configuration/Load() called more than once!") InitEntries() LoadModes() - storyteller_cache = typecacheof(/datum/dynamic_storyteller, TRUE) if(fexists("[directory]/config.txt") && LoadEntries("config.txt") <= 1) var/list/legacy_configs = list("game_options.txt", "dbconfig.txt", "comms.txt") for(var/I in legacy_configs) @@ -373,52 +372,6 @@ Example config: return new T return new /datum/game_mode/extended() -/// For dynamic. -/datum/controller/configuration/proc/pick_storyteller(storyteller_name) - for(var/T in storyteller_cache) - var/datum/dynamic_storyteller/S = T - var/name = initial(S.name) - if(name && name == storyteller_name) - return T - return /datum/dynamic_storyteller/classic - -/// Same with this -/datum/controller/configuration/proc/get_runnable_storytellers() - var/list/datum/dynamic_storyteller/runnable_storytellers = new - var/list/probabilities = Get(/datum/config_entry/keyed_list/storyteller_weight) - var/list/repeated_mode_adjust = Get(/datum/config_entry/number_list/repeated_mode_adjust) - var/list/min_player_counts = Get(/datum/config_entry/keyed_list/storyteller_min_players) - var/list/storyteller_min_chaos = Get(/datum/config_entry/keyed_list/storyteller_min_chaos) - var/list/storyteller_max_chaos = Get(/datum/config_entry/keyed_list/storyteller_max_chaos) - for(var/T in storyteller_cache) - var/datum/dynamic_storyteller/S = T - var/config_tag = initial(S.config_tag) - if(!config_tag) - continue - var/probability = (config_tag in probabilities) ? probabilities[config_tag] : initial(S.weight) - var/min_players = (config_tag in min_player_counts) ? min_player_counts[config_tag] : initial(S.min_players) - if(probability <= 0) - continue - if(length(GLOB.player_list) < min_players) - continue - if(!Get(/datum/config_entry/flag/no_storyteller_threat_removal)) - var/min_chaos = (probabilities in storyteller_min_chaos) ? storyteller_min_chaos[config_tag] : initial(S.min_chaos) - var/max_chaos = (probabilities in storyteller_max_chaos) ? storyteller_max_chaos[config_tag] : initial(S.max_chaos) - if(SSpersistence.average_threat + 50 < min_chaos) - continue - if(SSpersistence.average_threat + 50 > max_chaos) - continue - if(SSpersistence.saved_storytellers.len == repeated_mode_adjust.len) - var/name = initial(S.name) - var/recent_round = min(SSpersistence.saved_storytellers.Find(name),3) - var/adjustment = 0 - while(recent_round) - adjustment += repeated_mode_adjust[recent_round] - recent_round = SSpersistence.saved_modes.Find(name,recent_round+1,0) - probability *= max(0,((100-adjustment)/100)) - runnable_storytellers[S] = probability - return runnable_storytellers - /datum/controller/configuration/proc/get_runnable_modes() var/list/datum/game_mode/runnable_modes = new var/list/probabilities = Get(/datum/config_entry/keyed_list/probability) diff --git a/code/controllers/subsystem/persistence/recent_votes_etc.dm b/code/controllers/subsystem/persistence/recent_votes_etc.dm index 45b866b1c6..68b5a3132d 100644 --- a/code/controllers/subsystem/persistence/recent_votes_etc.dm +++ b/code/controllers/subsystem/persistence/recent_votes_etc.dm @@ -5,24 +5,18 @@ var/list/saved_modes = list(1,2,3) var/list/saved_chaos = list(5,5,5) var/list/saved_dynamic_rules = list(list(),list(),list()) - var/list/saved_storytellers = list("foo","bar","baz") var/average_threat = 50 var/list/saved_maps /datum/controller/subsystem/persistence/SaveServerPersistence() . = ..() CollectRoundtype() - if(istype(SSticker.mode, /datum/game_mode/dynamic)) - var/datum/game_mode/dynamic/mode = SSticker.mode - CollectStoryteller(mode) - CollectRulesets(mode) RecordMaps() /datum/controller/subsystem/persistence/LoadServerPersistence() . = ..() LoadRecentModes() LoadRecentChaos() - LoadRecentStorytellers() LoadRecentRulesets() LoadRecentMaps() @@ -45,30 +39,6 @@ fdel(json_file) WRITE_FILE(json_file, json_encode(file_data)) -/datum/controller/subsystem/persistence/proc/CollectStoryteller(var/datum/game_mode/dynamic/mode) - saved_storytellers.len = 3 - saved_storytellers[3] = saved_storytellers[2] - saved_storytellers[2] = saved_storytellers[1] - saved_storytellers[1] = mode.storyteller.name - var/json_file = file("data/RecentStorytellers.json") - var/list/file_data = list() - file_data["data"] = saved_storytellers - fdel(json_file) - WRITE_FILE(json_file, json_encode(file_data)) - -/datum/controller/subsystem/persistence/proc/CollectRulesets(var/datum/game_mode/dynamic/mode) - saved_dynamic_rules[3] = saved_dynamic_rules[2] - saved_dynamic_rules[2] = saved_dynamic_rules[1] - saved_dynamic_rules[1] = list() - for(var/r in mode.executed_rules) - var/datum/dynamic_ruleset/rule = r - saved_dynamic_rules[1] += rule.config_tag - var/json_file = file("data/RecentRulesets.json") - var/list/file_data = list() - file_data["data"] = saved_dynamic_rules - fdel(json_file) - WRITE_FILE(json_file, json_encode(file_data)) - /datum/controller/subsystem/persistence/proc/RecordMaps() saved_maps = saved_maps?.len ? list("[SSmapping.config.map_name]") | saved_maps : list("[SSmapping.config.map_name]") var/json_file = file("data/RecentMaps.json") @@ -107,15 +77,6 @@ return saved_dynamic_rules = json["data"] -/datum/controller/subsystem/persistence/proc/LoadRecentStorytellers() - var/json_file = file("data/RecentStorytellers.json") - if(!fexists(json_file)) - return - var/list/json = json_decode(file2text(json_file)) - if(!json) - return - saved_storytellers = json["data"] - /datum/controller/subsystem/persistence/proc/LoadRecentMaps() var/json_file = file("data/RecentMaps.json") if(!fexists(json_file)) diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index 1153088a91..deb57d75d8 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -354,19 +354,6 @@ SUBSYSTEM_DEF(vote) if(stored_modetier_results[score_name] <= raw_score_numbers[CONFIG_GET(number/dropped_modes)]) stored_modetier_results -= score_name stored_modetier_results += "traitor" - if("dynamic") - if(SSticker.current_state > GAME_STATE_PREGAME)//Don't change the mode if the round already started. - return message_admins("A vote has tried to change the gamemode, but the game has already started. Aborting.") - var/list/runnable_storytellers = config.get_runnable_storytellers() - var/datum/dynamic_storyteller/picked - for(var/T in runnable_storytellers) - var/datum/dynamic_storyteller/S = T - if(stored_gamemode_votes[initial(S.name)] == 1 && CHECK_BITFIELD(initial(S.flags), FORCE_IF_WON)) - picked = S - runnable_storytellers[S] *= round(stored_gamemode_votes[initial(S.name)]*100000,1) - if(!picked) - picked = pickweight(runnable_storytellers, 0) - GLOB.dynamic_storyteller_type = picked if("map") var/datum/map_config/VM = config.maplist[.] message_admins("The map has been voted for and will change to: [VM.map_name]") @@ -492,15 +479,6 @@ SUBSYSTEM_DEF(vote) modes_to_add -= tag modes_to_add -= "traitor" // makes it so that traitor is always available choices.Add(modes_to_add) - if("dynamic") - GLOB.master_mode = "dynamic" - var/list/probabilities = CONFIG_GET(keyed_list/storyteller_weight) - for(var/T in config.get_runnable_storytellers()) - var/datum/dynamic_storyteller/S = T - var/probability = ((initial(S.config_tag) in probabilities) ? probabilities[initial(S.config_tag)] : initial(S.weight)) - if(probability > 0) - choices.Add(initial(S.name)) - choice_descs.Add(initial(S.desc)) if("custom") question = stripped_input(usr,"What is the vote for?") if(!question) diff --git a/code/datums/mocking/client.dm b/code/datums/mocking/client.dm new file mode 100644 index 0000000000..4a665f828b --- /dev/null +++ b/code/datums/mocking/client.dm @@ -0,0 +1,7 @@ +/// This should match the interface of /client wherever necessary. +/datum/client_interface + /// Player preferences datum for the client + var/datum/preferences/prefs + + /// The view of the client, similar to /client/var/view. + var/view = "15x15" diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm index c7fee7df96..53047b74e7 100644 --- a/code/game/gamemodes/dynamic/dynamic.dm +++ b/code/game/gamemodes/dynamic/dynamic.dm @@ -1,77 +1,41 @@ +#define RULESET_STOP_PROCESSING 1 -// -- Injection delays -GLOBAL_VAR_INIT(dynamic_latejoin_delay_min, (10 MINUTES)) -GLOBAL_VAR_INIT(dynamic_latejoin_delay_max, (30 MINUTES)) +#define FAKE_REPORT_CHANCE 8 +#define REPORT_NEG_DIVERGENCE -15 +#define REPORT_POS_DIVERGENCE 15 -GLOBAL_VAR_INIT(dynamic_midround_delay_min, (10 MINUTES)) -GLOBAL_VAR_INIT(dynamic_midround_delay_max, (30 MINUTES)) - -// -- Roundstart injection delays -GLOBAL_VAR_INIT(dynamic_first_latejoin_delay_min, (2 MINUTES)) -GLOBAL_VAR_INIT(dynamic_first_latejoin_delay_max, (30 MINUTES)) - -GLOBAL_VAR_INIT(dynamic_first_midround_delay_min, (20 MINUTES)) -GLOBAL_VAR_INIT(dynamic_first_midround_delay_max, (30 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) -// How many roundstart players required for high population override to take effect. -GLOBAL_VAR_INIT(dynamic_high_pop_limit, 55) // 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. GLOBAL_LIST_EMPTY(dynamic_forced_roundstart_ruleset) // Forced threat level, setting this to zero or higher forces the roundstart threat to the value. GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) -// Storyteller picked by the voting. -GLOBAL_VAR_INIT(dynamic_storyteller_type, /datum/dynamic_storyteller/classic) -// Storyteller forced by admins during voting--will be used instead of above. -GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) /datum/game_mode/dynamic - name = "dynamic mode" - config_tag = "dynamic" - - announce_span = "danger" - announce_text = "Dynamic mode!" // This needs to be changed maybe - - reroll_friendly = FALSE; - // Current storyteller - var/datum/dynamic_storyteller/storyteller = null // Threat logging vars - /// Starting threat level, for things that increase it but can bring it back down. - var/initial_threat_level = 0 - /// Target threat level right now. Antags will try to keep the round at this level. + /// The "threat cap", threat shouldn't normally go above this and is used in ruleset calculations var/threat_level = 0 - /// The current antag threat. Recalculated every time a ruletype starts or ends. - var/threat = 0 - /// Things that cause a rolling threat adjustment to be displayed at roundend. - var/list/threat_tallies = list() + + /// 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() - /// As above, but with info such as refunds. - var/list/threat_log_verbose = list() - /// List of roundstart rules used for selecting the rules. - var/list/roundstart_rules = list() - /// List of minor roundstart rules used for selecting the rules. - var/list/minor_rules = list() /// List of latejoin rules used for selecting the rules. - var/list/latejoin_rules = list() + var/list/latejoin_rules /// List of midround rules used for selecting the rules. - var/list/midround_rules = list() + var/list/midround_rules /** # Pop range per requirement. * 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+ @@ -80,21 +44,7 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) * 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+ */ - var/pop_per_requirement = 9 - /// 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) - /// Threat requirement for a second ruleset when high pop override is in effect. - var/high_pop_second_rule_req = 40 - /// Threat requirement for a third ruleset when high pop override is in effect. - var/high_pop_third_rule_req = 60 - /// The amount of additional rulesets waiting to be picked. - var/extra_rulesets_amount = 0 + var/pop_per_requirement = 6 /// Number of players who were ready on roundstart. var/roundstart_pop_ready = 0 /// List of candidates used on roundstart rulesets. @@ -103,68 +53,120 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) var/list/current_rules = list() /// List of executed rulesets. var/list/executed_rules = list() - /// Associative list of current players, in order: living players, living antagonists, dead players and observers. - var/list/list/current_players = list(CURRENT_LIVING_PLAYERS, CURRENT_LIVING_ANTAGS, CURRENT_DEAD_PLAYERS, CURRENT_OBSERVERS) - /// 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 - /// When current_players was updated last time. - var/pop_last_updated = 0 /// How many percent of the rounds are more peaceful. var/peaceful_percentage = 50 - /// If a highlander executed. No other highlander rulesets will be run. - var/highlander_executed = FALSE - /// If a only ruleset has been executed. No other rulesets will be run. + /// 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 - /// If the first picked ruleset was a minor ruleset. Minor antagonists will be weighted higher. - var/minor_ruleset_start = FALSE - /// Antags rolled by rules so far, to keep track of and discourage scaling past a certain ratio of crew/antags especially on lowpop. - var/antags_rolled = 0 - // Arbitrary threat addition, for fudging purposes. - var/added_threat = 50 + /// Dynamic configuration, loaded on pre_setup + var/list/configuration = null -/datum/game_mode/dynamic/New() // i have NO IDEA if this is the proper way to do this. - ..() - pop_per_requirement = CONFIG_GET(number/dynamic_pop_per_requirement) - second_rule_req = CONFIG_GET(number_list/dynamic_second_rule_requirements) - third_rule_req = CONFIG_GET(number_list/dynamic_third_rule_requirements) - if(second_rule_req.len<10) - second_rule_req = list(101, 101, 101, 101, 100, 90, 80, 70, 60, 50) - if(third_rule_req.len<10) - third_rule_req = list(101, 101, 101, 101, 101, 100, 90, 80, 70, 60) - high_pop_second_rule_req = CONFIG_GET(number/dynamic_second_rule_high_pop_requirement) - high_pop_third_rule_req = CONFIG_GET(number/dynamic_third_rule_high_pop_requirement) - added_threat = CONFIG_GET(number/dynamic_threat_baseline) - GLOB.dynamic_high_pop_limit = CONFIG_GET(number/dynamic_high_pop_limit) - GLOB.dynamic_latejoin_delay_min = CONFIG_GET(number/dynamic_latejoin_delay_min)*600 - GLOB.dynamic_latejoin_delay_max = CONFIG_GET(number/dynamic_latejoin_delay_max)*600 - GLOB.dynamic_midround_delay_min = CONFIG_GET(number/dynamic_midround_delay_min)*600 - GLOB.dynamic_midround_delay_max = CONFIG_GET(number/dynamic_midround_delay_max)*600 - GLOB.dynamic_first_latejoin_delay_min = CONFIG_GET(number/dynamic_first_latejoin_delay_min)*600 - GLOB.dynamic_first_latejoin_delay_max = CONFIG_GET(number/dynamic_first_latejoin_delay_max)*600 - GLOB.dynamic_first_midround_delay_min = CONFIG_GET(number/dynamic_first_midround_delay_min)*600 - GLOB.dynamic_first_midround_delay_max = CONFIG_GET(number/dynamic_first_midround_delay_max)*600 + /// 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 + + /// The minimum amount of time for antag random events to be hijacked. + var/random_event_hijack_minimum = 10 MINUTES + + /// The maximum amount of time for antag random events to be hijacked. + var/random_event_hijack_maximum = 18 MINUTES + + /// A list of recorded "snapshots" of the round, stored in the dynamic.json log + var/list/datum/dynamic_snapshot/snapshots + + /// The time when the last midround injection was attempted, whether or not it was successful + var/last_midround_injection_attempt = 0 + + /// The amount to inject when a round event is hijacked + var/hijacked_random_event_injection_chance = 50 + + /// Whether or not a random event has been hijacked this midround cycle + var/random_event_hijacked = HIJACKED_NOTHING + + /// The timer ID for the cancellable midround rule injection + var/midround_injection_timer_id + + /// The last drafted midround rulesets (without the current one included). + /// Used for choosing different midround injections. + var/list/current_midround_rulesets + + /// The amount of threat shown on the piece of paper. + /// Can differ from the actual threat amount. + var/shown_threat /datum/game_mode/dynamic/admin_panel() var/list/dat = list("Game Mode Panel

Game Mode Panel

") - dat += "Dynamic Mode \[VV\]\[Refresh\]
" - dat += "Target threat: [threat_level]
" + 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 += "Current threat: [threat] \[Adjust\] \[View Log\]
" + dat += "Midround budget to spend: [mid_round_budget] \[Adjust\] \[View Log\]
" dat += "
" - dat += "Storyteller: [storyteller.name]
" - 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 += "
" + dat += "\[Force Next Latejoin Ruleset\]
" + if (forced_latejoin_rule) + dat += {"-> [forced_latejoin_rule.name] <-
"} + dat += "\[Execute Midround Ruleset\]
" + dat += "
" dat += "Executed rulesets: " if (executed_rules.len > 0) dat += "
" @@ -172,9 +174,9 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) dat += "[DR.ruletype] - [DR.name]
" else dat += "none.
" - dat += "
Injection Timers:
" - dat += "Latejoin: [(latejoin_injection_cooldown-world.time)>60*10 ? "[round((latejoin_injection_cooldown-world.time)/60/10,0.1)] minutes" : "[(latejoin_injection_cooldown-world.time)/10] seconds"] \[Now!\]
" - dat += "Midround: [(midround_injection_cooldown-world.time)>60*10 ? "[round((midround_injection_cooldown-world.time)/60/10,0.1)] minutes" : "[(midround_injection_cooldown-world.time)/10] seconds"] \[Now!\]
" + dat += "
Injection Timers: ([get_injection_chance(dry_run = TRUE)]% latejoin chance, [get_midround_injection_chance(dry_run = TRUE)]% midround chance)
" + dat += "Latejoin: [(latejoin_injection_cooldown-world.time)>60*10 ? "[round((latejoin_injection_cooldown-world.time)/60/10,0.1)] minutes" : "[(latejoin_injection_cooldown-world.time)] seconds"] \[Now!\]
" + dat += "Midround: [(midround_injection_cooldown-world.time)>60*10 ? "[round((midround_injection_cooldown-world.time)/60/10,0.1)] minutes" : "[(midround_injection_cooldown-world.time)] seconds"] \[Now!\]
" usr << browse(dat.Join(), "window=gamemode_panel;size=500x500") /datum/game_mode/dynamic/Topic(href, href_list) @@ -188,92 +190,89 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) 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) return - create_threat(threatadd) + if(threatadd > 0) + create_threat(threatadd) + threat_log += "[worldtime2text()]: [key_name(usr)] increased threat by [threatadd] threat." + else + spend_midround_budget(-threatadd) + threat_log += "[worldtime2text()]: [key_name(usr)] decreased threat by [-threatadd] threat." else if (href_list["injectlate"]) latejoin_injection_cooldown = 0 forced_injection = TRUE - message_admins("[key_name(usr)] forced a latejoin injection.", 1) + message_admins("[key_name(usr)] forced a latejoin injection.") else if (href_list["injectmid"]) midround_injection_cooldown = 0 forced_injection = TRUE - message_admins("[key_name(usr)] forced a midround injection.", 1) + message_admins("[key_name(usr)] forced a midround injection.") else if (href_list["threatlog"]) show_threatlog(usr) else if (href_list["stacking_limit"]) GLOB.dynamic_stacking_limit = input(usr,"Change the threat limit at which round-endings rulesets will start to stack.", "Change stacking limit", null) as num - else if (href_list["change_storyteller"]) - var/list/choices = list() - for(var/T in config.storyteller_cache) - var/datum/dynamic_storyteller/S = T - choices[initial(S.name)] = T - var/selected_storyteller = choices[input("Select storyteller:", "Storyteller", storyteller.name) as null|anything in choices] - storyteller = new selected_storyteller - storyteller.on_start() - message_admins("[key_name(usr)] changed the storyteller to [storyteller].", 1) + else if(href_list["force_latejoin_rule"]) + var/added_rule = input(usr,"What ruleset do you want to force upon the next latejoiner? This will bypass threat level and population restrictions.", "Rigging Latejoin", null) as null|anything in sortNames(init_rulesets(/datum/dynamic_ruleset/latejoin)) + if (!added_rule) + return + forced_latejoin_rule = added_rule + log_admin("[key_name(usr)] set [added_rule] to proc on the next latejoin.") + message_admins("[key_name(usr)] set [added_rule] to proc on the next latejoin.") + else if(href_list["clear_forced_latejoin"]) + forced_latejoin_rule = null + log_admin("[key_name(usr)] cleared the forced latejoin ruleset.") + message_admins("[key_name(usr)] cleared the forced latejoin ruleset.") + else if(href_list["force_midround_rule"]) + var/added_rule = input(usr,"What ruleset do you want to force right now? This will bypass threat level and population restrictions.", "Execute Ruleset", null) as null|anything in sortNames(init_rulesets(/datum/dynamic_ruleset/midround)) + if (!added_rule) + return + log_admin("[key_name(usr)] executed the [added_rule] ruleset.") + message_admins("[key_name(usr)] executed the [added_rule] ruleset.") + picking_specific_rule(added_rule, TRUE) + else if(href_list["cancelmidround"]) + admin_cancel_midround(usr, href_list["cancelmidround"]) + return + else if (href_list["differentmidround"]) + admin_different_midround(usr, href_list["differentmidround"]) + return 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 high impact ruleset if it exists for(var/datum/dynamic_ruleset/rule in executed_rules) - if(rule.flags & HIGHLANDER_RULESET) - if(rule.check_finished()) // Only the rule that actually finished the round sets round result. - return rule.round_result() - // If it got to this part, just pick one highlander 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 ..() -/datum/game_mode/dynamic/generate_report() - return "Mysterious signals that demonstrate strange dynamics have been detected in your sector. Watch out for oddities." - -/datum/game_mode/dynamic/send_intercept() +/datum/game_mode/dynamic/proc/send_intercept() . = "Central Command Status Summary
" - switch(round(threat_level)) - if(-INFINITY to 20) - . += "Peaceful Waypoint
" - . += "Your station orbits deep within controlled, core-sector systems and serves as a waypoint for routine traffic through Nanotrasen's trade empire. Due to the combination of high security, interstellar traffic, and low strategic value, it makes any direct threat of violence unlikely. Your primary enemies will be incompetence and bored crewmen: try to organize team-building events to keep staffers interested and productive. However, even deep in our territory there may be subversive elements, especially for such a high-value target as your station. Keep an eye out, but don't expect much trouble." - set_security_level(SEC_LEVEL_GREEN) - station_goals.len = 0 - for(var/T in subtypesof(/datum/station_goal)) - var/datum/station_goal/G = new T - if(!(G in station_goals)) - station_goals += G - if(21 to 79) - var/perc_green = 100-round(100*((threat_level-21)/(79-21))) - if(prob(perc_green)) + switch(round(shown_threat)) + if(0 to 19) + if(!GLOB.current_living_antags.len) + . += "Peaceful Waypoint
" + . += "Your station orbits deep within controlled, core-sector systems and serves as a waypoint for routine traffic through Nanotrasen's trade empire. Due to the combination of high security, interstellar traffic, and low strategic value, it makes any direct threat of violence unlikely. Your primary enemies will be incompetence and bored crewmen: try to organize team-building events to keep staffers interested and productive." + else . += "Core Territory
" . += "Your station orbits within reliably mundane, secure space. Although Nanotrasen has a firm grip on security in your region, the valuable resources and strategic position aboard your station make it a potential target for infiltrations. Monitor crew for non-loyal behavior, but expect a relatively tame shift free of large-scale destruction. We expect great things from your station." - set_security_level(SEC_LEVEL_GREEN) - station_goals.len = 0 - for(var/T in subtypesof(/datum/station_goal)) - var/datum/station_goal/G = new T - if(!(G in station_goals)) - station_goals += G - else if(prob(perc_green)) - . += "Contested System
" - . += "Your station's orbit passes along the edge of Nanotrasen's sphere of influence. While subversive elements remain the most likely threat against your station, hostile organizations are bolder here, where our grip is weaker. Exercise increased caution against elite Syndicate strike forces, or Executives forbid, some kind of ill-conceived unionizing attempt." - set_security_level(SEC_LEVEL_BLUE) - else - . += "Uncharted Space
" - . += "Congratulations and thank you for participating in the NT 'Frontier' space program! Your station is actively orbiting a high value system far from the nearest support stations. Little is known about your region of space, and the opportunity to encounter the unknown invites greater glory. You are encouraged to elevate security as necessary to protect Nanotrasen assets." - set_security_level(SEC_LEVEL_BLUE) - if(80 to 95) + if(20 to 39) + . += "Anomalous Exogeology
" + . += "Although your station lies within what is generally considered Nanotrasen-controlled space, the course of its orbit has caused it to cross unusually close to exogeological features with anomalous readings. Although these features offer opportunities for our research department, it is known that these little understood readings are often correlated with increased activity from competing interstellar organizations and individuals, among them the Wizard Federation and Cult of the Geometer of Blood - all known competitors for Anomaly Type B sites. Exercise elevated caution." + if(40 to 65) + . += "Contested System
" + . += "Your station's orbit passes along the edge of Nanotrasen's sphere of influence. While subversive elements remain the most likely threat against your station, hostile organizations are bolder here, where our grip is weaker. Exercise increased caution against elite Syndicate strike forces, or Executives forbid, some kind of ill-conceived unionizing attempt." + if(66 to 79) + . += "Uncharted Space
" + . += "Congratulations and thank you for participating in the NT 'Frontier' space program! Your station is actively orbiting a high value system far from the nearest support stations. Little is known about your region of space, and the opportunity to encounter the unknown invites greater glory. You are encouraged to elevate security as necessary to protect Nanotrasen assets." + if(80 to 99) . += "Black Orbit
" . += "As part of a mandatory security protocol, we are required to inform you that as a result of your orbital pattern directly behind an astrological body (oriented from our nearest observatory), your station will be under decreased monitoring and support. It is anticipated that your extreme location and decreased surveillance could pose security risks. Avoid unnecessary risks and attempt to keep your station in one piece." - set_security_level(SEC_LEVEL_AMBER) - if(96 to INFINITY) + if(100) . += "Impending Doom
" . += "Your station is somehow in the middle of hostile territory, in clear view of any enemy of the corporation. Your likelihood to survive is low, and station destruction is expected and almost inevitable. Secure any sensitive material and neutralize any enemy you will come across. It is important that you at least try to maintain the station.
" . += "Good luck." - set_security_level(SEC_LEVEL_RED) if(station_goals.len) . += "
Special Orders for [station_name()]:" @@ -281,341 +280,273 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) G.on_report() . += G.get_report() + if(SSstation.station_traits.len) + . += "
Identified shift divergencies:
" + for(var/i in SSstation.station_traits) + var/datum/station_trait/station_trait_iterator = i + if(!station_trait_iterator.show_in_report) + continue + . += "[station_trait_iterator.get_report()]
" + print_command_report(., "Central Command Status Summary", announce=FALSE) - if(GLOB.security_level >= SEC_LEVEL_BLUE) - priority_announce("A summary has been copied and printed to all communications consoles.", "Security level elevated.", "intercept") - else - priority_announce("Thanks to the tireless efforts of our security and intelligence divisions, there are currently no likely threats to [station_name()]. All station construction projects have been authorized. Have a secure shift!", "Security Report", "commandreport") - -// Yes, this is copy pasted from game_mode -/datum/game_mode/dynamic/check_finished(force_ending) - if(!SSticker.setup_done || !gamemode_ready) - return FALSE - if(replacementmode && round_converted == 2) - return replacementmode.check_finished() - if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME)) - return TRUE - if(station_was_nuked) - return TRUE - if(force_ending) - return TRUE - for(var/datum/dynamic_ruleset/rule in executed_rules) - if(rule.flags & HIGHLANDER_RULESET) - return rule.check_finished() - -/datum/game_mode/dynamic/proc/log_threat(var/log_str,var/verbose = FALSE) - threat_log_verbose += ("[worldtime2text()]: "+log_str) - SSblackbox.record_feedback("tally","dynamic_threat_log",1,log_str) - if(!verbose) - threat_log += log_str + priority_announce("A summary has been copied and printed to all communications consoles.", "Enemy communication intercepted. Security level elevated.", "intercept") + if(GLOB.security_level < SEC_LEVEL_BLUE) + set_security_level(SEC_LEVEL_BLUE) /datum/game_mode/dynamic/proc/show_threatlog(mob/admin) if(!SSticker.HasRoundStarted()) - alert("The round hasn't started yet!") + tgui_alert(usr, "The round hasn't started yet!") return if(!check_rights(R_ADMIN)) return - var/list/out = list("Threat LogThreat Log
Starting Threat: [initial_threat_level]
") + var/list/out = list("Threat LogThreat Log
Starting Threat: [threat_level]
") - for(var/entry in threat_log_verbose) + for(var/entry in threat_log) 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 - SSblackbox.record_feedback("tally","dynamic_threat",threat_level,"Initial threat level") - SSblackbox.record_feedback("tally","dynamic_threat",GLOB.dynamic_curve_centre,"Curve centre") - SSblackbox.record_feedback("tally","dynamic_threat",GLOB.dynamic_curve_width,"Curve width") - SSblackbox.record_feedback("tally","dynamic_threat",peaceful_percentage,"Percent of same-vote rounds that are more peaceful") +/// 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() - if(GLOB.dynamic_forced_storyteller) - GLOB.dynamic_storyteller_type = GLOB.dynamic_forced_storyteller - storyteller = new GLOB.dynamic_storyteller_type // this is where all the initialization happens - storyteller.on_start() - SSblackbox.record_feedback("text","dynamic_storyteller",1,storyteller.name) - message_admins("Dynamic mode parameters for the round:\n\ - Storyteller is [storyteller.name].\n\ - 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"].\n\ - Stacking limit is [GLOB.dynamic_stacking_limit], Classic secret is [GLOB.dynamic_classic_secret ? "Enabled" : "Disabled"], High population limit is [GLOB.dynamic_high_pop_limit].") +/datum/game_mode/dynamic/proc/setup_parameters() 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"], High population limit is [GLOB.dynamic_high_pop_limit].") + 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 - SSblackbox.record_feedback("tally","dynamic_threat",threat_level,"Threat level (forced)") else generate_threat() - - storyteller.start_injection_cooldowns() - log_game("DYNAMIC: Dynamic Mode initialized with a Threat Level of... [threat_level]!") - initial_threat_level = 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/setup_shown_threat() + if (prob(FAKE_REPORT_CHANCE)) + shown_threat = rand(1, 100) + else + shown_threat = clamp(threat_level + rand(REPORT_NEG_DIVERGENCE, REPORT_POS_DIVERGENCE), 0, 100) + +/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() - for (var/rule in subtypesof(/datum/dynamic_ruleset)) - var/datum/dynamic_ruleset/ruleset = new rule() - // Simple check if the ruleset should be added to the lists. - if(ruleset.name == "") - continue - switch(ruleset.ruletype) - if("Minor") - minor_rules += ruleset - if("Roundstart") - roundstart_rules += ruleset - if ("Latejoin") - latejoin_rules += ruleset - if ("Midround") - if (ruleset.weight) - midround_rules += ruleset - for(var/mob/dead/new_player/player in GLOB.player_list) - if(player.ready == PLAYER_READY_TO_PLAY && player.mind) + if(CONFIG_GET(flag/dynamic_config_enabled)) + var/json_file = file("[global.config.directory]/dynamic.json") + if(fexists(json_file)) + configuration = json_decode(file2text(json_file)) + if(configuration["Dynamic"]) + for(var/variable in configuration["Dynamic"]) + if(!(variable in vars)) + stack_trace("Invalid dynamic configuration variable [variable] in game mode variable changes.") + continue + vars[variable] = configuration["Dynamic"][variable] + + setup_parameters() + setup_hijacking() + setup_shown_threat() + setup_rulesets() + + //We do this here instead of with the midround rulesets and such because these rules can hang refs + //To new_player and such, and we want the datums to just free when the roundstart work is done + var/list/roundstart_rules = init_rulesets(/datum/dynamic_ruleset/roundstart) + + for(var/i in GLOB.new_player_list) + var/mob/dead/new_player/player = i + if(player.ready == PLAYER_READY_TO_PLAY && player.mind && player.check_preferences()) roundstart_pop_ready++ candidates.Add(player) log_game("DYNAMIC: Listing [roundstart_rules.len] round start rulesets, and [candidates.len] players ready.") if (candidates.len <= 0) log_game("DYNAMIC: [candidates.len] candidates.") return TRUE - if (roundstart_rules.len <= 0) - log_game("DYNAMIC: [roundstart_rules.len] rules.") - return TRUE - SSblackbox.record_feedback("tally","dynamic",roundstart_rules.len,"Roundstart rules considered") - SSblackbox.record_feedback("tally","dynamic",roundstart_pop_ready,"Players readied up") + if(GLOB.dynamic_forced_roundstart_ruleset.len > 0) rigged_roundstart() else - roundstart() - if(minor_ruleset_start) - log_game("DYNAMIC: Starting a minor ruleset round.") - else - var/starting_rulesets = "" - for (var/datum/dynamic_ruleset/roundstart/DR in executed_rules) - starting_rulesets += "[DR.name], " - log_game("DYNAMIC: Picked the following roundstart rules: [starting_rulesets]") + roundstart(roundstart_rules) + + 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], " + log_game("DYNAMIC: Picked the following roundstart rules: [starting_rulesets]") candidates.Cut() return TRUE /datum/game_mode/dynamic/post_setup(report) - update_playercounts() - if(minor_ruleset_start) - addtimer(CALLBACK(src, /datum/game_mode/dynamic/.proc/minor_roundstart),rand(1 MINUTES,3 MINUTES)) - else - for(var/datum/dynamic_ruleset/roundstart/rule in executed_rules) - addtimer(CALLBACK(src, /datum/game_mode/dynamic/.proc/execute_roundstart_rule, rule), rule.delay) + for(var/datum/dynamic_ruleset/roundstart/rule in executed_rules) + rule.candidates.Cut() // The rule should not use candidates at this point as they all are null. + addtimer(CALLBACK(src, /datum/game_mode/dynamic/.proc/execute_roundstart_rule, rule), rule.delay) + + if (!CONFIG_GET(flag/no_intercept_report)) + addtimer(CALLBACK(src, .proc/send_intercept), rand(waittime_l, waittime_h)) + ..() -/datum/game_mode/dynamic/proc/minor_roundstart() - message_admins("Dynamic beginning minor antag roundstart rolls.") - var/list/potential_minor_rulesets = storyteller.minor_rule_draft() - var/iterations = 0 - var/num_rulesets_executed = 0 - while(threat < threat_level && potential_minor_rulesets.len && (!CHECK_TICK || iterations < 100)) - var/datum/dynamic_ruleset/minor/rule = pickweight(potential_minor_rulesets) - rule.candidates = current_players[CURRENT_LIVING_PLAYERS].Copy() - rule.trim_candidates() - if(!check_blocking(rule.blocking_rules, executed_rules) && rule.ready()) - rule.execute() - executed_rules |= rule - log_threat("[rule.ruletype] - [rule.name] [rule.cost] threat", verbose = TRUE) - num_rulesets_executed++ - else - potential_minor_rulesets -= rule - update_playercounts() - iterations++ - message_admins("Minor antag roundstart rolls completed, with [iterations] rolls done and [num_rulesets_executed] antags or antag teams made.") - log_game("DYNAMIC: Minor antag roundstart made [num_rulesets_executed] antags or antag teams.") +/// Initializes the internal ruleset variables +/datum/game_mode/dynamic/proc/setup_rulesets() + midround_rules = init_rulesets(/datum/dynamic_ruleset/midround) + latejoin_rules = init_rulesets(/datum/dynamic_ruleset/latejoin) +/// Returns a list of the provided rulesets. +/// Configures their variables to match config. +/datum/game_mode/dynamic/proc/init_rulesets(ruleset_subtype) + var/list/rulesets = list() + + for (var/datum/dynamic_ruleset/ruleset_type as anything in subtypesof(ruleset_subtype)) + if (initial(ruleset_type.name) == "") + continue + + if (initial(ruleset_type.weight) == 0) + continue + + var/ruleset = new ruleset_type + configure_ruleset(ruleset) + rulesets += ruleset + + return rulesets /// A simple roundstart proc used when dynamic_forced_roundstart_ruleset has rules in it. /datum/game_mode/dynamic/proc/rigged_roundstart() message_admins("[GLOB.dynamic_forced_roundstart_ruleset.len] rulesets being forced. Will now attempt to draft players for them.") log_game("DYNAMIC: [GLOB.dynamic_forced_roundstart_ruleset.len] rulesets being forced. Will now attempt to draft players for them.") for (var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset) + configure_ruleset(rule) message_admins("Drafting players for forced ruleset [rule.name].") log_game("DYNAMIC: Drafting players for forced ruleset [rule.name].") rule.mode = src - rule.acceptable(roundstart_pop_ready, threat_level) // Assigns some vars in the modes, running it here for consistency + 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.scaling_cost) + scaled_times = round(max(round_start_budget - cost, 0) / rule.scaling_cost) + cost += rule.scaling_cost * scaled_times -/datum/game_mode/dynamic/proc/roundstart() + spend_roundstart_budget(picking_roundstart_rule(rule, scaled_times, forced = TRUE)) + +/datum/game_mode/dynamic/proc/roundstart(list/roundstart_rules) if (GLOB.dynamic_forced_extended) log_game("DYNAMIC: Starting a round of forced extended.") return TRUE - if(prob(storyteller.minor_start_chance())) - minor_ruleset_start = TRUE - message_admins("Dynamic has initialized a minor antag start. Antags will be assigned in 1-5 minutes.") - log_game("DYNAMIC: Minor start initialized.") - return TRUE - var/list/drafted_rules = storyteller.roundstart_draft() - if(!drafted_rules.len) - message_admins("No roundstart antags drafted! Falling back to minor ruleset start.") - log_game("DYNAMIC: No roundstart antags drafted! Falling back to minor ruleset start.") - minor_ruleset_start = TRUE - return FALSE - 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 - if (roundstart_pop_ready > GLOB.dynamic_high_pop_limit) - message_admins("High Population Override is in effect! Threat Level will have more impact on which roles will appear, and player population less.") - log_game("DYNAMIC: High Population Override is in effect! Threat Level will have more impact on which roles will appear, and player population less.") - if (threat_level > high_pop_second_rule_req) - extra_rulesets_amount++ - if (threat_level > high_pop_third_rule_req) - extra_rulesets_amount++ - 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/drafted_rules = list() + for (var/datum/dynamic_ruleset/roundstart/rule in roundstart_rules) + if (!rule.weight) + continue + 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(roundstart_pop_ready) && rule.candidates.len > 0) + drafted_rules[rule] = rule.weight - 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 + var/list/rulesets_picked = list() - if(threat_level >= 50) - message_admins("DYNAMIC: Picking first roundstart ruleset failed. You should report this. Falling back to minor antag start.") - log_game("DYNAMIC: Picking first roundstart ruleset failed. drafted_rules.len = [drafted_rules.len] and threat = [threat]/[threat_level]") - minor_ruleset_start = TRUE - 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) - 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() - if (starting_rule.pre_execute()) - log_threat("[starting_rule.ruletype] - [starting_rule.name] [starting_rule.cost + starting_rule.scaled_times * starting_rule.scaling_cost] threat", verbose = TRUE) - if(starting_rule.flags & HIGHLANDER_RULESET) - highlander_executed = TRUE - else if(starting_rule.flags & ONLY_RULESET) + round_start_budget_left -= cost + + 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)) + + update_log() + +/// 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) var/datum/dynamic_ruleset/rule = sent_rule if(rule.execute()) - current_rules += rule - SSblackbox.record_feedback("associative","dynamic_rulesets",1,rule.get_blackbox_info()) + if(rule.persistent) + current_rules += rule + new_snapshot(rule) return TRUE - rule.clean_up() // Refund threat, delete teams and so on. + rule.clean_up() // Refund threat, delete teams and so on. executed_rules -= rule stack_trace("The starting rule \"[rule.name]\" failed to execute.") return FALSE -/// Picks a random midround OR latejoin rule from the list given as an argument and executes it. -/// Also this could be named better. -/datum/game_mode/dynamic/proc/picking_midround_latejoin_rule(list/drafted_rules = list(), forced = FALSE) - var/datum/dynamic_ruleset/rule = pickweight(drafted_rules) - if(!rule) - return FALSE - - if(!forced) - if(only_ruleset_executed) - return FALSE - // Check if a blocking ruleset has been executed. - else if(check_blocking(rule.blocking_rules, executed_rules)) - drafted_rules -= rule - 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) - if(threat_level > GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking) - if(highlander_executed) - drafted_rules -= rule - if(drafted_rules.len <= 0) - return FALSE - rule = pickweight(drafted_rules) - - if(!rule.repeatable) - if(rule.ruletype == "Latejoin") - latejoin_rules = remove_from_list(latejoin_rules, rule.type) - else if(rule.ruletype == "Midround") - midround_rules = remove_from_list(midround_rules, rule.type) - message_admins("DYNAMIC: Picked [rule]; executing soon...") - addtimer(CALLBACK(src, /datum/game_mode/dynamic/.proc/execute_midround_latejoin_rule, rule), rule.delay) - return TRUE - /// An experimental proc to allow admins to call rules on the fly or have rules call other rules. /datum/game_mode/dynamic/proc/picking_specific_rule(ruletype, forced = FALSE) var/datum/dynamic_ruleset/midround/new_rule if(ispath(ruletype)) new_rule = new ruletype() // You should only use it to call midround rules though. + configure_ruleset(new_rule) // This makes sure the rule is set up properly. else if(istype(ruletype, /datum/dynamic_ruleset)) new_rule = ruletype else @@ -630,67 +561,37 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) // 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) - if(threat_level > GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking) - if(highlander_executed) + // 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(high_impact_ruleset_executed) return FALSE - update_playercounts() - if ((forced || (new_rule.acceptable(current_players[CURRENT_LIVING_PLAYERS].len, threat_level) && new_rule.cost <= threat))) + var/population = GLOB.alive_player_list.len + if((new_rule.acceptable(population, threat_level) && new_rule.cost <= mid_round_budget) || forced) new_rule.trim_candidates() if (new_rule.ready(forced)) - log_threat("[new_rule.ruletype] - [new_rule.name] [new_rule.cost] threat", verbose = TRUE) + spend_midround_budget(new_rule.cost) + threat_log += "[worldtime2text()]: Forced rule [new_rule.name] spent [new_rule.cost]" + 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]!") - SSblackbox.record_feedback("associative","dynamic_rulesets",1,new_rule.get_blackbox_info()) executed_rules += new_rule - current_rules += new_rule + if (new_rule.persistent) + current_rules += new_rule return TRUE else if (forced) - log_game("DYNAMIC: The ruleset [new_rule.name] couldn't be executed due to lack of eligible players.") - return FALSE - -/// 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 - if (rule.execute()) - message_admins("DYNAMIC: Injected a [rule.ruletype == "latejoin" ? "latejoin" : "midround"] ruleset [rule.name].") - log_game("DYNAMIC: Injected a [rule.ruletype == "latejoin" ? "latejoin" : "midround"] ruleset [rule.name].") - log_threat("[rule.ruletype] [rule.name] added [rule.cost]", verbose = TRUE) - if(rule.flags & HIGHLANDER_RULESET) - highlander_executed = TRUE - else if(rule.flags & ONLY_RULESET) - only_ruleset_executed = TRUE - if(rule.ruletype == "Latejoin") - var/mob/M = pick(rule.candidates) - message_admins("[key_name(M)] joined the station, and was selected by the [rule.name] ruleset.") - log_game("DYNAMIC: [key_name(M)] joined the station, and was selected by the [rule.name] ruleset.") - executed_rules += rule - SSblackbox.record_feedback("associative","dynamic_rulesets",1,rule.get_blackbox_info()) - rule.candidates.Cut() - if (rule.persistent) - current_rules += rule - return TRUE - stack_trace("The [rule.ruletype] rule \"[rule.name]\" failed to execute.") + log_game("DYNAMIC: The ruleset [new_rule.name] couldn't be executed due to lack of elligible players.") return FALSE /datum/game_mode/dynamic/process() - if (pop_last_updated < world.time - (120 SECONDS)) - pop_last_updated = world.time - update_playercounts() - for (var/datum/dynamic_ruleset/rule in current_rules) if(rule.rule_process() == RULESET_STOP_PROCESSING) // If rule_process() returns 1 (RULESET_STOP_PROCESSING), stop processing. current_rules -= rule - SSblackbox.record_feedback("tally","dynamic",1,"Rulesets finished") - SSblackbox.record_feedback("associative","dynamic_rulesets_finished",1,rule.get_blackbox_info()) - - storyteller.do_process() if (midround_injection_cooldown < world.time) if (GLOB.dynamic_forced_extended) @@ -698,51 +599,71 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) // 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. - midround_injection_cooldown = storyteller.get_midround_cooldown() + 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 return - if(SSshuttle.endvote_passed) // no rules after shuttle call is voted - return + message_admins("DYNAMIC: Checking for midround injection.") log_game("DYNAMIC: Checking for midround injection.") - update_playercounts() - if (storyteller.should_inject_antag()) - SSblackbox.record_feedback("tally","dynamic",1,"Attempted midround injections") - do_midround_injection() + last_midround_injection_attempt = world.time -/datum/game_mode/dynamic/proc/do_midround_injection() - set waitfor = FALSE - var/list/drafted_rules = storyteller.midround_draft() - if (drafted_rules.len > 0) - SSblackbox.record_feedback("tally","dynamic",1,"Successful midround injections") - picking_midround_latejoin_rule(drafted_rules) - - -/// Updates current_players. -/datum/game_mode/dynamic/proc/update_playercounts() - current_players[CURRENT_LIVING_PLAYERS] = list() - current_players[CURRENT_LIVING_ANTAGS] = list() - current_players[CURRENT_DEAD_PLAYERS] = list() - current_players[CURRENT_OBSERVERS] = list() - for (var/mob/M in GLOB.player_list) - if (istype(M, /mob/dead/new_player)) - continue - if (M.stat != DEAD) - current_players[CURRENT_LIVING_PLAYERS].Add(M) - if (M.mind && (M.mind.special_role || M.mind.antag_datums?.len > 0)) - current_players[CURRENT_LIVING_ANTAGS].Add(M) - else - if (isobserver(M)) - var/mob/dead/observer/O = M - if (O.started_as_observer) // Observers - current_players[CURRENT_OBSERVERS].Add(M) + if (prob(get_midround_injection_chance())) + var/list/drafted_rules = list() + for (var/datum/dynamic_ruleset/midround/rule in midround_rules) + if (!rule.weight) continue - if(!M.voluntary_ghosted) - current_players[CURRENT_DEAD_PLAYERS].Add(M) // Players who actually died (and admins who ghosted, would be nice to avoid counting them somehow) - threat = (SSactivity.current_threat * 0.6 + SSactivity.get_max_threat() * 0.2 + SSactivity.get_average_threat() * 0.2) + added_threat + if (rule.acceptable(GLOB.alive_player_list.len, threat_level) && mid_round_budget >= rule.cost) + rule.trim_candidates() + if (rule.ready()) + drafted_rules[rule] = rule.get_weight() + if (drafted_rules.len > 0) + pick_midround_rule(drafted_rules) + else if (random_event_hijacked == HIJACKED_TOO_SOON) + log_game("DYNAMIC: Midround injection failed when random event was hijacked. Spawning another random event in its place.") + + // A random event antag would have rolled had this injection check passed. + // As a refund, spawn a non-ghost-role random event. + SSevents.spawnEvent() + SSevents.reschedule() + + random_event_hijacked = HIJACKED_NOTHING + +/// Gets the chance for latejoin injection, the dry_run argument is only used for forced injection. +/datum/game_mode/dynamic/proc/get_injection_chance(dry_run = FALSE) + if(forced_injection) + forced_injection = dry_run + return 100 + var/chance = 0 + var/max_pop_per_antag = max(5,15 - round(threat_level/10) - round(GLOB.alive_player_list.len/5)) + if (!GLOB.current_living_antags.len) + chance += 50 // No antags at all? let's boost those odds! + else + var/current_pop_per_antag = GLOB.alive_player_list.len / GLOB.current_living_antags.len + if (current_pop_per_antag > max_pop_per_antag) + chance += min(50, 25+10*(current_pop_per_antag-max_pop_per_antag)) + else + chance += 25-10*(max_pop_per_antag-current_pop_per_antag) + if (GLOB.dead_player_list.len > GLOB.alive_player_list.len) + chance -= 30 // More than half the crew died? ew, let's calm down on antags + if (mid_round_budget > higher_injection_chance_minimum_threat) + chance += higher_injection_chance + if (mid_round_budget < lower_injection_chance_minimum_threat) + chance -= lower_injection_chance + return round(max(0,chance)) + +/// Gets the chance for midround injection, the dry_run argument is only used for forced injection. +/// Usually defers to the latejoin injection chance. +/datum/game_mode/dynamic/proc/get_midround_injection_chance(dry_run) + var/chance = get_injection_chance(dry_run) + + if (random_event_hijacked != HIJACKED_NOTHING) + chance += hijacked_random_event_injection_chance + + return chance /// Removes type from the list /datum/game_mode/dynamic/proc/remove_from_list(list/type_list, type) @@ -755,63 +676,82 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) /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 -/// Checks if client age is age or older. -/datum/game_mode/dynamic/proc/check_age(client/C, age) - enemy_minimum_age = age - if(get_remaining_days(C) == 0) - enemy_minimum_age = initial(enemy_minimum_age) - return TRUE // Available in 0 days = available right now = player is old enough to play. - enemy_minimum_age = initial(enemy_minimum_age) - return FALSE - /datum/game_mode/dynamic/make_antag_chance(mob/living/carbon/human/newPlayer) if (GLOB.dynamic_forced_extended) return if(EMERGENCY_ESCAPED_OR_ENDGAMED) // No more rules after the shuttle has left return - if(SSshuttle.endvote_passed) // no rules after shuttle is auto-called - return - update_playercounts() if (forced_latejoin_rule) forced_latejoin_rule.candidates = list(newPlayer) forced_latejoin_rule.trim_candidates() log_game("DYNAMIC: Forcing ruleset [forced_latejoin_rule]") if (forced_latejoin_rule.ready(TRUE)) - picking_midround_latejoin_rule(list(forced_latejoin_rule), forced = TRUE) + if (!forced_latejoin_rule.repeatable) + latejoin_rules = remove_from_list(latejoin_rules, forced_latejoin_rule.type) + addtimer(CALLBACK(src, /datum/game_mode/dynamic/.proc/execute_midround_latejoin_rule, forced_latejoin_rule), forced_latejoin_rule.delay) forced_latejoin_rule = null - else if (storyteller.should_inject_antag()) - SSblackbox.record_feedback("tally","dynamic",1,"Attempted latejoin injections") - var/list/drafted_rules = storyteller.latejoin_draft(newPlayer) - if (drafted_rules.len > 0 && picking_midround_latejoin_rule(drafted_rules)) - SSblackbox.record_feedback("tally","dynamic",1,"Successful latejoin injections") + else if (latejoin_injection_cooldown < world.time && prob(get_injection_chance())) + var/list/drafted_rules = list() + for (var/datum/dynamic_ruleset/latejoin/rule in latejoin_rules) + if (!rule.weight) + continue + if (rule.acceptable(GLOB.alive_player_list.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 & HIGH_IMPACT_RULESET && high_impact_ruleset_executed) + continue -/// Increase the threat level. + rule.candidates = list(newPlayer) + rule.trim_candidates() + if (rule.ready()) + drafted_rules[rule] = rule.get_weight() + + 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 + +/// Apply configurations to rule. +/datum/game_mode/dynamic/proc/configure_ruleset(datum/dynamic_ruleset/ruleset) + var/rule_conf = LAZYACCESSASSOC(configuration, ruleset.ruletype, ruleset.name) + for(var/variable in rule_conf) + if(!(variable in ruleset.vars)) + stack_trace("Invalid dynamic configuration variable [variable] in [ruleset.ruletype] [ruleset.name].") + continue + ruleset.vars[variable] = rule_conf[variable] + if(CONFIG_GET(flag/protect_roles_from_antagonist)) + ruleset.restricted_roles |= ruleset.protected_roles + if(CONFIG_GET(flag/protect_assistant_from_antagonist)) + ruleset.restricted_roles |= "Assistant" + +/// Refund threat, but no more than threat_level. +/datum/game_mode/dynamic/proc/refund_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_level += gain - SSblackbox.record_feedback("tally","dynamic_threat",gain,"Created threat level") - log_threat("[gain] created. Threat level is now [threat_level].", verbose = TRUE) + mid_round_budget = min(100, mid_round_budget + gain) + if(mid_round_budget > threat_level) + threat_level = mid_round_budget -/// Decrease the threat level. -/datum/game_mode/dynamic/proc/remove_threat(loss) - threat_level -= loss - SSblackbox.record_feedback("tally","dynamic_threat",loss,"Removed threat level") - log_threat("[loss] removed. Threat level is now [threat_level].", verbose = TRUE) +/// 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) -/// Fill up more of the threat level. -/datum/game_mode/dynamic/proc/spend_threat(cost) - added_threat += cost - SSblackbox.record_feedback("tally","dynamic_threat",cost,"Threat added") - log_threat("[cost] added. Threat is now [threat].", verbose = TRUE) +/// 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 threat value between 0 and 100. -/datum/game_mode/dynamic/proc/lorentz_to_threat(x) +/// 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) @@ -834,10 +774,11 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) if (20 to INFINITY) return rand(90, 100) -/datum/game_mode/dynamic/ghost_info() - . = list() - . += "Current threat: [threat]" - . += "Target threat: [threat_level]" - . += "Storyteller: [storyteller.name]
" - . += "Parameters: centre = [GLOB.dynamic_curve_centre] ; width = [GLOB.dynamic_curve_width].
" - . += "On average, [peaceful_percentage]% of the rounds are more peaceful.
" +/// Log to messages and to the game +/datum/game_mode/dynamic/proc/dynamic_log(text) + message_admins("DYNAMIC: [text]") + log_game("DYNAMIC: [text]") + +#undef FAKE_REPORT_CHANCE +#undef REPORT_NEG_DIVERGENCE +#undef REPORT_POS_DIVERGENCE diff --git a/code/game/gamemodes/dynamic/dynamic_logging.dm b/code/game/gamemodes/dynamic/dynamic_logging.dm new file mode 100644 index 0000000000..7490894b52 --- /dev/null +++ b/code/game/gamemodes/dynamic/dynamic_logging.dm @@ -0,0 +1,101 @@ +/// 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 + +/// Updates the log for the current snapshots. +/datum/game_mode/dynamic/proc/update_log() + 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 + serialized["shown_threat"] = shown_threat + + var/list/serialized_snapshots = list() + for (var/datum/dynamic_snapshot/snapshot as anything in snapshots) + serialized_snapshots += list(snapshot.to_list()) + serialized["snapshots"] = serialized_snapshots + + rustg_file_write(json_encode(serialized), "[GLOB.log_directory]/dynamic.json") + +/// 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 = GLOB.alive_player_list.len + new_snapshot.dead_players = GLOB.dead_player_list.len + new_snapshot.observers = GLOB.current_observers_list.len + new_snapshot.total_players = new_snapshot.alive_players + new_snapshot.dead_players + new_snapshot.observers + new_snapshot.alive_antags = GLOB.current_living_antags.len + new_snapshot.ruleset_chosen = new /datum/dynamic_snapshot_ruleset(ruleset_chosen) + + LAZYADD(snapshots, new_snapshot) + + update_log() diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm index f1e48eb31c..08aedc2e7b 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm @@ -1,16 +1,13 @@ -#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 /datum/dynamic_ruleset /// For admin logging and round end screen. + // If you want to change this variable name, the force latejoin/midround rulesets + // to not use sortNames. var/name = "" /// For admin logging and round end screen, do not change this unless making a new rule type. var/ruletype = "" - /// For config purposes, similar to config_tag for secret game modes. - var/config_tag = null /// If set to TRUE, the rule won't be discarded after being executed, and dynamic will call rule_process() every time it ticks. var/persistent = FALSE /// If set to TRUE, dynamic mode will be able to draft this ruleset again later on. (doesn't apply for roundstart rules) @@ -31,7 +28,7 @@ var/list/protected_roles = list() /// If set, rule will deny candidates from those roles always. var/list/restricted_roles = list() - /// If set, rule will only accept candidates from those roles, IMPORTANT: DOES NOT WORK ON ROUNDSTART RULESETS. + /// If set, rule will only accept candidates from those roles. If on a roundstart ruleset, requires the player to have the correct antag pref enabled and any of the possible roles enabled. var/list/exclusive_roles = list() /// If set, there needs to be a certain amount of players doing those roles (among the players who won't be drafted) for the rule to be drafted IMPORTANT: DOES NOT WORK ON ROUNDSTART RULESETS. var/list/enemy_roles = list() @@ -39,7 +36,7 @@ var/required_enemies = list(1,1,0,0,0,0,0,0,0,0) /// The rule needs this many candidates (post-trimming) to be executed (example: Cult needs 4 players at round start) var/required_candidates = 0 - /// 1 -> 9, probability for this rule to be picked against other rules + /// 0 -> 9, probability for this rule to be picked against other rules. If zero this will effectively disable the rule. var/weight = 5 /// Threat cost for this rule, this is decreased from the mode's threat when the rule is executed. var/cost = 0 @@ -49,21 +46,13 @@ var/scaled_times = 0 /// Used for the roundend report var/total_cost = 0 - /// A flag that determines how the ruleset is handled - /// ONLY_RULESET are rulesets that prevent ALL other rulesets from rolling. - /// HIGHLANDER_RULESET are rulesets can end the round. - /// TRAITOR_RULESET are the "default" ruleset--they should always be addable to a round, if the round type allows antags and dynamic thinks there should be another. - /// MINOR_RULESET is for rulesets whose antags can have multiple instances without causing too much issue. As roundstarts, they have their weights reduced based on the storyteller's minor-antag-round chance. - /// FAKE_ANTAG_RULESET is for rulesets whose antags aren't actually antagonistic--essentially just flavor meant to spice the round up. - /// ALWAYS_MAX_WEIGHT_RULESET means that the ruleset doesn't have its weight reduced based on recency. - var/flags = 0 + /// 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 /// Requirements are the threat level requirements per pop range. /// With the default values, The rule will never get drafted below 10 threat level (aka: "peaceful extended"), and it requires a higher threat level at lower pops. var/list/requirements = list(40,30,20,10,10,10,10,10,10,10) - /// An alternative, static requirement used instead when pop is over mode's high_pop_limit. - var/high_population_requirement = 10 /// Reference to the mode, use this instead of SSticker.mode. var/datum/game_mode/dynamic/mode = null /// If a role is to be considered another for the purpose of banning. @@ -77,53 +66,31 @@ 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 - /// List of tags for use in storytellers. - var/list/property_weights = list() - /// Weight reduction by recent-rounds. Saved on new. - var/weight_mult = 1 + + /// 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() + // Rulesets can be instantiated more than once, such as when an admin clicks + // "Execute Midround Ruleset". Thus, it would be wrong to perform any + // side effects here. Dynamic rulesets should be stateless anyway. + SHOULD_NOT_OVERRIDE(TRUE) + + mode = SSticker.mode + ..() - if(CONFIG_GET(flag/protect_roles_from_antagonist)) - restricted_roles += protected_roles - if(CONFIG_GET(flag/protect_assistant_from_antagonist)) - restricted_roles += "Assistant" - var/weights = CONFIG_GET(keyed_list/dynamic_weight) - var/costs = CONFIG_GET(keyed_list/dynamic_cost) - var/requirementses = CONFIG_GET(keyed_list/dynamic_requirements) // can't damn well use requirements - var/high_population_requirements = CONFIG_GET(keyed_list/dynamic_high_population_requirement) - var/list/repeated_mode_adjust = CONFIG_GET(number_list/repeated_mode_adjust) - if(config_tag in weights) - if(!(flags & ALWAYS_MAX_WEIGHT_RULESET) && SSpersistence.saved_dynamic_rules.len == 3 && repeated_mode_adjust.len == 3) - var/saved_dynamic_rules = SSpersistence.saved_dynamic_rules - for(var/i in 1 to 3) - if(config_tag in saved_dynamic_rules[i]) - weight_mult -= (repeated_mode_adjust[i]/100) - weight_mult = max(0,weight_mult) - if(config_tag in costs) - cost = costs[config_tag] - if(config_tag in requirementses) - requirements = requirementses[config_tag] - if(config_tag in high_population_requirements) - high_population_requirement = high_population_requirements[config_tag] - if (istype(SSticker.mode, /datum/game_mode/dynamic)) - mode = SSticker.mode - else if (GLOB.master_mode != "dynamic") // This is here to make roundstart forced ruleset function. - qdel(src) /datum/dynamic_ruleset/roundstart // One or more of those drafted at roundstart ruletype = "Roundstart" -/datum/dynamic_ruleset/minor // drafted at roundstart in minor rounds, one antag at a time, for a "mixed" round - ruletype = "Minor" - // Can be drafted when a player joins the server /datum/dynamic_ruleset/latejoin ruletype = "Latejoin" @@ -131,55 +98,45 @@ /// By default, a rule is acceptable if it satisfies the threat level/population requirements. /// 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 + indice_pop = min(requirements.len,round(population/pop_per_requirement)+1) + if(minimum_players > population) - SSblackbox.record_feedback("tally","dynamic",1,"Times rulesets rejected due to low pop") return FALSE if(maximum_players > 0 && population > maximum_players) - SSblackbox.record_feedback("tally","dynamic",1,"Times rulesets rejected due to high pop") return FALSE - if (population >= GLOB.dynamic_high_pop_limit) - indice_pop = 10 - if(threat_level < high_population_requirement) - SSblackbox.record_feedback("tally","dynamic",1,"Times rulesets rejected due to not enough threat level") - log_game("DYNAMIC: [name] did not reach threat level threshold: [threat_level]/[high_population_requirement]") - return FALSE - else - return TRUE - else - 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(threat_level < requirements[indice_pop]) - SSblackbox.record_feedback("tally","dynamic",1,"Times rulesets rejected due to not enough threat level") - log_game("DYNAMIC: [name] did not reach threat level threshold: [threat_level]/[requirements[indice_pop]]") - return FALSE - else - return TRUE + 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 amount of scaled steps. -/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/new_prob - var/pop_to_antags = (mode.antags_rolled + (antag_cap[indice_pop] * (scaled_times + 1))) / mode.roundstart_pop_ready - 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 && pop_to_antags < 0.25) - new_prob = base_prob + (remaining_threat_level) - (scaled_times * scaling_cost) - (extra_rulesets * EXTRA_RULESET_PENALTY) - (pop_to_antags * POP_SCALING_PENALTY) - if (!prob(new_prob)) - break - remaining_threat_level -= scaling_cost - scaled_times++ - pop_to_antags = (mode.antags_rolled + (antag_cap[indice_pop] * (scaled_times + 1))) / mode.roundstart_pop_ready - log_game("DYNAMIC: [name] roundstart ruleset failed scaling up at [new_prob ? new_prob : 0]% chance after [scaled_times]/3 successful scaleups. [remaining_threat_level] threat remaining, antag to crew ratio: [pop_to_antags*100]%.") - mode.antags_rolled += (1 + scaled_times) * antag_cap[indice_pop] - return scaled_times * scaling_cost +/// 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 + + 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 + + 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() - return TRUE + return /// Called on game mode pre_setup for roundstart rulesets. /// Do everything you need to do before job is assigned here. @@ -199,14 +156,14 @@ /// IMPORTANT: If ready() returns TRUE, that means pre_execute() or execute() should never fail! /datum/dynamic_ruleset/proc/ready(forced = 0) if (required_candidates > candidates.len) - SSblackbox.record_feedback("tally","dynamic",1,"Times rulesets rejected due to not enough candidates") return FALSE return TRUE /// Runs from gamemode process() if ruleset fails to start, like delayed rulesets not getting valid candidates. /// This one only handles refunding the threat, override in ruleset to clean up the rest. /datum/dynamic_ruleset/proc/clean_up() - return + mode.refund_threat(cost + (scaled_times * scaling_cost)) + mode.threat_log += "[worldtime2text()]: [ruletype] [name] refunded [cost + (scaled_times * scaling_cost)]. Failed to execute." /// Gets weight of the ruleset /// Note that this decreases weight if repeatable is TRUE and repeatable_weight_decrease is higher than 0 @@ -225,32 +182,9 @@ 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() -/// Checks if round is finished, return true to end the round. -/// Only called if ruleset is flagged as HIGHLANDER_RULESET -/datum/dynamic_ruleset/proc/check_finished() - return FALSE - -/// Returns a list to be displayed on statbus. -/datum/dynamic_ruleset/proc/get_blackbox_info() - var/list/ruleset_data = list() - ruleset_data["name"] = name - ruleset_data["rule_type"] = ruletype - ruleset_data["cost"] = total_cost - ruleset_data["weight"] = weight - ruleset_data["scaled_times"] = scaled_times - ruleset_data["antagonist_type"] = antag_datum - ruleset_data["population_tier"] = indice_pop - ruleset_data["assigned"] = list() - for (var/datum/mind/M in assigned) - var/assigned_data = list() - assigned_data["key"] = M.key - assigned_data["name"] = M.name - ruleset_data["assigned"] += list(assigned_data) - return ruleset_data - ////////////////////////////////////////////// // // // ROUNDSTART RULESETS // @@ -259,26 +193,45 @@ /// 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? - candidates.Remove(P) + for(var/mob/dead/new_player/candidate_player in candidates) + var/client/candidate_client = GET_CLIENT(candidate_player) + if (!candidate_client || !candidate_player.mind) // Are they connected? + candidates.Remove(candidate_player) continue - if(!mode.check_age(P.client, minimum_required_age)) - candidates.Remove(P) + + if(candidate_client.check_age(minimum_required_age) > 0) + candidates.Remove(candidate_player) continue - if(P.mind.special_role) // We really don't want to give antag to an antag. - candidates.Remove(P) + + if(candidate_player.mind.special_role) // We really don't want to give antag to an antag. + candidates.Remove(candidate_player) continue + if(antag_flag_override) - if(!(antag_flag_override in P.client.prefs.be_special) || jobban_isbanned(P.ckey, antag_flag_override)) - candidates.Remove(P) + if(!(antag_flag_override in candidate_client.prefs.be_special) || jobban_isbanned(candidate_player.ckey, list(antag_flag_override, ROLE_SYNDICATE))) + candidates.Remove(candidate_player) continue else - if(!(antag_flag in P.client.prefs.be_special) || jobban_isbanned(P.ckey, antag_flag)) - candidates.Remove(P) + if(!(antag_flag in candidate_client.prefs.be_special) || jobban_isbanned(candidate_player.ckey, list(antag_flag, ROLE_SYNDICATE))) + candidates.Remove(candidate_player) continue + // If this ruleset has exclusive_roles set, we want to only consider players who have those + // job prefs enabled and are eligible to play that job. Otherwise, continue as before. + if(length(exclusive_roles)) + var/exclusive_candidate = FALSE + for(var/role in exclusive_roles) + var/datum/job/job = SSjob.GetJob(role) + if((role in candidate_client.prefs.job_preferences) && !jobban_isbanned(candidate_player.ckey, role) && !job.required_playtime_remaining(candidate_client)) + exclusive_candidate = TRUE + break + + // If they didn't have any of the required job prefs enabled or were banned from all enabled prefs, + // they're not eligible for this antag type. + if(!exclusive_candidate) + candidates.Remove(candidate_player) + /// 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 7842c6e0f7..548686c77d 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm @@ -6,39 +6,33 @@ /datum/dynamic_ruleset/latejoin/trim_candidates() for(var/mob/P in candidates) - if (!P.client || !P.mind || !P.mind.assigned_role) // Are they connected? + if(!P.client || !P.mind || !P.mind.assigned_role) // Are they connected? candidates.Remove(P) - continue - if(!mode.check_age(P.client, minimum_required_age)) + else if (!mode.check_age(P.client, minimum_required_age)) candidates.Remove(P) - continue - if(antag_flag_override) - if(!(antag_flag_override in P.client.prefs.be_special) || jobban_isbanned(P.ckey, list(antag_flag_override))) + else if(P.mind.assigned_role in restricted_roles) // Does their job allow for it? + candidates.Remove(P) + else if((exclusive_roles.len > 0) && !(P.mind.assigned_role in exclusive_roles)) // Is the rule exclusive to their job? + candidates.Remove(P) + else if(antag_flag_override) + if(!(antag_flag_override in P.client.prefs.be_special) || jobban_isbanned(P.ckey, list(antag_flag_override, ROLE_SYNDICATE))) candidates.Remove(P) - continue else if(!(antag_flag in P.client.prefs.be_special) || jobban_isbanned(P.ckey, list(antag_flag, ROLE_SYNDICATE))) candidates.Remove(P) - continue - if (P.mind.assigned_role in restricted_roles) // Does their job allow for it? - candidates.Remove(P) - continue - if ((exclusive_roles.len > 0) && !(P.mind.assigned_role in exclusive_roles)) // Is the rule exclusive to their job? - candidates.Remove(P) - continue /datum/dynamic_ruleset/latejoin/ready(forced = 0) if (!forced) var/job_check = 0 if (enemy_roles.len > 0) - for (var/mob/M in mode.current_players[CURRENT_LIVING_PLAYERS]) + for (var/mob/M in GLOB.alive_player_list) if (M.stat == DEAD) continue // Dead players cannot count as opponents - if (M.mind && M.mind.assigned_role && (M.mind.assigned_role in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role in restricted_roles))) + if (M.mind && (M.mind.assigned_role in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role in restricted_roles))) job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it + var/threat = round(mode.threat_level/10) if (job_check < required_enemies[threat]) - SSblackbox.record_feedback("tally","dynamic",1,"Times rulesets rejected due to not enough enemy roles") return FALSE return ..() @@ -47,8 +41,6 @@ assigned += M.mind M.mind.special_role = antag_flag M.mind.add_antag_datum(antag_datum) - log_admin("[M.name] was made into a [name] by dynamic.") - message_admins("[M.name] was made into a [name] by dynamic.") return TRUE ////////////////////////////////////////////// @@ -59,19 +51,15 @@ /datum/dynamic_ruleset/latejoin/infiltrator name = "Syndicate Infiltrator" - config_tag = "latejoin_traitor" antag_datum = /datum/antagonist/traitor antag_flag = ROLE_TRAITOR - restricted_roles = list("AI", "Cyborg") - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + protected_roles = list("Security Officer", "Warden", "Head of Personnel", "Detective", "Head of Security", "Captain") + restricted_roles = list("AI","Cyborg") required_candidates = 1 weight = 7 cost = 5 - requirements = list(40,30,20,15,15,15,15,15,15,15) - high_population_requirement = 15 + requirements = list(40,30,20,10,10,10,10,10,10,10) repeatable = TRUE - flags = TRAITOR_RULESET | MINOR_RULESET | ALWAYS_MAX_WEIGHT_RULESET - property_weights = list("story_potential" = 2, "trust" = -1, "extended" = 1) ////////////////////////////////////////////// // // @@ -82,23 +70,23 @@ /datum/dynamic_ruleset/latejoin/provocateur name = "Provocateur" persistent = TRUE - config_tag = "latejoin_revolution" antag_datum = /datum/antagonist/rev/head antag_flag = ROLE_REV_HEAD antag_flag_override = ROLE_REV - restricted_roles = list("AI", "Cyborg", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + restricted_roles = list("AI", "Cyborg", "Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director") enemy_roles = list("AI", "Cyborg", "Security Officer","Detective","Head of Security", "Captain", "Warden") - required_enemies = list(4,4,3,3,3,3,3,2,2,1) + required_enemies = list(2,2,1,1,1,1,1,0,0,0) required_candidates = 1 weight = 2 - delay = 1 MINUTES // Prevents rule start while head is offstation. + delay = 1 MINUTES // Prevents rule start while head is offstation. cost = 20 - requirements = list(101,101,70,40,40,40,40,40,40,40) - high_population_requirement = 40 - flags = HIGHLANDER_RULESET - property_weights = list("trust" = -2, "chaos" = 2, "extended" = -2, "valid" = 2, "conversion" = 1) + requirements = list(101,101,70,40,30,20,20,20,20,20) + flags = HIGH_IMPACT_RULESET + blocking_rules = list(/datum/dynamic_ruleset/roundstart/revs) var/required_heads_of_staff = 3 var/finished = FALSE + /// How much threat should be injected when the revolution wins? + var/revs_win_threat_injection = 20 var/datum/team/revolution/revolution /datum/dynamic_ruleset/latejoin/provocateur/ready(forced=FALSE) @@ -107,14 +95,14 @@ if(!..()) return FALSE var/head_check = 0 - for(var/mob/player in mode.current_players[CURRENT_LIVING_PLAYERS]) + for(var/mob/player in GLOB.alive_player_list) if (player.mind.assigned_role in GLOB.command_positions) head_check++ return (head_check >= required_heads_of_staff) /datum/dynamic_ruleset/latejoin/provocateur/execute() - var/mob/M = pick(candidates) // This should contain a single player, but in case. - if(check_eligible(M.mind)) // Didnt die/run off z-level/get implanted since leaving shuttle. + var/mob/M = pick(candidates) // This should contain a single player, but in case. + if(check_eligible(M.mind)) // Didnt die/run off z-level/get implanted since leaving shuttle. assigned += M.mind M.mind.special_role = antag_flag revolution = new() @@ -125,9 +113,7 @@ new_head = M.mind.add_antag_datum(new_head, revolution) revolution.update_objectives() revolution.update_heads() - SSshuttle.registerHostileEnvironment(src) - log_admin("[M.name] was made into a revolutionary by dynamic.") - message_admins("[M.name] was made into a revolutionary by dynamic.") + SSshuttle.registerHostileEnvironment(revolution) return TRUE else log_game("DYNAMIC: [ruletype] [name] discarded [M.name] from head revolutionary due to ineligibility.") @@ -135,153 +121,37 @@ return FALSE /datum/dynamic_ruleset/latejoin/provocateur/rule_process() - if(check_rev_victory()) - finished = REVOLUTION_VICTORY - return RULESET_STOP_PROCESSING - else if (check_heads_victory()) - finished = STATION_VICTORY - SSshuttle.clearHostileEnvironment(src) - priority_announce("It appears the mutiny has been quelled. Please return yourself and your colleagues to work. \ - We have remotely blacklisted the head revolutionaries from your cloning software to prevent accidental cloning.", null, "attention", null, "Central Command Loyalty Monitoring Division") - for(var/datum/mind/M in revolution.members) // Remove antag datums and prevent headrev cloning then restarting rebellions. - if(M.has_antag_datum(/datum/antagonist/rev/head)) - var/datum/antagonist/rev/head/R = M.has_antag_datum(/datum/antagonist/rev/head) - R.remove_revolutionary(FALSE, "gamemode") - var/mob/living/carbon/C = M.current - if(C.stat == DEAD) - C.makeUncloneable() - if(M.has_antag_datum(/datum/antagonist/rev)) - var/datum/antagonist/rev/R = M.has_antag_datum(/datum/antagonist/rev) - R.remove_revolutionary(FALSE, "gamemode") - return RULESET_STOP_PROCESSING + var/winner = revolution.process_victory(revs_win_threat_injection) + if (isnull(winner)) + return + + finished = winner + return RULESET_STOP_PROCESSING /// Checks for revhead loss conditions and other antag datums. -/datum/dynamic_ruleset/latejoin/provocateur/proc/check_eligible(var/datum/mind/M) +/datum/dynamic_ruleset/latejoin/provocateur/proc/check_eligible(datum/mind/M) var/turf/T = get_turf(M.current) if(!considered_afk(M) && considered_alive(M) && is_station_level(T.z) && !M.antag_datums?.len && !HAS_TRAIT(M, TRAIT_MINDSHIELD)) return TRUE return FALSE -/datum/dynamic_ruleset/latejoin/provocateur/check_finished() - if(finished == REVOLUTION_VICTORY) - return TRUE - else - return ..() - -/datum/dynamic_ruleset/latejoin/provocateur/proc/check_rev_victory() - for(var/datum/objective/mutiny/objective in revolution.objectives) - if(!(objective.check_completion())) - return FALSE - return TRUE - -/datum/dynamic_ruleset/latejoin/provocateur/proc/check_heads_victory() - for(var/datum/mind/rev_mind in revolution.head_revolutionaries()) - var/turf/T = get_turf(rev_mind.current) - if(!considered_afk(rev_mind) && considered_alive(rev_mind) && is_station_level(T.z)) - if(ishuman(rev_mind.current) || ismonkey(rev_mind.current)) - return FALSE - return TRUE - /datum/dynamic_ruleset/latejoin/provocateur/round_result() - if(finished == REVOLUTION_VICTORY) - SSticker.mode_result = "win - heads killed" - SSticker.news_report = REVS_WIN - else if(finished == STATION_VICTORY) - SSticker.mode_result = "loss - rev heads killed" - SSticker.news_report = REVS_LOSE + revolution.round_result(finished) ////////////////////////////////////////////// // // -// HERETIC SMUGGLER // +// HERETIC SMUGGLER // // // ////////////////////////////////////////////// /datum/dynamic_ruleset/latejoin/heretic_smuggler name = "Heretic Smuggler" antag_datum = /datum/antagonist/heretic - antag_flag = "latejoin_heretic" - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + antag_flag = ROLE_HERETIC + protected_roles = list("Security Officer", "Warden", "Head of Personnel", "Detective", "Head of Security", "Captain","Prisoner") restricted_roles = list("AI","Cyborg") required_candidates = 1 weight = 4 - cost = 25 - requirements = list(60,60,60,55,50,50,50,50,50,50) - flags = MINOR_RULESET - high_population_requirement = 50 - property_weights = list("story_potential" = 1, "trust" = -1, "chaos" = 2, "extended" = -1, "valid" = 2) - repeatable = TRUE - -////////////////////////////////////////////// -// // -// BLOODSUCKERS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/latejoin/bloodsucker - name = "Bloodsucker Infiltrator" - config_tag = "latejoin_bloodsucker" - antag_datum = ANTAG_DATUM_BLOODSUCKER - antag_flag = ROLE_TRAITOR - restricted_roles = list("AI", "Cyborg") - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") - required_candidates = 1 - weight = 3 cost = 10 - property_weights = list("story_potential" = 2, "extended" = 2, "trust" = -2, "valid" = 1) - requirements = list(70,65,60,55,50,45,40,35,30,30) - flags = MINOR_RULESET - high_population_requirement = 30 + requirements = list(101,101,101,10,10,10,10,10,10,10) repeatable = TRUE - -/datum/dynamic_ruleset/latejoin/bloodsucker/execute() - var/mob/M = pick(candidates) - assigned += M.mind - M.mind.special_role = antag_flag - if(mode.make_bloodsucker(M.mind)) - mode.bloodsuckers += M - log_admin("[M.name] was made into a bloodsucker by dynamic.") - message_admins("[M.name] was made into a bloodsucker by dynamic.") - return TRUE - -////////////////////////////////////////////// -// // -// CHANGELINGS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/latejoin/changeling - name = "Changeling Infiltrator" - config_tag = "latejoin_changeling" - antag_flag = ROLE_CHANGELING - antag_datum = /datum/antagonist/changeling - restricted_roles = list("AI", "Cyborg") - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") - required_candidates = 1 - weight = 3 - cost = 15 - flags = MINOR_RULESET - requirements = list(101,101,101,101,101,101,101,101,101,101) - property_weights = list("trust" = -2, "valid" = 2) - high_population_requirement = 101 - -////////////////////////////////////////////// -// // -// COLLECTOR // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/latejoin/collector - name = "Contraband Collector" - config_tag = "latejoin_collector" - antag_datum = /datum/antagonist/collector - antag_flag = ROLE_MINOR_ANTAG - restricted_roles = list("AI", "Cyborg") - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") - required_candidates = 1 - weight = 5 - cost = 1 - requirements = list(10,10,10,10,10,10,10,10,10,10) - high_population_requirement = 10 - repeatable = TRUE - flags = TRAITOR_RULESET | MINOR_RULESET | FAKE_ANTAG_RULESET - property_weights = list("story_potential" = 1, "trust" = -1, "extended" = 2) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm index 7ce6e74cc1..c41cd00aba 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm @@ -1,5 +1,7 @@ -#define REVENANT_SPAWN_THRESHOLD 20 -#define ABDUCTOR_MAX_TEAMS 4 // blame TG for not using the defines files +/// Probability the AI going malf will be accompanied by an ion storm announcement and some ion laws. +#define MALF_ION_PROB 33 +/// The probability to replace an existing law with an ion law instead of adding a new ion law. +#define REPLACE_LAW_WITH_ION_PROB 10 ////////////////////////////////////////////// // // @@ -17,24 +19,20 @@ var/list/living_antags = list() var/list/dead_players = list() var/list/list_observers = list() - var/list/ghost_eligible = list() /datum/dynamic_ruleset/midround/from_ghosts weight = 0 + required_type = /mob/dead/observer /// Whether the ruleset should call generate_ruleset_body or not. var/makeBody = TRUE + /// The rule needs this many applicants to be properly executed. + var/required_applicants = 1 /datum/dynamic_ruleset/midround/trim_candidates() - // - // All you need to know is that here, the candidates list contains 4 lists itself, indexed with the following defines: - // Candidates = list(CURRENT_LIVING_PLAYERS, CURRENT_LIVING_ANTAGS, CURRENT_DEAD_PLAYERS, CURRENT_OBSERVERS) - // So for example you can get the list of all current dead players with var/list/dead_players = candidates[CURRENT_DEAD_PLAYERS] - // Make sure to properly typecheck the mobs in those lists, as the dead_players list could contain ghosts, or dead players still in their bodies. - // We're still gonna trim the obvious (mobs without clients, jobbanned players, etc) - living_players = trim_list(mode.current_players[CURRENT_LIVING_PLAYERS]) - living_antags = trim_list(mode.current_players[CURRENT_LIVING_ANTAGS]) - list_observers = trim_list(mode.current_players[CURRENT_OBSERVERS]) - ghost_eligible = trim_list(get_all_ghost_role_eligible()) + living_players = trim_list(GLOB.alive_player_list) + living_antags = trim_list(GLOB.current_living_antags) + dead_players = trim_list(GLOB.dead_player_list) + list_observers = trim_list(GLOB.current_observers_list) /datum/dynamic_ruleset/midround/proc/trim_list(list/L = list()) var/list/trimmed_list = L.Copy() @@ -42,24 +40,18 @@ if (!istype(M, required_type)) trimmed_list.Remove(M) continue - if (HAS_TRAIT(M, TRAIT_NO_MIDROUND_ANTAG)) - trimmed_list.Remove(M) - continue if (!M.client) // Are they connected? trimmed_list.Remove(M) continue - if(M.client.prefs && M.client.prefs.toggles & MIDROUND_ANTAG) - trimmed_list.Remove(M) - continue - if(!mode.check_age(M.client, minimum_required_age)) + if(M.client.check_age(minimum_required_age) > 0) trimmed_list.Remove(M) continue if(antag_flag_override) - if(!(antag_flag_override in M.client.prefs.be_special) || jobban_isbanned(M.ckey, antag_flag_override)) + if(!(antag_flag_override in M.client.prefs.be_special) || jobban_isbanned(M.ckey, list(antag_flag_override, ROLE_SYNDICATE))) trimmed_list.Remove(M) continue else - if(!(antag_flag in M.client.prefs.be_special) || jobban_isbanned(M.ckey, antag_flag)) + if(!(antag_flag in M.client.prefs.be_special) || jobban_isbanned(M.ckey, list(antag_flag, ROLE_SYNDICATE))) trimmed_list.Remove(M) continue if (M.mind) @@ -74,56 +66,35 @@ continue return trimmed_list -/datum/dynamic_ruleset/midround/from_ghosts/trim_list(list/L = list()) - var/list/trimmed_list = L.Copy() - for(var/mob/M in trimmed_list) - if (!M.client) // Are they connected? - trimmed_list.Remove(M) - continue - if(!mode.check_age(M.client, minimum_required_age)) - trimmed_list.Remove(M) - continue - if(antag_flag_override) - if(!(antag_flag_override in M.client.prefs.be_special) || jobban_isbanned(M.ckey, antag_flag_override)) - trimmed_list.Remove(M) - continue - else - if(!(antag_flag in M.client.prefs.be_special) || jobban_isbanned(M.ckey, antag_flag)) - trimmed_list.Remove(M) - continue - return trimmed_list - // You can then for example prompt dead players in execute() to join as strike teams or whatever // Or autotator someone // IMPORTANT, since /datum/dynamic_ruleset/midround may accept candidates from both living, dead, and even antag players, you need to manually check whether there are enough candidates -// (see /datum/dynamic_ruleset/midround/autotraitor/ready(var/forced = FALSE) for example) +// (see /datum/dynamic_ruleset/midround/autotraitor/ready(forced = FALSE) for example) /datum/dynamic_ruleset/midround/ready(forced = FALSE) if (!forced) var/job_check = 0 if (enemy_roles.len > 0) - for (var/mob/M in mode.current_players[CURRENT_LIVING_PLAYERS]) - if (M.stat == DEAD) - continue // Dead players cannot count as opponents - if (M.mind && M.mind.assigned_role && (M.mind.assigned_role in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role in restricted_roles))) + for (var/mob/M in GLOB.alive_player_list) + if (M.stat == DEAD || !M.client) + continue // Dead/disconnected players cannot count as opponents + if (M.mind && (M.mind.assigned_role in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role in restricted_roles))) job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it - var/threat = clamp(round(mode.threat_level/10),1,10) + var/threat = round(mode.threat_level/10) if (job_check < required_enemies[threat]) - SSblackbox.record_feedback("tally","dynamic",1,"Times rulesets rejected due to not enough enemy roles") return FALSE return TRUE -/datum/dynamic_ruleset/midround/from_ghosts/ready(forced = FALSE) - if (required_candidates > ghost_eligible.len) - SSblackbox.record_feedback("tally","dynamic",1,"Times rulesets rejected due to not enough ghosts") - return FALSE - return ..() - - /datum/dynamic_ruleset/midround/from_ghosts/execute() - var/application_successful = send_applications(ghost_eligible) - return assigned.len > 0 && application_successful + var/list/possible_candidates = list() + possible_candidates.Add(dead_players) + possible_candidates.Add(list_observers) + send_applications(possible_candidates) + if(assigned.len > 0) + return TRUE + else + return FALSE /// This sends a poll to ghosts if they want to be a ghost spawn from a ruleset. /datum/dynamic_ruleset/midround/from_ghosts/proc/send_applications(list/possible_volunteers = list()) @@ -133,25 +104,24 @@ message_admins("Polling [possible_volunteers.len] players to apply for the [name] ruleset.") log_game("DYNAMIC: Polling [possible_volunteers.len] players to apply for the [name] ruleset.") - candidates = pollGhostCandidates("The mode is looking for volunteers to become a [name]", antag_flag, SSticker.mode, antag_flag, poll_time = 300) + candidates = pollGhostCandidates("The mode is looking for volunteers to become [antag_flag] for [name]", antag_flag, antag_flag_override ? antag_flag_override : antag_flag, poll_time = 300) - if(!candidates || candidates.len < required_candidates) - message_admins("The ruleset [name] did not receive enough applications.") - if(candidates) - message_admins("Only received [candidates.len], needed [required_candidates].") - else - message_admins("There were no candidates.") - log_game("DYNAMIC: The ruleset [name] did not receive enough applications.") - return FALSE + if(!candidates || candidates.len <= 0) + mode.dynamic_log("The ruleset [name] received no applications.") + mode.executed_rules -= src + attempt_replacement() + return message_admins("[candidates.len] players volunteered for the ruleset [name].") log_game("DYNAMIC: [candidates.len] players volunteered for [name].") review_applications() - return TRUE /// Here is where you can check if your ghost applicants are valid for the ruleset. /// Called by send_applications(). /datum/dynamic_ruleset/midround/from_ghosts/proc/review_applications() + if(candidates.len < required_applicants) + mode.executed_rules -= src + return for (var/i = 1, i <= required_candidates, i++) if(candidates.len <= 0) break @@ -175,7 +145,7 @@ finish_setup(new_character, i) assigned += applicant - notify_ghosts("[new_character] has been picked for the ruleset [name]!", source = new_character, action = NOTIFY_ORBIT) + notify_ghosts("[new_character] has been picked for the ruleset [name]!", source = new_character, action = NOTIFY_ORBIT, header="Something Interesting!") /datum/dynamic_ruleset/midround/from_ghosts/proc/generate_ruleset_body(mob/applicant) var/mob/living/carbon/human/new_character = makeBody(applicant) @@ -191,6 +161,20 @@ /datum/dynamic_ruleset/midround/from_ghosts/proc/setup_role(datum/antagonist/new_role) return +/// Fired when there are no valid candidates. Will spawn a sleeper agent or latejoin traitor. +/datum/dynamic_ruleset/midround/from_ghosts/proc/attempt_replacement() + var/datum/dynamic_ruleset/midround/autotraitor/sleeper_agent = new + + // Otherwise, it has a chance to fail. We don't want that, since this is already pretty unlikely. + sleeper_agent.has_failure_chance = FALSE + + mode.configure_ruleset(sleeper_agent) + + if (!mode.picking_specific_rule(sleeper_agent)) + return + + mode.picking_specific_rule(/datum/dynamic_ruleset/latejoin/infiltrator) + ////////////////////////////////////////////// // // // SYNDICATE TRAITORS // @@ -199,39 +183,45 @@ /datum/dynamic_ruleset/midround/autotraitor name = "Syndicate Sleeper Agent" - config_tag = "midround_traitor" antag_datum = /datum/antagonist/traitor antag_flag = ROLE_TRAITOR - restricted_roles = list("AI", "Cyborg", "Positronic Brain") - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + protected_roles = list("Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain") + restricted_roles = list("Cyborg", "AI", "Positronic Brain") required_candidates = 1 weight = 7 cost = 10 - requirements = list(30,25,20,15,15,15,15,15,15,15) + requirements = list(50,40,30,20,10,10,10,10,10,10) repeatable = TRUE - high_population_requirement = 15 - flags = TRAITOR_RULESET | MINOR_RULESET | ALWAYS_MAX_WEIGHT_RULESET - property_weights = list("story_potential" = 2, "trust" = -1, "extended" = 1) + + /// Whether or not this instance of sleeper agent should be randomly acceptable. + /// If TRUE, then this has a threat level% chance to succeed. + var/has_failure_chance = TRUE /datum/dynamic_ruleset/midround/autotraitor/acceptable(population = 0, threat = 0) - var/player_count = mode.current_players[CURRENT_LIVING_PLAYERS].len - var/antag_count = mode.current_players[CURRENT_LIVING_ANTAGS].len + var/player_count = GLOB.alive_player_list.len + var/antag_count = GLOB.current_living_antags.len var/max_traitors = round(player_count / 10) + 1 - if ((antag_count < max_traitors) && prob(min(100,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 (has_failure_chance && !prob(mode.threat_level)) + log_game("DYNAMIC: Random chance to roll autotraitor failed, it was a [mode.threat_level]% chance.") + return FALSE + + return ..() + /datum/dynamic_ruleset/midround/autotraitor/trim_candidates() ..() for(var/mob/living/player in living_players) if(issilicon(player)) // Your assigned role doesn't change when you are turned into a silicon. living_players -= player - continue - if(is_centcom_level(player.z)) + else if(is_centcom_level(player.z)) living_players -= player // We don't autotator people in CentCom - continue - if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0)) + else if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0)) living_players -= player // We don't autotator people with roles already /datum/dynamic_ruleset/midround/autotraitor/ready(forced = FALSE) @@ -245,63 +235,55 @@ living_players -= M var/datum/antagonist/traitor/newTraitor = new M.mind.add_antag_datum(newTraitor) - log_admin("[M] was made into a traitor by dynamic.") - message_admins("[M] was made into a traitor by dynamic.") + message_admins("[ADMIN_LOOKUPFLW(M)] was selected by the [name] ruleset and has been made into a midround traitor.") + log_game("DYNAMIC: [key_name(M)] was selected by the [name] ruleset and has been made into a midround traitor.") return TRUE - ////////////////////////////////////////////// // // // Malfunctioning AI // -// // +// // ////////////////////////////////////////////// /datum/dynamic_ruleset/midround/malf name = "Malfunctioning AI" - config_tag = "midround_malf_ai" antag_datum = /datum/antagonist/traitor antag_flag = ROLE_MALF enemy_roles = list("Security Officer", "Warden","Detective","Head of Security", "Captain", "Scientist", "Chemist", "Research Director", "Chief Engineer") exclusive_roles = list("AI") - required_enemies = list(6,6,6,4,4,4,2,2,2,1) + required_enemies = list(4,4,4,4,4,4,2,2,2,0) required_candidates = 1 - weight = 2 + weight = 3 cost = 35 - requirements = list(101,101,70,50,50,50,40,30,30,30) - high_population_requirement = 30 + requirements = list(101,101,80,70,60,60,50,50,40,40) required_type = /mob/living/silicon/ai - property_weights = list("story_potential" = 2, "trust" = 1, "chaos" = 2) - var/ion_announce = 33 - var/removeDontImproveChance = 10 - -/datum/dynamic_ruleset/midround/malf/ready() - if(!candidates || !candidates.len) - return FALSE - return ..() /datum/dynamic_ruleset/midround/malf/trim_candidates() ..() - for(var/mob/living/player in living_players) + candidates = living_players + for(var/mob/living/player in candidates) if(!isAI(player)) candidates -= player continue + if(is_centcom_level(player.z)) candidates -= player continue + if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0)) candidates -= player /datum/dynamic_ruleset/midround/malf/execute() + if(!candidates || !candidates.len) + return FALSE var/mob/living/silicon/ai/M = pick_n_take(candidates) assigned += M.mind var/datum/antagonist/traitor/AI = new M.mind.special_role = antag_flag M.mind.add_antag_datum(AI) - log_admin("[M] was made into a malf AI by dynamic.") - message_admins("[M] was made into a malf AI by dynamic.") - if(prob(ion_announce)) + if(prob(MALF_ION_PROB)) priority_announce("Ion storm detected near the station. Please check all AI-controlled equipment for errors.", "Anomaly Alert", "ionstorm") - if(prob(removeDontImproveChance)) + if(prob(REPLACE_LAW_WITH_ION_PROB)) M.replace_random_law(generate_ion_law(), list(LAW_INHERENT, LAW_SUPPLIED, LAW_ION)) else M.add_ion_law(generate_ion_law()) @@ -315,21 +297,19 @@ /datum/dynamic_ruleset/midround/from_ghosts/wizard name = "Wizard" - config_tag = "midround_wizard" antag_datum = /datum/antagonist/wizard antag_flag = ROLE_WIZARD enemy_roles = list("Security Officer","Detective","Head of Security", "Captain") - required_enemies = list(4,4,3,2,2,1,1,0,0,0) + required_enemies = list(2,2,1,1,1,1,1,0,0,0) required_candidates = 1 weight = 1 cost = 20 - requirements = list(90,90,70,50,50,50,50,40,30,30) - high_population_requirement = 30 + requirements = list(90,90,90,80,60,40,30,20,10,10) repeatable = TRUE - property_weights = list("story_potential" = 2, "trust" = 1, "chaos" = 2, "extended" = -2) - var/datum/mind/wizard /datum/dynamic_ruleset/midround/from_ghosts/wizard/ready(forced = FALSE) + if (required_candidates > (dead_players.len + list_observers.len)) + return FALSE if(GLOB.wizardstart.len == 0) log_admin("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") message_admins("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") @@ -339,20 +319,6 @@ /datum/dynamic_ruleset/midround/from_ghosts/wizard/finish_setup(mob/new_character, index) ..() new_character.forceMove(pick(GLOB.wizardstart)) - wizard = new_character.mind - -/datum/dynamic_ruleset/midround/from_ghosts/wizard/rule_process() // i can literally copy this from are_special_antags_dead it's great - if(isliving(wizard.current) && wizard.current.stat!=DEAD) - return FALSE - for(var/obj/item/phylactery/P in GLOB.poi_list) //TODO : IsProperlyDead() - if(P.mind && P.mind.has_antag_datum(/datum/antagonist/wizard)) - return FALSE - - if(SSevents.wizardmode) //If summon events was active, turn it off - SSevents.toggleWizardmode() - SSevents.resetFrequency() - - return RULESET_STOP_PROCESSING ////////////////////////////////////////////// // // @@ -362,34 +328,30 @@ /datum/dynamic_ruleset/midround/from_ghosts/nuclear name = "Nuclear Assault" - config_tag = "midround_nuclear" antag_flag = ROLE_OPERATIVE antag_datum = /datum/antagonist/nukeop enemy_roles = list("AI", "Cyborg", "Security Officer", "Warden","Detective","Head of Security", "Captain") - required_enemies = list(5,5,4,3,3,2,2,2,1,1) + required_enemies = list(3,3,3,3,3,2,1,1,0,0) required_candidates = 5 weight = 5 cost = 35 - requirements = list(90,90,90,80,70,60,50,40,40,40) - high_population_requirement = 40 - property_weights = list("story_potential" = 2, "trust" = 2, "chaos" = 2, "extended" = -2, "valid" = 2) - var/operative_cap = list(2,2,3,3,4,5,5,5,5,5) + 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) return FALSE // Unavailable if nuke ops were already sent at roundstart - indice_pop = min(10, round(living_players.len/5)+1) - /* NOTE: The above line's magic value of "10" is a hack due to the fact that byond was - not recognizing operative_cap as a defined variable. It should be operative_cap.len-- - and yes, this means that if the len is changed, this variable must be changed along with it. - One day, once the mystery of why this issue was occuring is figured out, - we may change it back, but until this day comes, we must make it simply 10. - */ + indice_pop = min(operative_cap.len, round(living_players.len/5)+1) required_candidates = operative_cap[indice_pop] return ..() +/datum/dynamic_ruleset/midround/from_ghosts/nuclear/ready(forced = FALSE) + if (required_candidates > (dead_players.len + list_observers.len)) + return FALSE + return ..() + /datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_setup(mob/new_character, index) new_character.mind.special_role = "Nuclear Operative" new_character.mind.assigned_role = "Nuclear Operative" @@ -408,24 +370,55 @@ /datum/dynamic_ruleset/midround/from_ghosts/blob name = "Blob" - config_tag = "blob" antag_datum = /datum/antagonist/blob antag_flag = ROLE_BLOB enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") - required_enemies = list(3,3,2,2,2,1,1,1,1,0) + required_enemies = list(2,2,1,1,1,1,1,0,0,0) required_candidates = 1 - blocking_rules = list(/datum/dynamic_ruleset/roundstart/clockcult) - weight = 4 - cost = 20 - requirements = list(101,101,101,80,60,50,50,50,50,50) - high_population_requirement = 50 + weight = 2 + cost = 10 + requirements = list(101,101,101,80,60,50,30,20,10,10) repeatable = TRUE - property_weights = list("story_potential" = -1, "trust" = 2, "chaos" = 2, "extended" = -2, "valid" = 2) /datum/dynamic_ruleset/midround/from_ghosts/blob/generate_ruleset_body(mob/applicant) var/body = applicant.become_overmind() return body +/// Infects a random player, making them explode into a blob. +/datum/dynamic_ruleset/midround/blob_infection + name = "Blob Infection" + antag_datum = /datum/antagonist/blob + antag_flag = ROLE_BLOB + protected_roles = list("Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain") + restricted_roles = list("Cyborg", "AI", "Positronic Brain") + enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) + required_candidates = 1 + weight = 2 + cost = 10 + requirements = list(101,101,101,80,60,50,30,20,10,10) + repeatable = TRUE + +/datum/dynamic_ruleset/midround/blob_infection/trim_candidates() + ..() + candidates = living_players + for(var/mob/living/player as anything in candidates) + var/turf/player_turf = get_turf(player) + if(!player_turf || !is_station_level(player_turf.z)) + candidates -= player + continue + + if(player.mind && (player.mind.special_role || length(player.mind.antag_datums) > 0)) + candidates -= player + +/datum/dynamic_ruleset/midround/blob_infection/execute() + if(!candidates || !candidates.len) + return FALSE + var/mob/living/carbon/human/blob_antag = pick_n_take(candidates) + assigned += blob_antag.mind + blob_antag.mind.special_role = antag_flag + return ..() + ////////////////////////////////////////////// // // // XENOMORPH (GHOST) // @@ -434,22 +427,20 @@ /datum/dynamic_ruleset/midround/from_ghosts/xenomorph name = "Alien Infestation" - config_tag = "xenos" antag_datum = /datum/antagonist/xeno antag_flag = ROLE_ALIEN enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") - required_enemies = list(3,3,2,2,1,1,1,1,1,0) + required_enemies = list(2,2,1,1,1,1,1,0,0,0) required_candidates = 1 weight = 3 cost = 10 - requirements = list(101,101,101,70,50,50,50,50,50,50) - high_population_requirement = 50 - repeatable_weight_decrease = 2 + requirements = list(101,101,101,70,50,40,20,15,10,10) repeatable = TRUE - property_weights = list("story_potential" = -1, "trust" = 1, "chaos" = 2, "extended" = -2, "valid" = 2) var/list/vents = list() -/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/ready() +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/execute() + // 50% chance of being incremented by one + required_candidates += prob(50) for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent in GLOB.machines) if(QDELETED(temp_vent)) continue @@ -463,18 +454,12 @@ vents += temp_vent if(!vents.len) return FALSE - return ..() - - -/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/execute() - // 50% chance of being incremented by one - required_candidates += prob(50) . = ..() /datum/dynamic_ruleset/midround/from_ghosts/xenomorph/generate_ruleset_body(mob/applicant) var/obj/vent = pick_n_take(vents) var/mob/living/carbon/alien/larva/new_xeno = new(vent.loc) - applicant.transfer_ckey(new_xeno, FALSE) + new_xeno.key = applicant.key message_admins("[ADMIN_LOOKUPFLW(new_xeno)] has been made into an alien by the midround ruleset.") log_game("DYNAMIC: [key_name(new_xeno)] was spawned as an alien by the midround ruleset.") return new_xeno @@ -487,7 +472,6 @@ /datum/dynamic_ruleset/midround/from_ghosts/nightmare name = "Nightmare" - config_tag = "nightmare" antag_datum = /datum/antagonist/nightmare antag_flag = "Nightmare" antag_flag_override = ROLE_ALIEN @@ -496,15 +480,11 @@ required_candidates = 1 weight = 3 cost = 10 - flags = MINOR_RULESET - requirements = list(101,101,101,70,50,40,20,15,15,15) - high_population_requirement = 50 - repeatable_weight_decrease = 2 + requirements = list(101,101,101,70,50,40,20,15,10,10) repeatable = TRUE - property_weights = list("story_potential" = 1, "trust" = 1, "extended" = 1, "valid" = 2, "integrity" = 1) var/list/spawn_locs = list() -/datum/dynamic_ruleset/midround/from_ghosts/nightmare/ready() +/datum/dynamic_ruleset/midround/from_ghosts/nightmare/execute() for(var/X in GLOB.xeno_spawn) var/turf/T = X var/light_amount = T.get_lumcount() @@ -512,7 +492,7 @@ spawn_locs += T if(!spawn_locs.len) return FALSE - return ..() + . = ..() /datum/dynamic_ruleset/midround/from_ghosts/nightmare/generate_ruleset_body(mob/applicant) var/datum/mind/player_mind = new /datum/mind(applicant.key) @@ -525,162 +505,265 @@ player_mind.add_antag_datum(/datum/antagonist/nightmare) S.set_species(/datum/species/shadow/nightmare) - playsound(S, 'sound/magic/ethereal_exit.ogg', 50, 1, -1) + playsound(S, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1) message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a Nightmare by the midround ruleset.") log_game("DYNAMIC: [key_name(S)] was spawned as a Nightmare by the midround ruleset.") return S ////////////////////////////////////////////// // // -// SLAUGHTER DEMON // +// SPACE DRAGON (GHOST) // // // ////////////////////////////////////////////// - -/datum/dynamic_ruleset/midround/from_ghosts/slaughter_demon - name = "Slaughter Demon" - config_tag = "slaughter_demon" - antag_flag = ROLE_ALIEN - enemy_roles = list("Security Officer","Shaft Miner","Head of Security","Captain","Janitor","AI","Cyborg","Bartender") - required_enemies = list(3,2,2,2,2,1,1,1,1,0) +/datum/dynamic_ruleset/midround/from_ghosts/space_dragon + name = "Space Dragon" + antag_datum = /datum/antagonist/space_dragon + antag_flag = ROLE_SPACE_DRAGON + antag_flag_override = ROLE_SPACE_DRAGON + enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) required_candidates = 1 weight = 4 - cost = 15 - requirements = list(101,101,101,90,80,70,60,50,40,30) - property_weights = list("story_potential" = -2, "extended" = -2, "integrity" = 2, "valid" = 2, "trust" = 2) - high_population_requirement = 30 + cost = 10 + requirements = list(101,101,101,80,60,50,30,20,10,10) + repeatable = TRUE var/list/spawn_locs = list() -/datum/dynamic_ruleset/midround/from_ghosts/slaughter_demon/ready(forced = FALSE) - for(var/obj/effect/landmark/carpspawn/L in GLOB.landmarks_list) - if(isturf(L.loc)) - spawn_locs += L.loc - +/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/execute() + for(var/obj/effect/landmark/carpspawn/C in GLOB.landmarks_list) + spawn_locs += (C.loc) if(!spawn_locs.len) - return FALSE - return ..() + message_admins("No valid spawn locations found, aborting...") + return MAP_ERROR + . = ..() -/datum/dynamic_ruleset/midround/from_ghosts/slaughter_demon/generate_ruleset_body(mob/applicant) +/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/generate_ruleset_body(mob/applicant) var/datum/mind/player_mind = new /datum/mind(applicant.key) - player_mind.active = 1 - var/obj/effect/dummy/phased_mob/slaughter/holder = new /obj/effect/dummy/phased_mob/slaughter((pick(spawn_locs))) - var/mob/living/simple_animal/slaughter/S = new (holder) - S.holder = holder + player_mind.active = TRUE + + var/mob/living/simple_animal/hostile/space_dragon/S = new (pick(spawn_locs)) player_mind.transfer_to(S) - player_mind.assigned_role = "Slaughter Demon" - player_mind.special_role = "Slaughter Demon" - player_mind.add_antag_datum(/datum/antagonist/slaughter) - to_chat(S, S.playstyle_string) - to_chat(S, "You are currently not currently in the same plane of existence as the station. Blood Crawl near a blood pool to manifest.") - SEND_SOUND(S, 'sound/magic/demon_dies.ogg') - message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a slaughter demon by dynamic.") - log_game("[key_name(S)] was spawned as a slaughter demon by dynamic.") + player_mind.assigned_role = "Space Dragon" + player_mind.special_role = ROLE_SPACE_DRAGON + player_mind.add_antag_datum(/datum/antagonist/space_dragon) + + playsound(S, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1) + message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a Space Dragon by the midround ruleset.") + log_game("DYNAMIC: [key_name(S)] was spawned as a Space Dragon by the midround ruleset.") + priority_announce("A large organic energy flux has been recorded near of [station_name()], please stand-by.", "Lifesign Alert") return S ////////////////////////////////////////////// // // -// ABDUCTORS // +// ABDUCTORS (GHOST) // // // ////////////////////////////////////////////// +#define ABDUCTOR_MAX_TEAMS 4 /datum/dynamic_ruleset/midround/from_ghosts/abductors name = "Abductors" - config_tag = "abductors" - antag_flag = ROLE_ABDUCTOR - // Has two antagonist flags, in fact - enemy_roles = list("AI", "Cyborg", "Security Officer", "Warden","Detective","Head of Security", "Captain") - required_enemies = list(3,3,2,2,1,1,0,0,0,0) + antag_flag = "Abductor" + antag_flag_override = ROLE_ABDUCTOR + enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) required_candidates = 2 - weight = 8 + required_applicants = 2 + weight = 4 cost = 10 - requirements = list(101,101,70,50,40,30,30,30,30,30) - blocking_rules = list(/datum/dynamic_ruleset/roundstart/nuclear,/datum/dynamic_ruleset/midround/from_ghosts/nuclear) - high_population_requirement = 15 - var/datum/team/abductor_team/team - property_weights = list("extended" = -2, "valid" = 1, "trust" = -1, "chaos" = 2) - repeatable_weight_decrease = 4 + requirements = list(101,101,101,80,60,50,30,20,10,10) repeatable = TRUE + var/datum/team/abductor_team/new_team /datum/dynamic_ruleset/midround/from_ghosts/abductors/ready(forced = FALSE) - team = new /datum/team/abductor_team - if(team.team_number > ABDUCTOR_MAX_TEAMS) + if (required_candidates > (dead_players.len + list_observers.len)) return FALSE return ..() /datum/dynamic_ruleset/midround/from_ghosts/abductors/finish_setup(mob/new_character, index) - switch(index) - if(1) // yeah this seems like a baffling anti-pattern but it's actually the best way to do this, shit you not - var/mob/living/carbon/human/agent = new_character - agent.mind.add_antag_datum(/datum/antagonist/abductor/agent, team) - log_game("[key_name(agent)] has been selected as [team.name] abductor agent.") - if(2) - var/mob/living/carbon/human/scientist = new_character - scientist.mind.add_antag_datum(/datum/antagonist/abductor/scientist, team) - log_game("[key_name(scientist)] has been selected as [team.name] abductor scientist.") + if (index == 1) // Our first guy is the scientist. We also initialize the team here as well since this should only happen once per pair of abductors. + new_team = new + if(new_team.team_number > ABDUCTOR_MAX_TEAMS) + return MAP_ERROR + var/datum/antagonist/abductor/scientist/new_role = new + new_character.mind.add_antag_datum(new_role, new_team) + else // Our second guy is the agent, team is already created, don't need to make another one. + var/datum/antagonist/abductor/agent/new_role = new + new_character.mind.add_antag_datum(new_role, new_team) + +#undef ABDUCTOR_MAX_TEAMS ////////////////////////////////////////////// // // -// SPACE NINJA // +// SWARMERS (GHOST) // // // ////////////////////////////////////////////// -/datum/dynamic_ruleset/midround/from_ghosts/ninja +/datum/dynamic_ruleset/midround/swarmers + name = "Swarmers" + antag_datum = /datum/antagonist/swarmer + antag_flag = "Swarmer" + antag_flag_override = ROLE_ALIEN + required_type = /mob/dead/observer + enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) + required_candidates = 0 + weight = 3 + cost = 10 + requirements = list(101,101,101,80,60,50,30,20,10,10) + repeatable = TRUE + +/datum/dynamic_ruleset/midround/swarmers/execute() + var/list/spawn_locs = list() + for(var/x in GLOB.xeno_spawn) + var/turf/spawn_turf = x + var/light_amount = spawn_turf.get_lumcount() + if(light_amount < SHADOW_SPECIES_LIGHT_THRESHOLD) + spawn_locs += spawn_turf + if(!spawn_locs.len) + message_admins("No valid spawn locations found in GLOB.xeno_spawn, aborting swarmer spawning...") + return MAP_ERROR + var/obj/structure/swarmer_beacon/new_beacon = new(pick(spawn_locs)) + log_game("A Swarmer Beacon was spawned via Dynamic Mode.") + notify_ghosts("\A Swarmer Beacon has spawned!", source = new_beacon, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Swarmer Beacon Spawned") + return ..() + +////////////////////////////////////////////// +// // +// SPACE NINJA (GHOST) // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/from_ghosts/space_ninja name = "Space Ninja" - config_tag = "ninja" - antag_flag = ROLE_NINJA - enemy_roles = list("Security Officer","Head of Security","Captain","AI","Cyborg") - required_enemies = list(3,2,2,2,2,1,1,1,1,0) + antag_datum = /datum/antagonist/ninja + antag_flag = "Space Ninja" + antag_flag_override = ROLE_NINJA + enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) required_candidates = 1 weight = 4 - cost = 15 - flags = MINOR_RULESET - requirements = list(101,101,101,90,80,70,60,50,40,30) - high_population_requirement = 30 - property_weights = list("story_potential" = 1, "extended" = -2, "valid" = 2) + cost = 10 + requirements = list(101,101,101,80,60,50,30,20,10,10) + repeatable = TRUE var/list/spawn_locs = list() - var/spawn_loc -/datum/dynamic_ruleset/midround/from_ghosts/ninja/ready(forced = FALSE) - if(!spawn_loc) - var/list/spawn_locs = list() - for(var/obj/effect/landmark/carpspawn/L in GLOB.landmarks_list) - if(isturf(L.loc)) - spawn_locs += L.loc - for(var/obj/effect/landmark/loneopspawn/L in GLOB.landmarks_list) - if(isturf(L.loc)) - spawn_locs += L.loc - if(!spawn_locs.len) - return FALSE - spawn_loc = pick(spawn_locs) - if(!spawn_loc) +/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/execute() + for(var/obj/effect/landmark/carpspawn/carp_spawn in GLOB.landmarks_list) + if(!isturf(carp_spawn.loc)) + stack_trace("Carp spawn found not on a turf: [carp_spawn.type] on [isnull(carp_spawn.loc) ? "null" : carp_spawn.loc.type]") + continue + spawn_locs += carp_spawn.loc + if(!spawn_locs.len) + message_admins("No valid spawn locations found, aborting...") + return MAP_ERROR + return ..() + +/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/generate_ruleset_body(mob/applicant) + var/mob/living/carbon/human/ninja = create_space_ninja(pick(spawn_locs)) + ninja.key = applicant.key + ninja.mind.add_antag_datum(/datum/antagonist/ninja) + + message_admins("[ADMIN_LOOKUPFLW(ninja)] has been made into a Space Ninja by the midround ruleset.") + log_game("DYNAMIC: [key_name(ninja)] was spawned as a Space Ninja by the midround ruleset.") + return ninja + +////////////////////////////////////////////// +// // +// Revenant (GHOST) // +// // +////////////////////////////////////////////// + +/// Revenant ruleset +/datum/dynamic_ruleset/midround/from_ghosts/revenant + name = "Revenant" + antag_datum = /datum/antagonist/revenant + antag_flag = "Revenant" + antag_flag_override = ROLE_REVENANT + enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) + required_candidates = 1 + weight = 4 + cost = 10 + requirements = list(101,101,101,70,50,40,20,15,10,10) + repeatable = TRUE + var/dead_mobs_required = 20 + var/need_extra_spawns_value = 15 + var/list/spawn_locs = list() + +/datum/dynamic_ruleset/midround/from_ghosts/revenant/acceptable(population=0, threat=0) + if(GLOB.dead_mob_list.len < dead_mobs_required) return FALSE return ..() -/datum/dynamic_ruleset/midround/from_ghosts/ninja/generate_ruleset_body(mob/applicant) - var/key = applicant.key +/datum/dynamic_ruleset/midround/from_ghosts/revenant/execute() + for(var/mob/living/corpse in GLOB.dead_mob_list) //look for any dead bodies + var/turf/corpse_turf = get_turf(corpse) + if(corpse_turf && is_station_level(corpse_turf.z)) + spawn_locs += corpse_turf + if(!spawn_locs.len || spawn_locs.len < need_extra_spawns_value) //look for any morgue trays, crematoriums, ect if there weren't alot of dead bodies on the station to pick from + for(var/obj/structure/bodycontainer/corpse_container in GLOB.bodycontainers) + var/turf/container_turf = get_turf(corpse_container) + if(container_turf && is_station_level(container_turf.z)) + spawn_locs += container_turf + if(!spawn_locs.len) //If we can't find any valid spawnpoints, try the carp spawns + for(var/obj/effect/landmark/carpspawn/carp_spawnpoint in GLOB.landmarks_list) + if(isturf(carp_spawnpoint.loc)) + spawn_locs += carp_spawnpoint.loc + if(!spawn_locs.len) //If we can't find THAT, then just give up and cry + return FALSE + . = ..() - //Prepare ninja player mind - var/datum/mind/Mind = new /datum/mind(key) - Mind.assigned_role = ROLE_NINJA - Mind.special_role = ROLE_NINJA - Mind.active = 1 +/datum/dynamic_ruleset/midround/from_ghosts/revenant/generate_ruleset_body(mob/applicant) + var/mob/living/simple_animal/revenant/revenant = new(pick(spawn_locs)) + revenant.key = applicant.key + message_admins("[ADMIN_LOOKUPFLW(revenant)] has been made into a revenant by the midround ruleset.") + log_game("[key_name(revenant)] was spawned as a revenant by the midround ruleset.") + return revenant - //spawn the ninja and assign the candidate - var/mob/living/carbon/human/Ninja = create_space_ninja(spawn_loc) - Mind.transfer_to(Ninja) - var/datum/antagonist/ninja/ninjadatum = new - ninjadatum.helping_station = pick(TRUE,FALSE) - Mind.add_antag_datum(ninjadatum) +/// Sentient Disease ruleset +/datum/dynamic_ruleset/midround/from_ghosts/sentient_disease + name = "Sentient Disease" + antag_datum = /datum/antagonist/disease + antag_flag = "Sentient Disease" + antag_flag_override = ROLE_ALIEN + required_candidates = 1 + weight = 4 + cost = 10 + requirements = list(101,101,101,80,60,50,30,20,10,10) + repeatable = TRUE - if(Ninja.mind != Mind) //something has gone wrong! - stack_trace("Ninja created with incorrect mind") +/datum/dynamic_ruleset/midround/from_ghosts/sentient_disease/generate_ruleset_body(mob/applicant) + var/mob/camera/disease/virus = new /mob/camera/disease(SSmapping.get_station_center()) + virus.key = applicant.key + INVOKE_ASYNC(virus, /mob/camera/disease/proc/pick_name) + message_admins("[ADMIN_LOOKUPFLW(virus)] has been made into a sentient disease by the midround ruleset.") + log_game("[key_name(virus)] was spawned as a sentient disease by the midround ruleset.") + return virus - message_admins("[ADMIN_LOOKUPFLW(Ninja)] has been made into a ninja by dynamic.") - log_game("[key_name(Ninja)] was spawned as a ninja by dynamic.") - return Ninja +/// Space Pirates ruleset +/datum/dynamic_ruleset/midround/pirates + name = "Space Pirates" + antag_flag = "Space Pirates" + required_type = /mob/dead/observer + enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) + required_candidates = 0 + weight = 4 + cost = 10 + requirements = list(101,101,101,80,60,50,30,20,10,10) + repeatable = TRUE -/datum/dynamic_ruleset/midround/from_ghosts/ninja/finish_setup(mob/new_character, index) - return +/datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat=0) + if (!SSmapping.empty_space) + return FALSE + return ..() -#undef ABDUCTOR_MAX_TEAMS -#undef REVENANT_SPAWN_THRESHOLD +/datum/dynamic_ruleset/midround/pirates/execute() + send_pirate_threat() + return ..() + +/// Probability the AI going malf will be accompanied by an ion storm announcement and some ion laws. +#undef MALF_ION_PROB +/// The probability to replace an existing law with an ion law instead of adding a new ion law. +#undef REPLACE_LAW_WITH_ION_PROB diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_minor.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_minor.dm deleted file mode 100644 index 74a61bd3ff..0000000000 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_minor.dm +++ /dev/null @@ -1,255 +0,0 @@ -/datum/dynamic_ruleset/minor/proc/trim_list(list/L = list()) - var/list/trimmed_list = L.Copy() - for(var/mob/M in trimmed_list) - if (!ishuman(M)) - trimmed_list.Remove(M) - continue - if (HAS_TRAIT(M, TRAIT_NO_MIDROUND_ANTAG)) - trimmed_list.Remove(M) - continue - if (!M.client) // Are they connected? - trimmed_list.Remove(M) - continue - if(!mode.check_age(M.client, minimum_required_age)) - trimmed_list.Remove(M) - continue - if(antag_flag_override) - if(!(antag_flag_override in M.client.prefs.be_special) || jobban_isbanned(M.ckey, antag_flag_override)) - trimmed_list.Remove(M) - continue - else - if(!(antag_flag in M.client.prefs.be_special) || jobban_isbanned(M.ckey, antag_flag)) - trimmed_list.Remove(M) - continue - if (M.mind) - if ((M.mind.assigned_role in GLOB.exp_specialmap[EXP_TYPE_SPECIAL])) // Are they playing a ghost role? - trimmed_list.Remove(M) - continue - if (M.mind.assigned_role in restricted_roles) // Does their job allow it? - trimmed_list.Remove(M) - continue - if ((exclusive_roles.len > 0) && !(M.mind.assigned_role in exclusive_roles)) // Is the rule exclusive to their job? - trimmed_list.Remove(M) - continue - return trimmed_list - -/datum/dynamic_ruleset/minor/trim_candidates() - // - // All you need to know is that here, the candidates list contains 4 lists itself, indexed with the following defines: - // Candidates = list(CURRENT_LIVING_PLAYERS, CURRENT_LIVING_ANTAGS, CURRENT_DEAD_PLAYERS, CURRENT_OBSERVERS) - // So for example you can get the list of all current dead players with var/list/dead_players = candidates[CURRENT_DEAD_PLAYERS] - // Make sure to properly typecheck the mobs in those lists, as the dead_players list could contain ghosts, or dead players still in their bodies. - // We're still gonna trim the obvious (mobs without clients, jobbanned players, etc) - candidates = trim_list(mode.current_players[CURRENT_LIVING_PLAYERS]) - -////////////////////////////////////////////// -// // -// SYNDICATE TRAITORS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/minor/traitor - name = "Traitors" - config_tag = "traitor" // these having identical config tags to the roundstart modes is 100% intentional, so that config edits are simpler - persistent = TRUE - antag_flag = ROLE_TRAITOR - antag_datum = /datum/antagonist/traitor/ - minimum_required_age = 0 - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster", "Cyborg") - restricted_roles = list("Cyborg", "AI") - required_candidates = 1 - weight = 5 - flags = TRAITOR_RULESET | ALWAYS_MAX_WEIGHT_RULESET - cost = 10 // Avoid raising traitor threat above 10, as it is the default low cost ruleset. - requirements = list(50,50,50,50,50,50,50,50,50,50) - high_population_requirement = 40 - property_weights = list("story_potential" = 2, "trust" = -1, "extended" = 1, "valid" = 1) - -/datum/dynamic_ruleset/minor/traitor/execute() - var/mob/M = pick_n_take(candidates) - assigned += M - var/datum/antagonist/traitor/newTraitor = new - M.mind.add_antag_datum(newTraitor) - log_admin("[M] was made into a traitor by dynamic.") - message_admins("[M] was made into a traitor by dynamic.") - return TRUE - -////////////////////////////////////////// -// // -// BLOOD BROTHERS // -// // -////////////////////////////////////////// - -/datum/dynamic_ruleset/minor/traitorbro - name = "Blood Brothers" - config_tag = "traitorbro" - antag_flag = ROLE_BROTHER - antag_datum = /datum/antagonist/brother - restricted_roles = list("AI", "Cyborg") - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") - required_candidates = 2 - weight = 4 - cost = 10 - requirements = list(101,101,101,101,101,101,101,101,101,101) - high_population_requirement = 101 - 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. - property_weights = list("story_potential" = 1, "trust" = -1, "extended" = 1, "valid" = 1) - var/list/datum/team/brother_team/pre_brother_teams = list() - var/const/min_team_size = 2 - -/datum/dynamic_ruleset/minor/traitorbro/execute() - if(candidates.len < min_team_size || candidates.len < required_candidates) - return FALSE - var/datum/team/brother_team/team = new - var/team_size = prob(10) ? min(3, candidates.len) : 2 - for(var/k = 1 to team_size) - var/mob/bro = pick_n_take(candidates) - assigned += bro.mind - team.add_member(bro.mind) - bro.mind.special_role = "brother" - bro.mind.restricted_roles = restricted_roles - team.pick_meeting_area() - team.forge_brother_objectives() - for(var/datum/mind/M in team.members) - M.add_antag_datum(/datum/antagonist/brother, team) - team.update_name() - mode.brother_teams += team - -////////////////////////////////////////////// -// // -// CHANGELINGS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/minor/changeling - name = "Changelings" - config_tag = "changeling" - antag_flag = ROLE_CHANGELING - antag_datum = /datum/antagonist/changeling - restricted_roles = list("AI", "Cyborg") - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") - required_candidates = 1 - weight = 3 - cost = 15 - scaling_cost = 15 - requirements = list(101,101,101,101,101,101,101,101,101,101) - property_weights = list("trust" = -2, "valid" = 2) - high_population_requirement = 10 - antag_cap = list(1,1,1,1,1,2,2,2,2,3) - var/team_mode_probability = 30 - -/datum/dynamic_ruleset/minor/changeling/execute() - var/mob/M = pick_n_take(candidates) - assigned += M.mind - M.mind.restricted_roles = restricted_roles - M.mind.special_role = ROLE_CHANGELING - var/datum/antagonist/changeling/new_antag = new antag_datum() - M.mind.add_antag_datum(new_antag) - return TRUE - -////////////////////////////////////////////// -// // -// ELDRITCH CULT // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/minor/heretics - name = "Heretic" - antag_flag = "heretic" - antag_datum = /datum/antagonist/heretic - protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain") - restricted_roles = list("AI", "Cyborg") - required_candidates = 1 - weight = 3 - cost = 25 - scaling_cost = 15 - requirements = list(60,60,60,55,50,50,50,50,50,50) - property_weights = list("story_potential" = 1, "trust" = -1, "chaos" = 2, "extended" = -1, "valid" = 2) - antag_cap = list(1,1,1,1,2,2,2,2,3,3) - high_population_requirement = 50 - - -/datum/dynamic_ruleset/minor/heretics/pre_execute() - var/mob/picked_candidate = pick_n_take(candidates) - assigned += picked_candidate.mind - picked_candidate.mind.restricted_roles = restricted_roles - picked_candidate.mind.special_role = ROLE_HERETIC - var/datum/antagonist/heretic/new_antag = new antag_datum() - picked_candidate.mind.add_antag_datum(new_antag) - return TRUE - -////////////////////////////////////////////// -// // -// DEVIL // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/minor/devil - name = "Devil" - config_tag = "devil" - antag_flag = ROLE_DEVIL - antag_datum = /datum/antagonist/devil - restricted_roles = list("Lawyer", "Curator", "Chaplain", "Head of Security", "Captain", "AI") - required_candidates = 1 - weight = 3 - cost = 0 - requirements = list(101,101,101,101,101,101,101,101,101,101) - high_population_requirement = 101 - antag_cap = list(1,1,1,2,2,2,3,3,3,4) - property_weights = list("extended" = 1) - -/datum/dynamic_ruleset/minor/devil/pre_execute() - var/mob/devil = pick_n_take(candidates) - assigned += devil.mind - devil.mind.special_role = ROLE_DEVIL - devil.mind.restricted_roles = restricted_roles - - log_game("[key_name(devil)] has been selected as a devil") - add_devil(devil, ascendable = TRUE) - add_devil_objectives(devil.mind,2) - return TRUE - -/datum/dynamic_ruleset/minor/devil/proc/add_devil_objectives(datum/mind/devil_mind, quantity) - var/list/validtypes = list(/datum/objective/devil/soulquantity, /datum/objective/devil/soulquality, /datum/objective/devil/sintouch, /datum/objective/devil/buy_target) - var/datum/antagonist/devil/D = devil_mind.has_antag_datum(/datum/antagonist/devil) - for(var/i = 1 to quantity) - var/type = pick(validtypes) - var/datum/objective/devil/objective = new type(null) - objective.owner = devil_mind - D.objectives += objective - if(!istype(objective, /datum/objective/devil/buy_target)) - validtypes -= type - else - objective.find_target() - -////////////////////////////////////////////// -// // -// BLOODSUCKERS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/minor/bloodsucker - name = "Bloodsuckers" - config_tag = "bloodsucker" - antag_flag = ROLE_BLOODSUCKER - antag_datum = ANTAG_DATUM_BLOODSUCKER - minimum_required_age = 0 - protected_roles = list("Chaplain", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") - restricted_roles = list("Cyborg", "AI") - required_candidates = 1 - weight = 2 - cost = 15 - scaling_cost = 10 - property_weights = list("story_potential" = 1, "extended" = 1, "trust" = -2, "valid" = 1) - requirements = list(70,65,60,55,50,50,50,50,50,50) - high_population_requirement = 50 - -/datum/dynamic_ruleset/minor/bloodsucker/execute() - var/mob/M = pick_n_take(candidates) - assigned += M.mind - M.mind.special_role = ROLE_BLOODSUCKER - M.mind.restricted_roles = restricted_roles - mode.check_start_sunlight() - if(mode.make_bloodsucker(M.mind)) - mode.bloodsuckers += M.mind - return TRUE diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm index 1be5cb3c57..46b918b39c 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm @@ -7,25 +7,25 @@ /datum/dynamic_ruleset/roundstart/traitor name = "Traitors" - config_tag = "traitor" + persistent = TRUE antag_flag = ROLE_TRAITOR - antag_datum = /datum/antagonist/traitor/ + antag_datum = /datum/antagonist/traitor minimum_required_age = 0 - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster", "Cyborg") - restricted_roles = list("Cyborg", "AI") + protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain") + restricted_roles = list("AI", "Cyborg") required_candidates = 1 - flags = TRAITOR_RULESET | MINOR_RULESET | ALWAYS_MAX_WEIGHT_RULESET weight = 5 - cost = 10 // Avoid raising traitor threat above 10, as it is the default low cost ruleset. - scaling_cost = 10 - requirements = list(50,50,50,50,50,50,50,50,50,50) - high_population_requirement = 40 - antag_cap = list(1,1,1,1,2,2,2,2,3,3) - property_weights = list("story_potential" = 2, "trust" = -1, "extended" = 1, "valid" = 1) - var/autotraitor_cooldown = 450 // 15 minutes (ticks once per 2 sec) + 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("denominator" = 24) + var/autotraitor_cooldown = (15 MINUTES) + COOLDOWN_DECLARE(autotraitor_cooldown_check) -/datum/dynamic_ruleset/roundstart/traitor/pre_execute() - var/num_traitors = antag_cap[indice_pop] * (scaled_times + 1) +/datum/dynamic_ruleset/roundstart/traitor/pre_execute(population) + . = ..() + COOLDOWN_START(src, autotraitor_cooldown_check, autotraitor_cooldown) + var/num_traitors = get_antag_cap(population) * (scaled_times + 1) for (var/i = 1 to num_traitors) var/mob/M = pick_n_take(candidates) assigned += M.mind @@ -33,6 +33,12 @@ M.mind.restricted_roles = restricted_roles return TRUE +/datum/dynamic_ruleset/roundstart/traitor/rule_process() + if (COOLDOWN_FINISHED(src, autotraitor_cooldown_check)) + COOLDOWN_START(src, autotraitor_cooldown_check, autotraitor_cooldown) + log_game("DYNAMIC: Checking if we can turn someone into a traitor.") + mode.picking_specific_rule(/datum/dynamic_ruleset/midround/autotraitor) + ////////////////////////////////////////// // // // BLOOD BROTHERS // @@ -41,24 +47,22 @@ /datum/dynamic_ruleset/roundstart/traitorbro name = "Blood Brothers" - config_tag = "traitorbro" antag_flag = ROLE_BROTHER - antag_datum = /datum/antagonist/brother - restricted_roles = list("AI", "Cyborg") - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + antag_datum = /datum/antagonist/brother/ + protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain") + restricted_roles = list("Cyborg", "AI") required_candidates = 2 - flags = MINOR_RULESET weight = 4 - cost = 10 - requirements = list(101,101,101,101,101,101,101,101,101,101) - high_population_requirement = 101 - 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. - property_weights = list("story_potential" = 1, "trust" = -1, "extended" = 1, "valid" = 1) + cost = 15 + scaling_cost = 15 + requirements = list(40,30,30,20,20,15,15,15,10,10) + 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() - var/num_teams = (antag_cap[indice_pop]/min_team_size) * (scaled_times + 1) // 1 team per scaling +/datum/dynamic_ruleset/roundstart/traitorbro/pre_execute(population) + . = ..() + 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 @@ -91,24 +95,20 @@ /datum/dynamic_ruleset/roundstart/changeling name = "Changelings" - config_tag = "changeling" antag_flag = ROLE_CHANGELING antag_datum = /datum/antagonist/changeling + protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain") restricted_roles = list("AI", "Cyborg") - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") required_candidates = 1 - flags = MINOR_RULESET weight = 3 - cost = 15 - scaling_cost = 15 - requirements = list(101,101,101,101,101,101,101,101,101,101) - property_weights = list("trust" = -2, "valid" = 2) - high_population_requirement = 10 - antag_cap = list(1,1,1,1,1,2,2,2,2,3) - var/team_mode_probability = 30 + cost = 16 + scaling_cost = 10 + requirements = list(70,70,60,50,40,20,20,10,10,10) + antag_cap = list("denominator" = 29) -/datum/dynamic_ruleset/roundstart/changeling/pre_execute() - var/num_changelings = antag_cap[indice_pop] * (scaled_times + 1) +/datum/dynamic_ruleset/roundstart/changeling/pre_execute(population) + . = ..() + 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 @@ -117,21 +117,8 @@ return TRUE /datum/dynamic_ruleset/roundstart/changeling/execute() - var/team_mode = FALSE - if(prob(team_mode_probability)) - team_mode = TRUE - var/list/team_objectives = subtypesof(/datum/objective/changeling_team_objective) - var/list/possible_team_objectives = list() - for(var/T in team_objectives) - var/datum/objective/changeling_team_objective/CTO = T - if(assigned.len >= initial(CTO.min_lings)) - possible_team_objectives += T - - if(possible_team_objectives.len && prob(20*assigned.len)) - GLOB.changeling_team_objective_type = pick(possible_team_objectives) for(var/datum/mind/changeling in assigned) var/datum/antagonist/changeling/new_antag = new antag_datum() - new_antag.team_mode = team_mode changeling.add_antag_datum(new_antag) return TRUE @@ -143,24 +130,21 @@ /datum/dynamic_ruleset/roundstart/heretics name = "Heretics" - antag_flag = "heretic" + antag_flag = ROLE_HERETIC antag_datum = /datum/antagonist/heretic protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain") restricted_roles = list("AI", "Cyborg") required_candidates = 1 - flags = MINOR_RULESET weight = 3 - cost = 25 - scaling_cost = 15 - requirements = list(60,60,60,55,50,50,50,50,50,50) - property_weights = list("story_potential" = 1, "trust" = -1, "chaos" = 2, "extended" = -1, "valid" = 2) - antag_cap = list(1,1,1,1,2,2,2,2,3,3) - high_population_requirement = 50 + cost = 15 + scaling_cost = 9 + requirements = list(101,101,101,40,35,20,20,15,10,10) + 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) @@ -175,9 +159,9 @@ var/datum/mind/cultie = c var/datum/antagonist/heretic/new_antag = new antag_datum() cultie.add_antag_datum(new_antag) - return TRUE + ////////////////////////////////////////////// // // // WIZARDS // @@ -187,18 +171,15 @@ // Dynamic is a wonderful thing that adds wizards to every round and then adds even more wizards during the round. /datum/dynamic_ruleset/roundstart/wizard name = "Wizard" - config_tag = "wizard" - persistent = TRUE 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 = 1 - cost = 30 - requirements = list(101,101,101,60,50,50,50,50,50,50) - high_population_requirement = 50 - property_weights = list("story_potential" = 2, "trust" = 1, "chaos" = 2, "extended" = -2, "valid" = 2) + weight = 2 + cost = 20 + requirements = list(90,90,90,80,60,40,30,20,10,10) var/list/roundstart_wizards = list() /datum/dynamic_ruleset/roundstart/wizard/acceptable(population=0, threat=0) @@ -209,9 +190,9 @@ return ..() /datum/dynamic_ruleset/roundstart/wizard/pre_execute() + . = ..() if(GLOB.wizardstart.len == 0) return FALSE - mode.antags_rolled += 1 var/mob/M = pick_n_take(candidates) if (M) assigned += M.mind @@ -224,25 +205,8 @@ for(var/datum/mind/M in assigned) M.current.forceMove(pick(GLOB.wizardstart)) M.add_antag_datum(new antag_datum()) - roundstart_wizards += M return TRUE -/datum/dynamic_ruleset/roundstart/wizard/rule_process() // i can literally copy this from are_special_antags_dead it's great - for(var/datum/mind/wizard in roundstart_wizards) - if(isliving(wizard.current) && wizard.current.stat!=DEAD) - return FALSE - - for(var/obj/item/phylactery/P in GLOB.poi_list) //TODO : IsProperlyDead() - if(P.mind && P.mind.has_antag_datum(/datum/antagonist/wizard)) - return FALSE - - if(SSevents.wizardmode) //If summon events was active, turn it off - SSevents.toggleWizardmode() - SSevents.resetFrequency() - - return RULESET_STOP_PROCESSING - - ////////////////////////////////////////////// // // // BLOOD CULT // @@ -251,29 +215,25 @@ /datum/dynamic_ruleset/roundstart/bloodcult name = "Blood Cult" - config_tag = "cult" antag_flag = ROLE_CULTIST antag_datum = /datum/antagonist/cult minimum_required_age = 14 - restricted_roles = list("AI", "Cyborg") - protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + restricted_roles = list("AI", "Cyborg", "Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Chaplain", "Head of Personnel") required_candidates = 2 weight = 3 - cost = 30 - requirements = list(101,101,101,80,70,60,50,50,50,50) - property_weights = list("story_potential" = -1, "trust" = -1, "chaos" = 1, "conversion" = 1, "extended" = -2, "valid" = 2) - high_population_requirement = 50 - flags = HIGHLANDER_RULESET - antag_cap = list(2,2,2,3,3,4,4,4,4,4) + cost = 20 + requirements = list(100,90,80,60,40,30,10,10,10,10) + 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() - var/cultists = antag_cap[indice_pop] - mode.antags_rolled += cultists +/datum/dynamic_ruleset/roundstart/bloodcult/pre_execute(population) + . = ..() + var/cultists = get_antag_cap(population) for(var/cultists_number = 1 to cultists) if(candidates.len <= 0) break @@ -310,7 +270,6 @@ /datum/dynamic_ruleset/roundstart/nuclear name = "Nuclear Emergency" - config_tag = "nuclear" antag_flag = ROLE_OPERATIVE antag_datum = /datum/antagonist/nukeop var/datum/antagonist/antag_leader_datum = /datum/antagonist/nukeop/leader @@ -318,22 +277,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 - requirements = list(100,90,80,70,60,50,50,50,50,50) - high_population_requirement = 50 - flags = HIGHLANDER_RULESET - antag_cap = list(1,1,2,3,4,5,5,5,5,5) - property_weights = list("story_potential" = 2, "trust" = 2, "chaos" = 2, "extended" = -2, "valid" = 2) + cost = 20 + requirements = list(90,90,90,80,60,40,30,20,10,10) + 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] - mode.antags_rolled += operatives + var/operatives = get_antag_cap(population) for(var/operatives_number = 1 to operatives) if(candidates.len <= 0) break @@ -356,7 +313,7 @@ return TRUE /datum/dynamic_ruleset/roundstart/nuclear/round_result() - var result = nuke_team.get_result() + var/result = nuke_team.get_result() switch(result) if(NUKE_RESULT_FLUKE) SSticker.mode_result = "loss - syndicate nuked - disk secured" @@ -391,36 +348,36 @@ ////////////////////////////////////////////// // // -// REVS // +// REVS // // // ////////////////////////////////////////////// /datum/dynamic_ruleset/roundstart/revs name = "Revolution" - config_tag = "revolution" persistent = TRUE antag_flag = ROLE_REV_HEAD antag_flag_override = ROLE_REV antag_datum = /datum/antagonist/rev/head minimum_required_age = 14 - restricted_roles = list("AI", "Cyborg", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + 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 - requirements = list(101,101,101,60,50,50,50,50,50,50) - high_population_requirement = 50 - antag_cap = list(3,3,3,3,3,3,3,3,3,3) - flags = HIGHLANDER_RULESET + cost = 20 + requirements = list(101,101,70,40,30,20,10,10,10,10) + 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 - property_weights = list("trust" = -2, "chaos" = 2, "extended" = -2, "valid" = 2, "conversion" = 1) + /// How much threat should be injected when the revolution wins? + var/revs_win_threat_injection = 20 var/datum/team/revolution/revolution var/finished = FALSE -/datum/dynamic_ruleset/roundstart/revs/pre_execute() - var/max_candidates = antag_cap[indice_pop] - mode.antags_rolled += max_candidates +/datum/dynamic_ruleset/roundstart/revs/pre_execute(population) + . = ..() + var/max_candidates = get_antag_cap(population) for(var/i = 1 to max_candidates) if(candidates.len <= 0) break @@ -431,7 +388,6 @@ return TRUE /datum/dynamic_ruleset/roundstart/revs/execute() - var/success = TRUE revolution = new() for(var/datum/mind/M in assigned) if(check_eligible(M)) @@ -443,14 +399,12 @@ else assigned -= M log_game("DYNAMIC: [ruletype] [name] discarded [M.name] from head revolutionary due to ineligibility.") - if(!revolution.members.len) - success = FALSE - log_game("DYNAMIC: [ruletype] [name] failed to get any eligible headrevs. Refunding [cost] threat.") - if(success) + if(revolution.members.len) revolution.update_objectives() revolution.update_heads() - SSshuttle.registerHostileEnvironment(src) + SSshuttle.registerHostileEnvironment(revolution) return TRUE + log_game("DYNAMIC: [ruletype] [name] failed to get any eligible headrevs. Refunding [cost] threat.") return FALSE /datum/dynamic_ruleset/roundstart/revs/clean_up() @@ -458,61 +412,77 @@ ..() /datum/dynamic_ruleset/roundstart/revs/rule_process() - if(check_rev_victory()) - finished = REVOLUTION_VICTORY - return RULESET_STOP_PROCESSING - else if (check_heads_victory()) - finished = STATION_VICTORY - SSshuttle.clearHostileEnvironment(src) - priority_announce("It appears the mutiny has been quelled. Please return yourself and your incapacitated colleagues to work. \ - We have remotely blacklisted the head revolutionaries from your cloning software to prevent accidental cloning.", null, "attention", null, "Central Command Loyalty Monitoring Division") + var/winner = revolution.process_victory(revs_win_threat_injection) + if (isnull(winner)) + return - for(var/datum/mind/M in revolution.members) // Remove antag datums and prevents podcloned or exiled headrevs restarting rebellions. - if(M.has_antag_datum(/datum/antagonist/rev/head)) - var/datum/antagonist/rev/head/R = M.has_antag_datum(/datum/antagonist/rev/head) - R.remove_revolutionary(FALSE, "gamemode") - var/mob/living/carbon/C = M.current - if(C.stat == DEAD) - C.makeUncloneable() - if(M.has_antag_datum(/datum/antagonist/rev)) - var/datum/antagonist/rev/R = M.has_antag_datum(/datum/antagonist/rev) - R.remove_revolutionary(FALSE, "gamemode") - return RULESET_STOP_PROCESSING + finished = winner + return RULESET_STOP_PROCESSING /// Checks for revhead loss conditions and other antag datums. -/datum/dynamic_ruleset/roundstart/revs/proc/check_eligible(var/datum/mind/M) +/datum/dynamic_ruleset/roundstart/revs/proc/check_eligible(datum/mind/M) var/turf/T = get_turf(M.current) if(!considered_afk(M) && considered_alive(M) && is_station_level(T.z) && !M.antag_datums?.len && !HAS_TRAIT(M, TRAIT_MINDSHIELD)) return TRUE return FALSE -/datum/dynamic_ruleset/roundstart/revs/check_finished() - if(finished == REVOLUTION_VICTORY) - return TRUE - else - return ..() - -/datum/dynamic_ruleset/roundstart/revs/proc/check_rev_victory() - for(var/datum/objective/mutiny/objective in revolution.objectives) - if(!(objective.check_completion())) - return FALSE - return TRUE - -/datum/dynamic_ruleset/roundstart/revs/proc/check_heads_victory() - for(var/datum/mind/rev_mind in revolution.head_revolutionaries()) - var/turf/T = get_turf(rev_mind.current) - if(!considered_afk(rev_mind) && considered_alive(rev_mind) && is_station_level(T.z)) - if(ishuman(rev_mind.current) || ismonkey(rev_mind.current)) - return FALSE - return TRUE - /datum/dynamic_ruleset/roundstart/revs/round_result() - if(finished == REVOLUTION_VICTORY) - SSticker.mode_result = "win - heads killed" - SSticker.news_report = REVS_WIN - else if(finished == STATION_VICTORY) - SSticker.mode_result = "loss - rev heads killed" - SSticker.news_report = REVS_LOSE + revolution.round_result(finished) + +////////////////////////////////////////////// +// // +// Clock Cult // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/clockcult + name = "Clock Cult" + antag_flag = ROLE_SERVANT_OF_RATVAR + antag_datum = /datum/antagonist/clockcult + minimum_required_age = 14 + restricted_roles = list("AI", "Cyborg", "Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Chaplain", "Head of Personnel") + required_candidates = 2 + weight = 3 + cost = 20 + requirements = list(100,90,80,60,40,30,10,10,10,10) + flags = HIGH_IMPACT_RULESET + antag_cap = list("denominator" = 20, "offset" = 1) + var/datum/team/clockcult/main_clockcult + +/datum/dynamic_ruleset/roundstart/clockcult/ready(population, forced = FALSE) + required_candidates = get_antag_cap(population) + . = ..() + +/datum/dynamic_ruleset/roundstart/clockcult/pre_execute(population) + . = ..() + var/cultists = get_antag_cap(population) + for(var/cultists_number = 1 to cultists) + if(candidates.len <= 0) + break + var/mob/M = pick_n_take(candidates) + assigned += M.mind + M.mind.special_role = ROLE_SERVANT_OF_RATVAR + M.mind.restricted_roles = restricted_roles + return TRUE + +/datum/dynamic_ruleset/roundstart/clockcult/execute() + main_clockcult = new + for(var/datum/mind/M in assigned) + var/datum/antagonist/clockcult/new_cultist = new antag_datum() + new_cultist.clock_team = main_clockcult + new_cultist.equip_servant() + M.add_antag_datum(new_cultist) + main_clockcult.setup_objectives() + return TRUE + +/datum/dynamic_ruleset/roundstart/clockcult/round_result() + ..() + if(main_clockcult.check_cult_victory()) + SSticker.mode_result = "win - servants completed their objective (summon ratvar)" + SSticker.news_report = CLOCK_SUMMON + else + SSticker.mode_result = "loss - servants failed their objective (summon ratvar)" + SSticker.news_report = CULT_FAILURE // Admin only rulesets. The threat requirement is 101 so it is not possible to roll them. @@ -524,7 +494,6 @@ /datum/dynamic_ruleset/roundstart/extended name = "Extended" - config_tag = "extended" antag_flag = null antag_datum = null restricted_roles = list() @@ -532,121 +501,17 @@ weight = 3 cost = 0 requirements = list(101,101,101,101,101,101,101,101,101,101) - property_weights = list("extended" = 2) - high_population_requirement = 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 -////////////////////////////////////////////// -// // -// CLOCKCULT // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/clockcult - name = "Clockcult" - config_tag = "clockwork_cult" - antag_flag = ROLE_SERVANT_OF_RATVAR - antag_datum = /datum/antagonist/clockcult - restricted_roles = list("AI", "Cyborg", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") - required_candidates = 4 - weight = 3 - cost = 35 - requirements = list(101,101,101,80,70,60,50,50,50,50) - high_population_requirement = 50 - flags = HIGHLANDER_RULESET - antag_cap = list(2,3,3,4,4,4,4,4,4,4) - property_weights = list("trust" = 2, "chaos" = 2, "extended" = -2, "conversion" = 1, "valid" = 2) - var/ark_time - -/datum/dynamic_ruleset/roundstart/clockcult/pre_execute() - var/list/errorList = list() - var/list/reebes = SSmapping.LoadGroup(errorList, "Reebe", "map_files/generic", "City_of_Cogs.dmm", default_traits = ZTRAITS_REEBE, silent = TRUE) - if(errorList.len) - message_admins("Reebe failed to load!") - log_game("Reebe failed to load!") - return FALSE - for(var/datum/parsed_map/PM in reebes) - PM.initTemplateBounds() - - var/starter_servants = antag_cap[indice_pop] - var/number_players = mode.roundstart_pop_ready - if(number_players > 30) - number_players -= 30 - starter_servants += min(round(number_players / 10), 5) - mode.antags_rolled += starter_servants - GLOB.clockwork_vitality += 50 * starter_servants - for (var/i in 1 to starter_servants) - var/mob/servant = pick_n_take(candidates) - assigned += servant.mind - servant.mind.assigned_role = ROLE_SERVANT_OF_RATVAR - servant.mind.special_role = ROLE_SERVANT_OF_RATVAR - ark_time = 30 + round((number_players / 5)) - ark_time = min(ark_time, 35) - return TRUE - -/datum/dynamic_ruleset/roundstart/clockcult/execute() - var/list/spread_out_spawns = GLOB.servant_spawns.Copy() - for(var/datum/mind/servant in assigned) - var/mob/S = servant.current - if(!spread_out_spawns.len) - spread_out_spawns = GLOB.servant_spawns.Copy() - log_game("[key_name(servant)] was made an initial servant of Ratvar") - var/turf/T = pick_n_take(spread_out_spawns) - S.forceMove(T) - greet_servant(S) - equip_servant(S) - add_servant_of_ratvar(S, TRUE) - var/obj/structure/destructible/clockwork/massive/celestial_gateway/G = GLOB.ark_of_the_clockwork_justiciar //that's a mouthful - G.final_countdown(ark_time) - return TRUE - -/datum/dynamic_ruleset/roundstart/clockcult/proc/greet_servant(mob/M) //Description of their role - if(!M) - return 0 - to_chat(M, "You are a servant of Ratvar, the Clockwork Justiciar!") - to_chat(M, "You have approximately [ark_time] minutes until the Ark activates.") - to_chat(M, "Unlock Script scripture by converting a new servant.") - to_chat(M, "Application scripture will be unlocked halfway until the Ark's activation.") - M.playsound_local(get_turf(M), 'sound/ambience/antag/clockcultalr.ogg', 100, FALSE, pressure_affected = FALSE) - return 1 - -/datum/dynamic_ruleset/roundstart/clockcult/proc/equip_servant(mob/living/M) //Grants a clockwork slab to the mob, with one of each component - if(!M || !ishuman(M)) - return FALSE - var/mob/living/carbon/human/L = M - L.equipOutfit(/datum/outfit/servant_of_ratvar) - var/obj/item/clockwork/slab/S = new - var/slot = "At your feet" - var/list/slots = list("In your left pocket" = SLOT_L_STORE, "In your right pocket" = SLOT_R_STORE, "In your backpack" = SLOT_IN_BACKPACK, "On your belt" = SLOT_BELT) - if(ishuman(L)) - var/mob/living/carbon/human/H = L - slot = H.equip_in_one_of_slots(S, slots) - if(slot == "In your backpack") - slot = "In your [H.back.name]" - if(slot == "At your feet") - if(!S.forceMove(get_turf(L))) - qdel(S) - if(S && !QDELETED(S)) - to_chat(L, "There is a paper in your backpack! It'll tell you if anything's changed, as well as what to expect.") - to_chat(L, "[slot] is a clockwork slab, a multipurpose tool used to construct machines and invoke ancient words of power. If this is your first time \ - as a servant, you can find a concise tutorial in the Recollection category of its interface.") - to_chat(L, "If you want more information, you can read the wiki page to learn more.") - return TRUE - return FALSE - -/datum/dynamic_ruleset/roundstart/clockcult/round_result() - if(GLOB.clockwork_gateway_activated) - SSticker.news_report = CLOCK_SUMMON - SSticker.mode_result = "win - servants completed their objective (summon ratvar)" - else - SSticker.news_report = CULT_FAILURE - SSticker.mode_result = "loss - servants failed their objective (summon ratvar)" - ////////////////////////////////////////////// // // // CLOWN OPS // @@ -655,12 +520,9 @@ /datum/dynamic_ruleset/roundstart/nuclear/clown_ops name = "Clown Ops" - config_tag = "clownops" antag_datum = /datum/antagonist/nukeop/clownop antag_leader_datum = /datum/antagonist/nukeop/leader/clownop - weight = 1 - property_weights = list("trust" = 2, "chaos" = 2, "extended" = -2, "story_potential" = 2, "valid" = 2) - + requirements = list(101,101,101,101,101,101,101,101,101,101) /datum/dynamic_ruleset/roundstart/nuclear/clown_ops/pre_execute() . = ..() @@ -674,59 +536,6 @@ V.assigned_role = "Clown Operative" V.special_role = "Clown Operative" -////////////////////////////////////////////// -// // -// DEVIL // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/devil - name = "Devil" - config_tag = "devil" - antag_flag = ROLE_DEVIL - antag_datum = /datum/antagonist/devil - restricted_roles = list("Lawyer", "Curator", "Chaplain", "Head of Security", "Captain", "AI") - required_candidates = 1 - flags = MINOR_RULESET - weight = 3 - cost = 0 - requirements = list(101,101,101,101,101,101,101,101,101,101) - high_population_requirement = 101 - antag_cap = list(1,1,1,2,2,2,3,3,3,4) - property_weights = list("extended" = 1) - -/datum/dynamic_ruleset/roundstart/devil/pre_execute() - var/num_devils = antag_cap[indice_pop] - mode.antags_rolled += num_devils - for(var/j = 0, j < num_devils, j++) - if (!candidates.len) - break - var/mob/devil = pick_n_take(candidates) - assigned += devil.mind - devil.mind.special_role = ROLE_DEVIL - devil.mind.restricted_roles = restricted_roles - - log_game("[key_name(devil)] has been selected as a devil") - return TRUE - -/datum/dynamic_ruleset/roundstart/devil/execute() - for(var/datum/mind/devil in assigned) - add_devil(devil.current, ascendable = TRUE) - add_devil_objectives(devil,2) - return TRUE - -/datum/dynamic_ruleset/roundstart/devil/proc/add_devil_objectives(datum/mind/devil_mind, quantity) - var/list/validtypes = list(/datum/objective/devil/soulquantity, /datum/objective/devil/soulquality, /datum/objective/devil/sintouch, /datum/objective/devil/buy_target) - var/datum/antagonist/devil/D = devil_mind.has_antag_datum(/datum/antagonist/devil) - for(var/i = 1 to quantity) - var/type = pick(validtypes) - var/datum/objective/devil/objective = new type(null) - objective.owner = devil_mind - D.objectives += objective - if(!istype(objective, /datum/objective/devil/buy_target)) - validtypes -= type - else - objective.find_target() ////////////////////////////////////////////// // // @@ -736,24 +545,22 @@ /datum/dynamic_ruleset/roundstart/monkey name = "Monkey" - config_tag = "monkey" antag_flag = ROLE_MONKEY antag_datum = /datum/antagonist/monkey/leader - restricted_roles = list("Cyborg", "AI") + restricted_roles = list("Cyborg", "AI", "Prisoner") required_candidates = 1 weight = 3 cost = 0 requirements = list(101,101,101,101,101,101,101,101,101,101) - high_population_requirement = 101 - property_weights = list("extended" = -2, "chaos" = 2, "conversion" = 1, "valid" = 2) + flags = LONE_RULESET var/players_per_carrier = 30 var/monkeys_to_win = 1 var/escaped_monkeys = 0 var/datum/team/monkey/monkey_team /datum/dynamic_ruleset/roundstart/monkey/pre_execute() + . = ..() var/carriers_to_make = max(round(mode.roundstart_pop_ready / players_per_carrier, 1), 1) - mode.antags_rolled += carriers_to_make for(var/j = 0, j < carriers_to_make, j++) if (!candidates.len) @@ -767,7 +574,7 @@ /datum/dynamic_ruleset/roundstart/monkey/execute() for(var/datum/mind/carrier in assigned) - var/datum/antagonist/monkey/M = add_monkey_leader(carrier) + var/datum/antagonist/monkey/M = carrier.add_antag_datum(/datum/antagonist/monkey/leader) if(M) monkey_team = M.monkey_team return TRUE @@ -776,7 +583,9 @@ if(SSshuttle.emergency.mode != SHUTTLE_ENDGAME) return FALSE var/datum/disease/D = new /datum/disease/transformation/jungle_fever() - for(var/mob/living/carbon/monkey/M in GLOB.alive_mob_list) + for(var/mob/living/carbon/human/M in GLOB.alive_mob_list) + if(!ismonkey(M)) + continue if (M.HasDisease(D)) if(M.onCentCom() || M.onSyndieBase()) escaped_monkeys++ @@ -800,16 +609,14 @@ /datum/dynamic_ruleset/roundstart/meteor name = "Meteor" - config_tag = "meteor" persistent = TRUE required_candidates = 0 weight = 3 cost = 0 requirements = list(101,101,101,101,101,101,101,101,101,101) - high_population_requirement = 101 - property_weights = list("extended" = -2, "chaos" = 2, "trust" = 2) + flags = LONE_RULESET var/meteordelay = 2000 - var/nometeors = 0 + var/nometeors = FALSE var/rampupdelta = 5 /datum/dynamic_ruleset/roundstart/meteor/rule_process() @@ -828,43 +635,3 @@ var/ramp_up_final = clamp(round(meteorminutes/rampupdelta), 1, 10) spawn_meteors(ramp_up_final, wavetype) - -////////////////////////////////////////////// -// // -// BLOODSUCKERS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/bloodsucker - name = "Bloodsuckers" - config_tag = "bloodsucker" - antag_flag = ROLE_BLOODSUCKER - antag_datum = ANTAG_DATUM_BLOODSUCKER - minimum_required_age = 0 - protected_roles = list("Chaplain", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") - restricted_roles = list("Cyborg", "AI") - required_candidates = 1 - flags = MINOR_RULESET - weight = 2 - cost = 15 - scaling_cost = 10 - property_weights = list("story_potential" = 1, "extended" = 1, "trust" = -2, "valid" = 1) - requirements = list(70,65,60,55,50,50,50,50,50,50) - high_population_requirement = 50 - antag_cap = list(1,1,1,1,1,2,2,2,2,2) - -/datum/dynamic_ruleset/roundstart/bloodsucker/pre_execute() - var/num_bloodsuckers = antag_cap[indice_pop] * (scaled_times + 1) - for (var/i = 1 to num_bloodsuckers) - var/mob/M = pick_n_take(candidates) - assigned += M.mind - M.mind.special_role = ROLE_BLOODSUCKER - M.mind.restricted_roles = restricted_roles - return TRUE - -/datum/dynamic_ruleset/roundstart/bloodsucker/execute() - mode.check_start_sunlight() - for(var/datum/mind/M in assigned) - if(mode.make_bloodsucker(M)) - mode.bloodsuckers += M - return TRUE diff --git a/code/game/gamemodes/dynamic/dynamic_simulations.dm b/code/game/gamemodes/dynamic/dynamic_simulations.dm new file mode 100644 index 0000000000..63da54bece --- /dev/null +++ b/code/game/gamemodes/dynamic/dynamic_simulations.dm @@ -0,0 +1,122 @@ +#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 = new /datum/game_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 + +#endif diff --git a/code/game/gamemodes/dynamic/dynamic_storytellers.dm b/code/game/gamemodes/dynamic/dynamic_storytellers.dm deleted file mode 100644 index 927ab7796b..0000000000 --- a/code/game/gamemodes/dynamic/dynamic_storytellers.dm +++ /dev/null @@ -1,388 +0,0 @@ -/datum/dynamic_storyteller - var/name = "none" // Name for voting. - var/config_tag = null // Config tag for config weights. - var/desc = "A coder's idiocy." // Description for voting. - var/list/property_weights = list() // See below. - var/curve_centre = 0 // As GLOB.dynamic_curve_centre. - var/curve_width = 1.8 // As GLOB.dynamic_curve_width. - var/forced_threat_level = -1 // As GLOB.dynamic_forced_threat_level - /* - NO_ASSASSIN: Will not have permanent assassination targets. - WAROPS_ALWAYS_ALLOWED: Can always do warops, regardless of threat level. - USE_PREF_WEIGHTS: Will use peoples' preferences to change the threat centre. - FORCE_IF_WON: If this mode won the vote, forces it - USE_PREV_ROUND_WEIGHTS: Changes its threat centre based on the average chaos of previous rounds. - */ - var/flags = 0 - var/dead_player_weight = 1 // How much dead players matter for threat calculation - var/weight = 0 // Weights for randomly picking storyteller. Multiplied by score after voting. - var/min_chaos = -1000 // Won't show up if recent rounds have been below this chaotic on average - var/max_chaos = 1000 // Won't show up if recent rounds have been above this chaotic on average - var/event_frequency_lower = 6 MINUTES // How rare events will be, at least. - var/event_frequency_upper = 20 MINUTES // How rare events will be, at most. - var/min_players = -1 // How many players are required for this one to start. - var/soft_antag_ratio_cap = 4 // how many players-per-antag there should be - var/datum/game_mode/dynamic/mode = null // Cached as soon as it's made, by dynamic. - -/** -Property weights are added to the config weight of the ruleset. They are: -"story_potential" -- essentially how many different ways the antag can be played. -"trust" -- How much it makes the crew trust each other. Negative values means they're suspicious. Team antags are like this. -"chaos" -- How chaotic it makes the round. Has some overlap with "valid" and somewhat contradicts "extended". -"valid" -- How likely the non-antag-enemy crew are to get involved, e.g. nukies encouraging the warden to - let everyone into the armory, wizard moving around and being a nuisance, nightmare busting lights. -"extended" -- How much the antag is conducive to a long round. Nukies and cults are bad for this; Wizard is less bad; and so on. -"conversion" -- Basically a bool. Conversion antags, well, convert. It's in its own class 'cause people kinda hate conversion. -*/ - -/datum/dynamic_storyteller/proc/minor_start_chance() - return clamp(60 - mode.threat_level,0,100) // by default higher threat = lower chance of minor round - -/datum/dynamic_storyteller/proc/start_injection_cooldowns() - var/latejoin_injection_cooldown_middle = 0.5*(GLOB.dynamic_first_latejoin_delay_max + GLOB.dynamic_first_latejoin_delay_min) - mode.latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), GLOB.dynamic_first_latejoin_delay_min, GLOB.dynamic_first_latejoin_delay_max)) + world.time - - var/midround_injection_cooldown_middle = 0.5*(GLOB.dynamic_first_midround_delay_min + GLOB.dynamic_first_midround_delay_max) - mode.midround_injection_cooldown = round(clamp(EXP_DISTRIBUTION(midround_injection_cooldown_middle), GLOB.dynamic_first_midround_delay_min, GLOB.dynamic_first_midround_delay_max)) + world.time - -/datum/dynamic_storyteller/proc/do_process() - return - -/datum/dynamic_storyteller/proc/on_start() - if (istype(SSticker.mode, /datum/game_mode/dynamic)) - mode = SSticker.mode - GLOB.dynamic_curve_centre = curve_centre - GLOB.dynamic_curve_width = curve_width - if(flags & USE_PREF_WEIGHTS) - var/voters = 0 - var/mean = 0 - for(var/client/c in GLOB.clients) - var/vote = c.prefs.preferred_chaos - if(vote) - voters += 1 - switch(vote) - if(CHAOS_NONE) - mean -= 5 - if(CHAOS_LOW) - mean -= 2.5 - if(CHAOS_HIGH) - mean += 2.5 - if(CHAOS_MAX) - mean += 5 - else - voters += 0.5 - if(voters) - GLOB.dynamic_curve_centre += (mean/voters) - if(flags & USE_PREV_ROUND_WEIGHTS) - GLOB.dynamic_curve_centre += (SSpersistence.average_threat) / 10 - GLOB.dynamic_forced_threat_level = forced_threat_level - -/datum/dynamic_storyteller/proc/get_midround_cooldown() - var/midround_injection_cooldown_middle = 0.5*(GLOB.dynamic_midround_delay_max + GLOB.dynamic_midround_delay_min) - return round(clamp(EXP_DISTRIBUTION(midround_injection_cooldown_middle), GLOB.dynamic_midround_delay_min, GLOB.dynamic_midround_delay_max)) - -/datum/dynamic_storyteller/proc/should_inject_antag(dry_run = FALSE) - if(mode.forced_injection) - mode.forced_injection = dry_run - return TRUE - if(mode.current_players[CURRENT_LIVING_PLAYERS].len < (mode.current_players[CURRENT_LIVING_ANTAGS].len * soft_antag_ratio_cap)) - return FALSE - return mode.threat < mode.threat_level - -/datum/dynamic_storyteller/proc/roundstart_draft() - var/list/drafted_rules = list() - var/minor_round_weight_mult = (100-minor_start_chance()) / 100 - for (var/datum/dynamic_ruleset/roundstart/rule in mode.roundstart_rules) - if (rule.acceptable(mode.roundstart_pop_ready, mode.threat_level)) // If we got the population and threat required - rule.candidates = mode.candidates.Copy() - rule.trim_candidates() - if (rule.ready() && rule.candidates.len > 0) - var/property_weight = 0 - for(var/property in property_weights) - if(property in rule.property_weights) // just treat it as 0 if it's not in there - property_weight += rule.property_weights[property] * property_weights[property] - var/calced_weight = (rule.get_weight() + property_weight) * rule.weight_mult - if(CHECK_BITFIELD(rule.flags, MINOR_RULESET)) - calced_weight *= minor_round_weight_mult - if(calced_weight > 0) // negatives in the list might cause problems - drafted_rules[rule] = calced_weight - return drafted_rules - -/datum/dynamic_storyteller/proc/minor_rule_draft() - var/list/drafted_rules = list() - for (var/datum/dynamic_ruleset/rule in mode.minor_rules) - if (rule.acceptable(mode.current_players[CURRENT_LIVING_PLAYERS].len, mode.threat_level)) - rule.trim_candidates() - if (rule.ready()) - var/property_weight = 0 - for(var/property in property_weights) - if(property in rule.property_weights) // just treat it as 0 if it's not in there - property_weight += rule.property_weights[property] * property_weights[property] - var/calced_weight = (rule.get_weight() + property_weight) * rule.weight_mult - if(calced_weight > 0) // negatives in the list might cause problems - drafted_rules[rule] = calced_weight - return drafted_rules - -/datum/dynamic_storyteller/proc/midround_draft() - var/list/drafted_rules = list() - for (var/datum/dynamic_ruleset/midround/rule in mode.midround_rules) - // if there are antags OR the rule is an antag rule, antag_acceptable will be true. - if (rule.acceptable(mode.current_players[CURRENT_LIVING_PLAYERS].len, mode.threat_level)) - // Classic secret : only autotraitor/minor roles - if (GLOB.dynamic_classic_secret && !((rule.flags & TRAITOR_RULESET))) - continue - rule.trim_candidates() - if (rule.ready()) - var/property_weight = 0 - for(var/property in property_weights) - if(property in rule.property_weights) // just treat it as 0 if it's not in there - property_weight += rule.property_weights[property] * property_weights[property] - var/threat_weight = 1 - if(!(rule.flags & TRAITOR_RULESET)) // makes the traitor rulesets always possible anyway - var/cost_difference = rule.cost-(mode.threat_level-mode.threat) - /* Basically, the closer the cost is to the current threat-level-away-from-threat, the more likely it is to - pick this particular ruleset. - Let's use a toy example: there's 60 threat level and 10 threat spent. - We want to pick a ruleset that's close to that, so we run the below equation, on two rulesets. - Ruleset 1 has 30 cost, ruleset 2 has 5 cost. - When we do the math, ruleset 1's threat_weight is 0.538, and ruleset 2's is 0.238, meaning ruleset 1 - is 2.26 times as likely to be picked, all other things considered. - Of course, we don't want it to GUARANTEE the closest, that's no fun, so it's just a weight. - */ - threat_weight = abs(1-abs(1-LOGISTIC_FUNCTION(2,0.05,abs(cost_difference),0))) - if(cost_difference > 0) - threat_weight /= (1+(cost_difference*0.1)) - var/calced_weight = (rule.get_weight() + property_weight) * rule.weight_mult * threat_weight - if(calced_weight > 0) - drafted_rules[rule] = calced_weight - return drafted_rules - -/datum/dynamic_storyteller/proc/latejoin_draft(mob/living/carbon/human/newPlayer) - var/list/drafted_rules = list() - for (var/datum/dynamic_ruleset/latejoin/rule in mode.latejoin_rules) - if (rule.acceptable(mode.current_players[CURRENT_LIVING_PLAYERS].len, mode.threat_level - mode.threat)) - // Classic secret : only autotraitor/minor roles - if (GLOB.dynamic_classic_secret && !((rule.flags & TRAITOR_RULESET))) - continue - // No stacking : only one round-ender, unless threat level > stacking_limit. - if (mode.threat_level > GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking) - if(rule.flags & HIGHLANDER_RULESET && mode.highlander_executed) - continue - - rule.candidates = list(newPlayer) - rule.trim_candidates() - if (rule.ready()) - var/property_weight = 0 - for(var/property in property_weights) - if(property in rule.property_weights) - property_weight += rule.property_weights[property] * property_weights[property] - var/threat_weight = 1 - if(!(rule.flags & TRAITOR_RULESET)) - var/cost_difference = rule.cost-(mode.threat_level-mode.threat) - threat_weight = 1-abs(1-(LOGISTIC_FUNCTION(2,0.05,abs(cost_difference),0))) - if(cost_difference > 0) - threat_weight /= (1+(cost_difference*0.1)) - var/calced_weight = (rule.get_weight() + property_weight) * rule.weight_mult * threat_weight - if(calced_weight > 0) - drafted_rules[rule] = calced_weight - return drafted_rules - -/datum/dynamic_storyteller/chaotic - name = "Chaotic" - config_tag = "chaotic" - curve_centre = 10 - desc = "High chaos modes. Revs, wizard, clock cult. Multiple antags at once. Chaos is kept up all round." - property_weights = list("extended" = -1, "chaos" = 1) - weight = 1 - event_frequency_lower = 2 MINUTES - event_frequency_upper = 10 MINUTES - max_chaos = 50 - soft_antag_ratio_cap = 1 - flags = WAROPS_ALWAYS_ALLOWED | FORCE_IF_WON - min_players = 30 - var/refund_cooldown = 0 - -/datum/dynamic_storyteller/chaotic/minor_start_chance() - return 0 - -/datum/dynamic_storyteller/chaotic/do_process() - if(refund_cooldown < world.time) - mode.create_threat(20) - mode.log_threat("Chaotic storyteller ramped up the chaos. Threat level is now [mode.threat_level].") - refund_cooldown = world.time + 20 MINUTES - -/datum/dynamic_storyteller/chaotic/get_midround_cooldown() - return ..() / 4 - -/datum/dynamic_storyteller/team - name = "Teamwork" - config_tag = "teamwork" - desc = "Modes where the crew must band together. Nukies, xenos, blob. Only one antag threat at once." - curve_centre = 2 - curve_width = 1.5 - weight = 4 - max_chaos = 75 - min_players = 20 - flags = WAROPS_ALWAYS_ALLOWED | USE_PREV_ROUND_WEIGHTS - property_weights = list("valid" = 3, "trust" = 5) - -/datum/dynamic_storyteller/team/minor_start_chance() - return 0 - -/datum/dynamic_storyteller/team/should_inject_antag(dry_run = FALSE) - return (mode.current_players[CURRENT_LIVING_ANTAGS].len ? FALSE : ..()) - -/datum/dynamic_storyteller/conversion - name = "Conversion" - config_tag = "conversion" - desc = "Conversion antags. Cults, revs." - curve_centre = 3 - curve_width = 1 - weight = 0 - flags = WAROPS_ALWAYS_ALLOWED - property_weights = list("valid" = 1, "conversion" = 20) - -/datum/dynamic_storyteller/conversion/minor_start_chance() - return 0 - -/datum/dynamic_storyteller/random - name = "Random" - config_tag = "random" - weight = 1 - max_chaos = 60 - soft_antag_ratio_cap = 1 - desc = "No weighting at all; every ruleset has the same chance of happening. Cooldowns vary wildly. As random as it gets." - -/datum/dynamic_storyteller/random/on_start() - ..() - GLOB.dynamic_forced_threat_level = rand(0,100) - -/datum/dynamic_storyteller/random/get_midround_cooldown() - return rand(GLOB.dynamic_midround_delay_min/2, GLOB.dynamic_midround_delay_max*2) - -/datum/dynamic_storyteller/random/should_inject_antag() - return prob(50) - -/datum/dynamic_storyteller/random/minor_start_chance() - return 20 - -/datum/dynamic_storyteller/random/roundstart_draft() - var/list/drafted_rules = list() - for (var/datum/dynamic_ruleset/roundstart/rule in mode.roundstart_rules) - if (rule.acceptable(mode.roundstart_pop_ready, mode.threat_level)) // If we got the population and threat required - rule.candidates = mode.candidates.Copy() - rule.trim_candidates() - if (rule.ready() && rule.candidates.len > 0) - drafted_rules[rule] = 1 - return drafted_rules - -/datum/dynamic_storyteller/random/minor_rule_draft() - var/list/drafted_rules = list() - for (var/datum/dynamic_ruleset/minor/rule in mode.minor_rules) - if (rule.acceptable(mode.current_players[CURRENT_LIVING_PLAYERS].len, mode.threat_level)) - rule.candidates = mode.candidates.Copy() - rule.trim_candidates() - if (rule.ready() && rule.candidates.len > 0) - drafted_rules[rule] = 1 - return drafted_rules - -/datum/dynamic_storyteller/random/midround_draft() - var/list/drafted_rules = list() - for (var/datum/dynamic_ruleset/midround/rule in mode.midround_rules) - if (rule.acceptable(mode.current_players[CURRENT_LIVING_PLAYERS].len, mode.threat_level)) - // Classic secret : only autotraitor/minor roles - if (GLOB.dynamic_classic_secret && !((rule.flags & TRAITOR_RULESET))) - continue - rule.trim_candidates() - if (rule.ready()) - drafted_rules[rule] = 1 - return drafted_rules - -/datum/dynamic_storyteller/random/latejoin_draft(mob/living/carbon/human/newPlayer) - var/list/drafted_rules = list() - for (var/datum/dynamic_ruleset/latejoin/rule in mode.latejoin_rules) - if (rule.acceptable(mode.current_players[CURRENT_LIVING_PLAYERS].len, mode.threat_level)) - // Classic secret : only autotraitor/minor roles - if (GLOB.dynamic_classic_secret && !((rule.flags & TRAITOR_RULESET))) - continue - // No stacking : only one round-ender, unless threat level > stacking_limit. - if (mode.threat_level > GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking) - if(rule.flags & HIGHLANDER_RULESET && mode.highlander_executed) - continue - rule.candidates = list(newPlayer) - rule.trim_candidates() - if (rule.ready()) - drafted_rules[rule] = 1 - return drafted_rules - -/datum/dynamic_storyteller/story - name = "Story" - config_tag = "story" - desc = "Antags with options for loadouts and gimmicks. Traitor, wizard, nukies." - weight = 4 - curve_width = 2 - flags = USE_PREV_ROUND_WEIGHTS - property_weights = list("story_potential" = 2) - -/datum/dynamic_storyteller/classic - name = "Classic" - config_tag = "classic" - weight = 8 - desc = "No special antagonist weights. Good variety, but not like random. Uses your chaos preference to weight." - flags = USE_PREF_WEIGHTS | USE_PREV_ROUND_WEIGHTS - -/datum/dynamic_storyteller/suspicion - name = "Intrigue" - config_tag = "intrigue" - desc = "Antags that instill distrust in the crew. Traitors, bloodsuckers." - weight = 4 - curve_width = 2 - dead_player_weight = 2 - flags = USE_PREV_ROUND_WEIGHTS - property_weights = list("trust" = -2) - -/datum/dynamic_storyteller/intrigue/minor_start_chance() - return 100 - mode.threat_level - -/datum/dynamic_storyteller/grabbag - name = "Grab Bag" - config_tag = "grabbag" - desc = "Crew antags (e.g. traitor, changeling, bloodsucker, heretic) only at round start, all mixed together." - weight = 4 - flags = USE_PREF_WEIGHTS | USE_PREV_ROUND_WEIGHTS - -/datum/dynamic_storyteller/grabbag/minor_start_chance() - return 100 - -/datum/dynamic_storyteller/liteextended - name = "Calm" - config_tag = "calm" - desc = "Low-chaos round. Few antags. No conversion." - curve_centre = -3 - curve_width = 0.5 - flags = NO_ASSASSIN - min_chaos = 30 - weight = 3 - dead_player_weight = 5 - soft_antag_ratio_cap = 8 - property_weights = list("extended" = 2, "chaos" = -1, "valid" = -1, "conversion" = -10) - -/datum/dynamic_storyteller/liteextended/minor_start_chance() - return 90 - -/datum/dynamic_storyteller/no_antag - name = "Extended" - config_tag = "semiextended" - desc = "No standard antags." - curve_centre = -5 - curve_width = 0.5 - min_chaos = 40 - flags = NO_ASSASSIN | FORCE_IF_WON - weight = 1 - property_weights = list("extended" = 2) - -/datum/dynamic_storyteller/no_antag/roundstart_draft() - return list() - -/datum/dynamic_storyteller/no_antag/should_inject_antag(dry_run) - return FALSE diff --git a/code/game/gamemodes/dynamic/readme.md b/code/game/gamemodes/dynamic/readme.md index a98a02a8dd..3a8d41f230 100644 --- a/code/game/gamemodes/dynamic/readme.md +++ b/code/game/gamemodes/dynamic/readme.md @@ -1,56 +1,184 @@ -# DYNAMIC +# Dynamic Mode -Tries to keep the round at a certain level of action, based on the round's "threat level". - -## ROUNDSTART +## Roundstart Dynamic rolls threat based on a special sauce formula: -"dynamic_curve_width \* tan((rand() - 0.5) \* 180) + dynamic_curve_centre" -Midround injection cooldowns are set using exponential distribution between 15 minutes and 35 minutes. This value is then added to world.time and assigned to the injection cooldown variables. +> [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] -Latejoins are aggressively assigned whenever possible, to keep the round at a certain threat level. +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. -rigged_roundstart() is called instead if there are forced rules (an admin set the mode) +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. -can_start() -> pre_setup() -> roundstart() OR rigged_roundstart() -> picking_roundstart_rule(drafted_rules) -> post_setup() +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). -## PROCESS +## 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: -Calls rule_process on every rule which is in the current_rules list. +- 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 (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. + +[rigged_roundstart][/datum/game_mode/dynamic/proc/rigged_roundstart] is called instead if there are forced rules (an admin set the mode) + +1. [setup_parameters][/datum/game_mode/proc/setup_parameters]\() +2. [pre_setup][/datum/game_mode/proc/pre_setup]\() +3. [roundstart][/datum/game_mode/dynamic/proc/roundstart]\() OR [rigged_roundstart][/datum/game_mode/dynamic/proc/rigged_roundstart]\() +4. [picking_roundstart_rule][/datum/game_mode/dynamic/proc/picking_roundstart_rule]\(drafted_rules) +5. [post_setup][/datum/game_mode/proc/post_setup]\() + +## 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() Midround injection time is checked against world.time to see if an injection should happen. If midround injection time is lower than world.time, it updates playercounts again, then tries to inject and generates a new cooldown regardless of whether a rule is picked. -## LATEJOIN +## Latejoin -make_antag_chance(newPlayer) -> [For each latespawn rule...] +make_antag_chance(newPlayer) -> (For each latespawn rule...) -> acceptable(living players, threat_level) -> trim_candidates() -> ready(forced=FALSE) **If true, add to drafted rules +**NOTE that acceptable uses threat_level not threat! +**NOTE Latejoin timer is ONLY reset if at least one rule was drafted. **NOTE the new_player.dm AttemptLateSpawn() calls OnPostSetup for all roles (unless assigned role is MODE) -[After collecting all draftble rules...] + +(After collecting all draftble rules...) -> picking_latejoin_ruleset(drafted_rules) -> spend threat -> ruleset.execute() -## MIDROUND -process() -> [For each midround rule...] + +## Midround + +process() -> (For each midround rule... -> acceptable(living players, threat_level) -> trim_candidates() -> ready(forced=FALSE) -[After collecting all draftble rules...] +(After collecting all draftble rules...) -> picking_midround_ruleset(drafted_rules) -> spend threat -> ruleset.execute() -## FORCED + +## Forced + For latejoin, it simply sets forced_latejoin_rule make_antag_chance(newPlayer) -> trim_candidates() -> ready(forced=TRUE) **NOTE no acceptable() call + For midround, calls the below proc with forced = TRUE picking_specific_rule(ruletype,forced) -> forced OR acceptable(living_players, threat_level) -> trim_candidates() -> ready(forced) -> spend threat -> execute() **NOTE specific rule can be called by RS traitor->MR autotraitor w/ forced=FALSE **NOTE that due to short circuiting acceptable() need not be called if forced. -## RULESET + +## Ruleset + acceptable(population,threat) just checks if enough threat_level for population indice. **NOTE that we currently only send threat_level as the second arg, not threat. ready(forced) checks if enough candidates and calls the map's map_ruleset(dynamic_ruleset) at the parent level + trim_candidates() varies significantly according to the ruleset type Roundstart: All candidates are new_player mobs. Check them for standard stuff: connected, desire role, not banned, etc. **NOTE Roundstart deals with both candidates (trimmed list of valid players) and mode.candidates (everyone readied up). Don't confuse them! Latejoin: Only one candidate, the latejoiner. Standard checks. Midround: Instead of building a single list candidates, candidates contains four lists: living, dead, observing, and living antags. Standard checks in trim_list(list). + 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. +- `random_event_hijack_minimum` - The minimum amount of time for antag random events to be hijacked. (See [Random Event Hijacking](#random-event-hijacking)) +- `random_event_hijack_maximum` - The maximum amount of time for antag random events to be hijacked. (See [Random Event Hijacking](#random-event-hijacking)) +- `hijacked_random_event_injection_chance` - The amount of injection chance to give to Dynamic when a random event is hijacked. (See [Random Event Hijacking](#random-event-hijacking)) + +## Random Event "Hijacking" +Random events have the potential to be hijacked by Dynamic to keep the pace of midround injections, while also allowing greenshifts to contain some antagonists. + +`/datum/round_event_control/dynamic_should_hijack` is a variable to random events to allow Dynamic to hijack them, and defaults to FALSE. This is set to TRUE for random events that spawn antagonists. + +In `/datum/game_mode/dynamic/on_pre_random_event` (in `dynamic_hijacking.dm`), Dynamic hooks to random events. If the `dynamic_should_hijack` variable is TRUE, the following sequence of events occurs: + +![Flow chart to describe the chain of events for Dynamic 2021 to take](https://user-images.githubusercontent.com/35135081/109071468-9cab7e00-76a8-11eb-8f9f-2b920c602ef4.png) + +`n` is a random value between `random_event_hijack_minimum` and `random_event_hijack_maximum`. Injection chance, should it need to be raised, is increased by `hijacked_random_event_injection_chance`. diff --git a/code/game/gamemodes/dynamic/ruleset_picking.dm b/code/game/gamemodes/dynamic/ruleset_picking.dm new file mode 100644 index 0000000000..b92bce5522 --- /dev/null +++ b/code/game/gamemodes/dynamic/ruleset_picking.dm @@ -0,0 +1,121 @@ +#define ADMIN_CANCEL_MIDROUND_TIME (10 SECONDS) + +/// From a list of rulesets, returns one based on weight and availability. +/// Mutates the list that is passed into it to remove invalid rules. +/datum/game_mode/dynamic/proc/pick_ruleset(list/drafted_rules) + if (only_ruleset_executed) + return null + + while (TRUE) + var/datum/dynamic_ruleset/rule = pickweight(drafted_rules) + if (!rule) + return null + + if (check_blocking(rule.blocking_rules, executed_rules)) + drafted_rules -= rule + if(drafted_rules.len <= 0) + return null + continue + else if ( + rule.flags & HIGH_IMPACT_RULESET \ + && threat_level < GLOB.dynamic_stacking_limit \ + && GLOB.dynamic_no_stacking \ + && high_impact_ruleset_executed \ + ) + drafted_rules -= rule + if(drafted_rules.len <= 0) + return null + continue + + return rule + +/// Executes a random midround ruleset from the list of drafted rules. +/datum/game_mode/dynamic/proc/pick_midround_rule(list/drafted_rules) + var/datum/dynamic_ruleset/rule = pick_ruleset(drafted_rules) + if (isnull(rule)) + return + current_midround_rulesets = drafted_rules - rule + + midround_injection_timer_id = addtimer( + CALLBACK(src, .proc/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") + +/// Fired after admins do not cancel a midround injection. +/datum/game_mode/dynamic/proc/execute_midround_rule(datum/dynamic_ruleset/rule) + current_midround_rulesets = null + midround_injection_timer_id = null + if (!rule.repeatable) + midround_rules = remove_from_list(midround_rules, rule.type) + addtimer(CALLBACK(src, .proc/execute_midround_latejoin_rule, rule), rule.delay) + +/// Executes a random latejoin ruleset from the list of drafted rules. +/datum/game_mode/dynamic/proc/pick_latejoin_rule(list/drafted_rules) + var/datum/dynamic_ruleset/rule = pick_ruleset(drafted_rules) + if (isnull(rule)) + return + if (!rule.repeatable) + latejoin_rules = remove_from_list(latejoin_rules, rule.type) + addtimer(CALLBACK(src, .proc/execute_midround_latejoin_rule, rule), rule.delay) + +/// 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_midround_budget(rule.cost) + threat_log += "[worldtime2text()]: [rule.ruletype] [rule.name] spent [rule.cost]" + 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 & HIGH_IMPACT_RULESET) + high_impact_ruleset_executed = TRUE + else if(rule.flags & ONLY_RULESET) + only_ruleset_executed = TRUE + if(rule.ruletype == "Latejoin") + var/mob/M = pick(rule.candidates) + message_admins("[key_name(M)] joined the station, and was selected by the [rule.name] ruleset.") + log_game("DYNAMIC: [key_name(M)] joined the station, and was selected by the [rule.name] ruleset.") + executed_rules += rule + 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.") + return FALSE + +/// Fired when an admin cancels the current midround injection. +/datum/game_mode/dynamic/proc/admin_cancel_midround(mob/user, timer_id) + if (midround_injection_timer_id != timer_id || !deltimer(midround_injection_timer_id)) + to_chat(user, "Too late!") + return + + log_admin("[key_name(user)] cancelled the next midround injection.") + message_admins("[key_name(user)] cancelled the next midround injection.") + midround_injection_timer_id = null + current_midround_rulesets = null + +/// Fired when an admin requests a different midround injection. +/datum/game_mode/dynamic/proc/admin_different_midround(mob/user, timer_id) + if (midround_injection_timer_id != timer_id || !deltimer(midround_injection_timer_id)) + to_chat(user, "Too late!") + return + + midround_injection_timer_id = null + + if (isnull(current_midround_rulesets) || current_midround_rulesets.len == 0) + log_admin("[key_name(user)] asked for a different midround injection, but there were none left.") + message_admins("[key_name(user)] asked for a different midround injection, but there were none left.") + return + + log_admin("[key_name(user)] asked for a different midround injection.") + message_admins("[key_name(user)] asked for a different midround injection.") + pick_midround_rule(current_midround_rulesets) + +#undef ADMIN_CANCEL_MIDROUND_TIME diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index d4afc3dbb4..9c94ab5b12 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -435,18 +435,7 @@ for(var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset) dat += {"-> [rule.name] <-
"} dat += "(Clear Rulesets)
" - dat += "(Force Storyteller)
" - if (GLOB.dynamic_forced_storyteller) - var/datum/dynamic_storyteller/S = GLOB.dynamic_forced_storyteller - dat += "-> [initial(S.name)] <-
" dat += "(Dynamic mode options)
" - else if (SSticker.IsRoundInProgress()) - dat += "(Force Next Latejoin Ruleset)
" - if (SSticker && SSticker.mode && istype(SSticker.mode,/datum/game_mode/dynamic)) - var/datum/game_mode/dynamic/mode = SSticker.mode - if (mode.forced_latejoin_rule) - dat += {"-> [mode.forced_latejoin_rule.name] <-
"} - dat += "(Execute Midround Ruleset!)
" dat += "
" if(SSticker.IsRoundInProgress()) dat += "(Game Mode Panel)
" @@ -941,27 +930,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.

- High population limit: Current value : [GLOB.dynamic_high_pop_limit]. -
The threshold at which "high population override" will be in effect.

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 437c74991c..aa3e3f78d2 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -192,6 +192,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, /datum/admins/proc/fixcorruption, diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 4224b406b3..a8a7e51611 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1375,15 +1375,13 @@ else if(href_list["f_dynamic_roundstart"]) if(!check_rights(R_ADMIN)) return - if(SSticker && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - if(GLOB.master_mode != "dynamic") - return alert(usr, "The game mode has to be dynamic mode.", null, null, null, null) + if(SSticker?.mode) + return tgui_alert(usr, "The game has already started.") var/roundstart_rules = list() for (var/rule in subtypesof(/datum/dynamic_ruleset/roundstart)) var/datum/dynamic_ruleset/roundstart/newrule = new rule() roundstart_rules[newrule.name] = newrule - var/added_rule = input(usr,"What ruleset do you want to force? This will bypass threat level and population restrictions.", "Rigging Roundstart", null) as null|anything in roundstart_rules + var/added_rule = input(usr,"What ruleset do you want to force? This will bypass threat level and population restrictions.", "Rigging Roundstart", null) as null|anything in sortList(roundstart_rules) if (added_rule) GLOB.dynamic_forced_roundstart_ruleset += roundstart_rules[added_rule] log_admin("[key_name(usr)] set [added_rule] to be a forced roundstart ruleset.") @@ -1407,201 +1405,18 @@ log_admin("[key_name(usr)] removed [rule] from the forced roundstart rulesets.") message_admins("[key_name(usr)] removed [rule] from the forced roundstart rulesets.", 1) - else if(href_list["f_dynamic_storyteller"]) - if(!check_rights(R_ADMIN)) - return - if(SSticker && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - if(GLOB.master_mode != "dynamic") - return alert(usr, "The game mode has to be dynamic mode.", null, null, null, null) - var/list/choices = list() - for(var/T in config.storyteller_cache) - var/datum/dynamic_storyteller/S = T - choices[initial(S.name)] = T - var/choice = choices[input("Select storyteller:", "Storyteller", "Classic") as null|anything in choices] - if(choice) - GLOB.dynamic_forced_storyteller = choice - log_admin("[key_name(usr)] forced the storyteller to [GLOB.dynamic_forced_storyteller].") - message_admins("[key_name(usr)] forced the storyteller to [GLOB.dynamic_forced_storyteller].") - Game() - - else if(href_list["f_dynamic_storyteller_clear"]) - if(!check_rights(R_ADMIN)) - return - GLOB.dynamic_forced_storyteller = null - Game() - log_admin("[key_name(usr)] cleared the forced storyteller. The mode will pick one as normal.") - message_admins("[key_name(usr)] cleared the forced storyteller. The mode will pick one as normal.", 1) - - else if(href_list["f_dynamic_latejoin"]) - if(!check_rights(R_ADMIN)) - return - if(!SSticker || !SSticker.mode) - return alert(usr, "The game must start first.", null, null, null, null) - if(GLOB.master_mode != "dynamic") - return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) - var/latejoin_rules = list() - for (var/rule in subtypesof(/datum/dynamic_ruleset/latejoin)) - var/datum/dynamic_ruleset/latejoin/newrule = new rule() - latejoin_rules[newrule.name] = newrule - var/added_rule = input(usr,"What ruleset do you want to force upon the next latejoiner? This will bypass threat level and population restrictions.", "Rigging Latejoin", null) as null|anything in latejoin_rules - if (added_rule) - var/datum/game_mode/dynamic/mode = SSticker.mode - mode.forced_latejoin_rule = latejoin_rules[added_rule] - log_admin("[key_name(usr)] set [added_rule] to proc on the next latejoin.") - message_admins("[key_name(usr)] set [added_rule] to proc on the next latejoin.", 1) - Game() - - else if(href_list["f_dynamic_latejoin_clear"]) - if(!check_rights(R_ADMIN)) - return - if (SSticker && SSticker.mode && istype(SSticker.mode,/datum/game_mode/dynamic)) - var/datum/game_mode/dynamic/mode = SSticker.mode - mode.forced_latejoin_rule = null - Game() - log_admin("[key_name(usr)] cleared the forced latejoin ruleset.") - message_admins("[key_name(usr)] cleared the forced latejoin ruleset.", 1) - - else if(href_list["f_dynamic_midround"]) - if(!check_rights(R_ADMIN)) - return - if(!SSticker || !SSticker.mode) - return alert(usr, "The game must start first.", null, null, null, null) - if(GLOB.master_mode != "dynamic") - return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) - var/midround_rules = list() - for (var/rule in subtypesof(/datum/dynamic_ruleset/midround)) - var/datum/dynamic_ruleset/midround/newrule = new rule() - midround_rules[newrule.name] = rule - var/added_rule = input(usr,"What ruleset do you want to force right now? This will bypass threat level and population restrictions.", "Execute Ruleset", null) as null|anything in midround_rules - if (added_rule) - var/datum/game_mode/dynamic/mode = SSticker.mode - log_admin("[key_name(usr)] executed the [added_rule] ruleset.") - message_admins("[key_name(usr)] executed the [added_rule] ruleset.", 1) - mode.picking_specific_rule(midround_rules[added_rule],1) - else if (href_list["f_dynamic_options"]) if(!check_rights(R_ADMIN)) return - if(SSticker && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - if(GLOB.master_mode != "dynamic") - return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + if(SSticker?.mode) + return tgui_alert(usr, "The game has already started.") dynamic_mode_options(usr) - - else if(href_list["f_dynamic_roundstart_centre"]) - if(!check_rights(R_ADMIN)) - return - if(SSticker && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - if(GLOB.master_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 is allowed. This is adjusted by dynamic voting.", "Change curve centre", null) as num - - 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 && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - if(GLOB.master_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 && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - if(GLOB.master_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 && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - if(GLOB.master_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 && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - if(GLOB.master_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 && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - if(GLOB.master_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 - if(GLOB.master_mode != "dynamic") - return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) - GLOB.dynamic_forced_extended = !GLOB.dynamic_forced_extended log_admin("[key_name(usr)] set 'forced_extended' to [GLOB.dynamic_forced_extended].") message_admins("[key_name(usr)] set 'forced_extended' to [GLOB.dynamic_forced_extended].") @@ -1611,70 +1426,29 @@ if(!check_rights(R_ADMIN)) return - if(GLOB.master_mode != "dynamic") - return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) - GLOB.dynamic_no_stacking = !GLOB.dynamic_no_stacking 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(GLOB.master_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 - if(GLOB.master_mode != "dynamic") - return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) - GLOB.dynamic_stacking_limit = input(usr,"Change the threat limit at which round-endings rulesets will start to stack.", "Change stacking limit", null) as num log_admin("[key_name(usr)] set 'stacking_limit' to [GLOB.dynamic_stacking_limit].") message_admins("[key_name(usr)] set 'stacking_limit' to [GLOB.dynamic_stacking_limit].") dynamic_mode_options(usr) - else if(href_list["f_dynamic_high_pop_limit"]) - if(!check_rights(R_ADMIN)) - return - - if(SSticker && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - - if(GLOB.master_mode != "dynamic") - return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) - - var/new_value = input(usr, "Enter the high-pop override threshold for dynamic mode.", "High pop override") as num - if (new_value < 0) - return alert(usr, "Only positive values allowed!", null, null, null, null) - GLOB.dynamic_high_pop_limit = new_value - - log_admin("[key_name(usr)] set 'high_pop_limit' to [GLOB.dynamic_high_pop_limit].") - message_admins("[key_name(usr)] set 'high_pop_limit' to [GLOB.dynamic_high_pop_limit].") - dynamic_mode_options(usr) - else if(href_list["f_dynamic_forced_threat"]) if(!check_rights(R_ADMIN)) return - if(SSticker && SSticker.mode) - return alert(usr, "The game has already started.", null, null, null, null) - - if(GLOB.master_mode != "dynamic") - return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + if(SSticker?.mode) + return tgui_alert(usr, "The game has already started.") var/new_value = input(usr, "Enter the forced threat level for dynamic mode.", "Forced threat level") as num if (new_value > 100) - return alert(usr, "The value must be be under 100.", null, null, null, null) + return tgui_alert(usr, "The value must be be under 100.") GLOB.dynamic_forced_threat_level = new_value log_admin("[key_name(usr)] set 'forced_threat_level' to [GLOB.dynamic_forced_threat_level].") diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm b/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm index 39dc4a1ed2..78e4d38b3c 100644 --- a/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm +++ b/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm @@ -94,15 +94,6 @@ GLOBAL_VAR_INIT(war_declared, FALSE) if(board.moved) to_chat(user, "The shuttle has already been moved! You have forfeit the right to declare war.") return FALSE - if(istype(SSticker.mode, /datum/game_mode/dynamic)) - var/datum/game_mode/dynamic/mode = SSticker.mode - if(!(mode.storyteller.flags & WAROPS_ALWAYS_ALLOWED)) - if(mode.threat_level < CONFIG_GET(number/dynamic_warops_requirement)) - to_chat(user, "Due to the dynamic space in which the station resides, you are too deep into Nanotrasen territory to reasonably go loud.") - return FALSE - else if(mode.threat < CONFIG_GET(number/dynamic_warops_cost)) - to_chat(user, "Due to recent threats on the station, Nanotrasen is looking too closely for a war declaration to be wise.") - return FALSE return TRUE /obj/item/nuclear_challenge/clownops diff --git a/code/modules/antagonists/traitor/classes/human.dm b/code/modules/antagonists/traitor/classes/human.dm index f868d0e7e1..203ed37336 100644 --- a/code/modules/antagonists/traitor/classes/human.dm +++ b/code/modules/antagonists/traitor/classes/human.dm @@ -35,10 +35,6 @@ is_dynamic = TRUE assassin_prob = max(0,mode.threat_level-20) if(prob(assassin_prob)) - if(is_dynamic) - var/threat_spent = CONFIG_GET(number/dynamic_assassinate_cost) - mode.spend_threat(threat_spent) - mode.log_threat("[T.owner.name] added [threat_spent] on an assassination target.") var/list/active_ais = active_ais() if(active_ais.len && prob(100/GLOB.joined_player_list.len)) var/datum/objective/destroy/destroy_objective = new diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index f0bc6e5bfc..b6fc397233 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -46,17 +46,13 @@ traitor_kind.on_process(src) /proc/get_random_traitor_kind(var/list/blacklist = list()) - var/chaos_weight = 0 - if(istype(SSticker.mode,/datum/game_mode/dynamic)) - var/datum/game_mode/dynamic/mode = SSticker.mode - chaos_weight = (mode.threat - 50)/50 var/list/weights = list() for(var/C in GLOB.traitor_classes) if(!(C in blacklist)) var/datum/traitor_class/class = GLOB.traitor_classes[C] if(class.min_players > length(GLOB.joined_player_list)) continue - var/weight = LOGISTIC_FUNCTION(1.5*class.weight,chaos_weight,class.chaos,0) + var/weight = LOGISTIC_FUNCTION(1.5*class.weight,0,class.chaos,0) weights[C] = weight * 1000 var/choice = pickweight(weights, 0) if(!choice) diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index b77b9913ba..8e6226069c 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -180,3 +180,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 706a7a3910..705b0aea4f 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -59,6 +59,7 @@ // #include "designs.dm" // #include "dynamic_ruleset_sanity.dm" // #include "egg_glands.dm" +// #include "dynamic_ruleset_sanity.dm" // #include "emoting.dm" // #include "food_edibility_check.dm" // #include "greyscale_config.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 0000000000..9fff5daf9b --- /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 new file mode 100644 index 0000000000..24cb3313db --- /dev/null +++ b/config/dynamic.json @@ -0,0 +1,138 @@ +{ + "Dynamic": {}, + "Roundstart": { + "Traitors": { + "cost": 8, + "scaling_cost": 9, + "weight": 0, + "required_candidates": 1, + "minimum_required_age": 0, + "requirements": [ + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10 + ], + "antag_cap": { + "denominator": 24 + }, + "protected_roles": [ + "Prisoner", + "Security Officer", + "Warden", + "Detective", + "Head of Security", + "Captain" + ], + "restricted_roles": [ + "Cyborg" + ] + }, + + "Blood Brothers": { + "weight": 0 + }, + + "Changelings": { + "weight": 0 + }, + + "Heretics": { + "weight": 0 + }, + + "Wizard": { + "weight": 0 + }, + + "Blood Cult": { + "weight": 0 + }, + + "Nuclear Emergency": { + "weight": 0 + }, + + "Revolution": { + "weight": 0 + }, + + "Families": { + "weight": 0 + } + }, + + "Midround": { + "Syndicate Sleeper Agent": { + "weight": 0 + }, + + "Malfunctioning AI": { + "weight": 0 + }, + + "Wizard": { + "weight": 0 + }, + + "Nuclear Assault": { + "weight": 0 + }, + + "Blob": { + "weight": 0 + }, + + "Blob Infection": { + "weight": 0 + }, + + "Alien Infestation": { + "weight": 0 + }, + + "Nightmare": { + "weight": 0 + }, + + "Space Dragon": { + "weight": 0 + }, + + "Abductors": { + "weight": 0 + }, + + "Swarmers": { + "weight": 0 + }, + + "Space Ninja": { + "weight": 0 + }, + + "Spiders": { + "weight": 0 + } + }, + + "Latejoin": { + "Syndicate Infiltrator": { + "weight": 0 + }, + + "Provocateur": { + "weight": 0 + }, + + "Heretic Smuggler": { + "weight": 0 + } + } +} diff --git a/tgstation.dme b/tgstation.dme index 2178bc0f4d..80cfab4735 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -692,6 +692,7 @@ #include "code\datums\materials\basemats.dm" #include "code\datums\materials\meat.dm" #include "code\datums\materials\pizza.dm" +#include "code\datums\mocking\client.dm" #include "code\datums\mood_events\beauty_events.dm" #include "code\datums\mood_events\drink_events.dm" #include "code\datums\mood_events\drug_events.dm" @@ -811,12 +812,13 @@ #include "code\game\gamemodes\devil\objectives.dm" #include "code\game\gamemodes\devil\devil agent\devil_agent.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_minor.dm" #include "code\game\gamemodes\dynamic\dynamic_rulesets_roundstart.dm" -#include "code\game\gamemodes\dynamic\dynamic_storytellers.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\gangs\dominator.dm" @@ -3627,6 +3629,7 @@ #include "code\modules\tgui_panel\tgui_panel.dm" #include "code\modules\tooltip\tooltip.dm" #include "code\modules\unit_tests\_unit_tests.dm" +#include "code\modules\unit_tests\dynamic_ruleset_sanity.dm" #include "code\modules\uplink\uplink_devices.dm" #include "code\modules\uplink\uplink_items.dm" #include "code\modules\uplink\uplink_purchase_log.dm"