mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-21 20:20:33 +01:00
Remove /datum/game_mode, we SSdynamic now [again] (#79965)
I don't remember what was hard about this last time it took me like 20 minutes this time so I'm scared. Removes dynamic simulations, only I have used them and it's a lot more complicated now with this. I plan on making Dynamic simulations a part of moth.fans anyway
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
/datum/controller/subsystem/dynamic/proc/setup_hijacking()
|
||||
RegisterSignal(SSdcs, COMSIG_GLOB_PRE_RANDOM_EVENT, PROC_REF(on_pre_random_event))
|
||||
|
||||
/datum/controller/subsystem/dynamic/proc/on_pre_random_event(datum/source, datum/round_event_control/round_event_control)
|
||||
SIGNAL_HANDLER
|
||||
if (!round_event_control.dynamic_should_hijack)
|
||||
return
|
||||
|
||||
if (random_event_hijacked != HIJACKED_NOTHING)
|
||||
log_dynamic_and_announce("Random event [round_event_control.name] tried to roll, but Dynamic vetoed it (random event has already ran).")
|
||||
SSevents.spawnEvent()
|
||||
SSevents.reschedule()
|
||||
return CANCEL_PRE_RANDOM_EVENT
|
||||
|
||||
var/time_range = rand(random_event_hijack_minimum, random_event_hijack_maximum)
|
||||
|
||||
if (world.time - last_midround_injection_attempt < time_range)
|
||||
random_event_hijacked = HIJACKED_TOO_RECENT
|
||||
log_dynamic_and_announce("Random event [round_event_control.name] tried to roll, but the last midround injection \
|
||||
was too recent. Heavy injection chance has been raised to [get_heavy_midround_injection_chance(dry_run = TRUE)]%.")
|
||||
return CANCEL_PRE_RANDOM_EVENT
|
||||
|
||||
if (next_midround_injection() - world.time < time_range)
|
||||
log_dynamic_and_announce("Random event [round_event_control.name] tried to roll, but the next midround injection is too soon.")
|
||||
return CANCEL_PRE_RANDOM_EVENT
|
||||
@@ -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/controller/subsystem/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/controller/subsystem/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()
|
||||
@@ -0,0 +1,108 @@
|
||||
/// Returns the world.time of the next midround injection.
|
||||
/// Will return a cached result from `next_midround_injection`, the variable.
|
||||
/// If that variable is null, will generate a new one.
|
||||
/datum/controller/subsystem/dynamic/proc/next_midround_injection()
|
||||
if (!isnull(next_midround_injection))
|
||||
return next_midround_injection
|
||||
|
||||
// Admins can futz around with the midround threat, and we want to be able to react to that
|
||||
var/midround_threat = threat_level - round_start_budget
|
||||
|
||||
var/rolls = CEILING(midround_threat / threat_per_midround_roll, 1)
|
||||
var/distance = ((1 / (rolls + 1)) * midround_upper_bound) + midround_lower_bound
|
||||
|
||||
if (last_midround_injection_attempt == 0)
|
||||
last_midround_injection_attempt = SSticker.round_start_time
|
||||
|
||||
return last_midround_injection_attempt + distance
|
||||
|
||||
/datum/controller/subsystem/dynamic/proc/try_midround_roll()
|
||||
if (!forced_injection && next_midround_injection() > world.time)
|
||||
return
|
||||
|
||||
if (GLOB.dynamic_forced_extended)
|
||||
return
|
||||
|
||||
if (EMERGENCY_PAST_POINT_OF_NO_RETURN)
|
||||
return
|
||||
|
||||
var/spawn_heavy = prob(get_heavy_midround_injection_chance())
|
||||
|
||||
last_midround_injection_attempt = world.time
|
||||
next_midround_injection = null
|
||||
forced_injection = FALSE
|
||||
|
||||
log_dynamic_and_announce("A midround ruleset is rolling, and will be [spawn_heavy ? "HEAVY" : "LIGHT"].")
|
||||
|
||||
random_event_hijacked = HIJACKED_NOTHING
|
||||
|
||||
var/list/drafted_heavies = list()
|
||||
var/list/drafted_lights = list()
|
||||
|
||||
for (var/datum/dynamic_ruleset/midround/ruleset in midround_rules)
|
||||
if (ruleset.weight == 0)
|
||||
log_dynamic("FAIL: [ruleset] has a weight of 0")
|
||||
continue
|
||||
|
||||
if (!ruleset.acceptable(GLOB.alive_player_list.len, threat_level))
|
||||
var/ruleset_forced = GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED
|
||||
if (ruleset_forced == RULESET_NOT_FORCED)
|
||||
log_dynamic("FAIL: [ruleset] is not acceptable with the current parameters. Alive players: [GLOB.alive_player_list.len], threat level: [threat_level]")
|
||||
else
|
||||
log_dynamic("FAIL: [ruleset] was disabled.")
|
||||
continue
|
||||
|
||||
if (mid_round_budget < ruleset.cost)
|
||||
log_dynamic("FAIL: [ruleset] is too expensive, and cannot be bought. Midround budget: [mid_round_budget], ruleset cost: [ruleset.cost]")
|
||||
continue
|
||||
|
||||
if (ruleset.minimum_round_time > world.time - SSticker.round_start_time)
|
||||
log_dynamic("FAIL: [ruleset] is trying to run too early. Minimum round time: [ruleset.minimum_round_time], current round time: [world.time - SSticker.round_start_time]")
|
||||
continue
|
||||
|
||||
// If admins have disabled dynamic from picking from the ghost pool
|
||||
if(istype(ruleset, /datum/dynamic_ruleset/midround/from_ghosts) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT))
|
||||
log_dynamic("FAIL: [ruleset] is a from_ghosts ruleset, but ghost roles are disabled")
|
||||
continue
|
||||
|
||||
ruleset.trim_candidates()
|
||||
ruleset.load_templates()
|
||||
if (!ruleset.ready())
|
||||
log_dynamic("FAIL: [ruleset] is not ready()")
|
||||
continue
|
||||
|
||||
var/ruleset_is_heavy = (ruleset.midround_ruleset_style == MIDROUND_RULESET_STYLE_HEAVY)
|
||||
if (ruleset_is_heavy)
|
||||
drafted_heavies[ruleset] = ruleset.get_weight()
|
||||
else
|
||||
drafted_lights[ruleset] = ruleset.get_weight()
|
||||
|
||||
var/heavy_light_log_count = "[drafted_heavies.len] heavies / [drafted_lights.len] lights"
|
||||
|
||||
log_dynamic("Rolling [spawn_heavy ? "HEAVY" : "LIGHT"]... [heavy_light_log_count]")
|
||||
|
||||
if (spawn_heavy && drafted_heavies.len > 0 && pick_midround_rule(drafted_heavies, "heavy rulesets"))
|
||||
return
|
||||
else if (drafted_lights.len > 0 && pick_midround_rule(drafted_lights, "light rulesets"))
|
||||
if (spawn_heavy)
|
||||
log_dynamic_and_announce("A heavy ruleset was intended to roll, but there weren't any available. [heavy_light_log_count]")
|
||||
else
|
||||
log_dynamic_and_announce("No midround rulesets could be drafted. ([heavy_light_log_count])")
|
||||
|
||||
/// Gets the chance for a heavy ruleset midround injection, the dry_run argument is only used for forced injection.
|
||||
/datum/controller/subsystem/dynamic/proc/get_heavy_midround_injection_chance(dry_run)
|
||||
var/chance_modifier = 1
|
||||
var/next_midround_roll = next_midround_injection() - SSticker.round_start_time
|
||||
|
||||
if (random_event_hijacked != HIJACKED_NOTHING)
|
||||
chance_modifier += (hijacked_random_event_injection_chance_modifier / 100)
|
||||
|
||||
if (GLOB.current_living_antags.len == 0)
|
||||
chance_modifier += 0.5
|
||||
|
||||
if (GLOB.dead_player_list.len > GLOB.alive_player_list.len)
|
||||
chance_modifier -= 0.3
|
||||
|
||||
var/heavy_coefficient = CLAMP01((next_midround_roll - midround_light_upper_bound) / (midround_heavy_lower_bound - midround_light_upper_bound))
|
||||
|
||||
return 100 * (heavy_coefficient * max(1, chance_modifier))
|
||||
@@ -0,0 +1,285 @@
|
||||
/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 sort_names.
|
||||
var/name = ""
|
||||
/// For admin logging and round end screen, do not change this unless making a new rule type.
|
||||
var/ruletype = ""
|
||||
/// 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 will be able to draft this ruleset again later on. (doesn't apply for roundstart rules)
|
||||
var/repeatable = FALSE
|
||||
/// If set higher than 0 decreases weight by itself causing the ruleset to appear less often the more it is repeated.
|
||||
var/repeatable_weight_decrease = 2
|
||||
/// List of players that are being drafted for this rule
|
||||
var/list/mob/candidates = list()
|
||||
/// List of players that were selected for this rule. This can be minds, or mobs.
|
||||
var/list/assigned = list()
|
||||
/// Preferences flag such as ROLE_WIZARD that need to be turned on for players to be antag.
|
||||
var/antag_flag = null
|
||||
/// The antagonist datum that is assigned to the mobs mind on ruleset execution.
|
||||
var/datum/antagonist/antag_datum = null
|
||||
/// The required minimum account age for this ruleset.
|
||||
var/minimum_required_age = 7
|
||||
/// If set, and config flag protect_roles_from_antagonist is false, then the rule will not pick players from these roles.
|
||||
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. 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(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
/// If enemy_roles was set, this is the amount of enemy job workers needed per threat_level range (0-10,10-20,etc) IMPORTANT: DOES NOT WORK ON ROUNDSTART RULESETS.
|
||||
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
|
||||
/// 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 threat level when the rule is executed.
|
||||
var/cost = 0
|
||||
/// Cost per level the rule scales up.
|
||||
var/scaling_cost = 0
|
||||
/// How many times a rule has scaled up upon getting picked.
|
||||
var/scaled_times = 0
|
||||
/// Used for the roundend report
|
||||
var/total_cost = 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 dynamic'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)
|
||||
/// If a role is to be considered another for the purpose of banning.
|
||||
var/antag_flag_override = null
|
||||
/// If set, will check this preference instead of antag_flag.
|
||||
var/antag_preference = null
|
||||
/// If a ruleset type which is in this list has been executed, then the ruleset will not be executed.
|
||||
var/list/blocking_rules = list()
|
||||
/// The minimum amount of players required for the rule to be considered.
|
||||
var/minimum_players = 0
|
||||
/// The maximum amount of players required for the rule to be considered.
|
||||
/// Anything below zero or exactly zero is ignored.
|
||||
var/maximum_players = 0
|
||||
/// Calculated during acceptable(), used in scaling and team sizes.
|
||||
var/indice_pop = 0
|
||||
/// Base probability used in scaling. The higher it is, the more likely to scale. Kept as a var to allow for config editing._SendSignal(sigtype, list/arguments)
|
||||
var/base_prob = 60
|
||||
/// Delay for when execute will get called from the time of post_setup (roundstart) or process (midround/latejoin).
|
||||
/// Make sure your ruleset works with execute being called during the game when using this, and that the clean_up proc reverts it properly in case of faliure.
|
||||
var/delay = 0
|
||||
|
||||
/// Judges the amount of antagonists to apply, for both solo and teams.
|
||||
/// Note that some antagonists (such as traitors, lings, heretics, etc) will add more based on how many times they've been scaled.
|
||||
/// Written as a linear equation--ceil(x/denominator) + offset, or as a fixed constant.
|
||||
/// If written as a linear equation, will be in the form of `list("denominator" = denominator, "offset" = offset).
|
||||
var/antag_cap = 0
|
||||
|
||||
/// A list, or null, of templates that the ruleset depends on to function correctly
|
||||
var/list/ruleset_lazy_templates
|
||||
|
||||
/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)
|
||||
|
||||
..()
|
||||
|
||||
/datum/dynamic_ruleset/roundstart // One or more of those drafted at roundstart
|
||||
ruletype = ROUNDSTART_RULESET
|
||||
|
||||
// Can be drafted when a player joins the server
|
||||
/datum/dynamic_ruleset/latejoin
|
||||
ruletype = LATEJOIN_RULESET
|
||||
|
||||
/// 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)
|
||||
var/ruleset_forced = GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED
|
||||
if (ruleset_forced != RULESET_NOT_FORCED)
|
||||
if (ruleset_forced == RULESET_FORCE_ENABLED)
|
||||
return TRUE
|
||||
else
|
||||
log_dynamic("FAIL: [src] was disabled in admin panel.")
|
||||
return FALSE
|
||||
|
||||
if(!is_valid_population(population))
|
||||
var/range = maximum_players > 0 ? "([minimum_players] - [maximum_players])" : "(minimum: [minimum_players])"
|
||||
log_dynamic("FAIL: [src] failed acceptable: min/max players out of range [range] vs population ([population])")
|
||||
return FALSE
|
||||
|
||||
if (!is_valid_threat(population, threat_level))
|
||||
log_dynamic("FAIL: [src] failed acceptable: threat_level ([threat_level]) < requirement ([requirements[indice_pop]])")
|
||||
return FALSE
|
||||
|
||||
return TRUE
|
||||
|
||||
/// Returns true if we have enough players to run
|
||||
/datum/dynamic_ruleset/proc/is_valid_population(population)
|
||||
if(minimum_players > population)
|
||||
return FALSE
|
||||
if(maximum_players > 0 && population > maximum_players)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/// Sets the current threat indices and returns true if we're inside of them
|
||||
/datum/dynamic_ruleset/proc/is_valid_threat(population, threat_level)
|
||||
pop_per_requirement = pop_per_requirement > 0 ? pop_per_requirement : SSdynamic.pop_per_requirement
|
||||
indice_pop = min(requirements.len,round(population/pop_per_requirement)+1)
|
||||
return threat_level >= requirements[indice_pop]
|
||||
|
||||
/// 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 (SSdynamic.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)) / SSdynamic.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) / SSdynamic.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
|
||||
|
||||
/// Called on pre_setup for roundstart rulesets.
|
||||
/// Do everything you need to do before job is assigned here.
|
||||
/// IMPORTANT: ASSIGN special_role HERE
|
||||
/datum/dynamic_ruleset/proc/pre_execute()
|
||||
return TRUE
|
||||
|
||||
/// Called on post_setup on roundstart and when the rule executes on midround and latejoin.
|
||||
/// Give your candidates or assignees equipment and antag datum here.
|
||||
/datum/dynamic_ruleset/proc/execute()
|
||||
for(var/datum/mind/M in assigned)
|
||||
M.add_antag_datum(antag_datum)
|
||||
GLOB.pre_setup_antags -= M
|
||||
return TRUE
|
||||
|
||||
/// Rulesets can be reused, so when we're done setting one up we want to wipe its memory of the people it was selecting over
|
||||
/// This isn't Destroy we aren't deleting it here, rulesets free when nothing holds a ref. This is just to prevent hung refs.
|
||||
/datum/dynamic_ruleset/proc/forget_startup()
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
candidates = list()
|
||||
assigned = list()
|
||||
antag_datum = null
|
||||
|
||||
/// Here you can perform any additional checks you want. (such as checking the map etc)
|
||||
/// Remember that on roundstart no one knows what their job is at this point.
|
||||
/// IMPORTANT: If ready() returns TRUE, that means pre_execute() or execute() should never fail!
|
||||
/datum/dynamic_ruleset/proc/ready(forced = 0)
|
||||
return check_candidates()
|
||||
|
||||
/// This should always be called before ready is, to ensure that the ruleset can locate map/template based landmarks as needed
|
||||
/datum/dynamic_ruleset/proc/load_templates()
|
||||
for(var/template in ruleset_lazy_templates)
|
||||
SSmapping.lazy_load_template(template)
|
||||
|
||||
/// 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()
|
||||
SSdynamic.refund_threat(cost + (scaled_times * scaling_cost))
|
||||
SSdynamic.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
|
||||
/// Note: If you don't want repeatable rulesets to decrease their weight use the weight variable directly
|
||||
/datum/dynamic_ruleset/proc/get_weight()
|
||||
if(repeatable && weight > 1 && repeatable_weight_decrease > 0)
|
||||
for(var/datum/dynamic_ruleset/DR in SSdynamic.executed_rules)
|
||||
if(istype(DR, type))
|
||||
weight = max(weight-repeatable_weight_decrease,1)
|
||||
return weight
|
||||
|
||||
/// Checks if there are enough candidates to run, and logs otherwise
|
||||
/datum/dynamic_ruleset/proc/check_candidates()
|
||||
if (required_candidates <= candidates.len)
|
||||
return TRUE
|
||||
|
||||
log_dynamic("FAIL: [src] does not have enough candidates ([required_candidates] needed, [candidates.len] found)")
|
||||
return FALSE
|
||||
|
||||
/// Here you can remove candidates that do not meet your requirements.
|
||||
/// This means if their job is not correct or they have disconnected you can remove them from candidates here.
|
||||
/// Usually this does not need to be changed unless you need some specific requirements from your candidates.
|
||||
/datum/dynamic_ruleset/proc/trim_candidates()
|
||||
return
|
||||
|
||||
/// Set mode_result and news report here.
|
||||
/// Only called if ruleset is flagged as HIGH_IMPACT_RULESET
|
||||
/datum/dynamic_ruleset/proc/round_result()
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// ROUNDSTART RULESETS //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/// 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/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(candidate_client.get_remaining_days(minimum_required_age) > 0)
|
||||
candidates.Remove(candidate_player)
|
||||
continue
|
||||
|
||||
if(candidate_player.mind.special_role) // We really don't want to give antag to an antag.
|
||||
candidates.Remove(candidate_player)
|
||||
continue
|
||||
|
||||
if (!((antag_preference || antag_flag) in candidate_client.prefs.be_special))
|
||||
candidates.Remove(candidate_player)
|
||||
continue
|
||||
|
||||
if (is_banned_from(candidate_player.ckey, list(antag_flag_override || 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) && SSjob.check_job_eligibility(candidate_player, job, "Dynamic Roundstart TC", add_job_to_log = TRUE) == JOB_AVAILABLE)
|
||||
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(population, forced = FALSE)
|
||||
return ..()
|
||||
@@ -0,0 +1,254 @@
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// LATEJOIN RULESETS //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/latejoin/trim_candidates()
|
||||
for(var/mob/P in candidates)
|
||||
if(!P.client || !P.mind || is_unassigned_job(P.mind.assigned_role)) // Are they connected?
|
||||
candidates.Remove(P)
|
||||
else if (P.client.get_remaining_days(minimum_required_age) > 0)
|
||||
candidates.Remove(P)
|
||||
else if(P.mind.assigned_role.title in restricted_roles) // Does their job allow for it?
|
||||
candidates.Remove(P)
|
||||
else if((exclusive_roles.len > 0) && !(P.mind.assigned_role.title in exclusive_roles)) // Is the rule exclusive to their job?
|
||||
candidates.Remove(P)
|
||||
else if (!((antag_preference || antag_flag) in P.client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE)))
|
||||
candidates.Remove(P)
|
||||
|
||||
/datum/dynamic_ruleset/latejoin/ready(forced = 0)
|
||||
if (forced)
|
||||
return ..()
|
||||
|
||||
var/job_check = 0
|
||||
if (enemy_roles.len > 0)
|
||||
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.title in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role.title 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(SSdynamic.threat_level/10)
|
||||
var/ruleset_forced = (GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED) == RULESET_FORCE_ENABLED
|
||||
if (!ruleset_forced && job_check < required_enemies[threat])
|
||||
log_dynamic("FAIL: [src] is not ready, because there are not enough enemies: [required_enemies[threat]] needed, [job_check] found")
|
||||
return FALSE
|
||||
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/latejoin/execute()
|
||||
var/mob/M = pick(candidates)
|
||||
assigned += M.mind
|
||||
M.mind.special_role = antag_flag
|
||||
M.mind.add_antag_datum(antag_datum)
|
||||
return TRUE
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// SYNDICATE TRAITORS //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/latejoin/infiltrator
|
||||
name = "Syndicate Infiltrator"
|
||||
antag_datum = /datum/antagonist/traitor/infiltrator
|
||||
antag_flag = ROLE_SYNDICATE_INFILTRATOR
|
||||
antag_flag_override = ROLE_TRAITOR
|
||||
protected_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_PERSONNEL,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
)
|
||||
required_candidates = 1
|
||||
weight = 11
|
||||
cost = 5
|
||||
requirements = list(5,5,5,5,5,5,5,5,5,5)
|
||||
repeatable = TRUE
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// REVOLUTIONARY PROVOCATEUR //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/latejoin/provocateur
|
||||
name = "Provocateur"
|
||||
persistent = TRUE
|
||||
antag_datum = /datum/antagonist/rev/head
|
||||
antag_flag = ROLE_PROVOCATEUR
|
||||
antag_flag_override = ROLE_REV_HEAD
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CAPTAIN,
|
||||
JOB_CHIEF_ENGINEER,
|
||||
JOB_CHIEF_MEDICAL_OFFICER,
|
||||
JOB_CYBORG,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_PERSONNEL,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_QUARTERMASTER,
|
||||
JOB_RESEARCH_DIRECTOR,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
enemy_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
weight = 1
|
||||
delay = 1 MINUTES // Prevents rule start while head is offstation.
|
||||
cost = 10
|
||||
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
|
||||
var/datum/team/revolution/revolution
|
||||
|
||||
/datum/dynamic_ruleset/latejoin/provocateur/ready(forced=FALSE)
|
||||
if (forced)
|
||||
required_heads_of_staff = 1
|
||||
if(!..())
|
||||
return FALSE
|
||||
var/head_check = 0
|
||||
for(var/mob/player in GLOB.alive_player_list)
|
||||
if (player.mind.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
|
||||
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.
|
||||
assigned += M.mind
|
||||
M.mind.special_role = antag_flag
|
||||
revolution = new()
|
||||
var/datum/antagonist/rev/head/new_head = new()
|
||||
new_head.give_flash = TRUE
|
||||
new_head.give_hud = TRUE
|
||||
new_head.remove_clumsy = TRUE
|
||||
new_head = M.mind.add_antag_datum(new_head, revolution)
|
||||
revolution.update_objectives()
|
||||
revolution.update_rev_heads()
|
||||
SSshuttle.registerHostileEnvironment(revolution)
|
||||
return TRUE
|
||||
else
|
||||
log_dynamic("[ruletype] [name] discarded [M.name] from head revolutionary due to ineligibility.")
|
||||
log_dynamic("[ruletype] [name] failed to get any eligible headrevs. Refunding [cost] threat.")
|
||||
return FALSE
|
||||
|
||||
/datum/dynamic_ruleset/latejoin/provocateur/rule_process()
|
||||
var/winner = revolution.process_victory()
|
||||
if (isnull(winner))
|
||||
return
|
||||
|
||||
finished = winner
|
||||
|
||||
if(winner == REVOLUTION_VICTORY)
|
||||
GLOB.revolutionary_win = TRUE
|
||||
|
||||
return RULESET_STOP_PROCESSING
|
||||
|
||||
/// Checks for revhead loss conditions and other antag datums.
|
||||
/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/round_result()
|
||||
revolution.round_result(finished)
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// HERETIC SMUGGLER //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/latejoin/heretic_smuggler
|
||||
name = "Heretic Smuggler"
|
||||
antag_datum = /datum/antagonist/heretic
|
||||
antag_flag = ROLE_HERETIC_SMUGGLER
|
||||
antag_flag_override = ROLE_HERETIC
|
||||
protected_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_PERSONNEL,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
)
|
||||
required_candidates = 1
|
||||
weight = 8
|
||||
cost = 6
|
||||
requirements = list(101,101,50,10,10,10,10,10,10,10)
|
||||
repeatable = TRUE
|
||||
|
||||
/datum/dynamic_ruleset/latejoin/heretic_smuggler/execute()
|
||||
var/mob/picked_mob = pick(candidates)
|
||||
assigned += picked_mob.mind
|
||||
picked_mob.mind.special_role = antag_flag
|
||||
var/datum/antagonist/heretic/new_heretic = picked_mob.mind.add_antag_datum(antag_datum)
|
||||
|
||||
// Heretics passively gain influence over time.
|
||||
// As a consequence, latejoin heretics start out at a massive
|
||||
// disadvantage if the round's been going on for a while.
|
||||
// Let's give them some influence points when they arrive.
|
||||
new_heretic.knowledge_points += round((world.time - SSticker.round_start_time) / new_heretic.passive_gain_timer)
|
||||
// BUT let's not give smugglers a million points on arrival.
|
||||
// Limit it to four missed passive gain cycles (4 points).
|
||||
new_heretic.knowledge_points = min(new_heretic.knowledge_points, 5)
|
||||
|
||||
return TRUE
|
||||
|
||||
/// Ruleset for latejoin changelings
|
||||
/datum/dynamic_ruleset/latejoin/stowaway_changeling
|
||||
name = "Stowaway Changeling"
|
||||
antag_datum = /datum/antagonist/changeling
|
||||
antag_flag = ROLE_STOWAWAY_CHANGELING
|
||||
antag_flag_override = ROLE_CHANGELING
|
||||
protected_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_PERSONNEL,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
)
|
||||
required_candidates = 1
|
||||
weight = 2
|
||||
cost = 12
|
||||
requirements = list(101,101,60,50,40,20,20,10,10,10)
|
||||
repeatable = TRUE
|
||||
|
||||
/datum/dynamic_ruleset/latejoin/stowaway_changeling/execute()
|
||||
var/mob/picked_mob = pick(candidates)
|
||||
assigned += picked_mob.mind
|
||||
picked_mob.mind.special_role = antag_flag
|
||||
picked_mob.mind.add_antag_datum(antag_datum)
|
||||
return TRUE
|
||||
@@ -0,0 +1,933 @@
|
||||
/// 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
|
||||
|
||||
/// Midround Rulesets
|
||||
/datum/dynamic_ruleset/midround // Can be drafted once in a while during a round
|
||||
ruletype = MIDROUND_RULESET
|
||||
var/midround_ruleset_style
|
||||
/// If the ruleset should be restricted from ghost roles.
|
||||
var/restrict_ghost_roles = TRUE
|
||||
/// What mob type the ruleset is restricted to.
|
||||
var/required_type = /mob/living/carbon/human
|
||||
var/list/living_players = list()
|
||||
var/list/living_antags = list()
|
||||
var/list/dead_players = list()
|
||||
var/list/list_observers = list()
|
||||
|
||||
/// The minimum round time before this ruleset will show up
|
||||
var/minimum_round_time = 0
|
||||
/// Abstract root value
|
||||
var/abstract_type = /datum/dynamic_ruleset/midround
|
||||
|
||||
/datum/dynamic_ruleset/midround/forget_startup()
|
||||
living_players = list()
|
||||
living_antags = list()
|
||||
dead_players = list()
|
||||
list_observers = list()
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts
|
||||
weight = 0
|
||||
required_type = /mob/dead/observer
|
||||
abstract_type = /datum/dynamic_ruleset/midround/from_ghosts
|
||||
/// 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/from_ghosts/check_candidates()
|
||||
var/dead_count = dead_players.len + list_observers.len
|
||||
if (required_candidates <= dead_count)
|
||||
return TRUE
|
||||
|
||||
log_dynamic("FAIL: [src], a from_ghosts ruleset, did not have enough dead candidates: [required_candidates] needed, [dead_count] found")
|
||||
|
||||
return FALSE
|
||||
|
||||
/datum/dynamic_ruleset/midround/trim_candidates()
|
||||
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/to_trim = list())
|
||||
var/list/trimmed_list = to_trim.Copy()
|
||||
for(var/mob/creature in trimmed_list)
|
||||
if (!istype(creature, required_type))
|
||||
trimmed_list.Remove(creature)
|
||||
continue
|
||||
if (isnull(creature.client)) // Are they connected?
|
||||
trimmed_list.Remove(creature)
|
||||
continue
|
||||
if(creature.client.get_remaining_days(minimum_required_age) > 0)
|
||||
trimmed_list.Remove(creature)
|
||||
continue
|
||||
if (!((antag_preference || antag_flag) in creature.client.prefs.be_special))
|
||||
trimmed_list.Remove(creature)
|
||||
continue
|
||||
if (is_banned_from(creature.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE)))
|
||||
trimmed_list.Remove(creature)
|
||||
continue
|
||||
|
||||
if (isnull(creature.mind))
|
||||
continue
|
||||
|
||||
if (restrict_ghost_roles && (creature.mind.assigned_role.title in GLOB.exp_specialmap[EXP_TYPE_SPECIAL])) // Are they playing a ghost role?
|
||||
trimmed_list.Remove(creature)
|
||||
continue
|
||||
if (creature.mind.assigned_role.title in restricted_roles) // Does their job allow it?
|
||||
trimmed_list.Remove(creature)
|
||||
continue
|
||||
if (length(exclusive_roles) && !(creature.mind.assigned_role.title in exclusive_roles)) // Is the rule exclusive to their job?
|
||||
trimmed_list.Remove(creature)
|
||||
continue
|
||||
if(HAS_TRAIT(creature, TRAIT_MIND_TEMPORARILY_GONE)) // are they out of body?
|
||||
trimmed_list.Remove(creature)
|
||||
continue
|
||||
if(HAS_TRAIT(creature, TRAIT_TEMPORARY_BODY)) // are they an avatar?
|
||||
trimmed_list.Remove(creature)
|
||||
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
|
||||
// subtype your midround with /from_ghosts or /from_living to get candidate checking. Or check yourself by subtyping from neither
|
||||
/datum/dynamic_ruleset/midround/ready(forced = FALSE)
|
||||
if (forced)
|
||||
return TRUE
|
||||
|
||||
var/job_check = 0
|
||||
if (enemy_roles.len > 0)
|
||||
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.title in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role.title 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(SSdynamic.threat_level/10)
|
||||
var/ruleset_forced = (GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED) == RULESET_FORCE_ENABLED
|
||||
if (!ruleset_forced && job_check < required_enemies[threat])
|
||||
log_dynamic("FAIL: [src] is not ready, because there are not enough enemies: [required_enemies[threat]] needed, [job_check] found")
|
||||
return FALSE
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/execute()
|
||||
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())
|
||||
if (possible_volunteers.len <= 0) // This shouldn't happen, as ready() should return FALSE if there is not a single valid candidate
|
||||
message_admins("Possible volunteers was 0. This shouldn't appear, because of ready(), unless you forced it!")
|
||||
return
|
||||
|
||||
SSdynamic.log_dynamic_and_announce("Polling [possible_volunteers.len] players to apply for the [name] ruleset.")
|
||||
candidates = poll_ghost_candidates("Looking for volunteers to become [antag_flag] for [name]", antag_flag_override, antag_flag || antag_flag_override, poll_time = 300)
|
||||
|
||||
if(!candidates || candidates.len <= 0)
|
||||
SSdynamic.log_dynamic_and_announce("The ruleset [name] received no applications.")
|
||||
SSdynamic.executed_rules -= src
|
||||
attempt_replacement()
|
||||
return
|
||||
|
||||
SSdynamic.log_dynamic_and_announce("[candidates.len] players volunteered for [name].")
|
||||
review_applications()
|
||||
|
||||
/// 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)
|
||||
SSdynamic.executed_rules -= src
|
||||
return
|
||||
for (var/i = 1, i <= required_candidates, i++)
|
||||
if(candidates.len <= 0)
|
||||
break
|
||||
var/mob/applicant = pick(candidates)
|
||||
candidates -= applicant
|
||||
if(!isobserver(applicant))
|
||||
if(applicant.stat == DEAD) // Not an observer? If they're dead, make them one.
|
||||
applicant = applicant.ghostize(FALSE)
|
||||
else // Not dead? Disregard them, pick a new applicant
|
||||
i--
|
||||
continue
|
||||
if(!applicant)
|
||||
i--
|
||||
continue
|
||||
assigned += applicant
|
||||
finish_applications()
|
||||
|
||||
/// Here the accepted applications get generated bodies and their setup is finished.
|
||||
/// Called by review_applications()
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/proc/finish_applications()
|
||||
var/i = 0
|
||||
for(var/mob/applicant as anything in assigned)
|
||||
i++
|
||||
var/mob/new_character = applicant
|
||||
if(makeBody)
|
||||
new_character = generate_ruleset_body(applicant)
|
||||
finish_setup(new_character, i)
|
||||
notify_ghosts(
|
||||
"[applicant.name] has been picked for the ruleset [name]!",
|
||||
source = new_character,
|
||||
)
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/proc/generate_ruleset_body(mob/applicant)
|
||||
var/mob/living/carbon/human/new_character = make_body(applicant)
|
||||
new_character.dna.remove_all_mutations()
|
||||
return new_character
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/proc/finish_setup(mob/new_character, index)
|
||||
var/datum/antagonist/new_role = new antag_datum()
|
||||
setup_role(new_role)
|
||||
new_character.mind.add_antag_datum(new_role)
|
||||
new_character.mind.special_role = antag_flag
|
||||
|
||||
/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/from_living/autotraitor/sleeper_agent = new
|
||||
|
||||
SSdynamic.configure_ruleset(sleeper_agent)
|
||||
|
||||
if (!SSdynamic.picking_specific_rule(sleeper_agent))
|
||||
return
|
||||
|
||||
SSdynamic.picking_specific_rule(/datum/dynamic_ruleset/latejoin/infiltrator)
|
||||
|
||||
///subtype to handle checking players
|
||||
/datum/dynamic_ruleset/midround/from_living
|
||||
weight = 0
|
||||
abstract_type = /datum/dynamic_ruleset/midround/from_living
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_living/ready(forced)
|
||||
if(!check_candidates())
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
|
||||
/// Midround Traitor Ruleset (From Living)
|
||||
/datum/dynamic_ruleset/midround/from_living/autotraitor
|
||||
name = "Syndicate Sleeper Agent"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
||||
antag_datum = /datum/antagonist/traitor/infiltrator/sleeper_agent
|
||||
antag_flag = ROLE_SLEEPER_AGENT
|
||||
antag_flag_override = ROLE_TRAITOR
|
||||
protected_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_PERSONNEL,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
ROLE_POSITRONIC_BRAIN,
|
||||
)
|
||||
required_candidates = 1
|
||||
weight = 35
|
||||
cost = 3
|
||||
requirements = list(3,3,3,3,3,3,3,3,3,3)
|
||||
repeatable = TRUE
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_living/autotraitor/trim_candidates()
|
||||
..()
|
||||
candidates = living_players
|
||||
for(var/mob/living/player in candidates)
|
||||
if(issilicon(player)) // Your assigned role doesn't change when you are turned into a silicon.
|
||||
candidates -= player
|
||||
else if(is_centcom_level(player.z))
|
||||
candidates -= player // We don't autotator people in CentCom
|
||||
else if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0))
|
||||
candidates -= player // We don't autotator people with roles already
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_living/autotraitor/execute()
|
||||
var/mob/M = pick(candidates)
|
||||
assigned += M
|
||||
candidates -= M
|
||||
var/datum/antagonist/traitor/infiltrator/sleeper_agent/newTraitor = new
|
||||
M.mind.add_antag_datum(newTraitor)
|
||||
message_admins("[ADMIN_LOOKUPFLW(M)] was selected by the [name] ruleset and has been made into a midround traitor.")
|
||||
log_dynamic("[key_name(M)] was selected by the [name] ruleset and has been made into a midround traitor.")
|
||||
return TRUE
|
||||
|
||||
/// Midround Malf AI Ruleset (From Living)
|
||||
/datum/dynamic_ruleset/midround/malf
|
||||
name = "Malfunctioning AI"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_datum = /datum/antagonist/malf_ai
|
||||
antag_flag = ROLE_MALF_MIDROUND
|
||||
antag_flag_override = ROLE_MALF
|
||||
enemy_roles = list(
|
||||
JOB_CHEMIST,
|
||||
JOB_CHIEF_ENGINEER,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_RESEARCH_DIRECTOR,
|
||||
JOB_SCIENTIST,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
exclusive_roles = list(JOB_AI)
|
||||
required_enemies = list(4,4,4,4,4,4,2,2,2,0)
|
||||
required_candidates = 1
|
||||
minimum_players = 25
|
||||
weight = 2
|
||||
cost = 10
|
||||
required_type = /mob/living/silicon/ai
|
||||
blocking_rules = list(/datum/dynamic_ruleset/roundstart/malf_ai)
|
||||
|
||||
/datum/dynamic_ruleset/midround/malf/trim_candidates()
|
||||
..()
|
||||
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/new_malf_ai = pick_n_take(candidates)
|
||||
assigned += new_malf_ai.mind
|
||||
var/datum/antagonist/malf_ai/malf_antag_datum = new
|
||||
new_malf_ai.mind.special_role = antag_flag
|
||||
new_malf_ai.mind.add_antag_datum(malf_antag_datum)
|
||||
if(prob(MALF_ION_PROB))
|
||||
priority_announce("Ion storm detected near the station. Please check all AI-controlled equipment for errors.", "Anomaly Alert", ANNOUNCER_IONSTORM)
|
||||
if(prob(REPLACE_LAW_WITH_ION_PROB))
|
||||
new_malf_ai.replace_random_law(generate_ion_law(), list(LAW_INHERENT, LAW_SUPPLIED, LAW_ION), LAW_ION)
|
||||
else
|
||||
new_malf_ai.add_ion_law(generate_ion_law())
|
||||
return TRUE
|
||||
|
||||
/// Midround Wizard Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/wizard
|
||||
name = "Wizard"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_datum = /datum/antagonist/wizard
|
||||
antag_flag = ROLE_WIZARD_MIDROUND
|
||||
antag_flag_override = ROLE_WIZARD
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
weight = 1
|
||||
cost = 10
|
||||
requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED
|
||||
flags = HIGH_IMPACT_RULESET
|
||||
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_WIZARDDEN)
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/wizard/ready(forced = FALSE)
|
||||
if(!check_candidates())
|
||||
return FALSE
|
||||
if(!length(GLOB.wizardstart))
|
||||
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.")
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/wizard/finish_setup(mob/new_character, index)
|
||||
..()
|
||||
new_character.forceMove(pick(GLOB.wizardstart))
|
||||
|
||||
/// Midround Nuclear Operatives Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/nuclear
|
||||
name = "Nuclear Assault"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_flag = ROLE_OPERATIVE_MIDROUND
|
||||
antag_flag_override = ROLE_OPERATIVE
|
||||
antag_datum = /datum/antagonist/nukeop
|
||||
enemy_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
required_enemies = list(3,3,3,3,3,2,1,1,0,0)
|
||||
required_candidates = 5
|
||||
weight = 5
|
||||
cost = 7
|
||||
minimum_round_time = 70 MINUTES
|
||||
requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED
|
||||
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE)
|
||||
flags = HIGH_IMPACT_RULESET
|
||||
|
||||
var/list/operative_cap = list(2,2,3,3,4,5,5,5,5,5)
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/nuclear/acceptable(population=0, threat_level=0)
|
||||
if (locate(/datum/dynamic_ruleset/roundstart/nuclear) in SSdynamic.executed_rules)
|
||||
return FALSE // Unavailable if nuke ops were already sent at roundstart
|
||||
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 (!check_candidates())
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_applications()
|
||||
var/mob/leader = get_most_experienced(assigned, ROLE_NUCLEAR_OPERATIVE)
|
||||
if(leader)
|
||||
assigned.Remove(leader)
|
||||
assigned.Insert(1, leader)
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_setup(mob/new_character, index)
|
||||
new_character.mind.set_assigned_role(SSjob.GetJobType(/datum/job/nuclear_operative))
|
||||
new_character.mind.special_role = ROLE_NUCLEAR_OPERATIVE
|
||||
if(index == 1)
|
||||
var/datum/antagonist/nukeop/leader/leader_antag_datum = new()
|
||||
new_character.mind.add_antag_datum(leader_antag_datum)
|
||||
return
|
||||
return ..()
|
||||
|
||||
/// Midround Blob Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/blob
|
||||
name = "Blob"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_datum = /datum/antagonist/blob
|
||||
antag_flag = ROLE_BLOB
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
minimum_round_time = 35 MINUTES
|
||||
weight = 3
|
||||
cost = 8
|
||||
minimum_players = 25
|
||||
repeatable = TRUE
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/blob/generate_ruleset_body(mob/applicant)
|
||||
var/body = applicant.become_overmind()
|
||||
return body
|
||||
|
||||
/// Midround Blob Infection Ruleset (From Living)
|
||||
/datum/dynamic_ruleset/midround/from_living/blob_infection
|
||||
name = "Blob Infection"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_datum = /datum/antagonist/blob/infection
|
||||
antag_flag = ROLE_BLOB_INFECTION
|
||||
antag_flag_override = ROLE_BLOB
|
||||
protected_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
ROLE_POSITRONIC_BRAIN,
|
||||
)
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
minimum_round_time = 35 MINUTES
|
||||
weight = 3
|
||||
cost = 10
|
||||
minimum_players = 25
|
||||
repeatable = TRUE
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_living/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/from_living/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 ..()
|
||||
|
||||
/// Midround Xenomorph Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph
|
||||
name = "Alien Infestation"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_datum = /datum/antagonist/xeno
|
||||
antag_flag = ROLE_ALIEN
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
minimum_round_time = 40 MINUTES
|
||||
weight = 5
|
||||
cost = 10
|
||||
minimum_players = 25
|
||||
repeatable = TRUE
|
||||
var/list/vents = list()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/forget_startup()
|
||||
vents = list()
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/execute()
|
||||
// 50% chance of being incremented by one
|
||||
required_candidates += prob(50)
|
||||
var/list/vent_pumps = SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/atmospherics/components/unary/vent_pump)
|
||||
for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent as anything in vent_pumps)
|
||||
if(QDELETED(temp_vent))
|
||||
continue
|
||||
if(is_station_level(temp_vent.loc.z) && !temp_vent.welded)
|
||||
var/datum/pipeline/temp_vent_parent = temp_vent.parents[1]
|
||||
if(!temp_vent_parent)
|
||||
continue // No parent vent
|
||||
// Stops Aliens getting stuck in small networks.
|
||||
// See: Security, Virology
|
||||
if(temp_vent_parent.other_atmos_machines.len > 20)
|
||||
vents += temp_vent
|
||||
if(!vents.len)
|
||||
return FALSE
|
||||
. = ..()
|
||||
|
||||
/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)
|
||||
new_xeno.key = applicant.key
|
||||
new_xeno.move_into_vent(vent)
|
||||
message_admins("[ADMIN_LOOKUPFLW(new_xeno)] has been made into an alien by the midround ruleset.")
|
||||
log_dynamic("[key_name(new_xeno)] was spawned as an alien by the midround ruleset.")
|
||||
return new_xeno
|
||||
|
||||
/// Midround Nightmare Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/nightmare
|
||||
name = "Nightmare"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
||||
antag_datum = /datum/antagonist/nightmare
|
||||
antag_flag = ROLE_NIGHTMARE
|
||||
antag_flag_override = ROLE_ALIEN
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
weight = 3
|
||||
cost = 5
|
||||
minimum_players = 15
|
||||
repeatable = TRUE
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/nightmare/acceptable(population = 0, threat_level = 0)
|
||||
var/turf/spawn_loc = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE) //Checks if there's a single safe, dark tile on station.
|
||||
if(!spawn_loc)
|
||||
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)
|
||||
player_mind.active = TRUE
|
||||
|
||||
var/mob/living/carbon/human/new_nightmare = new (find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE))
|
||||
player_mind.transfer_to(new_nightmare)
|
||||
player_mind.set_assigned_role(SSjob.GetJobType(/datum/job/nightmare))
|
||||
player_mind.special_role = ROLE_NIGHTMARE
|
||||
player_mind.add_antag_datum(/datum/antagonist/nightmare)
|
||||
new_nightmare.set_species(/datum/species/shadow/nightmare)
|
||||
|
||||
playsound(new_nightmare, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
|
||||
message_admins("[ADMIN_LOOKUPFLW(new_nightmare)] has been made into a Nightmare by the midround ruleset.")
|
||||
log_dynamic("[key_name(new_nightmare)] was spawned as a Nightmare by the midround ruleset.")
|
||||
return new_nightmare
|
||||
|
||||
/// Midround Space Dragon Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/space_dragon
|
||||
name = "Space Dragon"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_datum = /datum/antagonist/space_dragon
|
||||
antag_flag = ROLE_SPACE_DRAGON
|
||||
antag_flag_override = ROLE_SPACE_DRAGON
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
weight = 4
|
||||
cost = 7
|
||||
minimum_players = 25
|
||||
repeatable = TRUE
|
||||
var/list/spawn_locs = list()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/forget_startup()
|
||||
spawn_locs = list()
|
||||
return ..()
|
||||
|
||||
/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)
|
||||
message_admins("No valid spawn locations found, aborting...")
|
||||
return MAP_ERROR
|
||||
. = ..()
|
||||
|
||||
/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 = TRUE
|
||||
|
||||
var/mob/living/basic/space_dragon/S = new (pick(spawn_locs))
|
||||
player_mind.transfer_to(S)
|
||||
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_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
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/abductors
|
||||
name = "Abductors"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
||||
antag_datum = /datum/antagonist/abductor
|
||||
antag_flag = ROLE_ABDUCTOR
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 2
|
||||
required_applicants = 2
|
||||
weight = 4
|
||||
cost = 7
|
||||
minimum_players = 25
|
||||
repeatable = TRUE
|
||||
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_ABDUCTOR_SHIPS)
|
||||
|
||||
var/datum/team/abductor_team/new_team
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/abductors/forget_startup()
|
||||
new_team = null
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/abductors/ready(forced = FALSE)
|
||||
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)
|
||||
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)
|
||||
|
||||
/// Midround Space Ninja Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/space_ninja
|
||||
name = "Space Ninja"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_datum = /datum/antagonist/ninja
|
||||
antag_flag = ROLE_NINJA
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
weight = 4
|
||||
cost = 8
|
||||
minimum_players = 30
|
||||
repeatable = TRUE
|
||||
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY) // I mean, no one uses the nets anymore but whateva
|
||||
|
||||
var/list/spawn_locs = list()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/forget_startup()
|
||||
spawn_locs = list()
|
||||
return ..()
|
||||
|
||||
/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_dynamic("[key_name(ninja)] was spawned as a Space Ninja by the midround ruleset.")
|
||||
return ninja
|
||||
|
||||
/// Midround Spiders Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/spiders
|
||||
name = "Spiders"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_flag = ROLE_SPIDER
|
||||
required_type = /mob/dead/observer
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 0
|
||||
weight = 3
|
||||
cost = 8
|
||||
minimum_players = 27
|
||||
repeatable = TRUE
|
||||
var/spawncount = 2
|
||||
|
||||
/datum/dynamic_ruleset/midround/spiders/execute()
|
||||
create_midwife_eggs(spawncount)
|
||||
return ..()
|
||||
|
||||
/// Midround Revenant Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/revenant
|
||||
name = "Revenant"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
||||
antag_datum = /datum/antagonist/revenant
|
||||
antag_flag = ROLE_REVENANT
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
weight = 4
|
||||
cost = 5
|
||||
minimum_players = 15
|
||||
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/forget_startup()
|
||||
spawn_locs = list()
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/revenant/acceptable(population=0, threat_level=0)
|
||||
if(GLOB.dead_mob_list.len < dead_mobs_required)
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/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
|
||||
. = ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/revenant/generate_ruleset_body(mob/applicant)
|
||||
var/mob/living/basic/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
|
||||
|
||||
/// Midround Sentient Disease Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/sentient_disease
|
||||
name = "Sentient Disease"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_datum = /datum/antagonist/disease
|
||||
antag_flag = ROLE_SENTIENT_DISEASE
|
||||
required_candidates = 1
|
||||
minimum_players = 25
|
||||
weight = 4
|
||||
cost = 8
|
||||
repeatable = TRUE
|
||||
|
||||
/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, TYPE_PROC_REF(/mob/camera/disease, 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
|
||||
|
||||
/// Midround Space Pirates Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/pirates
|
||||
name = "Space Pirates"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
||||
antag_flag = "Space Pirates"
|
||||
required_type = /mob/dead/observer
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 0
|
||||
weight = 3
|
||||
cost = 8
|
||||
minimum_players = 20
|
||||
repeatable = TRUE
|
||||
|
||||
/datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat_level=0)
|
||||
if (SSmapping.is_planetary() || GLOB.light_pirate_gangs.len == 0)
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/pirates/execute()
|
||||
send_pirate_threat(GLOB.light_pirate_gangs)
|
||||
return ..()
|
||||
|
||||
/// Dangerous Space Pirates ruleset
|
||||
/datum/dynamic_ruleset/midround/dangerous_pirates
|
||||
name = "Dangerous Space Pirates"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
||||
antag_flag = "Space Pirates"
|
||||
required_type = /mob/dead/observer
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 0
|
||||
weight = 3
|
||||
cost = 8
|
||||
minimum_players = 25
|
||||
repeatable = TRUE
|
||||
|
||||
/datum/dynamic_ruleset/midround/dangerous_pirates/acceptable(population=0, threat_level=0)
|
||||
if (SSmapping.is_planetary() || GLOB.heavy_pirate_gangs.len == 0)
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/dangerous_pirates/execute()
|
||||
send_pirate_threat(GLOB.heavy_pirate_gangs)
|
||||
return ..()
|
||||
|
||||
/// Midround Obsessed Ruleset (From Living)
|
||||
/datum/dynamic_ruleset/midround/from_living/obsessed
|
||||
name = "Obsessed"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
||||
antag_datum = /datum/antagonist/obsessed
|
||||
antag_flag = ROLE_OBSESSED
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
ROLE_POSITRONIC_BRAIN,
|
||||
)
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
weight = 4
|
||||
cost = 3 // Doesn't have the same impact on rounds as revenants, dragons, sentient disease (10) or syndicate infiltrators (5).
|
||||
repeatable = TRUE
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_living/obsessed/trim_candidates()
|
||||
..()
|
||||
candidates = living_players
|
||||
for(var/mob/living/carbon/human/candidate in candidates)
|
||||
if( \
|
||||
!candidate.get_organ_by_type(/obj/item/organ/internal/brain) \
|
||||
|| candidate.mind.has_antag_datum(/datum/antagonist/obsessed) \
|
||||
|| candidate.stat == DEAD \
|
||||
|| !(ROLE_OBSESSED in candidate.client?.prefs?.be_special) \
|
||||
|| !candidate.mind.assigned_role \
|
||||
)
|
||||
candidates -= candidate
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_living/obsessed/execute()
|
||||
var/mob/living/carbon/human/obsessed = pick_n_take(candidates)
|
||||
obsessed.gain_trauma(/datum/brain_trauma/special/obsessed)
|
||||
message_admins("[ADMIN_LOOKUPFLW(obsessed)] has been made Obsessed by the midround ruleset.")
|
||||
log_game("[key_name(obsessed)] was made Obsessed by the midround ruleset.")
|
||||
return TRUE
|
||||
|
||||
/// Midround Space Changeling Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/changeling_midround
|
||||
name = "Space Changeling"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
||||
antag_datum = /datum/antagonist/changeling/space
|
||||
antag_flag = ROLE_CHANGELING_MIDROUND
|
||||
antag_flag_override = ROLE_CHANGELING
|
||||
required_type = /mob/dead/observer
|
||||
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
||||
required_candidates = 1
|
||||
weight = 3
|
||||
cost = 7
|
||||
minimum_players = 15
|
||||
repeatable = TRUE
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/changeling_midround/generate_ruleset_body(mob/applicant)
|
||||
var/body = generate_changeling_meteor(applicant)
|
||||
message_admins("[ADMIN_LOOKUPFLW(body)] has been made into a space changeling by the midround ruleset.")
|
||||
log_dynamic("[key_name(body)] was spawned as a space changeling by the midround ruleset.")
|
||||
return body
|
||||
|
||||
/// Midround Paradox Clone Ruleset (From Ghosts)
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone
|
||||
name = "Paradox Clone"
|
||||
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
||||
antag_datum = /datum/antagonist/paradox_clone
|
||||
antag_flag = ROLE_PARADOX_CLONE
|
||||
enemy_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
required_enemies = list(2, 2, 1, 1, 1, 1, 1, 0, 0, 0)
|
||||
required_candidates = 1
|
||||
weight = 4
|
||||
cost = 3
|
||||
repeatable = TRUE
|
||||
var/list/possible_spawns = list() ///places the antag can spawn
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/forget_startup()
|
||||
possible_spawns = list()
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/execute()
|
||||
possible_spawns += find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE)
|
||||
if(!possible_spawns.len)
|
||||
return MAP_ERROR
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/generate_ruleset_body(mob/applicant)
|
||||
var/datum/mind/player_mind = new /datum/mind(applicant.key)
|
||||
player_mind.active = TRUE
|
||||
|
||||
var/mob/living/carbon/human/clone_victim = find_original()
|
||||
var/mob/living/carbon/human/clone = clone_victim.make_full_human_copy(pick(possible_spawns))
|
||||
player_mind.transfer_to(clone)
|
||||
|
||||
var/datum/antagonist/paradox_clone/new_datum = player_mind.add_antag_datum(/datum/antagonist/paradox_clone)
|
||||
new_datum.original_ref = WEAKREF(clone_victim.mind)
|
||||
new_datum.setup_clone()
|
||||
|
||||
playsound(clone, 'sound/weapons/zapbang.ogg', 30, TRUE)
|
||||
new /obj/item/storage/toolbox/mechanical(clone.loc) //so they dont get stuck in maints
|
||||
|
||||
message_admins("[ADMIN_LOOKUPFLW(clone)] has been made into a Paradox Clone by the midround ruleset.")
|
||||
clone.log_message("was spawned as a Paradox Clone of [key_name(clone)] by the midround ruleset.", LOG_GAME)
|
||||
|
||||
return clone
|
||||
|
||||
/**
|
||||
* Trims through GLOB.player_list and finds a target
|
||||
* Returns a single human victim, if none is possible then returns null.
|
||||
*/
|
||||
/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/proc/find_original()
|
||||
var/list/possible_targets = list()
|
||||
|
||||
for(var/mob/living/carbon/human/player in GLOB.player_list)
|
||||
if(!player.client || !player.mind || player.stat)
|
||||
continue
|
||||
if(!(player.mind.assigned_role.job_flags & JOB_CREW_MEMBER))
|
||||
continue
|
||||
possible_targets += player
|
||||
|
||||
if(possible_targets.len)
|
||||
return pick(possible_targets)
|
||||
return FALSE
|
||||
|
||||
#undef MALF_ION_PROB
|
||||
#undef REPLACE_LAW_WITH_ION_PROB
|
||||
@@ -0,0 +1,700 @@
|
||||
GLOBAL_VAR_INIT(revolutionary_win, FALSE)
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// SYNDICATE TRAITORS //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/traitor
|
||||
name = "Traitors"
|
||||
antag_flag = ROLE_TRAITOR
|
||||
antag_datum = /datum/antagonist/traitor
|
||||
minimum_required_age = 0
|
||||
protected_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
)
|
||||
required_candidates = 1
|
||||
weight = 5
|
||||
cost = 8 // Avoid raising traitor threat above this, as it is the default low cost ruleset.
|
||||
scaling_cost = 9
|
||||
requirements = list(8,8,8,8,8,8,8,8,8,8)
|
||||
antag_cap = list("denominator" = 38)
|
||||
var/autotraitor_cooldown = (15 MINUTES)
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/traitor/pre_execute(population)
|
||||
. = ..()
|
||||
var/num_traitors = get_antag_cap(population) * (scaled_times + 1)
|
||||
for (var/i = 1 to num_traitors)
|
||||
if(candidates.len <= 0)
|
||||
break
|
||||
var/mob/M = pick_n_take(candidates)
|
||||
assigned += M.mind
|
||||
M.mind.special_role = ROLE_TRAITOR
|
||||
M.mind.restricted_roles = restricted_roles
|
||||
GLOB.pre_setup_antags += M.mind
|
||||
return TRUE
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// MALFUNCTIONING AI //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/malf_ai
|
||||
name = "Malfunctioning AI"
|
||||
antag_flag = ROLE_MALF
|
||||
antag_datum = /datum/antagonist/malf_ai
|
||||
minimum_required_age = 14
|
||||
exclusive_roles = list(JOB_AI)
|
||||
required_candidates = 1
|
||||
weight = 3
|
||||
cost = 18
|
||||
requirements = list(101,101,101,80,60,50,30,20,10,10)
|
||||
antag_cap = 1
|
||||
flags = HIGH_IMPACT_RULESET
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/malf_ai/ready(forced)
|
||||
var/datum/job/ai_job = SSjob.GetJobType(/datum/job/ai)
|
||||
|
||||
// If we're not forced, we're going to make sure we can actually have an AI in this shift,
|
||||
if(!forced && min(ai_job.total_positions - ai_job.current_positions, ai_job.spawn_positions) <= 0)
|
||||
log_dynamic("FAIL: [src] could not run, because there is nobody who wants to be an AI")
|
||||
return FALSE
|
||||
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/malf_ai/pre_execute(population)
|
||||
. = ..()
|
||||
|
||||
var/datum/job/ai_job = SSjob.GetJobType(/datum/job/ai)
|
||||
// Maybe a bit too pedantic, but there should never be more malf AIs than there are available positions, spawn positions or antag cap allocations.
|
||||
var/num_malf = min(get_antag_cap(population), min(ai_job.total_positions - ai_job.current_positions, ai_job.spawn_positions))
|
||||
for (var/i in 1 to num_malf)
|
||||
if(candidates.len <= 0)
|
||||
break
|
||||
var/mob/new_malf = pick_n_take(candidates)
|
||||
assigned += new_malf.mind
|
||||
new_malf.mind.special_role = ROLE_MALF
|
||||
GLOB.pre_setup_antags += new_malf.mind
|
||||
// We need an AI for the malf roundstart ruleset to execute. This means that players who get selected as malf AI get priority, because antag selection comes before role selection.
|
||||
LAZYADDASSOC(SSjob.dynamic_forced_occupations, new_malf, "AI")
|
||||
return TRUE
|
||||
|
||||
//////////////////////////////////////////
|
||||
// //
|
||||
// BLOOD BROTHERS //
|
||||
// //
|
||||
//////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/traitorbro
|
||||
name = "Blood Brothers"
|
||||
antag_flag = ROLE_BROTHER
|
||||
antag_datum = /datum/antagonist/brother
|
||||
protected_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
)
|
||||
weight = 5
|
||||
cost = 8
|
||||
scaling_cost = 15
|
||||
requirements = list(40,30,30,20,20,15,15,15,10,10)
|
||||
antag_cap = 1
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/traitorbro/pre_execute(population)
|
||||
. = ..()
|
||||
|
||||
for (var/_ in 1 to get_antag_cap(population) * (scaled_times + 1))
|
||||
var/mob/candidate = pick_n_take(candidates)
|
||||
if (isnull(candidate))
|
||||
break
|
||||
|
||||
assigned += candidate.mind
|
||||
candidate.mind.restricted_roles = restricted_roles
|
||||
GLOB.pre_setup_antags += candidate.mind
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/traitorbro/execute()
|
||||
for (var/datum/mind/mind in assigned)
|
||||
var/datum/team/brother_team/team = new
|
||||
team.add_member(mind)
|
||||
team.forge_brother_objectives()
|
||||
mind.add_antag_datum(/datum/antagonist/brother, team)
|
||||
GLOB.pre_setup_antags -= mind
|
||||
|
||||
return TRUE
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// CHANGELINGS //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/changeling
|
||||
name = "Changelings"
|
||||
antag_flag = ROLE_CHANGELING
|
||||
antag_datum = /datum/antagonist/changeling
|
||||
protected_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
)
|
||||
required_candidates = 1
|
||||
weight = 3
|
||||
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(population)
|
||||
. = ..()
|
||||
var/num_changelings = get_antag_cap(population) * (scaled_times + 1)
|
||||
for (var/i = 1 to num_changelings)
|
||||
if(candidates.len <= 0)
|
||||
break
|
||||
var/mob/M = pick_n_take(candidates)
|
||||
assigned += M.mind
|
||||
M.mind.restricted_roles = restricted_roles
|
||||
M.mind.special_role = ROLE_CHANGELING
|
||||
GLOB.pre_setup_antags += M.mind
|
||||
return TRUE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/changeling/execute()
|
||||
for(var/datum/mind/changeling in assigned)
|
||||
var/datum/antagonist/changeling/new_antag = new antag_datum()
|
||||
changeling.add_antag_datum(new_antag)
|
||||
GLOB.pre_setup_antags -= changeling
|
||||
return TRUE
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// HERETICS //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/heretics
|
||||
name = "Heretics"
|
||||
antag_flag = ROLE_HERETIC
|
||||
antag_datum = /datum/antagonist/heretic
|
||||
protected_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CYBORG,
|
||||
)
|
||||
required_candidates = 1
|
||||
weight = 3
|
||||
cost = 10
|
||||
scaling_cost = 9
|
||||
requirements = list(101,101,60,30,30,25,20,15,10,10)
|
||||
antag_cap = list("denominator" = 24)
|
||||
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_HERETIC_SACRIFICE)
|
||||
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/heretics/pre_execute(population)
|
||||
. = ..()
|
||||
var/num_ecult = get_antag_cap(population) * (scaled_times + 1)
|
||||
|
||||
for (var/i = 1 to num_ecult)
|
||||
if(candidates.len <= 0)
|
||||
break
|
||||
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
|
||||
GLOB.pre_setup_antags += picked_candidate.mind
|
||||
return TRUE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/heretics/execute()
|
||||
|
||||
for(var/c in assigned)
|
||||
var/datum/mind/cultie = c
|
||||
var/datum/antagonist/heretic/new_antag = new antag_datum()
|
||||
cultie.add_antag_datum(new_antag)
|
||||
GLOB.pre_setup_antags -= cultie
|
||||
|
||||
return TRUE
|
||||
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// WIZARDS //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
// 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"
|
||||
antag_flag = ROLE_WIZARD
|
||||
antag_datum = /datum/antagonist/wizard
|
||||
flags = HIGH_IMPACT_RULESET
|
||||
minimum_required_age = 14
|
||||
restricted_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
) // Just to be sure that a wizard getting picked won't ever imply a Captain or HoS not getting drafted
|
||||
required_candidates = 1
|
||||
weight = 2
|
||||
cost = 20
|
||||
requirements = list(90,90,90,80,60,40,30,20,10,10)
|
||||
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_WIZARDDEN)
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/wizard/ready(forced = FALSE)
|
||||
if(!check_candidates())
|
||||
return FALSE
|
||||
if(!length(GLOB.wizardstart))
|
||||
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.")
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/wizard/round_result()
|
||||
for(var/datum/antagonist/wizard/wiz in GLOB.antagonists)
|
||||
var/mob/living/real_wiz = wiz.owner?.current
|
||||
if(isnull(real_wiz))
|
||||
continue
|
||||
|
||||
var/turf/wiz_location = get_turf(real_wiz)
|
||||
// If this wiz is alive AND not in an away level, then we know not all wizards are dead and can leave entirely
|
||||
if(considered_alive(wiz.owner) && wiz_location && !is_away_level(wiz_location.z))
|
||||
return
|
||||
|
||||
SSticker.news_report = WIZARD_KILLED
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/wizard/pre_execute()
|
||||
. = ..()
|
||||
if(GLOB.wizardstart.len == 0)
|
||||
return FALSE
|
||||
var/mob/M = pick_n_take(candidates)
|
||||
if (M)
|
||||
assigned += M.mind
|
||||
M.mind.set_assigned_role(SSjob.GetJobType(/datum/job/space_wizard))
|
||||
M.mind.special_role = ROLE_WIZARD
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/wizard/execute()
|
||||
for(var/datum/mind/M in assigned)
|
||||
M.current.forceMove(pick(GLOB.wizardstart))
|
||||
M.add_antag_datum(new antag_datum())
|
||||
return TRUE
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// BLOOD CULT //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/bloodcult
|
||||
name = "Blood Cult"
|
||||
antag_flag = ROLE_CULTIST
|
||||
antag_datum = /datum/antagonist/cult
|
||||
minimum_required_age = 14
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CAPTAIN,
|
||||
JOB_CHAPLAIN,
|
||||
JOB_CYBORG,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_PERSONNEL,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
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/cult/main_cult
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/bloodcult/ready(population, forced = FALSE)
|
||||
required_candidates = get_antag_cap(population)
|
||||
return ..()
|
||||
|
||||
/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
|
||||
var/mob/M = pick_n_take(candidates)
|
||||
assigned += M.mind
|
||||
M.mind.special_role = ROLE_CULTIST
|
||||
M.mind.restricted_roles = restricted_roles
|
||||
GLOB.pre_setup_antags += M.mind
|
||||
return TRUE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/bloodcult/execute()
|
||||
main_cult = new
|
||||
for(var/datum/mind/M in assigned)
|
||||
var/datum/antagonist/cult/new_cultist = new antag_datum()
|
||||
new_cultist.cult_team = main_cult
|
||||
new_cultist.give_equipment = TRUE
|
||||
M.add_antag_datum(new_cultist)
|
||||
GLOB.pre_setup_antags -= M
|
||||
main_cult.setup_objectives()
|
||||
return TRUE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/bloodcult/round_result()
|
||||
if(main_cult.check_cult_victory())
|
||||
SSticker.mode_result = "win - cult win"
|
||||
SSticker.news_report = CULT_SUMMON
|
||||
return
|
||||
|
||||
SSticker.mode_result = "loss - staff stopped the cult"
|
||||
|
||||
if(main_cult.size_at_maximum == 0)
|
||||
CRASH("Cult team existed with a size_at_maximum of 0 at round end!")
|
||||
|
||||
// If more than a certain ratio of our cultists have escaped, give the "cult escape" resport.
|
||||
// Otherwise, give the "cult failure" report.
|
||||
var/ratio_to_be_considered_escaped = 0.5
|
||||
var/escaped_cultists = 0
|
||||
for(var/datum/mind/escapee as anything in main_cult.members)
|
||||
if(considered_escaped(escapee))
|
||||
escaped_cultists++
|
||||
|
||||
SSticker.news_report = (escaped_cultists / main_cult.size_at_maximum) >= ratio_to_be_considered_escaped ? CULT_ESCAPE : CULT_FAILURE
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// NUCLEAR OPERATIVES //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/nuclear
|
||||
name = "Nuclear Emergency"
|
||||
antag_flag = ROLE_OPERATIVE
|
||||
antag_datum = /datum/antagonist/nukeop
|
||||
var/datum/antagonist/antag_leader_datum = /datum/antagonist/nukeop/leader
|
||||
minimum_required_age = 14
|
||||
restricted_roles = list(
|
||||
JOB_CAPTAIN,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
) // 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 = 20
|
||||
requirements = list(90,90,90,80,60,40,30,20,10,10)
|
||||
flags = HIGH_IMPACT_RULESET
|
||||
antag_cap = list("denominator" = 18, "offset" = 1)
|
||||
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE)
|
||||
var/required_role = ROLE_NUCLEAR_OPERATIVE
|
||||
var/datum/team/nuclear/nuke_team
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/nuclear/ready(population, forced = FALSE)
|
||||
required_candidates = get_antag_cap(population)
|
||||
return ..()
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/nuclear/pre_execute(population)
|
||||
. = ..()
|
||||
// If ready() did its job, candidates should have 5 or more members in it
|
||||
var/operatives = get_antag_cap(population)
|
||||
for(var/operatives_number = 1 to operatives)
|
||||
if(candidates.len <= 0)
|
||||
break
|
||||
var/mob/M = pick_n_take(candidates)
|
||||
assigned += M.mind
|
||||
M.mind.set_assigned_role(SSjob.GetJobType(/datum/job/nuclear_operative))
|
||||
M.mind.special_role = ROLE_NUCLEAR_OPERATIVE
|
||||
return TRUE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/nuclear/execute()
|
||||
var/datum/mind/most_experienced = get_most_experienced(assigned, required_role)
|
||||
if(!most_experienced)
|
||||
most_experienced = assigned[1]
|
||||
var/datum/antagonist/nukeop/leader/leader = most_experienced.add_antag_datum(antag_leader_datum)
|
||||
nuke_team = leader.nuke_team
|
||||
for(var/datum/mind/assigned_player in assigned)
|
||||
if(assigned_player == most_experienced)
|
||||
continue
|
||||
var/datum/antagonist/nukeop/new_op = new antag_datum()
|
||||
assigned_player.add_antag_datum(new_op)
|
||||
return TRUE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/nuclear/round_result()
|
||||
var/result = nuke_team.get_result()
|
||||
switch(result)
|
||||
if(NUKE_RESULT_FLUKE)
|
||||
SSticker.mode_result = "loss - syndicate nuked - disk secured"
|
||||
SSticker.news_report = NUKE_SYNDICATE_BASE
|
||||
if(NUKE_RESULT_NUKE_WIN)
|
||||
SSticker.mode_result = "win - syndicate nuke"
|
||||
SSticker.news_report = STATION_DESTROYED_NUKE
|
||||
if(NUKE_RESULT_NOSURVIVORS)
|
||||
SSticker.mode_result = "halfwin - syndicate nuke - did not evacuate in time"
|
||||
SSticker.news_report = STATION_DESTROYED_NUKE
|
||||
if(NUKE_RESULT_WRONG_STATION)
|
||||
SSticker.mode_result = "halfwin - blew wrong station"
|
||||
SSticker.news_report = NUKE_MISS
|
||||
if(NUKE_RESULT_WRONG_STATION_DEAD)
|
||||
SSticker.mode_result = "halfwin - blew wrong station - did not evacuate in time"
|
||||
SSticker.news_report = NUKE_MISS
|
||||
if(NUKE_RESULT_CREW_WIN_SYNDIES_DEAD)
|
||||
SSticker.mode_result = "loss - evacuation - disk secured - syndi team dead"
|
||||
SSticker.news_report = OPERATIVES_KILLED
|
||||
if(NUKE_RESULT_CREW_WIN)
|
||||
SSticker.mode_result = "loss - evacuation - disk secured"
|
||||
SSticker.news_report = OPERATIVES_KILLED
|
||||
if(NUKE_RESULT_DISK_LOST)
|
||||
SSticker.mode_result = "halfwin - evacuation - disk not secured"
|
||||
SSticker.news_report = OPERATIVE_SKIRMISH
|
||||
if(NUKE_RESULT_DISK_STOLEN)
|
||||
SSticker.mode_result = "halfwin - detonation averted"
|
||||
SSticker.news_report = OPERATIVE_SKIRMISH
|
||||
else
|
||||
SSticker.mode_result = "halfwin - interrupted"
|
||||
SSticker.news_report = OPERATIVE_SKIRMISH
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// REVS //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/revs
|
||||
name = "Revolution"
|
||||
persistent = TRUE
|
||||
antag_flag = ROLE_REV_HEAD
|
||||
antag_flag_override = ROLE_REV_HEAD
|
||||
antag_datum = /datum/antagonist/rev/head
|
||||
minimum_required_age = 14
|
||||
restricted_roles = list(
|
||||
JOB_AI,
|
||||
JOB_CAPTAIN,
|
||||
JOB_CHIEF_ENGINEER,
|
||||
JOB_CHIEF_MEDICAL_OFFICER,
|
||||
JOB_CYBORG,
|
||||
JOB_DETECTIVE,
|
||||
JOB_HEAD_OF_PERSONNEL,
|
||||
JOB_HEAD_OF_SECURITY,
|
||||
JOB_PRISONER,
|
||||
JOB_QUARTERMASTER,
|
||||
JOB_RESEARCH_DIRECTOR,
|
||||
JOB_SECURITY_OFFICER,
|
||||
JOB_WARDEN,
|
||||
)
|
||||
required_candidates = 3
|
||||
weight = 3
|
||||
delay = 7 MINUTES
|
||||
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
|
||||
var/datum/team/revolution/revolution
|
||||
var/finished = FALSE
|
||||
|
||||
/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
|
||||
var/mob/M = pick_n_take(candidates)
|
||||
assigned += M.mind
|
||||
M.mind.restricted_roles = restricted_roles
|
||||
M.mind.special_role = antag_flag
|
||||
GLOB.pre_setup_antags += M.mind
|
||||
return TRUE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/revs/execute()
|
||||
revolution = new()
|
||||
for(var/datum/mind/M in assigned)
|
||||
GLOB.pre_setup_antags -= M
|
||||
if(check_eligible(M))
|
||||
var/datum/antagonist/rev/head/new_head = new antag_datum()
|
||||
new_head.give_flash = TRUE
|
||||
new_head.give_hud = TRUE
|
||||
new_head.remove_clumsy = TRUE
|
||||
M.add_antag_datum(new_head,revolution)
|
||||
else
|
||||
assigned -= M
|
||||
log_dynamic("[ruletype] [name] discarded [M.name] from head revolutionary due to ineligibility.")
|
||||
if(revolution.members.len)
|
||||
revolution.update_objectives()
|
||||
revolution.update_rev_heads()
|
||||
SSshuttle.registerHostileEnvironment(revolution)
|
||||
return TRUE
|
||||
log_dynamic("[ruletype] [name] failed to get any eligible headrevs. Refunding [cost] threat.")
|
||||
return FALSE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/revs/clean_up()
|
||||
qdel(revolution)
|
||||
..()
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/revs/rule_process()
|
||||
var/winner = revolution.process_victory()
|
||||
if (isnull(winner))
|
||||
return
|
||||
|
||||
finished = winner
|
||||
|
||||
if(winner == REVOLUTION_VICTORY)
|
||||
GLOB.revolutionary_win = TRUE
|
||||
|
||||
return RULESET_STOP_PROCESSING
|
||||
|
||||
/// Checks for revhead loss conditions and other antag datums.
|
||||
/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/round_result()
|
||||
revolution.round_result(finished)
|
||||
|
||||
// Admin only rulesets. The threat requirement is 101 so it is not possible to roll them.
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// EXTENDED //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/extended
|
||||
name = "Extended"
|
||||
antag_flag = null
|
||||
antag_datum = null
|
||||
restricted_roles = list()
|
||||
required_candidates = 0
|
||||
weight = 3
|
||||
cost = 0
|
||||
requirements = list(101,101,101,101,101,101,101,101,101,101)
|
||||
flags = LONE_RULESET
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/extended/pre_execute()
|
||||
. = ..()
|
||||
message_admins("Starting a round of extended.")
|
||||
log_game("Starting a round of extended.")
|
||||
SSdynamic.spend_roundstart_budget(SSdynamic.round_start_budget)
|
||||
SSdynamic.spend_midround_budget(SSdynamic.mid_round_budget)
|
||||
SSdynamic.threat_log += "[worldtime2text()]: Extended ruleset set threat to 0."
|
||||
return TRUE
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// CLOWN OPS //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/nuclear/clown_ops
|
||||
name = "Clown Operatives"
|
||||
antag_datum = /datum/antagonist/nukeop/clownop
|
||||
antag_flag = ROLE_CLOWN_OPERATIVE
|
||||
antag_flag_override = ROLE_OPERATIVE
|
||||
antag_leader_datum = /datum/antagonist/nukeop/leader/clownop
|
||||
requirements = list(101,101,101,101,101,101,101,101,101,101)
|
||||
required_role = ROLE_CLOWN_OPERATIVE
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/nuclear/clown_ops/pre_execute()
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
|
||||
var/list/nukes = SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/syndicate)
|
||||
for(var/obj/machinery/nuclearbomb/syndicate/nuke as anything in nukes)
|
||||
new /obj/machinery/nuclearbomb/syndicate/bananium(nuke.loc)
|
||||
qdel(nuke)
|
||||
|
||||
for(var/datum/mind/clowns in assigned)
|
||||
clowns.set_assigned_role(SSjob.GetJobType(/datum/job/clown_operative))
|
||||
clowns.special_role = ROLE_CLOWN_OPERATIVE
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// //
|
||||
// METEOR //
|
||||
// //
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/meteor
|
||||
name = "Meteor"
|
||||
persistent = TRUE
|
||||
required_candidates = 0
|
||||
weight = 3
|
||||
cost = 0
|
||||
requirements = list(101,101,101,101,101,101,101,101,101,101)
|
||||
flags = LONE_RULESET
|
||||
var/meteordelay = 2000
|
||||
var/nometeors = FALSE
|
||||
var/rampupdelta = 5
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/meteor/rule_process()
|
||||
if(nometeors || meteordelay > world.time - SSticker.round_start_time)
|
||||
return
|
||||
|
||||
var/list/wavetype = GLOB.meteors_normal
|
||||
var/meteorminutes = (world.time - SSticker.round_start_time - meteordelay) / 10 / 60
|
||||
|
||||
if (prob(meteorminutes))
|
||||
wavetype = GLOB.meteors_threatening
|
||||
|
||||
if (prob(meteorminutes/2))
|
||||
wavetype = GLOB.meteors_catastrophic
|
||||
|
||||
var/ramp_up_final = clamp(round(meteorminutes/rampupdelta), 1, 10)
|
||||
|
||||
spawn_meteors(ramp_up_final, wavetype)
|
||||
|
||||
/// Ruleset for Nations
|
||||
/datum/dynamic_ruleset/roundstart/nations
|
||||
name = "Nations"
|
||||
required_candidates = 0
|
||||
weight = 0 //admin only (and for good reason)
|
||||
cost = 0
|
||||
flags = LONE_RULESET | ONLY_RULESET
|
||||
|
||||
/datum/dynamic_ruleset/roundstart/nations/execute()
|
||||
. = ..()
|
||||
//notably assistant is not in this list to prevent the round turning into BARBARISM instantly, and silicon is in this list for UN
|
||||
var/list/department_types = list(
|
||||
/datum/job_department/silicon, //united nations
|
||||
/datum/job_department/cargo,
|
||||
/datum/job_department/engineering,
|
||||
/datum/job_department/medical,
|
||||
/datum/job_department/science,
|
||||
/datum/job_department/security,
|
||||
/datum/job_department/service,
|
||||
)
|
||||
|
||||
for(var/department_type in department_types)
|
||||
create_separatist_nation(department_type, announcement = FALSE, dangerous = FALSE, message_admins = FALSE)
|
||||
|
||||
GLOB.round_default_lawset = /datum/ai_laws/united_nations
|
||||
@@ -0,0 +1,74 @@
|
||||
/// An easy interface to make...*waves hands* bad things happen.
|
||||
/// This is used for impactful events like traitors hacking and creating more threat, or a revolutions victory.
|
||||
/// It tries to spawn a heavy midround if possible, otherwise it will trigger a "bad" random event after a short period.
|
||||
/// Calling this function will not use up any threat.
|
||||
/datum/controller/subsystem/dynamic/proc/unfavorable_situation()
|
||||
SHOULD_NOT_SLEEP(TRUE)
|
||||
|
||||
INVOKE_ASYNC(src, PROC_REF(_unfavorable_situation))
|
||||
|
||||
/datum/controller/subsystem/dynamic/proc/_unfavorable_situation()
|
||||
var/static/list/unfavorable_random_events = list()
|
||||
if (!length(unfavorable_random_events))
|
||||
unfavorable_random_events = generate_unfavourable_events()
|
||||
var/list/possible_heavies = generate_unfavourable_heavy_rulesets()
|
||||
if (!length(possible_heavies))
|
||||
var/datum/round_event_control/round_event_control_type = pick(unfavorable_random_events)
|
||||
var/delay = rand(20 SECONDS, 1 MINUTES)
|
||||
|
||||
log_dynamic_and_announce("An unfavorable situation was requested, but no heavy rulesets could be drafted. Spawning [initial(round_event_control_type.name)] in [DisplayTimeText(delay)] instead.")
|
||||
force_event_after(round_event_control_type, "an unfavorable situation", delay)
|
||||
else
|
||||
var/datum/dynamic_ruleset/midround/heavy_ruleset = pick_weight(possible_heavies)
|
||||
log_dynamic_and_announce("An unfavorable situation was requested, spawning [initial(heavy_ruleset.name)]")
|
||||
picking_specific_rule(heavy_ruleset, forced = TRUE, ignore_cost = TRUE)
|
||||
|
||||
/// Return a valid heavy dynamic ruleset, or an empty list if there's no time to run any rulesets
|
||||
/datum/controller/subsystem/dynamic/proc/generate_unfavourable_heavy_rulesets()
|
||||
if (EMERGENCY_PAST_POINT_OF_NO_RETURN)
|
||||
return list()
|
||||
|
||||
var/list/possible_heavies = list()
|
||||
for (var/datum/dynamic_ruleset/midround/ruleset as anything in midround_rules)
|
||||
if (ruleset.midround_ruleset_style != MIDROUND_RULESET_STYLE_HEAVY)
|
||||
continue
|
||||
|
||||
if (ruleset.weight == 0)
|
||||
continue
|
||||
|
||||
if (ruleset.cost > max_threat_level)
|
||||
continue
|
||||
|
||||
if (!ruleset.acceptable(GLOB.alive_player_list.len, threat_level))
|
||||
continue
|
||||
|
||||
if (ruleset.minimum_round_time > world.time - SSticker.round_start_time)
|
||||
continue
|
||||
|
||||
if(istype(ruleset, /datum/dynamic_ruleset/midround/from_ghosts) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT))
|
||||
continue
|
||||
|
||||
ruleset.trim_candidates()
|
||||
|
||||
ruleset.load_templates()
|
||||
if (!ruleset.ready())
|
||||
continue
|
||||
|
||||
possible_heavies[ruleset] = ruleset.get_weight()
|
||||
return possible_heavies
|
||||
|
||||
/// Filter the below list by which events can actually run on this map
|
||||
/datum/controller/subsystem/dynamic/proc/generate_unfavourable_events()
|
||||
var/static/list/unfavorable_random_events = list(
|
||||
/datum/round_event_control/earthquake,
|
||||
/datum/round_event_control/immovable_rod,
|
||||
/datum/round_event_control/meteor_wave,
|
||||
/datum/round_event_control/portal_storm_syndicate,
|
||||
)
|
||||
var/list/picked_events = list()
|
||||
for(var/type in unfavorable_random_events)
|
||||
var/datum/round_event_control/event = new type()
|
||||
if(!event.valid_for_map())
|
||||
continue
|
||||
picked_events += type
|
||||
return picked_events
|
||||
@@ -0,0 +1,194 @@
|
||||
# Dynamic Mode
|
||||
|
||||
## Roundstart
|
||||
|
||||
Dynamic rolls threat based on a special sauce formula:
|
||||
|
||||
> [dynamic_curve_width][/datum/controller/global_vars/var/dynamic_curve_width] \* tan((3.1416 \* (rand() - 0.5) \* 57.2957795)) + [dynamic_curve_centre][/datum/controller/global_vars/var/dynamic_curve_centre]
|
||||
|
||||
This threat is split into two separate budgets--`round_start_budget` and `mid_round_budget`. For example, a round with 50 threat might be split into a 30 roundstart budget, and a 20 midround budget. The roundstart budget is used to apply antagonists applied on readied players when the roundstarts (`/datum/dynamic_ruleset/roundstart`). The midround budget is used for two types of rulesets:
|
||||
- `/datum/dynamic_ruleset/midround` - Rulesets that apply to either existing alive players, or to ghosts. Think Blob or Space Ninja, which poll ghosts asking if they want to play as these roles.
|
||||
- `/datum/dynamic_ruleset/latejoin` - Rulesets that apply to the next player that joins. Think Syndicate Infiltrator, which converts a player just joining an existing round into traitor.
|
||||
|
||||
This split is done with a similar method, known as the ["lorentz distribution"](https://en.wikipedia.org/wiki/Cauchy_distribution), exists to create a bell curve that ensures that while most rounds will have a threat level around ~50, chaotic and tame rounds still exist for variety.
|
||||
|
||||
The process of creating these numbers occurs in `/datum/controller/subsystem/dynamic/proc/generate_threat` (for creating the threat level) and `/datum/controller/subsystem/dynamic/proc/generate_budgets` (for splitting the threat level into budgets).
|
||||
|
||||
## Deciding roundstart threats
|
||||
In `/datum/controller/subsystem/dynamic/proc/roundstart()` (called when no admin chooses the rulesets explicitly), Dynamic uses the available roundstart budget to pick threats. This is done through the following system:
|
||||
|
||||
- All roundstart rulesets (remember, `/datum/dynamic_ruleset/roundstart`) are put into an associative list with their weight as the values (`drafted_rules`).
|
||||
- Until there is either no roundstart budget left, or until there is no ruleset we can choose from with the available threat, a `pickweight` is done based on the drafted_rules. If the same threat is picked twice, it will "scale up". The meaning of this depends on the ruleset itself, using the `scaled_times` variable; traitors for instance will create more the higher they scale.
|
||||
- If a ruleset is chosen with the `HIGH_IMPACT_RULESET` in its `flags`, then all other `HIGH_IMPACT_RULESET`s will be removed from `drafted_rules`. This is so that only one can ever be chosen.
|
||||
- If a ruleset has `LONE_RULESET` in its `flags`, then it will be removed from `drafted_rules`. This is to ensure it will only ever be picked once. An example of this in use is Wizard, to avoid creating multiple wizards.
|
||||
- After all roundstart threats are chosen, `/datum/dynamic_ruleset/proc/picking_roundstart_rule` is called for each, passing in the ruleset and the number of times it is scaled.
|
||||
- In this stage, `pre_execute` is called, which is the function that will determine what players get what antagonists. If this function returns FALSE for whatever reason (in the case of an error), then its threat is refunded.
|
||||
|
||||
After this process is done, any leftover roundstart threat will be given to the existing midround budget (done in `/datum/controller/subsystem/dynamic/pre_setup()`).
|
||||
|
||||
## Deciding midround threats
|
||||
|
||||
### Frequency
|
||||
|
||||
The frequency of midround threats is based on the midround threat of the round. The number of midround threats that will roll is `threat_level` / `threat_per_midround_roll` (configurable), rounded up. For example, if `threat_per_midround_roll` is set to 5, then for every 5 threat, one midround roll will be added. If you have 6 threat, with this configuration, you will get 2 midround rolls.
|
||||
|
||||
These midround roll points are then equidistantly spaced across the round, starting from `midround_lower_bound` (configurable) to `midround_upper_bound` (configurable), with a +/- of `midround_roll_distance` (configurable).
|
||||
|
||||
For example, if:
|
||||
1. `midround_lower_bound` is `10 MINUTES`
|
||||
2. `midround_upper_bound` is `100 MINUTES`
|
||||
3. `midround_roll_distance` is `3 MINUTES`
|
||||
4. You have 5 midround rolls for the round
|
||||
|
||||
...then those 5 midround rolls will be placed equidistantly (meaning equally apart) across the first 10-100 minutes of the round. Every individual roll will then be adjusted to either be 3 minutes earlier, or 3 minutes later.
|
||||
|
||||
### Threat variety
|
||||
|
||||
Threats are split between **heavy** rulesets and **light** rulesets. A heavy ruleset includes major threats like space dragons or blobs, while light rulesets are ones that don't often cause shuttle calls when rolled, such as revenants or traitors (sleeper agents).
|
||||
|
||||
When a midround roll occurs, the decision to choose between light or heavy depends on the current round time. If it is less than `midround_light_upper_bound` (configurable), then it is guaranteed to be a light ruleset. If it is more than `midround_heavy_lower_bound`, then it is guaranteed to be a heavy ruleset. If it is any point in between, it will interpolate the value between those. This means that the longer the round goes on, the more likely you are to get a heavy ruleset.
|
||||
|
||||
If no heavy ruleset can run, such as not having enough threat, then a light ruleset is guaranteed to run.
|
||||
|
||||
## 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
|
||||
|
||||
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...)
|
||||
-> picking_latejoin_ruleset(drafted_rules) -> spend threat -> ruleset.execute()
|
||||
|
||||
## Midround
|
||||
|
||||
process() -> (For each midround rule...
|
||||
-> acceptable(living players, threat_level) -> trim_candidates() -> ready(forced=FALSE)
|
||||
(After collecting all draftble rules...)
|
||||
-> picking_midround_ruleset(drafted_rules) -> spend threat -> ruleset.execute()
|
||||
|
||||
## 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
|
||||
|
||||
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_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/controller/subsystem/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/controller/subsystem/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. */
|
||||
},
|
||||
|
||||
"Station": {
|
||||
/* Special threat reductions for dangerous station traits. Traits are selected before dynamic, so traits will always */
|
||||
/* reduce threat even if there's no threat for it available. Only "cost" can be modified */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
This is not a complete list--search "configurable" in this README to learn more.
|
||||
|
||||
### 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/controller/subsystem/dynamic/proc/get_injection_chance()`). Regardless of its decision, a new timer will be started within the range of 1 to 3 minutes, repeatedly.
|
||||
- `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))
|
||||
- `max_threat_level` - Sets the maximum amount of threat that can be rolled. Defaults to 100. You should only use this to *lower* the maximum threat, as raising it higher will not do anything.
|
||||
|
||||
## 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/controller/subsystem/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:
|
||||
|
||||

|
||||
|
||||
`n` is a random value between `random_event_hijack_minimum` and `random_event_hijack_maximum`. Heavy injection chance, should it need to be raised, is increased by `hijacked_random_event_injection_chance_modifier`.
|
||||
@@ -0,0 +1,139 @@
|
||||
#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.
|
||||
*
|
||||
* * max_allowed_attempts - Allows you to configure how many times the proc will attempt to pick a ruleset before giving up.
|
||||
*/
|
||||
/datum/controller/subsystem/dynamic/proc/pick_ruleset(list/drafted_rules, max_allowed_attempts = INFINITY)
|
||||
if (only_ruleset_executed)
|
||||
log_dynamic("FAIL: only_ruleset_executed")
|
||||
return null
|
||||
|
||||
if(!length(drafted_rules))
|
||||
log_dynamic("FAIL: pick ruleset supplied with an empty list of drafted rules.")
|
||||
return null
|
||||
|
||||
var/attempts = 0
|
||||
while (attempts < max_allowed_attempts)
|
||||
attempts++
|
||||
var/datum/dynamic_ruleset/rule = pick_weight(drafted_rules)
|
||||
if (!rule)
|
||||
var/list/leftover_rules = list()
|
||||
for (var/leftover_rule in drafted_rules)
|
||||
leftover_rules += "[leftover_rule]"
|
||||
|
||||
log_dynamic("FAIL: No rulesets left to pick. Leftover rules: [leftover_rules.Join(", ")]")
|
||||
return null
|
||||
|
||||
if (check_blocking(rule.blocking_rules, executed_rules))
|
||||
log_dynamic("FAIL: [rule] can't execute as another rulset is blocking it.")
|
||||
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 \
|
||||
)
|
||||
log_dynamic("FAIL: [rule] can't execute as a high impact ruleset was already executed.")
|
||||
drafted_rules -= rule
|
||||
if(drafted_rules.len <= 0)
|
||||
return null
|
||||
continue
|
||||
|
||||
return rule
|
||||
|
||||
return null
|
||||
|
||||
/// Executes a random midround ruleset from the list of drafted rules.
|
||||
/datum/controller/subsystem/dynamic/proc/pick_midround_rule(list/drafted_rules, description)
|
||||
log_dynamic("Rolling [drafted_rules.len] [description]")
|
||||
|
||||
var/datum/dynamic_ruleset/rule = pick_ruleset(drafted_rules)
|
||||
if (isnull(rule))
|
||||
return null
|
||||
|
||||
current_midround_rulesets = drafted_rules - rule
|
||||
|
||||
midround_injection_timer_id = addtimer(
|
||||
CALLBACK(src, PROC_REF(execute_midround_rule), rule), \
|
||||
ADMIN_CANCEL_MIDROUND_TIME, \
|
||||
TIMER_STOPPABLE, \
|
||||
)
|
||||
|
||||
log_dynamic("[rule] ruleset executing...")
|
||||
message_admins("DYNAMIC: Executing midround ruleset [rule] in [DisplayTimeText(ADMIN_CANCEL_MIDROUND_TIME)]. \
|
||||
<a href='?src=[REF(src)];cancelmidround=[midround_injection_timer_id]'>CANCEL</a> | \
|
||||
<a href='?src=[REF(src)];differentmidround=[midround_injection_timer_id]'>SOMETHING ELSE</a>")
|
||||
|
||||
return rule
|
||||
|
||||
/// Fired after admins do not cancel a midround injection.
|
||||
/datum/controller/subsystem/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_REF(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/controller/subsystem/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]")
|
||||
rule.pre_execute(GLOB.alive_player_list.len)
|
||||
if (rule.execute())
|
||||
log_dynamic("Injected a [rule.ruletype] 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_RULESET)
|
||||
var/mob/M = pick(rule.candidates)
|
||||
message_admins("[key_name(M)] joined the station, and was selected by the [rule.name] ruleset.")
|
||||
log_dynamic("[key_name(M)] joined the station, and was selected by the [rule.name] ruleset.")
|
||||
executed_rules += rule
|
||||
if (rule.persistent)
|
||||
current_rules += rule
|
||||
new_snapshot(rule)
|
||||
rule.forget_startup()
|
||||
return TRUE
|
||||
rule.forget_startup()
|
||||
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/controller/subsystem/dynamic/proc/admin_cancel_midround(mob/user, timer_id)
|
||||
if (midround_injection_timer_id != timer_id || !deltimer(midround_injection_timer_id))
|
||||
to_chat(user, span_notice("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/controller/subsystem/dynamic/proc/admin_different_midround(mob/user, timer_id)
|
||||
if (midround_injection_timer_id != timer_id || !deltimer(midround_injection_timer_id))
|
||||
to_chat(user, span_notice("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, "different midround rulesets")
|
||||
|
||||
#undef ADMIN_CANCEL_MIDROUND_TIME
|
||||
@@ -20,8 +20,6 @@ SUBSYSTEM_DEF(ticker)
|
||||
/// Boolean to track and check if our subsystem setup is done.
|
||||
var/setup_done = FALSE
|
||||
|
||||
var/datum/game_mode/mode = null
|
||||
|
||||
var/login_music //music played in pregame lobby
|
||||
var/round_end_sound //music/jingle played when the world reboots
|
||||
var/round_end_sound_sent = TRUE //If all clients have loaded it
|
||||
@@ -206,10 +204,9 @@ SUBSYSTEM_DEF(ticker)
|
||||
SEND_SIGNAL(src, COMSIG_TICKER_ERROR_SETTING_UP)
|
||||
|
||||
if(GAME_STATE_PLAYING)
|
||||
mode.process(wait * 0.1)
|
||||
check_queue()
|
||||
|
||||
if(!roundend_check_paused && (mode.check_finished() || force_ending))
|
||||
if(!roundend_check_paused && (check_finished() || force_ending))
|
||||
current_state = GAME_STATE_FINISHED
|
||||
toggle_ooc(TRUE) // Turn it on
|
||||
toggle_dooc(TRUE)
|
||||
@@ -217,17 +214,27 @@ SUBSYSTEM_DEF(ticker)
|
||||
check_maprotate()
|
||||
Master.SetRunLevel(RUNLEVEL_POSTGAME)
|
||||
|
||||
/// Checks if the round should be ending, called every ticker tick
|
||||
/datum/controller/subsystem/ticker/proc/check_finished()
|
||||
if(!setup_done)
|
||||
return FALSE
|
||||
if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME))
|
||||
return TRUE
|
||||
if(GLOB.station_was_nuked)
|
||||
return TRUE
|
||||
if(GLOB.revolutionary_win)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
|
||||
/datum/controller/subsystem/ticker/proc/setup()
|
||||
to_chat(world, span_boldannounce("Starting game..."))
|
||||
var/init_start = world.timeofday
|
||||
|
||||
mode = new /datum/game_mode/dynamic
|
||||
|
||||
CHECK_TICK
|
||||
//Configure mode and assign player to special mode stuff
|
||||
var/can_continue = 0
|
||||
can_continue = src.mode.pre_setup() //Choose antagonists
|
||||
//Configure mode and assign player to antagonists
|
||||
var/can_continue = FALSE
|
||||
can_continue = SSdynamic.pre_setup() //Choose antagonists
|
||||
CHECK_TICK
|
||||
can_continue = can_continue && SSjob.DivideOccupations() //Distribute jobs
|
||||
CHECK_TICK
|
||||
@@ -235,7 +242,6 @@ SUBSYSTEM_DEF(ticker)
|
||||
if(!GLOB.Debug2)
|
||||
if(!can_continue)
|
||||
log_game("Game failed pre_setup")
|
||||
QDEL_NULL(mode)
|
||||
to_chat(world, "<B>Error setting up game.</B> Reverting to pre-game lobby.")
|
||||
SSjob.ResetOccupations()
|
||||
return FALSE
|
||||
@@ -293,7 +299,7 @@ SUBSYSTEM_DEF(ticker)
|
||||
|
||||
/datum/controller/subsystem/ticker/proc/PostSetup()
|
||||
set waitfor = FALSE
|
||||
mode.post_setup()
|
||||
SSdynamic.post_setup()
|
||||
GLOB.start_state = new /datum/station_state()
|
||||
GLOB.start_state.count()
|
||||
|
||||
@@ -525,7 +531,6 @@ SUBSYSTEM_DEF(ticker)
|
||||
/datum/controller/subsystem/ticker/Recover()
|
||||
current_state = SSticker.current_state
|
||||
force_ending = SSticker.force_ending
|
||||
mode = SSticker.mode
|
||||
|
||||
login_music = SSticker.login_music
|
||||
round_end_sound = SSticker.round_end_sound
|
||||
|
||||
Reference in New Issue
Block a user