diff --git a/code/__DEFINES/dynamic.dm b/code/__DEFINES/dynamic.dm index da8a76e71e..6a15834ed8 100644 --- a/code/__DEFINES/dynamic.dm +++ b/code/__DEFINES/dynamic.dm @@ -1,7 +1,7 @@ #define CURRENT_LIVING_PLAYERS 1 #define CURRENT_LIVING_ANTAGS 2 #define CURRENT_DEAD_PLAYERS 3 -#define CURRENT_OBSERVERS 4 +#define CURRENT_OBSERVERS 4 #define NO_ASSASSIN (1<<0) #define WAROPS_ALWAYS_ALLOWED (1<<1) @@ -9,9 +9,11 @@ #define FORCE_IF_WON (1<<3) #define USE_PREV_ROUND_WEIGHTS (1<<4) -#define ONLY_RULESET (1<<0) -#define HIGHLANDER_RULESET (1<<1) -#define TRAITOR_RULESET (1<<2) -#define MINOR_RULESET (1<<3) +#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) #define RULESET_STOP_PROCESSING 1 diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm index fbc433a8a0..836b00250f 100644 --- a/code/game/gamemodes/dynamic/dynamic.dm +++ b/code/game/gamemodes/dynamic/dynamic.dm @@ -72,6 +72,8 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) 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() /// List of midround rules used for selecting the rules. @@ -121,10 +123,12 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) var/pop_last_updated = 0 /// How many percent of the rounds are more peaceful. var/peaceful_percentage = 50 - /// If a highlander executed. + /// If a highlander executed. No other highlander rulesets will be run. var/highlander_executed = FALSE - /// If a only ruleset has been executed. + /// If a only ruleset has been executed. No other rulesets will be run. 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. @@ -372,6 +376,8 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) if(ruleset.name == "") continue switch(ruleset.ruletype) + if("Minor") + minor_rules += ruleset if("Roundstart") roundstart_rules += ruleset if ("Latejoin") @@ -396,21 +402,47 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) rigged_roundstart() else roundstart() - - 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]") + 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]") candidates.Cut() return TRUE /datum/game_mode/dynamic/post_setup(report) update_playercounts() - - for(var/datum/dynamic_ruleset/roundstart/rule in executed_rules) - addtimer(CALLBACK(src, /datum/game_mode/dynamic/.proc/execute_roundstart_rule, rule), rule.delay) + if(minor_ruleset_start) + addtimer(CALLBACK(src, /datum/game_mode/dynamic/.proc/minor_roundstart),rand(1 MINUTES,5 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) ..() +/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 [num_rulesets_executed] antags or antag teams made.") + log_game("DYNAMIC: Minor antag roundstart made [num_rulesets_executed] antags or antag teams.") + + /// 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.") @@ -429,12 +461,17 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) 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("Not enough threat level for roundstart antags!") - log_game("DYNAMIC: Not enough threat level for roundstart antags!") - midround_injection_cooldown = round((midround_injection_cooldown + world.time) / 2, 1) - latejoin_injection_cooldown = round((latejoin_injection_cooldown + world.time) / 2, 1) + 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) @@ -468,8 +505,9 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) else if(threat_level >= 50) - message_admins("DYNAMIC: Picking first roundstart ruleset failed. You should report this.") + 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 @@ -513,7 +551,6 @@ GLOBAL_VAR_INIT(dynamic_forced_storyteller, null) drafted_rules -= starting_rule starting_rule.trim_candidates() - starting_rule.scale_up(extra_rulesets_amount, threat_level-added_threat) 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) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm index eb7da2144f..3e7e504130 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm @@ -50,8 +50,12 @@ /// 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 and MINOR_RULESET can't end the round and have no difference right now. + /// 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 /// Pop range per requirement. If zero defaults to mode's pop_per_requirement. var/pop_per_requirement = 0 @@ -82,9 +86,6 @@ var/delay = 0 /// List of tags for use in storytellers. var/list/property_weights = list() - /// Whether or not recent-round weight values are taken into account for this ruleset. - /// Weight reduction uses the same values as secret's recent-round mode weight reduction. - var/always_max_weight = FALSE /// Weight reduction by recent-rounds. Saved on new. var/weight_mult = 1 @@ -100,7 +101,7 @@ 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(!always_max_weight && SSpersistence.saved_dynamic_rules.len == 3 && repeated_mode_adjust.len == 3) + 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]) @@ -119,6 +120,9 @@ /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" @@ -175,7 +179,7 @@ /// This is called if persistent variable is true everytime SSTicker ticks. /datum/dynamic_ruleset/proc/rule_process() return TRUE - + /// Called on game mode pre_setup for roundstart rulesets. /// Do everything you need to do before job is assigned here. /// IMPORTANT: ASSIGN special_role HERE diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm index d2fa8bcc70..7842c6e0f7 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm @@ -70,9 +70,8 @@ requirements = list(40,30,20,15,15,15,15,15,15,15) high_population_requirement = 15 repeatable = TRUE - flags = TRAITOR_RULESET + flags = TRAITOR_RULESET | MINOR_RULESET | ALWAYS_MAX_WEIGHT_RULESET property_weights = list("story_potential" = 2, "trust" = -1, "extended" = 1) - always_max_weight = TRUE ////////////////////////////////////////////// // // @@ -207,6 +206,7 @@ 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 @@ -229,6 +229,7 @@ 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 repeatable = TRUE @@ -258,6 +259,7 @@ 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 @@ -281,5 +283,5 @@ requirements = list(10,10,10,10,10,10,10,10,10,10) high_population_requirement = 10 repeatable = TRUE - flags = TRAITOR_RULESET | MINOR_RULESET - property_weights = list("story_potential" = 2, "trust" = -1, "extended" = 2) + 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 b29584aa58..416721702b 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm @@ -207,9 +207,8 @@ requirements = list(30,25,20,15,15,15,15,15,15,15) repeatable = TRUE high_population_requirement = 15 - flags = TRAITOR_RULESET + flags = TRAITOR_RULESET | MINOR_RULESET | ALWAYS_MAX_WEIGHT_RULESET property_weights = list("story_potential" = 2, "trust" = -1, "extended" = 1) - always_max_weight = TRUE /datum/dynamic_ruleset/midround/autotraitor/acceptable(population = 0, threat = 0) var/player_count = mode.current_players[CURRENT_LIVING_PLAYERS].len @@ -494,6 +493,7 @@ 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 @@ -630,6 +630,7 @@ 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) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_minor.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_minor.dm new file mode 100644 index 0000000000..618befbaa7 --- /dev/null +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_minor.dm @@ -0,0 +1,212 @@ + +////////////////////////////////////////////// +// // +// 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 db4ec99558..c5e0857461 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm @@ -15,6 +15,7 @@ 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 + 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 @@ -22,7 +23,6 @@ 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) - always_max_weight = TRUE var/autotraitor_cooldown = 450 // 15 minutes (ticks once per 2 sec) /datum/dynamic_ruleset/roundstart/traitor/pre_execute() @@ -57,6 +57,7 @@ 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 + flags = MINOR_RULESET weight = 4 cost = 10 requirements = list(101,101,101,101,101,101,101,101,101,101) @@ -106,6 +107,7 @@ 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 @@ -156,6 +158,7 @@ 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 @@ -694,6 +697,7 @@ 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) @@ -850,6 +854,7 @@ 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 diff --git a/code/game/gamemodes/dynamic/dynamic_storytellers.dm b/code/game/gamemodes/dynamic/dynamic_storytellers.dm index 04e295356a..d5766b287e 100644 --- a/code/game/gamemodes/dynamic/dynamic_storytellers.dm +++ b/code/game/gamemodes/dynamic/dynamic_storytellers.dm @@ -32,6 +32,9 @@ Property weights are added to the config weight of the ruleset. They are: "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 @@ -104,8 +107,27 @@ Property weights are added to the config weight of the ruleset. They are: /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/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) @@ -124,7 +146,7 @@ Property weights are added to the config weight of the ruleset. They are: // 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) || (rule.flags & MINOR_RULESET))) + if (GLOB.dynamic_classic_secret && !((rule.flags & TRAITOR_RULESET))) continue rule.trim_candidates() if (rule.ready()) @@ -133,7 +155,7 @@ Property weights are added to the config weight of the ruleset. They are: 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) || (rule.flags & MINOR_RULESET)) // makes the traitor rulesets always possible anyway + 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. @@ -157,7 +179,7 @@ Property weights are added to the config weight of the ruleset. They are: 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) || (rule.flags & MINOR_RULESET))) + 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) @@ -172,7 +194,7 @@ Property weights are added to the config weight of the ruleset. They are: if(property in rule.property_weights) property_weight += rule.property_weights[property] * property_weights[property] var/threat_weight = 1 - if(!(rule.flags & TRAITOR_RULESET) || (rule.flags & MINOR_RULESET)) + 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) @@ -195,6 +217,9 @@ Property weights are added to the config weight of the ruleset. They are: 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) @@ -215,6 +240,9 @@ Property weights are added to the config weight of the ruleset. They are: flags = WAROPS_ALWAYS_ALLOWED | USE_PREV_ROUND_WEIGHTS property_weights = list("valid" = 3, "trust" = 5) +/datum/dynamic_storyteller/chaotic/minor_start_chance() + return 0 + /datum/dynamic_storyteller/team/should_inject_antag(dry_run = FALSE) return (mode.current_players[CURRENT_LIVING_ANTAGS].len ? FALSE : ..()) @@ -228,6 +256,9 @@ Property weights are added to the config weight of the ruleset. They are: flags = WAROPS_ALWAYS_ALLOWED property_weights = list("valid" = 1, "conversion" = 20) +/datum/dynamic_storyteller/chaotic/minor_start_chance() + return 0 + /datum/dynamic_storyteller/random name = "Random" config_tag = "random" @@ -244,6 +275,9 @@ Property weights are added to the config weight of the ruleset. They are: /datum/dynamic_storyteller/random/should_inject_antag() return prob(50) +/datum/dynamic_storyteller/chaotic/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) @@ -254,12 +288,22 @@ Property weights are added to the config weight of the ruleset. They are: 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) || (rule.flags & MINOR_RULESET))) + if (GLOB.dynamic_classic_secret && !((rule.flags & TRAITOR_RULESET))) continue rule.trim_candidates() if (rule.ready()) @@ -271,7 +315,7 @@ Property weights are added to the config weight of the ruleset. They are: 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) || (rule.flags & MINOR_RULESET))) + 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) @@ -286,7 +330,7 @@ Property weights are added to the config weight of the ruleset. They are: /datum/dynamic_storyteller/story name = "Story" config_tag = "story" - desc = "Antags with options for loadouts and gimmicks. Traitor, wizard, nukies. Has a buildup-climax-falling action threat curve." + desc = "Antags with options for loadouts and gimmicks. Traitor, wizard, nukies." weight = 2 curve_width = 2 flags = USE_PREV_ROUND_WEIGHTS @@ -308,6 +352,19 @@ Property weights are added to the config weight of the ruleset. They are: 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, all mixed together." + weight = 2 + 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" @@ -319,6 +376,9 @@ Property weights are added to the config weight of the ruleset. They are: dead_player_weight = 5 property_weights = list("extended" = 2, "chaos" = -1, "valid" = -1, "conversion" = -10) +/datum/dynamic_storyteller/liteextended/minor_start_chance() + return 100 + /datum/dynamic_storyteller/no_antag name = "Extended" config_tag = "semiextended" diff --git a/tgstation.dme b/tgstation.dme index 14a6a7cd10..0c3f2bbee7 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -751,6 +751,7 @@ #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\eldritch_cult\eldritch_cult.dm"