Dynamic Rework (#91290)

## About The Pull Request

Implements https://hackmd.io/@tgstation/SkeUS7lSp , rewriting Dynamic
from the ground-up

- Dynamic configuration is now vastly streamlined, making it far far far
easier to understand and edit

- Threat is gone entirely; round chaos is now determined by dynamic
tiers
   - There's 5 dynamic tiers, 0 to 4.
      - 0 is a pure greenshift.
- Tiers are just picked via weight - "16% chance of getting a high chaos
round".
- Tiers have min pop ranges. "Tier 4 (high chaos) requires 25 pop to be
selected".
- Tier determines how much of every ruleset is picked. "Tier 4 (High
Chaos) will pick 3-4 roundstart[1], 1-2 light, 1-2 heavy, and 2-3
latejoins".
- The number of rulesets picked depends on how many people are in the
server - this is also configurable[2]. As an example, a tier that
demands "1-3" rulesets will not spawn 3 rulesets if population <= 40 and
will not spawn 2 rulesets if population <= 25.
- Tiers also determine time before light, heavy, and latejoin rulesets
are picked, as well as the cooldown range between spawns. More chaotic
tiers may send midrounds sooner or wait less time between sending them.

- On the ruleset side of things, "requirements", "scaling", and
"enemies" is gone.
- You can configure a ruleset's min pop and weight flat, or per tier.
- For example a ruleset like Obsession is weighted higher for tiers 1-2
and lower for tiers 3-4.
- Rather than scaling up, roundstart rulesets can just be selected
multiple times.
- Rulesets also have `min_antag_cap` and `max_antag_cap`.
`min_antag_cap` determines how many candidates are needed for it to run,
and `max_antag_cap` determines how many candidates are selected.

- Rulesets attempt to run every 2.5 minutes. [3]

- Light rulesets will ALWAYS be picked before heavy rulesets. [4]

- Light injection chance is no longer 100%, heavy injection chance
formula has been simplified.
- Chance simply scales based on number of dead players / total number
off players, with a flag 50% chance if no antags exist. [5]

[1] This does not guarantee you will actually GET 3-4 roundstart
rulesets. If a roundstart ruleset is picked, and it ends up being unable
to execute (such as "not enough candidates", that slot is effectively a
wash.) This might be revisited.

[2] Currently, this is a hard limit - below X pop, you WILL get a
quarter or a half of the rulesets. This might be revisited to just be
weighted - you are just MORE LIKELY to get a quarter or a half.

[3] Little worried about accidentally frontloading everything so we'll
see about this

[4] This may be revisited but in most contexts it seems sensible. 

[5] This may also be revisited, I'm not 100% sure what the best / most
simple way to tackle midround chances is.

Other implementation details

- The process of making rulesets has been streamlined as well. Many
rulesets only amount to a definition and `assign_role`.

- Dynamic.json -> Dynamic.toml

- Dynamic event hijacked was ripped out entirely.
- Most midround antag random events are now dynamic rulesets. Fugitives,
Morphs, Slaughter Demons, etc.
      - The 1 weight slaughter demon event is gone. RIP in peace. 
- There is now a hidden midround event that simply adds +1 latejoin, +1
light, or +1 heavy ruleset.

- `mind.special_role` is dead. Minds have a lazylist of special roles
now but it's essentially only used for traitor panel.

- Revs refactored almost entirely. Revs can now exist without a dynamic
ruleset.

- Cult refactored a tiny bit. 

- Antag datums cleaned up.

- Pre round setup is less centralized on Dynamic.

- Admins have a whole panel for interfacing with dynamic. It's pretty
slapdash I'm sure someone could make a nicer looking one.


