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"