![image](https://github.com/user-attachments/assets/e99ca607-20b0-4d30-ab4a-f602babe7ac7)


![image](https://github.com/user-attachments/assets/470c3c20-c354-4ee6-b63b-a8f36dda4b5c)

- Maybe some other things.

## Why It's Good For The Game

See readme for more info.

Will you see a massive change in how rounds play out? My hunch says
rounds will spawn less rulesets on average, but it's ultimately to how
it's configured

## Changelog

🆑 Melbert
refactor: Dynamic rewritten entirely, report any strange rounds
config: Dynamic config reworked, it's now a TOML file
refactor: Refactored antag roles somewhat, report any oddities
refactor: Refactored Revolution entirely, report any oddities
del: Deleted most midround events that spawn antags - they use dynamic
rulesets now
add: Dynamic rulesets can now be false alarms
add: Adds a random event that gives dynamic the ability to run another
ruleset later
admin: Adds a panel for messing around with dynamic
admin: Adds a panel for chance for every dynamic ruleset to be selected
admin: You can spawn revs without using dynamic now
fix: Nuke team leaders get their fun title back
/🆑
This commit is contained in:
MrMelbert
2025-06-25 19:36:10 -05:00
committed by GitHub
parent 02a9b2c5b3
commit 4c277dc572
189 changed files with 6051 additions and 6178 deletions

View File

@@ -278,6 +278,8 @@ GLOBAL_LIST_INIT(ai_employers, list(
/// Checks if the given mob is a wizard /// Checks if the given mob is a wizard
#define IS_WIZARD(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/wizard)) #define IS_WIZARD(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/wizard))
/// Checks if the given mob is a wizard apprentice
#define IS_WIZARD_APPRENTICE(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/wizard/apprentice))
/// Checks if the given mob is a revolutionary. Will return TRUE for rev heads as well. /// Checks if the given mob is a revolutionary. Will return TRUE for rev heads as well.
#define IS_REVOLUTIONARY(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/rev)) #define IS_REVOLUTIONARY(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/rev))
@@ -397,8 +399,11 @@ GLOBAL_LIST_INIT(human_invader_antagonists, list(
#define ANTAG_GROUP_CREW "Deviant Crew" #define ANTAG_GROUP_CREW "Deviant Crew"
// This flag disables certain checks that presume antagonist datums mean 'baddie'. /// Used to denote an antag datum that either isn't necessarily "evil" (like Valentines)
#define FLAG_FAKE_ANTAG (1 << 0) /// or isn't necessarily a "real" antag (like Ashwalkers)
#define ANTAG_FAKE (1 << 0)
/// Antag is not added to the global list of antags
#define ANTAG_SKIP_GLOBAL_LIST (1 << 1)
#define HUNTER_PACK_COPS "Spacepol Fugitive Hunters" #define HUNTER_PACK_COPS "Spacepol Fugitive Hunters"
#define HUNTER_PACK_RUSSIAN "Russian Fugitive Hunters" #define HUNTER_PACK_RUSSIAN "Russian Fugitive Hunters"

View File

@@ -77,7 +77,7 @@
/// From /mob/proc/ghostize() Called when a mob successfully ghosts /// From /mob/proc/ghostize() Called when a mob successfully ghosts
#define COMSIG_MOB_GHOSTIZED "mob_ghostized" #define COMSIG_MOB_GHOSTIZED "mob_ghostized"
/// can_roll_midround(datum/antagonist/antag_type) from certain midround rulesets, (mob/living/source, datum/mind/mind, datum/antagonist/antagonist) /// can_roll_midround(datum/antagonist/antag_type) from certain midround rulesets, (mob/living/source, datum/mind/mind, datum/antagonist/antagonist)
#define COMSIG_MOB_MIND_BEFORE_MIDROUND_ROLL "mob_mind_transferred_out_of" #define COMSIG_MOB_MIND_BEFORE_MIDROUND_ROLL "mob_mind_before_midround_roll"
#define CANCEL_ROLL (1<<1) #define CANCEL_ROLL (1<<1)
///signal sent when a mob has their holy role set. Sent to the mob having their role changed. ///signal sent when a mob has their holy role set. Sent to the mob having their role changed.

View File

@@ -26,3 +26,6 @@
///Sent after awards are saved in the database (/datum/controller/subsystem/achievements/save_achievements_to_db) ///Sent after awards are saved in the database (/datum/controller/subsystem/achievements/save_achievements_to_db)
#define COMSIG_ACHIEVEMENTS_SAVED_TO_DB "achievements_saved_to_db" #define COMSIG_ACHIEVEMENTS_SAVED_TO_DB "achievements_saved_to_db"
/// Send after config is loaded but before picking roundstart rulesets
#define COMSIG_DYNAMIC_PRE_ROUNDSTART "dynamic_pre_roundstart"

View File

@@ -1,46 +0,0 @@
/// This is the only ruleset that should be picked this round, used by admins and should not be on rulesets in code.
#define ONLY_RULESET (1 << 0)
/// Only one ruleset with this flag will be picked.
#define HIGH_IMPACT_RULESET (1 << 1)
/// This ruleset can only be picked once. Anything that does not have a scaling_cost MUST have this.
#define LONE_RULESET (1 << 2)
/// This is a "heavy" midround ruleset, and should be run later into the round
#define MIDROUND_RULESET_STYLE_HEAVY "Heavy"
/// This is a "light" midround ruleset, and should be run early into the round
#define MIDROUND_RULESET_STYLE_LIGHT "Light"
/// No round event was hijacked this cycle
#define HIJACKED_NOTHING "HIJACKED_NOTHING"
/// This cycle, a round event was hijacked when the last midround event was too recent.
#define HIJACKED_TOO_RECENT "HIJACKED_TOO_RECENT"
/// Kill this ruleset from continuing to process
#define RULESET_STOP_PROCESSING 1
/// Requirements when something needs a lot of threat to run, but still possible at low-pop
#define REQUIREMENTS_VERY_HIGH_THREAT_NEEDED list(90,90,90,80,60,50,40,40,40,40)
/// Max number of teams we can have for the abductor ruleset
#define ABDUCTOR_MAX_TEAMS 4
// Ruletype defines
#define ROUNDSTART_RULESET "Roundstart"
#define LATEJOIN_RULESET "Latejoin"
#define MIDROUND_RULESET "Midround"
#define RULESET_NOT_FORCED "not forced"
/// Ruleset should run regardless of population and threat available
#define RULESET_FORCE_ENABLED "force enabled"
/// Ruleset should not run regardless of population and threat available
#define RULESET_FORCE_DISABLED "force disabled"
// Flavor ruletypes, used by station traits
/// Rulesets selected by dynamic at default
#define RULESET_CATEGORY_DEFAULT (1 << 0)
/// Rulesets not including crew antagonists, non-witting referring to antags like obsessed which aren't really enemies of the station
#define RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS (1 << 1)

View File

@@ -241,6 +241,10 @@ DEFINE_BITFIELD(departments_bitflags, list(
#define JOB_LATEJOIN_ONLY (1<<11) #define JOB_LATEJOIN_ONLY (1<<11)
/// This job is a head of staff. /// This job is a head of staff.
#define JOB_HEAD_OF_STAFF (1<<12) #define JOB_HEAD_OF_STAFF (1<<12)
/// This job will NEVER be selected as an antag role
#define JOB_ANTAG_BLACKLISTED (1<<13)
/// This job will never be selected as an antag role IF config `protect_roles_from_antagonist` is set
#define JOB_ANTAG_PROTECTED (1<<14)
DEFINE_BITFIELD(job_flags, list( DEFINE_BITFIELD(job_flags, list(
"JOB_ANNOUNCE_ARRIVAL" = JOB_ANNOUNCE_ARRIVAL, "JOB_ANNOUNCE_ARRIVAL" = JOB_ANNOUNCE_ARRIVAL,

View File

@@ -31,6 +31,7 @@
#define ROLE_NINJA "Space Ninja" #define ROLE_NINJA "Space Ninja"
#define ROLE_OBSESSED "Obsessed" #define ROLE_OBSESSED "Obsessed"
#define ROLE_OPERATIVE_MIDROUND "Operative (Midround)" #define ROLE_OPERATIVE_MIDROUND "Operative (Midround)"
#define ROLE_CLOWN_OPERATIVE_MIDROUND "Clown Operative (Midround)"
#define ROLE_PARADOX_CLONE "Paradox Clone" #define ROLE_PARADOX_CLONE "Paradox Clone"
#define ROLE_REV_HEAD "Head Revolutionary" #define ROLE_REV_HEAD "Head Revolutionary"
#define ROLE_SLEEPER_AGENT "Syndicate Sleeper Agent" #define ROLE_SLEEPER_AGENT "Syndicate Sleeper Agent"
@@ -67,13 +68,13 @@
#define ROLE_REVENANT "Revenant" #define ROLE_REVENANT "Revenant"
#define ROLE_SENTIENCE "Sentience Potion Spawn" #define ROLE_SENTIENCE "Sentience Potion Spawn"
#define ROLE_SOULTRAPPED_HERETIC "Soultrapped Heretic" #define ROLE_SOULTRAPPED_HERETIC "Soultrapped Heretic"
/// This flag specifically is used as a generic catch-all antag ban
#define ROLE_SYNDICATE "Syndicate" #define ROLE_SYNDICATE "Syndicate"
#define ROLE_EXPERIMENTAL_CLONER "Experimental Cloner" #define ROLE_EXPERIMENTAL_CLONER "Experimental Cloner"
#define ROLE_CLOWN_OPERATIVE "Clown Operative" #define ROLE_CLOWN_OPERATIVE "Clown Operative"
#define ROLE_FREE_GOLEM "Free Golem" #define ROLE_FREE_GOLEM "Free Golem"
#define ROLE_MORPH "Morph" #define ROLE_MORPH "Morph"
#define ROLE_NUCLEAR_OPERATIVE "Nuclear Operative"
#define ROLE_POSITRONIC_BRAIN "Positronic Brain" #define ROLE_POSITRONIC_BRAIN "Positronic Brain"
#define ROLE_SANTA "Santa" #define ROLE_SANTA "Santa"
#define ROLE_SERVANT_GOLEM "Servant Golem" #define ROLE_SERVANT_GOLEM "Servant Golem"
@@ -123,57 +124,6 @@
#define ROLE_CYBER_TAC "Cyber Tac" #define ROLE_CYBER_TAC "Cyber Tac"
#define ROLE_NETGUARDIAN "NetGuardian Prime" #define ROLE_NETGUARDIAN "NetGuardian Prime"
/// This defines the antagonists you can operate with in the settings.
/// Keys are the antagonist, values are the number of days since the player's
/// first connection in order to play.
GLOBAL_LIST_INIT(special_roles, list(
// Roundstart
ROLE_BROTHER = 0,
ROLE_CHANGELING = 0,
ROLE_CLOWN_OPERATIVE = 14,
ROLE_CULTIST = 14,
ROLE_HERETIC = 0,
ROLE_MALF = 0,
ROLE_OPERATIVE = 14,
ROLE_REV_HEAD = 14,
ROLE_TRAITOR = 0,
ROLE_WIZARD = 14,
ROLE_SPY = 0,
// Midround
ROLE_ABDUCTOR = 0,
ROLE_ALIEN = 0,
ROLE_BLOB = 0,
ROLE_BLOB_INFECTION = 0,
ROLE_CHANGELING_MIDROUND = 0,
ROLE_FUGITIVE = 0,
ROLE_LONE_OPERATIVE = 14,
ROLE_MALF_MIDROUND = 0,
ROLE_NIGHTMARE = 0,
ROLE_NINJA = 0,
ROLE_OBSESSED = 0,
ROLE_OPERATIVE_MIDROUND = 14,
ROLE_PARADOX_CLONE = 0,
ROLE_REVENANT = 0,
ROLE_SLEEPER_AGENT = 0,
ROLE_SPACE_DRAGON = 0,
ROLE_SPIDER = 0,
ROLE_WIZARD_MIDROUND = 14,
ROLE_VOIDWALKER = 0,
// Latejoin
ROLE_HERETIC_SMUGGLER = 0,
ROLE_PROVOCATEUR = 14,
ROLE_SYNDICATE_INFILTRATOR = 0,
ROLE_STOWAWAY_CHANGELING = 0,
// I'm not too sure why these are here, but they're not moving.
ROLE_GLITCH = 0,
ROLE_PAI = 0,
ROLE_SENTIENCE = 0,
ROLE_RECOVERED_CREW = 0,
))
//Job defines for what happens when you fail to qualify for any job during job selection //Job defines for what happens when you fail to qualify for any job during job selection
#define BEOVERFLOW 1 #define BEOVERFLOW 1
#define BERANDOMJOB 2 #define BERANDOMJOB 2

View File

@@ -23,7 +23,6 @@
possible_spawns += spawn_turf possible_spawns += spawn_turf
if(!length(possible_spawns)) if(!length(possible_spawns))
message_admins("No valid generic_maintenance_landmark landmarks found, aborting...")
return null return null
return pick(possible_spawns) return pick(possible_spawns)
@@ -44,7 +43,6 @@
possible_spawns += get_turf(spawn_location) possible_spawns += get_turf(spawn_location)
if(!length(possible_spawns)) if(!length(possible_spawns))
message_admins("No valid carpspawn landmarks found, aborting...")
return null return null
return pick(possible_spawns) return pick(possible_spawns)

View File

@@ -150,8 +150,8 @@
return return
///Get active players who are playing in the round ///Get active players who are playing in the round
/proc/get_active_player_count(alive_check = FALSE, afk_check = FALSE, human_check = FALSE) /proc/get_active_player_list(alive_check = FALSE, afk_check = FALSE, human_check = FALSE)
var/active_players = 0 var/list/active_players = list()
for(var/mob/player_mob as anything in GLOB.player_list) for(var/mob/player_mob as anything in GLOB.player_list)
if(!player_mob?.client) if(!player_mob?.client)
continue continue
@@ -167,9 +167,13 @@
var/mob/dead/observer/ghost_player = player_mob var/mob/dead/observer/ghost_player = player_mob
if(ghost_player.started_as_observer) // Exclude people who started as observers if(ghost_player.started_as_observer) // Exclude people who started as observers
continue continue
active_players++ active_players += player_mob
return active_players return active_players
///Counts active players who are playing in the round
/proc/get_active_player_count(alive_check = FALSE, afk_check = FALSE, human_check = FALSE)
return length(get_active_player_list(alive_check, afk_check, human_check))
///Uses stripped down and bastardized code from respawn character ///Uses stripped down and bastardized code from respawn character
/proc/make_body(mob/dead/observer/ghost_player) /proc/make_body(mob/dead/observer/ghost_player)
if(!ghost_player || !ghost_player.key) if(!ghost_player || !ghost_player.key)

View File

@@ -1,8 +1,3 @@
/// Log to dynamic and message admins
/datum/controller/subsystem/dynamic/proc/log_dynamic_and_announce(text)
message_admins("DYNAMIC: [text]")
log_dynamic("[text]")
/// Logging for dynamic procs /// Logging for dynamic procs
/proc/log_dynamic(text, list/data) /proc/log_dynamic(text, list/data)
logger.Log(LOG_CATEGORY_DYNAMIC, text, data) logger.Log(LOG_CATEGORY_DYNAMIC, text, data)

View File

@@ -1,9 +1,10 @@
/// Logging for player manifest (ckey, name, job, special role, roundstart/latejoin) /// Logging for player manifest (ckey, name, job, special role, roundstart/latejoin)
/proc/log_manifest(ckey, datum/mind/mind, mob/body, latejoin = FALSE) /proc/log_manifest(ckey, datum/mind/mind, mob/body, latejoin = FALSE)
var/message = "[ckey] \\ [body.real_name] \\ [mind.assigned_role.title] \\ [mind.special_role || "NONE"] \\ [latejoin ? "LATEJOIN" : "ROUNDSTART"]" var/roles = english_list(mind.get_special_roles(), nothing_text = "NONE")
var/message = "[ckey] \\ [body.real_name] \\ [mind.assigned_role.title] \\ [roles] \\ [latejoin ? "LATEJOIN" : "ROUNDSTART"]"
logger.Log(LOG_CATEGORY_MANIFEST, message, list( logger.Log(LOG_CATEGORY_MANIFEST, message, list(
"mind" = mind, "body" = body, "latejoin" = latejoin "mind" = mind, "body" = body, "latejoin" = latejoin
)) ))
// Roundstart happens with SSblackbox.ReportRoundstartManifest // Roundstart happens with SSblackbox.ReportRoundstartManifest
if(latejoin) if(latejoin)
SSblackbox.ReportManifest(ckey, body.real_name, mind.assigned_role.title, mind.special_role, latejoin) SSblackbox.ReportManifest(ckey, body.real_name, mind.assigned_role.title, roles, latejoin)

View File

@@ -359,15 +359,9 @@ GLOBAL_LIST_INIT(achievements_unlocked, list())
else else
parts += "[FOURSPACES]<i>Nobody died this shift!</i>" parts += "[FOURSPACES]<i>Nobody died this shift!</i>"
parts += "[FOURSPACES]Threat level: [SSdynamic.threat_level]" parts += "[FOURSPACES]Round: [SSdynamic.current_tier.name]"
parts += "[FOURSPACES]Threat left: [SSdynamic.mid_round_budget]" for(var/datum/dynamic_ruleset/rule as anything in SSdynamic.executed_rulesets - SSdynamic.unreported_rulesets)
if(SSdynamic.roundend_threat_log.len) parts += "[FOURSPACES][FOURSPACES]- <b>[rule.name]</b> ([rule.config_tag])"
parts += "[FOURSPACES]Threat edits:"
for(var/entry as anything in SSdynamic.roundend_threat_log)
parts += "[FOURSPACES][FOURSPACES][entry]<BR>"
parts += "[FOURSPACES]Executed rules:"
for(var/datum/dynamic_ruleset/rule in SSdynamic.executed_rules)
parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - <b>[rule.name]</b>: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat"
return parts.Join("<br>") return parts.Join("<br>")

View File

@@ -340,7 +340,7 @@ Versioning
"name" = L.real_name, "name" = L.real_name,
"key" = L.ckey, "key" = L.ckey,
"job" = L.mind.assigned_role.title, "job" = L.mind.assigned_role.title,
"special" = L.mind.special_role, "special" = jointext(L.mind.get_special_roles(), " | "),
"pod" = get_area_name(L, TRUE), "pod" = get_area_name(L, TRUE),
"laname" = L.lastattacker, "laname" = L.lastattacker,
"lakey" = L.lastattackerckey, "lakey" = L.lastattackerckey,
@@ -428,7 +428,7 @@ Versioning
"ckey" = mob_ckey, "ckey" = mob_ckey,
"character_name" = new_character.real_name, "character_name" = new_character.real_name,
"job" = new_character.mind?.assigned_role?.title, "job" = new_character.mind?.assigned_role?.title,
"special" = new_character.mind?.special_role, "special" = english_list(new_character.mind?.get_special_roles(), nothing_text = "NONE"),
"latejoin" = 0, "latejoin" = 0,
)) ))
SSdbcore.MassInsert(format_table_name("manifest"), query_rows, special_columns = special_columns) SSdbcore.MassInsert(format_table_name("manifest"), query_rows, special_columns = special_columns)

View File

@@ -0,0 +1,55 @@
// Config values, don't change these randomly
/// Configuring "roundstart" type rulesets
#define ROUNDSTART "roundstart"
/// Configuring "light midround" type rulesets
#define LIGHT_MIDROUND "light_midround"
/// Configuring "heavy midround" type rulesets
#define HEAVY_MIDROUND "heavy_midround"
/// Configuring "latejoin" type rulesets
#define LATEJOIN "latejoin"
/// Lower end for how many of a ruleset type can be selected
#define LOW_END "low"
/// Upper end for how many of a ruleset type can be selected
#define HIGH_END "high"
/// Population threshold for ruleset types - below this, only a quarter of the low to high end is used
#define HALF_RANGE_POP_THRESHOLD "half_range_pop_threshold"
/// Population threshold for ruleset types - below this, only a half of the low to high end is used
#define FULL_RANGE_POP_THRESHOLD "full_range_pop_threshold"
/// Round time threshold for which a ruleset type will be selected
#define TIME_THRESHOLD "time_threshold"
/// Lower end for cooldown duration for a ruleset type
#define EXECUTION_COOLDOWN_LOW "execution_cooldown_low"
/// Upper end for cooldown duration for a ruleset type
#define EXECUTION_COOLDOWN_HIGH "execution_cooldown_high"
// Tiers, don't change these randomly
/// Tier 0, no antags at all
#define DYNAMIC_TIER_GREEN 0
/// Tier 1, low amount of antags
#define DYNAMIC_TIER_LOW 1
/// Tier 2, medium amount of antags
#define DYNAMIC_TIER_LOWMEDIUM 2
/// Tier 3, high amount of antags
#define DYNAMIC_TIER_MEDIUMHIGH 3
/// Tier 4, maximum amount of antags
#define DYNAMIC_TIER_HIGH 4
// Ruleset flags
/// Ruleset denotes that it involves an outside force spawning in to attack the station
#define RULESET_INVADER (1<<0)
/// Multiple high impact rulesets cannot be selected unless we're at the highest tier
#define RULESET_HIGH_IMPACT (1<<1)
/// Ruleset can be configured by admins (implements /proc/configure_ruleset)
/// Only implemented for midrounds currently
#define RULESET_ADMIN_CONFIGURABLE (1<<2)
/// Href for cancelling midround rulesets before execution
#define MIDROUND_CANCEL_HREF(...) "(<a href='byond://?src=[REF(src)];admin_cancel_midround=[REF(picked_ruleset)]'>CANCEL</a>)"
/// Href for rerolling midround rulesets before execution
#define MIDROUND_REROLL_HREF(rulesets) "[length(rulesets) \
? "(<a href='byond://?src=[REF(src)];admin_reroll=[REF(picked_ruleset)]'>SOMETHING ELSE</a>)" \
: "([span_tooltip("There are no more rulesets to pick from!", "NOTHING ELSE")])"\
]"
#define RULESET_CONFIG_CANCEL "Cancel"

View File

@@ -0,0 +1,372 @@
/**
* ## Dynamic ruleset datum
*
* These datums (which are not singletons) are used by dynamic to create antagonists
*/
/datum/dynamic_ruleset
/// Human-readable name of the ruleset.
var/name
/// Tag the ruleset uses for configuring.
/// Don't change this unless you know what you're doing.
var/config_tag
/// What flag to check for jobbans? Optional, if unset, uses pref_flag
var/jobban_flag
/// What flag to check for prefs? Required if the antag has an associated preference
var/pref_flag
/// Flags for this ruleset
var/ruleset_flags = NONE
/// Points to what antag datum this ruleset will use for generating a preview icon in the prefs menu
var/preview_antag_datum
/// List of all minds selected for this ruleset
VAR_FINAL/list/datum/mind/selected_minds = list()
/**
* The chance the ruleset is picked when selecting from the pool of rulesets.
*
* This can either be
* - A list of weight corresponding to dynamic tiers.
* If a tier is not specified, it will use the next highest tier.
* Or
* - A single weight for all tiers.
*/
var/list/weight = 0
/**
* The min population for which this ruleset is available.
*
* This can either be
* - A list of min populations corresponding to dynamic tiers.
* If a tier is not specified, it will use the next highest tier.
* Or
* - A single min population for all tiers.
*/
var/list/min_pop = 0
/// List of roles that are blacklisted from this ruleset
/// For roundstart rulesets, it will prevent players from being selected for this ruleset if they have one of these roles
/// For latejoin or midround rulesets, it will prevent players from being assigned to this ruleset if they have one of these roles
var/list/blacklisted_roles = list()
/**
* How many candidates are needed for this ruleset to be selected?
* Ie. "We won't even bother attempting to run this ruleset unless at least x players want to be it"
*
* This can either be
* - A number
* Or
* - A list in the form of list("denominator" = x, "offset" = y)
* which will divide the population size by x and add y to it to calculate the number of candidates
*/
var/min_antag_cap = 1
/**
* How many candidates will be this ruleset try to select?
* Ie. "We have 10 cadidates, but we only want x of them to be antags"
*
* This can either be
* - A number
* Or
* - A list in the form of list("denominator" = x, "offset" = y)
* which will divide the population size by x and add y to it to calculate the number of candidates
*
* If null, defaults to min_antag_cap
*/
var/max_antag_cap
/// If set to TRUE, dynamic will be able to draft this ruleset again later on
var/repeatable = FALSE
/// Every time this ruleset is selected, the weight will be decreased by this amount
var/repeatable_weight_decrease = 2
/// Players whose account is less than this many days old will be filtered out of the candidate list
var/minimum_required_age = 0
/// Templates necessary for this ruleset to be executed
VAR_PROTECTED/list/ruleset_lazy_templates
/datum/dynamic_ruleset/New(list/dynamic_config)
for(var/new_var in dynamic_config?[config_tag])
set_config_value(new_var, dynamic_config[config_tag][new_var])
/datum/dynamic_ruleset/Destroy()
selected_minds = null
return ..()
/// Used for parsing config entries to validate them
/datum/dynamic_ruleset/proc/set_config_value(new_var, new_val)
if(!(new_var in vars))
log_dynamic("Erroneous config edit rejected: [new_var]")
return FALSE
var/static/list/locked_config_values = list(
NAMEOF_STATIC(src, config_tag),
NAMEOF_STATIC(src, jobban_flag),
NAMEOF_STATIC(src, pref_flag),
NAMEOF_STATIC(src, preview_antag_datum),
NAMEOF_STATIC(src, ruleset_flags),
NAMEOF_STATIC(src, ruleset_lazy_templates),
NAMEOF_STATIC(src, selected_minds),
NAMEOF_STATIC(src, vars),
)
if(new_var in locked_config_values)
log_dynamic("Bad config edit rejected: [new_var]")
return FALSE
if(islist(new_val) && (new_var == NAMEOF(src, weight) || new_var == NAMEOF(src, min_pop)))
new_val = load_tier_list(new_val)
vars[new_var] = new_val
return TRUE
/datum/dynamic_ruleset/vv_edit_var(var_name, var_value)
if(var_name == NAMEOF(src, config_tag))
return FALSE
return ..()
/// Used to create tier lists for weights and min_pop values
/datum/dynamic_ruleset/proc/load_tier_list(list/incoming_list)
PRIVATE_PROC(TRUE)
var/list/tier_list = new /list(4)
// loads a list of list("2" = 1, "3" = 3) into a list(null, 1, 3, null)
for(var/tier in incoming_list)
tier_list[text2num(tier)] = incoming_list[tier]
// turn list(null, 1, 3, null) into list(1, 1, 3, null)
for(var/i in 1 to length(tier_list))
var/val = tier_list[i]
if(isnum(val))
break
for(var/j in i to length(tier_list))
var/other_val = tier_list[j]
if(!isnum(other_val))
continue
tier_list[i] = other_val
break
// turn list(1, 1, 3, null) into list(1, 1, 3, 3)
for(var/i in length(tier_list) to 1 step -1)
var/val = tier_list[i]
if(isnum(val))
break
for(var/j in i to 1 step -1)
var/other_val = tier_list[j]
if(!isnum(other_val))
continue
tier_list[i] = other_val
break
// we can assert that tier[1] and tier[4] are not null, but we cannot say the same for tier[2] and tier[3]
// this can be happen due to the following setup: list(1, null, null, 4)
// (which is an invalid config, and should be fixed by the operator)
if(isnull(tier_list[2]))
tier_list[2] = tier_list[1]
if(isnull(tier_list[3]))
tier_list[3] = tier_list[4]
return tier_list
/**
* Any additional checks to see if this ruleset can be selected
*/
/datum/dynamic_ruleset/proc/can_be_selected()
return TRUE
/**
* Calculates the weight of this ruleset for the given tier.
*
* * population_size - How many players are alive
* * tier - The dynamic tier to calculate the weight for
*/
/datum/dynamic_ruleset/proc/get_weight(population_size = 0, tier = DYNAMIC_TIER_LOW)
SHOULD_NOT_OVERRIDE(TRUE)
if(type in SSdynamic.admin_disabled_rulesets)
return 0
if(!can_be_selected())
return 0
var/final_minpop = islist(min_pop) ? min_pop[tier] : min_pop
if(final_minpop > population_size)
return 0
var/final_weight = islist(weight) ? weight[tier] : weight
for(var/datum/dynamic_ruleset/other_ruleset as anything in SSdynamic.executed_rulesets)
if(other_ruleset == src)
continue
if(tier != DYNAMIC_TIER_HIGH && (ruleset_flags & RULESET_HIGH_IMPACT) && (other_ruleset.ruleset_flags & RULESET_HIGH_IMPACT))
return 0
if(!istype(other_ruleset, type))
continue
if(!repeatable)
return 0
final_weight -= repeatable_weight_decrease
return max(final_weight, 0)
/// Returns what the antag cap with the given population is.
/datum/dynamic_ruleset/proc/get_antag_cap(population_size, antag_cap)
SHOULD_NOT_OVERRIDE(TRUE)
if (isnum(antag_cap))
return antag_cap
return ceil(population_size / antag_cap["denominator"]) + antag_cap["offset"]
/**
* Prepares the ruleset for execution, primarily used for selecting the players who will be assigned to this ruleset
*
* * antag_candidates - List of players who are candidates for this ruleset
* This list is mutated by this proc!
*
* Returns TRUE if execution is ready, FALSE if it should be canceled
*/
/datum/dynamic_ruleset/proc/prepare_execution(population_size = 0, list/mob/antag_candidates = list())
SHOULD_NOT_OVERRIDE(TRUE)
// !! THIS SLEEPS !!
load_templates()
// This is (mostly) redundant, buuuut the (potential) sleep above makes it iffy, so let's just be safe
if(!can_be_selected())
return FALSE
var/max_candidates = get_antag_cap(population_size, max_antag_cap || min_antag_cap)
var/min_candidates = get_antag_cap(population_size, min_antag_cap)
var/list/selected_candidates = select_candidates(antag_candidates, max_candidates)
if(length(selected_candidates) < min_candidates)
return FALSE
for(var/mob/candidate as anything in selected_candidates)
var/datum/mind/candidate_mind = get_candidate_mind(candidate)
prepare_for_role(candidate_mind)
LAZYADDASSOC(SSjob.prevented_occupations, candidate_mind, get_blacklisted_roles()) // this is what makes sure you can't roll traitor as a sec-off
selected_minds += candidate_mind
antag_candidates -= candidate
return TRUE
/// Gets the mind of a candidate, can be overridden to return a different mind if necessary
/datum/dynamic_ruleset/proc/get_candidate_mind(mob/dead/candidate)
return candidate.mind
/// Returns a list of roles that cannot be selected for this ruleset
/datum/dynamic_ruleset/proc/get_blacklisted_roles()
return get_config_blacklisted_roles() | get_always_blacklisted_roles()
/// Returns all the jobs the config says this ruleset cannot select
/datum/dynamic_ruleset/proc/get_config_blacklisted_roles()
SHOULD_NOT_OVERRIDE(TRUE)
var/list/blacklist = blacklisted_roles.Copy()
for(var/datum/job/job as anything in SSjob.all_occupations)
var/protected = (job.job_flags & JOB_ANTAG_PROTECTED)
var/blacklisted = (job.job_flags & JOB_ANTAG_BLACKLISTED)
if((CONFIG_GET(flag/protect_roles_from_antagonist) && protected) || blacklisted)
blacklist |= job.title
if(CONFIG_GET(flag/protect_assistant_from_antagonist))
blacklisted_roles |= JOB_ASSISTANT
return blacklist
/// Returns a list of roles that are always blacklisted from this ruleset, for mechanical reasons (an AI can't be a changeling)
/datum/dynamic_ruleset/proc/get_always_blacklisted_roles()
return list(
JOB_AI,
JOB_CYBORG,
)
/// Takes in a list of players and returns a list of players who are valid candidates for this ruleset
/// Don't touch this proc if you need to trim candidates further - override is_valid_candidate() instead
/datum/dynamic_ruleset/proc/trim_candidates(list/mob/antag_candidates)
SHOULD_NOT_OVERRIDE(TRUE)
var/list/valid_candidates = list()
for(var/mob/candidate as anything in antag_candidates)
var/client/candidate_client = GET_CLIENT(candidate)
if(isnull(candidate_client))
continue
if(candidate_client.get_remaining_days(minimum_required_age) > 0)
continue
if(pref_flag && !(pref_flag in candidate_client.prefs.be_special))
continue
if(is_banned_from(candidate.ckey, list(ROLE_SYNDICATE, jobban_flag || pref_flag)))
continue
if(!is_valid_candidate(candidate, candidate_client))
continue
valid_candidates += candidate
return valid_candidates
/// Returns a list of players picked for this ruleset
/datum/dynamic_ruleset/proc/select_candidates(list/mob/antag_candidates, num_candidates = 0)
SHOULD_NOT_OVERRIDE(TRUE)
PRIVATE_PROC(TRUE)
if(num_candidates <= 0)
return list()
// technically not pure
var/list/resulting_candidates = shuffle(trim_candidates(antag_candidates)) || list()
if(length(resulting_candidates) <= num_candidates)
return resulting_candidates
resulting_candidates.Cut(num_candidates + 1)
return resulting_candidates
/// Handles loading map templates that this ruleset requires
/datum/dynamic_ruleset/proc/load_templates()
SHOULD_NOT_OVERRIDE(TRUE)
PRIVATE_PROC(TRUE)
for(var/template in ruleset_lazy_templates)
SSmapping.lazy_load_template(template)
/**
* Any additional checks to see if this player is a valid candidate for this ruleset
*/
/datum/dynamic_ruleset/proc/is_valid_candidate(mob/candidate, client/candidate_client)
SHOULD_CALL_PARENT(TRUE)
return TRUE
/**
* Handles any special logic that needs to be done for a player before they are assigned to this ruleset
* This is ran before the player is in their job position, and before they even have a player character
*
* Override this proc to do things like set forced jobs, DON'T assign roles or give out equipments here!
*/
/datum/dynamic_ruleset/proc/prepare_for_role(datum/mind/candidate)
PROTECTED_PROC(TRUE)
return
/**
* Executes the ruleset, assigning the selected players to their roles.
* No backing out now, at this point it's guaranteed to run.
*
* Prefer to override assign_role() instead of this proc
*/
/datum/dynamic_ruleset/proc/execute()
var/list/execute_args = create_execute_args()
for(var/datum/mind/mind as anything in selected_minds)
assign_role(arglist(list(mind) + execute_args))
/// Allows you to supply extra arguments to assign_role() if needed
/datum/dynamic_ruleset/proc/create_execute_args()
return list()
/**
* Used by the ruleset to actually assign the role to the player
* This is ran after they have a player character spawned, and after they're in their job (with all their job equipment)
*
* Override this proc to give out antag datums or special items or whatever
*/
/datum/dynamic_ruleset/proc/assign_role(datum/mind/candidate)
PROTECTED_PROC(TRUE)
stack_trace("Ruleset [src] does not implement assign_role()")
return
/**
* Handles setting SSticker news report / mode result for more impactful rulsets
*
* Return TRUE if any result was set
*/
/datum/dynamic_ruleset/proc/round_result()
return FALSE
/**
* Allows admins to configure rulesets before prepare_execution() is called.
*
* Only called if RULESET_ADMIN_CONFIGURABLE is set in ruleset_flags.
* Also only called by midrounds currently.
*/
/datum/dynamic_ruleset/proc/configure_ruleset(mob/admin)
stack_trace("Ruleset [type] sets flag RULESET_ADMIN_CONFIGURABLE but does not implement configure_ruleset!")

View File

@@ -0,0 +1,316 @@
/**
* ## Dynamic tier datum
*
* These datums are essentially used to configure the dynamic system
* They serve as a very simple way to see at a glance what dynamic is doing and what it is going to do
*
* For example, a tier will say "we will spawn 1-2 roundstart antags"
*/
/datum/dynamic_tier
/// Tier number - A number which determines the severity of the tier - the higher the number, the more antags
var/tier = -1
/// The human readable name of the tier
var/name
/// Tag the tier uses for configuring.
/// Don't change this unless you know what you're doing.
var/config_tag
/// The chance this tier will be selected from all tiers
/// Keep all tiers added up to 100 weight, keeps things readable
var/weight = 0
/// This tier will not be selected if the population is below this number
var/min_pop = 0
/// String which is sent to the players reporting which tier is active
var/advisory_report
/**
* How Dynamic will select rulesets based on the tier
*
* Every tier configures each of the ruleset types - ie, roundstart, light midround, heavy midround, latejoin
*
* Every type can be configured with the following:
* - LOW_END: The lower for how many of this ruleset type can be selected
* - HIGH_END: The upper for how many of this ruleset type can be selected
* - HALF_RANGE_POP_THRESHOLD: Below this population range, the high end is quartered
* - FULL_RANGE_POP_THRESHOLD: Below this population range, the high end is halved
*
* Non-roundstart ruleset types also have:
* - TIME_THRESHOLD: World time must pass this threshold before dynamic starts running this ruleset type
* - EXECUTION_COOLDOWN_LOW: The lower end for how long to wait before running this ruleset type again
* - EXECUTION_COOLDOWN_HIGH: The upper end for how long to wait before running this ruleset type again
*/
var/list/ruleset_type_settings = list(
ROUNDSTART = list(
LOW_END = 0,
HIGH_END = 0,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 50,
TIME_THRESHOLD = 0 MINUTES,
EXECUTION_COOLDOWN_LOW = 0 MINUTES,
EXECUTION_COOLDOWN_HIGH = 0 MINUTES,
),
LIGHT_MIDROUND = list(
LOW_END = 0,
HIGH_END = 0,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 30 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
HEAVY_MIDROUND = list(
LOW_END = 0,
HIGH_END = 0,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 60 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
LATEJOIN = list(
LOW_END = 0,
HIGH_END = 0,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 0 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
)
/datum/dynamic_tier/New(list/dynamic_config)
for(var/new_var in dynamic_config?[config_tag])
if(!(new_var in vars))
continue
set_config_value(new_var, dynamic_config[config_tag][new_var])
/// Used for parsing config entries to validate them
/datum/dynamic_tier/proc/set_config_value(new_var, new_val)
switch(new_var)
if(NAMEOF(src, tier), NAMEOF(src, config_tag), NAMEOF(src, vars))
return FALSE
if(NAMEOF(src, ruleset_type_settings))
for(var/category in new_val)
for(var/rule in new_val[category])
if(rule == LOW_END || rule == HIGH_END)
ruleset_type_settings[category][rule] = max(0, new_val[category][rule])
else if(rule == TIME_THRESHOLD || rule == EXECUTION_COOLDOWN_LOW || rule == EXECUTION_COOLDOWN_HIGH)
ruleset_type_settings[category][rule] = new_val[category][rule] * 1 MINUTES
else
ruleset_type_settings[category][rule] = new_val[category][rule]
return TRUE
vars[new_var] = new_val
return TRUE
/datum/dynamic_tier/vv_edit_var(var_name, var_value)
switch(var_name)
if(NAMEOF(src, tier))
return FALSE
return ..()
/datum/dynamic_tier/greenshift
tier = DYNAMIC_TIER_GREEN
config_tag = "Greenshift"
name = "Greenshift"
weight = 2
advisory_report = "Advisory Level: <b>Green Star</b></center><BR>\
Your sector's advisory level is Green Star. \
Surveillance information shows no credible threats to Nanotrasen assets within the Spinward Sector at this time. \
As always, the Department advises maintaining vigilance against potential threats, regardless of a lack of known threats."
/datum/dynamic_tier/low
tier = DYNAMIC_TIER_LOW
config_tag = "Low Chaos"
name = "Low Chaos"
weight = 8
advisory_report = "Advisory Level: <b>Yellow Star</b></center><BR>\
Your sector's advisory level is Yellow Star. \
Surveillance shows a credible risk of enemy attack against our assets in the Spinward Sector. \
We advise a heightened level of security alongside maintaining vigilance against potential threats."
ruleset_type_settings = list(
ROUNDSTART = list(
LOW_END = 1,
HIGH_END = 1,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
),
LIGHT_MIDROUND = list(
LOW_END = 0,
HIGH_END = 2,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 30 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
HEAVY_MIDROUND = list(
LOW_END = 0,
HIGH_END = 1,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 60 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
LATEJOIN = list(
LOW_END = 0,
HIGH_END = 1,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 5 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
)
/datum/dynamic_tier/lowmedium
tier = DYNAMIC_TIER_LOWMEDIUM
config_tag = "Low-Medium Chaos"
name = "Low-Medium Chaos"
weight = 46
advisory_report = "Advisory Level: <b>Red Star</b></center><BR>\
Your sector's advisory level is Red Star. \
The Department of Intelligence has decrypted Cybersun communications suggesting a high likelihood of attacks \
on Nanotrasen assets within the Spinward Sector. \
Stations in the region are advised to remain highly vigilant for signs of enemy activity and to be on high alert."
ruleset_type_settings = list(
ROUNDSTART = list(
LOW_END = 1,
HIGH_END = 2,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
),
LIGHT_MIDROUND = list(
LOW_END = 0,
HIGH_END = 2,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 30 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
HEAVY_MIDROUND = list(
LOW_END = 0,
HIGH_END = 1,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 60 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
LATEJOIN = list(
LOW_END = 1,
HIGH_END = 2,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 5 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
)
/datum/dynamic_tier/mediumhigh
tier = DYNAMIC_TIER_MEDIUMHIGH
config_tag = "Medium-High Chaos"
name = "Medium-High Chaos"
weight = 36
advisory_report = "Advisory Level: <b>Black Orbit</b></center><BR>\
Your sector's advisory level is Black Orbit. \
Your sector's local communications network is currently undergoing a blackout, \
and we are therefore unable to accurately judge enemy movements within the region. \
However, information passed to us by GDI suggests a high amount of enemy activity in the sector, \
indicative of an impending attack. Remain on high alert and vigilant against any other potential threats."
ruleset_type_settings = list(
ROUNDSTART = list(
LOW_END = 2,
HIGH_END = 3,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
),
LIGHT_MIDROUND = list(
LOW_END = 1,
HIGH_END = 2,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 30 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
HEAVY_MIDROUND = list(
LOW_END = 1,
HIGH_END = 2,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 60 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
LATEJOIN = list(
LOW_END = 1,
HIGH_END = 3,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 5 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
)
/datum/dynamic_tier/high
tier = DYNAMIC_TIER_HIGH
config_tag = "High Chaos"
name = "High Chaos"
weight = 10
min_pop = 25
advisory_report = "Advisory Level: <b>Midnight Sun</b></center><BR>\
Your sector's advisory level is Midnight Sun. \
Credible information passed to us by GDI suggests that the Syndicate \
is preparing to mount a major concerted offensive on Nanotrasen assets in the Spinward Sector to cripple our foothold there. \
All stations should remain on high alert and prepared to defend themselves."
ruleset_type_settings = list(
ROUNDSTART = list(
LOW_END = 3,
HIGH_END = 4,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
),
LIGHT_MIDROUND = list(
LOW_END = 1,
HIGH_END = 2,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 20 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
HEAVY_MIDROUND = list(
LOW_END = 2,
HIGH_END = 4,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 30 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
LATEJOIN = list(
LOW_END = 2,
HIGH_END = 3,
HALF_RANGE_POP_THRESHOLD = 25,
FULL_RANGE_POP_THRESHOLD = 40,
TIME_THRESHOLD = 5 MINUTES,
EXECUTION_COOLDOWN_LOW = 10 MINUTES,
EXECUTION_COOLDOWN_HIGH = 20 MINUTES,
),
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,254 @@
ADMIN_VERB(dynamic_panel, R_ADMIN, "Dynamic Panel", "Mess with dynamic.", ADMIN_CATEGORY_GAME)
dynamic_panel(user.mob)
/proc/dynamic_panel(mob/user)
if(!check_rights(R_ADMIN))
return
var/datum/dynamic_panel/tgui = new()
tgui.ui_interact(user)
log_admin("[key_name(user)] opened the Dynamic Panel.")
if(!isobserver(user))
message_admins("[key_name_admin(user)] opened the Dynamic Panel.")
BLACKBOX_LOG_ADMIN_VERB("Dynamic Panel")
/datum/dynamic_panel
/datum/dynamic_panel/ui_state(mob/user)
return ADMIN_STATE(R_ADMIN)
/datum/dynamic_panel/ui_close()
qdel(src)
/datum/dynamic_panel/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "DynamicAdmin")
ui.open()
/datum/dynamic_panel/ui_data(mob/user)
var/list/data = list()
if(SSdynamic.current_tier)
data["current_tier"] = list(
"number" = SSdynamic.current_tier.tier,
"name" = SSdynamic.current_tier.name,
)
data["ruleset_count"] = list()
for(var/category in SSdynamic.rulesets_to_spawn)
data["ruleset_count"][category] = max(SSdynamic.rulesets_to_spawn[category], 0)
data["full_config"] = SSdynamic.get_config()
data["config_even_enabled"] = CONFIG_GET(flag/dynamic_config_enabled) && length(data["full_config"])
data["queued_rulesets"] = list()
for(var/i in 1 to length(SSdynamic.queued_rulesets))
data["queued_rulesets"] += list(ruleset_to_data(SSdynamic.queued_rulesets[i]) + list("index" = i))
data["active_rulesets"] = list()
for(var/i in 1 to length(SSdynamic.executed_rulesets))
data["active_rulesets"] += list(ruleset_to_data(SSdynamic.executed_rulesets[i]) + list("index" = i))
data["all_rulesets"] = list()
for(var/ruleset_type in subtypesof(/datum/dynamic_ruleset/roundstart))
data["all_rulesets"][ROUNDSTART] += list(ruleset_to_data(ruleset_type))
for(var/ruleset_type in subtypesof(/datum/dynamic_ruleset/midround))
var/datum/dynamic_ruleset/midround/midround = ruleset_type
switch(initial(midround.midround_type))
if(HEAVY_MIDROUND)
data["all_rulesets"][HEAVY_MIDROUND] += list(ruleset_to_data(ruleset_type))
if(LIGHT_MIDROUND)
data["all_rulesets"][LIGHT_MIDROUND] += list(ruleset_to_data(ruleset_type))
for(var/ruleset_type in subtypesof(/datum/dynamic_ruleset/latejoin))
data["all_rulesets"][LATEJOIN] += list(ruleset_to_data(ruleset_type))
data["time_until_lights"] = COOLDOWN_TIMELEFT(SSdynamic, light_ruleset_start)
data["time_until_heavies"] = COOLDOWN_TIMELEFT(SSdynamic, heavy_ruleset_start)
data["time_until_latejoins"] = COOLDOWN_TIMELEFT(SSdynamic, latejoin_ruleset_start)
data["time_until_next_midround"] = COOLDOWN_TIMELEFT(SSdynamic, midround_cooldown)
data["time_until_next_latejoin"] = COOLDOWN_TIMELEFT(SSdynamic, latejoin_cooldown)
data["failed_latejoins"] = SSdynamic.failed_latejoins
data["light_midround_chance"] = SSdynamic.get_midround_chance(LIGHT_MIDROUND)
data["heavy_midround_chance"] = SSdynamic.get_midround_chance(HEAVY_MIDROUND)
data["latejoin_chance"] = SSdynamic.get_latejoin_chance()
data["roundstarted"] = SSticker.HasRoundStarted()
data["light_chance_maxxed"] = SSdynamic.admin_forcing_next_light
data["heavy_chance_maxxed"] = SSdynamic.admin_forcing_next_heavy
data["latejoin_chance_maxxed"] = SSdynamic.admin_forcing_next_latejoin
data["next_dynamic_tick"] = SSdynamic.next_fire ? SSdynamic.next_fire - world.time : SSticker.GetTimeLeft()
data["antag_events_enabled"] = SSdynamic.antag_events_enabled
return data
/// Pass a ruleset typepath or a ruleset instance
/datum/dynamic_panel/proc/ruleset_to_data(datum/dynamic_ruleset/ruleset)
var/list/data = list()
var/ruleset_path = isdatum(ruleset) ? ruleset.type : ruleset
data["name"] = initial(ruleset.name)
data["id"] = initial(ruleset.config_tag)
data["typepath"] = ruleset_path
data["selected_players"] = list()
data["admin_disabled"] = (ruleset_path in SSdynamic.admin_disabled_rulesets)
if(isdatum(ruleset))
for(var/datum/mind/player as anything in ruleset.selected_minds)
data["selected_players"] += list(list(
"key" = player.key,
))
data["hidden"] = (ruleset in SSdynamic.unreported_rulesets)
return data
/datum/dynamic_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("remove_queued_ruleset")
var/index = params["ruleset_index"]
if(length(SSdynamic.queued_rulesets) < index)
return
var/datum/dynamic_ruleset/ruleset = SSdynamic.queued_rulesets[index]
if(!ruleset)
return
SSdynamic.queued_rulesets -= ruleset
message_admins("[key_name_admin(ui.user)] removed [ruleset.config_tag] from the dynamic ruleset queue.")
log_admin("[key_name_admin(ui.user)] removed [ruleset.config_tag] from the dynamic ruleset queue.")
qdel(ruleset)
return TRUE
if("add_queued_ruleset")
var/datum/dynamic_ruleset/ruleset_path = text2path(params["ruleset_type"])
if(!ruleset_path)
return
SSdynamic.queue_ruleset(ruleset_path)
message_admins("[key_name_admin(ui.user)] added [initial(ruleset_path.config_tag)] to the dynamic ruleset queue.")
log_admin("[key_name_admin(ui.user)] added [initial(ruleset_path.config_tag)] to the dynamic ruleset queue.")
return TRUE
if("dynamic_vv")
ui.user?.client?.debug_variables(SSdynamic)
return TRUE
if("add_ruleset_category_count")
var/category = params["ruleset_category"]
if(!category)
return
SSdynamic.rulesets_to_spawn[category] += 1
message_admins("[key_name_admin(ui.user)] added 1 to the [category] ruleset category.")
log_admin("[key_name_admin(ui.user)] added 1 to the [category] ruleset category.")
return TRUE
if("set_ruleset_category_count")
var/category = params["ruleset_category"]
var/count = params["ruleset_count"]
if(!category || !isnum(count))
return
SSdynamic.rulesets_to_spawn[category] = count
message_admins("[key_name_admin(ui.user)] set the [category] ruleset category to [count].")
log_admin("[key_name_admin(ui.user)] set the [category] ruleset category to [count].")
return TRUE
if("execute_ruleset")
var/datum/dynamic_ruleset/ruleset_path = text2path(params["ruleset_type"])
if(!ruleset_path)
return
message_admins("[key_name_admin(ui.user)] executed the ruleset [initial(ruleset_path.config_tag)].")
log_admin("[key_name_admin(ui.user)] executed the ruleset [initial(ruleset_path.config_tag)].")
ASYNC
SSdynamic.force_run_midround(ruleset_path, alert_admins_on_fail = TRUE, admin = ui.user)
return TRUE
if("disable_ruleset")
var/ruleset_path = text2path(params["ruleset_type"])
if(!ruleset_path)
return
if(ruleset_path in SSdynamic.admin_disabled_rulesets)
SSdynamic.admin_disabled_rulesets -= ruleset_path
message_admins("[key_name_admin(ui.user)] enabled [ruleset_path] to be selected.")
log_admin("[key_name_admin(ui.user)] enabled [ruleset_path] to be selected.")
else
SSdynamic.admin_disabled_rulesets += ruleset_path
message_admins("[key_name_admin(ui.user)] disabled [ruleset_path] from being selected.")
log_admin("[key_name_admin(ui.user)] disabled [ruleset_path] from being selected.")
return TRUE
if("disable_all")
SSdynamic.admin_disabled_rulesets |= subtypesof(/datum/dynamic_ruleset)
message_admins("[key_name_admin(ui.user)] disabled all rulesets from being selected.")
log_admin("[key_name_admin(ui.user)] disabled all rulesets from being selected.")
if("enable_all")
SSdynamic.admin_disabled_rulesets.Cut()
message_admins("[key_name_admin(ui.user)] re-enabled all rulesets.")
log_admin("[key_name_admin(ui.user)] re_enabled all rulesets.")
if("set_tier")
if(SSdynamic.current_tier && SSticker.HasRoundStarted())
return TRUE
var/list/tiers = list()
for(var/datum/dynamic_tier/tier as anything in subtypesof(/datum/dynamic_tier))
tiers[initial(tier.name)] = tier
var/datum/dynamic_tier/picked = tgui_input_list(ui.user, "Pick a dynamic tier before the game starts", "Pick tier", tiers, ui_state = ADMIN_STATE(R_ADMIN))
if(picked && !SSticker.HasRoundStarted())
SSdynamic.set_tier(tiers[picked])
message_admins("[key_name_admin(ui.user)] set the dynamic tier to [initial(picked.tier)].")
log_admin("[key_name_admin(ui.user)] set the dynamic tier to [initial(picked.tier)].")
return TRUE
if("max_light_chance")
SSdynamic.admin_forcing_next_light = !SSdynamic.admin_forcing_next_light
message_admins("[key_name_admin(ui.user)] [SSdynamic.admin_forcing_next_light ? "forced" : "reset"] the next light ruleset chance.")
log_admin("[key_name_admin(ui.user)] [SSdynamic.admin_forcing_next_light ? "forced" : "reset"] the next light ruleset chance.")
return TRUE
if("max_heavy_chance")
SSdynamic.admin_forcing_next_heavy = !SSdynamic.admin_forcing_next_heavy
message_admins("[key_name_admin(ui.user)] [SSdynamic.admin_forcing_next_heavy ? "forced" : "reset"] the next heavy ruleset chance.")
log_admin("[key_name_admin(ui.user)] [SSdynamic.admin_forcing_next_heavy ? "forced" : "reset"] the next heavy ruleset chance.")
return TRUE
if("max_latejoin_chance")
SSdynamic.admin_forcing_next_latejoin = !SSdynamic.admin_forcing_next_latejoin
message_admins("[key_name_admin(ui.user)] [SSdynamic.admin_forcing_next_latejoin ? "forced" : "reset"] the next latejoin ruleset chance.")
log_admin("[key_name_admin(ui.user)] [SSdynamic.admin_forcing_next_latejoin ? "forced" : "reset"] the next latejoin ruleset chance.")
return TRUE
if("light_start_now")
COOLDOWN_RESET(SSdynamic, light_ruleset_start)
message_admins("[key_name_admin(ui.user)] reset the light ruleset start cooldown.")
log_admin("[key_name_admin(ui.user)] reset the light ruleset start cooldown.")
return TRUE
if("heavy_start_now")
COOLDOWN_RESET(SSdynamic, heavy_ruleset_start)
message_admins("[key_name_admin(ui.user)] reset the heavy ruleset start cooldown.")
log_admin("[key_name_admin(ui.user)] reset the heavy ruleset start cooldown.")
return TRUE
if("latejoin_start_now")
COOLDOWN_RESET(SSdynamic, latejoin_ruleset_start)
message_admins("[key_name_admin(ui.user)] reset the latejoin ruleset start cooldown.")
log_admin("[key_name_admin(ui.user)] reset the latejoin ruleset start cooldown.")
return TRUE
if("reset_midround_cooldown")
COOLDOWN_RESET(SSdynamic, midround_cooldown)
message_admins("[key_name_admin(ui.user)] reset the midround cooldown.")
log_admin("[key_name_admin(ui.user)] reset the midround cooldown.")
return TRUE
if("reset_latejoin_cooldown")
COOLDOWN_RESET(SSdynamic, latejoin_cooldown)
message_admins("[key_name_admin(ui.user)] reset the latejoin cooldown.")
log_admin("[key_name_admin(ui.user)] reset the latejoin cooldown.")
return TRUE
if("hide_ruleset")
var/index = params["ruleset_index"]
if(length(SSdynamic.executed_rulesets) < index)
return
var/datum/dynamic_ruleset/ruleset = SSdynamic.executed_rulesets[index]
if(!ruleset)
return
if(ruleset in SSdynamic.unreported_rulesets)
SSdynamic.unreported_rulesets -= ruleset
message_admins("[key_name_admin(ui.user)] hid [ruleset] from the roundend report.")
log_admin("[key_name_admin(ui.user)] hid [ruleset] from the roundend report.")
else
SSdynamic.unreported_rulesets += ruleset
message_admins("[key_name_admin(ui.user)] unhid [ruleset] from the roundend report.")
log_admin("[key_name_admin(ui.user)] unhid [ruleset] from the roundend report.")
return TRUE
if("toggle_antag_events")
SSdynamic.antag_events_enabled = !SSdynamic.antag_events_enabled
message_admins("[key_name_admin(ui.user)] [SSdynamic.antag_events_enabled ? "enabled" : "disabled"] antag events.")
log_admin("[key_name_admin(ui.user)] [SSdynamic.antag_events_enabled ? "enabled" : "disabled"] antag events.")
return TRUE

View File

@@ -1,25 +0,0 @@
/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

View File

@@ -1,100 +0,0 @@
/// 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
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()

View File

@@ -1,108 +0,0 @@
/// 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 (!mid_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
mid_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))

View File

@@ -0,0 +1,128 @@
/datum/dynamic_ruleset/latejoin
min_antag_cap = 1
max_antag_cap = 1
repeatable = TRUE
/datum/dynamic_ruleset/latejoin/set_config_value(nvar, nval)
if(nvar == NAMEOF(src, min_antag_cap) || nvar == NAMEOF(src, max_antag_cap))
return FALSE
return ..()
/datum/dynamic_ruleset/latejoin/vv_edit_var(var_name, var_value)
if(var_name == NAMEOF(src, min_antag_cap) || var_name == NAMEOF(src, max_antag_cap))
return FALSE
return ..()
/datum/dynamic_ruleset/latejoin/is_valid_candidate(mob/candidate, client/candidate_client)
if(isnull(candidate.mind))
return FALSE
if(candidate.mind.assigned_role.title in get_blacklisted_roles())
return FALSE
return ..()
/datum/dynamic_ruleset/latejoin/traitor
name = "Traitor"
config_tag = "Latejoin Traitor"
preview_antag_datum = /datum/antagonist/traitor
pref_flag = ROLE_SYNDICATE_INFILTRATOR
jobban_flag = ROLE_TRAITOR
weight = 10
min_pop = 3
blacklisted_roles = list(
JOB_HEAD_OF_PERSONNEL,
)
/datum/dynamic_ruleset/latejoin/traitor/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/traitor)
/datum/dynamic_ruleset/latejoin/heretic
name = "Heretic"
config_tag = "Latejoin Heretic"
preview_antag_datum = /datum/antagonist/heretic
pref_flag = ROLE_HERETIC_SMUGGLER
jobban_flag = ROLE_HERETIC
weight = 3
min_pop = 30 // Ensures good spread of sacrifice targets
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_HERETIC_SACRIFICE)
blacklisted_roles = list(
JOB_HEAD_OF_PERSONNEL,
)
/datum/dynamic_ruleset/latejoin/heretic/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/heretic)
/datum/dynamic_ruleset/latejoin/changeling
name = "Changeling"
config_tag = "Latejoin Changeling"
preview_antag_datum = /datum/antagonist/changeling
pref_flag = ROLE_STOWAWAY_CHANGELING
jobban_flag = ROLE_CHANGELING
weight = 3
min_pop = 15
blacklisted_roles = list(
JOB_HEAD_OF_PERSONNEL,
)
/datum/dynamic_ruleset/latejoin/changeling/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/changeling)
/datum/dynamic_ruleset/latejoin/revolution
name = "Revolution"
config_tag = "Latejoin Revolution"
preview_antag_datum = /datum/antagonist/rev/head
pref_flag = ROLE_PROVOCATEUR
jobban_flag = ROLE_REV_HEAD
ruleset_flags = RULESET_HIGH_IMPACT
weight = 1
min_pop = 30
repeatable = FALSE
/// How many heads of staff are required to be on the station for this to be selected
var/heads_necessary = 3
/datum/dynamic_ruleset/latejoin/revolution/can_be_selected()
if(GLOB.revolution_handler)
return FALSE
var/head_check = 0
for(var/mob/player as anything in get_active_player_list(alive_check = TRUE, afk_check = TRUE))
if (player.mind.assigned_role.job_flags & JOB_HEAD_OF_STAFF)
head_check++
return head_check >= heads_necessary
/datum/dynamic_ruleset/latejoin/revolution/get_always_blacklisted_roles()
. = ..()
for(var/datum/job/job as anything in SSjob.all_occupations)
if(job.job_flags & JOB_HEAD_OF_STAFF)
. |= job.title
/datum/dynamic_ruleset/latejoin/revolution/assign_role(datum/mind/candidate)
LAZYADD(candidate.special_roles, "Dormant Head Revolutionary")
addtimer(CALLBACK(src, PROC_REF(reveal_head), candidate), 1 MINUTES, TIMER_DELETE_ME)
/datum/dynamic_ruleset/latejoin/revolution/proc/reveal_head(datum/mind/candidate)
LAZYREMOVE(candidate.special_roles, "Dormant Head Revolutionary")
var/head_check = 0
for(var/mob/player as anything in get_active_player_list(alive_check = TRUE, afk_check = TRUE))
if(player.mind?.assigned_role.job_flags & JOB_HEAD_OF_STAFF)
head_check++
if(head_check < heads_necessary - 1) // little bit of leeway
SSdynamic.unreported_rulesets += src
name += " (Canceled)"
log_dynamic("[config_tag]: Not enough heads of staff were present to start a revolution.")
return
if(!can_be_headrev(candidate))
SSdynamic.unreported_rulesets += src
name += " (Canceled)"
log_dynamic("[config_tag]: [key_name(candidate)] was ineligible after the timer expired. Ruleset canceled.")
message_admins("[config_tag]: [key_name(candidate)] was ineligible after the timer expired. Ruleset canceled.")
return
GLOB.revolution_handler ||= new()
var/datum/antagonist/rev/head/new_head = new()
new_head.give_flash = TRUE
new_head.give_hud = TRUE
new_head.remove_clumsy = TRUE
candidate.add_antag_datum(new_head, GLOB.revolution_handler.revs)
GLOB.revolution_handler.start_revolution()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,433 @@
/datum/dynamic_ruleset/roundstart
// We can pick multiple of a roundstart ruleset to "scale up" (spawn more of the same type of antag)
// Set this to FALSE if you DON'T want this ruleset to "scale up"
repeatable = TRUE
/// If TRUE, the ruleset will be the only one selected for roundstart
var/solo = FALSE
/datum/dynamic_ruleset/roundstart/is_valid_candidate(mob/candidate, client/candidate_client)
if(isnull(candidate.mind))
return FALSE
// Checks that any other roundstart ruleset hasn't already picked this guy
for(var/datum/dynamic_ruleset/roundstart/ruleset as anything in SSdynamic.queued_rulesets)
if(candidate.mind in ruleset.selected_minds)
return FALSE
return ..()
/// Helpful proc - to use if your ruleset forces a job - which ensures a candidate can play the passed job typepath
/datum/dynamic_ruleset/roundstart/proc/ruleset_forced_job_check(mob/candidate, client/candidate_client, datum/job/job_typepath)
// Malf AI can only go to people who want to be AI
if(!candidate_client.prefs.job_preferences[job_typepath::title])
return FALSE
// And only to people who can actually be AI this round
if(SSjob.check_job_eligibility(candidate, SSjob.get_job_type(job_typepath), "[name] Candidacy") != JOB_AVAILABLE)
return FALSE
// (Something else forced us to play a job that isn't AI)
var/forced_job = LAZYACCESS(SSjob.forced_occupations, candidate)
if(forced_job && forced_job != job_typepath)
return FALSE
// (Something else forced us NOT to play AI)
if(job_typepath::title in LAZYACCESS(SSjob.prevented_occupations, candidate))
return FALSE
return TRUE
/datum/dynamic_ruleset/roundstart/traitor
name = "Traitors"
config_tag = "Roundstart Traitor"
preview_antag_datum = /datum/antagonist/traitor
pref_flag = ROLE_TRAITOR
weight = 10
min_pop = 3
max_antag_cap = list("denominator" = 38)
/datum/dynamic_ruleset/roundstart/traitor/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/traitor)
/datum/dynamic_ruleset/roundstart/malf_ai
name = "Malfunctioning AI"
config_tag = "Roundstart Malfunctioning AI"
pref_flag = ROLE_MALF
preview_antag_datum = /datum/antagonist/malf_ai
ruleset_flags = RULESET_HIGH_IMPACT
weight = list(
DYNAMIC_TIER_LOW = 0,
DYNAMIC_TIER_LOWMEDIUM = 1,
DYNAMIC_TIER_MEDIUMHIGH = 3,
DYNAMIC_TIER_HIGH = 3,
)
min_pop = 30
max_antag_cap = 1
repeatable = FALSE
/datum/dynamic_ruleset/roundstart/malf_ai/get_always_blacklisted_roles()
return list()
/datum/dynamic_ruleset/roundstart/malf_ai/is_valid_candidate(mob/candidate, client/candidate_client)
return ..() && ruleset_forced_job_check(candidate, candidate_client, /datum/job/ai)
/datum/dynamic_ruleset/roundstart/malf_ai/prepare_for_role(datum/mind/candidate)
LAZYSET(SSjob.forced_occupations, candidate, /datum/job/ai)
/datum/dynamic_ruleset/roundstart/malf_ai/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/malf_ai)
/datum/dynamic_ruleset/roundstart/malf_ai/can_be_selected()
return ..() && !HAS_TRAIT(SSstation, STATION_TRAIT_HUMAN_AI)
/datum/dynamic_ruleset/roundstart/blood_brother
name = "Blood Brothers"
config_tag = "Roundstart Blood Brothers"
preview_antag_datum = /datum/antagonist/brother
pref_flag = ROLE_BROTHER
weight = 5
max_antag_cap = list("denominator" = 29)
min_pop = 10
/datum/dynamic_ruleset/roundstart/blood_brother/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/brother)
/datum/dynamic_ruleset/roundstart/changeling
name = "Changelings"
config_tag = "Roundstart Changeling"
preview_antag_datum = /datum/antagonist/changeling
pref_flag = ROLE_CHANGELING
weight = 3
min_pop = 15
max_antag_cap = list("denominator" = 29)
/datum/dynamic_ruleset/roundstart/changeling/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/changeling)
/datum/dynamic_ruleset/roundstart/heretic
name = "Heretics"
config_tag = "Roundstart Heretics"
preview_antag_datum = /datum/antagonist/heretic
pref_flag = ROLE_HERETIC
weight = 3
max_antag_cap = list("denominator" = 24)
min_pop = 30 // Ensures good spread of sacrifice targets
/datum/dynamic_ruleset/roundstart/heretic/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/heretic)
/datum/dynamic_ruleset/roundstart/wizard
name = "Wizard"
config_tag = "Roundstart Wizard"
preview_antag_datum = /datum/antagonist/wizard
pref_flag = ROLE_WIZARD
ruleset_flags = RULESET_INVADER|RULESET_HIGH_IMPACT
weight = list(
DYNAMIC_TIER_LOW = 0,
DYNAMIC_TIER_LOWMEDIUM = 0,
DYNAMIC_TIER_MEDIUMHIGH = 1,
DYNAMIC_TIER_HIGH = 2,
)
max_antag_cap = 1
min_pop = 30
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_WIZARDDEN)
repeatable = FALSE
/datum/dynamic_ruleset/roundstart/wizard/prepare_for_role(datum/mind/candidate)
LAZYSET(SSjob.forced_occupations, candidate, /datum/job/space_wizard)
/datum/dynamic_ruleset/roundstart/wizard/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/wizard) // moves to lair for us
/datum/dynamic_ruleset/roundstart/wizard/round_result()
for(var/datum/mind/wiz as anything in selected_minds)
if(considered_alive(wiz) && !considered_exiled(wiz))
return FALSE
SSticker.news_report = WIZARD_KILLED
return TRUE
/datum/dynamic_ruleset/roundstart/blood_cult
name = "Blood Cult"
config_tag = "Roundstart Blood Cult"
preview_antag_datum = /datum/antagonist/cult
pref_flag = ROLE_CULTIST
ruleset_flags = RULESET_HIGH_IMPACT
weight = list(
DYNAMIC_TIER_LOW = 0,
DYNAMIC_TIER_LOWMEDIUM = 1,
DYNAMIC_TIER_MEDIUMHIGH = 3,
DYNAMIC_TIER_HIGH = 3,
)
min_pop = 30
blacklisted_roles = list(
JOB_HEAD_OF_PERSONNEL,
)
min_antag_cap = list("denominator" = 20, "offset" = 1)
repeatable = FALSE
/// Ratio of cultists getting on the shuttle to be considered a minor win
var/ratio_to_be_considered_escaped = 0.5
/datum/dynamic_ruleset/roundstart/blood_cult/get_always_blacklisted_roles()
return ..() | JOB_CHAPLAIN
/datum/dynamic_ruleset/roundstart/blood_cult/create_execute_args()
return list(
new /datum/team/cult(),
get_most_experienced(selected_minds, pref_flag),
)
/datum/dynamic_ruleset/roundstart/blood_cult/execute()
. = ..()
// future todo, find a cleaner way to get this from execute args
var/datum/team/cult/main_cult = locate() in GLOB.antagonist_teams
main_cult.setup_objectives()
/datum/dynamic_ruleset/roundstart/blood_cult/assign_role(datum/mind/candidate, datum/team/cult/cult, datum/mind/most_experienced)
var/datum/antagonist/cult/cultist = new()
cultist.give_equipment = TRUE
candidate.add_antag_datum(cultist, cult)
if(most_experienced == candidate)
cultist.make_cult_leader()
/datum/dynamic_ruleset/roundstart/blood_cult/round_result()
var/datum/team/cult/main_cult = locate() in GLOB.antagonist_teams
if(main_cult.check_cult_victory())
SSticker.mode_result = "win - cult win"
SSticker.news_report = CULT_SUMMON
return TRUE
var/num_cultists = main_cult.size_at_maximum || 100
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.mode_result = "loss - staff stopped the cult"
SSticker.news_report = (escaped_cultists / num_cultists) >= ratio_to_be_considered_escaped ? CULT_ESCAPE : CULT_FAILURE
return TRUE
/datum/dynamic_ruleset/roundstart/nukies
name = "Nuclear Operatives"
config_tag = "Roundstart Nukeops"
preview_antag_datum = /datum/antagonist/nukeop
pref_flag = ROLE_OPERATIVE
ruleset_flags = RULESET_INVADER|RULESET_HIGH_IMPACT
weight = list(
DYNAMIC_TIER_LOW = 0,
DYNAMIC_TIER_LOWMEDIUM = 1,
DYNAMIC_TIER_MEDIUMHIGH = 3,
DYNAMIC_TIER_HIGH = 3,
)
min_pop = 30
min_antag_cap = list("denominator" = 18, "offset" = 1)
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE)
repeatable = FALSE
/datum/dynamic_ruleset/roundstart/nukies/prepare_for_role(datum/mind/candidate)
LAZYSET(SSjob.forced_occupations, candidate, /datum/job/nuclear_operative)
/datum/dynamic_ruleset/roundstart/nukies/create_execute_args()
return list(
new /datum/team/nuclear(),
get_most_experienced(selected_minds, pref_flag),
)
/datum/dynamic_ruleset/roundstart/nukies/assign_role(datum/mind/candidate, datum/team/nuke_team, datum/mind/most_experienced)
if(most_experienced == candidate)
candidate.add_antag_datum(/datum/antagonist/nukeop/leader, nuke_team)
else
candidate.add_antag_datum(/datum/antagonist/nukeop, nuke_team)
/datum/dynamic_ruleset/roundstart/nukies/round_result()
var/datum/antagonist/nukeop/nukie = selected_minds[1].has_antag_datum(/datum/antagonist/nukeop)
var/datum/team/nuclear/nuke_team = nukie.get_team()
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
/datum/dynamic_ruleset/roundstart/nukies/clown
name = "Clown Operatives"
config_tag = "Roundstart Clownops"
preview_antag_datum = /datum/antagonist/nukeop/clownop
pref_flag = ROLE_CLOWN_OPERATIVE
weight = 0
/datum/dynamic_ruleset/roundstart/nukies/clown/prepare_for_role(datum/mind/candidate)
LAZYSET(SSjob.forced_occupations, candidate, /datum/job/nuclear_operative/clown_operative)
/datum/dynamic_ruleset/roundstart/nukies/clown/assign_role(datum/mind/candidate, datum/team/nuke_team, datum/mind/most_experienced)
if(most_experienced == candidate)
candidate.add_antag_datum(/datum/antagonist/nukeop/leader/clownop)
else
candidate.add_antag_datum(/datum/antagonist/nukeop/clownop)
/datum/dynamic_ruleset/roundstart/revolution
name = "Revolution"
config_tag = "Roundstart Revolution"
preview_antag_datum = /datum/antagonist/rev/head
pref_flag = ROLE_REV_HEAD
ruleset_flags = RULESET_HIGH_IMPACT
weight = list(
DYNAMIC_TIER_LOW = 0,
DYNAMIC_TIER_LOWMEDIUM = 1,
DYNAMIC_TIER_MEDIUMHIGH = 3,
DYNAMIC_TIER_HIGH = 3,
)
min_pop = 30
min_antag_cap = 1
max_antag_cap = 3
repeatable = FALSE
/// If we have fewer heads of staff than this 7 minutes into the round, we'll cancel the revolution
var/heads_necessary = 2
/datum/dynamic_ruleset/roundstart/revolution/get_always_blacklisted_roles()
. = ..()
for(var/datum/job/job as anything in SSjob.all_occupations)
if(job.job_flags & JOB_HEAD_OF_STAFF)
. |= job.title
/datum/dynamic_ruleset/roundstart/revolution/assign_role(datum/mind/candidate)
LAZYADD(candidate.special_roles, "Dormant Head Revolutionary")
addtimer(CALLBACK(src, PROC_REF(reveal_head), candidate), 7 MINUTES, TIMER_DELETE_ME)
/// Reveals the headrev after a set amount of time
/datum/dynamic_ruleset/roundstart/revolution/proc/reveal_head(datum/mind/candidate)
LAZYREMOVE(candidate.special_roles, "Dormant Head Revolutionary")
var/head_check = 0
for(var/mob/player as anything in get_active_player_list(alive_check = TRUE, afk_check = TRUE))
if(player.mind?.assigned_role.job_flags & JOB_HEAD_OF_STAFF)
head_check++
if(head_check < heads_necessary)
log_dynamic("[config_tag]: Not enough heads of staff were present to start a revolution.")
addtimer(CALLBACK(src, PROC_REF(revs_execution_failed)), 1 MINUTES, TIMER_UNIQUE|TIMER_DELETE_ME)
return
if(!can_be_headrev(candidate))
log_dynamic("[config_tag]: [key_name(candidate)] was not eligible to be a headrev after the timer expired - finding a replacement.")
find_another_headrev()
return
GLOB.revolution_handler ||= new()
var/datum/antagonist/rev/head/new_head = new()
new_head.give_flash = TRUE
new_head.give_hud = TRUE
new_head.remove_clumsy = TRUE
candidate.add_antag_datum(new_head, GLOB.revolution_handler.revs)
GLOB.revolution_handler.start_revolution()
/datum/dynamic_ruleset/roundstart/revolution/proc/find_another_headrev()
for(var/mob/living/carbon/human/upstanding_citizen in GLOB.player_list)
if(!can_be_headrev(upstanding_citizen.mind))
continue
reveal_head(upstanding_citizen.mind)
log_dynamic("[config_tag]: [key_name(upstanding_citizen)] was selected as a replacement headrev.")
return
log_dynamic("[config_tag]: Failed to find a replacement headrev.")
addtimer(CALLBACK(src, PROC_REF(revs_execution_failed)), 1 MINUTES, TIMER_UNIQUE|TIMER_DELETE_ME)
/datum/dynamic_ruleset/roundstart/revolution/proc/revs_execution_failed()
if(GLOB.revolution_handler)
return
// Execution is effectively cancelled by this point, but it's not like we can go back and refund it
SSdynamic.unreported_rulesets += src
name += " (Canceled)"
log_dynamic("[config_tag]: All headrevs were ineligible after the timer expired, and no replacements could be found. Ruleset canceled.")
message_admins("[config_tag]: All headrevs were ineligible after the timer expired, and no replacements could be found. Ruleset canceled.")
/datum/dynamic_ruleset/roundstart/spies
name = "Spies"
config_tag = "Roundstart Spies"
preview_antag_datum = /datum/antagonist/spy
pref_flag = ROLE_SPY
weight = list(
DYNAMIC_TIER_LOW = 0,
DYNAMIC_TIER_LOWMEDIUM = 1,
DYNAMIC_TIER_MEDIUMHIGH = 3,
DYNAMIC_TIER_HIGH = 3,
)
min_pop = 10
min_antag_cap = list("denominator" = 20, "offset" = 1)
/datum/dynamic_ruleset/roundstart/spies/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/spy)
/datum/dynamic_ruleset/roundstart/extended
name = "Extended"
config_tag = "Extended"
weight = 0
min_antag_cap = 0
repeatable = FALSE
solo = TRUE
/datum/dynamic_ruleset/roundstart/extended/execute()
// No midrounds no latejoins
for(var/category in SSdynamic.rulesets_to_spawn)
SSdynamic.rulesets_to_spawn[category] = 0
/datum/dynamic_ruleset/roundstart/meteor
name = "Meteor"
config_tag = "Meteor"
weight = 0
min_antag_cap = 0
repeatable = FALSE
/datum/dynamic_ruleset/roundstart/meteor/execute()
GLOB.meteor_mode ||= new()
GLOB.meteor_mode.start_meteor()
/datum/dynamic_ruleset/roundstart/nations
name = "Nations"
config_tag = "Nations"
weight = 0
min_antag_cap = 0
repeatable = FALSE
solo = TRUE
/datum/dynamic_ruleset/roundstart/nations/execute()
// No midrounds no latejoins
for(var/category in SSdynamic.rulesets_to_spawn)
SSdynamic.rulesets_to_spawn[category] = 0
//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

View File

@@ -1,300 +0,0 @@
/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
/// In what categories is this ruleset allowed to run? Used by station traits
var/ruleset_category = RULESET_CATEGORY_DEFAULT
/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)
SHOULD_NOT_OVERRIDE(TRUE)
if (!scaling_cost)
return 0
var/antag_fraction = 0
for(var/datum/dynamic_ruleset/ruleset as anything in (SSdynamic.executed_rules + list(src))) // we care about the antags we *will* assign, too
antag_fraction += ruleset.get_antag_cap_scaling_included(population) / SSdynamic.roundstart_pop_ready
for(var/i in 1 to max_scale)
if(antag_fraction < 0.25)
scaled_times += 1
antag_fraction += get_scaling_antag_cap(population) / SSdynamic.roundstart_pop_ready // we added new antags, gotta update the %
return scaled_times * scaling_cost
/// Returns how many more antags to add while scaling with a given population.
/// By default rulesets scale linearly, but you can override this to make them scale differently.
/datum/dynamic_ruleset/proc/get_scaling_antag_cap(population)
return get_antag_cap(population)
/// Returns what the antag cap with the given population is.
/datum/dynamic_ruleset/proc/get_antag_cap(population)
SHOULD_NOT_OVERRIDE(TRUE)
if (isnum(antag_cap))
return antag_cap
return CEILING(population / antag_cap["denominator"], 1) + (antag_cap["offset"] || 0)
/// Gets the 'final' antag cap for this ruleset, which is the base cap plus the scaled cap.
/datum/dynamic_ruleset/proc/get_antag_cap_scaling_included(population)
SHOULD_NOT_OVERRIDE(TRUE)
var/base_cap = get_antag_cap(population)
var/modded_cap = scaled_times * get_scaling_antag_cap(population)
return base_cap + modded_cap
/// 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 += "[gameTimestamp()]: [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.get_job(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 ..()

View File

@@ -1,254 +0,0 @@
//////////////////////////////////////////////
// //
// 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
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.job_flags & JOB_HEAD_OF_STAFF)
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_MIND_TRAIT(M.current, TRAIT_UNCONVERTABLE))
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 = 4
cost = 12
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,746 +0,0 @@
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)
. = ..()
for (var/i in 1 to get_antag_cap_scaling_included(population))
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.get_job_type(/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.get_job_type(/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/i in 1 to get_antag_cap_scaling_included(population))
var/mob/candidate = pick_n_take(candidates)
if (isnull(candidate))
break
assigned += candidate.mind
candidate.mind.restricted_roles = restricted_roles
candidate.mind.special_role = ROLE_BROTHER
GLOB.pre_setup_antags += candidate.mind
return TRUE
/datum/dynamic_ruleset/roundstart/traitorbro/execute()
for (var/datum/mind/mind in assigned)
new /datum/team/brother_team(mind)
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)
. = ..()
for (var/i in 1 to get_antag_cap_scaling_included(population))
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 = 16
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
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
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.get_job_type(/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()
var/datum/mind/most_experienced = get_most_experienced(assigned, antag_flag)
if(!most_experienced)
most_experienced = assigned[1]
var/datum/antagonist/cult/leader = most_experienced.has_antag_datum(/datum/antagonist/cult)
leader.make_cult_leader()
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
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
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
///The job type to dress up our nuclear operative as.
var/datum/job/job_type = /datum/job/nuclear_operative
/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.get_job_type(job_type))
M.mind.special_role = required_role
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_MIND_TRAIT(M.current, TRAIT_UNCONVERTABLE))
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 += "[gameTimestamp()]: 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
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
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
job_type = /datum/job/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)
//////////////////////////////////////////////
// //
// 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
/datum/dynamic_ruleset/roundstart/spies
name = "Spies"
antag_flag = ROLE_SPY
antag_datum = /datum/antagonist/spy
minimum_required_age = 0
protected_roles = list(
JOB_CAPTAIN,
JOB_DETECTIVE,
JOB_HEAD_OF_PERSONNEL, // AA = bad
JOB_HEAD_OF_SECURITY,
JOB_PRISONER,
JOB_SECURITY_OFFICER,
JOB_WARDEN,
)
restricted_roles = list(
JOB_AI,
JOB_CYBORG,
)
required_candidates = 3 // lives or dies by there being a few spies
weight = 5
cost = 8
scaling_cost = 4
minimum_players = 10
antag_cap = list("denominator" = 20, "offset" = 1)
requirements = list(8, 8, 8, 8, 8, 8, 8, 8, 8, 8)
/// What fraction is added to the antag cap for each additional scale
var/fraction_per_scale = 0.2
/datum/dynamic_ruleset/roundstart/spies/pre_execute(population)
for(var/i in 1 to get_antag_cap_scaling_included(population))
if(length(candidates) <= 0)
break
var/mob/picked_player = pick_n_take(candidates)
assigned += picked_player.mind
picked_player.mind.special_role = ROLE_SPY
picked_player.mind.restricted_roles = restricted_roles
GLOB.pre_setup_antags += picked_player.mind
return TRUE
// Scaling adds a fraction of the amount of additional spies rather than the full amount.
/datum/dynamic_ruleset/roundstart/spies/get_scaling_antag_cap(population)
return ceil(..() * fraction_per_scale)

View File

@@ -0,0 +1,111 @@
/// Verb to open the create command report window and send command reports.
ADMIN_VERB(dynamic_tester, R_DEBUG, "Dynamic Tester", "See dynamic probabilities.", ADMIN_CATEGORY_DEBUG)
BLACKBOX_LOG_ADMIN_VERB("Dynamic Tester")
var/datum/dynamic_tester/tgui = new()
tgui.ui_interact(user.mob)
/datum/dynamic_tester
/// Instances of every roundstart ruleset
var/list/roundstart_rulesets = list()
/// Instances of every midround ruleset
var/list/midround_rulesets = list()
/// A formatted report of the weights of each roundstart ruleset, refreshed occasionally and sent to the UI.
var/list/roundstart_ruleset_report = list()
/// A formatted report of the weights of each midround ruleset, refreshed occasionally and sent to the UI.
var/list/midround_ruleset_report = list()
/// What is the tier we are testing?
var/tier = 1
/// How many players are we testing with?
var/num_players = 10
/datum/dynamic_tester/New()
for(var/datum/dynamic_ruleset/rtype as anything in subtypesof(/datum/dynamic_ruleset/roundstart))
if(!initial(rtype.config_tag))
continue
var/datum/dynamic_ruleset/roundstart/created = new rtype(SSdynamic.get_config())
roundstart_rulesets += created
// snowflake so we can see headrev stats
if(istype(created, /datum/dynamic_ruleset/roundstart/revolution))
var/datum/dynamic_ruleset/roundstart/revolution/revs = created
revs.heads_necessary = 0
for(var/datum/dynamic_ruleset/rtype as anything in subtypesof(/datum/dynamic_ruleset/midround))
if(!initial(rtype.config_tag))
continue
var/datum/dynamic_ruleset/midround/created = new rtype(SSdynamic.get_config())
midround_rulesets += created
update_reports()
/datum/dynamic_tester/ui_state(mob/user)
return ADMIN_STATE(R_DEBUG)
/datum/dynamic_tester/ui_close()
qdel(src)
/datum/dynamic_tester/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "DynamicTester")
ui.open()
/datum/dynamic_tester/ui_static_data(mob/user)
var/list/data = list()
data["tier"] = tier
data["num_players"] = num_players
data["roundstart_ruleset_report"] = flatten_list(roundstart_ruleset_report)
data["midround_ruleset_report"] = flatten_list(midround_ruleset_report)
return data
/datum/dynamic_tester/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("set_num_players")
var/old_num = num_players
num_players = text2num(params["num_players"])
if(old_num != num_players)
update_reports()
return TRUE
if("set_tier")
var/old_tier = tier
tier = text2num(params["tier"])
if(old_tier != tier)
update_reports()
return TRUE
/datum/dynamic_tester/proc/update_reports()
roundstart_ruleset_report.Cut()
for(var/datum/dynamic_ruleset/roundstart/ruleset as anything in roundstart_rulesets)
var/comment = ""
if(istype(ruleset, /datum/dynamic_ruleset/roundstart/revolution))
var/datum/dynamic_ruleset/roundstart/revolution/revs = ruleset
comment = " (Assuming [initial(revs.heads_necessary)] heads of staff)"
roundstart_ruleset_report[ruleset] = list(
"name" = ruleset.name,
"weight" = ruleset.get_weight(num_players, tier),
"max_candidates" = ruleset.get_antag_cap(num_players, ruleset.max_antag_cap || ruleset.min_antag_cap),
"min_candidates" = ruleset.get_antag_cap(num_players, ruleset.min_antag_cap),
"comment" = comment,
)
midround_ruleset_report.Cut()
for(var/datum/dynamic_ruleset/midround/ruleset as anything in midround_rulesets)
midround_ruleset_report[ruleset] = list(
"name" = ruleset.name,
"weight" = ruleset.get_weight(num_players, tier),
"max_candidates" = ruleset.get_antag_cap(num_players, ruleset.max_antag_cap || ruleset.min_antag_cap),
"min_candidates" = ruleset.get_antag_cap(num_players, ruleset.min_antag_cap),
"comment" = ruleset.midround_type,
)
update_static_data_for_all_viewers()

View File

@@ -1,74 +0,0 @@
/// 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

View File

@@ -1,202 +0,0 @@
# 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:
![Flow chart to describe the chain of events for Dynamic 2021 to take](https://github.com/tgstation/documentation-assets/blob/main/dynamic/random_event_hijacking.png)
`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`.

View File

@@ -1,139 +0,0 @@
#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='byond://?src=[REF(src)];cancelmidround=[midround_injection_timer_id]'>CANCEL</a> | \
<a href='byond://?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, "[gameTimestamp()]: [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

View File

@@ -35,8 +35,10 @@ SUBSYSTEM_DEF(job)
var/list/level_order = list(JP_HIGH, JP_MEDIUM, JP_LOW) var/list/level_order = list(JP_HIGH, JP_MEDIUM, JP_LOW)
/// Lazylist of mob:occupation_string pairs. /// Lazylist of mob:occupation_string pairs. Forces mobs into certain occupations with highest priority.
var/list/dynamic_forced_occupations var/list/forced_occupations
/// Lazylist of mob:list(occupation_string) pairs. Prevents mobs from taking certain occupations at all.
var/list/prevented_occupations
/** /**
* Keys should be assigned job roles. Values should be >= 1. * Keys should be assigned job roles. Values should be >= 1.
@@ -316,7 +318,6 @@ SUBSYSTEM_DEF(job)
if(!player?.mind) if(!player?.mind)
continue continue
player.mind.set_assigned_role(get_job_type(/datum/job/unassigned)) player.mind.set_assigned_role(get_job_type(/datum/job/unassigned))
player.mind.special_role = null
setup_occupations() setup_occupations()
unassigned = list() unassigned = list()
if(CONFIG_GET(flag/load_jobs_from_txt)) if(CONFIG_GET(flag/load_jobs_from_txt))
@@ -409,9 +410,8 @@ SUBSYSTEM_DEF(job)
SEND_SIGNAL(src, COMSIG_OCCUPATIONS_DIVIDED, pure, allow_all) SEND_SIGNAL(src, COMSIG_OCCUPATIONS_DIVIDED, pure, allow_all)
//Get the players who are ready //Get the players who are ready
for(var/i in GLOB.new_player_list) for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
var/mob/dead/new_player/player = i if(player.ready == PLAYER_READY_TO_PLAY && player.check_job_preferences(!pure) && player.mind && is_unassigned_job(player.mind.assigned_role))
if(player.ready == PLAYER_READY_TO_PLAY && player.check_preferences() && player.mind && is_unassigned_job(player.mind.assigned_role))
unassigned += player unassigned += player
initial_players_to_assign = length(unassigned) initial_players_to_assign = length(unassigned)
@@ -698,9 +698,10 @@ SUBSYSTEM_DEF(job)
return 0 return 0
/datum/controller/subsystem/job/proc/try_reject_player(mob/dead/new_player/player) /datum/controller/subsystem/job/proc/try_reject_player(mob/dead/new_player/player)
if(player.mind && player.mind.special_role) for(var/datum/dynamic_ruleset/roundstart/ruleset in SSdynamic.queued_rulesets)
job_debug("RJCT: Player unable to be rejected due to special_role, Player: [player], SpecialRole: [player.mind.special_role]") if(player.mind in ruleset.selected_minds)
return FALSE job_debug("RJCT: Player unable to be rejected due to being selected by dynamic, Player: [player], Ruleset: [ruleset]")
return FALSE
job_debug("RJCT: Player rejected, Player: [player]") job_debug("RJCT: Player rejected, Player: [player]")
unassigned -= player unassigned -= player
@@ -871,11 +872,12 @@ SUBSYSTEM_DEF(job)
/// Assigns roles that are considered high priority, either due to dynamic needing to force a specific role for a specific ruleset /// Assigns roles that are considered high priority, either due to dynamic needing to force a specific role for a specific ruleset
/// or making sure roles critical to round progression exist where possible every shift. /// or making sure roles critical to round progression exist where possible every shift.
/datum/controller/subsystem/job/proc/assign_priority_positions() /datum/controller/subsystem/job/proc/assign_priority_positions()
job_debug("APP: Assigning Dynamic ruleset forced occupations: [length(dynamic_forced_occupations)]") job_debug("APP: Assigning Dynamic ruleset forced occupations: [LAZYLEN(forced_occupations)]")
for(var/mob/new_player in dynamic_forced_occupations) for(var/datum/mind/mind as anything in forced_occupations)
var/mob/dead/new_player = mind.current
// Eligibility checks already carried out as part of the dynamic ruleset trim_candidates proc. // Eligibility checks already carried out as part of the dynamic ruleset trim_candidates proc.
// However no guarantee of game state between then and now, so don't skip eligibility checks on assign_role. // However no guarantee of game state between then and now, so don't skip eligibility checks on assign_role.
assign_role(new_player, get_job(dynamic_forced_occupations[new_player])) assign_role(new_player, get_job_type(LAZYACCESS(forced_occupations, mind)))
// Get JP_HIGH department Heads of Staff in place. Indirectly useful for the Revolution ruleset to have as many Heads as possible. // Get JP_HIGH department Heads of Staff in place. Indirectly useful for the Revolution ruleset to have as many Heads as possible.
job_debug("APP: Assigning all JP_HIGH head of staff roles.") job_debug("APP: Assigning all JP_HIGH head of staff roles.")
@@ -940,7 +942,7 @@ SUBSYSTEM_DEF(job)
job_debug("[debug_prefix]: Player has no mind, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") job_debug("[debug_prefix]: Player has no mind, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
return JOB_UNAVAILABLE_GENERIC return JOB_UNAVAILABLE_GENERIC
if(possible_job.title in player.mind.restricted_roles) if(possible_job.title in LAZYACCESS(prevented_occupations, player.mind))
job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_ANTAG_INCOMPAT, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_ANTAG_INCOMPAT, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
return JOB_UNAVAILABLE_ANTAG_INCOMPAT return JOB_UNAVAILABLE_ANTAG_INCOMPAT
@@ -959,7 +961,8 @@ SUBSYSTEM_DEF(job)
return JOB_UNAVAILABLE_BANNED return JOB_UNAVAILABLE_BANNED
// Check for character age // Check for character age
if(possible_job.required_character_age > player.client.prefs.read_preference(/datum/preference/numeric/age) && possible_job.required_character_age != null) var/client/player_client = GET_CLIENT(player)
if(isnum(possible_job.required_character_age) && possible_job.required_character_age > player_client.prefs.read_preference(/datum/preference/numeric/age))
job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_AGE)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_AGE)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
return JOB_UNAVAILABLE_AGE return JOB_UNAVAILABLE_AGE

View File

@@ -285,11 +285,10 @@ SUBSYSTEM_DEF(polling)
if(the_ignore_category) if(the_ignore_category)
if(potential_candidate.ckey in GLOB.poll_ignore[the_ignore_category]) if(potential_candidate.ckey in GLOB.poll_ignore[the_ignore_category])
return FALSE return FALSE
if(role) if(role && potential_candidate.client)
if(!(role in potential_candidate.client.prefs.be_special)) if(!(role in potential_candidate.client.prefs.be_special))
return FALSE return FALSE
var/required_time = GLOB.special_roles[role] || 0 if(potential_candidate.client.get_days_to_play_antag(role) > 0)
if(potential_candidate.client && potential_candidate.client.get_remaining_days(required_time) > 0)
return FALSE return FALSE
if(check_jobban) if(check_jobban)

View File

@@ -224,7 +224,7 @@ SUBSYSTEM_DEF(ticker)
return TRUE return TRUE
if(GLOB.station_was_nuked) if(GLOB.station_was_nuked)
return TRUE return TRUE
if(GLOB.revolutionary_win) if(GLOB.revolution_handler?.result == REVOLUTION_VICTORY)
return TRUE return TRUE
return FALSE return FALSE
@@ -235,7 +235,7 @@ SUBSYSTEM_DEF(ticker)
CHECK_TICK CHECK_TICK
//Configure mode and assign player to antagonists //Configure mode and assign player to antagonists
var/can_continue = FALSE var/can_continue = FALSE
can_continue = SSdynamic.pre_setup() //Choose antagonists can_continue = SSdynamic.select_roundstart_antagonists() //Choose antagonists
CHECK_TICK CHECK_TICK
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PRE_JOBS_ASSIGNED, src) SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PRE_JOBS_ASSIGNED, src)
can_continue = can_continue && SSjob.divide_occupations() //Distribute jobs can_continue = can_continue && SSjob.divide_occupations() //Distribute jobs
@@ -301,7 +301,37 @@ SUBSYSTEM_DEF(ticker)
/datum/controller/subsystem/ticker/proc/PostSetup() /datum/controller/subsystem/ticker/proc/PostSetup()
set waitfor = FALSE set waitfor = FALSE
SSdynamic.post_setup()
// Spawn traitors and stuff
for(var/datum/dynamic_ruleset/roundstart/ruleset in SSdynamic.queued_rulesets)
ruleset.execute()
SSdynamic.queued_rulesets -= ruleset
SSdynamic.executed_rulesets += ruleset
// Queue roundstart intercept report
if(!CONFIG_GET(flag/no_intercept_report))
GLOB.communications_controller.queue_roundstart_report()
// Queue admin logout report
addtimer(CALLBACK(src, PROC_REF(display_roundstart_logout_report)), ROUNDSTART_LOGOUT_REPORT_TIME)
// Queue suicide slot handling
if(CONFIG_GET(flag/reopen_roundstart_suicide_roles))
var/delay = (CONFIG_GET(number/reopen_roundstart_suicide_roles_delay) * 1 SECONDS) || 4 MINUTES
addtimer(CALLBACK(src, PROC_REF(reopen_roundstart_suicide_roles)), delay)
// Handle database
if(SSdbcore.Connect())
var/list/to_set = list()
var/arguments = list()
if(GLOB.revdata.originmastercommit)
to_set += "commit_hash = :commit_hash"
arguments["commit_hash"] = GLOB.revdata.originmastercommit
if(to_set.len)
arguments["round_id"] = GLOB.round_id
var/datum/db_query/query_round_game_mode = SSdbcore.NewQuery(
"UPDATE [format_table_name("round")] SET [to_set.Join(", ")] WHERE id = :round_id",
arguments
)
query_round_game_mode.Execute()
qdel(query_round_game_mode)
GLOB.start_state = new /datum/station_state() GLOB.start_state = new /datum/station_state()
GLOB.start_state.count() GLOB.start_state.count()
@@ -328,11 +358,97 @@ SUBSYSTEM_DEF(ticker)
if(!iter_human.hardcore_survival_score) if(!iter_human.hardcore_survival_score)
continue continue
if(iter_human.mind?.special_role) if(iter_human.is_antag())
to_chat(iter_human, span_notice("You will gain [round(iter_human.hardcore_survival_score) * 2] hardcore random points if you greentext this round!")) to_chat(iter_human, span_notice("You will gain [round(iter_human.hardcore_survival_score) * 2] hardcore random points if you greentext this round!"))
else else
to_chat(iter_human, span_notice("You will gain [round(iter_human.hardcore_survival_score)] hardcore random points if you survive this round!")) to_chat(iter_human, span_notice("You will gain [round(iter_human.hardcore_survival_score)] hardcore random points if you survive this round!"))
/datum/controller/subsystem/ticker/proc/display_roundstart_logout_report()
var/list/msg = list("[span_boldnotice("Roundstart logout report")]\n\n")
for(var/i in GLOB.mob_living_list)
var/mob/living/L = i
var/mob/living/carbon/C = L
if (istype(C) && !C.last_mind)
continue // never had a client
if(L.ckey && !GLOB.directory[L.ckey])
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (<font color='#ffcc00'><b>Disconnected</b></font>)\n"
if(L.ckey && L.client)
var/failed = FALSE
if(L.client.inactivity >= ROUNDSTART_LOGOUT_AFK_THRESHOLD) //Connected, but inactive (alt+tabbed or something)
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (<font color='#ffcc00'><b>Connected, Inactive</b></font>)\n"
failed = TRUE //AFK client
if(!failed && L.stat)
if(HAS_TRAIT(L, TRAIT_SUICIDED)) //Suicider
msg += "<b>[L.name]</b> ([L.key]), the [L.job] ([span_bolddanger("Suicide")])\n"
failed = TRUE //Disconnected client
if(!failed && (L.stat == UNCONSCIOUS || L.stat == HARD_CRIT))
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (Dying)\n"
failed = TRUE //Unconscious
if(!failed && L.stat == DEAD)
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (Dead)\n"
failed = TRUE //Dead
continue //Happy connected client
for(var/mob/dead/observer/D in GLOB.dead_mob_list)
if(D.mind && D.mind.current == L)
if(L.stat == DEAD)
if(HAS_TRAIT(L, TRAIT_SUICIDED)) //Suicider
msg += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] ([span_bolddanger("Suicide")])\n"
continue //Disconnected client
else
msg += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] (Dead)\n"
continue //Dead mob, ghost abandoned
else
if(D.can_reenter_corpse)
continue //Adminghost, or cult/wizard ghost
else
msg += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] ([span_bolddanger("Ghosted")])\n"
continue //Ghosted while alive
var/concatenated_message = msg.Join()
log_admin(concatenated_message)
to_chat(GLOB.admins, concatenated_message)
/datum/controller/subsystem/ticker/proc/reopen_roundstart_suicide_roles()
var/include_command = CONFIG_GET(flag/reopen_roundstart_suicide_roles_command_positions)
var/list/reopened_jobs = list()
for(var/mob/living/quitter in GLOB.suicided_mob_list)
var/datum/job/job = SSjob.get_job(quitter.job)
if(!job || !(job.job_flags & JOB_REOPEN_ON_ROUNDSTART_LOSS))
continue
if(!include_command && job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
continue
job.current_positions = max(job.current_positions - 1, 0)
reopened_jobs += quitter.job
if(CONFIG_GET(flag/reopen_roundstart_suicide_roles_command_report))
if(reopened_jobs.len)
var/reopened_job_report_positions
for(var/dead_dudes_job in reopened_jobs)
reopened_job_report_positions = "[reopened_job_report_positions ? "[reopened_job_report_positions]\n":""][dead_dudes_job]"
var/suicide_command_report = {"
<font size = 3><b>[command_name()] Human Resources Board</b><br>
Notice of Personnel Change</font><hr>
To personnel management staff aboard [station_name()]:<br><br>
Our medical staff have detected a series of anomalies in the vital sensors
of some of the staff aboard your station.<br><br>
Further investigation into the situation on our end resulted in us discovering
a series of rather... unforturnate decisions that were made on the part of said staff.<br><br>
As such, we have taken the liberty to automatically reopen employment opportunities for the positions of the crew members
who have decided not to partake in our research. We will be forwarding their cases to our employment review board
to determine their eligibility for continued service with the company (and of course the
continued storage of cloning records within the central medical backup server.)<br><br>
<i>The following positions have been reopened on our behalf:<br><br>
[reopened_job_report_positions]</i>
"}
print_command_report(suicide_command_report, "Central Command Personnel Update")
//These callbacks will fire after roundstart key transfer //These callbacks will fire after roundstart key transfer
/datum/controller/subsystem/ticker/proc/OnRoundstart(datum/callback/cb) /datum/controller/subsystem/ticker/proc/OnRoundstart(datum/callback/cb)
if(!HasRoundStarted()) if(!HasRoundStarted())

View File

@@ -257,11 +257,11 @@ GLOBAL_VAR(round_default_lawset)
return FALSE return FALSE
// If the owner is an antag (has a special role) they also shouldn't be wiped // If the owner is an antag (has a special role) they also shouldn't be wiped
if(owner?.mind?.special_role) if(owner?.is_antag())
return FALSE return FALSE
if (isAI(owner)) if (isAI(owner))
var/mob/living/silicon/ai/ai_owner = owner var/mob/living/silicon/ai/ai_owner = owner
if(ai_owner.deployed_shell?.mind?.special_role) if(ai_owner.deployed_shell?.is_antag())
return FALSE return FALSE
zeroth = null zeroth = null

View File

@@ -20,6 +20,11 @@ GLOBAL_DATUM_INIT(communications_controller, /datum/communciations_controller, n
/// The location where the special xenomorph egg was planted /// The location where the special xenomorph egg was planted
var/area/captivity_area var/area/captivity_area
/// What is the lower bound of when the roundstart announcement is sent out?
var/waittime_l = 60 SECONDS
/// What is the higher bound of when the roundstart announcement is sent out?
var/waittime_h = 180 SECONDS
/datum/communciations_controller/proc/can_announce(mob/living/user, is_silicon) /datum/communciations_controller/proc/can_announce(mob/living/user, is_silicon)
if(is_silicon && COOLDOWN_FINISHED(src, silicon_message_cooldown)) if(is_silicon && COOLDOWN_FINISHED(src, silicon_message_cooldown))
return TRUE return TRUE
@@ -58,6 +63,75 @@ GLOBAL_DATUM_INIT(communications_controller, /datum/communciations_controller, n
printed_paper.add_raw_text(sending.content) printed_paper.add_raw_text(sending.content)
printed_paper.update_appearance() printed_paper.update_appearance()
// Called AFTER everyone is equipped with their job
/datum/communciations_controller/proc/queue_roundstart_report()
addtimer(CALLBACK(src, PROC_REF(send_roundstart_report)), rand(waittime_l, waittime_h))
/datum/communciations_controller/proc/send_roundstart_report(greenshift)
if(block_command_report) //If we don't want the report to be printed just yet, we put it off until it's ready
addtimer(CALLBACK(src, PROC_REF(send_roundstart_report), greenshift), 10 SECONDS)
return
var/dynamic_report = SSdynamic.get_advisory_report()
if(isnull(greenshift)) // if we're not forced to be greenshift or not - check if we are an actual greenshift
greenshift = SSdynamic.current_tier.tier == 0 && dynamic_report == /datum/dynamic_tier/greenshift::advisory_report
. = "<b><i>Nanotrasen Department of Intelligence Threat Advisory, Spinward Sector, TCD [time2text(world.realtime, "DDD, MMM DD")], [CURRENT_STATION_YEAR]:</i></b><hr>"
. += dynamic_report
SSstation.generate_station_goals(greenshift ? INFINITY : CONFIG_GET(number/station_goal_budget))
var/list/datum/station_goal/goals = SSstation.get_station_goals()
if(length(goals))
var/list/texts = list("<hr><b>Special Orders for [station_name()]:</b><br>")
for(var/datum/station_goal/station_goal as anything in goals)
station_goal.on_report()
texts += station_goal.get_report()
. += texts.Join("<hr>")
var/list/trait_list_strings = list()
for(var/datum/station_trait/station_trait as anything in SSstation.station_traits)
if(!station_trait.show_in_report)
continue
trait_list_strings += "[station_trait.get_report()]<BR>"
if(trait_list_strings.len > 0)
. += "<hr><b>Identified shift divergencies:</b><BR>" + trait_list_strings.Join()
if(length(command_report_footnotes))
var/footnote_pile = ""
for(var/datum/command_footnote/footnote as anything in command_report_footnotes)
footnote_pile += "[footnote.message]<BR>"
footnote_pile += "<i>[footnote.signature]</i><BR>"
footnote_pile += "<BR>"
. += "<hr><b>Additional Notes: </b><BR><BR>" + footnote_pile
#ifndef MAP_TEST
print_command_report(., "[command_name()] Status Summary", announce=FALSE)
if(greenshift)
priority_announce(
"Thanks to the tireless efforts of our security and intelligence divisions, \
there are currently no credible threats to [station_name()]. \
All station construction projects have been authorized. Have a secure shift!",
"Security Report",
SSstation.announcer.get_rand_report_sound(),
color_override = "green",
)
else
if(SSsecurity_level.get_current_level_as_number() < SEC_LEVEL_BLUE)
SSsecurity_level.set_level(SEC_LEVEL_BLUE, announce = FALSE)
priority_announce(
"[SSsecurity_level.current_security_level.elevating_to_announcement]\n\n\
A summary has been copied and printed to all communications consoles.",
"Security level elevated.",
ANNOUNCER_INTERCEPT,
color_override = SSsecurity_level.current_security_level.announcement_color,
)
#endif
return .
#undef COMMUNICATION_COOLDOWN #undef COMMUNICATION_COOLDOWN
#undef COMMUNICATION_COOLDOWN_AI #undef COMMUNICATION_COOLDOWN_AI
#undef COMMUNICATION_COOLDOWN_MEETING #undef COMMUNICATION_COOLDOWN_MEETING

View File

@@ -48,8 +48,6 @@
/// Job datum indicating the mind's role. This should always exist after initialization, as a reference to a singleton. /// Job datum indicating the mind's role. This should always exist after initialization, as a reference to a singleton.
var/datum/job/assigned_role var/datum/job/assigned_role
var/special_role
var/list/restricted_roles = list()
/// List of antag datums on this mind /// List of antag datums on this mind
var/list/antag_datums var/list/antag_datums
@@ -89,7 +87,9 @@
///Skill multiplier list, just slap your multiplier change onto this with the type it is coming from as key. ///Skill multiplier list, just slap your multiplier change onto this with the type it is coming from as key.
var/list/experience_multiplier_reasons = list() var/list/experience_multiplier_reasons = list()
/// A lazy list of statuses to add next to this mind in the traitor panel /// A lazy list of roles to display that this mind has, stuff like "Traitor" or "Special Creature"
var/list/special_roles
/// A lazy list of statuses to display that this mind has, stuff like "Infected" or "Mindshielded"
var/list/special_statuses var/list/special_statuses
///Assoc list of addiction values, key is the type of withdrawal (as singleton type), and the value is the amount of addiction points (as number) ///Assoc list of addiction values, key is the type of withdrawal (as singleton type), and the value is the amount of addiction points (as number)
@@ -124,7 +124,7 @@
.["memories"] = memories .["memories"] = memories
.["antag_datums"] = antag_datums .["antag_datums"] = antag_datums
.["holy_role"] = holy_role .["holy_role"] = holy_role
.["special_role"] = special_role .["special_role"] = jointext(get_special_roles(), " | ")
.["assigned_role"] = assigned_role.title .["assigned_role"] = assigned_role.title
.["current"] = current .["current"] = current
@@ -485,16 +485,6 @@
var/datum/addiction/affected_addiction = SSaddiction.all_addictions[type] var/datum/addiction/affected_addiction = SSaddiction.all_addictions[type]
return affected_addiction.on_lose_addiction_points(src) return affected_addiction.on_lose_addiction_points(src)
/// Whether or not we can roll for midrounds, specifically checking if we have any major antag datums that should block it
/datum/mind/proc/can_roll_midround(datum/antagonist/antag_type)
if(SEND_SIGNAL(current, COMSIG_MOB_MIND_BEFORE_MIDROUND_ROLL, src, antag_type) & CANCEL_ROLL)
return FALSE
for(var/datum/antagonist/antag as anything in antag_datums)
if(antag.block_midrounds)
return FALSE
return TRUE
/// Setter for the assigned_role job datum. /// Setter for the assigned_role job datum.
/datum/mind/proc/set_assigned_role(datum/job/new_role) /datum/mind/proc/set_assigned_role(datum/job/new_role)
if(assigned_role == new_role) if(assigned_role == new_role)

View File

@@ -60,37 +60,6 @@
return TRUE return TRUE
return FALSE return FALSE
/*
Removes antag type's references from a mind.
objectives, uplinks, powers etc are all handled.
*/
/datum/mind/proc/remove_changeling()
var/datum/antagonist/changeling/C = has_antag_datum(/datum/antagonist/changeling)
if(C)
remove_antag_datum(/datum/antagonist/changeling)
special_role = null
/datum/mind/proc/remove_traitor()
remove_antag_datum(/datum/antagonist/traitor)
/datum/mind/proc/remove_nukeop()
var/datum/antagonist/nukeop/nuke = has_antag_datum(/datum/antagonist/nukeop,TRUE)
if(nuke)
remove_antag_datum(nuke.type)
special_role = null
/datum/mind/proc/remove_wizard()
remove_antag_datum(/datum/antagonist/wizard)
special_role = null
/datum/mind/proc/remove_rev()
var/datum/antagonist/rev/rev = has_antag_datum(/datum/antagonist/rev)
if(rev)
remove_antag_datum(rev.type)
special_role = null
/datum/mind/proc/remove_antag_equip() /datum/mind/proc/remove_antag_equip()
if(!current) if(!current)
return return
@@ -226,7 +195,7 @@
current.log_message("has been enslaved to [key_name(creator)].", LOG_GAME) current.log_message("has been enslaved to [key_name(creator)].", LOG_GAME)
log_admin("[key_name(current)] has been enslaved to [key_name(creator)].") log_admin("[key_name(current)] has been enslaved to [key_name(creator)].")
if(creator.mind?.special_role) if(creator.is_antag())
message_admins("[ADMIN_LOOKUPFLW(current)] has been created by [ADMIN_LOOKUPFLW(creator)], an antagonist.") message_admins("[ADMIN_LOOKUPFLW(current)] has been created by [ADMIN_LOOKUPFLW(creator)], an antagonist.")
to_chat(current, span_userdanger("Despite your creator's current allegiances, your true master remains [creator.real_name]. If their loyalties change, so do yours. This will never change unless your creator's body is destroyed.")) to_chat(current, span_userdanger("Despite your creator's current allegiances, your true master remains [creator.real_name]. If their loyalties change, so do yours. This will never change unless your creator's body is destroyed."))
@@ -273,33 +242,12 @@
/datum/mind/proc/take_uplink() /datum/mind/proc/take_uplink()
qdel(find_syndicate_uplink()) qdel(find_syndicate_uplink())
/datum/mind/proc/make_traitor()
if(!(has_antag_datum(/datum/antagonist/traitor)))
add_antag_datum(/datum/antagonist/traitor)
/datum/mind/proc/make_changeling()
var/datum/antagonist/changeling/C = has_antag_datum(/datum/antagonist/changeling)
if(!C)
C = add_antag_datum(/datum/antagonist/changeling)
special_role = ROLE_CHANGELING
return C
/datum/mind/proc/make_wizard() /datum/mind/proc/make_wizard()
if(has_antag_datum(/datum/antagonist/wizard)) if(has_antag_datum(/datum/antagonist/wizard))
return return
set_assigned_role(SSjob.get_job_type(/datum/job/space_wizard)) set_assigned_role(SSjob.get_job_type(/datum/job/space_wizard))
special_role = ROLE_WIZARD
add_antag_datum(/datum/antagonist/wizard) add_antag_datum(/datum/antagonist/wizard)
/datum/mind/proc/make_rev()
var/datum/antagonist/rev/head/head = new()
head.give_flash = TRUE
head.give_hud = TRUE
add_antag_datum(head)
special_role = ROLE_REV_HEAD
/// Sets our can_hijack to the fastest speed our antag datums allow. /// Sets our can_hijack to the fastest speed our antag datums allow.
/datum/mind/proc/get_hijack_speed() /datum/mind/proc/get_hijack_speed()
. = 0 . = 0

View File

@@ -36,7 +36,6 @@
/mob/living/silicon/pai/mind_initialize() /mob/living/silicon/pai/mind_initialize()
. = ..() . = ..()
mind.set_assigned_role(SSjob.get_job_type(/datum/job/personal_ai)) mind.set_assigned_role(SSjob.get_job_type(/datum/job/personal_ai))
mind.special_role = ""
/// Signal proc for [COMSIG_ADMIN_DELETING], to ghostize a mob beforehand if an admin is manually deleting it. /// Signal proc for [COMSIG_ADMIN_DELETING], to ghostize a mob beforehand if an admin is manually deleting it.
/mob/proc/ghost_before_admin_delete(datum/source) /mob/proc/ghost_before_admin_delete(datum/source)

View File

@@ -272,7 +272,7 @@ GLOBAL_LIST_EMPTY(heretic_arenas)
replace_banned = FALSE replace_banned = FALSE
objectives = list() objectives = list()
antag_hud_name = "brainwashed" antag_hud_name = "brainwashed"
block_midrounds = FALSE antag_flags = ANTAG_FAKE
/datum/antagonist/heretic_arena_participant/on_gain() /datum/antagonist/heretic_arena_participant/on_gain()
forge_objectives() forge_objectives()

View File

@@ -33,10 +33,6 @@ GLOBAL_LIST_EMPTY(lobby_station_traits)
var/list/lobby_buttons = list() var/list/lobby_buttons = list()
/// The ID that we look for in dynamic.json. Not synced with 'name' because I can already see this go wrong /// The ID that we look for in dynamic.json. Not synced with 'name' because I can already see this go wrong
var/dynamic_threat_id var/dynamic_threat_id
/// If ran during dynamic, do we reduce the total threat? Will be overridden by config if set
var/threat_reduction = 0
/// Which ruleset flags to allow dynamic to use. NONE to disregard
var/dynamic_category = NONE
/// Trait should not be instantiated in a round if its type matches this type /// Trait should not be instantiated in a round if its type matches this type
var/abstract_type = /datum/station_trait var/abstract_type = /datum/station_trait
@@ -45,10 +41,6 @@ GLOBAL_LIST_EMPTY(lobby_station_traits)
RegisterSignal(SSticker, COMSIG_TICKER_ROUND_STARTING, PROC_REF(on_round_start)) RegisterSignal(SSticker, COMSIG_TICKER_ROUND_STARTING, PROC_REF(on_round_start))
if(threat_reduction)
GLOB.dynamic_station_traits[src] = threat_reduction
if(dynamic_category)
GLOB.dynamic_ruleset_categories = dynamic_category
if(sign_up_button) if(sign_up_button)
GLOB.lobby_station_traits += src GLOB.lobby_station_traits += src
if(SSstation.initialized) if(SSstation.initialized)
@@ -62,7 +54,6 @@ GLOBAL_LIST_EMPTY(lobby_station_traits)
destroy_lobby_buttons() destroy_lobby_buttons()
SSstation.station_traits -= src SSstation.station_traits -= src
GLOB.lobby_station_traits -= src GLOB.lobby_station_traits -= src
GLOB.dynamic_station_traits -= src
REMOVE_TRAIT(SSstation, trait_to_give, STATION_TRAIT) REMOVE_TRAIT(SSstation, trait_to_give, STATION_TRAIT)
return ..() return ..()

View File

@@ -1,7 +1,3 @@
#define CAN_ROLL_ALWAYS 1 //always can roll for antag
#define CAN_ROLL_PROTECTED 2 //can roll if config lets protected roles roll
#define CAN_ROLL_NEVER 3 //never roll antag
/** /**
* A station trait which enables a temporary job * A station trait which enables a temporary job
* Generally speaking these should always all be mutually exclusive, don't have too many at once * Generally speaking these should always all be mutually exclusive, don't have too many at once
@@ -11,8 +7,6 @@
abstract_type = /datum/station_trait/job abstract_type = /datum/station_trait/job
/// What tooltip to show on the button /// What tooltip to show on the button
var/button_desc = "Sign up to gain some kind of unusual job, not available in most rounds." var/button_desc = "Sign up to gain some kind of unusual job, not available in most rounds."
/// Can this job roll antag?
var/can_roll_antag = CAN_ROLL_ALWAYS
/// How many positions to spawn? /// How many positions to spawn?
var/position_amount = 1 var/position_amount = 1
/// Type of job to enable /// Type of job to enable
@@ -22,11 +16,6 @@
/datum/station_trait/job/New() /datum/station_trait/job/New()
. = ..() . = ..()
switch(can_roll_antag)
if(CAN_ROLL_PROTECTED)
SSstation.antag_protected_roles += job_to_add::title
if(CAN_ROLL_NEVER)
SSstation.antag_restricted_roles += job_to_add::title
blacklist += subtypesof(/datum/station_trait/job) - type // All but ourselves blacklist += subtypesof(/datum/station_trait/job) - type // All but ourselves
RegisterSignal(SSdcs, COMSIG_GLOB_PRE_JOBS_ASSIGNED, PROC_REF(pre_jobs_assigned)) RegisterSignal(SSdcs, COMSIG_GLOB_PRE_JOBS_ASSIGNED, PROC_REF(pre_jobs_assigned))
@@ -86,7 +75,6 @@
button_desc = "Sign up to become the Cargo Gorilla, a peaceful shepherd of boxes." button_desc = "Sign up to become the Cargo Gorilla, a peaceful shepherd of boxes."
weight = 1 weight = 1
show_in_report = FALSE // Selective attention test. Did you spot the gorilla? show_in_report = FALSE // Selective attention test. Did you spot the gorilla?
can_roll_antag = CAN_ROLL_NEVER
job_to_add = /datum/job/cargo_gorilla job_to_add = /datum/job/cargo_gorilla
/datum/station_trait/job/cargorilla/New() /datum/station_trait/job/cargorilla/New()
@@ -119,7 +107,6 @@
weight = 2 weight = 2
report_message = "We have installed a Bridge Assistant on your station." report_message = "We have installed a Bridge Assistant on your station."
show_in_report = TRUE show_in_report = TRUE
can_roll_antag = CAN_ROLL_PROTECTED
job_to_add = /datum/job/bridge_assistant job_to_add = /datum/job/bridge_assistant
/datum/station_trait/job/bridge_assistant/New() /datum/station_trait/job/bridge_assistant/New()
@@ -173,7 +160,6 @@
weight = 2 weight = 2
report_message = "Veteran Security Advisor has been assigned to your station to help with Security matters." report_message = "Veteran Security Advisor has been assigned to your station to help with Security matters."
show_in_report = TRUE show_in_report = TRUE
can_roll_antag = CAN_ROLL_PROTECTED
job_to_add = /datum/job/veteran_advisor job_to_add = /datum/job/veteran_advisor
/datum/station_trait/job/veteran_advisor/on_lobby_button_update_overlays(atom/movable/screen/lobby/button/sign_up/lobby_button, list/overlays) /datum/station_trait/job/veteran_advisor/on_lobby_button_update_overlays(atom/movable/screen/lobby/button/sign_up/lobby_button, list/overlays)
@@ -187,7 +173,6 @@
trait_flags = parent_type::trait_flags | STATION_TRAIT_REQUIRES_AI trait_flags = parent_type::trait_flags | STATION_TRAIT_REQUIRES_AI
report_message = "Our recent technological advancements in machine Artificial Intelligence has proven futile. In the meantime, we're sending an Intern to help out." report_message = "Our recent technological advancements in machine Artificial Intelligence has proven futile. In the meantime, we're sending an Intern to help out."
show_in_report = TRUE show_in_report = TRUE
can_roll_antag = CAN_ROLL_PROTECTED
job_to_add = /datum/job/human_ai job_to_add = /datum/job/human_ai
trait_to_give = STATION_TRAIT_HUMAN_AI trait_to_give = STATION_TRAIT_HUMAN_AI
@@ -253,7 +238,6 @@
weight = 0 //Unrollable by default, available all day during monkey day. weight = 0 //Unrollable by default, available all day during monkey day.
report_message = "We've evaluated the bartender's monkey to have the mental capacity of the average crewmember. As such, we made them one." report_message = "We've evaluated the bartender's monkey to have the mental capacity of the average crewmember. As such, we made them one."
show_in_report = TRUE show_in_report = TRUE
can_roll_antag = CAN_ROLL_ALWAYS
job_to_add = /datum/job/pun_pun job_to_add = /datum/job/pun_pun
/datum/station_trait/job/pun_pun/New() /datum/station_trait/job/pun_pun/New()
@@ -267,7 +251,3 @@
/datum/station_trait/job/pun_pun/on_lobby_button_update_overlays(atom/movable/screen/lobby/button/sign_up/lobby_button, list/overlays) /datum/station_trait/job/pun_pun/on_lobby_button_update_overlays(atom/movable/screen/lobby/button/sign_up/lobby_button, list/overlays)
. = ..() . = ..()
overlays += LAZYFIND(lobby_candidates, lobby_button.get_mob()) ? "pun_pun_on" : "pun_pun_off" overlays += LAZYFIND(lobby_candidates, lobby_button.get_mob()) ? "pun_pun_on" : "pun_pun_off"
#undef CAN_ROLL_ALWAYS
#undef CAN_ROLL_PROTECTED
#undef CAN_ROLL_NEVER

View File

@@ -559,7 +559,6 @@
trait_to_give = STATION_TRAIT_RADIOACTIVE_NEBULA trait_to_give = STATION_TRAIT_RADIOACTIVE_NEBULA
blacklist = list(/datum/station_trait/random_event_weight_modifier/rad_storms) blacklist = list(/datum/station_trait/random_event_weight_modifier/rad_storms)
threat_reduction = 30
dynamic_threat_id = "Radioactive Nebula" dynamic_threat_id = "Radioactive Nebula"
intensity_increment_time = 5 MINUTES intensity_increment_time = 5 MINUTES

View File

@@ -501,16 +501,27 @@
/// Crew don't ever spawn as enemies of the station. Obsesseds, blob infection, space changelings etc can still happen though /// Crew don't ever spawn as enemies of the station. Obsesseds, blob infection, space changelings etc can still happen though
/datum/station_trait/background_checks /datum/station_trait/background_checks
name = "Station-Wide Background Checks" name = "Station-Wide Background Checks"
report_message = "We replaced the intern doing your crew's background checks with a trained screener for this shift! That said, our enemies may just find another way to infiltrate the station, so be careful." report_message = "We replaced the intern doing your crew's background checks with a trained screener for this shift! \
That said, our enemies may just find another way to infiltrate the station, so be careful."
trait_type = STATION_TRAIT_NEUTRAL trait_type = STATION_TRAIT_NEUTRAL
weight = 1 weight = 1
show_in_report = TRUE show_in_report = TRUE
can_revert = FALSE can_revert = FALSE
dynamic_category = RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
threat_reduction = 15
dynamic_threat_id = "Background Checks" dynamic_threat_id = "Background Checks"
/datum/station_trait/background_checks/New()
. = ..()
RegisterSignal(SSdynamic, COMSIG_DYNAMIC_PRE_ROUNDSTART, PROC_REF(modify_config))
/datum/station_trait/background_checks/proc/modify_config(datum/source, list/dynamic_config)
SIGNAL_HANDLER
for(var/datum/dynamic_ruleset/ruleset as anything in subtypesof(/datum/dynamic_ruleset))
if(ruleset.ruleset_flags & RULESET_INVADER)
continue
dynamic_config[initial(ruleset.config_tag)] ||= list()
dynamic_config[initial(ruleset.config_tag)][NAMEOF(ruleset, weight)] = 0
/datum/station_trait/pet_day /datum/station_trait/pet_day
name = "Bring Your Pet To Work Day" name = "Bring Your Pet To Work Day"

View File

@@ -230,7 +230,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list
/datum/objective/assassinate/update_explanation_text() /datum/objective/assassinate/update_explanation_text()
..() ..()
if(target?.current) if(target?.current)
explanation_text = "Assassinate [target.name], the [!target_role_type ? target.assigned_role.title : target.special_role]." explanation_text = "Assassinate [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())]."
else else
explanation_text = "Free objective." explanation_text = "Free objective."
@@ -280,7 +280,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list
/datum/objective/mutiny/update_explanation_text() /datum/objective/mutiny/update_explanation_text()
..() ..()
if(target?.current) if(target?.current)
explanation_text = "Assassinate or exile [target.name], the [!target_role_type ? target.assigned_role.title : target.special_role]." explanation_text = "Assassinate or exile [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())]."
else else
explanation_text = "Free objective." explanation_text = "Free objective."
@@ -302,7 +302,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list
/datum/objective/maroon/update_explanation_text() /datum/objective/maroon/update_explanation_text()
if(target?.current) if(target?.current)
explanation_text = "Prevent [target.name], the [!target_role_type ? target.assigned_role.title : target.special_role], from escaping alive." explanation_text = "Prevent [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())], from escaping alive."
else else
explanation_text = "Free objective." explanation_text = "Free objective."
@@ -333,7 +333,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list
/datum/objective/debrain/update_explanation_text() /datum/objective/debrain/update_explanation_text()
..() ..()
if(target?.current) if(target?.current)
explanation_text = "Steal the brain of [target.name], the [!target_role_type ? target.assigned_role.title : target.special_role]." explanation_text = "Steal the brain of [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())]."
else else
explanation_text = "Free objective." explanation_text = "Free objective."
@@ -359,7 +359,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list
/datum/objective/protect/update_explanation_text() /datum/objective/protect/update_explanation_text()
..() ..()
if(target?.current) if(target?.current)
explanation_text = "Protect [target.name], the [!target_role_type ? target.assigned_role.title : target.special_role]." explanation_text = "Protect [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())]."
else else
explanation_text = "Free objective." explanation_text = "Free objective."
@@ -384,7 +384,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list
/datum/objective/jailbreak/update_explanation_text() /datum/objective/jailbreak/update_explanation_text()
..() ..()
if(target?.current) if(target?.current)
explanation_text = "Ensure that [target.name], the [!target_role_type ? target.assigned_role.title : target.special_role] escapes alive and out of custody." explanation_text = "Ensure that [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())] escapes alive and out of custody."
else else
explanation_text = "Free objective." explanation_text = "Free objective."
@@ -400,7 +400,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list
/datum/objective/jailbreak/detain/update_explanation_text() /datum/objective/jailbreak/detain/update_explanation_text()
..() ..()
if(target?.current) if(target?.current)
explanation_text = "Ensure that [target.name], the [!target_role_type ? target.assigned_role.title : target.special_role] is delivered to Nanotrasen alive and in custody." explanation_text = "Ensure that [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())] is delivered to Nanotrasen alive and in custody."
else else
explanation_text = "Free objective." explanation_text = "Free objective."

View File

@@ -789,7 +789,6 @@
#define HACK_PIRATE "Pirates" #define HACK_PIRATE "Pirates"
#define HACK_FUGITIVES "Fugitives" #define HACK_FUGITIVES "Fugitives"
#define HACK_SLEEPER "Sleeper Agents" #define HACK_SLEEPER "Sleeper Agents"
#define HACK_THREAT "Threat Boost"
/// The minimum number of ghosts / observers to have the chance of spawning pirates. /// The minimum number of ghosts / observers to have the chance of spawning pirates.
#define MIN_GHOSTS_FOR_PIRATES 4 #define MIN_GHOSTS_FOR_PIRATES 4
@@ -833,7 +832,7 @@
*/ */
/obj/machinery/computer/communications/proc/hack_console(mob/living/hacker) /obj/machinery/computer/communications/proc/hack_console(mob/living/hacker)
// All hack results we'll choose from. // All hack results we'll choose from.
var/list/hack_options = list(HACK_THREAT) var/list/hack_options = list(HACK_SLEEPER)
// If we have a certain amount of ghosts, we'll add some more !!fun!! options to the list // If we have a certain amount of ghosts, we'll add some more !!fun!! options to the list
var/num_ghosts = length(GLOB.current_observers_list) + length(GLOB.dead_player_list) var/num_ghosts = length(GLOB.current_observers_list) + length(GLOB.dead_player_list)
@@ -848,11 +847,6 @@
if(num_ghosts >= MIN_GHOSTS_FOR_FUGITIVES) if(num_ghosts >= MIN_GHOSTS_FOR_FUGITIVES)
hack_options += HACK_FUGITIVES hack_options += HACK_FUGITIVES
if (!EMERGENCY_PAST_POINT_OF_NO_RETURN)
// If less than a certain percent of the population is ghosts, consider sleeper agents
if(num_ghosts < (length(GLOB.clients) * MAX_PERCENT_GHOSTS_FOR_SLEEPER))
hack_options += HACK_SLEEPER
var/picked_option = pick(hack_options) var/picked_option = pick(hack_options)
message_admins("[ADMIN_LOOKUPFLW(hacker)] hacked a [name] located at [ADMIN_VERBOSEJMP(src)], resulting in: [picked_option]!") message_admins("[ADMIN_LOOKUPFLW(hacker)] hacked a [name] located at [ADMIN_VERBOSEJMP(src)], resulting in: [picked_option]!")
hacker.log_message("hacked a communications console, resulting in: [picked_option].", LOG_GAME, log_globally = TRUE) hacker.log_message("hacked a communications console, resulting in: [picked_option].", LOG_GAME, log_globally = TRUE)
@@ -860,59 +854,29 @@
if(HACK_PIRATE) // Triggers pirates, which the crew may be able to pay off to prevent if(HACK_PIRATE) // Triggers pirates, which the crew may be able to pay off to prevent
var/list/pirate_rulesets = list( var/list/pirate_rulesets = list(
/datum/dynamic_ruleset/midround/pirates, /datum/dynamic_ruleset/midround/pirates,
/datum/dynamic_ruleset/midround/dangerous_pirates, /datum/dynamic_ruleset/midround/pirates/heavy,
) )
priority_announce( SSdynamic.force_run_midround(pick(pirate_rulesets))
"Attention crew: sector monitoring reports a massive jump-trace from an enemy vessel destined for your system. Prepare for imminent hostile contact.",
"[command_name()] High-Priority Update",
)
SSdynamic.picking_specific_rule(pick(pirate_rulesets), forced = TRUE, ignore_cost = TRUE)
if(HACK_FUGITIVES) // Triggers fugitives, which can cause confusion / chaos as the crew decides which side help if(HACK_FUGITIVES) // Triggers fugitives, which can cause confusion / chaos as the crew decides which side help
priority_announce( priority_announce(
"Attention crew: sector monitoring reports a jump-trace from an unidentified vessel destined for your system. Prepare for probable contact.", "Attention crew: sector monitoring reports a jump-trace from an unidentified vessel destined for your system. Prepare for probable contact.",
"[command_name()] High-Priority Update", "[command_name()] High-Priority Update",
) )
SSdynamic.force_run_midround(/datum/dynamic_ruleset/midround/from_ghosts/fugitives)
force_event_after(/datum/round_event_control/fugitives, "[hacker] hacking a communications console", rand(20 SECONDS, 1 MINUTES))
if(HACK_THREAT) // Force an unfavorable situation on the crew
priority_announce(
"Attention crew, the Nanotrasen Department of Intelligence has received intel suggesting increased enemy activity in your sector beyond that initially reported in today's threat advisory.",
"[command_name()] High-Priority Update",
)
for(var/mob/crew_member as anything in GLOB.player_list)
if(!is_station_level(crew_member.z))
continue
shake_camera(crew_member, 15, 1)
SSdynamic.unfavorable_situation()
if(HACK_SLEEPER) // Trigger one or multiple sleeper agents with the crew (or for latejoining crew) if(HACK_SLEEPER) // Trigger one or multiple sleeper agents with the crew (or for latejoining crew)
var/datum/dynamic_ruleset/midround/sleeper_agent_type = /datum/dynamic_ruleset/midround/from_living/autotraitor priority_announce(
var/max_number_of_sleepers = clamp(round(length(GLOB.alive_player_list) / 20), 1, 3) "Attention crew, it appears that someone on your station has hijacked your telecommunications and broadcasted an unknown signal.",
var/num_agents_created = 0 "[command_name()] High-Priority Update",
for(var/num_agents in 1 to rand(1, max_number_of_sleepers)) )
if(!SSdynamic.picking_specific_rule(sleeper_agent_type, forced = TRUE, ignore_cost = TRUE)) var/max_number_of_sleepers = clamp(round(length(GLOB.alive_player_list) / 40), 1, 3)
break if(!SSdynamic.force_run_midround(/datum/dynamic_ruleset/midround/from_living/traitor, forced_max_cap = max_number_of_sleepers))
num_agents_created++ SSdynamic.queue_ruleset(/datum/dynamic_ruleset/latejoin/traitor)
if(num_agents_created <= 0)
// We failed to run any midround sleeper agents, so let's be patient and run latejoin traitor
SSdynamic.picking_specific_rule(/datum/dynamic_ruleset/latejoin/infiltrator, forced = TRUE, ignore_cost = TRUE)
else
// We spawned some sleeper agents, nice - give them a report to kickstart the paranoia
priority_announce(
"Attention crew, it appears that someone on your station has hijacked your telecommunications and broadcasted an unknown signal.",
"[command_name()] High-Priority Update",
)
#undef HACK_PIRATE #undef HACK_PIRATE
#undef HACK_FUGITIVES #undef HACK_FUGITIVES
#undef HACK_SLEEPER #undef HACK_SLEEPER
#undef HACK_THREAT
#undef MIN_GHOSTS_FOR_PIRATES #undef MIN_GHOSTS_FOR_PIRATES
#undef MIN_GHOSTS_FOR_FUGITIVES #undef MIN_GHOSTS_FOR_FUGITIVES

View File

@@ -22,7 +22,7 @@
to_chat(user, span_boldnotice("You feel a dark stirring inside of the Wish Granter, something you want nothing of. Your instincts are better than any man's.")) to_chat(user, span_boldnotice("You feel a dark stirring inside of the Wish Granter, something you want nothing of. Your instincts are better than any man's."))
return return
else if(is_special_character(user)) else if(user.is_antag())
to_chat(user, span_boldnotice("Even to a heart as dark as yours, you know nothing good will come of this. Something instinctual makes you pull away.")) to_chat(user, span_boldnotice("Even to a heart as dark as yours, you know nothing good will come of this. Something instinctual makes you pull away."))
else if (!insisting) else if (!insisting)

View File

@@ -54,7 +54,6 @@
if(isnull(chosen_one)) if(isnull(chosen_one))
return return
pyro.PossessByPlayer(chosen_one.key) pyro.PossessByPlayer(chosen_one.key)
pyro.mind.special_role = ROLE_PYROCLASTIC_SLIME
pyro.mind.add_antag_datum(/datum/antagonist/pyro_slime) pyro.mind.add_antag_datum(/datum/antagonist/pyro_slime)
pyro.log_message("was made into a slime by pyroclastic anomaly", LOG_GAME) pyro.log_message("was made into a slime by pyroclastic anomaly", LOG_GAME)
chosen_one = null chosen_one = null

View File

@@ -18,7 +18,7 @@
/// You can use any spraypaint can on a quirk poster to turn it into a contraband poster from the traitor objective /// You can use any spraypaint can on a quirk poster to turn it into a contraband poster from the traitor objective
/obj/item/poster/quirk/attackby(obj/item/postertool, mob/user, list/modifiers, list/attack_modifiers) /obj/item/poster/quirk/attackby(obj/item/postertool, mob/user, list/modifiers, list/attack_modifiers)
if(!is_special_character(user) || !HAS_TRAIT(user, TRAIT_POSTERBOY) || !istype(postertool, /obj/item/toy/crayon)) if(!user.is_antag() || !HAS_TRAIT(user, TRAIT_POSTERBOY) || !istype(postertool, /obj/item/toy/crayon))
return ..() return ..()
balloon_alert(user, "converting poster...") balloon_alert(user, "converting poster...")
if(!do_after(user, 5 SECONDS, user)) if(!do_after(user, 5 SECONDS, user))
@@ -32,7 +32,7 @@
/// Screentip for the above /// Screentip for the above
/obj/item/poster/quirk/add_context(atom/source, list/context, obj/item/held_item, mob/user) /obj/item/poster/quirk/add_context(atom/source, list/context, obj/item/held_item, mob/user)
if(!is_special_character(user) || !HAS_TRAIT(user, TRAIT_POSTERBOY) || !istype(held_item, /obj/item/toy/crayon)) if(!user.is_antag() || !HAS_TRAIT(user, TRAIT_POSTERBOY) || !istype(held_item, /obj/item/toy/crayon))
return NONE return NONE
context[SCREENTIP_CONTEXT_LMB] = "Turn into Demoralizing Poster" context[SCREENTIP_CONTEXT_LMB] = "Turn into Demoralizing Poster"
return CONTEXTUAL_SCREENTIP_SET return CONTEXTUAL_SCREENTIP_SET

View File

@@ -57,7 +57,7 @@
var/datum/antagonist/nukeop/nuke_datum = new() var/datum/antagonist/nukeop/nuke_datum = new()
nuke_datum.send_to_spawnpoint = FALSE nuke_datum.send_to_spawnpoint = FALSE
new_ai.mind.add_antag_datum(nuke_datum, op_datum.nuke_team) new_ai.mind.add_antag_datum(nuke_datum, op_datum.nuke_team)
new_ai.mind.special_role = "Syndicate AI" LAZYADD(new_ai.mind.special_roles, "Syndicate AI")
new_ai.faction |= ROLE_SYNDICATE new_ai.faction |= ROLE_SYNDICATE
// Make it look evil!!! // Make it look evil!!!
new_ai.hologram_appearance = mutable_appearance('icons/mob/silicon/ai.dmi',"xeno_queen") //good enough new_ai.hologram_appearance = mutable_appearance('icons/mob/silicon/ai.dmi',"xeno_queen") //good enough

View File

@@ -49,7 +49,7 @@
if(H.mind && (has_job_loyalties || has_role_loyalties)) if(H.mind && (has_job_loyalties || has_role_loyalties))
if(has_job_loyalties && (H.mind.assigned_role.departments_bitflags & job_loyalties)) if(has_job_loyalties && (H.mind.assigned_role.departments_bitflags & job_loyalties))
inspired += H inspired += H
else if(has_role_loyalties && (H.mind.special_role in role_loyalties)) else if(has_role_loyalties && length(H.mind.get_special_roles() & role_loyalties))
inspired += H inspired += H
else if(check_inspiration(H)) else if(check_inspiration(H))
inspired += H inspired += H

View File

@@ -321,7 +321,7 @@
brainmob.mind.transfer_to(O) brainmob.mind.transfer_to(O)
playsound(O.loc, 'sound/mobs/non-humanoids/cyborg/liveagain.ogg', 75, TRUE) playsound(O.loc, 'sound/mobs/non-humanoids/cyborg/liveagain.ogg', 75, TRUE)
if(O.mind && O.mind.special_role) if(O.is_antag())
to_chat(O, span_userdanger("You have been robotized!")) to_chat(O, span_userdanger("You have been robotized!"))
to_chat(O, span_danger("You must obey your silicon laws and master AI above all else. Your objectives will consider you to be dead.")) to_chat(O, span_danger("You must obey your silicon laws and master AI above all else. Your objectives will consider you to be dead."))

View File

@@ -788,13 +788,7 @@
human_target.reagents.add_reagent(/datum/reagent/toxin, 2) human_target.reagents.add_reagent(/datum/reagent/toxin, 2)
return FALSE return FALSE
/// If all the antag datums are 'fake' or none exist, disallow induction! No self-antagging. if(!human_target.is_antag()) // GTFO. Technically not foolproof but making a heartbreaker or a paradox clone a nuke op sounds hilarious
var/faker
for(var/datum/antagonist/antag_datum as anything in human_target.mind.antag_datums)
if((antag_datum.antag_flags & FLAG_FAKE_ANTAG))
faker = TRUE
if(faker || isnull(human_target.mind.antag_datums)) // GTFO. Technically not foolproof but making a heartbreaker or a paradox clone a nuke op sounds hilarious
to_chat(human_target, span_notice("Huh? Nothing happened? But you're starting to feel a little ill...")) to_chat(human_target, span_notice("Huh? Nothing happened? But you're starting to feel a little ill..."))
human_target.reagents.add_reagent(/datum/reagent/toxin, 15) human_target.reagents.add_reagent(/datum/reagent/toxin, 15)
return FALSE return FALSE

View File

@@ -20,19 +20,9 @@
return return
var/dat var/dat
if(SSticker.current_state <= GAME_STATE_PREGAME)
dat += "<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_manage=1'>(Manage Dynamic Rulesets)</A><br>" dat += "<a href='byond://?src=[REF(src)];[HrefToken()];gamemode_panel=1'>Dynamic Panel</a><BR>"
dat += "<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_roundstart=1'>(Force Roundstart Rulesets)</A><br>" dat += "<hr/>"
if (GLOB.dynamic_forced_roundstart_ruleset.len > 0)
for(var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset)
dat += {"<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_roundstart_remove=[text_ref(rule)]'>-> [rule.name] <-</A><br>"}
dat += "<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_roundstart_clear=1'>(Clear Rulesets)</A><br>"
dat += "<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_options=1'>(Dynamic mode options)</A><br>"
dat += "<hr/>"
if(SSticker.IsRoundInProgress())
dat += "<a href='byond://?src=[REF(src)];[HrefToken()];gamemode_panel=1'>(Game Mode Panel)</a><BR>"
dat += "<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_manage=1'>(Manage Dynamic Rulesets)</A><br>"
dat += "<hr/>"
dat += {" dat += {"
<A href='byond://?src=[REF(src)];[HrefToken()];create_object=1'>Create Object</A><br> <A href='byond://?src=[REF(src)];[HrefToken()];create_object=1'>Create Object</A><br>
<A href='byond://?src=[REF(src)];[HrefToken()];quick_create_object=1'>Quick Create Object</A><br> <A href='byond://?src=[REF(src)];[HrefToken()];quick_create_object=1'>Quick Create Object</A><br>
@@ -105,134 +95,6 @@ ADMIN_VERB(spawn_cargo, R_SPAWN, "Spawn Cargo", "Spawn a cargo crate.", ADMIN_CA
log_admin("[key_name(user)] spawned cargo pack [chosen] at [AREACOORD(user.mob)]") log_admin("[key_name(user)] spawned cargo pack [chosen] at [AREACOORD(user.mob)]")
BLACKBOX_LOG_ADMIN_VERB("Spawn Cargo") BLACKBOX_LOG_ADMIN_VERB("Spawn Cargo")
/datum/admins/proc/dynamic_mode_options(mob/user)
var/dat = {"<h3>Common options</h3>
<i>All these options can be changed midround.</i> <br/>
<br/>
<b>Force extended:</b> - Option is <a href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_force_extended=1'> <b>[GLOB.dynamic_forced_extended ? "ON" : "OFF"]</a></b>.
<br/>This will force the round to be extended. No rulesets will be drafted. <br/>
<br/>
<b>No stacking:</b> - Option is <a href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_no_stacking=1'> <b>[GLOB.dynamic_no_stacking ? "ON" : "OFF"]</b></a>.
<br/>Unless the threat goes above [GLOB.dynamic_stacking_limit], only one "round-ender" ruleset will be drafted. <br/>
<br/>
<b>Forced threat level:</b> Current value : <a href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_forced_threat=1'><b>[GLOB.dynamic_forced_threat_level]</b></a>.
<br/>The value threat is set to if it is higher than -1.<br/>
<br/>
<br/>
<b>Stacking threeshold:</b> Current value : <a href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_stacking_limit=1'><b>[GLOB.dynamic_stacking_limit]</b></a>.
<br/>The threshold at which "round-ender" rulesets will stack. A value higher than 100 ensure this never happens. <br/>
"}
var/datum/browser/browser = new(user, "dyn_mode_options", "Dynamic Mode Options", 900, 650)
browser.set_content(dat)
browser.open()
/datum/admins/proc/dynamic_ruleset_manager(mob/user)
var/datum/browser/browser = new(user, "dyn_mode_options", "Dynamic Ruleset Management", 900, 650)
var/dat = {"
Change these options to forcibly enable or disable dynamic rulesets.<br/>
Disabled rulesets will never run, even if they would otherwise be valid.<br/>
Enabled rulesets will run even if the qualifying minimum of threat or player count is not present, this does not guarantee that they will necessarily be chosen (for example their weight may be set to 0 in config).<br/>
<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_force_all_on=1'>force enable all</A>
<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_force_all_off=1'>force disable all</A>
<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_force_all_reset=1'>reset all</A>
"}
if (SSticker.current_state <= GAME_STATE_PREGAME) // Don't bother displaying after the round has started
var/static/list/rulesets_by_context = list()
if (!length(rulesets_by_context))
for (var/datum/dynamic_ruleset/rule as anything in subtypesof(/datum/dynamic_ruleset))
if (initial(rule.name) == "")
continue
LAZYADD(rulesets_by_context[initial(rule.ruletype)], rule)
dat += dynamic_ruleset_category_pre_start_display("Roundstart", rulesets_by_context[ROUNDSTART_RULESET])
dat += dynamic_ruleset_category_pre_start_display("Latejoin", rulesets_by_context[LATEJOIN_RULESET])
dat += dynamic_ruleset_category_pre_start_display("Midround", rulesets_by_context[MIDROUND_RULESET])
browser.set_content(dat)
browser.open()
return
var/pop_count = length(GLOB.alive_player_list)
var/threat_level = SSdynamic.threat_level
dat += dynamic_ruleset_category_during_round_display("Latejoin", SSdynamic.latejoin_rules, pop_count, threat_level)
dat += dynamic_ruleset_category_during_round_display("Midround", SSdynamic.midround_rules, pop_count, threat_level)
browser.set_content(dat)
browser.open()
/datum/admins/proc/dynamic_ruleset_category_pre_start_display(title, list/rules)
var/dat = "<B><h3>[title]</h3></B><table class='ml-2'>"
for (var/datum/dynamic_ruleset/rule as anything in rules)
var/forced = GLOB.dynamic_forced_rulesets[rule] || RULESET_NOT_FORCED
var/color = COLOR_SILVER
switch (forced)
if (RULESET_FORCE_ENABLED)
color = COLOR_GREEN
if (RULESET_FORCE_DISABLED)
color = COLOR_RED
dat += "<tr><td><b>[initial(rule.name)]</b></td><td>\[<font color=[color]> [forced] </font>\]</td><td> \
<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_force_on=[text_ref(rule)]'>force enabled</A> \
<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_force_off=[text_ref(rule)]'>force disabled</A> \
<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_force_reset=[text_ref(rule)]'>reset</A></td></tr>"
dat += "</table>"
return dat
/datum/admins/proc/dynamic_ruleset_category_during_round_display(title, list/rules, pop_count, threat_level)
var/dat = "<B><h3>[title]</h3></B><table class='ml-2'>"
for (var/datum/dynamic_ruleset/rule as anything in rules)
var/active = rule.acceptable(population = pop_count, threat_level = threat_level) && rule.weight > 0
var/forced = GLOB.dynamic_forced_rulesets[rule.type] || RULESET_NOT_FORCED
var/color = (active) ? COLOR_GREEN : COLOR_RED
var/explanation = ""
if (!active)
if (rule.weight <= 0)
explanation = " - Weight is zero"
else if (forced == RULESET_FORCE_DISABLED)
explanation = " - Forcibly disabled"
else if (forced == RULESET_FORCE_ENABLED)
explanation = " - Failed spawn conditions"
else if (!rule.is_valid_population(pop_count))
explanation = " - Invalid player count"
else if (!rule.is_valid_threat(pop_count, threat_level))
explanation = " - Insufficient threat"
else
explanation = " - Failed spawn conditions"
else if (forced == RULESET_FORCE_ENABLED)
explanation = " - Forcibly enabled"
active = active ? "Active" : "Inactive"
dat += {"<tr><td><b>[rule.name]</b></td>
<td>\[ Weight: [rule.weight] \]
<td>\[<font color=[color]> [active][explanation] </font>\]</td><td>
<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_force_on=[text_ref(rule.type)]'>force enabled</A>
<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_force_off=[text_ref(rule.type)]'>force disabled</A>
<A href='byond://?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_force_reset=[text_ref(rule.type)]'>reset</A></td>
<td><A href='byond://?src=[REF(src)];[HrefToken()];f_inspect_ruleset=[text_ref(rule)]'>VV</A></td></tr>
"}
dat += "</table>"
return dat
/datum/admins/proc/force_all_rulesets(mob/user, force_value)
if (force_value == RULESET_NOT_FORCED)
GLOB.dynamic_forced_rulesets = list()
else
for (var/datum/dynamic_ruleset/rule as anything in subtypesof(/datum/dynamic_ruleset))
GLOB.dynamic_forced_rulesets[rule] = force_value
var/logged_message = "[key_name(user)] set all dynamic rulesets to [force_value]."
log_admin(logged_message)
message_admins(logged_message)
dynamic_ruleset_manager(user)
/datum/admins/proc/set_dynamic_ruleset_forced(mob/user, datum/dynamic_ruleset/type, force_value)
if (isnull(type))
return
GLOB.dynamic_forced_rulesets[type] = force_value
dynamic_ruleset_manager(user)
var/logged_message = "[key_name(user)] set '[initial(type.name)] ([initial(type.ruletype)])' to [GLOB.dynamic_forced_rulesets[type]]."
log_admin(logged_message)
message_admins(logged_message)
ADMIN_VERB(create_or_modify_area, R_DEBUG, "Create Or Modify Area", "Create of modify an area. wow.", ADMIN_CATEGORY_DEBUG) ADMIN_VERB(create_or_modify_area, R_DEBUG, "Create Or Modify Area", "Create of modify an area. wow.", ADMIN_CATEGORY_DEBUG)
create_area(user.mob) create_area(user.mob)
@@ -305,4 +167,3 @@ ADMIN_VERB(create_or_modify_area, R_DEBUG, "Create Or Modify Area", "Create of m
if(!logout && CONFIG_GET(flag/announce_admin_login) && (prefs.toggles & ANNOUNCE_LOGIN)) if(!logout && CONFIG_GET(flag/announce_admin_login) && (prefs.toggles & ANNOUNCE_LOGIN))
message_admins("Admin login: [key_name(src)]") message_admins("Admin login: [key_name(src)]")
return return

View File

@@ -74,6 +74,9 @@ GLOBAL_VAR(antag_prototypes)
break break
return common_commands return common_commands
/**
* Returns a list of "statuses" this mind has - like "Infected", "Mindshielded", etc
*/
/datum/mind/proc/get_special_statuses() /datum/mind/proc/get_special_statuses()
var/list/result = LAZYCOPY(special_statuses) var/list/result = LAZYCOPY(special_statuses)
if(!current) if(!current)
@@ -82,12 +85,18 @@ GLOBAL_VAR(antag_prototypes)
result += span_good("Mindshielded") result += span_good("Mindshielded")
if(current && HAS_MIND_TRAIT(current, TRAIT_UNCONVERTABLE)) if(current && HAS_MIND_TRAIT(current, TRAIT_UNCONVERTABLE))
result += span_good("Unconvertable") result += span_good("Unconvertable")
//Move these to mob return result
/**
* Returns a list of "roles" this mind has - like "Traitor", "Ex Head Rev", "Emagged", etc
*/
/datum/mind/proc/get_special_roles()
var/list/roles = LAZYCOPY(special_roles)
if(iscyborg(current)) if(iscyborg(current))
var/mob/living/silicon/robot/robot = current var/mob/living/silicon/robot/robot = current
if (robot.emagged) if (robot.emagged)
result += span_bad("Emagged") roles += "Emagged"
return result.Join(" | ") return roles
/datum/mind/proc/traitor_panel() /datum/mind/proc/traitor_panel()
if(!SSticker.HasRoundStarted()) if(!SSticker.HasRoundStarted())
@@ -100,12 +109,11 @@ GLOBAL_VAR(antag_prototypes)
var/out = "<B>[name]</B>[(current && (current.real_name != name))?" (as [current.real_name])":""]<br>" var/out = "<B>[name]</B>[(current && (current.real_name != name))?" (as [current.real_name])":""]<br>"
out += "Mind currently owned by key: [key] [active?"(synced)":"(not synced)"]<br>" out += "Mind currently owned by key: [key] [active?"(synced)":"(not synced)"]<br>"
out += "Assigned role: [assigned_role.title]. <a href='byond://?src=[REF(src)];role_edit=1'>Edit</a><br>" out += "Assigned role: [assigned_role.title]. <a href='byond://?src=[REF(src)];role_edit=1'>Edit</a><br>"
out += "Faction and special role: <b><font color='red'>[special_role]</font></b><br>"
out += "<a href='byond://?_src_=holder;[HrefToken()];check_teams=1'>Show Teams</a><br><br>" out += "<a href='byond://?_src_=holder;[HrefToken()];check_teams=1'>Show Teams</a><br><br>"
var/special_statuses = get_special_statuses() var/special_statuses = get_special_roles() | get_special_statuses()
if(length(special_statuses)) if(length(special_statuses))
out += get_special_statuses() + "<br>" out += "Roles: [jointext(special_statuses, " | ")]<br>"
if(!GLOB.antag_prototypes) if(!GLOB.antag_prototypes)
GLOB.antag_prototypes = list() GLOB.antag_prototypes = list()
@@ -167,7 +175,7 @@ GLOBAL_VAR(antag_prototypes)
continue continue
pref_source = prototype pref_source = prototype
break break
if(pref_source.job_rank) if(pref_source.pref_flag)
antag_header_parts += pref_source.enabled_in_preferences(src) ? "Enabled in Prefs" : "Disabled in Prefs" antag_header_parts += pref_source.enabled_in_preferences(src) ? "Enabled in Prefs" : "Disabled in Prefs"
//Traitor : None | Traitor | IAA //Traitor : None | Traitor | IAA

View File

@@ -95,7 +95,7 @@
tgui_alert(usr, "The game hasn't started yet!") tgui_alert(usr, "The game hasn't started yet!")
return return
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Round Status</title></head><body><h1><B>Round Status</B></h1>") var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Round Status</title></head><body><h1><B>Round Status</B></h1>")
dat += "<a href='byond://?_src_=holder;[HrefToken()];gamemode_panel=1'>Game Mode Panel</a><br>" dat += "<a href='byond://?_src_=holder;[HrefToken()];gamemode_panel=1'>Dynamic Panel</a><br>"
dat += "Round Duration: <B>[DisplayTimeText(world.time - SSticker.round_start_time)]</B><BR>" dat += "Round Duration: <B>[DisplayTimeText(world.time - SSticker.round_start_time)]</B><BR>"
dat += "<B>Emergency shuttle</B><BR>" dat += "<B>Emergency shuttle</B><BR>"
if(EMERGENCY_IDLE_OR_RECALLED) if(EMERGENCY_IDLE_OR_RECALLED)
@@ -152,7 +152,7 @@
if (checked_mob.client) if (checked_mob.client)
observers_connected++ observers_connected++
if(checked_mob.mind.special_role) if(checked_mob.is_antag())
antagonists++ antagonists++
if(checked_mob.stat == DEAD) if(checked_mob.stat == DEAD)
antagonists_dead++ antagonists_dead++

View File

@@ -228,7 +228,7 @@
var/color = "#e6e6e6" var/color = "#e6e6e6"
if(i%2 == 0) if(i%2 == 0)
color = "#f2f2f2" color = "#f2f2f2"
var/is_antagonist = is_special_character(M, allow_fake_antags = TRUE) var/is_antagonist = M.is_antag(NONE)
var/M_job = "" var/M_job = ""

View File

@@ -67,9 +67,7 @@
edit_rights_topic(href_list) edit_rights_topic(href_list)
else if(href_list["gamemode_panel"]) else if(href_list["gamemode_panel"])
if(!check_rights(R_ADMIN)) dynamic_panel(usr)
return
SSdynamic.admin_panel()
else if(href_list["call_shuttle"]) else if(href_list["call_shuttle"])
if(!check_rights(R_ADMIN)) if(!check_rights(R_ADMIN))
@@ -387,122 +385,6 @@
return return
cmd_admin_mute(href_list["mute"], text2num(href_list["mute_type"])) cmd_admin_mute(href_list["mute"], text2num(href_list["mute_type"]))
else if(href_list["f_dynamic_roundstart"])
if(!check_rights(R_ADMIN))
return
if(SSticker.HasRoundStarted())
return tgui_alert(usr, "The game has already started.")
var/roundstart_rules = list()
for (var/rule in subtypesof(/datum/dynamic_ruleset/roundstart))
var/datum/dynamic_ruleset/roundstart/newrule = new rule()
roundstart_rules[newrule.name] = newrule
var/added_rule = input(usr,"What ruleset do you want to force? This will bypass threat level and population restrictions.", "Rigging Roundstart", null) as null|anything in sort_list(roundstart_rules)
if (added_rule)
GLOB.dynamic_forced_roundstart_ruleset += roundstart_rules[added_rule]
log_admin("[key_name(usr)] set [added_rule] to be a forced roundstart ruleset.")
message_admins("[key_name(usr)] set [added_rule] to be a forced roundstart ruleset.", 1)
Game()
else if(href_list["f_dynamic_roundstart_clear"])
if(!check_rights(R_ADMIN))
return
GLOB.dynamic_forced_roundstart_ruleset = list()
Game()
log_admin("[key_name(usr)] cleared the rigged roundstart rulesets. The mode will pick them as normal.")
message_admins("[key_name(usr)] cleared the rigged roundstart rulesets. The mode will pick them as normal.", 1)
else if(href_list["f_dynamic_roundstart_remove"])
if(!check_rights(R_ADMIN))
return
var/datum/dynamic_ruleset/roundstart/rule = locate(href_list["f_dynamic_roundstart_remove"])
GLOB.dynamic_forced_roundstart_ruleset -= rule
Game()
log_admin("[key_name(usr)] removed [rule] from the forced roundstart rulesets.")
message_admins("[key_name(usr)] removed [rule] from the forced roundstart rulesets.", 1)
else if (href_list["f_dynamic_ruleset_manage"])
if(!check_rights(R_ADMIN))
return
dynamic_ruleset_manager(usr)
else if (href_list["f_dynamic_ruleset_force_all_on"])
if(!check_rights(R_ADMIN))
return
force_all_rulesets(usr, RULESET_FORCE_ENABLED)
else if (href_list["f_dynamic_ruleset_force_all_off"])
if(!check_rights(R_ADMIN))
return
force_all_rulesets(usr, RULESET_FORCE_DISABLED)
else if (href_list["f_dynamic_ruleset_force_all_reset"])
if(!check_rights(R_ADMIN))
return
force_all_rulesets(usr, RULESET_NOT_FORCED)
else if (href_list["f_dynamic_ruleset_force_on"])
if(!check_rights(R_ADMIN))
return
set_dynamic_ruleset_forced(usr, locate(href_list["f_dynamic_ruleset_force_on"]), RULESET_FORCE_ENABLED)
else if (href_list["f_dynamic_ruleset_force_off"])
if(!check_rights(R_ADMIN))
return
set_dynamic_ruleset_forced(usr, locate(href_list["f_dynamic_ruleset_force_off"]), RULESET_FORCE_DISABLED)
else if (href_list["f_dynamic_ruleset_force_reset"])
if(!check_rights(R_ADMIN))
return
set_dynamic_ruleset_forced(usr, locate(href_list["f_dynamic_ruleset_force_reset"]), RULESET_NOT_FORCED)
else if (href_list["f_inspect_ruleset"])
if(!check_rights(R_ADMIN))
return
usr.client.debug_variables(locate(href_list["f_inspect_ruleset"]))
else if (href_list["f_dynamic_options"])
if(!check_rights(R_ADMIN))
return
if(SSticker.HasRoundStarted())
return tgui_alert(usr, "The game has already started.")
dynamic_mode_options(usr)
else if(href_list["f_dynamic_force_extended"])
if(!check_rights(R_ADMIN))
return
GLOB.dynamic_forced_extended = !GLOB.dynamic_forced_extended
log_admin("[key_name(usr)] set 'forced_extended' to [GLOB.dynamic_forced_extended].")
message_admins("[key_name(usr)] set 'forced_extended' to [GLOB.dynamic_forced_extended].")
dynamic_mode_options(usr)
else if(href_list["f_dynamic_no_stacking"])
if(!check_rights(R_ADMIN))
return
GLOB.dynamic_no_stacking = !GLOB.dynamic_no_stacking
log_admin("[key_name(usr)] set 'no_stacking' to [GLOB.dynamic_no_stacking].")
message_admins("[key_name(usr)] set 'no_stacking' to [GLOB.dynamic_no_stacking].")
dynamic_mode_options(usr)
else if(href_list["f_dynamic_stacking_limit"])
if(!check_rights(R_ADMIN))
return
GLOB.dynamic_stacking_limit = input(usr,"Change the threat limit at which round-endings rulesets will start to stack.", "Change stacking limit", null) as num
log_admin("[key_name(usr)] set 'stacking_limit' to [GLOB.dynamic_stacking_limit].")
message_admins("[key_name(usr)] set 'stacking_limit' to [GLOB.dynamic_stacking_limit].")
dynamic_mode_options(usr)
else if(href_list["f_dynamic_forced_threat"])
if(!check_rights(R_ADMIN))
return
if(SSticker.HasRoundStarted())
return tgui_alert(usr, "The game has already started.")
var/new_value = input(usr, "Enter the forced threat level for dynamic mode.", "Forced threat level") as num
if (new_value > 100)
return tgui_alert(usr, "The value must be under 100.")
GLOB.dynamic_forced_threat_level = new_value
log_admin("[key_name(usr)] set 'forced_threat_level' to [GLOB.dynamic_forced_threat_level].")
message_admins("[key_name(usr)] set 'forced_threat_level' to [GLOB.dynamic_forced_threat_level].")
dynamic_mode_options(usr)
else if(href_list["forcespeech"]) else if(href_list["forcespeech"])
if(!check_rights(R_FUN)) if(!check_rights(R_FUN))
return return

View File

@@ -249,33 +249,14 @@ ADMIN_VERB(respawn_character, R_ADMIN, "Respawn Character", "Respawn a player th
SSjob.equip_rank(new_character, new_character.mind.assigned_role, new_character.client) SSjob.equip_rank(new_character, new_character.mind.assigned_role, new_character.client)
new_character.mind.give_uplink(silent = TRUE, antag_datum = traitordatum) new_character.mind.give_uplink(silent = TRUE, antag_datum = traitordatum)
switch(new_character.mind.special_role) var/skip_job_respawn = FALSE
if(ROLE_WIZARD) for(var/datum/antagonist/antag as anything in new_character.mind.antag_datums)
new_character.forceMove(pick(GLOB.wizardstart)) skip_job_respawn ||= antag.on_respawn(new_character)
var/datum/antagonist/wizard/A = new_character.mind.has_antag_datum(/datum/antagonist/wizard,TRUE) if(skip_job_respawn)
A.equip_wizard() break
if(ROLE_SYNDICATE)
new_character.forceMove(pick(GLOB.nukeop_start))
var/datum/antagonist/nukeop/N = new_character.mind.has_antag_datum(/datum/antagonist/nukeop,TRUE)
N.equip_op()
if(ROLE_NINJA)
var/list/ninja_spawn = list()
for(var/obj/effect/landmark/carpspawn/L in GLOB.landmarks_list)
ninja_spawn += L
var/datum/antagonist/ninja/ninjadatum = new_character.mind.has_antag_datum(/datum/antagonist/ninja)
ninjadatum.equip_space_ninja()
if(ninja_spawn.len)
new_character.forceMove(pick(ninja_spawn))
else//They may also be a cyborg or AI. if(!skip_job_respawn)
switch(new_character.mind.assigned_role.type) new_character.mind.assigned_role.on_respawn(new_character)
if(/datum/job/cyborg)//More rigging to make em' work and check if they're traitor.
new_character = new_character.Robotize(TRUE)
if(/datum/job/ai)
new_character = new_character.AIize()
else
if(!traitordatum) // Already equipped there.
SSjob.equip_rank(new_character, new_character.mind.assigned_role, new_character.client)//Or we simply equip them.
//Announces the character on all the systems, based on the record. //Announces the character on all the systems, based on the record.
if(!record_found && (new_character.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) if(!record_found && (new_character.mind.assigned_role.job_flags & JOB_CREW_MEMBER))

View File

@@ -1022,9 +1022,7 @@ GLOBAL_DATUM_INIT(admin_help_ui_handler, /datum/admin_help_ui_handler, new)
mobs_found += found mobs_found += found
if(!ai_found && isAI(found)) if(!ai_found && isAI(found))
ai_found = 1 ai_found = 1
var/is_antag = 0 var/is_antag = found.is_antag()
if(is_special_character(found))
is_antag = 1
founds += "Name: [found.name]([found.real_name]) Key: [found.key] Ckey: [found.ckey] [is_antag ? "(Antag)" : null] " founds += "Name: [found.name]([found.real_name]) Key: [found.key] Ckey: [found.ckey] [is_antag ? "(Antag)" : null] "
msg += "[original_word]<font size='1' color='[is_antag ? "red" : "black"]'>(<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];adminmoreinfo=[REF(found)]'>?</A>|<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];adminplayerobservefollow=[REF(found)]'>F</A>)</font> " msg += "[original_word]<font size='1' color='[is_antag ? "red" : "black"]'>(<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];adminmoreinfo=[REF(found)]'>?</A>|<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];adminplayerobservefollow=[REF(found)]'>F</A>)</font> "
continue continue

View File

@@ -10,18 +10,14 @@ GLOBAL_LIST_EMPTY(antagonists)
var/roundend_category = "other antagonists" var/roundend_category = "other antagonists"
///Set to false to hide the antagonists from roundend report ///Set to false to hide the antagonists from roundend report
var/show_in_roundend = TRUE var/show_in_roundend = TRUE
///If false, the roundtype will still convert with this antag active
var/prevent_roundtype_conversion = TRUE
///Mind that owns this datum ///Mind that owns this datum
var/datum/mind/owner var/datum/mind/owner
///Silent will prevent the gain/lose texts to show ///Silent will prevent the gain/lose texts to show
var/silent = FALSE var/silent = FALSE
///Whether or not the person will be able to have more than one datum /// What flag is checked for jobbans and polling? Optional, if unset, will use pref_flag
var/can_coexist_with_others = TRUE var/jobban_flag
///List of datums this type can't coexist with /// What flag to check for prefs? Required for antags with preferences associated
var/list/typecache_datum_blacklist = list() var/pref_flag
///The define string we use to identify the role for bans/player polls to spawn a random new one in.
var/job_rank
///Should replace jobbanned player with ghosts if granted. ///Should replace jobbanned player with ghosts if granted.
var/replace_banned = TRUE var/replace_banned = TRUE
///List of the objective datums that this role currently has, completing all objectives at round-end will cause this antagonist to greentext. ///List of the objective datums that this role currently has, completing all objectives at round-end will cause this antagonist to greentext.
@@ -38,8 +34,6 @@ GLOBAL_LIST_EMPTY(antagonists)
var/hud_icon = 'icons/mob/huds/antag_hud.dmi' var/hud_icon = 'icons/mob/huds/antag_hud.dmi'
///Name of the antag hud we provide to this mob. ///Name of the antag hud we provide to this mob.
var/antag_hud_name var/antag_hud_name
/// If set to true, the antag will not be added to the living antag list.
var/count_against_dynamic_roll_chance = TRUE
/// The battlecry this antagonist shouts when suiciding with C4/X4. /// The battlecry this antagonist shouts when suiciding with C4/X4.
var/suicide_cry = "" var/suicide_cry = ""
//Antag panel properties //Antag panel properties
@@ -63,8 +57,6 @@ GLOBAL_LIST_EMPTY(antagonists)
var/hardcore_random_bonus = FALSE var/hardcore_random_bonus = FALSE
/// A path to the audio stinger that plays upon gaining this datum. /// A path to the audio stinger that plays upon gaining this datum.
var/stinger_sound var/stinger_sound
/// Whether this antag datum blocks rolling new antag datums
var/block_midrounds = TRUE
//ANTAG UI //ANTAG UI
@@ -78,7 +70,6 @@ GLOBAL_LIST_EMPTY(antagonists)
/datum/antagonist/New() /datum/antagonist/New()
GLOB.antagonists += src GLOB.antagonists += src
typecache_datum_blacklist = typecacheof(typecache_datum_blacklist)
/datum/antagonist/Destroy() /datum/antagonist/Destroy()
GLOB.antagonists -= src GLOB.antagonists -= src
@@ -179,12 +170,7 @@ GLOBAL_LIST_EMPTY(antagonists)
/datum/antagonist/proc/can_be_owned(datum/mind/new_owner) /datum/antagonist/proc/can_be_owned(datum/mind/new_owner)
var/datum/mind/tested = new_owner || owner var/datum/mind/tested = new_owner || owner
if(tested.has_antag_datum(type)) return !tested.has_antag_datum(type)
return FALSE
for(var/datum/antagonist/badguy as anything in tested.antag_datums)
if(is_type_in_typecache(src, badguy.typecache_datum_blacklist))
return FALSE
return TRUE
//This will be called in add_antag_datum before owner assignment. //This will be called in add_antag_datum before owner assignment.
//Should return antag datum without owner. //Should return antag datum without owner.
@@ -203,7 +189,7 @@ GLOBAL_LIST_EMPTY(antagonists)
info_button.Remove(old_body) info_button.Remove(old_body)
info_button.Grant(new_body) info_button.Grant(new_body)
apply_innate_effects(new_body) apply_innate_effects(new_body)
if(count_against_dynamic_roll_chance && new_body.stat != DEAD) if(new_body.stat != DEAD)
new_body.add_to_current_living_antags() new_body.add_to_current_living_antags()
//This handles the application of antag huds/special abilities //This handles the application of antag huds/special abilities
@@ -271,12 +257,14 @@ GLOBAL_LIST_EMPTY(antagonists)
replace_banned_player() replace_banned_player()
else if(owner.current.client?.holder && (CONFIG_GET(flag/auto_deadmin_antagonists) || owner.current.client.prefs?.toggles & DEADMIN_ANTAGONIST)) else if(owner.current.client?.holder && (CONFIG_GET(flag/auto_deadmin_antagonists) || owner.current.client.prefs?.toggles & DEADMIN_ANTAGONIST))
owner.current.client.holder.auto_deadmin() owner.current.client.holder.auto_deadmin()
if(count_against_dynamic_roll_chance && owner.current.stat != DEAD && owner.current.client) if(owner.current.stat != DEAD && owner.current.client)
owner.current.add_to_current_living_antags() owner.current.add_to_current_living_antags()
for (var/datum/atom_hud/alternate_appearance/basic/antag_hud as anything in GLOB.active_alternate_appearances) for (var/datum/atom_hud/alternate_appearance/basic/antag_hud as anything in GLOB.active_alternate_appearances)
antag_hud.apply_to_new_mob(owner.current) antag_hud.apply_to_new_mob(owner.current)
LAZYADD(owner.special_roles, (jobban_flag || pref_flag))
SEND_SIGNAL(owner, COMSIG_ANTAGONIST_GAINED, src) SEND_SIGNAL(owner, COMSIG_ANTAGONIST_GAINED, src)
/** /**
@@ -293,7 +281,7 @@ GLOBAL_LIST_EMPTY(antagonists)
if(!player.ckey) if(!player.ckey)
return FALSE return FALSE
return (is_banned_from(player.ckey, list(ROLE_SYNDICATE, job_rank)) || QDELETED(player)) return (is_banned_from(player.ckey, list(ROLE_SYNDICATE, jobban_flag || pref_flag)) || QDELETED(player))
/** /**
* Proc that replaces a player who cannot play a specific antagonist due to being banned via a poll, and alerts the player of their being on the banlist. * Proc that replaces a player who cannot play a specific antagonist due to being banned via a poll, and alerts the player of their being on the banlist.
@@ -301,7 +289,7 @@ GLOBAL_LIST_EMPTY(antagonists)
/datum/antagonist/proc/replace_banned_player() /datum/antagonist/proc/replace_banned_player()
set waitfor = FALSE set waitfor = FALSE
var/mob/chosen_one = SSpolling.poll_ghosts_for_target(check_jobban = job_rank, role = job_rank, poll_time = 5 SECONDS, checked_target = owner.current, alert_pic = owner.current, role_name_text = name) var/mob/chosen_one = SSpolling.poll_ghosts_for_target(check_jobban = jobban_flag || pref_flag, role = pref_flag, poll_time = 5 SECONDS, checked_target = owner.current, alert_pic = owner.current, role_name_text = name)
if(chosen_one) if(chosen_one)
to_chat(owner, "Your mob has been taken over by a ghost! Appeal your job ban if you want to avoid this in the future!") to_chat(owner, "Your mob has been taken over by a ghost! Appeal your job ban if you want to avoid this in the future!")
message_admins("[key_name_admin(chosen_one)] has taken control of ([key_name_admin(owner)]) to replace antagonist banned player.") message_admins("[key_name_admin(chosen_one)] has taken control of ([key_name_admin(owner)]) to replace antagonist banned player.")
@@ -334,6 +322,7 @@ GLOBAL_LIST_EMPTY(antagonists)
var/datum/team/team = get_team() var/datum/team/team = get_team()
if(team) if(team)
team.remove_member(owner) team.remove_member(owner)
LAZYREMOVE(owner.special_roles, (jobban_flag || pref_flag))
SEND_SIGNAL(owner, COMSIG_ANTAGONIST_REMOVED, src) SEND_SIGNAL(owner, COMSIG_ANTAGONIST_REMOVED, src)
if(owner.current) if(owner.current)
SEND_SIGNAL(owner.current, COMSIG_MOB_ANTAGONIST_REMOVED, src) SEND_SIGNAL(owner.current, COMSIG_MOB_ANTAGONIST_REMOVED, src)
@@ -456,8 +445,8 @@ GLOBAL_LIST_EMPTY(antagonists)
return "" return ""
/datum/antagonist/proc/enabled_in_preferences(datum/mind/noggin) /datum/antagonist/proc/enabled_in_preferences(datum/mind/noggin)
if(job_rank) if(pref_flag)
if(noggin.current && noggin.current.client && (job_rank in noggin.current.client.prefs.be_special)) if(noggin.current && noggin.current.client && (pref_flag in noggin.current.client.prefs.be_special))
return TRUE return TRUE
else else
return FALSE return FALSE
@@ -614,3 +603,7 @@ GLOBAL_LIST_EMPTY(antagonists)
return TRUE return TRUE
#undef CUSTOM_OBJECTIVE_MAX_LENGTH #undef CUSTOM_OBJECTIVE_MAX_LENGTH
/// Return TRUE to prevent the antag's job from handling the respawn
/datum/antagonist/proc/on_respawn(mob/new_character)
return FALSE

View File

@@ -10,9 +10,10 @@
. |= A.owner . |= A.owner
/// From a list of players (minds, mobs or clients), finds the one with the highest playtime (either from a specific role or overall living) and returns it. /// From a list of players (minds, mobs or clients), finds the one with the highest playtime (either from a specific role or overall living) and returns it.
/// If playtime tracking is disabled, just returns the first player in the list.
/proc/get_most_experienced(list/players, specific_role) /proc/get_most_experienced(list/players, specific_role)
if(!CONFIG_GET(flag/use_exp_tracking)) //woops if(!CONFIG_GET(flag/use_exp_tracking)) //woops
return return players[1]
var/most_experienced var/most_experienced
for(var/player in players) for(var/player in players)
if(!most_experienced) if(!most_experienced)

View File

@@ -91,7 +91,6 @@
master_wizard.wiz_team.add_member(app_mind) master_wizard.wiz_team.add_member(app_mind)
app_mind.add_antag_datum(app) app_mind.add_antag_datum(app)
app_mind.set_assigned_role(SSjob.get_job_type(/datum/job/wizard_apprentice)) app_mind.set_assigned_role(SSjob.get_job_type(/datum/job/wizard_apprentice))
app_mind.special_role = ROLE_WIZARD_APPRENTICE
SEND_SOUND(M, sound('sound/effects/magic.ogg')) SEND_SOUND(M, sound('sound/effects/magic.ogg'))
///////////BORGS AND OPERATIVES ///////////BORGS AND OPERATIVES
@@ -106,7 +105,7 @@
icon = 'icons/obj/devices/voice.dmi' icon = 'icons/obj/devices/voice.dmi'
icon_state = "nukietalkie" icon_state = "nukietalkie"
/// The name of the special role given to the recruit /// The name of the special role given to the recruit
var/special_role_name = ROLE_NUCLEAR_OPERATIVE var/special_role_name = ROLE_OPERATIVE
/// The applied outfit /// The applied outfit
var/datum/outfit/syndicate/outfit = /datum/outfit/syndicate/reinforcement var/datum/outfit/syndicate/outfit = /datum/outfit/syndicate/reinforcement
/// The antag datum applied /// The antag datum applied
@@ -164,7 +163,7 @@
var/datum/antagonist/nukeop/creator_op = user.has_antag_datum(/datum/antagonist/nukeop, TRUE) var/datum/antagonist/nukeop/creator_op = user.has_antag_datum(/datum/antagonist/nukeop, TRUE)
op_mind.add_antag_datum(new_datum, creator_op ? creator_op.get_team() : null) op_mind.add_antag_datum(new_datum, creator_op ? creator_op.get_team() : null)
op_mind.special_role = special_role_name LAZYADD(op_mind.special_roles, special_role_name)
if(outfit) if(outfit)
var/datum/antagonist/nukeop/nukie_datum = op_mind.has_antag_datum(antag_datum) var/datum/antagonist/nukeop/nukie_datum = op_mind.has_antag_datum(antag_datum)
@@ -250,7 +249,7 @@
borg.PossessByPlayer(C.key) borg.PossessByPlayer(C.key)
borg.mind.add_antag_datum(antag_datum, creator_op ? creator_op.get_team() : null) borg.mind.add_antag_datum(antag_datum, creator_op ? creator_op.get_team() : null)
borg.mind.special_role = special_role_name LAZYADD(borg.mind.special_roles, special_role_name)
borg.forceMove(pod) borg.forceMove(pod)
new /obj/effect/pod_landingzone(get_turf(src), pod) new /obj/effect/pod_landingzone(get_turf(src), pod)
@@ -394,7 +393,7 @@
human_mob.equipOutfit(outfit) human_mob.equipOutfit(outfit)
op_mind.special_role = role_to_play LAZYADD(op_mind.special_roles, role_to_play)
do_special_things(spawned_mob, user) do_special_things(spawned_mob, user)
@@ -466,4 +465,3 @@
internals_slot = NONE internals_slot = NONE
belt = /obj/item/lighter/skull belt = /obj/item/lighter/skull
r_hand = /obj/item/food/grown/banana r_hand = /obj/item/food/grown/banana

View File

@@ -2,7 +2,7 @@
name = "\improper Abductor" name = "\improper Abductor"
roundend_category = "abductors" roundend_category = "abductors"
antagpanel_category = ANTAG_GROUP_ABDUCTORS antagpanel_category = ANTAG_GROUP_ABDUCTORS
job_rank = ROLE_ABDUCTOR pref_flag = ROLE_ABDUCTOR
antag_hud_name = "abductor" antag_hud_name = "abductor"
show_in_antagpanel = FALSE //should only show subtypes show_in_antagpanel = FALSE //should only show subtypes
show_to_ghosts = TRUE show_to_ghosts = TRUE
@@ -71,7 +71,6 @@
/datum/antagonist/abductor/on_gain() /datum/antagonist/abductor/on_gain()
owner.set_assigned_role(SSjob.get_job_type(role_job)) owner.set_assigned_role(SSjob.get_job_type(role_job))
owner.special_role = ROLE_ABDUCTOR
objectives += team.objectives objectives += team.objectives
finalize_abductor() finalize_abductor()
// We don't want abductors to be converted by other antagonists // We don't want abductors to be converted by other antagonists
@@ -79,7 +78,6 @@
return ..() return ..()
/datum/antagonist/abductor/on_removal() /datum/antagonist/abductor/on_removal()
owner.special_role = null
owner.remove_traits(list(TRAIT_ABDUCTOR_TRAINING, TRAIT_UNCONVERTABLE), ABDUCTOR_ANTAGONIST) owner.remove_traits(list(TRAIT_ABDUCTOR_TRAINING, TRAIT_UNCONVERTABLE), ABDUCTOR_ANTAGONIST)
return ..() return ..()

View File

@@ -1,12 +1,11 @@
/datum/antagonist/ashwalker /datum/antagonist/ashwalker
name = "\improper Ash Walker" name = "\improper Ash Walker"
job_rank = ROLE_LAVALAND pref_flag = ROLE_LAVALAND
show_in_antagpanel = FALSE show_in_antagpanel = FALSE
show_to_ghosts = TRUE show_to_ghosts = TRUE
prevent_roundtype_conversion = FALSE
antagpanel_category = ANTAG_GROUP_ASHWALKERS antagpanel_category = ANTAG_GROUP_ASHWALKERS
suicide_cry = "I HAVE NO IDEA WHAT THIS THING DOES!!" suicide_cry = "I HAVE NO IDEA WHAT THIS THING DOES!!"
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST
var/datum/team/ashwalkers/ashie_team var/datum/team/ashwalkers/ashie_team
/datum/antagonist/ashwalker/create_team(datum/team/ashwalkers/ashwalker_team) /datum/antagonist/ashwalker/create_team(datum/team/ashwalkers/ashwalker_team)

View File

@@ -19,7 +19,7 @@
suicide_cry = "FOR THE SYNDICATE!!!" suicide_cry = "FOR THE SYNDICATE!!!"
antag_hud_name = "battlecruiser_crew" antag_hud_name = "battlecruiser_crew"
antagpanel_category = ANTAG_GROUP_SYNDICATE antagpanel_category = ANTAG_GROUP_SYNDICATE
job_rank = ROLE_BATTLECRUISER_CREW pref_flag = ROLE_BATTLECRUISER_CREW
stinger_sound = 'sound/music/antag/ops.ogg' stinger_sound = 'sound/music/antag/ops.ogg'
/// Team to place the crewmember on. /// Team to place the crewmember on.
var/datum/team/battlecruiser/battlecruiser_team var/datum/team/battlecruiser/battlecruiser_team
@@ -39,7 +39,7 @@
/datum/antagonist/battlecruiser/captain /datum/antagonist/battlecruiser/captain
name = "Battlecruiser Captain" name = "Battlecruiser Captain"
antag_hud_name = "battlecruiser_lead" antag_hud_name = "battlecruiser_lead"
job_rank = ROLE_BATTLECRUISER_CAPTAIN pref_flag = ROLE_BATTLECRUISER_CAPTAIN
/datum/antagonist/battlecruiser/create_team(datum/team/battlecruiser/team) /datum/antagonist/battlecruiser/create_team(datum/team/battlecruiser/team)
if(!team) if(!team)

View File

@@ -4,7 +4,7 @@
antagpanel_category = ANTAG_GROUP_BIOHAZARDS antagpanel_category = ANTAG_GROUP_BIOHAZARDS
show_to_ghosts = TRUE show_to_ghosts = TRUE
show_in_antagpanel = FALSE show_in_antagpanel = FALSE
job_rank = ROLE_BLOB pref_flag = ROLE_BLOB
ui_name = "AntagInfoBlob" ui_name = "AntagInfoBlob"
stinger_sound = 'sound/music/antag/blobalert.ogg' stinger_sound = 'sound/music/antag/blobalert.ogg'
antag_hud_name = "blob" antag_hud_name = "blob"
@@ -156,7 +156,7 @@
/datum/antagonist/blob/infection /datum/antagonist/blob/infection
name = "\improper Blob Infection" name = "\improper Blob Infection"
show_in_antagpanel = TRUE show_in_antagpanel = TRUE
job_rank = ROLE_BLOB_INFECTION pref_flag = ROLE_BLOB_INFECTION
/datum/antagonist/blob/infection/get_preview_icon() /datum/antagonist/blob/infection/get_preview_icon()
var/icon/blob_icon = ..() var/icon/blob_icon = ..()

View File

@@ -29,14 +29,14 @@
/datum/antagonist/brainwashed /datum/antagonist/brainwashed
name = "\improper Brainwashed Victim" name = "\improper Brainwashed Victim"
job_rank = ROLE_BRAINWASHED pref_flag = ROLE_BRAINWASHED
stinger_sound = 'sound/music/antag/brainwashed.ogg' stinger_sound = 'sound/music/antag/brainwashed.ogg'
roundend_category = "brainwashed victims" roundend_category = "brainwashed victims"
show_in_antagpanel = TRUE show_in_antagpanel = TRUE
antag_hud_name = "brainwashed" antag_hud_name = "brainwashed"
antagpanel_category = ANTAG_GROUP_CREW antagpanel_category = ANTAG_GROUP_CREW
show_name_in_check_antagonists = TRUE show_name_in_check_antagonists = TRUE
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST
ui_name = "AntagInfoBrainwashed" ui_name = "AntagInfoBrainwashed"
suicide_cry = "FOR... SOMEONE!!" suicide_cry = "FOR... SOMEONE!!"

View File

@@ -1,7 +1,7 @@
/datum/antagonist/brother /datum/antagonist/brother
name = "\improper Brother" name = "\improper Brother"
antagpanel_category = "Brother" antagpanel_category = "Brother"
job_rank = ROLE_BROTHER pref_flag = ROLE_BROTHER
var/special_role = ROLE_BROTHER var/special_role = ROLE_BROTHER
antag_hud_name = "brother" antag_hud_name = "brother"
hijack_speed = 0.5 hijack_speed = 0.5
@@ -15,6 +15,7 @@
/datum/antagonist/brother/create_team(datum/team/brother_team/new_team) /datum/antagonist/brother/create_team(datum/team/brother_team/new_team)
if(!new_team) if(!new_team)
team = new()
return return
if(!istype(new_team)) if(!istype(new_team))
stack_trace("Wrong team type passed to [type] initialization.") stack_trace("Wrong team type passed to [type] initialization.")
@@ -25,7 +26,6 @@
/datum/antagonist/brother/on_gain() /datum/antagonist/brother/on_gain()
objectives += team.objectives objectives += team.objectives
owner.special_role = special_role
finalize_brother() finalize_brother()
if (team.brothers_left <= 0) if (team.brothers_left <= 0)
@@ -45,7 +45,6 @@
return ..() return ..()
/datum/antagonist/brother/on_removal() /datum/antagonist/brother/on_removal()
owner.special_role = null
remove_conversion_skills() remove_conversion_skills()
return ..() return ..()
@@ -178,7 +177,7 @@
return brother_text return brother_text
/datum/antagonist/brother/greet() /datum/antagonist/brother/greet()
to_chat(owner.current, span_alertsyndie("You are the [owner.special_role].")) to_chat(owner.current, span_alertsyndie("You are a Blood Brother."))
owner.announce_objectives() owner.announce_objectives()
/datum/antagonist/brother/proc/finalize_brother() /datum/antagonist/brother/proc/finalize_brother()

View File

@@ -5,7 +5,7 @@
name = "\improper Changeling" name = "\improper Changeling"
roundend_category = "changelings" roundend_category = "changelings"
antagpanel_category = "Changeling" antagpanel_category = "Changeling"
job_rank = ROLE_CHANGELING pref_flag = ROLE_CHANGELING
antag_moodlet = /datum/mood_event/focused antag_moodlet = /datum/mood_event/focused
antag_hud_name = "changeling" antag_hud_name = "changeling"
hijack_speed = 0.5 hijack_speed = 0.5
@@ -1023,7 +1023,7 @@
name = "\improper Headslug Changeling" name = "\improper Headslug Changeling"
show_in_antagpanel = FALSE show_in_antagpanel = FALSE
give_objectives = FALSE give_objectives = FALSE
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_SKIP_GLOBAL_LIST
genetic_points = 5 genetic_points = 5
total_genetic_points = 5 total_genetic_points = 5

View File

@@ -3,11 +3,10 @@
name = "\improper Fallen Changeling" name = "\improper Fallen Changeling"
roundend_category = "changelings" roundend_category = "changelings"
antagpanel_category = "Changeling" antagpanel_category = "Changeling"
job_rank = ROLE_CHANGELING pref_flag = ROLE_CHANGELING
antag_moodlet = /datum/mood_event/fallen_changeling antag_moodlet = /datum/mood_event/fallen_changeling
antag_hud_name = "changeling" antag_hud_name = "changeling"
/datum/mood_event/fallen_changeling /datum/mood_event/fallen_changeling
description = "My powers! Where are my powers?!" description = "My powers! Where are my powers?!"
mood_change = -4 mood_change = -4

View File

@@ -4,6 +4,7 @@
roundend_category = "clown operatives" roundend_category = "clown operatives"
antagpanel_category = ANTAG_GROUP_CLOWNOPS antagpanel_category = ANTAG_GROUP_CLOWNOPS
nukeop_outfit = /datum/outfit/syndicate/clownop nukeop_outfit = /datum/outfit/syndicate/clownop
job_type = /datum/job/nuclear_operative/clown_operative
suicide_cry = "HAPPY BIRTHDAY!!" suicide_cry = "HAPPY BIRTHDAY!!"
preview_outfit = /datum/outfit/clown_operative_elite preview_outfit = /datum/outfit/clown_operative_elite
@@ -11,7 +12,6 @@
nuke_icon_state = "bananiumbomb_base" nuke_icon_state = "bananiumbomb_base"
/datum/antagonist/nukeop/clownop/admin_add(datum/mind/new_owner,mob/admin) /datum/antagonist/nukeop/clownop/admin_add(datum/mind/new_owner,mob/admin)
new_owner.set_assigned_role(SSjob.get_job_type(/datum/job/clown_operative))
new_owner.add_antag_datum(src) new_owner.add_antag_datum(src)
message_admins("[key_name_admin(admin)] has clown op'ed [key_name_admin(new_owner)].") message_admins("[key_name_admin(admin)] has clown op'ed [key_name_admin(new_owner)].")
log_admin("[key_name(admin)] has clown op'ed [key_name(new_owner)].") log_admin("[key_name(admin)] has clown op'ed [key_name(new_owner)].")
@@ -33,11 +33,12 @@
ADD_TRAIT(liver, TRAIT_COMEDY_METABOLISM, CLOWNOP_TRAIT) ADD_TRAIT(liver, TRAIT_COMEDY_METABOLISM, CLOWNOP_TRAIT)
/datum/antagonist/nukeop/leader/clownop/give_alias() /datum/antagonist/nukeop/leader/clownop/give_alias()
title = pick("Head Honker", "Slipmaster", "Clown King", "Honkbearer") title ||= pick("Head Honker", "Slipmaster", "Clown King", "Honkbearer")
if(nuke_team?.syndicate_name) . = ..()
owner.current.real_name = "[nuke_team.syndicate_name] [title]" if(ishuman(owner.current))
owner.current.fully_replace_character_name(owner.current.real_name, "[title] [owner.current.real_name]")
else else
owner.current.real_name = "Syndicate [title]" owner.current.fully_replace_character_name(owner.current.real_name, "[nuke_team.syndicate_name] [title]")
/datum/antagonist/nukeop/leader/clownop /datum/antagonist/nukeop/leader/clownop
name = "Clown Operative Leader" name = "Clown Operative Leader"

View File

@@ -5,7 +5,7 @@
antag_moodlet = /datum/mood_event/cult antag_moodlet = /datum/mood_event/cult
suicide_cry = "FOR NAR'SIE!!" suicide_cry = "FOR NAR'SIE!!"
preview_outfit = /datum/outfit/cultist preview_outfit = /datum/outfit/cultist
job_rank = ROLE_CULTIST pref_flag = ROLE_CULTIST
antag_hud_name = "cult" antag_hud_name = "cult"
stinger_sound = 'sound/music/antag/bloodcult/bloodcult_gain.ogg' stinger_sound = 'sound/music/antag/bloodcult/bloodcult_gain.ogg'
@@ -275,4 +275,3 @@
///Used to check if the owner is counted as a secondary invoker for runes. ///Used to check if the owner is counted as a secondary invoker for runes.
/datum/antagonist/cult/proc/check_invoke_validity() /datum/antagonist/cult/proc/check_invoke_validity()
return TRUE return TRUE

View File

@@ -322,7 +322,6 @@ structure_check() searches for nearby cultist structures required for the invoca
convertee.Unconscious(10 SECONDS) convertee.Unconscious(10 SECONDS)
new /obj/item/melee/cultblade/dagger(get_turf(src)) new /obj/item/melee/cultblade/dagger(get_turf(src))
convertee.mind.special_role = ROLE_CULTIST
convertee.mind.add_antag_datum(/datum/antagonist/cult, cult_team) convertee.mind.add_antag_datum(/datum/antagonist/cult, cult_team)
to_chat(convertee, span_cult_bold_italic("Your blood pulses. Your head throbs. The world goes red. \ to_chat(convertee, span_cult_bold_italic("Your blood pulses. Your head throbs. The world goes red. \
@@ -1197,7 +1196,7 @@ GLOBAL_VAR_INIT(narsie_summon_count, 0)
force_event_async(/datum/round_event_control/meteor_wave, "an apocalypse rune") force_event_async(/datum/round_event_control/meteor_wave, "an apocalypse rune")
if(51 to 60) if(51 to 60)
force_event_async(/datum/round_event_control/spider_infestation, "an apocalypse rune") SSdynamic.force_run_midround(/datum/dynamic_ruleset/midround/spiders)
if(61 to 70) if(61 to 70)
force_event_async(/datum/round_event_control/anomaly/anomaly_flux, "an apocalypse rune") force_event_async(/datum/round_event_control/anomaly/anomaly_flux, "an apocalypse rune")

View File

@@ -11,9 +11,8 @@
antag_moodlet = /datum/mood_event/focused antag_moodlet = /datum/mood_event/focused
antagpanel_category = ANTAG_GROUP_ERT antagpanel_category = ANTAG_GROUP_ERT
suicide_cry = "FOR NANOTRASEN!!" suicide_cry = "FOR NANOTRASEN!!"
count_against_dynamic_roll_chance = FALSE
// Not 'true' antags, this disables certain interactions that assume the owner is a baddie // Not 'true' antags, this disables certain interactions that assume the owner is a baddie
antag_flags = FLAG_FAKE_ANTAG antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST
var/datum/team/ert/ert_team var/datum/team/ert/ert_team
var/leader = FALSE var/leader = FALSE
var/datum/outfit/outfit = /datum/outfit/centcom/ert/security var/datum/outfit/outfit = /datum/outfit/centcom/ert/security

View File

@@ -2,12 +2,12 @@
/datum/antagonist/evil_clone /datum/antagonist/evil_clone
name = "\improper Evil Clone" name = "\improper Evil Clone"
stinger_sound = 'sound/music/antag/hypnotized.ogg' stinger_sound = 'sound/music/antag/hypnotized.ogg'
job_rank = ROLE_EVIL_CLONE pref_flag = ROLE_EVIL_CLONE
roundend_category = "evil clones" roundend_category = "evil clones"
show_in_antagpanel = TRUE show_in_antagpanel = TRUE
antagpanel_category = ANTAG_GROUP_CREW antagpanel_category = ANTAG_GROUP_CREW
show_name_in_check_antagonists = TRUE show_name_in_check_antagonists = TRUE
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_SKIP_GLOBAL_LIST
/datum/antagonist/evil_clone/on_gain() /datum/antagonist/evil_clone/on_gain()
if (owner.current) if (owner.current)

View File

@@ -2,16 +2,14 @@
/datum/antagonist/fugitive /datum/antagonist/fugitive
name = "\improper Fugitive" name = "\improper Fugitive"
roundend_category = "Fugitive" roundend_category = "Fugitive"
job_rank = ROLE_FUGITIVE pref_flag = ROLE_FUGITIVE
silent = TRUE //greet called by the event
show_in_antagpanel = FALSE show_in_antagpanel = FALSE
show_to_ghosts = TRUE show_to_ghosts = TRUE
antagpanel_category = ANTAG_GROUP_FUGITIVES antagpanel_category = ANTAG_GROUP_FUGITIVES
prevent_roundtype_conversion = FALSE
antag_hud_name = "fugitive" antag_hud_name = "fugitive"
suicide_cry = "FOR FREEDOM!!" suicide_cry = "FOR FREEDOM!!"
preview_outfit = /datum/outfit/prisoner preview_outfit = /datum/outfit/prisoner
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_SKIP_GLOBAL_LIST
var/datum/team/fugitive/fugitive_team var/datum/team/fugitive/fugitive_team
var/is_captured = FALSE var/is_captured = FALSE
var/backstory = "error" var/backstory = "error"
@@ -38,10 +36,14 @@
return fugitive_icon return fugitive_icon
/datum/antagonist/fugitive/on_gain() /datum/antagonist/fugitive/on_gain()
forge_objectives() forge_objectives()
. = ..() . = ..()
owner.set_assigned_role(SSjob.get_job_type(/datum/job/fugitive))
/datum/antagonist/fugitive/on_removal()
. = ..()
owner?.set_assigned_role(SSjob.get_job_type(/datum/job/unassigned))
/datum/antagonist/fugitive/forge_objectives() //this isn't the actual survive objective because it's about who in the team survives /datum/antagonist/fugitive/forge_objectives() //this isn't the actual survive objective because it's about who in the team survives
var/datum/objective/survive = new /datum/objective var/datum/objective/survive = new /datum/objective
@@ -49,9 +51,8 @@
survive.explanation_text = "Avoid capture from the fugitive hunters." survive.explanation_text = "Avoid capture from the fugitive hunters."
objectives += survive objectives += survive
/datum/antagonist/fugitive/greet(back_story) /datum/antagonist/fugitive/greet()
. = ..() . = ..()
backstory = back_story
var/message = "<span class='warningplain'>" var/message = "<span class='warningplain'>"
switch(backstory) switch(backstory)
if(FUGITIVE_BACKSTORY_PRISONER) if(FUGITIVE_BACKSTORY_PRISONER)

View File

@@ -6,10 +6,9 @@
show_in_antagpanel = FALSE show_in_antagpanel = FALSE
show_to_ghosts = TRUE show_to_ghosts = TRUE
antagpanel_category = ANTAG_GROUP_HUNTERS antagpanel_category = ANTAG_GROUP_HUNTERS
prevent_roundtype_conversion = FALSE
antag_hud_name = "fugitive_hunter" antag_hud_name = "fugitive_hunter"
suicide_cry = "FOR GLORY!!" suicide_cry = "FOR GLORY!!"
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_SKIP_GLOBAL_LIST
var/datum/team/fugitive_hunters/hunter_team var/datum/team/fugitive_hunters/hunter_team
var/backstory = "error" var/backstory = "error"

View File

@@ -3,7 +3,7 @@
show_in_antagpanel = FALSE show_in_antagpanel = FALSE
show_name_in_check_antagonists = TRUE //Not that it will be there for long show_name_in_check_antagonists = TRUE //Not that it will be there for long
suicide_cry = "FOR THE GREENTEXT!!" // This can never actually show up, but not including it is a missed opportunity suicide_cry = "FOR THE GREENTEXT!!" // This can never actually show up, but not including it is a missed opportunity
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST
hardcore_random_bonus = TRUE hardcore_random_bonus = TRUE
/datum/antagonist/greentext/forge_objectives() /datum/antagonist/greentext/forge_objectives()

View File

@@ -20,7 +20,7 @@
antagpanel_category = "Heretic" antagpanel_category = "Heretic"
ui_name = "AntagInfoHeretic" ui_name = "AntagInfoHeretic"
antag_moodlet = /datum/mood_event/heretics antag_moodlet = /datum/mood_event/heretics
job_rank = ROLE_HERETIC pref_flag = ROLE_HERETIC
antag_hud_name = "heretic" antag_hud_name = "heretic"
hijack_speed = 0.5 hijack_speed = 0.5
suicide_cry = "THE MANSUS SMILES UPON ME!!" suicide_cry = "THE MANSUS SMILES UPON ME!!"

View File

@@ -4,7 +4,7 @@
roundend_category = "Heretics" roundend_category = "Heretics"
antagpanel_category = ANTAG_GROUP_HORRORS antagpanel_category = ANTAG_GROUP_HORRORS
antag_moodlet = /datum/mood_event/heretics antag_moodlet = /datum/mood_event/heretics
job_rank = ROLE_HERETIC pref_flag = ROLE_HERETIC
antag_hud_name = "heretic_beast" antag_hud_name = "heretic_beast"
suicide_cry = "MY MASTER SMILES UPON ME!!" suicide_cry = "MY MASTER SMILES UPON ME!!"
show_in_antagpanel = FALSE show_in_antagpanel = FALSE

View File

@@ -3,7 +3,7 @@
name = "\improper Soultrapped Heretic" name = "\improper Soultrapped Heretic"
roundend_category = "Heretics" roundend_category = "Heretics"
antagpanel_category = "Heretic" antagpanel_category = "Heretic"
job_rank = ROLE_HERETIC pref_flag = ROLE_HERETIC
antag_moodlet = /datum/mood_event/soultrapped_heretic antag_moodlet = /datum/mood_event/soultrapped_heretic
antag_hud_name = "heretic" antag_hud_name = "heretic"

View File

@@ -5,7 +5,7 @@
show_name_in_check_antagonists = TRUE show_name_in_check_antagonists = TRUE
can_elimination_hijack = ELIMINATION_ENABLED can_elimination_hijack = ELIMINATION_ENABLED
suicide_cry = "FOR SCOTLAND!!" // If they manage to lose their no-drop stuff somehow suicide_cry = "FOR SCOTLAND!!" // If they manage to lose their no-drop stuff somehow
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST
/// Traits we apply/remove to our target on-demand. /// Traits we apply/remove to our target on-demand.
var/static/list/applicable_traits = list( var/static/list/applicable_traits = list(
TRAIT_NOBREATH, TRAIT_NOBREATH,
@@ -38,7 +38,6 @@
/datum/antagonist/highlander/on_gain() /datum/antagonist/highlander/on_gain()
forge_objectives() forge_objectives()
owner.special_role = "highlander"
give_equipment() give_equipment()
. = ..() . = ..()

View File

@@ -2,14 +2,14 @@
/datum/antagonist/hypnotized /datum/antagonist/hypnotized
name = "\improper Hypnotized Victim" name = "\improper Hypnotized Victim"
stinger_sound = 'sound/music/antag/hypnotized.ogg' stinger_sound = 'sound/music/antag/hypnotized.ogg'
job_rank = ROLE_HYPNOTIZED pref_flag = ROLE_HYPNOTIZED
roundend_category = "hypnotized victims" roundend_category = "hypnotized victims"
antag_hud_name = "brainwashed" antag_hud_name = "brainwashed"
ui_name = "AntagInfoBrainwashed" ui_name = "AntagInfoBrainwashed"
show_in_antagpanel = TRUE show_in_antagpanel = TRUE
antagpanel_category = ANTAG_GROUP_CREW antagpanel_category = ANTAG_GROUP_CREW
show_name_in_check_antagonists = TRUE show_name_in_check_antagonists = TRUE
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST
/// Brain trauma associated with this antag datum /// Brain trauma associated with this antag datum
var/datum/brain_trauma/hypnosis/trauma var/datum/brain_trauma/hypnosis/trauma

View File

@@ -5,7 +5,7 @@
name = "\improper Malfunctioning AI" name = "\improper Malfunctioning AI"
roundend_category = "traitors" roundend_category = "traitors"
antagpanel_category = "Malf AI" antagpanel_category = "Malf AI"
job_rank = ROLE_MALF pref_flag = ROLE_MALF
antag_hud_name = "traitor" antag_hud_name = "traitor"
ui_name = "AntagInfoMalf" ui_name = "AntagInfoMalf"
can_assign_self_objectives = TRUE can_assign_self_objectives = TRUE
@@ -32,7 +32,6 @@
stack_trace("Attempted to give malf AI antag datum to \[[owner]\], who did not meet the requirements.") stack_trace("Attempted to give malf AI antag datum to \[[owner]\], who did not meet the requirements.")
return ..() return ..()
owner.special_role = job_rank
if(give_objectives) if(give_objectives)
forge_ai_objectives() forge_ai_objectives()
if(!employer) if(!employer)
@@ -58,7 +57,6 @@
malf_ai.remove_malf_abilities() malf_ai.remove_malf_abilities()
QDEL_NULL(malf_ai.malf_picker) QDEL_NULL(malf_ai.malf_picker)
owner.special_role = null
UnregisterSignal(owner, COMSIG_SILICON_AI_CORE_STATUS) UnregisterSignal(owner, COMSIG_SILICON_AI_CORE_STATUS)
return ..() return ..()

View File

@@ -1,7 +1,7 @@
/datum/antagonist/nightmare /datum/antagonist/nightmare
name = "\improper Nightmare" name = "\improper Nightmare"
antagpanel_category = ANTAG_GROUP_ABOMINATIONS antagpanel_category = ANTAG_GROUP_ABOMINATIONS
job_rank = ROLE_NIGHTMARE pref_flag = ROLE_NIGHTMARE
show_in_antagpanel = FALSE show_in_antagpanel = FALSE
show_name_in_check_antagonists = TRUE show_name_in_check_antagonists = TRUE
show_to_ghosts = TRUE show_to_ghosts = TRUE

View File

@@ -1,8 +1,8 @@
/datum/antagonist/nukeop /datum/antagonist/nukeop
name = ROLE_NUCLEAR_OPERATIVE name = ROLE_OPERATIVE
roundend_category = "syndicate operatives" //just in case roundend_category = "syndicate operatives" //just in case
antagpanel_category = ANTAG_GROUP_SYNDICATE antagpanel_category = ANTAG_GROUP_SYNDICATE
job_rank = ROLE_OPERATIVE pref_flag = ROLE_OPERATIVE
antag_hud_name = "synd" antag_hud_name = "synd"
antag_moodlet = /datum/mood_event/focused antag_moodlet = /datum/mood_event/focused
show_to_ghosts = TRUE show_to_ghosts = TRUE
@@ -12,10 +12,10 @@
/// Which nukie team are we on? /// Which nukie team are we on?
var/datum/team/nuclear/nuke_team var/datum/team/nuclear/nuke_team
/// If not assigned a team by default ops will try to join existing ones, set this to TRUE to always create new team.
var/always_new_team = FALSE
/// Should the user be moved to default spawnpoint after being granted this datum. /// Should the user be moved to default spawnpoint after being granted this datum.
var/send_to_spawnpoint = TRUE var/send_to_spawnpoint = TRUE
var/job_type = /datum/job/nuclear_operative
/// The DEFAULT outfit we will give to players granted this datum /// The DEFAULT outfit we will give to players granted this datum
var/nukeop_outfit = /datum/outfit/syndicate var/nukeop_outfit = /datum/outfit/syndicate
@@ -23,6 +23,7 @@
/// In the preview icon, the nukies who are behind the leader /// In the preview icon, the nukies who are behind the leader
var/preview_outfit_behind = /datum/outfit/nuclear_operative var/preview_outfit_behind = /datum/outfit/nuclear_operative
/// In the preview icon, a nuclear fission explosive device, only appearing if there's an icon state for it. /// In the preview icon, a nuclear fission explosive device, only appearing if there's an icon state for it.
var/nuke_icon_state = "nuclearbomb_base" var/nuke_icon_state = "nuclearbomb_base"
@@ -40,6 +41,7 @@
give_alias() give_alias()
forge_objectives() forge_objectives()
. = ..() . = ..()
owner.set_assigned_role(SSjob.get_job_type(job_type))
equip_op() equip_op()
if(send_to_spawnpoint) if(send_to_spawnpoint)
move_to_spawnpoint() move_to_spawnpoint()
@@ -61,7 +63,8 @@
nuke_team.team_discounts += create_uplink_sales(discount_limited_amount, /datum/uplink_category/limited_discount_team_gear, 1, uplink_items) nuke_team.team_discounts += create_uplink_sales(discount_limited_amount, /datum/uplink_category/limited_discount_team_gear, 1, uplink_items)
uplink.uplink_handler.extra_purchasable += nuke_team.team_discounts uplink.uplink_handler.extra_purchasable += nuke_team.team_discounts
memorize_code() if(nuke_team?.tracked_nuke && nuke_team?.memorized_code)
memorize_code()
/datum/antagonist/nukeop/get_team() /datum/antagonist/nukeop/get_team()
return nuke_team return nuke_team
@@ -78,24 +81,19 @@
/datum/antagonist/nukeop/create_team(datum/team/nuclear/new_team) /datum/antagonist/nukeop/create_team(datum/team/nuclear/new_team)
if(!new_team) if(!new_team)
if(!always_new_team) // Find the first leader to join up
for(var/datum/antagonist/nukeop/N in GLOB.antagonists) for(var/datum/antagonist/nukeop/leader/leader in GLOB.antagonists)
if(!N.owner) if(leader.nuke_team)
stack_trace("Antagonist datum without owner in GLOB.antagonists: [N]") nuke_team = leader.nuke_team
continue return
if(N.nuke_team) // Otherwise make a new team entirely
nuke_team = N.nuke_team nuke_team = new /datum/team/nuclear()
return
nuke_team = new /datum/team/nuclear
nuke_team.update_objectives()
assign_nuke() //This is bit ugly
return return
if(!istype(new_team)) if(!istype(new_team))
stack_trace("Wrong team type passed to [type] initialization.") stack_trace("Wrong team type passed to [type] initialization.")
nuke_team = new_team nuke_team = new_team
/datum/antagonist/nukeop/admin_add(datum/mind/new_owner,mob/admin) /datum/antagonist/nukeop/admin_add(datum/mind/new_owner,mob/admin)
new_owner.set_assigned_role(SSjob.get_job_type(/datum/job/nuclear_operative))
new_owner.add_antag_datum(src) new_owner.add_antag_datum(src)
message_admins("[key_name_admin(admin)] has nuke op'ed [key_name_admin(new_owner)].") message_admins("[key_name_admin(admin)] has nuke op'ed [key_name_admin(new_owner)].")
log_admin("[key_name(admin)] has nuke op'ed [key_name(new_owner)].") log_admin("[key_name(admin)] has nuke op'ed [key_name(new_owner)].")
@@ -158,42 +156,21 @@
else else
to_chat(admin, span_danger("No valid nuke found!")) to_chat(admin, span_danger("No valid nuke found!"))
/datum/antagonist/nukeop/proc/assign_nuke()
if(!nuke_team || nuke_team.tracked_nuke)
return
nuke_team.memorized_code = random_nukecode()
var/obj/machinery/nuclearbomb/syndicate/nuke = locate() in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/syndicate)
if(!nuke)
stack_trace("Syndicate nuke not found during nuke team creation.")
nuke_team.memorized_code = null
return
nuke_team.tracked_nuke = nuke
if(nuke.r_code == NUKE_CODE_UNSET)
nuke.r_code = nuke_team.memorized_code
else //Already set by admins/something else?
nuke_team.memorized_code = nuke.r_code
for(var/obj/machinery/nuclearbomb/beer/beernuke as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/beer))
beernuke.r_code = nuke_team.memorized_code
/datum/antagonist/nukeop/proc/give_alias() /datum/antagonist/nukeop/proc/give_alias()
if(nuke_team?.syndicate_name) if(nuke_team?.syndicate_name)
var/mob/living/carbon/human/human_to_rename = owner.current var/mob/living/carbon/human/human_to_rename = owner.current
if(istype(human_to_rename)) // Reinforcements get a real name if(istype(human_to_rename)) // Reinforcements get a real name
var/first_name = owner.current.client?.prefs?.read_preference(/datum/preference/name/operative_alias) || pick(GLOB.operative_aliases) var/first_name = owner.current.client?.prefs?.read_preference(/datum/preference/name/operative_alias) || pick(GLOB.operative_aliases)
var/chosen_name = "[first_name] [nuke_team.syndicate_name]" var/chosen_name = "[first_name] [nuke_team.syndicate_name]"
human_to_rename.fully_replace_character_name(human_to_rename.real_name, chosen_name) human_to_rename.fully_replace_character_name(null, chosen_name)
else else
var/number = 1 var/number = nuke_team?.members.Find(owner) || 1
number = nuke_team.members.Find(owner) owner.current.fully_replace_character_name(null, "[nuke_team.syndicate_name] Operative #[number]")
owner.current.real_name = "[nuke_team.syndicate_name] Operative #[number]"
/datum/antagonist/nukeop/proc/memorize_code() /datum/antagonist/nukeop/proc/memorize_code()
if(nuke_team && nuke_team.tracked_nuke && nuke_team.memorized_code) antag_memory += "<B>[nuke_team.tracked_nuke] Code</B>: [nuke_team.memorized_code]<br>"
antag_memory += "<B>[nuke_team.tracked_nuke] Code</B>: [nuke_team.memorized_code]<br>" owner.add_memory(/datum/memory/key/nuke_code, nuclear_code = nuke_team.memorized_code)
owner.add_memory(/datum/memory/key/nuke_code, nuclear_code = nuke_team.memorized_code) to_chat(owner, "The nuclear authorization code is: <B>[nuke_team.memorized_code]</B>")
to_chat(owner, "The nuclear authorization code is: <B>[nuke_team.memorized_code]</B>")
else
to_chat(owner, "Unfortunately the syndicate was unable to provide you with nuclear authorization code.")
/// Actually moves our nukie to where they should be /// Actually moves our nukie to where they should be
/datum/antagonist/nukeop/proc/move_to_spawnpoint() /datum/antagonist/nukeop/proc/move_to_spawnpoint()
@@ -212,3 +189,8 @@
team_number = nuke_team.members.Find(owner) team_number = nuke_team.members.Find(owner)
return GLOB.nukeop_start[((team_number - 1) % GLOB.nukeop_start.len) + 1] return GLOB.nukeop_start[((team_number - 1) % GLOB.nukeop_start.len) + 1]
/datum/antagonist/nukeop/on_respawn(mob/new_character)
new_character.forceMove(pick(GLOB.nukeop_start))
equip_op()
return TRUE

View File

@@ -1,29 +1,31 @@
/datum/antagonist/nukeop/leader /datum/antagonist/nukeop/leader
name = "Nuclear Operative Leader" name = "Nuclear Operative Leader"
nukeop_outfit = /datum/outfit/syndicate/leader nukeop_outfit = /datum/outfit/syndicate/leader
always_new_team = TRUE
/// Randomly chosen honorific, for distinction /// Randomly chosen honorific, for distinction
var/title var/title
/// The nuclear challenge remote we will spawn this player with. /// The nuclear challenge remote we will spawn this player with.
var/challengeitem = /obj/item/nuclear_challenge var/challengeitem = /obj/item/nuclear_challenge
/datum/antagonist/nukeop/leader/memorize_code() /datum/antagonist/nukeop/leader/memorize_code()
..() . = ..()
if(nuke_team?.memorized_code) var/obj/item/paper/nuke_code_paper = new(get_turf(owner.current))
var/obj/item/paper/nuke_code_paper = new nuke_code_paper.add_raw_text("The nuclear authorization code is: <b>[nuke_team.memorized_code]</b>")
nuke_code_paper.add_raw_text("The nuclear authorization code is: <b>[nuke_team.memorized_code]</b>") nuke_code_paper.name = "nuclear bomb code"
nuke_code_paper.name = "nuclear bomb code" nuke_code_paper.update_appearance()
var/mob/living/carbon/human/H = owner.current owner.current.put_in_hands(nuke_code_paper)
if(!istype(H))
nuke_code_paper.forceMove(get_turf(H)) /datum/antagonist/nukeop/leader/give_alias()
else title ||= pick("Czar", "Boss", "Commander", "Chief", "Kingpin", "Director", "Overlord")
H.put_in_hands(nuke_code_paper, TRUE) . = ..()
H.update_icons() if(ishuman(owner.current))
owner.current.fully_replace_character_name(owner.current.real_name, "[title] [owner.current.real_name]")
else
owner.current.fully_replace_character_name(owner.current.real_name, "[nuke_team.syndicate_name] [title]")
/datum/antagonist/nukeop/leader/greet() /datum/antagonist/nukeop/leader/greet()
play_stinger() play_stinger()
to_chat(owner, "<span class='warningplain'><B>You are the Syndicate [title] for this mission. You are responsible for guiding the team and your ID is the only one who can open the launch bay doors.</B></span>") to_chat(owner, "<span class='warningplain'><B>You are the Syndicate [title] for this mission. You are responsible for guiding your team.</B></span>")
to_chat(owner, "<span class='warningplain'><B>If you feel you are not up to this task, give your ID and radio to another operative.</B></span>") to_chat(owner, "<span class='warningplain'><B>If you feel you are not up to this task, trade your headset with another operative.</B></span>")
if(!CONFIG_GET(flag/disable_warops)) if(!CONFIG_GET(flag/disable_warops))
to_chat(owner, "<span class='warningplain'><B>In your hand you will find a special item capable of triggering a greater challenge for your team. Examine it carefully and consult with your fellow operatives before activating it.</B></span>") to_chat(owner, "<span class='warningplain'><B>In your hand you will find a special item capable of triggering a greater challenge for your team. Examine it carefully and consult with your fellow operatives before activating it.</B></span>")
owner.announce_objectives() owner.announce_objectives()
@@ -59,3 +61,9 @@
newname = randomname newname = randomname
return capitalize(newname) return capitalize(newname)
/datum/antagonist/nukeop/leader/create_team(datum/team/nuclear/new_team)
if(new_team)
return ..()
// Leaders always make new teams
nuke_team = new /datum/team/nuclear()

View File

@@ -1,22 +1,13 @@
/datum/antagonist/nukeop/lone /datum/antagonist/nukeop/lone
name = "Lone Operative" name = "Lone Operative"
always_new_team = TRUE
send_to_spawnpoint = FALSE //Handled by event send_to_spawnpoint = FALSE //Handled by event
nukeop_outfit = /datum/outfit/syndicate/full/loneop nukeop_outfit = /datum/outfit/syndicate/full/loneop
preview_outfit = /datum/outfit/nuclear_operative preview_outfit = /datum/outfit/nuclear_operative
preview_outfit_behind = null preview_outfit_behind = null
nuke_icon_state = null nuke_icon_state = null
/datum/antagonist/nukeop/lone/assign_nuke() /datum/antagonist/nukeop/lone/create_team(datum/team/nuclear/new_team)
if(nuke_team && !nuke_team.tracked_nuke) if(new_team)
nuke_team.memorized_code = random_nukecode() return ..()
var/obj/machinery/nuclearbomb/selfdestruct/nuke = locate() in SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/selfdestruct) // Lone ops always get a solo team solely because a lot of nukie code is on the team
if(nuke) nuke_team = new /datum/team/nuclear/loneop()
nuke_team.tracked_nuke = nuke
if(nuke.r_code == NUKE_CODE_UNSET)
nuke.r_code = nuke_team.memorized_code
else //Already set by admins/something else?
nuke_team.memorized_code = nuke.r_code
else
stack_trace("Station self-destruct not found during lone op team creation.")
nuke_team.memorized_code = null

View File

@@ -13,6 +13,13 @@
..() ..()
syndicate_name = syndicate_name() syndicate_name = syndicate_name()
var/datum/objective/maingoal = new core_objective()
maingoal.team = src
objectives += maingoal
// when a nuke team is created, the infiltrator has not loaded in yet - it takes some time. so no nuke, we have to wait
addtimer(CALLBACK(src, PROC_REF(assign_nuke_delayed)), 4 SECONDS)
/datum/team/nuclear/roundend_report() /datum/team/nuclear/roundend_report()
var/list/parts = list() var/list/parts = list()
parts += span_header("[syndicate_name] Operatives:") parts += span_header("[syndicate_name] Operatives:")
@@ -129,14 +136,9 @@
/datum/team/nuclear/proc/rename_team(new_name) /datum/team/nuclear/proc/rename_team(new_name)
syndicate_name = new_name syndicate_name = new_name
name = "[syndicate_name] Team" name = "[syndicate_name] Team"
for(var/I in members) for(var/datum/mind/synd_mind in members)
var/datum/mind/synd_mind = I var/datum/antagonist/nukeop/synd_datum = synd_mind.has_antag_datum(/datum/antagonist/nukeop)
var/mob/living/carbon/human/human_to_rename = synd_mind.current synd_datum?.give_alias()
if(!istype(human_to_rename))
continue
var/first_name = human_to_rename.client?.prefs?.read_preference(/datum/preference/name/operative_alias) || pick(GLOB.operative_aliases)
var/chosen_name = "[first_name] [syndicate_name]"
human_to_rename.fully_replace_character_name(human_to_rename.real_name, chosen_name)
/datum/team/nuclear/proc/admin_spawn_reinforcement(mob/admin) /datum/team/nuclear/proc/admin_spawn_reinforcement(mob/admin)
if(!check_rights_for(admin.client, R_ADMIN)) if(!check_rights_for(admin.client, R_ADMIN))
@@ -213,12 +215,6 @@
tgui_alert(admin, "Reinforcement spawned at [infil_or_nukebase] with [tc_to_spawn].", "Reinforcements have arrived", list("God speed")) tgui_alert(admin, "Reinforcement spawned at [infil_or_nukebase] with [tc_to_spawn].", "Reinforcements have arrived", list("God speed"))
/datum/team/nuclear/proc/update_objectives()
if(core_objective)
var/datum/objective/O = new core_objective
O.team = src
objectives += O
/datum/team/nuclear/proc/is_disk_rescued() /datum/team/nuclear/proc/is_disk_rescued()
for(var/obj/item/disk/nuclear/nuke_disk in SSpoints_of_interest.real_nuclear_disks) for(var/obj/item/disk/nuclear/nuke_disk in SSpoints_of_interest.real_nuclear_disks)
//If emergency shuttle is in transit disk is only safe on it //If emergency shuttle is in transit disk is only safe on it
@@ -317,5 +313,42 @@
..() ..()
SEND_SIGNAL(src, COMSIG_NUKE_TEAM_ADDITION, new_member.current) SEND_SIGNAL(src, COMSIG_NUKE_TEAM_ADDITION, new_member.current)
/datum/team/nuclear/proc/assign_nuke_delayed()
assign_nuke()
if(tracked_nuke && memorized_code)
for(var/datum/mind/synd_mind in members)
var/datum/antagonist/nukeop/synd_datum = synd_mind.has_antag_datum(/datum/antagonist/nukeop)
synd_datum?.memorize_code()
/datum/team/nuclear/proc/assign_nuke()
memorized_code = random_nukecode()
var/obj/machinery/nuclearbomb/syndicate/nuke = locate() in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/syndicate)
if(!nuke)
stack_trace("Syndicate nuke not found during nuke team creation.")
memorized_code = null
return
tracked_nuke = nuke
if(nuke.r_code == NUKE_CODE_UNSET)
nuke.r_code = memorized_code
else //Already set by admins/something else?
memorized_code = nuke.r_code
for(var/obj/machinery/nuclearbomb/beer/beernuke as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/beer))
beernuke.r_code = memorized_code
#undef SPAWN_AT_BASE #undef SPAWN_AT_BASE
#undef SPAWN_AT_INFILTRATOR #undef SPAWN_AT_INFILTRATOR
/datum/team/nuclear/loneop
/datum/team/nuclear/loneop/assign_nuke()
memorized_code = random_nukecode()
var/obj/machinery/nuclearbomb/selfdestruct/nuke = locate() in SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/selfdestruct)
if(nuke)
tracked_nuke = nuke
if(nuke.r_code == NUKE_CODE_UNSET)
nuke.r_code = memorized_code
else //Already set by admins/something else?
memorized_code = nuke.r_code
else
stack_trace("Station self-destruct not found during lone op team creation.")
memorized_code = null

View File

@@ -8,12 +8,12 @@
name = "Obsessed" name = "Obsessed"
show_in_antagpanel = TRUE show_in_antagpanel = TRUE
antagpanel_category = ANTAG_GROUP_CREW antagpanel_category = ANTAG_GROUP_CREW
job_rank = ROLE_OBSESSED pref_flag = ROLE_OBSESSED
show_to_ghosts = TRUE show_to_ghosts = TRUE
antag_hud_name = "obsessed" antag_hud_name = "obsessed"
show_name_in_check_antagonists = TRUE show_name_in_check_antagonists = TRUE
roundend_category = "obsessed" roundend_category = "obsessed"
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_SKIP_GLOBAL_LIST
silent = TRUE //not actually silent, because greet will be called by the trauma anyway. silent = TRUE //not actually silent, because greet will be called by the trauma anyway.
suicide_cry = "FOR MY LOVE!!" suicide_cry = "FOR MY LOVE!!"
preview_outfit = /datum/outfit/obsessed preview_outfit = /datum/outfit/obsessed
@@ -31,10 +31,9 @@
show_name_in_check_antagonists = TRUE show_name_in_check_antagonists = TRUE
antagpanel_category = ANTAG_GROUP_CREW antagpanel_category = ANTAG_GROUP_CREW
show_in_roundend = FALSE show_in_roundend = FALSE
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST
silent = TRUE silent = TRUE
can_elimination_hijack = ELIMINATION_PREVENT can_elimination_hijack = ELIMINATION_PREVENT
antag_flags = FLAG_FAKE_ANTAG
/datum/antagonist/obsessed/admin_add(datum/mind/new_owner,mob/admin) /datum/antagonist/obsessed/admin_add(datum/mind/new_owner,mob/admin)
var/mob/living/carbon/C = new_owner.current var/mob/living/carbon/C = new_owner.current
@@ -190,7 +189,7 @@
/datum/objective/assassinate/obsessed/update_explanation_text() /datum/objective/assassinate/obsessed/update_explanation_text()
..() ..()
if(target?.current) if(target?.current)
explanation_text = "Murder [target.name], the [!target_role_type ? target.assigned_role.title : target.special_role]." explanation_text = "Murder [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())]."
else else
message_admins("WARNING! [ADMIN_LOOKUPFLW(owner)] obsessed objectives forged without an obsession!") message_admins("WARNING! [ADMIN_LOOKUPFLW(owner)] obsessed objectives forged without an obsession!")
explanation_text = "Free Objective" explanation_text = "Free Objective"

View File

@@ -1,7 +1,7 @@
/datum/antagonist/paradox_clone /datum/antagonist/paradox_clone
name = "\improper Paradox Clone" name = "\improper Paradox Clone"
roundend_category = "Paradox Clone" roundend_category = "Paradox Clone"
job_rank = ROLE_PARADOX_CLONE pref_flag = ROLE_PARADOX_CLONE
antagpanel_category = ANTAG_GROUP_PARADOX antagpanel_category = ANTAG_GROUP_PARADOX
antag_hud_name = "paradox_clone" antag_hud_name = "paradox_clone"
show_to_ghosts = TRUE show_to_ghosts = TRUE
@@ -28,17 +28,6 @@
return clone_icon return clone_icon
/datum/antagonist/paradox_clone/on_gain()
owner.special_role = ROLE_PARADOX_CLONE
return ..()
/datum/antagonist/paradox_clone/on_removal()
//don't null it if we got a different one added on top, somehow.
if(owner.special_role == ROLE_PARADOX_CLONE)
owner.special_role = null
original_ref = null
return ..()
/datum/antagonist/paradox_clone/Destroy() /datum/antagonist/paradox_clone/Destroy()
original_ref = null original_ref = null
return ..() return ..()
@@ -94,7 +83,7 @@
if(!target?.current) if(!target?.current)
explanation_text = "Free Objective" explanation_text = "Free Objective"
CRASH("WARNING! [ADMIN_LOOKUPFLW(owner)] paradox clone objectives forged without an original!") CRASH("WARNING! [ADMIN_LOOKUPFLW(owner)] paradox clone objectives forged without an original!")
explanation_text = "Murder and replace [target.name], the [!target_role_type ? target.assigned_role.title : target.special_role]. Remember, your mission is to blend in, do not kill anyone else unless you have to!" explanation_text = "Murder and replace [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())]. Remember, your mission is to blend in, do not kill anyone else unless you have to!"
///Static bluespace stream used in its ghost poll icon. ///Static bluespace stream used in its ghost poll icon.
/obj/effect/bluespace_stream /obj/effect/bluespace_stream

View File

@@ -1,6 +1,6 @@
/datum/antagonist/pirate /datum/antagonist/pirate
name = "\improper Space Pirate" name = "\improper Space Pirate"
job_rank = ROLE_TRAITOR pref_flag = ROLE_TRAITOR
roundend_category = "space pirates" roundend_category = "space pirates"
antagpanel_category = ANTAG_GROUP_PIRATES antagpanel_category = ANTAG_GROUP_PIRATES
show_in_antagpanel = FALSE show_in_antagpanel = FALSE

View File

@@ -1,119 +0,0 @@
#define NO_ANSWER 0
#define POSITIVE_ANSWER 1
#define NEGATIVE_ANSWER 2
/datum/round_event_control/pirates
name = "Space Pirates"
typepath = /datum/round_event/pirates
weight = 10
max_occurrences = 1
min_players = 20
dynamic_should_hijack = TRUE
category = EVENT_CATEGORY_INVASION
description = "The crew will either pay up, or face a pirate assault."
admin_setup = list(/datum/event_admin_setup/listed_options/pirates)
map_flags = EVENT_SPACE_ONLY
/datum/round_event_control/pirates/preRunEvent()
if (SSmapping.is_planetary())
return EVENT_CANT_RUN
return ..()
/datum/round_event/pirates
///admin chosen pirate team
var/list/datum/pirate_gang/gang_list
/datum/round_event/pirates/start()
send_pirate_threat(gang_list)
/proc/send_pirate_threat(list/pirate_selection)
var/datum/pirate_gang/chosen_gang = pick_n_take(pirate_selection)
///If there was nothing to pull from our requested list, stop here.
if(!chosen_gang)
message_admins("Error attempting to run the space pirate event, as the given pirate gangs list was empty.")
return
//set payoff
var/payoff = 0
var/datum/bank_account/account = SSeconomy.get_dep_account(ACCOUNT_CAR)
if(account)
payoff = max(PAYOFF_MIN, FLOOR(account.account_balance * 0.80, 1000))
var/datum/comm_message/threat = chosen_gang.generate_message(payoff)
//send message
priority_announce("Incoming subspace communication. Secure channel opened at all communication consoles.", "Incoming Message", SSstation.announcer.get_rand_report_sound())
threat.answer_callback = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pirates_answered), threat, chosen_gang, payoff, world.time)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(spawn_pirates), threat, chosen_gang), RESPONSE_MAX_TIME)
GLOB.communications_controller.send_message(threat, unique = TRUE)
/proc/pirates_answered(datum/comm_message/threat, datum/pirate_gang/chosen_gang, payoff, initial_send_time)
if(world.time > initial_send_time + RESPONSE_MAX_TIME)
priority_announce(chosen_gang.response_too_late, sender_override = chosen_gang.ship_name, color_override = chosen_gang.announcement_color)
return
if(!threat?.answered)
return
if(threat.answered == NEGATIVE_ANSWER)
priority_announce(chosen_gang.response_rejected, sender_override = chosen_gang.ship_name, color_override = chosen_gang.announcement_color)
return
var/datum/bank_account/plundered_account = SSeconomy.get_dep_account(ACCOUNT_CAR)
if(plundered_account)
if(plundered_account.adjust_money(-payoff))
chosen_gang.paid_off = TRUE
priority_announce(chosen_gang.response_received, sender_override = chosen_gang.ship_name, color_override = chosen_gang.announcement_color)
else
priority_announce(chosen_gang.response_not_enough, sender_override = chosen_gang.ship_name, color_override = chosen_gang.announcement_color)
/proc/spawn_pirates(datum/comm_message/threat, datum/pirate_gang/chosen_gang)
if(chosen_gang.paid_off)
return
var/list/candidates = SSpolling.poll_ghost_candidates("Do you wish to be considered for a [span_notice("pirate crew of [chosen_gang.name]?")]", check_jobban = ROLE_TRAITOR, alert_pic = /obj/item/claymore/cutlass, role_name_text = "pirate crew")
shuffle_inplace(candidates)
var/template_key = "pirate_[chosen_gang.ship_template_id]"
var/datum/map_template/shuttle/pirate/ship = SSmapping.shuttle_templates[template_key]
var/x = rand(TRANSITIONEDGE,world.maxx - TRANSITIONEDGE - ship.width)
var/y = rand(TRANSITIONEDGE,world.maxy - TRANSITIONEDGE - ship.height)
var/z = SSmapping.empty_space.z_value
var/turf/T = locate(x,y,z)
if(!T)
CRASH("Pirate event found no turf to load in")
if(!ship.load(T))
CRASH("Loading pirate ship failed!")
for(var/turf/area_turf as anything in ship.get_affected_turfs(T))
for(var/obj/effect/mob_spawn/ghost_role/human/pirate/spawner in area_turf)
if(candidates.len > 0)
var/mob/our_candidate = candidates[1]
var/mob/spawned_mob = spawner.create_from_ghost(our_candidate)
candidates -= our_candidate
notify_ghosts(
"The [chosen_gang.ship_name] has an object of interest: [spawned_mob]!",
source = spawned_mob,
header = "Pirates!",
)
else
notify_ghosts(
"The [chosen_gang.ship_name] has an object of interest: [spawner]!",
source = spawner,
header = "Pirate Spawn Here!",
)
priority_announce(chosen_gang.arrival_announcement, sender_override = chosen_gang.ship_name)
/datum/event_admin_setup/listed_options/pirates
input_text = "Select Pirate Gang"
normal_run_option = "Random Pirate Gang"
/datum/event_admin_setup/listed_options/pirates/get_list()
return subtypesof(/datum/pirate_gang)
/datum/event_admin_setup/listed_options/pirates/apply_to_event(datum/round_event/pirates/event)
if(isnull(chosen))
event.gang_list = GLOB.light_pirate_gangs + GLOB.heavy_pirate_gangs
else
event.gang_list = list(new chosen)
#undef NO_ANSWER
#undef POSITIVE_ANSWER
#undef NEGATIVE_ANSWER

View File

@@ -5,6 +5,7 @@
show_in_antagpanel = FALSE show_in_antagpanel = FALSE
show_name_in_check_antagonists = TRUE show_name_in_check_antagonists = TRUE
show_to_ghosts = TRUE show_to_ghosts = TRUE
pref_flag = ROLE_PYROCLASTIC_SLIME
/datum/antagonist/pyro_slime/on_gain() /datum/antagonist/pyro_slime/on_gain()
forge_objectives() forge_objectives()

View File

@@ -23,7 +23,7 @@
/datum/antagonist/enemy_of_the_state/on_gain() /datum/antagonist/enemy_of_the_state/on_gain()
owner.add_memory(/datum/memory/revolution_rev_defeat) owner.add_memory(/datum/memory/revolution_rev_defeat)
owner.special_role = "exiled headrev" // LAZYADD(owner.special_statuses, "Exiled Head Revolutionary")
forge_objectives() forge_objectives()
. = ..() . = ..()

View File

@@ -2,7 +2,7 @@
name = "\improper Revolutionary" name = "\improper Revolutionary"
roundend_category = "revolutionaries" // if by some miracle revolutionaries without revolution happen roundend_category = "revolutionaries" // if by some miracle revolutionaries without revolution happen
antagpanel_category = "Revolution" antagpanel_category = "Revolution"
job_rank = ROLE_REV pref_flag = ROLE_REV
antag_moodlet = /datum/mood_event/revolution antag_moodlet = /datum/mood_event/revolution
antag_hud_name = "rev" antag_hud_name = "rev"
suicide_cry = "VIVA LA REVOLUTION!!" suicide_cry = "VIVA LA REVOLUTION!!"
@@ -55,14 +55,9 @@
/datum/antagonist/rev/on_gain() /datum/antagonist/rev/on_gain()
. = ..() . = ..()
create_objectives()
equip_rev() equip_rev()
owner.current.log_message("has been converted to the revolution!", LOG_ATTACK, color="red") owner.current.log_message("has been converted to the revolution!", LOG_ATTACK, color="red")
/datum/antagonist/rev/on_removal()
remove_objectives()
. = ..()
/datum/antagonist/rev/greet() /datum/antagonist/rev/greet()
. = ..() . = ..()
to_chat(owner, span_userdanger("Help your cause. Do not harm your fellow freedom fighters. You can identify your comrades by the red \"R\" icons, and your leaders by the blue \"R\" icons. Help them kill the heads to win the revolution!")) to_chat(owner, span_userdanger("Help your cause. Do not harm your fellow freedom fighters. You can identify your comrades by the red \"R\" icons, and your leaders by the blue \"R\" icons. Help them kill the heads to win the revolution!"))
@@ -70,16 +65,9 @@
/datum/antagonist/rev/create_team(datum/team/revolution/new_team) /datum/antagonist/rev/create_team(datum/team/revolution/new_team)
if(!new_team) if(!new_team)
//For now only one revolution at a time GLOB.revolution_handler ||= new()
for(var/datum/antagonist/rev/head/H in GLOB.antagonists) rev_team = GLOB.revolution_handler.revs
if(!H.owner) GLOB.revolution_handler.start_revolution()
continue
if(H.rev_team)
rev_team = H.rev_team
return
rev_team = new /datum/team/revolution
rev_team.update_objectives()
rev_team.update_rev_heads()
return return
if(!istype(new_team)) if(!istype(new_team))
stack_trace("Wrong team type passed to [type] initialization.") stack_trace("Wrong team type passed to [type] initialization.")
@@ -88,12 +76,6 @@
/datum/antagonist/rev/get_team() /datum/antagonist/rev/get_team()
return rev_team return rev_team
/datum/antagonist/rev/proc/create_objectives()
objectives |= rev_team.objectives
/datum/antagonist/rev/proc/remove_objectives()
objectives -= rev_team.objectives
//Bump up to head_rev //Bump up to head_rev
/datum/antagonist/rev/proc/promote() /datum/antagonist/rev/proc/promote()
var/old_team = rev_team var/old_team = rev_team
@@ -171,7 +153,7 @@
/datum/antagonist/rev/head /datum/antagonist/rev/head
name = "\improper Head Revolutionary" name = "\improper Head Revolutionary"
antag_hud_name = "rev_head" antag_hud_name = "rev_head"
job_rank = ROLE_REV_HEAD pref_flag = ROLE_REV_HEAD
preview_outfit = /datum/outfit/revolutionary preview_outfit = /datum/outfit/revolutionary
hardcore_random_bonus = TRUE hardcore_random_bonus = TRUE
@@ -298,7 +280,6 @@
rev_mind.add_memory(/datum/memory/recruited_by_headrev, protagonist = rev_mind.current, antagonist = owner.current) rev_mind.add_memory(/datum/memory/recruited_by_headrev, protagonist = rev_mind.current, antagonist = owner.current)
rev_mind.add_antag_datum(/datum/antagonist/rev,rev_team) rev_mind.add_antag_datum(/datum/antagonist/rev,rev_team)
rev_mind.special_role = ROLE_REV
return TRUE return TRUE
/datum/antagonist/rev/head/proc/demote() /datum/antagonist/rev/head/proc/demote()
@@ -342,7 +323,6 @@
owner.current.log_message("has been deconverted from the revolution by [ismob(deconverter) ? key_name(deconverter) : deconverter]!", LOG_ATTACK, color=COLOR_CULT_RED) owner.current.log_message("has been deconverted from the revolution by [ismob(deconverter) ? key_name(deconverter) : deconverter]!", LOG_ATTACK, color=COLOR_CULT_RED)
if(deconverter == DECONVERTER_BORGED) if(deconverter == DECONVERTER_BORGED)
message_admins("[ADMIN_LOOKUPFLW(owner.current)] has been borged while being a [name]") message_admins("[ADMIN_LOOKUPFLW(owner.current)] has been borged while being a [name]")
owner.special_role = null
if(iscarbon(owner.current) && deconverter) if(iscarbon(owner.current) && deconverter)
var/mob/living/carbon/formerrev = owner.current var/mob/living/carbon/formerrev = owner.current
formerrev.Unconscious(10 SECONDS) formerrev.Unconscious(10 SECONDS)
@@ -382,40 +362,19 @@
/datum/team/revolution /datum/team/revolution
name = "\improper Revolution" name = "\improper Revolution"
/// Maximum number of headrevs /// Maximum number of headrevs
var/max_headrevs = 3 var/max_headrevs = 3
/// List of all ex-headrevs. Useful because dynamic removes antag status when it ends, so this can be kept for the roundend report. /// List of all ex-headrevs. Useful because dynamic removes antag status when it ends, so this can be kept for the roundend report.
var/list/ex_headrevs = list() var/list/datum/mind/ex_headrevs = list()
/// List of all ex-revs. Useful because dynamic removes antag status when it ends, so this can be kept for the roundend report. /// List of all ex-revs. Useful because dynamic removes antag status when it ends, so this can be kept for the roundend report.
var/list/ex_revs = list() var/list/datum/mind/ex_revs = list()
/// The objective of the heads of staff, aka to kill the headrevs. /// Saves all current headrevs and revs
var/list/datum/objective/mutiny/heads_objective = list() /datum/team/revolution/proc/save_members()
ex_headrevs = get_head_revolutionaries()
/// Proc called on periodic timer. ex_revs = members - ex_headrevs
/// Updates the rev team's objectives to make sure all heads are targets, useful when new heads latejoin.
/// Propagates all objectives to all revs.
/datum/team/revolution/proc/update_objectives(initial = FALSE)
var/untracked_heads = SSjob.get_all_heads()
for(var/datum/objective/mutiny/mutiny_objective in objectives)
untracked_heads -= mutiny_objective.target
for(var/datum/mind/extra_mutiny_target in untracked_heads)
var/datum/objective/mutiny/new_target = new()
new_target.team = src
new_target.target = extra_mutiny_target
new_target.update_explanation_text()
objectives += new_target
for(var/datum/mind/rev_member in members)
var/datum/antagonist/rev/rev_antag = rev_member.has_antag_datum(/datum/antagonist/rev)
rev_antag.objectives |= objectives
addtimer(CALLBACK(src, PROC_REF(update_objectives)), HEAD_UPDATE_PERIOD, TIMER_UNIQUE)
/// Returns a list of all headrevs. /// Returns a list of all headrevs.
/datum/team/revolution/proc/get_head_revolutionaries() /datum/team/revolution/proc/get_head_revolutionaries()
@@ -427,129 +386,39 @@
return headrev_list return headrev_list
/// Proc called on periodic timer. /datum/team/revolution/proc/headrev_cap()
var/list/datum/mind/heads = SSjob.get_all_heads()
var/list/sec = SSjob.get_all_sec()
return clamp(round(length(heads) - ((8 - length(sec)) / 3)), 1, max_headrevs)
/// Tries to make sure an appropriate number of headrevs are part of the revolution. /// Tries to make sure an appropriate number of headrevs are part of the revolution.
/// Will promote up revs to headrevs as necessary based on the hard max_headrevs cap and the soft cap based on the number of heads of staff and sec. /// Will promote up revs to headrevs as necessary based on the hard max_headrevs cap and the soft cap based on the number of heads of staff and sec.
/datum/team/revolution/proc/update_rev_heads() /datum/team/revolution/proc/update_rev_heads()
if(SSticker.HasRoundStarted()) var/list/datum/mind/head_revolutionaries = get_head_revolutionaries()
var/list/datum/mind/head_revolutionaries = get_head_revolutionaries()
var/list/datum/mind/heads = SSjob.get_all_heads()
var/list/sec = SSjob.get_all_sec()
if(head_revolutionaries.len < max_headrevs && head_revolutionaries.len < round(heads.len - ((8 - sec.len) / 3))) if(length(head_revolutionaries) >= headrev_cap())
var/list/datum/mind/non_heads = members - head_revolutionaries
var/list/datum/mind/promotable = list()
var/list/datum/mind/monkey_promotable = list()
for(var/datum/mind/khrushchev in non_heads)
if(khrushchev.current && !khrushchev.current.incapacitated && !HAS_TRAIT(khrushchev.current, TRAIT_RESTRAINED) && khrushchev.current.client)
if((ROLE_REV_HEAD in khrushchev.current.client.prefs.be_special) || (ROLE_PROVOCATEUR in khrushchev.current.client.prefs.be_special))
if(!ismonkey(khrushchev.current))
promotable += khrushchev
else
monkey_promotable += khrushchev
if(!promotable.len && monkey_promotable.len) //if only monkey revolutionaries remain, promote one of them to the leadership.
promotable = monkey_promotable
if(promotable.len)
var/datum/mind/new_leader = pick(promotable)
var/datum/antagonist/rev/rev = new_leader.has_antag_datum(/datum/antagonist/rev)
rev.promote()
addtimer(CALLBACK(src, PROC_REF(update_rev_heads)),HEAD_UPDATE_PERIOD,TIMER_UNIQUE)
/// Saves a list of all ex-headrevs and a list of all revs.
/datum/team/revolution/proc/save_members()
ex_headrevs = get_antag_minds(/datum/antagonist/rev/head, TRUE)
ex_revs = get_antag_minds(/datum/antagonist/rev, TRUE)
/// Checks if revs have won
/datum/team/revolution/proc/check_rev_victory()
for(var/datum/objective/mutiny/objective in objectives)
if(!(objective.check_completion()))
return FALSE
return TRUE
/// Checks if heads have won
/datum/team/revolution/proc/check_heads_victory()
// List of headrevs we're currently tracking
var/list/included_headrevs = list()
// List of current headrevs
var/list/current_headrevs = get_head_revolutionaries()
// A copy of the head of staff objective list, since we're going to be modifying the original list.
var/list/heads_objective_copy = heads_objective.Copy()
var/objective_complete = TRUE
// Here, we check current head of staff objectives and remove them if the target doesn't exist as a headrev anymore
for(var/datum/objective/mutiny/objective in heads_objective_copy)
if(!(objective.target in current_headrevs))
heads_objective -= objective
continue
if(!objective.check_completion())
objective_complete = FALSE
included_headrevs += objective.target
// Here, we check current headrevs and add them as objectives if they didn't exist as a head of staff objective before.
// Additionally, we make sure the objective is not completed by running the check_completion check on them.
for(var/datum/mind/rev_mind as anything in current_headrevs)
if(!(rev_mind in included_headrevs))
var/datum/objective/mutiny/objective = new()
objective.target = rev_mind
if(!objective.check_completion())
objective_complete = FALSE
heads_objective += objective
return objective_complete
/// Updates the state of the world depending on if revs won or loss.
/// Returns who won, at which case this method should no longer be called.
/datum/team/revolution/proc/process_victory()
if (check_rev_victory())
victory_effects()
return REVOLUTION_VICTORY
if (!check_heads_victory())
return return
. = STATION_VICTORY var/list/datum/mind/promotable = list()
var/list/datum/mind/monkey_promotable = list()
SSshuttle.clearHostileEnvironment(src) for(var/datum/mind/khrushchev as anything in members - head_revolutionaries)
if(!can_be_headrev(khrushchev))
// Save rev lists before we remove the antag datums.
save_members()
// Remove everyone as a revolutionary
for (var/datum/mind/rev_mind as anything in members)
var/datum/antagonist/rev/rev_antag = rev_mind.has_antag_datum(/datum/antagonist/rev)
if (!isnull(rev_antag))
rev_antag.remove_revolutionary(DECONVERTER_STATION_WIN)
if(rev_mind in ex_headrevs)
LAZYADD(rev_mind.special_statuses, "<span class='bad'>Former head revolutionary</span>")
else
LAZYADD(rev_mind.special_statuses, "<span class='bad'>Former revolutionary</span>")
defeat_effects()
/// Handles any pre-round-ending effects on rev victory. An example use case is recording memories.
/datum/team/revolution/proc/victory_effects()
for(var/datum/mind/headrev_mind as anything in ex_headrevs)
var/mob/living/real_headrev = headrev_mind.current
if(isnull(real_headrev))
continue continue
add_memory_in_range(real_headrev, 5, /datum/memory/revolution_rev_victory, protagonist = real_headrev) var/client/khruschevs_client = GET_CLIENT(khrushchev.current)
if(!(ROLE_REV_HEAD in khruschevs_client.prefs.be_special) && !(ROLE_PROVOCATEUR in khruschevs_client.prefs.be_special))
/// Handles effects of revs losing, such as making ex-headrevs unrevivable and setting up head of staff memories. continue
/datum/team/revolution/proc/defeat_effects() if(ismonkey(khrushchev.current))
// If the revolution was quelled, make rev heads unable to be revived through pods monkey_promotable += khrushchev
for (var/datum/mind/rev_head as anything in ex_headrevs) else
if(!isnull(rev_head.current)) promotable += khrushchev
ADD_TRAIT(rev_head.current, TRAIT_DEFIB_BLACKLISTED, REF(src)) if(!length(promotable) && length(monkey_promotable))
promotable = monkey_promotable
for(var/datum/objective/mutiny/head_tracker in objectives) if(!length(promotable))
var/mob/living/head_of_staff = head_tracker.target?.current return
if(!isnull(head_of_staff)) var/datum/mind/new_leader = pick(promotable)
add_memory_in_range(head_of_staff, 5, /datum/memory/revolution_heads_victory, protagonist = head_of_staff) var/datum/antagonist/rev/rev = new_leader.has_antag_datum(/datum/antagonist/rev)
rev.promote()
priority_announce("It appears the mutiny has been quelled. Please return yourself and your incapacitated colleagues to work. \
We have remotely blacklisted the head revolutionaries in your medical records to prevent accidental revival.", null, null, null, "[command_name()] Loyalty Monitoring Division")
/// Mutates the ticker to report that the revs have won /// Mutates the ticker to report that the revs have won
/datum/team/revolution/proc/round_result(finished) /datum/team/revolution/proc/round_result(finished)
@@ -595,13 +464,13 @@
if(headrevs.len) if(headrevs.len)
var/list/headrev_part = list() var/list/headrev_part = list()
headrev_part += span_header("The head revolutionaries were:") headrev_part += span_header("The head revolutionaries were:")
headrev_part += printplayerlist(headrevs, !check_rev_victory()) headrev_part += printplayerlist(headrevs, GLOB.revolution_handler.result != REVOLUTION_VICTORY)
result += headrev_part.Join("<br>") result += headrev_part.Join("<br>")
if(revs.len) if(revs.len)
var/list/rev_part = list() var/list/rev_part = list()
rev_part += span_header("The revolutionaries were:") rev_part += span_header("The revolutionaries were:")
rev_part += printplayerlist(revs, !check_rev_victory()) rev_part += printplayerlist(revs, GLOB.revolution_handler.result != REVOLUTION_VICTORY)
result += rev_part.Join("<br>") result += rev_part.Join("<br>")
var/list/heads = SSjob.get_all_heads() var/list/heads = SSjob.get_all_heads()

View File

@@ -0,0 +1,159 @@
GLOBAL_DATUM(revolution_handler, /datum/revolution_handler)
/datum/revolution_handler
/// The revolution team
var/datum/team/revolution/revs
/// The objective of the heads of staff, aka to kill the headrevs.
var/list/datum/objective/mutiny/heads_objective = list()
/// Cooldown between head revs being promoted
COOLDOWN_DECLARE(rev_head_promote_cd)
var/result
/datum/revolution_handler/New()
revs = new()
/datum/revolution_handler/proc/start_revolution()
if((datum_flags & DF_ISPROCESSING) || result)
return
START_PROCESSING(SSprocessing, src)
SSshuttle.registerHostileEnvironment(src)
for(var/datum/mind/mutiny_target as anything in SSjob.get_all_heads())
var/datum/objective/mutiny/new_target = new()
new_target.team = revs
new_target.target = mutiny_target
new_target.update_explanation_text()
revs.objectives += new_target
RegisterSignal(SSdcs, COMSIG_GLOB_JOB_AFTER_LATEJOIN_SPAWN, PROC_REF(update_objectives))
COOLDOWN_START(src, rev_head_promote_cd, 5 MINUTES)
/datum/revolution_handler/proc/cleanup()
STOP_PROCESSING(SSprocessing, src)
SSshuttle.clearHostileEnvironment(src)
UnregisterSignal(SSdcs, COMSIG_GLOB_JOB_AFTER_LATEJOIN_SPAWN)
/datum/revolution_handler/process(seconds_per_tick)
if(check_rev_victory())
declare_revs_win()
. = PROCESS_KILL
else if(check_heads_victory())
declare_heads_win()
. = PROCESS_KILL
if(. == PROCESS_KILL)
cleanup()
return .
if(COOLDOWN_FINISHED(src, rev_head_promote_cd))
revs.update_rev_heads()
COOLDOWN_START(src, rev_head_promote_cd, 5 MINUTES)
return .
/datum/revolution_handler/proc/update_objectives(datum/source, datum/job/job, mob/living/spawned)
SIGNAL_HANDLER
if(!(job.job_flags & JOB_HEAD_OF_STAFF))
return
var/datum/objective/mutiny/new_target = new()
new_target.team = revs
new_target.target = spawned.mind
new_target.update_explanation_text()
revs.objectives += new_target
/datum/revolution_handler/proc/declare_revs_win()
for(var/datum/mind/headrev_mind as anything in revs.ex_headrevs)
var/mob/living/real_headrev = headrev_mind.current
if(isnull(real_headrev))
continue
add_memory_in_range(real_headrev, 5, /datum/memory/revolution_rev_victory, protagonist = real_headrev)
result = REVOLUTION_VICTORY
/datum/revolution_handler/proc/declare_heads_win()
// Save rev lists before we remove the antag datums.
revs.save_members()
// Remove everyone as a revolutionary
for(var/datum/mind/rev_mind as anything in revs.members)
var/datum/antagonist/rev/rev_antag = rev_mind.has_antag_datum(/datum/antagonist/rev)
if (!isnull(rev_antag))
rev_antag.remove_revolutionary(DECONVERTER_STATION_WIN)
if(rev_mind in revs.ex_headrevs)
LAZYADD(rev_mind.special_roles, "Former Head Revolutionary")
else
LAZYADD(rev_mind.special_roles, "Former Revolutionary")
// If the revolution was quelled, make rev heads unable to be revived through pods
for(var/datum/mind/rev_head as anything in revs.ex_headrevs)
if(!isnull(rev_head.current))
ADD_TRAIT(rev_head.current, TRAIT_DEFIB_BLACKLISTED, REF(src))
for(var/datum/objective/mutiny/head_tracker in revs.objectives)
var/mob/living/head_of_staff = head_tracker.target?.current
if(!isnull(head_of_staff))
add_memory_in_range(head_of_staff, 5, /datum/memory/revolution_heads_victory, protagonist = head_of_staff)
priority_announce("It appears the mutiny has been quelled. Please return yourself and your incapacitated colleagues to work. \
We have remotely blacklisted the head revolutionaries in your medical records to prevent accidental revival.", null, null, null, "[command_name()] Loyalty Monitoring Division")
result = STATION_VICTORY
/datum/revolution_handler/proc/check_rev_victory()
for(var/datum/objective/mutiny/objective in revs.objectives)
if(!(objective.check_completion()))
return FALSE
return TRUE
/datum/revolution_handler/proc/check_heads_victory()
// List of headrevs we're currently tracking
var/list/included_headrevs = list()
// List of current headrevs
var/list/current_headrevs = revs.get_head_revolutionaries()
// A copy of the head of staff objective list, since we're going to be modifying the original list.
var/list/heads_objective_copy = heads_objective.Copy()
var/objective_complete = TRUE
// Here, we check current head of staff objectives and remove them if the target doesn't exist as a headrev anymore
for(var/datum/objective/mutiny/objective in heads_objective_copy)
if(!(objective.target in current_headrevs))
heads_objective -= objective
continue
if(!objective.check_completion())
objective_complete = FALSE
included_headrevs += objective.target
// Here, we check current headrevs and add them as objectives if they didn't exist as a head of staff objective before.
// Additionally, we make sure the objective is not completed by running the check_completion check on them.
for(var/datum/mind/rev_mind as anything in current_headrevs)
if(!(rev_mind in included_headrevs))
var/datum/objective/mutiny/objective = new()
objective.target = rev_mind
if(!objective.check_completion())
objective_complete = FALSE
heads_objective += objective
return objective_complete
/// Checks if someone is valid to be a headrev
/proc/can_be_headrev(datum/mind/candidate)
var/turf/head_turf = get_turf(candidate.current)
if(considered_afk(candidate))
return FALSE
if(!considered_alive(candidate))
return FALSE
if(!is_station_level(head_turf.z))
return FALSE
if(candidate.current.is_antag())
return FALSE
if(candidate.assigned_role.job_flags & JOB_HEAD_OF_STAFF)
return FALSE
if(HAS_MIND_TRAIT(candidate.current, TRAIT_UNCONVERTABLE))
return FALSE
return TRUE

View File

@@ -2,7 +2,7 @@
name = "\improper Sentient Creature" name = "\improper Sentient Creature"
show_in_antagpanel = FALSE show_in_antagpanel = FALSE
show_in_roundend = FALSE show_in_roundend = FALSE
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST
ui_name = "AntagInfoSentient" ui_name = "AntagInfoSentient"
/datum/antagonist/sentient_creature/get_preview_icon() /datum/antagonist/sentient_creature/get_preview_icon()

View File

@@ -10,7 +10,7 @@
show_in_roundend = FALSE show_in_roundend = FALSE
silent = TRUE silent = TRUE
ui_name = "AntagInfoShade" ui_name = "AntagInfoShade"
count_against_dynamic_roll_chance = FALSE antag_flags = ANTAG_SKIP_GLOBAL_LIST
/// Name of this shade's master. /// Name of this shade's master.
var/master_name = "nobody?" var/master_name = "nobody?"

Some files were not shown because too many files have changed in this diff Show More