From e0bdfc3f5ffd98ce0a6b0e6b6a7e060700204540 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:36:10 -0500 Subject: [PATCH] Dynamic Rework (#91290) 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. 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 :cl: 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 /:cl: (cherry picked from commit 4c277dc5727c6bd8344a5b508400370863de5deb) --- code/__DEFINES/antagonists.dm | 9 +- .../signals/signals_mob/signals_mob_main.dm | 2 +- .../dcs/signals/signals_subsystem.dm | 3 + code/__DEFINES/dynamic.dm | 46 - code/__DEFINES/jobs.dm | 4 + code/__DEFINES/role_preferences.dm | 90 +- code/__HELPERS/events.dm | 2 - code/__HELPERS/game.dm | 10 +- code/__HELPERS/logging/dynamic.dm | 5 - code/__HELPERS/logging/manifest.dm | 5 +- code/__HELPERS/roundend.dm | 12 +- code/controllers/subsystem/blackbox.dm | 4 +- .../subsystem/dynamic/__dynamic_defines.dm | 55 + .../subsystem/dynamic/_dynamic_ruleset.dm | 372 ++++ .../subsystem/dynamic/_dynamic_tier.dm | 316 ++++ code/controllers/subsystem/dynamic/dynamic.dm | 1665 +++++++---------- .../subsystem/dynamic/dynamic_admin.dm | 254 +++ .../subsystem/dynamic/dynamic_hijacking.dm | 25 - .../subsystem/dynamic/dynamic_logging.dm | 101 - .../dynamic/dynamic_midround_rolling.dm | 108 -- .../dynamic/dynamic_ruleset_latejoin.dm | 128 ++ .../dynamic/dynamic_ruleset_midround.dm | 1150 ++++++++++++ .../dynamic/dynamic_ruleset_roundstart.dm | 433 +++++ .../subsystem/dynamic/dynamic_rulesets.dm | 310 --- .../dynamic/dynamic_rulesets_latejoin.dm | 263 --- .../dynamic/dynamic_rulesets_midround.dm | 1009 ---------- .../dynamic/dynamic_rulesets_roundstart.dm | 747 -------- .../subsystem/dynamic/dynamic_testing.dm | 111 ++ .../dynamic/dynamic_unfavorable_situation.dm | 74 - code/controllers/subsystem/dynamic/readme.md | 202 -- .../subsystem/dynamic/ruleset_picking.dm | 159 -- code/controllers/subsystem/job.dm | 31 +- code/controllers/subsystem/polling.dm | 5 +- code/controllers/subsystem/ticker.dm | 124 +- code/datums/ai_laws/ai_laws.dm | 4 +- code/datums/communications.dm | 74 + code/datums/mind/_mind.dm | 18 +- code/datums/mind/antag.dm | 54 +- code/datums/mind/initialization.dm | 1 - .../proximity_monitor/fields/heretic_arena.dm | 2 +- code/datums/station_traits/_station_trait.dm | 9 - code/datums/station_traits/job_traits.dm | 20 - code/datums/station_traits/negative_traits.dm | 1 - code/datums/station_traits/neutral_traits.dm | 17 +- code/game/gamemodes/objective.dm | 14 +- .../game/machinery/computer/communications.dm | 58 +- code/game/machinery/wishgranter.dm | 2 +- .../anomalies/anomalies_pyroclastic.dm | 1 - .../objects/effects/poster_motivational.dm | 4 +- .../game/objects/items/devices/aicard_evil.dm | 2 +- code/game/objects/items/religion.dm | 2 +- code/game/objects/items/robot/robot_parts.dm | 2 +- .../game/objects/items/storage/uplink_kits.dm | 8 +- code/modules/admin/admin.dm | 148 +- code/modules/admin/antag_panel.dm | 22 +- code/modules/admin/check_antagonists.dm | 4 +- code/modules/admin/player_panel.dm | 2 +- code/modules/admin/topic.dm | 121 +- code/modules/admin/verbs/admingame.dm | 33 +- code/modules/admin/verbs/adminhelp.dm | 4 +- .../antagonists/_common/antag_datum.dm | 43 +- .../antagonists/_common/antag_helpers.dm | 3 +- .../antagonists/_common/antag_spawner.dm | 10 +- code/modules/antagonists/abductor/abductor.dm | 4 +- .../antagonists/ashwalker/ashwalker.dm | 5 +- .../battlecruiser/battlecruiser.dm | 4 +- code/modules/antagonists/blob/blob_antag.dm | 4 +- .../antagonists/brainwashing/brainwashing.dm | 4 +- code/modules/antagonists/brother/brother.dm | 7 +- .../antagonists/changeling/changeling.dm | 4 +- .../changeling/fallen_changeling.dm | 3 +- code/modules/antagonists/clown_ops/clownop.dm | 11 +- .../antagonists/cult/datums/cultist.dm | 2 +- code/modules/antagonists/cult/runes.dm | 3 +- code/modules/antagonists/ert/ert.dm | 3 +- .../antagonists/evil_clone/evil_clone.dm | 4 +- code/modules/antagonists/fugitive/fugitive.dm | 15 +- .../antagonists/fugitive/hunters/hunter.dm | 3 +- .../antagonists/greentext/greentext.dm | 2 +- .../antagonists/heretic/heretic_antag.dm | 2 +- .../antagonists/heretic/heretic_monsters.dm | 2 +- .../heretic/soultrapped_heretic.dm | 2 +- .../antagonists/highlander/highlander.dm | 3 +- .../antagonists/hypnotized/hypnotized.dm | 4 +- code/modules/antagonists/malf_ai/malf_ai.dm | 4 +- .../antagonists/nightmare/nightmare.dm | 2 +- .../antagonists/nukeop/datums/operative.dm | 70 +- .../nukeop/datums/operative_leader.dm | 36 +- .../nukeop/datums/operative_lone.dm | 19 +- .../nukeop/datums/operative_team.dm | 61 +- code/modules/antagonists/obsessed/obsessed.dm | 9 +- .../paradox_clone/paradox_clone.dm | 15 +- code/modules/antagonists/pirate/pirate.dm | 2 +- .../antagonists/pirate/pirate_event.dm | 119 -- .../antagonists/pyro_slime/pyro_slime.dm | 1 + .../revolution/enemy_of_the_state.dm | 2 +- .../antagonists/revolution/revolution.dm | 209 +-- .../revolution/revolution_handler.dm | 159 ++ .../sentient_creature/sentient_creature.dm | 2 +- .../modules/antagonists/shade/shade_minion.dm | 2 +- .../antagonists/space_dragon/space_dragon.dm | 4 +- .../antagonists/space_ninja/space_ninja.dm | 11 +- code/modules/antagonists/spy/spy.dm | 2 +- .../antagonists/survivalist/survivalist.dm | 1 - .../syndicate_monkey/syndicate_monkey.dm | 2 +- .../traitor/components/demoraliser.dm | 2 +- .../traitor/contractor/contract_teammate.dm | 2 +- .../antagonists/traitor/datum_traitor.dm | 10 +- .../antagonists/valentines/valentine.dm | 4 +- .../antagonists/voidwalker/voidwalker.dm | 2 +- .../antagonists/wishgranter/wishgranter.dm | 1 - .../equipment/spellbook_entries/summons.dm | 23 +- .../wizard/equipment/wizard_spellbook.dm | 2 +- .../wizard/grand_ritual/finales/armageddon.dm | 6 +- .../finales/grand_ritual_finale.dm | 2 +- .../antagonists/wizard/slaughter_antag.dm | 2 +- code/modules/antagonists/wizard/wizard.dm | 7 +- code/modules/antagonists/xeno/xeno.dm | 5 +- .../modules/bitrunning/antagonists/_parent.dm | 2 +- .../bitrunning/antagonists/ghost_role.dm | 2 +- code/modules/bitrunning/event.dm | 1 - code/modules/bitrunning/server/threats.dm | 1 - .../client/preferences/middleware/antags.dm | 102 +- code/modules/client/preferences_savefile.dm | 2 +- code/modules/client/verbs/who.dm | 2 +- code/modules/clothing/outfits/event.dm | 1 - code/modules/events/_event.dm | 6 - code/modules/events/creep_awakening.dm | 30 - code/modules/events/dynamic_tweak.dm | 35 + code/modules/events/false_alarm.dm | 77 +- code/modules/events/ghost_role/abductor.dm | 43 - .../events/ghost_role/alien_infestation.dm | 82 - code/modules/events/ghost_role/blob.dm | 44 - .../events/ghost_role/changeling_event.dm | 29 - .../events/ghost_role/fugitive_event.dm | 192 -- code/modules/events/ghost_role/morph_event.dm | 35 - code/modules/events/ghost_role/nightmare.dm | 38 - code/modules/events/ghost_role/operative.dm | 4 +- .../events/ghost_role/revenant_event.dm | 61 - .../events/ghost_role/slaughter_event.dm | 36 - .../modules/events/ghost_role/space_dragon.dm | 35 - code/modules/events/ghost_role/space_ninja.dm | 65 - code/modules/events/holiday/vday.dm | 2 - code/modules/events/spider_infestation.dm | 36 - code/modules/events/wizard/imposter.dm | 1 - code/modules/jobs/job_exp.dm | 2 +- code/modules/jobs/job_types/_job.dm | 4 + code/modules/jobs/job_types/ai.dm | 3 + .../job_types/antagonists/clown_operative.dm | 2 - .../antagonists/nuclear_operative.dm | 21 +- code/modules/jobs/job_types/captain.dm | 2 +- code/modules/jobs/job_types/cyborg.dm | 3 + code/modules/jobs/job_types/detective.dm | 2 +- .../jobs/job_types/head_of_security.dm | 2 +- code/modules/jobs/job_types/prisoner.dm | 2 +- .../jobs/job_types/security_officer.dm | 2 +- .../station_trait/bridge_assistant.dm | 2 +- .../job_types/station_trait/cargo_gorilla.dm | 4 +- .../jobs/job_types/station_trait/human_ai.dm | 2 +- .../station_trait/veteran_advisor.dm | 2 +- code/modules/jobs/job_types/warden.dm | 2 +- code/modules/lost_crew/recovered_crew.dm | 4 +- .../modules/meteors/meteor_mode_controller.dm | 31 + code/modules/meteors/meteor_spawning.dm | 8 +- .../mining_loot/megafauna/the_thing.dm | 6 +- .../modules/mob/dead/new_player/new_player.dm | 29 +- .../mob/living/basic/drone/interaction.dm | 4 +- .../living/basic/space_fauna/demon/demon.dm | 1 - .../basic/space_fauna/revenant/_revenant.dm | 1 - code/modules/mob/living/carbon/human/human.dm | 1 - code/modules/mob/living/living.dm | 7 +- code/modules/mob/living/silicon/ai/ai.dm | 8 +- .../modules/mob/living/silicon/robot/robot.dm | 5 +- code/modules/mob/mob_helpers.dm | 77 +- code/modules/mob/mob_lists.dm | 11 +- .../modular_computers/computers/item/pda.dm | 2 +- .../programs/messenger/messenger_program.dm | 2 +- code/modules/projectiles/projectile/magic.dm | 16 +- code/modules/research/server.dm | 2 +- code/modules/shuttle/shuttle_events/humans.dm | 2 +- .../unit_tests/dynamic_ruleset_sanity.dm | 35 +- ...hot_antag_icons_clownoperativemidround.png | Bin 0 -> 2004 bytes ...creenshot_antag_icons_nuclearoperative.png | Bin 0 -> 1895 bytes code/modules/unit_tests/traitor.dm | 2 +- config/dynamic.json | 155 -- config/dynamic.toml | 612 ++++++ tgstation.dme | 36 +- .../packages/tgui/interfaces/DynamicAdmin.tsx | 739 ++++++++ .../tgui/interfaces/DynamicTester.tsx | 145 ++ 189 files changed, 6052 insertions(+), 6318 deletions(-) delete mode 100644 code/__DEFINES/dynamic.dm create mode 100644 code/controllers/subsystem/dynamic/__dynamic_defines.dm create mode 100644 code/controllers/subsystem/dynamic/_dynamic_ruleset.dm create mode 100644 code/controllers/subsystem/dynamic/_dynamic_tier.dm create mode 100644 code/controllers/subsystem/dynamic/dynamic_admin.dm delete mode 100644 code/controllers/subsystem/dynamic/dynamic_hijacking.dm delete mode 100644 code/controllers/subsystem/dynamic/dynamic_logging.dm delete mode 100644 code/controllers/subsystem/dynamic/dynamic_midround_rolling.dm create mode 100644 code/controllers/subsystem/dynamic/dynamic_ruleset_latejoin.dm create mode 100644 code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm create mode 100644 code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm delete mode 100644 code/controllers/subsystem/dynamic/dynamic_rulesets.dm delete mode 100644 code/controllers/subsystem/dynamic/dynamic_rulesets_latejoin.dm delete mode 100644 code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm delete mode 100644 code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm create mode 100644 code/controllers/subsystem/dynamic/dynamic_testing.dm delete mode 100644 code/controllers/subsystem/dynamic/dynamic_unfavorable_situation.dm delete mode 100644 code/controllers/subsystem/dynamic/readme.md delete mode 100644 code/controllers/subsystem/dynamic/ruleset_picking.dm delete mode 100644 code/modules/antagonists/pirate/pirate_event.dm create mode 100644 code/modules/antagonists/revolution/revolution_handler.dm delete mode 100644 code/modules/events/creep_awakening.dm create mode 100644 code/modules/events/dynamic_tweak.dm delete mode 100644 code/modules/events/ghost_role/abductor.dm delete mode 100644 code/modules/events/ghost_role/alien_infestation.dm delete mode 100644 code/modules/events/ghost_role/blob.dm delete mode 100644 code/modules/events/ghost_role/changeling_event.dm delete mode 100644 code/modules/events/ghost_role/fugitive_event.dm delete mode 100644 code/modules/events/ghost_role/morph_event.dm delete mode 100644 code/modules/events/ghost_role/nightmare.dm delete mode 100644 code/modules/events/ghost_role/revenant_event.dm delete mode 100644 code/modules/events/ghost_role/slaughter_event.dm delete mode 100644 code/modules/events/ghost_role/space_dragon.dm delete mode 100644 code/modules/events/ghost_role/space_ninja.dm delete mode 100644 code/modules/events/spider_infestation.dm delete mode 100644 code/modules/jobs/job_types/antagonists/clown_operative.dm create mode 100644 code/modules/meteors/meteor_mode_controller.dm create mode 100644 code/modules/unit_tests/screenshots/screenshot_antag_icons_clownoperativemidround.png create mode 100644 code/modules/unit_tests/screenshots/screenshot_antag_icons_nuclearoperative.png delete mode 100644 config/dynamic.json create mode 100644 config/dynamic.toml create mode 100644 tgui/packages/tgui/interfaces/DynamicAdmin.tsx create mode 100644 tgui/packages/tgui/interfaces/DynamicTester.tsx diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index eedd2f2329f..011f48f7b73 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -278,6 +278,8 @@ GLOBAL_LIST_INIT(ai_employers, list( /// Checks if the given mob is a 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. #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" -// This flag disables certain checks that presume antagonist datums mean 'baddie'. -#define FLAG_FAKE_ANTAG (1 << 0) +/// Used to denote an antag datum that either isn't necessarily "evil" (like Valentines) +/// 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_RUSSIAN "Russian Fugitive Hunters" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm index 53fa1021a0f..222ceaa158c 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm @@ -77,7 +77,7 @@ /// From /mob/proc/ghostize() Called when a mob successfully ghosts #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) -#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) ///signal sent when a mob has their holy role set. Sent to the mob having their role changed. diff --git a/code/__DEFINES/dcs/signals/signals_subsystem.dm b/code/__DEFINES/dcs/signals/signals_subsystem.dm index 749ba9f6381..2260a480a79 100644 --- a/code/__DEFINES/dcs/signals/signals_subsystem.dm +++ b/code/__DEFINES/dcs/signals/signals_subsystem.dm @@ -26,3 +26,6 @@ ///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" + +/// Send after config is loaded but before picking roundstart rulesets +#define COMSIG_DYNAMIC_PRE_ROUNDSTART "dynamic_pre_roundstart" diff --git a/code/__DEFINES/dynamic.dm b/code/__DEFINES/dynamic.dm deleted file mode 100644 index 0162ef755e3..00000000000 --- a/code/__DEFINES/dynamic.dm +++ /dev/null @@ -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) diff --git a/code/__DEFINES/jobs.dm b/code/__DEFINES/jobs.dm index d71da7a326b..5c466a6ca12 100644 --- a/code/__DEFINES/jobs.dm +++ b/code/__DEFINES/jobs.dm @@ -279,6 +279,10 @@ DEFINE_BITFIELD(departments_bitflags, list( #define JOB_LATEJOIN_ONLY (1<<11) /// This job is a head of staff. #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( "JOB_ANNOUNCE_ARRIVAL" = JOB_ANNOUNCE_ARRIVAL, diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index da975f90ea5..a560083458a 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -35,6 +35,7 @@ #define ROLE_NINJA "Space Ninja" #define ROLE_OBSESSED "Obsessed" #define ROLE_OPERATIVE_MIDROUND "Operative (Midround)" +#define ROLE_CLOWN_OPERATIVE_MIDROUND "Clown Operative (Midround)" #define ROLE_PARADOX_CLONE "Paradox Clone" #define ROLE_REV_HEAD "Head Revolutionary" #define ROLE_SLEEPER_AGENT "Syndicate Sleeper Agent" @@ -77,13 +78,13 @@ #define ROLE_REVENANT "Revenant" #define ROLE_SENTIENCE "Sentience Potion Spawn" #define ROLE_SOULTRAPPED_HERETIC "Soultrapped Heretic" +/// This flag specifically is used as a generic catch-all antag ban #define ROLE_SYNDICATE "Syndicate" #define ROLE_EXPERIMENTAL_CLONER "Experimental Cloner" #define ROLE_CLOWN_OPERATIVE "Clown Operative" #define ROLE_FREE_GOLEM "Free Golem" #define ROLE_MORPH "Morph" -#define ROLE_NUCLEAR_OPERATIVE "Nuclear Operative" #define ROLE_POSITRONIC_BRAIN "Positronic Brain" #define ROLE_SANTA "Santa" #define ROLE_SERVANT_GOLEM "Servant Golem" @@ -132,93 +133,6 @@ #define ROLE_CYBER_POLICE "Cyber Police" #define ROLE_CYBER_TAC "Cyber Tac" #define ROLE_NETGUARDIAN "NetGuardian Prime" -//SKYRAT EDIT START - Skyrat roles -#define ROLE_BLACK_MARKET_DEALER "Black Market Dealer" -#define ROLE_DS2 "DS2 Syndicate" -#define ROLE_FREIGHTER_CREW "Freighter Crew" -#define ROLE_GHOST_CAFE "Ghost Cafe Visitor" -#define ROLE_PORT_TARKON "Port Tarkon Survivor" -//SKYRAT EDIT END - -//BUBBER EDIT START -#define ROLE_DAUNTLESS "Dauntless Syndicate" -#define ROLE_PERSISTENCE "Persistence Syndicate" - -#define ROLE_BLOODSUCKER "Bloodsucker" -#define ROLE_VAMPIRICACCIDENT "Bloodsucker (Midround)" -#define ROLE_BLOODSUCKERBREAKOUT "Bloodsucker (Latejoin)" -#define ROLE_MONSTERHUNTER "Monster Hunter" -#define ROLE_VASSAL "Ghoul" - -#define ROLE_CHANGELING_ZOMBIE "Changeling Zombie" -//BUBBER EDIT END - -/// 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, - // SKYRAT EDIT ADDITION - ROLE_ASSAULT_OPERATIVE = 14, - // SKYRAT EDIT END - - // 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, - //SKYRAT EDIT START - ROLE_LONE_INFILTRATOR = 0, - ROLE_BORER = 0, - ROLE_MUTANT = 0, - //SKYRAT EDIT END - 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, - - //BUBBER EDIT START - ROLE_BLOODSUCKER = 14, - ROLE_VAMPIRICACCIDENT = 14, - ROLE_BLOODSUCKERBREAKOUT = 14, - ROLE_MONSTERHUNTER = 14, - ROLE_CHANGELING_ZOMBIE = 0 - //BUBBER EDIT END -)) //Job defines for what happens when you fail to qualify for any job during job selection #define BEOVERFLOW 1 diff --git a/code/__HELPERS/events.dm b/code/__HELPERS/events.dm index 9b5f939de15..58d91c8caa8 100644 --- a/code/__HELPERS/events.dm +++ b/code/__HELPERS/events.dm @@ -23,7 +23,6 @@ possible_spawns += spawn_turf if(!length(possible_spawns)) - message_admins("No valid generic_maintenance_landmark landmarks found, aborting...") return null return pick(possible_spawns) @@ -44,7 +43,6 @@ possible_spawns += get_turf(spawn_location) if(!length(possible_spawns)) - message_admins("No valid carpspawn landmarks found, aborting...") return null return pick(possible_spawns) diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index 8ae96a3aeb1..afe155b50d2 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -150,8 +150,8 @@ return ///Get active players who are playing in the round -/proc/get_active_player_count(alive_check = FALSE, afk_check = FALSE, human_check = FALSE) - var/active_players = 0 +/proc/get_active_player_list(alive_check = FALSE, afk_check = FALSE, human_check = FALSE) + var/list/active_players = list() for(var/mob/player_mob as anything in GLOB.player_list) if(!player_mob?.client) continue @@ -167,9 +167,13 @@ var/mob/dead/observer/ghost_player = player_mob if(ghost_player.started_as_observer) // Exclude people who started as observers continue - active_players++ + active_players += player_mob 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 /proc/make_body(mob/dead/observer/ghost_player) if(!ghost_player || !ghost_player.key) diff --git a/code/__HELPERS/logging/dynamic.dm b/code/__HELPERS/logging/dynamic.dm index 488aeaec3be..70e3d873163 100644 --- a/code/__HELPERS/logging/dynamic.dm +++ b/code/__HELPERS/logging/dynamic.dm @@ -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 /proc/log_dynamic(text, list/data) logger.Log(LOG_CATEGORY_DYNAMIC, text, data) diff --git a/code/__HELPERS/logging/manifest.dm b/code/__HELPERS/logging/manifest.dm index 5c18b5e4b18..70ebd8ace0a 100644 --- a/code/__HELPERS/logging/manifest.dm +++ b/code/__HELPERS/logging/manifest.dm @@ -1,9 +1,10 @@ /// Logging for player manifest (ckey, name, job, special role, roundstart/latejoin) /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( "mind" = mind, "body" = body, "latejoin" = latejoin )) // Roundstart happens with SSblackbox.ReportRoundstartManifest 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) diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index c0de1b973a6..c8e9ffb88e6 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -371,15 +371,9 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) else parts += "[FOURSPACES]Nobody died this shift!" - parts += "[FOURSPACES]Threat level: [SSdynamic.threat_level]" - parts += "[FOURSPACES]Threat left: [SSdynamic.mid_round_budget]" - if(SSdynamic.roundend_threat_log.len) - parts += "[FOURSPACES]Threat edits:" - for(var/entry as anything in SSdynamic.roundend_threat_log) - parts += "[FOURSPACES][FOURSPACES][entry]
" - parts += "[FOURSPACES]Executed rules:" - for(var/datum/dynamic_ruleset/rule in SSdynamic.executed_rules) - parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat" + parts += "[FOURSPACES]Round: [SSdynamic.current_tier.name]" + for(var/datum/dynamic_ruleset/rule as anything in SSdynamic.executed_rulesets - SSdynamic.unreported_rulesets) + parts += "[FOURSPACES][FOURSPACES]- [rule.name] ([rule.config_tag])" return parts.Join("
") diff --git a/code/controllers/subsystem/blackbox.dm b/code/controllers/subsystem/blackbox.dm index 9d761f5322d..b4e67fe7f91 100644 --- a/code/controllers/subsystem/blackbox.dm +++ b/code/controllers/subsystem/blackbox.dm @@ -351,7 +351,7 @@ Versioning "name" = L.real_name, "key" = L.ckey, "job" = L.mind.assigned_role.title, - "special" = L.mind.special_role, + "special" = jointext(L.mind.get_special_roles(), " | "), "pod" = get_area_name(L, TRUE), "laname" = L.lastattacker, "lakey" = L.lastattackerckey, @@ -440,7 +440,7 @@ Versioning "ckey" = mob_ckey, "character_name" = new_character.real_name, "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, )) SSdbcore.MassInsert(format_table_name("manifest"), query_rows, special_columns = special_columns) diff --git a/code/controllers/subsystem/dynamic/__dynamic_defines.dm b/code/controllers/subsystem/dynamic/__dynamic_defines.dm new file mode 100644 index 00000000000..b22e7ff0798 --- /dev/null +++ b/code/controllers/subsystem/dynamic/__dynamic_defines.dm @@ -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(...) "(CANCEL)" +/// Href for rerolling midround rulesets before execution +#define MIDROUND_REROLL_HREF(rulesets) "[length(rulesets) \ + ? "(SOMETHING ELSE)" \ + : "([span_tooltip("There are no more rulesets to pick from!", "NOTHING ELSE")])"\ +]" + +#define RULESET_CONFIG_CANCEL "Cancel" diff --git a/code/controllers/subsystem/dynamic/_dynamic_ruleset.dm b/code/controllers/subsystem/dynamic/_dynamic_ruleset.dm new file mode 100644 index 00000000000..2ca15349339 --- /dev/null +++ b/code/controllers/subsystem/dynamic/_dynamic_ruleset.dm @@ -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!") diff --git a/code/controllers/subsystem/dynamic/_dynamic_tier.dm b/code/controllers/subsystem/dynamic/_dynamic_tier.dm new file mode 100644 index 00000000000..3d1fe57f17e --- /dev/null +++ b/code/controllers/subsystem/dynamic/_dynamic_tier.dm @@ -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: Green Star
\ + 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: Yellow Star
\ + 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: Red Star
\ + 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: Black Orbit
\ + 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: Midnight Sun
\ + 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, + ), + ) diff --git a/code/controllers/subsystem/dynamic/dynamic.dm b/code/controllers/subsystem/dynamic/dynamic.dm index 65ffc205b56..6fd7fd28972 100644 --- a/code/controllers/subsystem/dynamic/dynamic.dm +++ b/code/controllers/subsystem/dynamic/dynamic.dm @@ -1,306 +1,591 @@ -// Are HIGH_IMPACT_RULESETs allowed to stack? -GLOBAL_VAR_INIT(dynamic_no_stacking, TRUE) -// If enabled does not accept or execute any rulesets. -GLOBAL_VAR_INIT(dynamic_forced_extended, FALSE) -// How high threat is required for HIGH_IMPACT_RULESETs stacking. -// This is independent of dynamic_no_stacking. -GLOBAL_VAR_INIT(dynamic_stacking_limit, 90) -// List of forced roundstart rulesets. -GLOBAL_LIST_EMPTY(dynamic_forced_roundstart_ruleset) -// Forced threat level, setting this to zero or higher forces the roundstart threat to the value. -GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) -/// Modify the threat level for station traits before dynamic can be Initialized. List(instance = threat_reduction) -GLOBAL_LIST_EMPTY(dynamic_station_traits) -/// Rulesets which have been forcibly enabled or disabled -GLOBAL_LIST_EMPTY(dynamic_forced_rulesets) -/// Bitflags used during init by Dynamic to determine which rulesets we're allowed to use, used by station traits for gamemode-esque experiences -GLOBAL_VAR_INIT(dynamic_ruleset_categories, RULESET_CATEGORY_DEFAULT) - SUBSYSTEM_DEF(dynamic) name = "Dynamic" flags = SS_NO_INIT - wait = 1 SECONDS + wait = 5 MINUTES - // Threat logging vars - /// The "threat cap", threat shouldn't normally go above this and is used in ruleset calculations - var/threat_level = 0 + // These vars just exist for admins interfacing with dynamic + /// Cooldown between "we're going to spawn a midround" and "we're actually spawning a midround", to give admins a chance to cancel + COOLDOWN_DECLARE(midround_admin_cancel_period) + /// Set to TRUE by hrefs if admins cancel a midround + var/tmp/midround_admin_cancel = FALSE + /// Set to TRUE by hrefs if admins reroll a midround + var/tmp/midround_admin_reroll = FALSE + /// Set to TRUE by admin panels if they want to max out the chance of a light ruleset spawning + var/tmp/admin_forcing_next_light = FALSE + /// Set to TRUE by admin panels if they want to max out the chance of a heavy ruleset spawning + var/tmp/admin_forcing_next_heavy = FALSE + /// Set to TRUE by admin panels if they want to max out the chance of a latejoin ruleset spawning + var/tmp/admin_forcing_next_latejoin = FALSE + /// List of ruleset typepaths that admins have explicitly disabled + var/tmp/list/admin_disabled_rulesets = list() - /// Set at the beginning of the round. Spent by the mode to "purchase" rules. Everything else goes in the postround budget. - var/round_start_budget = 0 + // Dynamic vars + /// Reference to a dynamic tier datum, the tier picked for this round + var/datum/dynamic_tier/current_tier + /// The config for dynamic loaded from the toml file + var/list/dynamic_config = list() + /// Tracks how many of each ruleset category is yet to be spawned + var/list/rulesets_to_spawn = list( + ROUNDSTART = -1, + LIGHT_MIDROUND = -1, + HEAVY_MIDROUND = -1, + LATEJOIN = -1, + ) + /// Tracks the number of rulesets to spawn at game start (for admin reference) + var/list/base_rulesets_to_spawn = list( + ROUNDSTART = 0, + LIGHT_MIDROUND = 0, + HEAVY_MIDROUND = 0, + LATEJOIN = 0, + ) + /// Cooldown for when we are allowed to spawn light rulesets + COOLDOWN_DECLARE(light_ruleset_start) + /// Cooldown for when we are allowed to spawn heavy rulesets + COOLDOWN_DECLARE(heavy_ruleset_start) + /// Cooldown for when we are allowed to spawn latejoin rulesets + COOLDOWN_DECLARE(latejoin_ruleset_start) + /// Tracks how many time we fail to spawn a latejoin (to up the odds next time) + var/failed_latejoins = 0 + /// Cooldown between midround ruleset executions + COOLDOWN_DECLARE(midround_cooldown) + /// Cooldown between latejoin ruleset executions + COOLDOWN_DECLARE(latejoin_cooldown) + /// List of rulesets that have been executed this round + var/list/datum/dynamic_ruleset/executed_rulesets = list() + /// List of rulesets that have been set up to run, but not yet executed + var/list/datum/dynamic_ruleset/queued_rulesets = list() + /// Rulesets in this list will be excluded from the roundend report + var/list/datum/dynamic_ruleset/unreported_rulesets = list() + /// Whether random events that spawn antagonists or modify dynamic are enabled + var/antag_events_enabled = TRUE - /// Set at the beginning of the round. Spent by midrounds and latejoins. - var/mid_round_budget = 0 - - /// The initial round start budget for logging purposes, set once at the beginning of the round. - var/initial_round_start_budget = 0 - - /// Running information about the threat. Can store text or datum entries. - var/list/threat_log = list() - /// Threat log shown on the roundend report. Should only list player-made edits. - var/list/roundend_threat_log = list() - /// List of latejoin rules used for selecting the rules. - var/list/latejoin_rules - /// List of midround rules used for selecting the rules. - var/list/midround_rules - /** # Pop range per requirement. - * If the value is five the range is: - * 0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30-34, 35-39, 40-54, 45+ - * 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+ - */ - var/pop_per_requirement = 6 - /// Number of players who were ready on roundstart. - var/roundstart_pop_ready = 0 - /// List of candidates used on roundstart rulesets. - var/list/candidates = list() - /// Rules that are processed, rule_process is called on the rules in this list. - var/list/current_rules = list() - /// List of executed rulesets. - var/list/executed_rules = list() - /// If TRUE, the next player to latejoin will guarantee roll for a random latejoin antag - /// (this does not guarantee they get said antag roll, depending on preferences and circumstances) - var/late_forced_injection = FALSE - /// If TRUE, a midround ruleset will be rolled - var/mid_forced_injection = FALSE - /// Forced ruleset to be executed for the next latejoin. - var/datum/dynamic_ruleset/latejoin/forced_latejoin_rule = null - /// How many percent of the rounds are more peaceful. - var/peaceful_percentage = 50 - /// If a high impact ruleset was executed. Only one will run at a time in most circumstances. - var/high_impact_ruleset_executed = FALSE - /// If a only ruleset has been executed. - var/only_ruleset_executed = FALSE - /// Dynamic configuration, loaded on pre_setup - var/list/configuration = null - - /// When world.time is over this number the mode tries to inject a latejoin ruleset. - var/latejoin_injection_cooldown = 0 - - /// The minimum time the recurring latejoin ruleset timer is allowed to be. - var/latejoin_delay_min = (5 MINUTES) - - /// The maximum time the recurring latejoin ruleset timer is allowed to be. - var/latejoin_delay_max = (25 MINUTES) - - /// The low bound for the midround roll time splits. - /// This number influences where to place midround rolls, making this smaller - /// will make midround rolls more frequent, and vice versa. - /// A midround will never be able to roll before this. - var/midround_lower_bound = 10 MINUTES - - /// The upper bound for the midround roll time splits. - /// This number influences where to place midround rolls, making this larger - /// will make midround rolls less frequent, and vice versa. - /// A midround will never be able to roll farther than this. - var/midround_upper_bound = 100 MINUTES - - /// The distance between the chosen midround roll point (which is deterministic), - /// and when it can actually roll. - /// Basically, if this is set to 5 minutes, and a midround roll point is decided to be at 20 minutes, - /// then it can roll anywhere between 15 and 25 minutes. - var/midround_roll_distance = 3 MINUTES - - /// The amount of threat per midround roll. - /// Basically, if this is set to 5, then for every 5 threat, one midround roll will be added. - /// The equation this is used in rounds up, meaning that if this is set to 5, and you have 6 - /// threat, then you will get 2 midround rolls. - var/threat_per_midround_roll = 7 - - /// A number between -5 and +5. - /// A negative value will give a more peaceful round and - /// a positive value will give a round with higher threat. - var/threat_curve_centre = 0 - - /// A number between 0.5 and 4. - /// Higher value will favour extreme rounds and - /// lower value rounds closer to the average. - var/threat_curve_width = 1.8 - - /// A number between -5 and +5. - /// Equivalent to threat_curve_centre, but for the budget split. - /// A negative value will weigh towards midround rulesets, and a positive - /// value will weight towards roundstart ones. - var/roundstart_split_curve_centre = 1 - - /// A number between 0.5 and 4. - /// Equivalent to threat_curve_width, but for the budget split. - /// Higher value will favour more variance in splits and - /// lower value rounds closer to the average. - var/roundstart_split_curve_width = 1.8 - - /// The minimum amount of time for antag random events to be hijacked. - var/random_event_hijack_minimum = 10 MINUTES - - /// The maximum amount of time for antag random events to be hijacked. - var/random_event_hijack_maximum = 18 MINUTES - - /// What is the lower bound of when the roundstart announcement is sent out? - var/waittime_l = 600 - - /// What is the higher bound of when the roundstart announcement is sent out? - var/waittime_h = 1800 - - /// A number between 0 and 100. The maximum amount of threat allowed to generate. - var/max_threat_level = 100 - - /// The extra chance multiplier that a heavy impact midround ruleset will run next time. - /// For example, if this is set to 50, then the next heavy roll will be about 50% more likely to happen. - var/hijacked_random_event_injection_chance_modifier = 50 - - /// Any midround before this point is guaranteed to be light - var/midround_light_upper_bound = 25 MINUTES - - /// Any midround after this point is guaranteed to be heavy - var/midround_heavy_lower_bound = 55 MINUTES - - /// If there are less than this many players readied, threat level will be lowered. - /// This number should be kept fairly low, as there are other measures that population - /// impacts Dynamic, such as the requirements variable on rulesets. - var/low_pop_player_threshold = 20 - - /// The maximum threat that can roll with *zero* players. - /// As the number of players approaches `low_pop_player_threshold`, the maximum - /// threat level will increase. - /// For example, if `low_pop_maximum_threat` is 50, `low_pop_player_threshold` is 20, - /// and the number of readied players is 10, then the highest threat that can roll is - /// lerp(50, 100, 10 / 20), AKA 75. - var/low_pop_maximum_threat = 40 - - /// The chance for latejoins to roll when ready - var/latejoin_roll_chance = 50 - - // == EVERYTHING BELOW THIS POINT SHOULD NOT BE CONFIGURED == - - /// A list of recorded "snapshots" of the round, stored in the dynamic.json log - var/list/datum/dynamic_snapshot/snapshots - - /// The time when the last midround injection was attempted, whether or not it was successful - var/last_midround_injection_attempt = 0 - - /// Whether or not a random event has been hijacked this midround cycle - var/random_event_hijacked = HIJACKED_NOTHING - - /// The timer ID for the cancellable midround rule injection - var/midround_injection_timer_id - - /// The last drafted midround rulesets (without the current one included). - /// Used for choosing different midround injections. - var/list/current_midround_rulesets - - VAR_PRIVATE/next_midround_injection - -/datum/controller/subsystem/dynamic/proc/admin_panel() - var/list/dat = list() - dat += "Dynamic Mode VVRefresh
" - dat += "Threat Level: [threat_level]
" - dat += "Budgets (Roundstart/Midrounds): [initial_round_start_budget]/[threat_level - initial_round_start_budget]
" - - dat += "Midround budget to spend: [mid_round_budget] AdjustView Log
" - dat += "
" - dat += "Parameters: centre = [threat_curve_centre] ; width = [threat_curve_width].
" - dat += "Split parameters: centre = [roundstart_split_curve_centre] ; width = [roundstart_split_curve_width].
" - dat += "On average, [clamp(peaceful_percentage, 1, 99)]% of the rounds are more peaceful.
" - dat += "Forced extended: [GLOB.dynamic_forced_extended ? "On" : "Off"]
" - dat += "No stacking (only one round-ender): [GLOB.dynamic_no_stacking ? "On" : "Off"]
" - dat += "Stacking limit: [GLOB.dynamic_stacking_limit] Adjust" - dat += "
" - dat += "Force Next Latejoin Ruleset
" - if (forced_latejoin_rule) - dat += {"-> [forced_latejoin_rule.name] <-
"} - dat += "Execute Midround Ruleset
" - dat += "
" - dat += "Executed rulesets: " - if (executed_rules.len > 0) - dat += "
" - for (var/datum/dynamic_ruleset/DR in executed_rules) - dat += "[DR.ruletype] - [DR.name]
" - else - dat += "none.
" - dat += "
Injection Timers: ([get_heavy_midround_injection_chance(dry_run = TRUE)]% heavy midround chance)
" - dat += "Latejoin: [DisplayTimeText(latejoin_injection_cooldown-world.time)] Now!
" - - var/next_injection = next_midround_injection() - if (next_injection == INFINITY) - dat += "All midrounds have been exhausted." - else - dat += "Midround: [DisplayTimeText(next_injection - world.time)] Now!
" - - var/datum/browser/browser = new(usr, "gamemode_panel", "Game Mode Panel", 500, 500) - browser.set_content(dat.Join()) - browser.open() - -/datum/controller/subsystem/dynamic/Topic(href, href_list) - if (..()) // Sanity, maybe ? +/datum/controller/subsystem/dynamic/fire(resumed) + if(!COOLDOWN_FINISHED(src, midround_cooldown) || EMERGENCY_PAST_POINT_OF_NO_RETURN) return - if(!check_rights(R_ADMIN)) - message_admins("[usr.key] has attempted to override the game mode panel!") - log_admin("[key_name(usr)] tried to use the game mode panel without authorization.") - return - if (href_list["forced_extended"]) - GLOB.dynamic_forced_extended = !GLOB.dynamic_forced_extended - else if (href_list["no_stacking"]) - GLOB.dynamic_no_stacking = !GLOB.dynamic_no_stacking - else if (href_list["adjustthreat"]) - var/threatadd = input("Specify how much threat to add (negative to subtract). This can inflate the threat level.", "Adjust Threat", 0) as null|num - if(!threatadd) + + if(COOLDOWN_FINISHED(src, light_ruleset_start)) + if(try_spawn_midround(LIGHT_MIDROUND)) return - if(threatadd > 0) - create_threat(threatadd, threat_log, "[gameTimestamp()]: increased by [key_name(usr)]") - else - spend_midround_budget(-threatadd, threat_log, "[gameTimestamp()]: decreased by [key_name(usr)]") - else if (href_list["injectlate"]) - latejoin_injection_cooldown = 0 - late_forced_injection = TRUE - message_admins("[key_name(usr)] forced a latejoin injection.") - else if (href_list["injectmid"]) - mid_forced_injection = TRUE - message_admins("[key_name(usr)] forced a midround injection.") - try_midround_roll() - else if (href_list["threatlog"]) - show_threatlog(usr) - else if (href_list["stacking_limit"]) - GLOB.dynamic_stacking_limit = input(usr,"Change the threat limit at which round-endings rulesets will start to stack.", "Change stacking limit", null) as num - else if(href_list["force_latejoin_rule"]) - var/added_rule = input(usr,"What ruleset do you want to force upon the next latejoiner? This will bypass threat level and population restrictions.", "Rigging Latejoin", null) as null|anything in sort_names(init_rulesets(/datum/dynamic_ruleset/latejoin)) - if (!added_rule) + + if(COOLDOWN_FINISHED(src, heavy_ruleset_start)) + if(try_spawn_midround(HEAVY_MIDROUND)) return - forced_latejoin_rule = added_rule - log_admin("[key_name(usr)] set [added_rule] to proc on the next latejoin.") - message_admins("[key_name(usr)] set [added_rule] to proc on the next valid latejoin.") - else if(href_list["clear_forced_latejoin"]) - forced_latejoin_rule = null - log_admin("[key_name(usr)] cleared the forced latejoin ruleset.") - message_admins("[key_name(usr)] cleared the forced latejoin ruleset.") - else if(href_list["force_midround_rule"]) - var/added_rule = input(usr,"What ruleset do you want to force right now? This will bypass threat level and population restrictions.", "Execute Ruleset", null) as null|anything in sort_names(init_rulesets(/datum/dynamic_ruleset/midround)) - if (!added_rule) - return - log_admin("[key_name(usr)] executed the [added_rule] ruleset.") - message_admins("[key_name(usr)] executed the [added_rule] ruleset.") - picking_specific_rule(added_rule, TRUE) - else if(href_list["cancelmidround"]) - admin_cancel_midround(usr, href_list["cancelmidround"]) - return - else if (href_list["differentmidround"]) - admin_different_midround(usr, href_list["differentmidround"]) + +/datum/controller/subsystem/dynamic/proc/get_config() + if(!length(dynamic_config)) + load_config() + return dynamic_config + +/** + * Selects which rulesets are to run at roundstart, and sets them up + * + * Note: This proc can sleep (due to lazyloading of templates)! + */ +/datum/controller/subsystem/dynamic/proc/select_roundstart_antagonists() + load_config() + SEND_SIGNAL(src, COMSIG_DYNAMIC_PRE_ROUNDSTART, dynamic_config) + // we start by doing a dry run of the job selection process to detect antag rollers + SSjob.divide_occupations(pure = TRUE, allow_all = TRUE) + + var/list/antag_candidates = list() + // anyone who was readied up and NOT unassigned is a potential candidate (even those who have no antag preferences) + for(var/mob/dead/new_player/player as anything in GLOB.new_player_list - SSjob.unassigned) + if(player.ready == PLAYER_READY_TO_PLAY && player.mind) + antag_candidates += player + + // anyone unassigned are potential antag rollers, so we need to warn them + for(var/mob/dead/new_player/player as anything in SSjob.unassigned) + var/list/job_data = list() + var/job_prefs = player.client?.prefs.job_preferences + for(var/job in job_prefs) + var/priority = job_prefs[job] + job_data += "[job]: [SSjob.job_priority_level_to_string(priority)]" + to_chat(player, span_danger("You were unable to qualify for any roundstart antagonist role this round \ + because your job preferences presented a high chance of all of your selected jobs being unavailable, \ + along with 'return to lobby if job is unavailable' enabled. \ + Increase the number of roles set to medium or low priority to reduce the chances of this happening.")) + log_admin("[player.ckey] failed to qualify for any roundstart antagonist role \ + because their job preferences presented a high chance of all of their selected jobs being unavailable, \ + along with 'return to lobby if job is unavailable' enabled and has [player.client.prefs.be_special.len] antag preferences enabled. \ + They will be unable to qualify for any roundstart antagonist role. These are their job preferences - [job_data.Join(" | ")]") + + var/num_real_players = length(antag_candidates) + // now select a tier (if admins didn't) + // this also calculates the number of rulesets to spawn + if(!current_tier) + pick_tier(num_real_players) + // put rulesets in the queue (if admins didn't) + // this will even handle the case in which the tier wants 0 roundstart rulesets + if(!length(queued_rulesets)) + queued_rulesets += pick_roundstart_rulesets(antag_candidates) + // we got what we needed, reset so we can do real job selection later + // reset only happens AFTER roundstart selection so we can verify stuff like "can we get 3 heads of staff for revs?" + SSjob.reset_occupations() + // finally, run through the queue and prepare rulesets for execution + // (actual execution, ie assigning antags, will happen after job assignment) + for(var/datum/dynamic_ruleset/roundstart/ruleset in queued_rulesets) + // NOTE: !! THIS CAN SLEEP !! + if(!ruleset.prepare_execution( num_real_players, antag_candidates )) + log_dynamic("Roundstart: Selected ruleset [ruleset.config_tag], but preparation failed!") + queued_rulesets -= ruleset + qdel(ruleset) + continue + + // Just logs who was selected at roundstart + for(var/datum/mind/selected as anything in ruleset.selected_minds) + log_dynamic("Roundstart: [key_name(selected)] has been selected for [ruleset.config_tag].") + + rulesets_to_spawn[ROUNDSTART] -= 1 + // and start ticking + COOLDOWN_START(src, light_ruleset_start, current_tier.ruleset_type_settings[LIGHT_MIDROUND][TIME_THRESHOLD]) + COOLDOWN_START(src, heavy_ruleset_start, current_tier.ruleset_type_settings[HEAVY_MIDROUND][TIME_THRESHOLD]) + COOLDOWN_START(src, latejoin_ruleset_start, current_tier.ruleset_type_settings[LATEJOIN][TIME_THRESHOLD]) + + return TRUE + +/datum/controller/subsystem/dynamic/proc/load_config() + PRIVATE_PROC(TRUE) + + if(!CONFIG_GET(flag/dynamic_config_enabled)) return - admin_panel() // Refreshes the window + var/config_file = "[global.config.directory]/dynamic.toml" + var/list/result = rustg_raw_read_toml_file(config_file) + if(!result["success"]) + log_dynamic("Failed to load config file! ([config_file] - [result["content"]])") + return + + dynamic_config = json_decode(result["content"]) + +/// Sets the tier to the typepath passed in +/datum/controller/subsystem/dynamic/proc/set_tier(picked_tier, population = length(GLOB.player_list)) + current_tier = new picked_tier(dynamic_config) + + for(var/category in current_tier.ruleset_type_settings) + var/list/range = current_tier.ruleset_type_settings[category] || list() + var/low_end = range[LOW_END] || 0 + var/high_end = range[HIGH_END] || 0 + + if(population <= (range[HALF_RANGE_POP_THRESHOLD] || 0)) + high_end = max(low_end, ceil(high_end * 0.25)) + else if(population <= (range[FULL_RANGE_POP_THRESHOLD] || 0)) + high_end = max(low_end, ceil(high_end * 0.5)) + + rulesets_to_spawn[category] = rand(low_end, high_end) + base_rulesets_to_spawn[category] = rulesets_to_spawn[category] + +/// Picks what tier we are going to use for this round and sets up all the corresponding variables and ranges +/datum/controller/subsystem/dynamic/proc/pick_tier(roundstart_population = 0) + PRIVATE_PROC(TRUE) + + var/list/tier_weighted = list() + for(var/datum/dynamic_tier/tier_datum as anything in subtypesof(/datum/dynamic_tier)) + var/min_players_config = dynamic_config[tier_datum::config_tag]?[NAMEOF(tier_datum, min_pop)] + var/min_players = isnull(min_players_config) ? tier_datum::min_pop : min_players_config + if(roundstart_population < min_players) + continue + + var/tier_config_weight = dynamic_config[tier_datum::config_tag]?[NAMEOF(tier_datum, weight)] + var/tier_weight = isnull(tier_config_weight) ? tier_datum::weight : tier_config_weight + if(tier_weight <= 0) + continue + + tier_weighted[tier_datum] = tier_weight + + set_tier(pick_weight(tier_weighted), roundstart_population) + + var/roundstart_spawn = rulesets_to_spawn[ROUNDSTART] + var/light_midround_spawn = rulesets_to_spawn[LIGHT_MIDROUND] + var/heavy_midround_spawn = rulesets_to_spawn[HEAVY_MIDROUND] + var/latejoin_spawn = rulesets_to_spawn[LATEJOIN] + + log_dynamic("Selected tier: [current_tier.tier]") + log_dynamic("- Roundstart population: [roundstart_population]") + log_dynamic("- Roundstart ruleset count: [roundstart_spawn]") + log_dynamic("- Light midround ruleset count: [light_midround_spawn]") + log_dynamic("- Heavy midround ruleset count: [heavy_midround_spawn]") + log_dynamic("- Latejoin ruleset count: [latejoin_spawn]") + SSblackbox.record_feedback( + "associative", + "dynamic_tier", + 1, + list( + "server_name" = CONFIG_GET(string/serversqlname), + "tier" = current_tier.tier, + "player_count" = roundstart_population, + "roundstart_ruleset_count" = roundstart_spawn, + "light_midround_ruleset_count" = light_midround_spawn, + "heavy_midround_ruleset_count" = heavy_midround_spawn, + "latejoin_ruleset_count" = latejoin_spawn, + ), + ) + +/// Gets a weighted list of roundstart rulesets +/datum/controller/subsystem/dynamic/proc/get_roundstart_rulesets(list/antag_candidates) + PRIVATE_PROC(TRUE) + + var/list/datum/dynamic_ruleset/roundstart/rulesets = list() + for(var/ruleset_type in subtypesof(/datum/dynamic_ruleset/roundstart)) + var/datum/dynamic_ruleset/roundstart/ruleset = new ruleset_type(dynamic_config) + rulesets[ruleset] = ruleset.get_weight(length(antag_candidates), current_tier.tier) + + return rulesets + +/// Picks as many roundstart rulesets as we are allowed to spawn, returns them +/datum/controller/subsystem/dynamic/proc/pick_roundstart_rulesets(list/antag_candidates) + PRIVATE_PROC(TRUE) + + if(rulesets_to_spawn[ROUNDSTART] <= 0) + return list() + + var/list/rulesets_weighted = get_roundstart_rulesets(antag_candidates) + var/total_weight = 0 + for(var/ruleset in rulesets_weighted) + total_weight += rulesets_weighted[ruleset] + if(total_weight <= 0) + log_dynamic("Roundstart: No rulesets to pick from!") + return list() + + var/list/picked_rulesets = list() + while(rulesets_to_spawn[ROUNDSTART] > 0) + if(!length(rulesets_weighted) || total_weight <= 0) + log_dynamic("Roundstart: No more rulesets to pick from with [rulesets_to_spawn[ROUNDSTART]] left!") + break + rulesets_to_spawn[ROUNDSTART] -= 1 + var/datum/dynamic_ruleset/roundstart/picked_ruleset = pick_weight(rulesets_weighted) + log_dynamic("Roundstart: Ruleset [picked_ruleset.config_tag] (Chance: [round(rulesets_weighted[picked_ruleset] / total_weight * 100, 0.01)]%)") + if(picked_ruleset.solo) + log_dynamic("Roundstart: Ruleset is a solo ruleset. Cancelling other picks.") + QDEL_LIST(picked_rulesets) + rulesets_weighted -= picked_ruleset + picked_rulesets += picked_ruleset + break + if(!picked_ruleset.repeatable) + rulesets_weighted -= picked_ruleset + picked_rulesets += picked_ruleset + continue + + rulesets_weighted[picked_ruleset] -= picked_ruleset.repeatable_weight_decrease + total_weight -= picked_ruleset.repeatable_weight_decrease + // Rulesets are not singletons. We need to to make a new one + picked_rulesets += new picked_ruleset.type(dynamic_config) + + // clean up unused rulesets + QDEL_LIST(rulesets_weighted) + return picked_rulesets + +/datum/controller/subsystem/dynamic/proc/get_advisory_report() + var/shown_tier = current_tier.tier + if(prob(10)) + shown_tier = pick(list(DYNAMIC_TIER_LOW, DYNAMIC_TIER_LOWMEDIUM, DYNAMIC_TIER_MEDIUMHIGH, DYNAMIC_TIER_HIGH) - current_tier.tier) + + else if(prob(15)) + shown_tier = clamp(current_tier.tier + pick(-1, 1), DYNAMIC_TIER_LOW, DYNAMIC_TIER_HIGH) + + for(var/datum/dynamic_tier/tier_datum as anything in subtypesof(/datum/dynamic_tier)) + if(tier_datum::tier == shown_tier) + return tier_datum::advisory_report + + return null + +/** + * Invoked by SSdynamic to try to spawn a random midround ruleset + * Respects ranges and thresholds + * + * Prioritizes light midround rulesets first, then heavy midround rulesets + * + * Returns TRUE if a ruleset was spawned, FALSE otherwise + */ +/datum/controller/subsystem/dynamic/proc/try_spawn_midround(range) + if(rulesets_to_spawn[range] <= 0) + return FALSE + var/midround_chance = get_midround_chance(range) + if(!prob(midround_chance)) + log_dynamic("Midround ([range]): Ruleset chance failed ([midround_chance]% chance)") + return FALSE + + midround_admin_cancel = FALSE + midround_admin_reroll = FALSE + COOLDOWN_RESET(src, midround_admin_cancel_period) + + var/player_count = get_active_player_count(afk_check = TRUE) + var/list/rulesets_weighted = get_midround_rulesets(player_count, range) + var/datum/dynamic_ruleset/midround/picked_ruleset = pick_weight(rulesets_weighted) + if(isnull(picked_ruleset)) + log_dynamic("Midround ([range]): No rulesets to pick from!") + return FALSE + message_admins("Midround ([range]): Executing [picked_ruleset.config_tag] \ + [MIDROUND_CANCEL_HREF()] [MIDROUND_REROLL_HREF(rulesets_weighted)]") + // if we have admins online, we have a waiting period before execution to allow them to cancel or reroll + if(length(GLOB.admins)) + COOLDOWN_START(src, midround_admin_cancel_period, 15 SECONDS) + while(!COOLDOWN_FINISHED(src, midround_admin_cancel_period)) + if(midround_admin_cancel) + QDEL_LIST(rulesets_weighted) + COOLDOWN_START(src, midround_cooldown, get_ruleset_cooldown(range)) + return FALSE + if(midround_admin_reroll && length(rulesets_weighted) >= 2) + midround_admin_reroll = FALSE + COOLDOWN_START(src, midround_admin_cancel_period, 15 SECONDS) + rulesets_weighted -= picked_ruleset + qdel(picked_ruleset) + picked_ruleset = pick_weight(rulesets_weighted) + if(isnull(picked_ruleset)) + log_dynamic("Midround ([range]): No rulesets to pick from!") + message_admins("Rerolling Midround ([range]): Failed to pick a new ruleset, cancelling instead!") + midround_admin_cancel = TRUE + continue + message_admins("Rerolling Midround ([range]): Executing [picked_ruleset.config_tag] - \ + [length(rulesets_weighted) - 1] remaining rulesets in pool. [MIDROUND_CANCEL_HREF()] [MIDROUND_REROLL_HREF(rulesets_weighted)]") + stoplag() + + // NOTE: !! THIS CAN SLEEP !! + if(!picked_ruleset.prepare_execution(player_count, picked_ruleset.collect_candidates())) + log_dynamic("Midround ([range]): Selected ruleset [picked_ruleset.config_tag], but preparation failed!") + QDEL_LIST(rulesets_weighted) + return FALSE + // Run the thing + executed_rulesets += picked_ruleset + rulesets_weighted -= picked_ruleset + picked_ruleset.execute() + // Post execute logging + for(var/datum/mind/selected as anything in picked_ruleset.selected_minds) + message_admins("Midround ([range]): [ADMIN_LOOKUPFLW(selected.current)] has been selected for [picked_ruleset.config_tag].") + log_dynamic("Midround ([range]): [key_name(selected.current)] has been selected for [picked_ruleset.config_tag].") + notify_ghosts("[selected.name] has been picked for [picked_ruleset.config_tag]!", source = selected.current) + // Clean up unused rulesets + QDEL_LIST(rulesets_weighted) + rulesets_to_spawn[range] -= 1 + if(range == LIGHT_MIDROUND) + admin_forcing_next_light = FALSE + if(range == HEAVY_MIDROUND) + admin_forcing_next_heavy = FALSE + COOLDOWN_START(src, midround_cooldown, get_ruleset_cooldown(range)) + return TRUE + +/// Gets a weighted list of midround rulesets +/datum/controller/subsystem/dynamic/proc/get_midround_rulesets(player_count, midround_type) + PRIVATE_PROC(TRUE) + + var/list/datum/dynamic_ruleset/midround/rulesets = list() + for(var/datum/dynamic_ruleset/midround/ruleset_type as anything in subtypesof(/datum/dynamic_ruleset/midround)) + if(initial(ruleset_type.midround_type) != midround_type) + continue + var/datum/dynamic_ruleset/midround/ruleset = new ruleset_type(dynamic_config) + rulesets[ruleset] = ruleset.get_weight(player_count, current_tier.tier) + + return rulesets + +/** + * Attempt to run a midround ruleset of the given type + * + * * midround_type - The type of midround ruleset to force + * * forced_max_cap - Rather than using the ruleset's max antag cap, use this value + * As an example, this allows you to only spawn 1 traitor rather than the ruleset's default of 3 + * Can't be set to 0 (why are you forcing a ruleset that spawns 0 antags?) + * * alert_admins_on_fail - If TRUE, alert admins if the ruleset fails to prepare/execute + * * mob/admin - The admin who is forcing the ruleset, used for configuring the ruleset if possible + */ +/datum/controller/subsystem/dynamic/proc/force_run_midround(midround_typepath, forced_max_cap, alert_admins_on_fail = FALSE, mob/admin) + if(!ispath(midround_typepath, /datum/dynamic_ruleset/midround)) + CRASH("force_run_midround() was called with an invalid midround type: [midround_typepath]") + + var/datum/dynamic_ruleset/midround/running = new midround_typepath(dynamic_config) + if(isnum(forced_max_cap) && forced_max_cap > 0) + running.min_antag_cap = min(forced_max_cap, running.min_antag_cap) + running.max_antag_cap = forced_max_cap + + if(admin && (running.ruleset_flags & RULESET_ADMIN_CONFIGURABLE)) + if(running.configure_ruleset(admin) == RULESET_CONFIG_CANCEL) + qdel(running) + return FALSE + + // NOTE: !! THIS CAN SLEEP !! + if(!running.prepare_execution(get_active_player_count(afk_check = TRUE), running.collect_candidates())) + if(alert_admins_on_fail) + message_admins("Midround (forced): Forced ruleset [running.config_tag], but preparation failed!") + log_dynamic("Midround (forced): Forced ruleset [running.config_tag], but preparation failed!") + qdel(running) + return FALSE + + executed_rulesets += running + running.execute() + // Post execute logging + for(var/datum/mind/selected as anything in running.selected_minds) + message_admins("Midround (forced): [ADMIN_LOOKUPFLW(selected.current)] has been selected for [running.config_tag].") + log_dynamic("Midround (forced): [key_name(selected.current)] has been selected for [running.config_tag].") + notify_ghosts("[selected.name] has been picked for [running.config_tag]!", source = selected.current) + return TRUE + +/** + * Called when someone latejoins + * (This could be a signal in the future) + */ +/datum/controller/subsystem/dynamic/proc/on_latejoin(mob/living/carbon/human/latejoiner) + // First check queued rulesets - queued rulesets by pass cooldowns and probability checks, + // because they're generally forced by events or admins (and thus have higher priority) + for(var/datum/dynamic_ruleset/latejoin/queued in queued_rulesets) + // NOTE: !! THIS CAN SLEEP !! + if(!queued.prepare_execution(get_active_player_count(afk_check = TRUE), list(latejoiner))) + message_admins("Latejoin (forced): Queued ruleset [queued.config_tag] failed to prepare! It remains queued for next latejoin. (REMOVE FROM QUEUE)") + log_dynamic("Latejoin (forced): Queued ruleset [queued.config_tag] failed to prepare! It remains queued for next latejoin.") + continue + message_admins("Latejoin (forced): [ADMIN_LOOKUPFLW(latejoiner)] has been selected for [queued.config_tag].") + log_dynamic("Latejoin (forced): [key_name(latejoiner)] has been selected for [queued.config_tag].") + queued_rulesets -= queued + executed_rulesets += queued + queued.execute() + return + + if(COOLDOWN_FINISHED(src, latejoin_ruleset_start) && COOLDOWN_FINISHED(src, latejoin_cooldown)) + if(try_spawn_latejoin(latejoiner)) + return + +/** + * Invoked by SSdynamic to try to spawn a latejoin ruleset + * Respects ranges and thresholds + * + * Returns TRUE if a ruleset was spawned, FALSE otherwise + */ +/datum/controller/subsystem/dynamic/proc/try_spawn_latejoin(mob/living/carbon/human/latejoiner) + + if(rulesets_to_spawn[LATEJOIN] <= 0) + return FALSE + var/latejoin_chance = get_latejoin_chance() + if(!prob(latejoin_chance)) + log_dynamic("Latejoin: Ruleset chance failed ([latejoin_chance]% chance)") + return FALSE + + var/player_count = get_active_player_count(afk_check = TRUE) + var/list/rulesets_weighted = get_latejoin_rulesets(player_count) + // Note, we make no effort to actually pick a valid ruleset here + // We pick a ruleset, and they player might not even have that antag selected. And that's fine + var/datum/dynamic_ruleset/latejoin/picked_ruleset = pick_weight(rulesets_weighted) + if(isnull(picked_ruleset)) + log_dynamic("Latejoin: No rulesets to pick from!") + return FALSE + // NOTE: !! THIS CAN SLEEP !! + if(!picked_ruleset.prepare_execution(player_count, list(latejoiner))) + log_dynamic("Latejoin: Selected ruleset [picked_ruleset.name] for [key_name(latejoiner)], but preparation failed! Latejoin chance has increased.") + QDEL_LIST(rulesets_weighted) + failed_latejoins++ + return FALSE + // Run the thing + executed_rulesets += picked_ruleset + rulesets_weighted -= picked_ruleset + picked_ruleset.execute() + // Post execute logging + if(!(latejoiner.mind in picked_ruleset.selected_minds)) + stack_trace("Dynamic: Latejoin [picked_ruleset.type] executed, but the latejoiner was not in its selected minds list!") + message_admins("Latejoin: [ADMIN_LOOKUPFLW(latejoiner)] has been selected for [picked_ruleset.config_tag].") + log_dynamic("Latejoin: [key_name(latejoiner)] has been selected for [picked_ruleset.config_tag].") + // Clean up unused rulesets + QDEL_LIST(rulesets_weighted) + rulesets_to_spawn[LATEJOIN] -= 1 + failed_latejoins = 0 + admin_forcing_next_latejoin = FALSE + COOLDOWN_START(src, latejoin_cooldown, get_ruleset_cooldown(LATEJOIN)) + return TRUE + +/// Gets a weighted list of latejoin rulesets +/datum/controller/subsystem/dynamic/proc/get_latejoin_rulesets(player_count) + PRIVATE_PROC(TRUE) + + var/list/datum/dynamic_ruleset/latejoin/rulesets = list() + for(var/ruleset_type in subtypesof(/datum/dynamic_ruleset/latejoin)) + var/datum/dynamic_ruleset/latejoin/ruleset = new ruleset_type(dynamic_config) + rulesets[ruleset] = ruleset.get_weight(player_count, current_tier.tier) + + return rulesets + +/** + * Queues a ruleset to run on roundstart or next latejoin which fulfills all requirements + * + * For example, if you queue a latejoin revolutionary, it'll only run when population gets large enough and there are enough heads of staff + * For all latejoins until then, it will simply do nothing + * + * * latejoin_type - The type of latejoin ruleset to force + */ +/datum/controller/subsystem/dynamic/proc/queue_ruleset(ruleset_typepath) + if(!ispath(ruleset_typepath, /datum/dynamic_ruleset/latejoin) && !ispath(ruleset_typepath, /datum/dynamic_ruleset/roundstart)) + CRASH("queue_ruleset() was called with an invalid type: [ruleset_typepath]") + + queued_rulesets += new ruleset_typepath(dynamic_config) + +/** + * Get the cooldown between attempts to spawn a ruleset of the given type + */ +/datum/controller/subsystem/dynamic/proc/get_ruleset_cooldown(range) + if(range == ROUNDSTART) + stack_trace("Attempting to get cooldown for roundstart rulesets - this is redundant and is likely an error") + return 0 + + var/low = current_tier.ruleset_type_settings[range][EXECUTION_COOLDOWN_LOW] || 0 + var/high = current_tier.ruleset_type_settings[range][EXECUTION_COOLDOWN_HIGH] || 0 + return rand(low, high) + +/** + * Gets the chance of a midround ruleset being selected + */ +/datum/controller/subsystem/dynamic/proc/get_midround_chance(range) + if(admin_forcing_next_light && range == LIGHT_MIDROUND) + return 100 + if(admin_forcing_next_heavy && range == HEAVY_MIDROUND) + return 100 + + var/chance = 0 + var/num_antags = length(GLOB.current_living_antags) + var/num_dead = length(GLOB.dead_player_list) + var/num_alive = get_active_player_count(afk_check = TRUE) + if(num_dead + num_alive <= 0) + return 0 + + chance += 100 - (200 * (num_dead / (num_alive + num_dead))) + if(num_antags < 0) + chance += 50 + + return chance + +/** + * Gets the chance of a latejoin ruleset being selected + */ +/datum/controller/subsystem/dynamic/proc/get_latejoin_chance() + if(admin_forcing_next_latejoin) + return 100 + + var/chance = 0 + var/num_antags = length(GLOB.current_living_antags) + var/num_dead = length(GLOB.dead_player_list) + var/num_alive = get_active_player_count(afk_check = TRUE) + if(num_dead + num_alive <= 0) + return 0 + + chance += 100 - (200 * (num_dead / (num_alive + num_dead))) + if(num_antags < 0) + chance += 50 + chance += (failed_latejoins * 15) + // Reduced chance before lights start + if(!COOLDOWN_FINISHED(src, light_ruleset_start)) + chance *= 0.2 + + return chance -// Set result and news report here /datum/controller/subsystem/dynamic/proc/set_round_result() // If it got to this part, just pick one high impact ruleset if it exists - for(var/datum/dynamic_ruleset/rule in executed_rules) - if(rule.flags & HIGH_IMPACT_RULESET) - rule.round_result() - // One was set, so we're done here - if(SSticker.news_report) - return + for(var/datum/dynamic_ruleset/rule as anything in executed_rulesets) + if(rule.round_result()) + return SSticker.mode_result = "undefined" + switch(GLOB.revolution_handler?.result) + if(STATION_VICTORY) + SSticker.mode_result = "loss - rev heads killed" + SSticker.news_report = REVS_LOSE + if(REVOLUTION_VICTORY) + SSticker.mode_result = "win - heads killed" + SSticker.news_report = REVS_WIN + // Something nuked the station - it wasn't nuke ops (they set their own via their rulset) if(GLOB.station_was_nuked) SSticker.news_report = STATION_NUKED @@ -312,717 +597,121 @@ SUBSYSTEM_DEF(dynamic) if(EMERGENCY_ESCAPED_OR_ENDGAMED && !SSticker.news_report) SSticker.news_report = SSshuttle.emergency?.is_hijacked() ? SHUTTLE_HIJACK : STATION_EVACUATED -/datum/controller/subsystem/dynamic/proc/send_intercept() - /*if(GLOB.communications_controller.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_intercept)), 10 SECONDS) +/// Helper to clear all queued rulesets and stop any other rulesets from naturally spawning +/datum/controller/subsystem/dynamic/proc/force_extended() + for(var/category in rulesets_to_spawn) + rulesets_to_spawn[category] = 0 + QDEL_LIST(queued_rulesets) + +/datum/controller/subsystem/dynamic/Topic(href, list/href_list) + . = ..() + if(href_list["admin_dequeue"]) + if(!check_rights(R_ADMIN)) + return + var/datum/dynamic_ruleset/to_remove = locate(href_list["admin_dequeue"]) in queued_rulesets + if(!istype(to_remove)) + return + queued_rulesets -= to_remove + qdel(to_remove) + message_admins(span_adminnotice("[key_name_admin(usr)] [to_remove.config_tag] from the latejoin queue.")) + log_admin("[key_name(usr)] removed [to_remove.config_tag] from the latejoin queue.") return - . = "Nanotrasen Department of Intelligence Threat Advisory, Spinward Sector, TCD [time2text(world.realtime, "DDD, MMM DD", NO_TIMEZONE)], [CURRENT_STATION_YEAR]:
" - . += generate_advisory_level() + if(href_list["admin_reroll"]) + if(!check_rights(R_ADMIN) || midround_admin_reroll) + return + if(COOLDOWN_FINISHED(src, midround_admin_cancel_period)) + to_chat(usr, span_alert("Too late!")) + return + midround_admin_reroll = TRUE + message_admins(span_adminnotice("[key_name_admin(usr)] rerolled the queued midround ruleset.")) + log_admin("[key_name(usr)] rerolled the queued midround ruleset.") + return - var/min_threat = 100 - for(var/datum/dynamic_ruleset/ruleset as anything in init_rulesets(/datum/dynamic_ruleset)) - if(ruleset.weight <= 0 || ruleset.cost <= 0) + if(href_list["admin_cancel_midround"]) + if(!check_rights(R_ADMIN) || midround_admin_cancel) + return + if(COOLDOWN_FINISHED(src, midround_admin_cancel_period)) + to_chat(usr, span_alert("Too late!")) + return + midround_admin_cancel = TRUE + message_admins(span_adminnotice("[key_name_admin(usr)] cancelled the queued midround ruleset.")) + log_admin("[key_name(usr)] cancelled the queued midround ruleset.") + return + +#ifdef TESTING +/// Puts all repo defaults into a dynamic.toml file +/datum/controller/subsystem/dynamic/proc/build_dynamic_toml() + var/data = "" + for(var/tier_type in subtypesof(/datum/dynamic_tier)) + var/datum/dynamic_tier/tier = new tier_type() + if(!tier.config_tag) + qdel(tier) continue - min_threat = min(ruleset.cost, min_threat) - var/greenshift = GLOB.dynamic_forced_extended || (threat_level < min_threat) //if threat is below any ruleset, its extended time - SSstation.generate_station_goals(greenshift ? INFINITY : CONFIG_GET(number/station_goal_budget)) + data += "\[\"[tier.config_tag]\"\]\n" + data += "name = \"[tier.name]\"\n" + data += "min_pop = [tier.min_pop]\n" + data += "weight = [tier.weight]\n" + data += "advisory_report = \"[tier.advisory_report]\"\n" + for(var/range in tier.ruleset_type_settings) + data += "ruleset_type_settings.[range].[LOW_END] = [tier.ruleset_type_settings[range]?[LOW_END] || 0]\n" + data += "ruleset_type_settings.[range].[HIGH_END] = [tier.ruleset_type_settings[range]?[HIGH_END] || 0]\n" + data += "ruleset_type_settings.[range].[HALF_RANGE_POP_THRESHOLD] = [tier.ruleset_type_settings[range]?[HALF_RANGE_POP_THRESHOLD] || 0]\n" + data += "ruleset_type_settings.[range].[FULL_RANGE_POP_THRESHOLD] = [tier.ruleset_type_settings[range]?[FULL_RANGE_POP_THRESHOLD] || 0]\n" + if(range != ROUNDSTART) + data += "ruleset_type_settings.[range].[TIME_THRESHOLD] = [(tier.ruleset_type_settings[range]?[TIME_THRESHOLD] || 0) / 60 / 10]\n" + data += "ruleset_type_settings.[range].[EXECUTION_COOLDOWN_LOW] = [(tier.ruleset_type_settings[range]?[EXECUTION_COOLDOWN_LOW] || 0) / 60 / 10]\n" + data += "ruleset_type_settings.[range].[EXECUTION_COOLDOWN_HIGH] = [(tier.ruleset_type_settings[range]?[EXECUTION_COOLDOWN_HIGH] || 0) / 60 / 10]\n" - var/list/datum/station_goal/goals = SSstation.get_station_goals() - if(length(goals)) - var/list/texts = list("
Special Orders for [station_name()]:
") - for(var/datum/station_goal/station_goal as anything in goals) - station_goal.on_report() - texts += station_goal.get_report() - . += texts.Join("
") + data += "\n" + qdel(tier) - 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) + for(var/ruleset_type in subtypesof(/datum/dynamic_ruleset)) + var/datum/dynamic_ruleset/ruleset = new ruleset_type() + if(!ruleset.config_tag) + qdel(ruleset) continue - trait_list_strings += "[station_trait.get_report()]
" - if(trait_list_strings.len > 0) - . += "
Identified shift divergencies:
" + trait_list_strings.Join() - if(length(GLOB.communications_controller.command_report_footnotes)) - var/footnote_pile = "" - - for(var/datum/command_footnote/footnote in GLOB.communications_controller.command_report_footnotes) - footnote_pile += "[footnote.message]
" - footnote_pile += "[footnote.signature]
" - footnote_pile += "
" - - . += "
Additional Notes:

" + 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\nA 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*/ // BUBBER EDIT END - - return . - -/// Generate the advisory level depending on the shown threat level. -/datum/controller/subsystem/dynamic/proc/generate_advisory_level() - var/advisory_string = "" - switch(round(threat_level)) - if(0 to 65) - advisory_string += "Advisory Level: Yellow Star
" - advisory_string += "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." - if(66 to 79) - advisory_string += "Advisory Level: Red Star
" - advisory_string += "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." - if(80 to 99) - advisory_string += "Advisory Level: Black Orbit
" - advisory_string += "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." - if(100) - advisory_string += "Advisory Level: Midnight Sun
" - advisory_string += "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." - - return advisory_string - -/datum/controller/subsystem/dynamic/proc/show_threatlog(mob/admin) - if(!SSticker.HasRoundStarted()) - tgui_alert(usr, "The round hasn't started yet!") - return - - if(!check_rights(R_ADMIN)) - return - - var/list/out = list("Threat Log
Starting Threat: [threat_level]
") - - for(var/entry in threat_log) - if(istext(entry)) - out += "[entry]
" - - out += "Remaining threat/threat_level: [mid_round_budget]/[threat_level]" - - usr << browse(HTML_SKELETON_TITLE("Threat Log", out.Join()), "window=threatlog;size=700x500") - -/// Generates the threat level using lorentz distribution and assigns peaceful_percentage. -/datum/controller/subsystem/dynamic/proc/generate_threat() - // At lower pop levels we run a Liner Interpolation against the max threat based proportionally on the number - // of players ready. This creates a balanced lorentz curve within a smaller range than 0 to max_threat_level. - var/calculated_max_threat = (SSticker.totalPlayersReady < low_pop_player_threshold) ? LERP(low_pop_maximum_threat, max_threat_level, SSticker.totalPlayersReady / low_pop_player_threshold) : max_threat_level - log_dynamic("Calculated maximum threat level based on player count of [SSticker.totalPlayersReady]: [calculated_max_threat]") - - threat_level = lorentz_to_amount(threat_curve_centre, threat_curve_width, calculated_max_threat) - - for(var/datum/station_trait/station_trait in GLOB.dynamic_station_traits) - threat_level = max(threat_level - GLOB.dynamic_station_traits[station_trait], 0) - log_dynamic("Threat reduced by [GLOB.dynamic_station_traits[station_trait]]. Source: [type].") - - peaceful_percentage = (threat_level/max_threat_level)*100 - -/// Generates the midround and roundstart budgets -/datum/controller/subsystem/dynamic/proc/generate_budgets() - round_start_budget = lorentz_to_amount(roundstart_split_curve_centre, roundstart_split_curve_width, threat_level, 0.1) - initial_round_start_budget = round_start_budget - mid_round_budget = threat_level - round_start_budget - -/datum/controller/subsystem/dynamic/proc/setup_parameters() - log_dynamic("Dynamic mode parameters for the round:") - log_dynamic("Centre is [threat_curve_centre], Width is [threat_curve_width], Forced extended is [GLOB.dynamic_forced_extended ? "Enabled" : "Disabled"], No stacking is [GLOB.dynamic_no_stacking ? "Enabled" : "Disabled"].") - log_dynamic("Stacking limit is [GLOB.dynamic_stacking_limit].") - if(GLOB.dynamic_forced_threat_level >= 0) - threat_level = round(GLOB.dynamic_forced_threat_level, 0.1) - else - generate_threat() - generate_budgets() - set_cooldowns() - log_dynamic("Dynamic Mode initialized with a Threat Level of... [threat_level]! ([round_start_budget] round start budget)") - SSblackbox.record_feedback( - "associative", - "dynamic_threat", - 1, - list( - "server_name" = CONFIG_GET(string/serversqlname), - "forced_threat_level" = GLOB.dynamic_forced_threat_level, - "threat_level" = threat_level, - "max_threat" = (SSticker.totalPlayersReady < low_pop_player_threshold) ? LERP(low_pop_maximum_threat, max_threat_level, SSticker.totalPlayersReady / low_pop_player_threshold) : max_threat_level, - "player_count" = SSticker.totalPlayersReady, - "round_start_budget" = round_start_budget, - "parameters" = list( - "threat_curve_centre" = threat_curve_centre, - "threat_curve_width" = threat_curve_width, - "forced_extended" = GLOB.dynamic_forced_extended, - "no_stacking" = GLOB.dynamic_no_stacking, - "stacking_limit" = GLOB.dynamic_stacking_limit, - ), - ), - ) - return TRUE - - -/datum/controller/subsystem/dynamic/proc/set_cooldowns() - var/latejoin_injection_cooldown_middle = 0.5*(latejoin_delay_max + latejoin_delay_min) - latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), latejoin_delay_min, latejoin_delay_max)) + world.time - -// Called BEFORE everyone is equipped with their job -/datum/controller/subsystem/dynamic/proc/pre_setup() - if(CONFIG_GET(flag/dynamic_config_enabled)) - var/json_file = file("[global.config.directory]/dynamic.json") - if(fexists(json_file)) - configuration = json_decode(file2text(json_file)) - if(configuration["Dynamic"]) - for(var/variable in configuration["Dynamic"]) - if(!(variable in vars)) - stack_trace("Invalid dynamic configuration variable [variable] in game mode variable changes.") - continue - vars[variable] = configuration["Dynamic"][variable] - - configure_station_trait_costs() - setup_parameters() - setup_hijacking() - setup_rulesets() - - //We do this here instead of with the midround rulesets and such because these rules can hang refs - //To new_player and such, and we want the datums to just free when the roundstart work is done - var/list/roundstart_rules = init_rulesets(/datum/dynamic_ruleset/roundstart) - - SSjob.divide_occupations(pure = TRUE, allow_all = TRUE) - for(var/i in GLOB.new_player_list) - var/mob/dead/new_player/player = i - if(player.ready == PLAYER_READY_TO_PLAY && player.mind && player.check_preferences()) - if(is_unassigned_job(player.mind.assigned_role)) - var/list/job_data = list() - var/job_prefs = player.client.prefs.job_preferences - for(var/job in job_prefs) - var/priority = job_prefs[job] - job_data += "[job]: [SSjob.job_priority_level_to_string(priority)]" - to_chat(player, span_danger("You were unable to qualify for any roundstart antagonist role this round because your job preferences presented a high chance of all of your selected jobs being unavailable, along with 'return to lobby if job is unavailable' enabled. Increase the number of roles set to medium or low priority to reduce the chances of this happening.")) - log_admin("[player.ckey] failed to qualify for any roundstart antagonist role because their job preferences presented a high chance of all of their selected jobs being unavailable, along with 'return to lobby if job is unavailable' enabled and has [player.client.prefs.be_special.len] antag preferences enabled. They will be unable to qualify for any roundstart antagonist role. These are their job preferences - [job_data.Join(" | ")]") + data += "\[\"[ruleset.config_tag]\"\]\n" + if(islist(ruleset.weight)) + for(var/i in 1 to length(ruleset.weight)) + data += "weight.[i] = [ruleset.weight[i]]\n" + else + data += "weight = [ruleset.weight || 0]\n" + if(islist(ruleset.min_pop)) + for(var/i in 1 to length(ruleset.min_pop)) + data += "min_pop.[i] = [ruleset.min_pop[i]]\n" + else + data += "min_pop = [ruleset.min_pop || 0]\n" + if(length(ruleset.blacklisted_roles)) + data += "blacklisted_roles = \[\n" + for(var/i in ruleset.blacklisted_roles) + data += "\t\"[i]\",\n" + data += "\]\n" + else + data += "blacklisted_roles = \[\]\n" + if(!istype(ruleset, /datum/dynamic_ruleset/latejoin) && !istype(ruleset, /datum/dynamic_ruleset/midround/from_living)) + if(islist(ruleset.min_antag_cap)) + for(var/ruleset_min_antag_cap in ruleset.min_antag_cap) + data += "min_antag_cap.[ruleset_min_antag_cap] = [ruleset.min_antag_cap[ruleset_min_antag_cap]]\n" else - roundstart_pop_ready++ - candidates.Add(player) - SSjob.reset_occupations() - log_dynamic("Listing [roundstart_rules.len] round start rulesets, and [candidates.len] players ready.") - if (candidates.len <= 0) - log_dynamic("[candidates.len] candidates.") - return TRUE + data += "min_antag_cap = [ruleset.min_antag_cap || 0]\n" + if(islist(ruleset.max_antag_cap)) + for(var/ruleset_max_antag_cap in ruleset.max_antag_cap) + data += "max_antag_cap.[ruleset_max_antag_cap] = [ruleset.max_antag_cap[ruleset_max_antag_cap]]\n" + else if(!isnull(ruleset.max_antag_cap)) + data += "max_antag_cap = [ruleset.max_antag_cap]\n" + else + data += "# max_antag_cap = min_antag_cap\n" + data += "repeatable_weight_decrease = [ruleset.repeatable_weight_decrease]\n" + data += "repeatable = [ruleset.repeatable]\n" + data += "minimum_required_age = [ruleset.minimum_required_age]\n" + data += "\n" + qdel(ruleset) - if(GLOB.dynamic_forced_roundstart_ruleset.len > 0) - rigged_roundstart() - else - roundstart(roundstart_rules) - - log_dynamic("[round_start_budget] round start budget was left, donating it to midrounds.") - threat_log += "[gameTimestamp()]: [round_start_budget] round start budget was left, donating it to midrounds." - mid_round_budget += round_start_budget - - var/starting_rulesets = "" - for (var/datum/dynamic_ruleset/roundstart/DR in executed_rules) - starting_rulesets += "[DR.name], " - log_dynamic("Picked the following roundstart rules: [starting_rulesets]") - candidates.Cut() + var/filepath = "[global.config.directory]/dynamic.toml" + fdel(file(filepath)) + text2file(data, filepath) return TRUE - -// Called AFTER everyone is equipped with their job -/datum/controller/subsystem/dynamic/proc/post_setup(report) - for(var/datum/dynamic_ruleset/roundstart/rule in executed_rules) - rule.candidates.Cut() // The rule should not use candidates at this point as they all are null. - addtimer(CALLBACK(src, PROC_REF(execute_roundstart_rule), rule), rule.delay) - - if (!CONFIG_GET(flag/no_intercept_report)) - addtimer(CALLBACK(src, PROC_REF(send_intercept)), rand(waittime_l, waittime_h)) - //SKYRAT EDIT START - DIVERGENCY/GOALS REPORT -// else // BUBBER EDIT REMOVAL -// addtimer(CALLBACK(src, PROC_REF(send_trait_report)), rand(waittime_l, waittime_h)) // BUBBER EDIT REMOVAL - //SKYRAT EDIT END - addtimer(CALLBACK(src, PROC_REF(display_roundstart_logout_report)), ROUNDSTART_LOGOUT_REPORT_TIME) - - if(CONFIG_GET(flag/reopen_roundstart_suicide_roles)) - var/delay = CONFIG_GET(number/reopen_roundstart_suicide_roles_delay) - if(delay) - delay *= (1 SECONDS) - else - delay = (4 MINUTES) //default to 4 minutes if the delay isn't defined. - addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(reopen_roundstart_suicide_roles)), delay) - - 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) - return TRUE - -/datum/controller/subsystem/dynamic/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 += "[L.name] ([L.key]), the [L.job] (Disconnected)\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 += "[L.name] ([L.key]), the [L.job] (Connected, Inactive)\n" - failed = TRUE //AFK client - if(!failed && L.stat) - if(HAS_TRAIT(L, TRAIT_SUICIDED)) //Suicider - msg += "[L.name] ([L.key]), the [L.job] ([span_bolddanger("Suicide")])\n" - failed = TRUE //Disconnected client - if(!failed && (L.stat == UNCONSCIOUS || L.stat == HARD_CRIT)) - msg += "[L.name] ([L.key]), the [L.job] (Dying)\n" - failed = TRUE //Unconscious - if(!failed && L.stat == DEAD) - msg += "[L.name] ([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 += "[L.name] ([ckey(D.mind.key)]), the [L.job] ([span_bolddanger("Suicide")])\n" - continue //Disconnected client - else - msg += "[L.name] ([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 += "[L.name] ([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) - -/// Initializes the internal ruleset variables -/datum/controller/subsystem/dynamic/proc/setup_rulesets() - midround_rules = init_rulesets(/datum/dynamic_ruleset/midround) - latejoin_rules = init_rulesets(/datum/dynamic_ruleset/latejoin) - -/// Returns a list of the provided rulesets. -/// Configures their variables to match config. -/datum/controller/subsystem/dynamic/proc/init_rulesets(ruleset_subtype) - var/list/rulesets = list() - - for (var/datum/dynamic_ruleset/ruleset_type as anything in subtypesof(ruleset_subtype)) - if (initial(ruleset_type.name) == "") - continue - - if (initial(ruleset_type.weight) == 0) - continue - - var/ruleset = new ruleset_type - configure_ruleset(ruleset) - rulesets += ruleset - - return rulesets - -/// A simple roundstart proc used when dynamic_forced_roundstart_ruleset has rules in it. -/datum/controller/subsystem/dynamic/proc/rigged_roundstart() - message_admins("[GLOB.dynamic_forced_roundstart_ruleset.len] rulesets being forced. Will now attempt to draft players for them.") - log_dynamic("[GLOB.dynamic_forced_roundstart_ruleset.len] rulesets being forced. Will now attempt to draft players for them.") - for (var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset) - configure_ruleset(rule) - message_admins("Drafting players for forced ruleset [rule.name].") - log_dynamic("Drafting players for forced ruleset [rule.name].") - rule.acceptable(roundstart_pop_ready, threat_level) // Assigns some vars in the modes, running it here for consistency - rule.candidates = candidates.Copy() - rule.trim_candidates() - rule.load_templates() - if (rule.ready(roundstart_pop_ready, TRUE)) - var/cost = rule.cost - var/scaled_times = 0 - if (rule.scaling_cost) - scaled_times = round(max(round_start_budget - cost, 0) / rule.scaling_cost) - cost += rule.scaling_cost * scaled_times - - spend_roundstart_budget(picking_roundstart_rule(rule, scaled_times, forced = TRUE)) - -/datum/controller/subsystem/dynamic/proc/roundstart(list/roundstart_rules) - if (GLOB.dynamic_forced_extended) - log_dynamic("Starting a round of forced extended.") - return TRUE - var/list/drafted_rules = list() - for (var/datum/dynamic_ruleset/roundstart/rule in roundstart_rules) - if (!rule.weight) - continue - if (rule.acceptable(roundstart_pop_ready, threat_level) && round_start_budget >= rule.cost) // If we got the population and threat required - rule.candidates = candidates.Copy() - rule.trim_candidates() - rule.load_templates() - if (rule.ready(roundstart_pop_ready) && rule.candidates.len > 0) - drafted_rules[rule] = rule.weight - - var/list/rulesets_picked = list() - - // Kept in case a ruleset can't be initialized for whatever reason, we want to be able to only spend what we can use. - var/round_start_budget_left = round_start_budget - - while (round_start_budget_left > 0) - var/datum/dynamic_ruleset/roundstart/ruleset = pick_weight(drafted_rules) - if (isnull(ruleset)) - log_dynamic("No more rules can be applied, stopping with [round_start_budget] left.") - break - - var/cost = (ruleset in rulesets_picked) ? ruleset.scaling_cost : ruleset.cost - if (cost == 0) - stack_trace("[ruleset] cost 0, this is going to result in an infinite loop.") - drafted_rules[ruleset] = null - continue - - if (cost > round_start_budget_left) - drafted_rules[ruleset] = null - continue - - if (check_blocking(ruleset.blocking_rules, rulesets_picked)) - drafted_rules[ruleset] = null - continue - - round_start_budget_left -= cost - - rulesets_picked[ruleset] += 1 - - if (ruleset.flags & HIGH_IMPACT_RULESET) - for (var/_other_ruleset in drafted_rules) - var/datum/dynamic_ruleset/other_ruleset = _other_ruleset - if (other_ruleset.flags & HIGH_IMPACT_RULESET) - drafted_rules[other_ruleset] = null - - if (ruleset.flags & LONE_RULESET) - drafted_rules[ruleset] = null - - for (var/ruleset in rulesets_picked) - spend_roundstart_budget(picking_roundstart_rule(ruleset, rulesets_picked[ruleset] - 1)) - - update_log() - -/// Initializes the round start ruleset provided to it. Returns how much threat to spend. -/datum/controller/subsystem/dynamic/proc/picking_roundstart_rule(datum/dynamic_ruleset/roundstart/ruleset, scaled_times = 0, forced = FALSE) - log_dynamic("Picked a ruleset: [ruleset.name], scaled [scaled_times] times") - - ruleset.trim_candidates() - var/added_threat = ruleset.scale_up(roundstart_pop_ready, scaled_times) - - if(ruleset.pre_execute(roundstart_pop_ready)) - threat_log += "[gameTimestamp()]: Roundstart [ruleset.name] spent [ruleset.cost + added_threat]. [ruleset.scaling_cost ? "Scaled up [ruleset.scaled_times]/[scaled_times] times." : ""]" - if(ruleset.flags & ONLY_RULESET) - only_ruleset_executed = TRUE - if(ruleset.flags & HIGH_IMPACT_RULESET) - high_impact_ruleset_executed = TRUE - executed_rules += ruleset - return ruleset.cost + added_threat - else - stack_trace("The starting rule \"[ruleset.name]\" failed to pre_execute.") - return 0 - -/// Mainly here to facilitate delayed rulesets. All roundstart rulesets are executed with a timered callback to this proc. -/datum/controller/subsystem/dynamic/proc/execute_roundstart_rule(sent_rule) - var/datum/dynamic_ruleset/rule = sent_rule - if(rule.execute()) - if(rule.persistent) - current_rules += rule - new_snapshot(rule) - rule.forget_startup() - return TRUE - rule.clean_up() // Refund threat, delete teams and so on. - rule.forget_startup() - executed_rules -= rule - stack_trace("The starting rule \"[rule.name]\" failed to execute.") - return FALSE - -/// An experimental proc to allow admins to call rules on the fly or have rules call other rules. -/datum/controller/subsystem/dynamic/proc/picking_specific_rule(ruletype, forced = FALSE, ignore_cost = FALSE) - var/datum/dynamic_ruleset/midround/new_rule - if(ispath(ruletype)) - new_rule = new ruletype() // You should only use it to call midround rules though. - configure_ruleset(new_rule) // This makes sure the rule is set up properly. - else if(istype(ruletype, /datum/dynamic_ruleset)) - new_rule = ruletype - else - return FALSE - - if(!new_rule) - return FALSE - - if(!forced) - if(only_ruleset_executed) - return FALSE - // Check if a blocking ruleset has been executed. - else if(check_blocking(new_rule.blocking_rules, executed_rules)) - return FALSE - // Check if the ruleset is high impact and if a high impact ruleset has been executed - else if(new_rule.flags & HIGH_IMPACT_RULESET) - if(threat_level < GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking) - if(high_impact_ruleset_executed) - return FALSE - - var/population = GLOB.alive_player_list.len - if((new_rule.acceptable(population, threat_level) && (ignore_cost || new_rule.cost <= mid_round_budget)) || forced) - new_rule.trim_candidates() - new_rule.load_templates() - if (new_rule.ready(forced)) - if (!ignore_cost) - spend_midround_budget(new_rule.cost, threat_log, "[gameTimestamp()]: Forced rule [new_rule.name]") - new_rule.pre_execute(population) - if (new_rule.execute()) // This should never fail since ready() returned 1 - if(new_rule.flags & HIGH_IMPACT_RULESET) - high_impact_ruleset_executed = TRUE - else if(new_rule.flags & ONLY_RULESET) - only_ruleset_executed = TRUE - log_dynamic("Making a call to a specific ruleset...[new_rule.name]!") - executed_rules += new_rule - if (new_rule.persistent) - current_rules += new_rule - new_rule.forget_startup() - return TRUE - else if (forced) - log_dynamic("The ruleset [new_rule.name] couldn't be executed due to lack of elligible players.") - new_rule.forget_startup() - return FALSE - -/datum/controller/subsystem/dynamic/fire() - for (var/datum/dynamic_ruleset/rule in current_rules) - if(rule.rule_process() == RULESET_STOP_PROCESSING) // If rule_process() returns 1 (RULESET_STOP_PROCESSING), stop processing. - current_rules -= rule - - try_midround_roll() - -/// Removes type from the list -/datum/controller/subsystem/dynamic/proc/remove_from_list(list/type_list, type) - for(var/I in type_list) - if(istype(I, type)) - type_list -= I - return type_list - -/// Checks if a type in blocking_list is in rule_list. -/datum/controller/subsystem/dynamic/proc/check_blocking(list/blocking_list, list/rule_list) - if(blocking_list.len > 0) - for(var/blocking in blocking_list) - for(var/_executed in rule_list) - var/datum/executed = _executed - if(blocking == executed.type) - log_dynamic("FAIL: check_blocking - [blocking] conflicts with [executed.type]") - return TRUE - return FALSE - -/// Handles late-join antag assignments -/datum/controller/subsystem/dynamic/proc/make_antag_chance(mob/living/carbon/human/newPlayer) - if (GLOB.dynamic_forced_extended) - return - if(EMERGENCY_ESCAPED_OR_ENDGAMED) // No more rules after the shuttle has left - return - - if (forced_latejoin_rule) - log_dynamic("Forcing specific [forced_latejoin_rule.ruletype] ruleset [forced_latejoin_rule].") - if(!handle_executing_latejoin(forced_latejoin_rule, newPlayer, forced = TRUE)) - message_admins("The forced latejoin ruleset [forced_latejoin_rule.name] couldn't be executed \ - as the most recent latejoin did not fulfill the ruleset's requirements.") - forced_latejoin_rule = null - return - - if(!late_forced_injection) - if(latejoin_injection_cooldown >= world.time) - return - if(!prob(latejoin_roll_chance)) - return - - var/was_forced = late_forced_injection - late_forced_injection = FALSE - var/list/possible_latejoin_rules = list() - for (var/datum/dynamic_ruleset/latejoin/rule in latejoin_rules) - if(!rule.weight) - continue - if(mid_round_budget < rule.cost) - continue - if(!rule.acceptable(GLOB.alive_player_list.len, threat_level)) - continue - possible_latejoin_rules[rule] = rule.get_weight() - - if(!length(possible_latejoin_rules)) - log_dynamic("FAIL: [newPlayer] was selected to roll for a latejoin ruleset, but there were no valid rulesets.") - return - - log_dynamic("[newPlayer] was selected to roll for a latejoin ruleset from the following list: [english_list(possible_latejoin_rules)].") - // You get one shot at becoming a latejoin antag, if it fails the next guy will try. - var/datum/dynamic_ruleset/latejoin/picked_rule = pick_ruleset(possible_latejoin_rules, max_allowed_attempts = 1) - if(isnull(picked_rule)) - log_dynamic("FAIL: No valid rulset was selected for [newPlayer]'s latejoin[was_forced ? "" : ", the next player will be checked instead"].") - return - if(was_forced) - log_dynamic("Forcing random [picked_rule.ruletype] ruleset [picked_rule].") - handle_executing_latejoin(picked_rule, newPlayer, forced = was_forced) - -/** - * This proc handles the execution of a latejoin ruleset, including removing it from latejoin rulesets if not repeatable, - * upping the injection cooldown, and starting a timer to execute the ruleset on delay. - */ -/datum/controller/subsystem/dynamic/proc/handle_executing_latejoin(datum/dynamic_ruleset/ruleset, mob/living/carbon/human/only_candidate, forced = FALSE) - ruleset.candidates = list(only_candidate) - ruleset.trim_candidates() - ruleset.load_templates() - if (!ruleset.ready(forced)) - log_dynamic("FAIL: [only_candidate] was selected to latejoin with the [ruleset] ruleset, \ - but the ruleset failed to execute[length(ruleset.candidates) ? "":" as they were not a valid candiate"].") - return FALSE - if (!ruleset.repeatable) - latejoin_rules = remove_from_list(latejoin_rules, ruleset.type) - addtimer(CALLBACK(src, PROC_REF(execute_midround_latejoin_rule), ruleset), ruleset.delay) - - if(!forced) - var/latejoin_injection_cooldown_middle = 0.5 * (latejoin_delay_max + latejoin_delay_min) - latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), latejoin_delay_min, latejoin_delay_max)) + world.time - log_dynamic("A latejoin rulset triggered successfully, the next latejoin injection will happen at [latejoin_injection_cooldown] round time.") - - return TRUE - -/// Apply configurations to rule. -/datum/controller/subsystem/dynamic/proc/configure_ruleset(datum/dynamic_ruleset/ruleset) - var/rule_conf = LAZYACCESSASSOC(configuration, ruleset.ruletype, ruleset.name) - for(var/variable in rule_conf) - if(!(variable in ruleset.vars)) - stack_trace("Invalid dynamic configuration variable [variable] in [ruleset.ruletype] [ruleset.name].") - continue - ruleset.vars[variable] = rule_conf[variable] - ruleset.restricted_roles |= SSstation.antag_restricted_roles - if(length(ruleset.protected_roles)) //if we care to protect any role, we should protect station trait roles too - ruleset.protected_roles |= SSstation.antag_protected_roles - if(CONFIG_GET(flag/protect_roles_from_antagonist)) - ruleset.restricted_roles |= ruleset.protected_roles - if(CONFIG_GET(flag/protect_assistant_from_antagonist)) - ruleset.restricted_roles |= JOB_ASSISTANT - // SKYRAT EDIT ADDITION - for(var/datum/job/iterating_job as anything in subtypesof(/datum/job)) - if(!initial(iterating_job.antagonist_restricted)) - continue - if(initial(iterating_job.restricted_antagonists)) - var/list/restricted_antagonists = initial(iterating_job.restricted_antagonists) - if(!(ruleset.antag_flag in restricted_antagonists)) - continue - ruleset.restricted_roles |= initial(iterating_job.title) - else - ruleset.restricted_roles |= initial(iterating_job.title) - // SKYRAT EDIT END - if(!(ruleset.ruleset_category & GLOB.dynamic_ruleset_categories)) - ruleset.requirements = list(101,101,101,101,101,101,101,101,101,101) - -/// Get station traits and call for their config -/datum/controller/subsystem/dynamic/proc/configure_station_trait_costs() - if(!CONFIG_GET(flag/dynamic_config_enabled)) - return - for(var/datum/station_trait/station_trait as anything in GLOB.dynamic_station_traits) - configure_station_trait(station_trait) - -/// Apply configuration for station trait costs -/datum/controller/subsystem/dynamic/proc/configure_station_trait(datum/station_trait/station_trait) - var/list/station_trait_config = LAZYACCESSASSOC(configuration, "Station", station_trait.dynamic_threat_id) - var/cost = station_trait_config["cost"] - - if(isnull(cost)) //0 is valid so check for null specifically - return - - if(cost != GLOB.dynamic_station_traits[station_trait]) - log_dynamic("Config set [station_trait.dynamic_threat_id] cost from [station_trait.threat_reduction] to [cost]") - - GLOB.dynamic_station_traits[station_trait] = cost - -/// Refund threat, but no more than threat_level. -/datum/controller/subsystem/dynamic/proc/refund_threat(regain) - mid_round_budget = min(threat_level, mid_round_budget + regain) - -/// Generate threat and increase the threat_level if it goes beyond, capped at 100 -/datum/controller/subsystem/dynamic/proc/create_threat(gain, list/threat_log, reason) - mid_round_budget = min(100, mid_round_budget + gain) - if(mid_round_budget > threat_level) - threat_level = mid_round_budget - for(var/list/logs in threat_log) - log_threat(gain, logs, reason) - -/datum/controller/subsystem/dynamic/proc/log_threat(threat_change, list/threat_log, reason) - var/gain_or_loss = "+" - if(threat_change < 0) - gain_or_loss = "-" - threat_log += "Threat [gain_or_loss][abs(threat_change)] - [reason]." - -/// Expend round start threat, can't fall under 0. -/datum/controller/subsystem/dynamic/proc/spend_roundstart_budget(cost, list/threat_log, reason) - round_start_budget = max(round_start_budget - cost,0) - if (!isnull(threat_log)) - log_threat(-cost, threat_log, reason) - -/// Expend midround threat, can't fall under 0. -/datum/controller/subsystem/dynamic/proc/spend_midround_budget(cost, list/threat_log, reason) - mid_round_budget = max(mid_round_budget - cost,0) - if (!isnull(threat_log)) - log_threat(-cost, threat_log, reason) - -#define MAXIMUM_DYN_DISTANCE 5 - -/** - * Returns the comulative distribution of threat centre and width, and a random location of -5 to 5 - * plus or minus the otherwise unattainable lower and upper percentiles. All multiplied by the maximum - * threat and then rounded to the nearest interval. - * rand() calls without arguments returns a value between 0 and 1, allowing for smaller intervals. - */ -/datum/controller/subsystem/dynamic/proc/lorentz_to_amount(centre = 0, scale = 1.8, max_threat = 100, interval = 1) - var/location = RANDOM_DECIMAL(-MAXIMUM_DYN_DISTANCE, MAXIMUM_DYN_DISTANCE) * rand() - var/lorentz_result = LORENTZ_CUMULATIVE_DISTRIBUTION(centre, location, scale) - var/std_threat = lorentz_result * max_threat - ///Without these, the amount won't come close to hitting 0% or 100% of the max threat. - var/lower_deviation = max(std_threat * (location-centre)/MAXIMUM_DYN_DISTANCE, 0) - var/upper_deviation = max((max_threat - std_threat) * (centre-location)/MAXIMUM_DYN_DISTANCE, 0) - return clamp(round(std_threat + upper_deviation - lower_deviation, interval), 0, 100) - -/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 = {" - [command_name()] Human Resources Board
- Notice of Personnel Change

- To personnel management staff aboard [station_name()]:

- Our medical staff have detected a series of anomalies in the vital sensors - of some of the staff aboard your station.

- 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.

- 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.)

- The following positions have been reopened on our behalf:

- [reopened_job_report_positions]
- "} - - print_command_report(suicide_command_report, "Central Command Personnel Update") - - -#undef MAXIMUM_DYN_DISTANCE +#endif diff --git a/code/controllers/subsystem/dynamic/dynamic_admin.dm b/code/controllers/subsystem/dynamic/dynamic_admin.dm new file mode 100644 index 00000000000..7c8cb3ecabd --- /dev/null +++ b/code/controllers/subsystem/dynamic/dynamic_admin.dm @@ -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 diff --git a/code/controllers/subsystem/dynamic/dynamic_hijacking.dm b/code/controllers/subsystem/dynamic/dynamic_hijacking.dm deleted file mode 100644 index 7577cbcd84f..00000000000 --- a/code/controllers/subsystem/dynamic/dynamic_hijacking.dm +++ /dev/null @@ -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 diff --git a/code/controllers/subsystem/dynamic/dynamic_logging.dm b/code/controllers/subsystem/dynamic/dynamic_logging.dm deleted file mode 100644 index 1ff3b4d3119..00000000000 --- a/code/controllers/subsystem/dynamic/dynamic_logging.dm +++ /dev/null @@ -1,101 +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") - rustg_file_write(json_encode(serialized), "[GLOB.public_log_directory]/dynamic.json") // BUBBER EDIT - -/// 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() diff --git a/code/controllers/subsystem/dynamic/dynamic_midround_rolling.dm b/code/controllers/subsystem/dynamic/dynamic_midround_rolling.dm deleted file mode 100644 index c44bf3d591c..00000000000 --- a/code/controllers/subsystem/dynamic/dynamic_midround_rolling.dm +++ /dev/null @@ -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)) diff --git a/code/controllers/subsystem/dynamic/dynamic_ruleset_latejoin.dm b/code/controllers/subsystem/dynamic/dynamic_ruleset_latejoin.dm new file mode 100644 index 00000000000..d780e2fa344 --- /dev/null +++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_latejoin.dm @@ -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() diff --git a/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm b/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm new file mode 100644 index 00000000000..40e46d4cf0e --- /dev/null +++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm @@ -0,0 +1,1150 @@ +/datum/dynamic_ruleset/midround + repeatable = TRUE + repeatable_weight_decrease = 2 + /// LIGHT_MIDROUND or HEAVY_MIDROUND - determines which pool it enters + var/midround_type + /// If the false alarm event can pick this ruleset to trigger, well, a false alarm + var/false_alarm_able = FALSE + +/** + * Collect candidates handles getting the broad pool of players we want to pick from + * + * You can sleep in this - say, if you wanted to poll players. + */ +/datum/dynamic_ruleset/midround/proc/collect_candidates() + return list() + +/** + * Called when the ruleset is selected for false alarm + */ +/datum/dynamic_ruleset/midround/proc/false_alarm() + return + +/datum/dynamic_ruleset/midround/spiders + name = "Spiders" + config_tag = "Spiders" + midround_type = HEAVY_MIDROUND + false_alarm_able = TRUE + ruleset_flags = RULESET_INVADER + weight = list( + DYNAMIC_TIER_LOW = 0, + DYNAMIC_TIER_LOWMEDIUM = 0, + DYNAMIC_TIER_MEDIUMHIGH = 1, + DYNAMIC_TIER_HIGH = 2, + ) + min_pop = 30 + min_antag_cap = 0 + /// Determines how many eggs to create - can take a formula like antag_cap + var/egg_count = 2 + +/datum/dynamic_ruleset/midround/spiders/can_be_selected() + return ..() && (GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT) && !isnull(find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE)) + +/datum/dynamic_ruleset/midround/spiders/execute() + var/num_egg = get_antag_cap(length(GLOB.alive_player_list), egg_count) + while(num_egg > 0) + var/turf/spawn_loc = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE) + if(isnull(spawn_loc)) + break + var/obj/effect/mob_spawn/ghost_role/spider/midwife/new_eggs = new(spawn_loc) + new_eggs.amount_grown = 98 + num_egg-- + + addtimer(CALLBACK(src, PROC_REF(announce_spiders)), rand(375, 600) SECONDS) + +/datum/dynamic_ruleset/midround/spiders/proc/announce_spiders() + priority_announce("Unidentified lifesigns detected coming aboard [station_name()]. Secure any exterior access, including ducting and ventilation.", "Lifesign Alert", ANNOUNCER_ALIENS) + +/datum/dynamic_ruleset/midround/spiders/false_alarm() + announce_spiders() + +/datum/dynamic_ruleset/midround/pirates + name = "Pirates" + config_tag = "Light Pirates" + midround_type = LIGHT_MIDROUND + jobban_flag = ROLE_TRAITOR + ruleset_flags = RULESET_INVADER|RULESET_ADMIN_CONFIGURABLE + weight = 3 + min_pop = 15 + min_antag_cap = 0 // ship will spawn if there are no ghosts around + + /// Pool to pick pirates from + var/list/datum/pirate_gang/pirate_pool + +/datum/dynamic_ruleset/midround/pirates/New(list/dynamic_config) + . = ..() + pirate_pool = default_pirate_pool() + +/datum/dynamic_ruleset/midround/pirates/can_be_selected() + return ..() && !SSmapping.is_planetary() && (GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT) && length(default_pirate_pool()) > 0 + +// An abornmal ruleset that selects no players, but just spawns a pirate ship +/datum/dynamic_ruleset/midround/pirates/execute() + send_pirate_threat(pirate_pool) + +/// Returns what pool of pirates to drawn from +/// Returned list is mutated by the ruleset +/datum/dynamic_ruleset/midround/pirates/proc/default_pirate_pool() + return GLOB.light_pirate_gangs + +/datum/dynamic_ruleset/midround/pirates/heavy + name = "Pirates" + config_tag = "Heavy Pirates" + midround_type = HEAVY_MIDROUND + jobban_flag = ROLE_TRAITOR + ruleset_flags = RULESET_INVADER + weight = 3 + min_pop = 25 + min_antag_cap = 0 // ship will spawn if there are no ghosts around + +/datum/dynamic_ruleset/midround/pirates/heavy/default_pirate_pool() + return GLOB.heavy_pirate_gangs + +#define RANDOM_PIRATE_POOL "Random" + +/datum/dynamic_ruleset/midround/pirates/configure_ruleset(mob/admin) + var/list/admin_pool = list("[RULESET_CONFIG_CANCEL]" = TRUE, "[RANDOM_PIRATE_POOL]" = TRUE) + for(var/datum/pirate_gang/gang as anything in default_pirate_pool()) + admin_pool[gang.name] = gang + var/picked = tgui_input_list(admin, "Select a pirate gang", "Pirate Gang Selection", admin_pool) + if(!picked || picked == RULESET_CONFIG_CANCEL) + return RULESET_CONFIG_CANCEL + if(picked == RANDOM_PIRATE_POOL) + return null + + pirate_pool = list(admin_pool[picked]) + return null + +#undef RANDOM_PIRATE_POOL + +#define NO_ANSWER 0 +#define POSITIVE_ANSWER 1 +#define NEGATIVE_ANSWER 2 + +/datum/dynamic_ruleset/midround/pirates/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(src, PROC_REF(pirates_answered), threat, chosen_gang, payoff, world.time) + addtimer(CALLBACK(src, PROC_REF(spawn_pirates), threat, chosen_gang), RESPONSE_MAX_TIME) + GLOB.communications_controller.send_message(threat, unique = TRUE) + +/datum/dynamic_ruleset/midround/pirates/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) + +/datum/dynamic_ruleset/midround/pirates/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) + +#undef NO_ANSWER +#undef POSITIVE_ANSWER +#undef NEGATIVE_ANSWER +/** + * ### Ghost rulesets + * + * Rulesets which select an observer/ghost player to play as a new character + * + * Implementation notes: + * - prepare_role will handle making the body for the mob for you. Avoid touching it if not necessary. + * - create_ruleset_body is what makes the new /mob for the candidate. It handles putting the player in the body for you. + * You can override it entirely for to spawn a different mob type. + * You can also override it to spawn nothing, if you're doing special handling in assign_role, but you'll have to handle moving the player yourself. + * - assign_role is what gives the player their antag datum. + */ +/datum/dynamic_ruleset/midround/from_ghosts + ///Path of an item to show up in ghost polls for applicants to sign up. + var/signup_atom_appearance = /obj/structure/sign/poster/contraband/syndicate_recruitment + /// Text shown in the candidate poll. Optional, if unset uses pref_flag. (Though required if pref_flag is unset) + var/candidate_role + +/datum/dynamic_ruleset/midround/from_ghosts/can_be_selected() + SHOULD_CALL_PARENT(TRUE) + return ..() && (GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT) + +/datum/dynamic_ruleset/midround/from_ghosts/get_candidate_mind(mob/dead/candidate) + // Ghost roles will always get a fresh mind + return new /datum/mind(candidate.key) + +/datum/dynamic_ruleset/midround/from_ghosts/prepare_for_role(datum/mind/candidate) + var/mob/living/body = create_ruleset_body() + if(isnull(body)) + return + candidate.transfer_to(body, force_key_move = TRUE) // yoinks the candidate's client + if(ishuman(body)) + var/mob/living/carbon/human/human_body = body + body.client?.prefs.safe_transfer_prefs_to(body) + human_body.dna.remove_all_mutations() + human_body.dna.update_dna_identity() + +/** + * Handles making the body for the candidate + * + * Handling loc is not necessary here - you can do it in assign_role + * + * Returning null will skip body creation entirely, though you will be expected to do it yourself in assign_role + */ +/datum/dynamic_ruleset/midround/from_ghosts/proc/create_ruleset_body() + return new /mob/living/carbon/human + +/datum/dynamic_ruleset/midround/from_ghosts/collect_candidates() + var/readable_poll_role = candidate_role || pref_flag + if(isnull(readable_poll_role)) + stack_trace("[config_tag]: No candidate role or pref_flag set, give it a human readable candidate roll at the bare minimum.") + readable_poll_role = "Some Midround Antagonist Without A Role Set (Yell At Coders)" + + return SSpolling.poll_candidates( + group = trim_candidates(GLOB.dead_player_list | GLOB.current_observers_list), + question = "Looking for volunteers to become [span_notice(readable_poll_role)] for [span_danger(name)]", + // check_jobban = list(ROLE_SYNDICATE, jobban_flag || pref_flag), // Not necessary, handled in trim_candidates() + // role = pref_flag, // Not necessary, handled in trim_candidates() + poll_time = 30 SECONDS, + alert_pic = signup_atom_appearance, + role_name_text = readable_poll_role, + ) + +/datum/dynamic_ruleset/midround/from_ghosts/wizard + name = "Wizard" + config_tag = "Midround Wizard" + preview_antag_datum = /datum/antagonist/wizard + midround_type = HEAVY_MIDROUND + candidate_role = "Wizard" + pref_flag = ROLE_WIZARD_MIDROUND + jobban_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, + ) + min_pop = 30 + max_antag_cap = 1 + ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_WIZARDDEN) + signup_atom_appearance = /obj/item/clothing/head/wizard + +/datum/dynamic_ruleset/midround/from_ghosts/wizard/assign_role(datum/mind/candidate) + candidate.add_antag_datum(/datum/antagonist/wizard) // moves to lair for us + +/datum/dynamic_ruleset/midround/from_ghosts/nukies + name = "Nuclear Operatives" + config_tag = "Midround Nukeops" + preview_antag_datum = /datum/antagonist/nukeop + midround_type = HEAVY_MIDROUND + candidate_role = "Operative" + pref_flag = ROLE_OPERATIVE_MIDROUND + jobban_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) + repeatable = FALSE + ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE) + signup_atom_appearance = /obj/machinery/nuclearbomb/syndicate + +/datum/dynamic_ruleset/midround/from_ghosts/nukies/create_execute_args() + return list( + new /datum/team/nuclear(), + get_most_experienced(selected_minds, pref_flag), + ) + +/datum/dynamic_ruleset/midround/from_ghosts/nukies/assign_role(datum/mind/candidate, datum/team/nuclear/nuke_team, datum/mind/most_experienced) + if(most_experienced == candidate) + candidate.add_antag_datum(/datum/antagonist/nukeop/leader, nuke_team) // moves to nuke base for us + else + candidate.add_antag_datum(/datum/antagonist/nukeop, nuke_team) // moves to nuke base for us + +/datum/dynamic_ruleset/midround/from_ghosts/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/midround/from_ghosts/nukies/clown + name = "Clown Operatives" + config_tag = "Midround Clownops" + preview_antag_datum = /datum/antagonist/nukeop/clownop + candidate_role = "Operative" + pref_flag = ROLE_CLOWN_OPERATIVE_MIDROUND + jobban_flag = ROLE_CLOWN_OPERATIVE + weight = 0 + signup_atom_appearance = /obj/machinery/nuclearbomb/syndicate/bananium + +/datum/dynamic_ruleset/midround/from_ghosts/nukies/clown/assign_role(datum/mind/candidate, datum/team/nuclear/nuke_team, datum/mind/most_experienced) + if(most_experienced == candidate) + candidate.add_antag_datum(/datum/antagonist/nukeop/leader/clownop, nuke_team) // moves to nuke base for us + else + candidate.add_antag_datum(/datum/antagonist/nukeop/clownop, nuke_team) // moves to nuke base for us + +/datum/dynamic_ruleset/midround/from_ghosts/blob + name = "Blob" + config_tag = "Blob" + preview_antag_datum = /datum/antagonist/blob + midround_type = HEAVY_MIDROUND + false_alarm_able = TRUE + pref_flag = ROLE_BLOB + ruleset_flags = RULESET_INVADER + 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_weight_decrease = 3 + signup_atom_appearance = /obj/structure/blob/normal + /// How many points does the blob spawn with + var/starting_points = OVERMIND_STARTING_POINTS + +/datum/dynamic_ruleset/midround/from_ghosts/blob/create_ruleset_body() + return new /mob/eye/blob(get_blobspawn(), starting_points) + +/datum/dynamic_ruleset/midround/from_ghosts/blob/assign_role(datum/mind/candidate) + return // everything is handled by blob new() + +/datum/dynamic_ruleset/midround/from_ghosts/blob/proc/get_blobspawn() + if(!length(GLOB.blobstart)) + var/obj/effect/landmark/observer_start/default = locate() in GLOB.landmarks_list + return get_turf(default) + + return pick(GLOB.blobstart) + +/datum/dynamic_ruleset/midround/from_ghosts/blob/false_alarm() + priority_announce("Confirmed outbreak of level 5 biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", ANNOUNCER_OUTBREAK5) + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph + name = "Alien Infestation" + config_tag = "Xenomorph" + preview_antag_datum = /datum/antagonist/xeno + midround_type = HEAVY_MIDROUND + false_alarm_able = TRUE + pref_flag = ROLE_ALIEN + ruleset_flags = RULESET_INVADER + weight = list( + DYNAMIC_TIER_LOW = 0, + DYNAMIC_TIER_LOWMEDIUM = 1, + DYNAMIC_TIER_MEDIUMHIGH = 5, + DYNAMIC_TIER_HIGH = 5, + ) + min_pop = 30 + max_antag_cap = 1 + min_antag_cap = 1 + repeatable_weight_decrease = 3 + signup_atom_appearance = /mob/living/basic/alien + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/New(list/dynamic_config) + . = ..() + max_antag_cap += prob(50) // 50% chance to get a second xeno, free! + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/can_be_selected() + return ..() && length(find_vents()) > 0 + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/execute() + . = ..() + addtimer(CALLBACK(src, PROC_REF(announce_xenos)), rand(375, 600) SECONDS) + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/proc/announce_xenos() + priority_announce("Unidentified lifesigns detected coming aboard [station_name()]. Secure any exterior access, including ducting and ventilation.", "Lifesign Alert", ANNOUNCER_ALIENS) + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/false_alarm() + announce_xenos() + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/create_ruleset_body() + return new /mob/living/carbon/alien/larva + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/create_execute_args() + return list(find_vents()) + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/assign_role(datum/mind/candidate, list/vent_list) + // xeno login gives antag datums + var/obj/vent = length(vent_list) >= 2 ? pick_n_take(vent_list) : vent_list[1] + candidate.current.move_into_vent(vent) + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/proc/find_vents() + var/list/vents = list() + var/list/vent_pumps = SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/atmospherics/components/unary/vent_pump) + for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent as anything in vent_pumps) + if(QDELETED(temp_vent)) + continue + if(!is_station_level(temp_vent.loc.z) || temp_vent.welded) + continue + var/datum/pipeline/temp_vent_parent = temp_vent.parents[1] + if(!temp_vent_parent) + continue + // Stops Aliens getting stuck in small networks. + // See: Security, Virology + if(length(temp_vent_parent.other_atmos_machines) <= 20) + continue + vents += temp_vent + return vents + +/datum/dynamic_ruleset/midround/from_ghosts/nightmare + name = "Nightmare" + config_tag = "Nightmare" + preview_antag_datum = /datum/antagonist/nightmare + midround_type = LIGHT_MIDROUND + pref_flag = ROLE_NIGHTMARE + ruleset_flags = RULESET_INVADER + weight = 5 + min_pop = 15 + max_antag_cap = 1 + signup_atom_appearance = /obj/item/light_eater + +/datum/dynamic_ruleset/midround/from_ghosts/nightmare/can_be_selected() + return ..() && !isnull(find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE)) + +/datum/dynamic_ruleset/midround/from_ghosts/nightmare/assign_role(datum/mind/candidate) + candidate.add_antag_datum(/datum/antagonist/nightmare) + candidate.current.set_species(/datum/species/shadow/nightmare) + candidate.current.forceMove(find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE)) + playsound(candidate.current, 'sound/effects/magic/ethereal_exit.ogg', 50, TRUE, -1) + +/datum/dynamic_ruleset/midround/from_ghosts/space_dragon + name = "Space Dragon" + config_tag = "Space Dragon" + preview_antag_datum = /datum/antagonist/space_dragon + midround_type = HEAVY_MIDROUND + false_alarm_able = TRUE + pref_flag = ROLE_SPACE_DRAGON + ruleset_flags = RULESET_INVADER + weight = list( + DYNAMIC_TIER_LOW = 0, + DYNAMIC_TIER_LOWMEDIUM = 3, + DYNAMIC_TIER_MEDIUMHIGH = 5, + DYNAMIC_TIER_HIGH = 5, + ) + min_pop = 30 + max_antag_cap = 1 + repeatable_weight_decrease = 3 + signup_atom_appearance = /mob/living/basic/space_dragon + +/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/can_be_selected() + return ..() && !isnull(find_space_spawn()) + +/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/create_ruleset_body() + return new /mob/living/basic/space_dragon + +/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/assign_role(datum/mind/candidate) + candidate.add_antag_datum(/datum/antagonist/space_dragon) + candidate.current.forceMove(find_space_spawn()) + playsound(candidate.current, 'sound/effects/magic/ethereal_exit.ogg', 50, TRUE, -1) + +/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/execute() + . = ..() + addtimer(CALLBACK(src, PROC_REF(announce_space_dragon)), rand(5, 10) SECONDS) + +/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/proc/announce_space_dragon() + priority_announce("A large organic energy flux has been recorded near of [station_name()], please stand-by.", "Lifesign Alert") + +/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/false_alarm() + announce_space_dragon() + +/datum/dynamic_ruleset/midround/from_ghosts/abductors + name = "Abductors" + config_tag = "Abductors" + preview_antag_datum = /datum/antagonist/abductor + midround_type = LIGHT_MIDROUND + pref_flag = ROLE_ABDUCTOR + ruleset_flags = RULESET_INVADER + weight = 5 + min_pop = 20 + min_antag_cap = 2 + repeatable_weight_decrease = 3 + ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_ABDUCTOR_SHIPS) + signup_atom_appearance = /obj/item/melee/baton/abductor + +/datum/dynamic_ruleset/midround/from_ghosts/abductors/can_be_selected() + if(!..()) + return FALSE + var/num_abductors = 0 + for(var/datum/team/abductor_team/team in GLOB.antagonist_teams) + num_abductors++ + return num_abductors < 4 + +/datum/dynamic_ruleset/midround/from_ghosts/abductors/create_execute_args() + return list(new /datum/team/abductor_team()) + +/datum/dynamic_ruleset/midround/from_ghosts/abductors/assign_role(datum/mind/candidate, datum/team/abductor_team/team) + if(candidate == selected_minds[1]) + candidate.add_antag_datum(/datum/antagonist/abductor/scientist, team) // sets species and moves to spawn point + else + candidate.add_antag_datum(/datum/antagonist/abductor/agent, team) // sets species and moves to spawn point + +/datum/dynamic_ruleset/midround/from_ghosts/space_ninja + name = "Space Ninja" + config_tag = "Space Ninja" + preview_antag_datum = /datum/antagonist/ninja + midround_type = HEAVY_MIDROUND + pref_flag = ROLE_NINJA + ruleset_flags = RULESET_INVADER + weight = list( + DYNAMIC_TIER_LOW = 0, + DYNAMIC_TIER_LOWMEDIUM = 0, + DYNAMIC_TIER_MEDIUMHIGH = 1, + DYNAMIC_TIER_HIGH = 2, + ) + min_pop = 30 + max_antag_cap = 1 + repeatable = FALSE + ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY) + signup_atom_appearance = /obj/item/energy_katana + +/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/can_be_selected() + return ..() && !isnull(find_space_spawn()) + +/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/assign_role(datum/mind/candidate) + var/mob/living/carbon/human/new_ninja = candidate.current + new_ninja.forceMove(find_space_spawn()) // ninja antag datum needs the mob to be in place first + randomize_human_normie(new_ninja) + var/new_name = "[pick(GLOB.ninja_titles)] [pick(GLOB.ninja_names)]" + new_ninja.name = new_name + new_ninja.real_name = new_name + new_ninja.dna.update_dna_identity() // ninja antag datum needs dna to be set first + candidate.add_antag_datum(/datum/antagonist/ninja) + +/datum/dynamic_ruleset/midround/from_ghosts/revenant + name = "Revenant" + config_tag = "Revenant" + preview_antag_datum = /datum/antagonist/revenant + midround_type = LIGHT_MIDROUND + pref_flag = ROLE_REVENANT + ruleset_flags = RULESET_INVADER + weight = 5 + min_pop = 10 + max_antag_cap = 1 + repeatable = FALSE + signup_atom_appearance = /mob/living/basic/revenant + /// There must be this many dead mobs on the station for a revenant to spawn (of all mob types, not just humans) + /// Remember there's usually 2-3 that spawn in the Morgue roundstart, so adjust this accordingly + var/required_station_corpses = 10 + +/datum/dynamic_ruleset/midround/from_ghosts/revenant/can_be_selected() + if(!..()) + return FALSE + var/num_station_corpses = 0 + for(var/mob/deceased as anything in GLOB.dead_mob_list) + var/turf/deceased_turf = get_turf(deceased) + if(is_station_level(deceased_turf?.z)) + num_station_corpses++ + + return num_station_corpses > required_station_corpses + +/datum/dynamic_ruleset/midround/from_ghosts/revenant/create_ruleset_body() + return new /mob/living/basic/revenant(pick(get_revenant_spawns())) + +/datum/dynamic_ruleset/midround/from_ghosts/revenant/assign_role(datum/mind/candidate) + return // revenant new() handles everything + +/datum/dynamic_ruleset/midround/from_ghosts/revenant/proc/get_revenant_spawns() + var/list/spawn_locs = list() + for(var/mob/deceased in GLOB.dead_mob_list) + var/turf/deceased_turf = get_turf(deceased) + if(is_station_level(deceased_turf?.z)) + spawn_locs += deceased_turf + if(!length(spawn_locs) || length(spawn_locs) < 12) // get a comfortably large pool of spawnpoints + for(var/obj/structure/bodycontainer/corpse_container in GLOB.bodycontainers) + var/turf/container_turf = get_turf(corpse_container) + if(is_station_level(container_turf?.z)) + spawn_locs += container_turf + if(!length(spawn_locs) || length(spawn_locs) < 4) // get a comfortably large pool of spawnpoints + for(var/obj/effect/landmark/carpspawn/carpspawn in GLOB.landmarks_list) + spawn_locs += carpspawn.loc + + return spawn_locs + +/datum/dynamic_ruleset/midround/from_ghosts/space_changeling + name = "Space Changeling" + config_tag = "Midround Changeling" + preview_antag_datum = /datum/antagonist/changeling/space + midround_type = LIGHT_MIDROUND + candidate_role = "Changeling" + pref_flag = ROLE_CHANGELING_MIDROUND + jobban_flag = ROLE_CHANGELING + ruleset_flags = RULESET_INVADER + weight = 5 + min_pop = 15 + max_antag_cap = 1 + signup_atom_appearance = /obj/effect/meteor/meaty/changeling + +/datum/dynamic_ruleset/midround/from_ghosts/space_changeling/create_ruleset_body() + return // handled by generate_changeling_meteor() entirely + +/datum/dynamic_ruleset/midround/from_ghosts/space_changeling/assign_role(datum/mind/candidate) + generate_changeling_meteor(candidate) + +/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone + name = "Paradox Clone" + config_tag = "Paradox Clone" + preview_antag_datum = /datum/antagonist/paradox_clone + midround_type = LIGHT_MIDROUND + pref_flag = ROLE_PARADOX_CLONE + ruleset_flags = RULESET_INVADER + weight = 5 + min_pop = 10 + max_antag_cap = 1 + signup_atom_appearance = /obj/effect/bluespace_stream + +/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/can_be_selected() + return ..() && !isnull(find_clone()) && !isnull(find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE)) + +/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/create_ruleset_body() + return // handled by assign_role() entirely + +/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/assign_role(datum/mind/candidate) + var/mob/living/carbon/human/good_version = find_clone() + var/mob/living/carbon/human/bad_version = good_version.make_full_human_copy(find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE)) + candidate.transfer_to(bad_version, force_key_move = TRUE) + + var/datum/antagonist/paradox_clone/antag = candidate.add_antag_datum(/datum/antagonist/paradox_clone) + antag.original_ref = WEAKREF(good_version.mind) + antag.setup_clone() + + playsound(bad_version, 'sound/items/weapons/zapbang.ogg', 30, TRUE) + bad_version.put_in_hands(new /obj/item/storage/toolbox/mechanical()) //so they dont get stuck in maints + +/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/proc/find_clone() + var/list/possible_targets = list() + + for(var/mob/living/carbon/human/player in GLOB.player_list) + if(!player.client || !player.mind || player.stat != CONSCIOUS) + continue + if(!(player.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) + continue + possible_targets += player + + if(length(possible_targets)) + return pick(possible_targets) + return null + +/datum/dynamic_ruleset/midround/from_ghosts/voidwalker + name = "Voidwalker" + config_tag = "Voidwalker" + preview_antag_datum = /datum/antagonist/voidwalker + midround_type = LIGHT_MIDROUND + pref_flag = ROLE_VOIDWALKER + ruleset_flags = RULESET_INVADER + weight = 5 + min_pop = 30 // Ensures there's a lot of people near windows + max_antag_cap = 1 + ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_VOIDWALKER_VOID) + signup_atom_appearance = /obj/item/clothing/head/helmet/skull/cosmic + +/datum/dynamic_ruleset/midround/from_ghosts/voidwalker/can_be_selected() + return ..() && !SSmapping.is_planetary() && !isnull(find_space_spawn()) + +/datum/dynamic_ruleset/midround/from_ghosts/voidwalker/assign_role(datum/mind/candidate) + candidate.add_antag_datum(/datum/antagonist/voidwalker) + candidate.current.set_species(/datum/species/voidwalker) + candidate.current.forceMove(find_space_spawn()) + playsound(candidate.current, 'sound/effects/magic/ethereal_exit.ogg', 50, TRUE, -1) + +/datum/dynamic_ruleset/midround/from_ghosts/fugitives + name = "Fugitive" + config_tag = "Fugitives" + preview_antag_datum = /datum/antagonist/fugitive + midround_type = LIGHT_MIDROUND + pref_flag = ROLE_FUGITIVE + ruleset_flags = RULESET_INVADER|RULESET_ADMIN_CONFIGURABLE + weight = 3 + min_pop = 20 + max_antag_cap = 4 + min_antag_cap = 3 + repeatable = FALSE + signup_atom_appearance = /obj/item/card/id/advanced/prisoner + /// What backstory is the fugitive(s)? + VAR_FINAL/fugitive_backstory + /// What backstory is the hunter(s)? + VAR_FINAL/hunter_backstory + +/datum/dynamic_ruleset/midround/from_ghosts/fugitives/can_be_selected() + return ..() && !SSmapping.is_planetary() && !isnull(find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE)) + +// If less than a certain number of candidates accept the poll, it varies how many antags are spawned +/datum/dynamic_ruleset/midround/from_ghosts/fugitives/collect_candidates() + . = ..() + if(length(.) <= 1 || prob(30 - (length(.) * 2))) + min_antag_cap = 1 + max_antag_cap = 1 + +/datum/dynamic_ruleset/midround/from_ghosts/fugitives/create_execute_args() + return list( + new /datum/team/fugitive(), + find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE), + ) + +#define RANDOM_BACKSTORY "Random" + +/datum/dynamic_ruleset/midround/from_ghosts/fugitives/configure_ruleset(mob/admin) + var/list/fugitive_backstories = list( + FUGITIVE_BACKSTORY_CULTIST, + FUGITIVE_BACKSTORY_INVISIBLE, + FUGITIVE_BACKSTORY_PRISONER, + FUGITIVE_BACKSTORY_SYNTH, + FUGITIVE_BACKSTORY_WALDO, + RANDOM_BACKSTORY, + RULESET_CONFIG_CANCEL, + ) + var/list/hunter_backstories = list( + HUNTER_PACK_BOUNTY, + HUNTER_PACK_COPS, + HUNTER_PACK_MI13, + HUNTER_PACK_PSYKER, + HUNTER_PACK_RUSSIAN, + RANDOM_BACKSTORY, + RULESET_CONFIG_CANCEL, + ) + + var/picked_fugitive_backstory = tgui_input_list(admin, "Select a fugitive backstory", "Fugitive Backstory", fugitive_backstories) + if(!picked_fugitive_backstory || picked_fugitive_backstory == RULESET_CONFIG_CANCEL) + return RULESET_CONFIG_CANCEL + if(picked_fugitive_backstory != RANDOM_BACKSTORY) + fugitive_backstory = picked_fugitive_backstory + + var/picked_hunter_backstory = tgui_input_list(admin, "Select a hunter backstory", "Hunter Backstory", hunter_backstories) + if(!picked_hunter_backstory || picked_hunter_backstory == RULESET_CONFIG_CANCEL) + return RULESET_CONFIG_CANCEL + if(picked_hunter_backstory != RANDOM_BACKSTORY) + hunter_backstory = picked_hunter_backstory + + return null + +#undef RANDOM_BACKSTORY + +/datum/dynamic_ruleset/midround/from_ghosts/fugitives/execute() + if(length(selected_minds) == 1) + fugitive_backstory ||= pick( + FUGITIVE_BACKSTORY_INVISIBLE, + FUGITIVE_BACKSTORY_WALDO, + ) + else + fugitive_backstory ||= pick( + FUGITIVE_BACKSTORY_CULTIST, + FUGITIVE_BACKSTORY_PRISONER, + FUGITIVE_BACKSTORY_SYNTH, + ) + + hunter_backstory ||= pick( + HUNTER_PACK_COPS, + HUNTER_PACK_RUSSIAN, + HUNTER_PACK_BOUNTY, + HUNTER_PACK_PSYKER, + HUNTER_PACK_MI13, + ) + . = ..() + addtimer(CALLBACK(src, PROC_REF(check_spawn_hunters), hunter_backstory, 10 MINUTES), 1 MINUTES) + +/datum/dynamic_ruleset/midround/from_ghosts/fugitives/assign_role(datum/mind/candidate, datum/team/fugitive/team, turf/team_spawn) + candidate.current.forceMove(team_spawn) + equip_fugitive(candidate.current, team) + if(length(selected_minds) > 1 && candidate == selected_minds[1]) + equip_fugitive_leader(candidate.current) + playsound(candidate.current, 'sound/items/weapons/emitter.ogg', 50, TRUE) + +/datum/dynamic_ruleset/midround/from_ghosts/fugitives/proc/equip_fugitive(mob/living/carbon/human/fugitive, datum/team/fugitive/team) + fugitive.set_species(/datum/species/human) + randomize_human_normie(fugitive) + + var/datum/antagonist/fugitive/antag = new() + antag.backstory = fugitive_backstory + fugitive.mind.add_antag_datum(antag, team) + // Should really datumize this at some point + switch(fugitive_backstory) + if(FUGITIVE_BACKSTORY_PRISONER) + fugitive.equipOutfit(/datum/outfit/prisoner) + if(FUGITIVE_BACKSTORY_CULTIST) + fugitive.equipOutfit(/datum/outfit/yalp_cultist) + if(FUGITIVE_BACKSTORY_WALDO) + fugitive.equipOutfit(/datum/outfit/waldo) + if(FUGITIVE_BACKSTORY_SYNTH) + fugitive.equipOutfit(/datum/outfit/synthetic) + if(FUGITIVE_BACKSTORY_INVISIBLE) + fugitive.equipOutfit(/datum/outfit/invisible_man) + +/datum/dynamic_ruleset/midround/from_ghosts/fugitives/proc/equip_fugitive_leader(mob/living/carbon/human/fugitive) + var/turf/leader_turf = get_turf(fugitive) + var/obj/item/storage/toolbox/mechanical/toolbox = new(leader_turf) + fugitive.put_in_hands(toolbox) + + switch(fugitive_backstory) + if(FUGITIVE_BACKSTORY_SYNTH) + new /obj/item/choice_beacon/augments(leader_turf) + new /obj/item/autosurgeon(leader_turf) + +/datum/dynamic_ruleset/midround/from_ghosts/fugitives/proc/check_spawn_hunters(remaining_time) + //if the emergency shuttle has been called, spawn hunters now to give them a chance + if(remaining_time == 0 || !EMERGENCY_IDLE_OR_RECALLED) + spawn_hunters() + return + addtimer(CALLBACK(src, PROC_REF(check_spawn_hunters), remaining_time - 1 MINUTES), 1 MINUTES) + +/datum/dynamic_ruleset/midround/from_ghosts/fugitives/proc/spawn_hunters() + var/list/candidates = SSpolling.poll_ghost_candidates("Do you wish to be considered for a group of [span_notice(hunter_backstory)]?", check_jobban = list(ROLE_FUGITIVE_HUNTER, ROLE_SYNDICATE), alert_pic = /obj/machinery/sleeper, role_name_text = hunter_backstory) + shuffle_inplace(candidates) + + var/datum/map_template/shuttle/hunter/ship + switch(hunter_backstory) + if(HUNTER_PACK_COPS) + ship = new /datum/map_template/shuttle/hunter/space_cop + if(HUNTER_PACK_RUSSIAN) + ship = new /datum/map_template/shuttle/hunter/russian + if(HUNTER_PACK_BOUNTY) + ship = new /datum/map_template/shuttle/hunter/bounty + if(HUNTER_PACK_PSYKER) + ship = new /datum/map_template/shuttle/hunter/psyker + if(HUNTER_PACK_MI13) + ship = new/datum/map_template/shuttle/hunter/mi13_foodtruck + + 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/placement_turf = locate(x, y ,z) + if(!placement_turf) + CRASH("Fugitive Hunters (Created from fugitive event) found no turf to load in") + if(!ship.load(placement_turf)) + CRASH("Loading [hunter_backstory] ship failed!") + + for(var/turf/shuttle_turf in ship.get_affected_turfs(placement_turf)) + for(var/obj/effect/mob_spawn/ghost_role/human/fugitive/spawner in shuttle_turf) + if(length(candidates)) + var/mob/our_candidate = candidates[1] + var/mob/spawned_mob = spawner.create_from_ghost(our_candidate) + candidates -= our_candidate + notify_ghosts( + "[spawner.prompt_name] has awoken: [spawned_mob]!", + source = spawned_mob, + header = "Come look!", + ) + else + notify_ghosts( + "[spawner.prompt_name] spawner has been created!", + source = spawner, + header = "Spawn Here!", + ) + + var/list/announcement_text_list = list() + var/announcement_title = "" + switch(hunter_backstory) + if(HUNTER_PACK_COPS) + announcement_text_list += "Attention Crew of [station_name()], this is the Police. A wanted criminal has been reported taking refuge on your station." + announcement_text_list += "We have a warrant from the SSC authorities to take them into custody. Officers have been dispatched to your location." + announcement_text_list += "We demand your cooperation in bringing this criminal to justice." + announcement_title += "Spacepol Command" + if(HUNTER_PACK_RUSSIAN) + announcement_text_list += "Zdraviya zhelaju, [station_name()] crew. We are coming to your station." + announcement_text_list += "There is a criminal aboard. We will arrest them and return them to the gulag. That's good, yes?" + announcement_title += "Russian Freighter" + if(HUNTER_PACK_BOUNTY) + announcement_text_list += "[station_name()]. One of our bounty marks has ended up on your station. We will be arriving to collect shortly." + announcement_text_list += "Let's make this quick. If you don't want trouble, stay the hell out of our way." + announcement_title += "Unregistered Signal" + if(HUNTER_PACK_PSYKER) + announcement_text_list += "HEY, CAN YOU HEAR US? We're coming to your station. There's a bad guy down there, really bad guy. We need to arrest them." + announcement_text_list += "We're also offering fortune telling services out of the front door if you have paying customers." + announcement_title += "Fortune-Telling Entertainment Shuttle" + if(HUNTER_PACK_MI13) + announcement_text_list += "Illegal intrusion detected in the crew monitoring network. Central Command has been informed." + announcement_text_list += "Please report any suspicious individuals or behaviour to your local security team." + announcement_title += "Nanotrasen Intrusion Countermeasures Electronics" + + if(!length(announcement_text_list)) + announcement_text_list += "Unidentified ship detected near the station." + stack_trace("Fugitive hunter announcement was unable to generate an announcement text based on backstory: [hunter_backstory]") + + if(!length(announcement_title)) + announcement_title += "Unknown Signal" + stack_trace("Fugitive hunter announcement was unable to generate an announcement title based on backstory: [hunter_backstory]") + + priority_announce(jointext(announcement_text_list, " "), announcement_title) + +/datum/dynamic_ruleset/midround/from_ghosts/morph + name = "Morph" + config_tag = "Morph" + // preview_antag_datum = /datum/antagonist/morph // Doesn't actually have its own pref + midround_type = LIGHT_MIDROUND + candidate_role = "Morphling" + jobban_flag = ROLE_ALIEN + ruleset_flags = RULESET_INVADER + weight = 0 + max_antag_cap = 1 + signup_atom_appearance = /mob/living/basic/morph + +/datum/dynamic_ruleset/midround/from_ghosts/morph/can_be_selected() + return ..() && !isnull(find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE)) + +/datum/dynamic_ruleset/midround/from_ghosts/morph/create_ruleset_body() + return new /mob/living/basic/morph(find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE)) + +/datum/dynamic_ruleset/midround/from_ghosts/morph/assign_role(datum/mind/candidate) + candidate.set_assigned_role(SSjob.get_job_type(/datum/job/morph)) + candidate.add_antag_datum(/datum/antagonist/morph) + +/datum/dynamic_ruleset/midround/from_ghosts/slaughter_demon + name = "Slaughter Demon" + config_tag = "Slaughter Demon" + candidate_role = "Slaughter Demon" + // preview_antag_datum = /datum/antagonist/slaughter // Doesn't actually have its own pref + midround_type = HEAVY_MIDROUND + jobban_flag = ROLE_ALIEN + ruleset_flags = RULESET_INVADER + weight = 0 + min_pop = 20 + max_antag_cap = 1 + signup_atom_appearance = /mob/living/basic/demon/slaughter + +/datum/dynamic_ruleset/midround/from_ghosts/slaughter_demon/can_be_selected() + return ..() && !isnull(find_space_spawn()) + +/datum/dynamic_ruleset/midround/from_ghosts/slaughter_demon/create_ruleset_body() + var/turf/spawnloc = find_space_spawn() + . = new /mob/living/basic/demon/slaughter(spawnloc) + new /obj/effect/dummy/phased_mob/blood(spawnloc, .) + +/datum/dynamic_ruleset/midround/from_ghosts/slaughter_demon/assign_role(datum/mind/candidate) + return // handled by new() entirely + +/datum/dynamic_ruleset/midround/from_living + min_antag_cap = 1 + max_antag_cap = 1 + repeatable = TRUE + +/datum/dynamic_ruleset/midround/from_living/set_config_value(nvar, nval) + if(nvar == NAMEOF(src, min_antag_cap) || nvar == NAMEOF(src, max_antag_cap)) + return FALSE + return ..() + +/datum/dynamic_ruleset/midround/from_living/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/midround/from_living/collect_candidates() + return GLOB.alive_player_list + +/datum/dynamic_ruleset/midround/from_living/is_valid_candidate(mob/candidate, client/candidate_client) + if(candidate.stat == DEAD || isnull(candidate.mind)) + return FALSE + // only pick members of the crew + if(!job_check(candidate)) + return FALSE + if(!antag_check(candidate)) + return FALSE + // checks for stuff like bitrunner avatars and ghost mafia + if(HAS_TRAIT(candidate, TRAIT_MIND_TEMPORARILY_GONE) || HAS_TRAIT(candidate, TRAIT_TEMPORARY_BODY)) + return FALSE + if(SEND_SIGNAL(candidate, COMSIG_MOB_MIND_BEFORE_MIDROUND_ROLL, src, pref_flag) & CANCEL_ROLL) + return FALSE + return ..() + +/// Checks if the candidate is a valid job for this ruleset - by default you probably only want crew members. (Return FALSE to mark the candidate invalid) +/datum/dynamic_ruleset/midround/from_living/proc/job_check(mob/candidate) + if(!(candidate.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) + return FALSE + if(candidate.mind.assigned_role.title in get_blacklisted_roles()) + return FALSE + return TRUE + +/// Checks if the candidate is an antag - most of the time you don't want to double dip. (Return FALSE to mark the candidate invalid) +/datum/dynamic_ruleset/midround/from_living/proc/antag_check(mob/candidate) + return !candidate.is_antag() + +/datum/dynamic_ruleset/midround/from_living/traitor + name = "Traitor" + config_tag = "Midround Traitor" + preview_antag_datum = /datum/antagonist/traitor + midround_type = LIGHT_MIDROUND + false_alarm_able = TRUE + pref_flag = ROLE_SLEEPER_AGENT + jobban_flag = ROLE_TRAITOR + weight = 10 + min_pop = 3 + blacklisted_roles = list( + JOB_HEAD_OF_PERSONNEL, + ) + +/datum/dynamic_ruleset/midround/from_living/traitor/assign_role(datum/mind/candidate) + candidate.add_antag_datum(/datum/antagonist/traitor) + +/datum/dynamic_ruleset/midround/from_living/traitor/false_alarm() + 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", + ) + +/datum/dynamic_ruleset/midround/from_living/malf_ai + name = "Malfunctioning AI" + config_tag = "Midround Malfunctioning AI" + preview_antag_datum = /datum/antagonist/malf_ai + midround_type = HEAVY_MIDROUND + pref_flag = ROLE_MALF_MIDROUND + jobban_flag = ROLE_MALF + 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 + repeatable = FALSE + +/datum/dynamic_ruleset/midround/from_living/malf_ai/get_always_blacklisted_roles() + return list() + +/datum/dynamic_ruleset/midround/from_living/malf_ai/job_check(mob/candidate) + return istype(candidate.mind.assigned_role, /datum/job/ai) + +/datum/dynamic_ruleset/midround/from_living/malf_ai/assign_role(datum/mind/candidate) + candidate.add_antag_datum(/datum/antagonist/malf_ai) + +/datum/dynamic_ruleset/midround/from_living/malf_ai/can_be_selected() + return ..() && !HAS_TRAIT(SSstation, STATION_TRAIT_HUMAN_AI) + +/datum/dynamic_ruleset/midround/from_living/blob + name = "Blob Infection" + config_tag = "Blob Infection" + preview_antag_datum = /datum/antagonist/blob/infection + midround_type = HEAVY_MIDROUND + pref_flag = ROLE_BLOB_INFECTION + jobban_flag = ROLE_BLOB + weight = list( + DYNAMIC_TIER_LOW = 0, + DYNAMIC_TIER_LOWMEDIUM = 1, + DYNAMIC_TIER_MEDIUMHIGH = 3, + DYNAMIC_TIER_HIGH = 3, + ) + min_pop = 30 + repeatable_weight_decrease = 3 + +/datum/dynamic_ruleset/midround/from_living/blob/assign_role(datum/mind/candidate) + candidate.add_antag_datum(/datum/antagonist/blob/infection) + notify_ghosts( + "[candidate.current.real_name] has become a blob host!", + source = candidate.current, + header = "So Bulbous...", + ) + +/datum/dynamic_ruleset/midround/from_living/obsesed + name = "Obsession" + config_tag = "Midround Obsessed" + preview_antag_datum = /datum/antagonist/obsessed + midround_type = LIGHT_MIDROUND + pref_flag = ROLE_OBSESSED + blacklisted_roles = list() + weight = list( + DYNAMIC_TIER_LOW = 5, + DYNAMIC_TIER_LOWMEDIUM = 5, + DYNAMIC_TIER_MEDIUMHIGH = 3, + DYNAMIC_TIER_HIGH = 1, + ) + min_pop = 5 + +/datum/dynamic_ruleset/midround/from_living/obsesed/is_valid_candidate(mob/candidate, client/candidate_client) + return ..() && !!candidate.get_organ_by_type(/obj/item/organ/brain) + +/datum/dynamic_ruleset/midround/from_living/obsesed/antag_check(mob/candidate) + // Obsessed is a special case, it can select other antag players + return !candidate.mind.has_antag_datum(/datum/antagonist/obsessed) + +/datum/dynamic_ruleset/midround/from_living/obsesed/assign_role(datum/mind/candidate) + var/obj/item/organ/brain/brain = candidate.current.get_organ_by_type(__IMPLIED_TYPE__) + brain.brain_gain_trauma(/datum/brain_trauma/special/obsessed) + notify_ghosts( + "[candidate.current.real_name] has developed an obsession with someone!", + source = candidate.current, + header = "Love Can Bloom", + ) diff --git a/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm b/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm new file mode 100644 index 00000000000..1bdfe947615 --- /dev/null +++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm @@ -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 diff --git a/code/controllers/subsystem/dynamic/dynamic_rulesets.dm b/code/controllers/subsystem/dynamic/dynamic_rulesets.dm deleted file mode 100644 index 1eca2aeaff3..00000000000 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets.dm +++ /dev/null @@ -1,310 +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 - - //SKYRAT EDIT ADDITION - if(!candidate_client.prefs?.read_preference(/datum/preference/toggle/be_antag)) - candidates.Remove(candidate_player) - continue - - if(is_banned_from(candidate_client.ckey, BAN_ANTAGONIST)) - candidates.Remove(candidate_player) - continue - //SKYRAT EDIT END - - 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 ..() diff --git a/code/controllers/subsystem/dynamic/dynamic_rulesets_latejoin.dm b/code/controllers/subsystem/dynamic/dynamic_rulesets_latejoin.dm deleted file mode 100644 index a5f1ded382a..00000000000 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets_latejoin.dm +++ /dev/null @@ -1,263 +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) - // SKYRAT EDIT ADDITION - PROTECTED JOBS - else if(P.client?.prefs && !P.client.prefs.read_preference(/datum/preference/toggle/be_antag)) - candidates.Remove(P) - continue - else if(is_banned_from(P.client?.ckey, BAN_ANTAGONIST)) - candidates.Remove(P) - continue - // SKYRAT EDIT END - -/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, - JOB_CHAPLAIN, // BUBBER EDIT - Chaplains can't heretic - ) - 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 diff --git a/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm b/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm deleted file mode 100644 index 0821fcbf2e5..00000000000 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm +++ /dev/null @@ -1,1009 +0,0 @@ -/// Probability the AI going malf will be accompanied by an ion storm announcement and some ion laws. -#define MALF_ION_PROB 33 -/// The probability to replace an existing law with an ion law instead of adding a new ion law. -#define REPLACE_LAW_WITH_ION_PROB 10 - -/// Midround Rulesets -/datum/dynamic_ruleset/midround // Can be drafted once in a while during a round - ruletype = MIDROUND_RULESET - var/midround_ruleset_style - /// If the ruleset should be restricted from ghost roles. - var/restrict_ghost_roles = TRUE - /// What mob type the ruleset is restricted to. - var/required_type = /mob/living/carbon/human - var/list/living_players = list() - var/list/living_antags = list() - var/list/dead_players = list() - var/list/list_observers = list() - - /// The minimum round time before this ruleset will show up - var/minimum_round_time = 0 - /// Abstract root value - var/abstract_type = /datum/dynamic_ruleset/midround - -/datum/dynamic_ruleset/midround/forget_startup() - living_players = list() - living_antags = list() - dead_players = list() - list_observers = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts - weight = 0 - required_type = /mob/dead/observer - abstract_type = /datum/dynamic_ruleset/midround/from_ghosts - /// Whether the ruleset should call generate_ruleset_body or not. - var/makeBody = TRUE - /// The rule needs this many applicants to be properly executed. - var/required_applicants = 1 - ///Path of an item to show up in ghost polls for applicants to sign up. - var/signup_item_path = /obj/structure/sign/poster/contraband/syndicate_recruitment - -/datum/dynamic_ruleset/midround/from_ghosts/check_candidates() - var/dead_count = dead_players.len + list_observers.len - if (required_candidates <= dead_count) - return TRUE - - log_dynamic("FAIL: [src], a from_ghosts ruleset, did not have enough dead candidates: [required_candidates] needed, [dead_count] found") - - return FALSE - -/datum/dynamic_ruleset/midround/trim_candidates() - living_players = trim_list(GLOB.alive_player_list) - living_antags = trim_list(GLOB.current_living_antags) - dead_players = trim_list(GLOB.dead_player_list) - list_observers = trim_list(GLOB.current_observers_list) - -/datum/dynamic_ruleset/midround/proc/trim_list(list/to_trim = list()) - var/list/trimmed_list = to_trim.Copy() - for(var/mob/creature in trimmed_list) - if (!istype(creature, required_type)) - trimmed_list.Remove(creature) - continue - if (isnull(creature.client)) // Are they connected? - trimmed_list.Remove(creature) - continue - //SKYRAT EDIT ADDITION - if(is_banned_from(creature.client.ckey, BAN_ANTAGONIST)) - trimmed_list.Remove(creature) - continue - if(!creature.client?.prefs?.read_preference(/datum/preference/toggle/be_antag)) - trimmed_list.Remove(creature) - continue - //SKYRAT EDIT END - if(creature.client.get_remaining_days(minimum_required_age) > 0) - trimmed_list.Remove(creature) - continue - if (!((antag_preference || antag_flag) in creature.client.prefs.be_special)) - trimmed_list.Remove(creature) - continue - if (is_banned_from(creature.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE))) - trimmed_list.Remove(creature) - continue - if (isnull(creature.mind)) - continue - if (restrict_ghost_roles && !(creature.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) // Are they not playing a station role? - trimmed_list.Remove(creature) - continue - if (creature.mind.assigned_role.title in restricted_roles) // Does their job allow it? - trimmed_list.Remove(creature) - continue - if (length(exclusive_roles) && !(creature.mind.assigned_role.title in exclusive_roles)) // Is the rule exclusive to their job? - trimmed_list.Remove(creature) - continue - if(HAS_TRAIT(creature, TRAIT_MIND_TEMPORARILY_GONE)) // are they out of body? - trimmed_list.Remove(creature) - continue - if(HAS_TRAIT(creature, TRAIT_TEMPORARY_BODY)) // are they an avatar? - trimmed_list.Remove(creature) - continue - return trimmed_list - -// You can then for example prompt dead players in execute() to join as strike teams or whatever -// Or autotator someone - -// IMPORTANT, since /datum/dynamic_ruleset/midround may accept candidates from both living, dead, and even antag players -// subtype your midround with /from_ghosts or /from_living to get candidate checking. Or check yourself by subtyping from neither -/datum/dynamic_ruleset/midround/ready(forced = FALSE) - if (forced) - return TRUE - - var/job_check = 0 - if (enemy_roles.len > 0) - for (var/mob/M in GLOB.alive_player_list) - if (M.stat == DEAD || !M.client) - continue // Dead/disconnected players cannot count as opponents - if (M.mind && (M.mind.assigned_role.title in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role.title in restricted_roles))) - job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it - - var/threat = round(SSdynamic.threat_level/10) - var/ruleset_forced = (GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED) == RULESET_FORCE_ENABLED - if (!ruleset_forced && job_check < required_enemies[threat]) - log_dynamic("FAIL: [src] is not ready, because there are not enough enemies: [required_enemies[threat]] needed, [job_check] found") - return FALSE - - return TRUE - -/datum/dynamic_ruleset/midround/from_ghosts/execute() - var/list/possible_candidates = list() - possible_candidates.Add(dead_players) - possible_candidates.Add(list_observers) - send_applications(possible_candidates) - if(assigned.len > 0) - return TRUE - else - return FALSE - -/// This sends a poll to ghosts if they want to be a ghost spawn from a ruleset. -/datum/dynamic_ruleset/midround/from_ghosts/proc/send_applications(list/possible_volunteers = list()) - if (possible_volunteers.len <= 0) // This shouldn't happen, as ready() should return FALSE if there is not a single valid candidate - message_admins("Possible volunteers was 0. This shouldn't appear, because of ready(), unless you forced it!") - return - - SSdynamic.log_dynamic_and_announce("Polling [possible_volunteers.len] players to apply for the [name] ruleset.") - candidates = SSpolling.poll_ghost_candidates( - question = "Looking for volunteers to become [span_notice(antag_flag)] for [span_danger(name)]", - check_jobban = antag_flag_override, - role = antag_flag || antag_flag_override, - poll_time = 30 SECONDS, - alert_pic = signup_item_path, - role_name_text = antag_flag, - ) - - if(!candidates || candidates.len <= 0) - SSdynamic.log_dynamic_and_announce("The ruleset [name] received no applications.") - SSdynamic.executed_rules -= src - attempt_replacement() - return - - SSdynamic.log_dynamic_and_announce("[candidates.len] players volunteered for [name].") - review_applications() - -/// Here is where you can check if your ghost applicants are valid for the ruleset. -/// Called by send_applications(). -/datum/dynamic_ruleset/midround/from_ghosts/proc/review_applications() - if(candidates.len < required_applicants) - SSdynamic.executed_rules -= src - return - for (var/i = 1, i <= required_candidates, i++) - if(candidates.len <= 0) - break - var/mob/applicant = pick(candidates) - candidates -= applicant - if(!isobserver(applicant)) - if(applicant.stat == DEAD) // Not an observer? If they're dead, make them one. - applicant = applicant.ghostize(FALSE) - else // Not dead? Disregard them, pick a new applicant - i-- - continue - if(!applicant) - i-- - continue - assigned += applicant - finish_applications() - -/// Here the accepted applications get generated bodies and their setup is finished. -/// Called by review_applications() -/datum/dynamic_ruleset/midround/from_ghosts/proc/finish_applications() - var/i = 0 - for(var/mob/applicant as anything in assigned) - i++ - var/mob/new_character = applicant - if(makeBody) - new_character = generate_ruleset_body(applicant) - SEND_GLOBAL_SIGNAL(COMSIG_RULESET_BODY_GENERATED_FROM_GHOSTS, applicant) - finish_setup(new_character, i) - notify_ghosts( - "[applicant.name] has been picked for the ruleset [name]!", - source = new_character, - ) - -/datum/dynamic_ruleset/midround/from_ghosts/proc/generate_ruleset_body(mob/applicant) - var/mob/living/carbon/human/new_character = make_body(applicant) - new_character.dna.remove_all_mutations() - return new_character - -/datum/dynamic_ruleset/midround/from_ghosts/proc/finish_setup(mob/new_character, index) - var/datum/antagonist/new_role = new antag_datum() - setup_role(new_role) - new_character.mind.add_antag_datum(new_role) - new_character.mind.special_role = antag_flag - -/datum/dynamic_ruleset/midround/from_ghosts/proc/setup_role(datum/antagonist/new_role) - return - -/// Fired when there are no valid candidates. Will spawn a sleeper agent or latejoin traitor. -/datum/dynamic_ruleset/midround/from_ghosts/proc/attempt_replacement() - var/datum/dynamic_ruleset/midround/from_living/autotraitor/sleeper_agent = new - - SSdynamic.configure_ruleset(sleeper_agent) - - if (!SSdynamic.picking_specific_rule(sleeper_agent)) - return - - SSdynamic.picking_specific_rule(/datum/dynamic_ruleset/latejoin/infiltrator) - -///subtype to handle checking players -/datum/dynamic_ruleset/midround/from_living - weight = 0 - abstract_type = /datum/dynamic_ruleset/midround/from_living - -/datum/dynamic_ruleset/midround/from_living/ready(forced) - if(!check_candidates()) - return FALSE - return ..() - - -/// Midround Traitor Ruleset (From Living) -/datum/dynamic_ruleset/midround/from_living/autotraitor - name = "Syndicate Sleeper Agent" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/traitor - antag_flag = ROLE_SLEEPER_AGENT - antag_flag_override = ROLE_TRAITOR - protected_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_PERSONNEL, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ROLE_POSITRONIC_BRAIN, - ) - required_candidates = 1 - weight = 35 - cost = 3 - requirements = list(3,3,3,3,3,3,3,3,3,3) - repeatable = TRUE - -/datum/dynamic_ruleset/midround/from_living/autotraitor/trim_candidates() - ..() - candidates = living_players - for(var/mob/living/player in candidates) - if(issilicon(player)) // Your assigned role doesn't change when you are turned into a silicon. - candidates -= player - else if(is_centcom_level(player.z)) - candidates -= player // We don't autotator people in CentCom - else if(player.mind && (player.mind.special_role || !player.mind.can_roll_midround(antag_datum))) - candidates -= player // We don't autotator people with roles already - -/datum/dynamic_ruleset/midround/from_living/autotraitor/execute() - var/mob/M = pick(candidates) - assigned += M - candidates -= M - var/datum/antagonist/traitor/newTraitor = new - M.mind.add_antag_datum(newTraitor) - message_admins("[ADMIN_LOOKUPFLW(M)] was selected by the [name] ruleset and has been made into a midround traitor.") - log_dynamic("[key_name(M)] was selected by the [name] ruleset and has been made into a midround traitor.") - return TRUE - -/// Midround Malf AI Ruleset (From Living) -/datum/dynamic_ruleset/midround/malf - name = "Malfunctioning AI" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/malf_ai - antag_flag = ROLE_MALF_MIDROUND - antag_flag_override = ROLE_MALF - enemy_roles = list( - JOB_CHEMIST, - JOB_CHIEF_ENGINEER, - JOB_HEAD_OF_SECURITY, - JOB_RESEARCH_DIRECTOR, - JOB_SCIENTIST, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - exclusive_roles = list(JOB_AI) - required_enemies = list(4,4,4,4,4,4,2,2,2,0) - required_candidates = 1 - minimum_players = 25 - weight = 2 - cost = 10 - required_type = /mob/living/silicon/ai - blocking_rules = list(/datum/dynamic_ruleset/roundstart/malf_ai) - // AIs are technically considered "Ghost roles" as far as candidate selection are concerned - // So we need to allow it here. We filter of actual ghost role AIs (charlie) via trim_candidates ourselves - restrict_ghost_roles = FALSE - -/datum/dynamic_ruleset/midround/malf/trim_candidates() - ..() - candidates = list() - for(var/mob/living/silicon/ai/player in living_players) - if(!is_station_level(player.z)) - continue - if(isnull(player.mind)) - continue - if(player.mind.special_role || !player.mind.can_roll_midround(antag_datum)) - continue - candidates += player - -/datum/dynamic_ruleset/midround/malf/ready(forced) - if(!check_candidates()) - log_dynamic("FAIL: No valid AI found for the Malfunctioning AI ruleset.") - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/malf/execute() - var/mob/living/silicon/ai/new_malf_ai = pick_n_take(candidates) - assigned += new_malf_ai.mind - var/datum/antagonist/malf_ai/malf_antag_datum = new - new_malf_ai.mind.special_role = antag_flag - new_malf_ai.mind.add_antag_datum(malf_antag_datum) - if(prob(MALF_ION_PROB)) - priority_announce("Ion storm detected near the station. Please check all AI-controlled equipment for errors.", "Anomaly Alert", ANNOUNCER_IONSTORM) - if(prob(REPLACE_LAW_WITH_ION_PROB)) - new_malf_ai.replace_random_law(generate_ion_law(), list(LAW_INHERENT, LAW_SUPPLIED, LAW_ION), LAW_ION) - else - new_malf_ai.add_ion_law(generate_ion_law()) - return TRUE - -/// Midround Wizard Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/wizard - name = "Wizard" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/wizard - antag_flag = ROLE_WIZARD_MIDROUND - antag_flag_override = ROLE_WIZARD - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 1 - cost = 10 - requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED - flags = HIGH_IMPACT_RULESET - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_WIZARDDEN) - signup_item_path = /obj/item/clothing/head/wizard - -/datum/dynamic_ruleset/midround/from_ghosts/wizard/ready(forced = FALSE) - if(!check_candidates()) - return FALSE - if(!length(GLOB.wizardstart)) - log_admin("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") - message_admins("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/wizard/finish_setup(mob/new_character, index) - ..() - new_character.forceMove(pick(GLOB.wizardstart)) - -/// Midround Nuclear Operatives Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/nuclear - name = "Nuclear Assault" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_flag = ROLE_OPERATIVE_MIDROUND - antag_flag_override = ROLE_OPERATIVE - antag_datum = /datum/antagonist/nukeop - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - enemy_roles = list( - JOB_AI, - JOB_CYBORG, - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_SECURITY, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - required_enemies = list(3,3,3,3,3,2,1,1,0,0) - required_candidates = 5 - weight = 5 - cost = 7 - minimum_round_time = 70 MINUTES - requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE) - flags = HIGH_IMPACT_RULESET - signup_item_path = /obj/machinery/nuclearbomb - - var/list/operative_cap = list(2,2,3,3,4,5,5,5,5,5) - -/datum/dynamic_ruleset/midround/from_ghosts/nuclear/acceptable(population=0, threat_level=0) - if (locate(/datum/dynamic_ruleset/roundstart/nuclear) in SSdynamic.executed_rules) - return FALSE // Unavailable if nuke ops were already sent at roundstart - indice_pop = min(operative_cap.len, round(living_players.len/5)+1) - required_candidates = operative_cap[indice_pop] - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/nuclear/ready(forced = FALSE) - if (!check_candidates()) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_applications() - var/mob/leader = get_most_experienced(assigned, ROLE_NUCLEAR_OPERATIVE) - if(leader) - assigned.Remove(leader) - assigned.Insert(1, leader) - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_setup(mob/new_character, index) - new_character.mind.set_assigned_role(SSjob.get_job_type(/datum/job/nuclear_operative)) - new_character.mind.special_role = ROLE_NUCLEAR_OPERATIVE - if(index == 1) - var/datum/antagonist/nukeop/leader/leader_antag_datum = new() - new_character.mind.add_antag_datum(leader_antag_datum) - return - return ..() - -/// Midround Blob Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/blob - name = "Blob" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/blob - antag_flag = ROLE_BLOB - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - minimum_round_time = 35 MINUTES - weight = 3 - cost = 8 - minimum_players = 25 - repeatable = TRUE - signup_item_path = /obj/structure/blob/normal - -/datum/dynamic_ruleset/midround/from_ghosts/blob/generate_ruleset_body(mob/applicant) - var/body = applicant.become_overmind() - return body - -/// Midround Blob Infection Ruleset (From Living) -/datum/dynamic_ruleset/midround/from_living/blob_infection - name = "Blob Infection" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/blob/infection - antag_flag = ROLE_BLOB_INFECTION - antag_flag_override = ROLE_BLOB - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - protected_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ROLE_POSITRONIC_BRAIN, - ) - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - minimum_round_time = 35 MINUTES - weight = 3 - cost = 10 - minimum_players = 25 - repeatable = TRUE - -/datum/dynamic_ruleset/midround/from_living/blob_infection/trim_candidates() - ..() - candidates = living_players - for(var/mob/living/player as anything in candidates) - var/turf/player_turf = get_turf(player) - if(!player_turf || !is_station_level(player_turf.z)) - candidates -= player - continue - - if(player.mind && (player.mind.special_role || !player.mind.can_roll_midround(antag_datum))) - candidates -= player - -/datum/dynamic_ruleset/midround/from_living/blob_infection/execute() - if(!candidates || !candidates.len) - return FALSE - var/mob/living/carbon/human/blob_antag = pick_n_take(candidates) - assigned += blob_antag.mind - blob_antag.mind.special_role = antag_flag - notify_ghosts( - "[blob_antag] has become a blob host!", - source = blob_antag, - header = "So Bulbous...", - ) - return ..() - -/// Midround Xenomorph Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/xenomorph - name = "Alien Infestation" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/xeno - antag_flag = ROLE_ALIEN - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - minimum_round_time = 40 MINUTES - weight = 5 - cost = 10 - minimum_players = 25 - repeatable = TRUE - signup_item_path = /mob/living/basic/alien - var/list/vents = list() - -/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/forget_startup() - vents = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/execute() - // 50% chance of being incremented by one - required_candidates += prob(50) - var/list/vent_pumps = SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/atmospherics/components/unary/vent_pump) - for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent as anything in vent_pumps) - if(QDELETED(temp_vent)) - continue - if(is_station_level(temp_vent.loc.z) && !temp_vent.welded) - var/datum/pipeline/temp_vent_parent = temp_vent.parents[1] - if(!temp_vent_parent) - continue // No parent vent - // Stops Aliens getting stuck in small networks. - // See: Security, Virology - if(temp_vent_parent.other_atmos_machines.len > 20) - vents += temp_vent - if(!vents.len) - return FALSE - . = ..() - -/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/generate_ruleset_body(mob/applicant) - var/obj/vent = pick_n_take(vents) - var/mob/living/carbon/alien/larva/new_xeno = new(vent.loc) - new_xeno.PossessByPlayer(applicant.ckey) - new_xeno.move_into_vent(vent) - message_admins("[ADMIN_LOOKUPFLW(new_xeno)] has been made into an alien by the midround ruleset.") - log_dynamic("[key_name(new_xeno)] was spawned as an alien by the midround ruleset.") - return new_xeno - -/// Midround Nightmare Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/nightmare - name = "Nightmare" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/nightmare - antag_flag = ROLE_NIGHTMARE - antag_flag_override = ROLE_ALIEN - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 3 - cost = 5 - minimum_players = 15 - repeatable = TRUE - signup_item_path = /obj/item/light_eater - -/datum/dynamic_ruleset/midround/from_ghosts/nightmare/acceptable(population = 0, threat_level = 0) - var/turf/spawn_loc = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE) //Checks if there's a single safe, dark tile on station. - if(!spawn_loc) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/nightmare/generate_ruleset_body(mob/applicant) - var/datum/mind/player_mind = new /datum/mind(applicant.key) - player_mind.active = TRUE - - var/mob/living/carbon/human/new_nightmare = new (find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE)) - player_mind.transfer_to(new_nightmare) - player_mind.set_assigned_role(SSjob.get_job_type(/datum/job/nightmare)) - player_mind.special_role = ROLE_NIGHTMARE - player_mind.add_antag_datum(/datum/antagonist/nightmare) - new_nightmare.set_species(/datum/species/shadow/nightmare) - - playsound(new_nightmare, 'sound/effects/magic/ethereal_exit.ogg', 50, TRUE, -1) - message_admins("[ADMIN_LOOKUPFLW(new_nightmare)] has been made into a Nightmare by the midround ruleset.") - log_dynamic("[key_name(new_nightmare)] was spawned as a Nightmare by the midround ruleset.") - return new_nightmare - -/// Midround Space Dragon Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/space_dragon - name = "Space Dragon" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/space_dragon - antag_flag = ROLE_SPACE_DRAGON - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 4 - cost = 7 - minimum_players = 25 - repeatable = TRUE - signup_item_path = /mob/living/basic/space_dragon - var/list/spawn_locs = list() - -/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/forget_startup() - spawn_locs = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/execute() - for(var/obj/effect/landmark/carpspawn/C in GLOB.landmarks_list) - spawn_locs += (C.loc) - if(!spawn_locs.len) - message_admins("No valid spawn locations found, aborting...") - return MAP_ERROR - . = ..() - -/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/generate_ruleset_body(mob/applicant) - var/datum/mind/player_mind = new /datum/mind(applicant.key) - player_mind.active = TRUE - - var/mob/living/basic/space_dragon/S = new (pick(spawn_locs)) - player_mind.transfer_to(S) - player_mind.add_antag_datum(/datum/antagonist/space_dragon) - - playsound(S, 'sound/effects/magic/ethereal_exit.ogg', 50, TRUE, -1) - message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a Space Dragon by the midround ruleset.") - log_dynamic("[key_name(S)] was spawned as a Space Dragon by the midround ruleset.") - priority_announce("A large organic energy flux has been recorded near of [station_name()], please stand-by.", "Lifesign Alert") - return S - -/datum/dynamic_ruleset/midround/from_ghosts/abductors - name = "Abductors" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/abductor - antag_flag = ROLE_ABDUCTOR - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 2 - required_applicants = 2 - weight = 4 - cost = 7 - minimum_players = 25 - repeatable = TRUE - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_ABDUCTOR_SHIPS) - - var/datum/team/abductor_team/new_team - -/datum/dynamic_ruleset/midround/from_ghosts/abductors/forget_startup() - new_team = null - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/abductors/ready(forced = FALSE) - if (required_candidates > (dead_players.len + list_observers.len)) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/abductors/finish_setup(mob/new_character, index) - if (index == 1) // Our first guy is the scientist. We also initialize the team here as well since this should only happen once per pair of abductors. - new_team = new - if(new_team.team_number > ABDUCTOR_MAX_TEAMS) - return MAP_ERROR - var/datum/antagonist/abductor/scientist/new_role = new - new_character.mind.add_antag_datum(new_role, new_team) - else // Our second guy is the agent, team is already created, don't need to make another one. - var/datum/antagonist/abductor/agent/new_role = new - new_character.mind.add_antag_datum(new_role, new_team) - -/// Midround Space Ninja Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/space_ninja - name = "Space Ninja" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/ninja - antag_flag = ROLE_NINJA - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 4 - cost = 8 - minimum_players = 30 - repeatable = TRUE - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY) // I mean, no one uses the nets anymore but whateva - signup_item_path = /obj/item/energy_katana - - var/list/spawn_locs = list() - -/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/forget_startup() - spawn_locs = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/execute() - for(var/obj/effect/landmark/carpspawn/carp_spawn in GLOB.landmarks_list) - if(!isturf(carp_spawn.loc)) - stack_trace("Carp spawn found not on a turf: [carp_spawn.type] on [isnull(carp_spawn.loc) ? "null" : carp_spawn.loc.type]") - continue - spawn_locs += carp_spawn.loc - if(!spawn_locs.len) - message_admins("No valid spawn locations found, aborting...") - return MAP_ERROR - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/generate_ruleset_body(mob/applicant) - var/mob/living/carbon/human/ninja = create_space_ninja(pick(spawn_locs)) - ninja.PossessByPlayer(applicant.ckey) - ninja.mind.add_antag_datum(/datum/antagonist/ninja) - - message_admins("[ADMIN_LOOKUPFLW(ninja)] has been made into a Space Ninja by the midround ruleset.") - log_dynamic("[key_name(ninja)] was spawned as a Space Ninja by the midround ruleset.") - return ninja - -/// Midround Spiders Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/spiders - name = "Spiders" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_flag = ROLE_SPIDER - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - required_type = /mob/dead/observer - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 0 - weight = 3 - cost = 8 - minimum_players = 27 - repeatable = TRUE - var/spawncount = 2 - -/datum/dynamic_ruleset/midround/spiders/execute() - create_midwife_eggs(spawncount) - return ..() - -/// Midround Revenant Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/revenant - name = "Revenant" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - antag_datum = /datum/antagonist/revenant - antag_flag = ROLE_REVENANT - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 4 - cost = 5 - minimum_players = 15 - repeatable = TRUE - signup_item_path = /mob/living/basic/revenant - var/dead_mobs_required = 20 - var/need_extra_spawns_value = 15 - var/list/spawn_locs = list() - -/datum/dynamic_ruleset/midround/from_ghosts/revenant/forget_startup() - spawn_locs = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/revenant/acceptable(population=0, threat_level=0) - if(GLOB.dead_mob_list.len < dead_mobs_required) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/revenant/execute() - for(var/mob/living/corpse in GLOB.dead_mob_list) //look for any dead bodies - var/turf/corpse_turf = get_turf(corpse) - if(corpse_turf && is_station_level(corpse_turf.z)) - spawn_locs += corpse_turf - if(!spawn_locs.len || spawn_locs.len < need_extra_spawns_value) //look for any morgue trays, crematoriums, ect if there weren't alot of dead bodies on the station to pick from - for(var/obj/structure/bodycontainer/corpse_container in GLOB.bodycontainers) - var/turf/container_turf = get_turf(corpse_container) - if(container_turf && is_station_level(container_turf.z)) - spawn_locs += container_turf - if(!spawn_locs.len) //If we can't find any valid spawnpoints, try the carp spawns - for(var/obj/effect/landmark/carpspawn/carp_spawnpoint in GLOB.landmarks_list) - if(isturf(carp_spawnpoint.loc)) - spawn_locs += carp_spawnpoint.loc - if(!spawn_locs.len) //If we can't find THAT, then just give up and cry - return FALSE - . = ..() - -/datum/dynamic_ruleset/midround/from_ghosts/revenant/generate_ruleset_body(mob/applicant) - var/mob/living/basic/revenant/revenant = new(pick(spawn_locs)) - revenant.PossessByPlayer(applicant.ckey) - message_admins("[ADMIN_LOOKUPFLW(revenant)] has been made into a revenant by the midround ruleset.") - log_game("[key_name(revenant)] was spawned as a revenant by the midround ruleset.") - return revenant - -/// Midround Space Pirates Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/pirates - name = "Space Pirates" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - antag_flag = "Space Pirates" - required_type = /mob/dead/observer - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 0 - weight = 3 - cost = 8 - minimum_players = 20 - repeatable = TRUE - -/datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat_level=0) - if (SSmapping.is_planetary() || GLOB.light_pirate_gangs.len == 0) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/pirates/execute() - send_pirate_threat(GLOB.light_pirate_gangs) - return ..() - -/// Dangerous Space Pirates ruleset -/datum/dynamic_ruleset/midround/dangerous_pirates - name = "Dangerous Space Pirates" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - antag_flag = "Space Pirates" - required_type = /mob/dead/observer - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 0 - weight = 3 - cost = 8 - minimum_players = 25 - repeatable = TRUE - -/datum/dynamic_ruleset/midround/dangerous_pirates/acceptable(population=0, threat_level=0) - if (SSmapping.is_planetary() || GLOB.heavy_pirate_gangs.len == 0) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/dangerous_pirates/execute() - send_pirate_threat(GLOB.heavy_pirate_gangs) - return ..() - -/// Midround Obsessed Ruleset (From Living) -/datum/dynamic_ruleset/midround/from_living/obsessed - name = "Obsessed" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/obsessed - antag_flag = ROLE_OBSESSED - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ROLE_POSITRONIC_BRAIN, - ) - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 4 - cost = 3 // Doesn't have the same impact on rounds as revenants, dragons, sentient disease (10) or syndicate infiltrators (5). - repeatable = TRUE - -/datum/dynamic_ruleset/midround/from_living/obsessed/trim_candidates() - ..() - candidates = living_players - for(var/mob/living/carbon/human/candidate in candidates) - if( \ - !candidate.get_organ_by_type(/obj/item/organ/brain) \ - || candidate.mind.has_antag_datum(/datum/antagonist/obsessed) \ - || candidate.stat == DEAD \ - || !(ROLE_OBSESSED in candidate.client?.prefs?.be_special) \ - || !candidate.mind.assigned_role \ - ) - candidates -= candidate - -/datum/dynamic_ruleset/midround/from_living/obsessed/execute() - var/mob/living/carbon/human/obsessed = pick_n_take(candidates) - obsessed.gain_trauma(/datum/brain_trauma/special/obsessed) - message_admins("[ADMIN_LOOKUPFLW(obsessed)] has been made Obsessed by the midround ruleset.") - log_game("[key_name(obsessed)] was made Obsessed by the midround ruleset.") - notify_ghosts( - "[obsessed.real_name] has developed an obsession with someone!", - source = obsessed, - header = "Love Can Bloom", - ) - return TRUE - -/// Midround Space Changeling Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/changeling_midround - name = "Space Changeling" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/changeling/space - antag_flag = ROLE_CHANGELING_MIDROUND - antag_flag_override = ROLE_CHANGELING - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - required_type = /mob/dead/observer - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 3 - cost = 7 - minimum_players = 15 - repeatable = TRUE - signup_item_path = /obj/effect/meteor/meaty/changeling - -/datum/dynamic_ruleset/midround/from_ghosts/changeling_midround/generate_ruleset_body(mob/applicant) - var/body = generate_changeling_meteor(applicant) - message_admins("[ADMIN_LOOKUPFLW(body)] has been made into a space changeling by the midround ruleset.") - log_dynamic("[key_name(body)] was spawned as a space changeling by the midround ruleset.") - return body - -/// Midround Paradox Clone Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone - name = "Paradox Clone" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/paradox_clone - antag_flag = ROLE_PARADOX_CLONE - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - enemy_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_SECURITY, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - required_enemies = list(2, 2, 1, 1, 1, 1, 1, 0, 0, 0) - required_candidates = 1 - weight = 4 - cost = 3 - repeatable = TRUE - signup_item_path = /obj/effect/bluespace_stream - var/list/possible_spawns = list() ///places the antag can spawn - -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/forget_startup() - possible_spawns = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/execute() - possible_spawns += find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE) - if(!possible_spawns.len) - return MAP_ERROR - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/generate_ruleset_body(mob/applicant) - var/datum/mind/player_mind = new /datum/mind(applicant.key) - player_mind.active = TRUE - - var/mob/living/carbon/human/clone_victim = find_original() - var/mob/living/carbon/human/clone = clone_victim.make_full_human_copy(pick(possible_spawns)) - player_mind.transfer_to(clone) - - var/datum/antagonist/paradox_clone/new_datum = player_mind.add_antag_datum(/datum/antagonist/paradox_clone) - new_datum.original_ref = WEAKREF(clone_victim.mind) - new_datum.setup_clone() - - playsound(clone, 'sound/items/weapons/zapbang.ogg', 30, TRUE) - new /obj/item/storage/toolbox/mechanical(clone.loc) //so they dont get stuck in maints - - message_admins("[ADMIN_LOOKUPFLW(clone)] has been made into a Paradox Clone by the midround ruleset.") - clone.log_message("was spawned as a Paradox Clone of [key_name(clone)] by the midround ruleset.", LOG_GAME) - - return clone - -/** - * Trims through GLOB.player_list and finds a target - * Returns a single human victim, if none is possible then returns null. - */ -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/proc/find_original() - var/list/possible_targets = list() - - for(var/mob/living/carbon/human/player in GLOB.player_list) - if(!player.client || !player.mind || player.stat) - continue - if(!(player.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) - continue - possible_targets += player - - if(possible_targets.len) - return pick(possible_targets) - return FALSE - -#undef MALF_ION_PROB -#undef REPLACE_LAW_WITH_ION_PROB - -/// Midround Voidwalker Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/voidwalker - name = "Voidwalker" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/voidwalker - antag_flag = ROLE_VOIDWALKER - antag_flag_override = ROLE_VOIDWALKER - ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 2 - cost = 5 - minimum_players = 40 - repeatable = TRUE - signup_item_path = /obj/item/clothing/head/helmet/skull/cosmic - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_VOIDWALKER_VOID) - /// The space turf we find in acceptable(), cached for ease - var/space_turf - -/datum/dynamic_ruleset/midround/from_ghosts/voidwalker/acceptable(population = 0, threat_level = 0) - space_turf = find_space_spawn() - // Space only antag and will die on planetary gravity. - if(SSmapping.is_planetary() || !space_turf) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/voidwalker/generate_ruleset_body(mob/applicant) - var/datum/mind/player_mind = new /datum/mind(applicant.key) - player_mind.active = TRUE - - var/mob/living/carbon/human/voidwalker = new (space_turf) - player_mind.transfer_to(voidwalker) - player_mind.set_assigned_role(SSjob.get_job_type(/datum/job/voidwalker)) - player_mind.special_role = antag_flag - player_mind.add_antag_datum(antag_datum) - - playsound(voidwalker, 'sound/effects/magic/ethereal_exit.ogg', 50, TRUE, -1) - message_admins("[ADMIN_LOOKUPFLW(voidwalker)] has been made into a Voidwalker by the midround ruleset.") - log_dynamic("[key_name(voidwalker)] was spawned as a Voidwalker by the midround ruleset.") - return voidwalker diff --git a/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm b/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm deleted file mode 100644 index 7880e1cb12b..00000000000 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm +++ /dev/null @@ -1,747 +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, - JOB_CHAPLAIN, // BUBBER EDIT - Chaplains can't heretic - ) - 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) diff --git a/code/controllers/subsystem/dynamic/dynamic_testing.dm b/code/controllers/subsystem/dynamic/dynamic_testing.dm new file mode 100644 index 00000000000..5df1e3d2977 --- /dev/null +++ b/code/controllers/subsystem/dynamic/dynamic_testing.dm @@ -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() diff --git a/code/controllers/subsystem/dynamic/dynamic_unfavorable_situation.dm b/code/controllers/subsystem/dynamic/dynamic_unfavorable_situation.dm deleted file mode 100644 index 994f2e3f5de..00000000000 --- a/code/controllers/subsystem/dynamic/dynamic_unfavorable_situation.dm +++ /dev/null @@ -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 diff --git a/code/controllers/subsystem/dynamic/readme.md b/code/controllers/subsystem/dynamic/readme.md deleted file mode 100644 index d59ed6215a9..00000000000 --- a/code/controllers/subsystem/dynamic/readme.md +++ /dev/null @@ -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`. diff --git a/code/controllers/subsystem/dynamic/ruleset_picking.dm b/code/controllers/subsystem/dynamic/ruleset_picking.dm deleted file mode 100644 index baeceada57c..00000000000 --- a/code/controllers/subsystem/dynamic/ruleset_picking.dm +++ /dev/null @@ -1,159 +0,0 @@ -#define ADMIN_CANCEL_MIDROUND_TIME (120 SECONDS) // BUBBER EDIT - -/// -/// -/** - * 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, \ - ) - // SKYRAT EDIT REMOVAL BEGIN - Event notification - /** - log_dynamic("[rule] ruleset executing...") - message_admins("DYNAMIC: Executing midround ruleset [rule] in [DisplayTimeText(ADMIN_CANCEL_MIDROUND_TIME)]. \ - CANCEL | \ - SOMETHING ELSE") - - return rule - */ - // SKYRAT EDIT REMOVAL END - Event notification - - // SKYRAT EDIT ADDITION BEGIN - Event notification - message_admins("Dynamic Event triggering in [DisplayTimeText(ADMIN_CANCEL_MIDROUND_TIME)]: [rule]. (\ - CANCEL | \ - SOMETHING ELSE)") - for(var/client/staff as anything in GLOB.admins) - if(staff?.prefs.read_preference(/datum/preference/toggle/comms_notification)) - SEND_SOUND(staff, sound('sound/misc/server-ready.ogg')) - sleep(ADMIN_CANCEL_MIDROUND_TIME * 0.5) - - if(!midround_injection_timer_id == null) - message_admins("Dynamic Event triggering in [DisplayTimeText(ADMIN_CANCEL_MIDROUND_TIME * 0.5)]: [rule]. (\ - CANCEL | \ - SOMETHING ELSE)") - - return rule - // SKYRAT EDIT ADDITION END - Event notification - -/// 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 diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm index e97be33d6e5..3863ae8f202 100644 --- a/code/controllers/subsystem/job.dm +++ b/code/controllers/subsystem/job.dm @@ -35,8 +35,10 @@ SUBSYSTEM_DEF(job) var/list/level_order = list(JP_HIGH, JP_MEDIUM, JP_LOW) - /// Lazylist of mob:occupation_string pairs. - var/list/dynamic_forced_occupations + /// Lazylist of mob:occupation_string pairs. Forces mobs into certain occupations with highest priority. + 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. @@ -322,7 +324,6 @@ SUBSYSTEM_DEF(job) if(!player?.mind) continue player.mind.set_assigned_role(get_job_type(/datum/job/unassigned)) - player.mind.special_role = null setup_occupations() unassigned = list() if(CONFIG_GET(flag/load_jobs_from_txt)) @@ -415,9 +416,8 @@ SUBSYSTEM_DEF(job) SEND_SIGNAL(src, COMSIG_OCCUPATIONS_DIVIDED, pure, allow_all) //Get the players who are ready - for(var/i in GLOB.new_player_list) - var/mob/dead/new_player/player = i - if(player.ready == PLAYER_READY_TO_PLAY && player.check_preferences() && player.mind && is_unassigned_job(player.mind.assigned_role)) + for(var/mob/dead/new_player/player as anything in GLOB.new_player_list) + if(player.ready == PLAYER_READY_TO_PLAY && player.check_job_preferences(!pure) && player.mind && is_unassigned_job(player.mind.assigned_role)) unassigned += player initial_players_to_assign = length(unassigned) @@ -710,9 +710,10 @@ SUBSYSTEM_DEF(job) return 0 /datum/controller/subsystem/job/proc/try_reject_player(mob/dead/new_player/player) - if(player.mind && player.mind.special_role) - job_debug("RJCT: Player unable to be rejected due to special_role, Player: [player], SpecialRole: [player.mind.special_role]") - return FALSE + for(var/datum/dynamic_ruleset/roundstart/ruleset in SSdynamic.queued_rulesets) + if(player.mind in ruleset.selected_minds) + 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]") unassigned -= player @@ -884,11 +885,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 /// or making sure roles critical to round progression exist where possible every shift. /datum/controller/subsystem/job/proc/assign_priority_positions() - job_debug("APP: Assigning Dynamic ruleset forced occupations: [length(dynamic_forced_occupations)]") - for(var/mob/new_player in dynamic_forced_occupations) + job_debug("APP: Assigning Dynamic ruleset forced occupations: [LAZYLEN(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. // 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. job_debug("APP: Assigning all JP_HIGH head of staff roles.") @@ -953,7 +955,7 @@ SUBSYSTEM_DEF(job) job_debug("[debug_prefix]: Player has no mind, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") 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]" : ""]") return JOB_UNAVAILABLE_ANTAG_INCOMPAT @@ -972,7 +974,8 @@ SUBSYSTEM_DEF(job) return JOB_UNAVAILABLE_BANNED // 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]" : ""]") return JOB_UNAVAILABLE_AGE diff --git a/code/controllers/subsystem/polling.dm b/code/controllers/subsystem/polling.dm index 99b179af6e7..dccb826a12f 100644 --- a/code/controllers/subsystem/polling.dm +++ b/code/controllers/subsystem/polling.dm @@ -285,11 +285,10 @@ SUBSYSTEM_DEF(polling) if(the_ignore_category) if(potential_candidate.ckey in GLOB.poll_ignore[the_ignore_category]) return FALSE - if(role) + if(role && potential_candidate.client) if(!(role in potential_candidate.client.prefs.be_special)) return FALSE - var/required_time = GLOB.special_roles[role] || 0 - if(potential_candidate.client && potential_candidate.client.get_remaining_days(required_time) > 0) + if(potential_candidate.client.get_days_to_play_antag(role) > 0) return FALSE if(check_jobban) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index fc58389340b..6625dfd3ba9 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -241,7 +241,7 @@ SUBSYSTEM_DEF(ticker) return TRUE if(GLOB.station_was_nuked) return TRUE - if(GLOB.revolutionary_win) + if(GLOB.revolution_handler?.result == REVOLUTION_VICTORY) return TRUE return FALSE @@ -252,7 +252,7 @@ SUBSYSTEM_DEF(ticker) CHECK_TICK //Configure mode and assign player to antagonists var/can_continue = FALSE -// can_continue = SSdynamic.pre_setup() //Choose antagonists // BUBBER EDIT - STORYTELLER (note: maybe disable) +// can_continue = SSdynamic.select_roundstart_antagonists() //Choose antagonists // BUBBER EDIT - STORYTELLER (note: maybe disable) //BUBBER EDIT BEGIN - STORYTELLER SSgamemode.init_storyteller() can_continue = SSgamemode.pre_setup() @@ -325,7 +325,37 @@ SUBSYSTEM_DEF(ticker) /datum/controller/subsystem/ticker/proc/PostSetup() 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) + SSgamemode.post_setup() // BUBBER EDIT - Storyteller GLOB.start_state = new /datum/station_state() GLOB.start_state.count() @@ -355,11 +385,97 @@ SUBSYSTEM_DEF(ticker) if(!iter_human.hardcore_survival_score) 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!")) else 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 += "[L.name] ([L.key]), the [L.job] (Disconnected)\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 += "[L.name] ([L.key]), the [L.job] (Connected, Inactive)\n" + failed = TRUE //AFK client + if(!failed && L.stat) + if(HAS_TRAIT(L, TRAIT_SUICIDED)) //Suicider + msg += "[L.name] ([L.key]), the [L.job] ([span_bolddanger("Suicide")])\n" + failed = TRUE //Disconnected client + if(!failed && (L.stat == UNCONSCIOUS || L.stat == HARD_CRIT)) + msg += "[L.name] ([L.key]), the [L.job] (Dying)\n" + failed = TRUE //Unconscious + if(!failed && L.stat == DEAD) + msg += "[L.name] ([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 += "[L.name] ([ckey(D.mind.key)]), the [L.job] ([span_bolddanger("Suicide")])\n" + continue //Disconnected client + else + msg += "[L.name] ([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 += "[L.name] ([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 = {" + [command_name()] Human Resources Board
+ Notice of Personnel Change

+ To personnel management staff aboard [station_name()]:

+ Our medical staff have detected a series of anomalies in the vital sensors + of some of the staff aboard your station.

+ 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.

+ 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.)

+ The following positions have been reopened on our behalf:

+ [reopened_job_report_positions]
+ "} + + print_command_report(suicide_command_report, "Central Command Personnel Update") + //These callbacks will fire after roundstart key transfer /datum/controller/subsystem/ticker/proc/OnRoundstart(datum/callback/cb) if(!HasRoundStarted()) diff --git a/code/datums/ai_laws/ai_laws.dm b/code/datums/ai_laws/ai_laws.dm index 3b3848f3bf0..a472ef04f50 100644 --- a/code/datums/ai_laws/ai_laws.dm +++ b/code/datums/ai_laws/ai_laws.dm @@ -261,11 +261,11 @@ GLOBAL_VAR(round_default_lawset) return FALSE // 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 if (isAI(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 zeroth = null diff --git a/code/datums/communications.dm b/code/datums/communications.dm index c24602e4a26..ce598f11f6c 100644 --- a/code/datums/communications.dm +++ b/code/datums/communications.dm @@ -20,6 +20,11 @@ GLOBAL_DATUM_INIT(communications_controller, /datum/communciations_controller, n /// The location where the special xenomorph egg was planted 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) if(is_silicon && COOLDOWN_FINISHED(src, silicon_message_cooldown)) return TRUE @@ -58,6 +63,75 @@ GLOBAL_DATUM_INIT(communications_controller, /datum/communciations_controller, n printed_paper.add_raw_text(sending.content) 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 + + . = "Nanotrasen Department of Intelligence Threat Advisory, Spinward Sector, TCD [time2text(world.realtime, "DDD, MMM DD")], [CURRENT_STATION_YEAR]:
" + . += 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("
Special Orders for [station_name()]:
") + for(var/datum/station_goal/station_goal as anything in goals) + station_goal.on_report() + texts += station_goal.get_report() + . += texts.Join("
") + + 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()]
" + if(trait_list_strings.len > 0) + . += "
Identified shift divergencies:
" + 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]
" + footnote_pile += "[footnote.signature]
" + footnote_pile += "
" + + . += "
Additional Notes:

" + 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_AI #undef COMMUNICATION_COOLDOWN_MEETING diff --git a/code/datums/mind/_mind.dm b/code/datums/mind/_mind.dm index ad05f409ff6..5bbc69f6101 100644 --- a/code/datums/mind/_mind.dm +++ b/code/datums/mind/_mind.dm @@ -48,8 +48,6 @@ /// 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/special_role - var/list/restricted_roles = list() /// List of antag datums on this mind 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. 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 ///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 .["antag_datums"] = antag_datums .["holy_role"] = holy_role - .["special_role"] = special_role + .["special_role"] = jointext(get_special_roles(), " | ") .["assigned_role"] = assigned_role.title .["current"] = current @@ -492,16 +492,6 @@ var/datum/addiction/affected_addiction = SSaddiction.all_addictions[type] 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. /datum/mind/proc/set_assigned_role(datum/job/new_role) if(assigned_role == new_role) diff --git a/code/datums/mind/antag.dm b/code/datums/mind/antag.dm index 7f65cb8fdc7..9f1efc3b154 100644 --- a/code/datums/mind/antag.dm +++ b/code/datums/mind/antag.dm @@ -61,37 +61,6 @@ return TRUE 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() if(!current) return @@ -227,7 +196,7 @@ current.log_message("has been enslaved to [key_name(creator)].", LOG_GAME) 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.") 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.")) @@ -274,33 +243,12 @@ /datum/mind/proc/take_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() if(has_antag_datum(/datum/antagonist/wizard)) return set_assigned_role(SSjob.get_job_type(/datum/job/space_wizard)) - special_role = ROLE_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. /datum/mind/proc/get_hijack_speed() . = 0 diff --git a/code/datums/mind/initialization.dm b/code/datums/mind/initialization.dm index a09cb040dbe..d40fa3d8759 100644 --- a/code/datums/mind/initialization.dm +++ b/code/datums/mind/initialization.dm @@ -36,7 +36,6 @@ /mob/living/silicon/pai/mind_initialize() . = ..() 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. /mob/proc/ghost_before_admin_delete(datum/source) diff --git a/code/datums/proximity_monitor/fields/heretic_arena.dm b/code/datums/proximity_monitor/fields/heretic_arena.dm index 1f9d85094df..fc89fe824d1 100644 --- a/code/datums/proximity_monitor/fields/heretic_arena.dm +++ b/code/datums/proximity_monitor/fields/heretic_arena.dm @@ -272,7 +272,7 @@ GLOBAL_LIST_EMPTY(heretic_arenas) replace_banned = FALSE objectives = list() antag_hud_name = "brainwashed" - block_midrounds = FALSE + antag_flags = ANTAG_FAKE /datum/antagonist/heretic_arena_participant/on_gain() forge_objectives() diff --git a/code/datums/station_traits/_station_trait.dm b/code/datums/station_traits/_station_trait.dm index be2f14dea39..7073b9d0bd2 100644 --- a/code/datums/station_traits/_station_trait.dm +++ b/code/datums/station_traits/_station_trait.dm @@ -33,10 +33,6 @@ GLOBAL_LIST_EMPTY(lobby_station_traits) 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 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 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)) - if(threat_reduction) - GLOB.dynamic_station_traits[src] = threat_reduction - if(dynamic_category) - GLOB.dynamic_ruleset_categories = dynamic_category /* BUBBER EDIT REMOVAL - We have our own lobby buttons if(sign_up_button) GLOB.lobby_station_traits += src @@ -64,7 +56,6 @@ GLOBAL_LIST_EMPTY(lobby_station_traits) destroy_lobby_buttons() SSstation.station_traits -= src GLOB.lobby_station_traits -= src - GLOB.dynamic_station_traits -= src REMOVE_TRAIT(SSstation, trait_to_give, STATION_TRAIT) return ..() diff --git a/code/datums/station_traits/job_traits.dm b/code/datums/station_traits/job_traits.dm index 05774b5345b..f42a86031dc 100644 --- a/code/datums/station_traits/job_traits.dm +++ b/code/datums/station_traits/job_traits.dm @@ -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 * 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 /// What tooltip to show on the button 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? var/position_amount = 1 /// Type of job to enable @@ -22,11 +16,6 @@ /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 RegisterSignal(SSdcs, COMSIG_GLOB_PRE_JOBS_ASSIGNED, PROC_REF(pre_jobs_assigned)) /* SKYRAT EDIT REMOVAL @@ -86,7 +75,6 @@ button_desc = "Sign up to become the Cargo Gorilla, a peaceful shepherd of boxes." weight = 1 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 /datum/station_trait/job/cargorilla/New() @@ -120,7 +108,6 @@ weight = 2 report_message = "We have installed a Bridge Assistant on your station." show_in_report = TRUE - can_roll_antag = CAN_ROLL_PROTECTED job_to_add = /datum/job/bridge_assistant /datum/station_trait/job/bridge_assistant/New() @@ -174,7 +161,6 @@ weight = 2 report_message = "Veteran Security Advisor has been assigned to your station to help with Security matters." show_in_report = TRUE - can_roll_antag = CAN_ROLL_PROTECTED job_to_add = /datum/job/veteran_advisor /* SKYRAT EDIT -- REMOVAL -- We handle the lobby a bit differently for time being @@ -190,7 +176,6 @@ 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." show_in_report = TRUE - can_roll_antag = CAN_ROLL_PROTECTED job_to_add = /datum/job/human_ai trait_to_give = STATION_TRAIT_HUMAN_AI @@ -258,7 +243,6 @@ 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." show_in_report = TRUE - can_roll_antag = CAN_ROLL_ALWAYS job_to_add = /datum/job/pun_pun /datum/station_trait/job/pun_pun/New() @@ -274,7 +258,3 @@ . = ..() 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 diff --git a/code/datums/station_traits/negative_traits.dm b/code/datums/station_traits/negative_traits.dm index 848025cfe81..4432ff599fd 100644 --- a/code/datums/station_traits/negative_traits.dm +++ b/code/datums/station_traits/negative_traits.dm @@ -559,7 +559,6 @@ trait_to_give = STATION_TRAIT_RADIOACTIVE_NEBULA blacklist = list(/datum/station_trait/random_event_weight_modifier/rad_storms) - threat_reduction = 30 dynamic_threat_id = "Radioactive Nebula" intensity_increment_time = 10 MINUTES // BUBBER EDIT: ORG - 5 MINUTES diff --git a/code/datums/station_traits/neutral_traits.dm b/code/datums/station_traits/neutral_traits.dm index 8d785c7e0a1..7e2375eb12e 100644 --- a/code/datums/station_traits/neutral_traits.dm +++ b/code/datums/station_traits/neutral_traits.dm @@ -501,16 +501,27 @@ /// 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 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 weight = 1 show_in_report = TRUE can_revert = FALSE - dynamic_category = RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS - threat_reduction = 15 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 /* BUBBER EDIT REMOVAL BEGIN - Lobby buttons /datum/station_trait/pet_day name = "Bring Your Pet To Work Day" diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm index bc8bed1340a..e8d3db7694b 100644 --- a/code/game/gamemodes/objective.dm +++ b/code/game/gamemodes/objective.dm @@ -241,7 +241,7 @@ GLOBAL_LIST_EMPTY(objectives) //SKYRAT EDIT ADDITION /datum/objective/assassinate/update_explanation_text() ..() if(target?.current) - explanation_text = "Assassinate [target.name], the [!target_role_type ? target.assigned_role.title : target.special_role] ONCE." //SKYRAT EDIT CHANGE + explanation_text = "Assassinate [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())] ONCE." // BUBBER EDIT ADDITION - Add ONCE else explanation_text = "Free objective." @@ -291,7 +291,7 @@ GLOBAL_LIST_EMPTY(objectives) //SKYRAT EDIT ADDITION /datum/objective/mutiny/update_explanation_text() ..() 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 explanation_text = "Free objective." @@ -313,7 +313,7 @@ GLOBAL_LIST_EMPTY(objectives) //SKYRAT EDIT ADDITION /datum/objective/maroon/update_explanation_text() 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 explanation_text = "Free objective." @@ -344,7 +344,7 @@ GLOBAL_LIST_EMPTY(objectives) //SKYRAT EDIT ADDITION /datum/objective/debrain/update_explanation_text() ..() 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 explanation_text = "Free objective." @@ -370,7 +370,7 @@ GLOBAL_LIST_EMPTY(objectives) //SKYRAT EDIT ADDITION /datum/objective/protect/update_explanation_text() ..() 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 explanation_text = "Free objective." @@ -395,7 +395,7 @@ GLOBAL_LIST_EMPTY(objectives) //SKYRAT EDIT ADDITION /datum/objective/jailbreak/update_explanation_text() ..() 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 explanation_text = "Free objective." @@ -411,7 +411,7 @@ GLOBAL_LIST_EMPTY(objectives) //SKYRAT EDIT ADDITION /datum/objective/jailbreak/detain/update_explanation_text() ..() 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 explanation_text = "Free objective." diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm index 7e74246ab3c..de0c1c54d26 100755 --- a/code/game/machinery/computer/communications.dm +++ b/code/game/machinery/computer/communications.dm @@ -856,7 +856,6 @@ GLOBAL_VAR_INIT(cops_arrived, FALSE) #define HACK_PIRATE "Pirates" #define HACK_FUGITIVES "Fugitives" #define HACK_SLEEPER "Sleeper Agents" -#define HACK_THREAT "Threat Boost" /// The minimum number of ghosts / observers to have the chance of spawning pirates. #define MIN_GHOSTS_FOR_PIRATES 4 @@ -900,7 +899,7 @@ GLOBAL_VAR_INIT(cops_arrived, FALSE) */ /obj/machinery/computer/communications/proc/hack_console(mob/living/hacker) // 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 var/num_ghosts = length(GLOB.current_observers_list) + length(GLOB.dead_player_list) @@ -915,11 +914,6 @@ GLOBAL_VAR_INIT(cops_arrived, FALSE) if(num_ghosts >= MIN_GHOSTS_FOR_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) 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) @@ -927,59 +921,29 @@ GLOBAL_VAR_INIT(cops_arrived, FALSE) if(HACK_PIRATE) // Triggers pirates, which the crew may be able to pay off to prevent var/list/pirate_rulesets = list( /datum/dynamic_ruleset/midround/pirates, - /datum/dynamic_ruleset/midround/dangerous_pirates, + /datum/dynamic_ruleset/midround/pirates/heavy, ) - priority_announce( - "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) + SSdynamic.force_run_midround(pick(pirate_rulesets)) if(HACK_FUGITIVES) // Triggers fugitives, which can cause confusion / chaos as the crew decides which side help priority_announce( "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", ) - - 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() + SSdynamic.force_run_midround(/datum/dynamic_ruleset/midround/from_ghosts/fugitives) 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 - var/max_number_of_sleepers = clamp(round(length(GLOB.alive_player_list) / 20), 1, 3) - var/num_agents_created = 0 - 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)) - break - num_agents_created++ - - 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", - ) + 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", + ) + var/max_number_of_sleepers = clamp(round(length(GLOB.alive_player_list) / 40), 1, 3) + if(!SSdynamic.force_run_midround(/datum/dynamic_ruleset/midround/from_living/traitor, forced_max_cap = max_number_of_sleepers)) + SSdynamic.queue_ruleset(/datum/dynamic_ruleset/latejoin/traitor) #undef HACK_PIRATE #undef HACK_FUGITIVES #undef HACK_SLEEPER -#undef HACK_THREAT #undef MIN_GHOSTS_FOR_PIRATES #undef MIN_GHOSTS_FOR_FUGITIVES diff --git a/code/game/machinery/wishgranter.dm b/code/game/machinery/wishgranter.dm index 7d03fc8efae..6bfbb57d411 100644 --- a/code/game/machinery/wishgranter.dm +++ b/code/game/machinery/wishgranter.dm @@ -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.")) 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.")) else if (!insisting) diff --git a/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm b/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm index 9a33e16f5db..e59116f9688 100644 --- a/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm +++ b/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm @@ -54,7 +54,6 @@ if(isnull(chosen_one)) return pyro.PossessByPlayer(chosen_one.key) - pyro.mind.special_role = ROLE_PYROCLASTIC_SLIME pyro.mind.add_antag_datum(/datum/antagonist/pyro_slime) pyro.log_message("was made into a slime by pyroclastic anomaly", LOG_GAME) chosen_one = null diff --git a/code/game/objects/effects/poster_motivational.dm b/code/game/objects/effects/poster_motivational.dm index 377fa271211..adee497096d 100644 --- a/code/game/objects/effects/poster_motivational.dm +++ b/code/game/objects/effects/poster_motivational.dm @@ -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 /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 ..() balloon_alert(user, "converting poster...") if(!do_after(user, 5 SECONDS, user)) @@ -32,7 +32,7 @@ /// Screentip for the above /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 context[SCREENTIP_CONTEXT_LMB] = "Turn into Demoralizing Poster" return CONTEXTUAL_SCREENTIP_SET diff --git a/code/game/objects/items/devices/aicard_evil.dm b/code/game/objects/items/devices/aicard_evil.dm index f4c89c31444..d59df54073a 100644 --- a/code/game/objects/items/devices/aicard_evil.dm +++ b/code/game/objects/items/devices/aicard_evil.dm @@ -57,7 +57,7 @@ var/datum/antagonist/nukeop/nuke_datum = new() nuke_datum.send_to_spawnpoint = FALSE 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 // Make it look evil!!! new_ai.hologram_appearance = mutable_appearance('icons/mob/silicon/ai.dmi',"xeno_queen") //good enough diff --git a/code/game/objects/items/religion.dm b/code/game/objects/items/religion.dm index 95746907440..bba45718362 100644 --- a/code/game/objects/items/religion.dm +++ b/code/game/objects/items/religion.dm @@ -49,7 +49,7 @@ if(H.mind && (has_job_loyalties || has_role_loyalties)) if(has_job_loyalties && (H.mind.assigned_role.departments_bitflags & job_loyalties)) 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 else if(check_inspiration(H)) inspired += H diff --git a/code/game/objects/items/robot/robot_parts.dm b/code/game/objects/items/robot/robot_parts.dm index 631775bcb04..5dee6df4308 100644 --- a/code/game/objects/items/robot/robot_parts.dm +++ b/code/game/objects/items/robot/robot_parts.dm @@ -321,7 +321,7 @@ brainmob.mind.transfer_to(O) 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_danger("You must obey your silicon laws and master AI above all else. Your objectives will consider you to be dead.")) diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm index 86f73dcf609..dde40b22393 100644 --- a/code/game/objects/items/storage/uplink_kits.dm +++ b/code/game/objects/items/storage/uplink_kits.dm @@ -788,13 +788,7 @@ human_target.reagents.add_reagent(/datum/reagent/toxin, 2) return FALSE - /// If all the antag datums are 'fake' or none exist, disallow induction! No self-antagging. - 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 + if(!human_target.is_antag()) // 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...")) human_target.reagents.add_reagent(/datum/reagent/toxin, 15) return FALSE diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 99950d227dc..77d3b4d8a06 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -20,22 +20,9 @@ return var/dat - if(SSticker.current_state <= GAME_STATE_PREGAME) - // BUBBER EDIT START - STORYTELLER - //dat += "(Manage Dynamic Rulesets)
" - //dat += "(Force Roundstart Rulesets)
" - dat += "(Game Mode Panel)
" - // BUBBER EDIT END - if (GLOB.dynamic_forced_roundstart_ruleset.len > 0) - for(var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset) - dat += {"-> [rule.name] <-
"} - dat += "(Clear Rulesets)
" - dat += "(Dynamic mode options)
" - dat += "
" - if(SSticker.IsRoundInProgress()) - dat += "(Game Mode Panel)
" - //dat += "(Manage Dynamic Rulesets)
" // BUBBER EDIT - STORYTELLER - dat += "
" + + dat += "Dynamic Panel
" + dat += "
" dat += {" Create Object
Quick Create Object
@@ -108,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)]") BLACKBOX_LOG_ADMIN_VERB("Spawn Cargo") -/datum/admins/proc/dynamic_mode_options(mob/user) - var/dat = {"

Common options

- All these options can be changed midround.
-
- Force extended: - Option is [GLOB.dynamic_forced_extended ? "ON" : "OFF"]. -
This will force the round to be extended. No rulesets will be drafted.
-
- No stacking: - Option is [GLOB.dynamic_no_stacking ? "ON" : "OFF"]. -
Unless the threat goes above [GLOB.dynamic_stacking_limit], only one "round-ender" ruleset will be drafted.
-
- Forced threat level: Current value : [GLOB.dynamic_forced_threat_level]. -
The value threat is set to if it is higher than -1.
-
-
- Stacking threeshold: Current value : [GLOB.dynamic_stacking_limit]. -
The threshold at which "round-ender" rulesets will stack. A value higher than 100 ensure this never happens.
- "} - - 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.
- Disabled rulesets will never run, even if they would otherwise be valid.
- 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).
- force enable all - force disable all - reset all - "} - - 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 = "

[title]

" - 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 += "" - dat += "
[initial(rule.name)]\[ [forced] \] \ - force enabled \ - force disabled \ - reset
" - return dat - -/datum/admins/proc/dynamic_ruleset_category_during_round_display(title, list/rules, pop_count, threat_level) - var/dat = "

[title]

" - 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 += {" - - - "} - dat += "
[rule.name]\[ Weight: [rule.weight] \] - \[ [active][explanation] \] - force enabled - force disabled - resetVV
" - 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) create_area(user.mob) @@ -308,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)) message_admins("Admin login: [key_name(src)]") return - diff --git a/code/modules/admin/antag_panel.dm b/code/modules/admin/antag_panel.dm index 453113f885e..d06a583c515 100644 --- a/code/modules/admin/antag_panel.dm +++ b/code/modules/admin/antag_panel.dm @@ -75,6 +75,9 @@ GLOBAL_VAR(antag_prototypes) common_commands += "Toggle exploitables override" //SKYRAT EDIT ADDITION -- EXPLOITABLES return common_commands +/** + * Returns a list of "statuses" this mind has - like "Infected", "Mindshielded", etc + */ /datum/mind/proc/get_special_statuses() var/list/result = LAZYCOPY(special_statuses) if(!current) @@ -83,12 +86,18 @@ GLOBAL_VAR(antag_prototypes) result += span_good("Mindshielded") if(current && HAS_MIND_TRAIT(current, TRAIT_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)) var/mob/living/silicon/robot/robot = current if (robot.emagged) - result += span_bad("Emagged") - return result.Join(" | ") + roles += "Emagged" + return roles /datum/mind/proc/traitor_panel() if(!SSticker.HasRoundStarted()) @@ -104,12 +113,11 @@ GLOBAL_VAR(antag_prototypes) out += "Antag Tickets: [current.client.get_antag_tickets()] (Current multiplier: x[current.client.antag_ticket_multiplier()])
" //BUBBERSTATION CHANGE END: ANTAG TICKET INTEGRAATION out += "Assigned role: [assigned_role.title]. Edit
" - out += "Faction and special role: [special_role]
" out += "Show Teams

" - var/special_statuses = get_special_statuses() + var/special_statuses = get_special_roles() | get_special_statuses() if(length(special_statuses)) - out += get_special_statuses() + "
" + out += "Roles: [jointext(special_statuses, " | ")]
" if(!GLOB.antag_prototypes) GLOB.antag_prototypes = list() @@ -171,7 +179,7 @@ GLOBAL_VAR(antag_prototypes) continue pref_source = prototype 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" //Traitor : None | Traitor | IAA diff --git a/code/modules/admin/check_antagonists.dm b/code/modules/admin/check_antagonists.dm index ee5ec587b04..9c176dc0613 100644 --- a/code/modules/admin/check_antagonists.dm +++ b/code/modules/admin/check_antagonists.dm @@ -95,7 +95,7 @@ tgui_alert(usr, "The game hasn't started yet!") return var/list/dat = list("Round Status

Round Status

") - dat += "Game Mode Panel
" + dat += "Dynamic Panel
" dat += "Round Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]
" dat += "Emergency shuttle
" if(EMERGENCY_IDLE_OR_RECALLED) @@ -152,7 +152,7 @@ if (checked_mob.client) observers_connected++ - if(checked_mob.mind.special_role) + if(checked_mob.is_antag()) antagonists++ if(checked_mob.stat == DEAD) antagonists_dead++ diff --git a/code/modules/admin/player_panel.dm b/code/modules/admin/player_panel.dm index c6cd2387cc6..714cb41a9ed 100644 --- a/code/modules/admin/player_panel.dm +++ b/code/modules/admin/player_panel.dm @@ -228,7 +228,7 @@ var/color = "#e6e6e6" if(i%2 == 0) color = "#f2f2f2" - var/is_antagonist = is_special_character(M, allow_fake_antags = TRUE) + var/is_antagonist = M.is_antag(NONE) var/M_job = "" diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 837f3bcaf49..987b795cb8d 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -111,10 +111,9 @@ edit_rights_topic(href_list) else if(href_list["gamemode_panel"]) - if(!check_rights(R_ADMIN)) - return - //SSdynamic.admin_panel() // BUBBER EDIT - STORYTELLER + // dynamic_panel(usr) // BUBBER EDIT - STORYTELLER SSgamemode.ui_interact(usr) // BUBBER EDIT - STORYTELLER + else if(href_list["call_shuttle"]) if(!check_rights(R_ADMIN)) return @@ -431,122 +430,6 @@ return 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"]) if(!check_rights(R_FUN)) return diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm index a07bd889e98..0f7e2eed297 100644 --- a/code/modules/admin/verbs/admingame.dm +++ b/code/modules/admin/verbs/admingame.dm @@ -269,33 +269,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) new_character.mind.give_uplink(silent = TRUE, antag_datum = traitordatum) - switch(new_character.mind.special_role) - if(ROLE_WIZARD) - new_character.forceMove(pick(GLOB.wizardstart)) - var/datum/antagonist/wizard/A = new_character.mind.has_antag_datum(/datum/antagonist/wizard,TRUE) - A.equip_wizard() - 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)) + var/skip_job_respawn = FALSE + for(var/datum/antagonist/antag as anything in new_character.mind.antag_datums) + skip_job_respawn ||= antag.on_respawn(new_character) + if(skip_job_respawn) + break - else//They may also be a cyborg or AI. - switch(new_character.mind.assigned_role.type) - 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. + if(!skip_job_respawn) + new_character.mind.assigned_role.on_respawn(new_character) //Announces the character on all the systems, based on the record. if(!record_found && (new_character.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index 652c55dc149..0ba41705a35 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -1082,9 +1082,7 @@ GLOBAL_DATUM_INIT(admin_help_ui_handler, /datum/admin_help_ui_handler, new) mobs_found += found if(!ai_found && isAI(found)) ai_found = 1 - var/is_antag = 0 - if(is_special_character(found)) - is_antag = 1 + var/is_antag = found.is_antag() founds += "Name: [found.name]([found.real_name]) Key: [found.key] Ckey: [found.ckey] [is_antag ? "(Antag)" : null] " msg += "[original_word](?|F) " continue diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index 7f93e0c57ce..bd8d131439d 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -10,18 +10,14 @@ GLOBAL_LIST_EMPTY(antagonists) var/roundend_category = "other antagonists" ///Set to false to hide the antagonists from roundend report 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 var/datum/mind/owner ///Silent will prevent the gain/lose texts to show var/silent = FALSE - ///Whether or not the person will be able to have more than one datum - var/can_coexist_with_others = TRUE - ///List of datums this type can't coexist with - var/list/typecache_datum_blacklist = list() - ///The define string we use to identify the role for bans/player polls to spawn a random new one in. - var/job_rank + /// What flag is checked for jobbans and polling? Optional, if unset, will use pref_flag + var/jobban_flag + /// What flag to check for prefs? Required for antags with preferences associated + var/pref_flag ///Should replace jobbanned player with ghosts if granted. 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. @@ -38,8 +34,6 @@ GLOBAL_LIST_EMPTY(antagonists) var/hud_icon = 'icons/mob/huds/antag_hud.dmi' ///Name of the antag hud we provide to this mob. 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. var/suicide_cry = "" //Antag panel properties @@ -63,8 +57,6 @@ GLOBAL_LIST_EMPTY(antagonists) var/hardcore_random_bonus = FALSE /// A path to the audio stinger that plays upon gaining this datum. var/stinger_sound - /// Whether this antag datum blocks rolling new antag datums - var/block_midrounds = TRUE //ANTAG UI @@ -78,7 +70,6 @@ GLOBAL_LIST_EMPTY(antagonists) /datum/antagonist/New() GLOB.antagonists += src - typecache_datum_blacklist = typecacheof(typecache_datum_blacklist) /datum/antagonist/Destroy() GLOB.antagonists -= src @@ -179,12 +170,7 @@ GLOBAL_LIST_EMPTY(antagonists) /datum/antagonist/proc/can_be_owned(datum/mind/new_owner) var/datum/mind/tested = new_owner || owner - if(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 + return !tested.has_antag_datum(type) //This will be called in add_antag_datum before owner assignment. //Should return antag datum without owner. @@ -203,7 +189,7 @@ GLOBAL_LIST_EMPTY(antagonists) info_button.Remove(old_body) info_button.Grant(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() //This handles the application of antag huds/special abilities @@ -271,12 +257,14 @@ GLOBAL_LIST_EMPTY(antagonists) replace_banned_player() 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() - 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() 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) + LAZYADD(owner.special_roles, (jobban_flag || pref_flag)) + SEND_SIGNAL(owner, COMSIG_ANTAGONIST_GAINED, src) /** @@ -293,7 +281,7 @@ GLOBAL_LIST_EMPTY(antagonists) if(!player.ckey) 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. @@ -301,7 +289,7 @@ GLOBAL_LIST_EMPTY(antagonists) /datum/antagonist/proc/replace_banned_player() 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) 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.") @@ -334,6 +322,7 @@ GLOBAL_LIST_EMPTY(antagonists) var/datum/team/team = get_team() if(team) team.remove_member(owner) + LAZYREMOVE(owner.special_roles, (jobban_flag || pref_flag)) SEND_SIGNAL(owner, COMSIG_ANTAGONIST_REMOVED, src) if(owner.current) SEND_SIGNAL(owner.current, COMSIG_MOB_ANTAGONIST_REMOVED, src) @@ -460,8 +449,8 @@ GLOBAL_LIST_EMPTY(antagonists) return "" /datum/antagonist/proc/enabled_in_preferences(datum/mind/noggin) - if(job_rank) - if(noggin.current && noggin.current.client && (job_rank in noggin.current.client.prefs.be_special)) + if(pref_flag) + if(noggin.current && noggin.current.client && (pref_flag in noggin.current.client.prefs.be_special)) return TRUE else return FALSE @@ -618,3 +607,7 @@ GLOBAL_LIST_EMPTY(antagonists) return TRUE #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 diff --git a/code/modules/antagonists/_common/antag_helpers.dm b/code/modules/antagonists/_common/antag_helpers.dm index 6ccaa80e55f..de3ad9865e7 100644 --- a/code/modules/antagonists/_common/antag_helpers.dm +++ b/code/modules/antagonists/_common/antag_helpers.dm @@ -10,9 +10,10 @@ . |= 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. +/// If playtime tracking is disabled, just returns the first player in the list. /proc/get_most_experienced(list/players, specific_role) if(!CONFIG_GET(flag/use_exp_tracking)) //woops - return + return players[1] var/most_experienced for(var/player in players) if(!most_experienced) diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm index bc03fc2c02c..f5958a127f4 100644 --- a/code/modules/antagonists/_common/antag_spawner.dm +++ b/code/modules/antagonists/_common/antag_spawner.dm @@ -91,7 +91,6 @@ master_wizard.wiz_team.add_member(app_mind) app_mind.add_antag_datum(app) 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')) ///////////BORGS AND OPERATIVES @@ -106,7 +105,7 @@ icon = 'icons/obj/devices/voice.dmi' icon_state = "nukietalkie" /// 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 var/datum/outfit/syndicate/outfit = /datum/outfit/syndicate/reinforcement /// The antag datum applied @@ -164,7 +163,7 @@ 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.special_role = special_role_name + LAZYADD(op_mind.special_roles, special_role_name) if(outfit) var/datum/antagonist/nukeop/nukie_datum = op_mind.has_antag_datum(antag_datum) @@ -250,7 +249,7 @@ borg.PossessByPlayer(C.key) 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) new /obj/effect/pod_landingzone(get_turf(src), pod) @@ -394,7 +393,7 @@ 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) @@ -466,4 +465,3 @@ internals_slot = NONE belt = /obj/item/lighter/skull r_hand = /obj/item/food/grown/banana - diff --git a/code/modules/antagonists/abductor/abductor.dm b/code/modules/antagonists/abductor/abductor.dm index 313c1b3a485..2bc2346aae8 100644 --- a/code/modules/antagonists/abductor/abductor.dm +++ b/code/modules/antagonists/abductor/abductor.dm @@ -2,7 +2,7 @@ name = "\improper Abductor" roundend_category = "abductors" antagpanel_category = ANTAG_GROUP_ABDUCTORS - job_rank = ROLE_ABDUCTOR + pref_flag = ROLE_ABDUCTOR antag_hud_name = "abductor" show_in_antagpanel = FALSE //should only show subtypes show_to_ghosts = TRUE @@ -71,7 +71,6 @@ /datum/antagonist/abductor/on_gain() owner.set_assigned_role(SSjob.get_job_type(role_job)) - owner.special_role = ROLE_ABDUCTOR objectives += team.objectives finalize_abductor() // We don't want abductors to be converted by other antagonists @@ -79,7 +78,6 @@ return ..() /datum/antagonist/abductor/on_removal() - owner.special_role = null owner.remove_traits(list(TRAIT_ABDUCTOR_TRAINING, TRAIT_UNCONVERTABLE), ABDUCTOR_ANTAGONIST) return ..() diff --git a/code/modules/antagonists/ashwalker/ashwalker.dm b/code/modules/antagonists/ashwalker/ashwalker.dm index 015677de5af..99f1e708f30 100644 --- a/code/modules/antagonists/ashwalker/ashwalker.dm +++ b/code/modules/antagonists/ashwalker/ashwalker.dm @@ -1,12 +1,11 @@ /datum/antagonist/ashwalker name = "\improper Ash Walker" - job_rank = ROLE_LAVALAND + pref_flag = ROLE_LAVALAND show_in_antagpanel = FALSE show_to_ghosts = TRUE - prevent_roundtype_conversion = FALSE antagpanel_category = ANTAG_GROUP_ASHWALKERS 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 /datum/antagonist/ashwalker/create_team(datum/team/ashwalkers/ashwalker_team) diff --git a/code/modules/antagonists/battlecruiser/battlecruiser.dm b/code/modules/antagonists/battlecruiser/battlecruiser.dm index 194034e31bb..3688091ba30 100644 --- a/code/modules/antagonists/battlecruiser/battlecruiser.dm +++ b/code/modules/antagonists/battlecruiser/battlecruiser.dm @@ -19,7 +19,7 @@ suicide_cry = "FOR THE SYNDICATE!!!" antag_hud_name = "battlecruiser_crew" antagpanel_category = ANTAG_GROUP_SYNDICATE - job_rank = ROLE_BATTLECRUISER_CREW + pref_flag = ROLE_BATTLECRUISER_CREW stinger_sound = 'sound/music/antag/ops.ogg' /// Team to place the crewmember on. var/datum/team/battlecruiser/battlecruiser_team @@ -39,7 +39,7 @@ /datum/antagonist/battlecruiser/captain name = "Battlecruiser Captain" antag_hud_name = "battlecruiser_lead" - job_rank = ROLE_BATTLECRUISER_CAPTAIN + pref_flag = ROLE_BATTLECRUISER_CAPTAIN /datum/antagonist/battlecruiser/create_team(datum/team/battlecruiser/team) if(!team) diff --git a/code/modules/antagonists/blob/blob_antag.dm b/code/modules/antagonists/blob/blob_antag.dm index bf2c65d634f..a2e07edaa2e 100644 --- a/code/modules/antagonists/blob/blob_antag.dm +++ b/code/modules/antagonists/blob/blob_antag.dm @@ -4,7 +4,7 @@ antagpanel_category = ANTAG_GROUP_BIOHAZARDS show_to_ghosts = TRUE show_in_antagpanel = FALSE - job_rank = ROLE_BLOB + pref_flag = ROLE_BLOB ui_name = "AntagInfoBlob" stinger_sound = 'sound/music/antag/blobalert.ogg' antag_hud_name = "blob" @@ -156,7 +156,7 @@ /datum/antagonist/blob/infection name = "\improper Blob Infection" show_in_antagpanel = TRUE - job_rank = ROLE_BLOB_INFECTION + pref_flag = ROLE_BLOB_INFECTION /datum/antagonist/blob/infection/get_preview_icon() var/icon/blob_icon = ..() diff --git a/code/modules/antagonists/brainwashing/brainwashing.dm b/code/modules/antagonists/brainwashing/brainwashing.dm index ebad949060a..716e2cb494f 100644 --- a/code/modules/antagonists/brainwashing/brainwashing.dm +++ b/code/modules/antagonists/brainwashing/brainwashing.dm @@ -29,14 +29,14 @@ /datum/antagonist/brainwashed name = "\improper Brainwashed Victim" - job_rank = ROLE_BRAINWASHED + pref_flag = ROLE_BRAINWASHED stinger_sound = 'sound/music/antag/brainwashed.ogg' roundend_category = "brainwashed victims" show_in_antagpanel = TRUE antag_hud_name = "brainwashed" antagpanel_category = ANTAG_GROUP_CREW show_name_in_check_antagonists = TRUE - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST ui_name = "AntagInfoBrainwashed" suicide_cry = "FOR... SOMEONE!!" diff --git a/code/modules/antagonists/brother/brother.dm b/code/modules/antagonists/brother/brother.dm index c3725212ceb..28fba0132a5 100644 --- a/code/modules/antagonists/brother/brother.dm +++ b/code/modules/antagonists/brother/brother.dm @@ -1,7 +1,7 @@ /datum/antagonist/brother name = "\improper Brother" antagpanel_category = "Brother" - job_rank = ROLE_BROTHER + pref_flag = ROLE_BROTHER var/special_role = ROLE_BROTHER antag_hud_name = "brother" hijack_speed = 0.5 @@ -15,6 +15,7 @@ /datum/antagonist/brother/create_team(datum/team/brother_team/new_team) if(!new_team) + team = new() return if(!istype(new_team)) stack_trace("Wrong team type passed to [type] initialization.") @@ -25,7 +26,6 @@ /datum/antagonist/brother/on_gain() objectives += team.objectives - owner.special_role = special_role finalize_brother() if (team.brothers_left <= 0) @@ -45,7 +45,6 @@ return ..() /datum/antagonist/brother/on_removal() - owner.special_role = null remove_conversion_skills() return ..() @@ -180,7 +179,7 @@ return brother_text /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() /datum/antagonist/brother/proc/finalize_brother() diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm index 288e935bc35..3c63bcb4cf3 100644 --- a/code/modules/antagonists/changeling/changeling.dm +++ b/code/modules/antagonists/changeling/changeling.dm @@ -5,7 +5,7 @@ name = "\improper Changeling" roundend_category = "changelings" antagpanel_category = "Changeling" - job_rank = ROLE_CHANGELING + pref_flag = ROLE_CHANGELING antag_moodlet = /datum/mood_event/focused antag_hud_name = "changeling" hijack_speed = 0.5 @@ -1157,7 +1157,7 @@ name = "\improper Headslug Changeling" show_in_antagpanel = FALSE give_objectives = FALSE - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_SKIP_GLOBAL_LIST genetic_points = 5 total_genetic_points = 5 diff --git a/code/modules/antagonists/changeling/fallen_changeling.dm b/code/modules/antagonists/changeling/fallen_changeling.dm index c44c1b66cd3..9114886df29 100644 --- a/code/modules/antagonists/changeling/fallen_changeling.dm +++ b/code/modules/antagonists/changeling/fallen_changeling.dm @@ -3,11 +3,10 @@ name = "\improper Fallen Changeling" roundend_category = "changelings" antagpanel_category = "Changeling" - job_rank = ROLE_CHANGELING + pref_flag = ROLE_CHANGELING antag_moodlet = /datum/mood_event/fallen_changeling antag_hud_name = "changeling" /datum/mood_event/fallen_changeling description = "My powers! Where are my powers?!" mood_change = -4 - diff --git a/code/modules/antagonists/clown_ops/clownop.dm b/code/modules/antagonists/clown_ops/clownop.dm index e1e9b313f45..1b64eeab7ba 100644 --- a/code/modules/antagonists/clown_ops/clownop.dm +++ b/code/modules/antagonists/clown_ops/clownop.dm @@ -4,6 +4,7 @@ roundend_category = "clown operatives" antagpanel_category = ANTAG_GROUP_CLOWNOPS nukeop_outfit = /datum/outfit/syndicate/clownop + job_type = /datum/job/nuclear_operative/clown_operative suicide_cry = "HAPPY BIRTHDAY!!" preview_outfit = /datum/outfit/clown_operative_elite @@ -11,7 +12,6 @@ nuke_icon_state = "bananiumbomb_base" /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) 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)].") @@ -33,11 +33,12 @@ ADD_TRAIT(liver, TRAIT_COMEDY_METABOLISM, CLOWNOP_TRAIT) /datum/antagonist/nukeop/leader/clownop/give_alias() - title = pick("Head Honker", "Slipmaster", "Clown King", "Honkbearer") - if(nuke_team?.syndicate_name) - owner.current.real_name = "[nuke_team.syndicate_name] [title]" + title ||= pick("Head Honker", "Slipmaster", "Clown King", "Honkbearer") + . = ..() + if(ishuman(owner.current)) + owner.current.fully_replace_character_name(owner.current.real_name, "[title] [owner.current.real_name]") 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 name = "Clown Operative Leader" diff --git a/code/modules/antagonists/cult/datums/cultist.dm b/code/modules/antagonists/cult/datums/cultist.dm index 234949084dc..ba6678e53ff 100644 --- a/code/modules/antagonists/cult/datums/cultist.dm +++ b/code/modules/antagonists/cult/datums/cultist.dm @@ -5,7 +5,7 @@ antag_moodlet = /datum/mood_event/cult suicide_cry = "FOR NAR'SIE!!" preview_outfit = /datum/outfit/cultist - job_rank = ROLE_CULTIST + pref_flag = ROLE_CULTIST antag_hud_name = "cult" stinger_sound = 'sound/music/antag/bloodcult/bloodcult_gain.ogg' diff --git a/code/modules/antagonists/cult/runes.dm b/code/modules/antagonists/cult/runes.dm index bda737f4912..4c781158da5 100644 --- a/code/modules/antagonists/cult/runes.dm +++ b/code/modules/antagonists/cult/runes.dm @@ -321,7 +321,6 @@ structure_check() searches for nearby cultist structures required for the invoca convertee.Unconscious(10 SECONDS) 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) to_chat(convertee, span_cult_bold_italic("Your blood pulses. Your head throbs. The world goes red. \ @@ -1219,7 +1218,7 @@ GLOBAL_VAR_INIT(narsie_summon_count, 0) force_event_async(/datum/round_event_control/meteor_wave, "an apocalypse rune") 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) force_event_async(/datum/round_event_control/anomaly/anomaly_flux, "an apocalypse rune") diff --git a/code/modules/antagonists/ert/ert.dm b/code/modules/antagonists/ert/ert.dm index 45c7bd1c957..3ea73859527 100644 --- a/code/modules/antagonists/ert/ert.dm +++ b/code/modules/antagonists/ert/ert.dm @@ -11,9 +11,8 @@ antag_moodlet = /datum/mood_event/focused antagpanel_category = ANTAG_GROUP_ERT suicide_cry = "FOR NANOTRASEN!!" - count_against_dynamic_roll_chance = FALSE // 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/leader = FALSE var/datum/outfit/outfit = /datum/outfit/centcom/ert/security diff --git a/code/modules/antagonists/evil_clone/evil_clone.dm b/code/modules/antagonists/evil_clone/evil_clone.dm index 3d6521d97da..ef8455c3574 100644 --- a/code/modules/antagonists/evil_clone/evil_clone.dm +++ b/code/modules/antagonists/evil_clone/evil_clone.dm @@ -2,12 +2,12 @@ /datum/antagonist/evil_clone name = "\improper Evil Clone" stinger_sound = 'sound/music/antag/hypnotized.ogg' - job_rank = ROLE_EVIL_CLONE + pref_flag = ROLE_EVIL_CLONE roundend_category = "evil clones" show_in_antagpanel = TRUE antagpanel_category = ANTAG_GROUP_CREW show_name_in_check_antagonists = TRUE - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_SKIP_GLOBAL_LIST /datum/antagonist/evil_clone/on_gain() if (owner.current) diff --git a/code/modules/antagonists/fugitive/fugitive.dm b/code/modules/antagonists/fugitive/fugitive.dm index 7f2cc4a9f3d..e42782bbf74 100644 --- a/code/modules/antagonists/fugitive/fugitive.dm +++ b/code/modules/antagonists/fugitive/fugitive.dm @@ -2,16 +2,14 @@ /datum/antagonist/fugitive name = "\improper Fugitive" roundend_category = "Fugitive" - job_rank = ROLE_FUGITIVE - silent = TRUE //greet called by the event + pref_flag = ROLE_FUGITIVE show_in_antagpanel = FALSE show_to_ghosts = TRUE antagpanel_category = ANTAG_GROUP_FUGITIVES - prevent_roundtype_conversion = FALSE antag_hud_name = "fugitive" suicide_cry = "FOR FREEDOM!!" preview_outfit = /datum/outfit/prisoner - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_SKIP_GLOBAL_LIST var/datum/team/fugitive/fugitive_team var/is_captured = FALSE var/backstory = "error" @@ -38,10 +36,14 @@ return fugitive_icon - /datum/antagonist/fugitive/on_gain() 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 var/datum/objective/survive = new /datum/objective @@ -49,9 +51,8 @@ survive.explanation_text = "Avoid capture from the fugitive hunters." objectives += survive -/datum/antagonist/fugitive/greet(back_story) +/datum/antagonist/fugitive/greet() . = ..() - backstory = back_story var/message = "" switch(backstory) if(FUGITIVE_BACKSTORY_PRISONER) diff --git a/code/modules/antagonists/fugitive/hunters/hunter.dm b/code/modules/antagonists/fugitive/hunters/hunter.dm index d65cdc75949..362d81e2540 100644 --- a/code/modules/antagonists/fugitive/hunters/hunter.dm +++ b/code/modules/antagonists/fugitive/hunters/hunter.dm @@ -6,10 +6,9 @@ show_in_antagpanel = FALSE show_to_ghosts = TRUE antagpanel_category = ANTAG_GROUP_HUNTERS - prevent_roundtype_conversion = FALSE antag_hud_name = "fugitive_hunter" suicide_cry = "FOR GLORY!!" - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_SKIP_GLOBAL_LIST var/datum/team/fugitive_hunters/hunter_team var/backstory = "error" diff --git a/code/modules/antagonists/greentext/greentext.dm b/code/modules/antagonists/greentext/greentext.dm index 7133066f856..32f624c6505 100644 --- a/code/modules/antagonists/greentext/greentext.dm +++ b/code/modules/antagonists/greentext/greentext.dm @@ -3,7 +3,7 @@ show_in_antagpanel = FALSE 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 - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST hardcore_random_bonus = TRUE /datum/antagonist/greentext/forge_objectives() diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm index b9472e0081c..b3c5a2e4494 100644 --- a/code/modules/antagonists/heretic/heretic_antag.dm +++ b/code/modules/antagonists/heretic/heretic_antag.dm @@ -18,7 +18,7 @@ antagpanel_category = "Heretic" ui_name = "AntagInfoHeretic" antag_moodlet = /datum/mood_event/heretics - job_rank = ROLE_HERETIC + pref_flag = ROLE_HERETIC antag_hud_name = "heretic" hijack_speed = 0.5 suicide_cry = "THE MANSUS SMILES UPON ME!!" diff --git a/code/modules/antagonists/heretic/heretic_monsters.dm b/code/modules/antagonists/heretic/heretic_monsters.dm index 2cb3dd3bfa4..53ca3b88aac 100644 --- a/code/modules/antagonists/heretic/heretic_monsters.dm +++ b/code/modules/antagonists/heretic/heretic_monsters.dm @@ -4,7 +4,7 @@ roundend_category = "Heretics" antagpanel_category = ANTAG_GROUP_HORRORS antag_moodlet = /datum/mood_event/heretics - job_rank = ROLE_HERETIC + pref_flag = ROLE_HERETIC antag_hud_name = "heretic_beast" suicide_cry = "MY MASTER SMILES UPON ME!!" show_in_antagpanel = FALSE diff --git a/code/modules/antagonists/heretic/soultrapped_heretic.dm b/code/modules/antagonists/heretic/soultrapped_heretic.dm index 13365316db1..5cb6112f798 100644 --- a/code/modules/antagonists/heretic/soultrapped_heretic.dm +++ b/code/modules/antagonists/heretic/soultrapped_heretic.dm @@ -3,7 +3,7 @@ name = "\improper Soultrapped Heretic" roundend_category = "Heretics" antagpanel_category = "Heretic" - job_rank = ROLE_HERETIC + pref_flag = ROLE_HERETIC antag_moodlet = /datum/mood_event/soultrapped_heretic antag_hud_name = "heretic" diff --git a/code/modules/antagonists/highlander/highlander.dm b/code/modules/antagonists/highlander/highlander.dm index fa2dcee4d36..5d381f623bd 100644 --- a/code/modules/antagonists/highlander/highlander.dm +++ b/code/modules/antagonists/highlander/highlander.dm @@ -5,7 +5,7 @@ show_name_in_check_antagonists = TRUE can_elimination_hijack = ELIMINATION_ENABLED 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. var/static/list/applicable_traits = list( TRAIT_NOBREATH, @@ -38,7 +38,6 @@ /datum/antagonist/highlander/on_gain() forge_objectives() - owner.special_role = "highlander" give_equipment() . = ..() diff --git a/code/modules/antagonists/hypnotized/hypnotized.dm b/code/modules/antagonists/hypnotized/hypnotized.dm index bde67390bf7..0a78f2edc77 100644 --- a/code/modules/antagonists/hypnotized/hypnotized.dm +++ b/code/modules/antagonists/hypnotized/hypnotized.dm @@ -2,14 +2,14 @@ /datum/antagonist/hypnotized name = "\improper Hypnotized Victim" stinger_sound = 'sound/music/antag/hypnotized.ogg' - job_rank = ROLE_HYPNOTIZED + pref_flag = ROLE_HYPNOTIZED roundend_category = "hypnotized victims" antag_hud_name = "brainwashed" ui_name = "AntagInfoBrainwashed" show_in_antagpanel = TRUE antagpanel_category = ANTAG_GROUP_CREW 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 var/datum/brain_trauma/hypnosis/trauma diff --git a/code/modules/antagonists/malf_ai/malf_ai.dm b/code/modules/antagonists/malf_ai/malf_ai.dm index 4f78ceef99b..0821969d1f1 100644 --- a/code/modules/antagonists/malf_ai/malf_ai.dm +++ b/code/modules/antagonists/malf_ai/malf_ai.dm @@ -5,7 +5,7 @@ name = "\improper Malfunctioning AI" roundend_category = "traitors" antagpanel_category = "Malf AI" - job_rank = ROLE_MALF + pref_flag = ROLE_MALF antag_hud_name = "traitor" ui_name = "AntagInfoMalf" 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.") return ..() - owner.special_role = job_rank if(give_objectives) forge_ai_objectives() // SKYRAT EDIT START - Moving voice changing to Malf only @@ -70,7 +69,6 @@ // SKYRAT EDIT END QDEL_NULL(malf_ai.malf_picker) - owner.special_role = null UnregisterSignal(owner, COMSIG_SILICON_AI_CORE_STATUS) return ..() diff --git a/code/modules/antagonists/nightmare/nightmare.dm b/code/modules/antagonists/nightmare/nightmare.dm index 256833482c9..6b9aeb60549 100644 --- a/code/modules/antagonists/nightmare/nightmare.dm +++ b/code/modules/antagonists/nightmare/nightmare.dm @@ -1,7 +1,7 @@ /datum/antagonist/nightmare name = "\improper Nightmare" antagpanel_category = ANTAG_GROUP_ABOMINATIONS - job_rank = ROLE_NIGHTMARE + pref_flag = ROLE_NIGHTMARE show_in_antagpanel = FALSE show_name_in_check_antagonists = TRUE show_to_ghosts = TRUE diff --git a/code/modules/antagonists/nukeop/datums/operative.dm b/code/modules/antagonists/nukeop/datums/operative.dm index ebaaf8692b2..0279a12939d 100644 --- a/code/modules/antagonists/nukeop/datums/operative.dm +++ b/code/modules/antagonists/nukeop/datums/operative.dm @@ -1,8 +1,8 @@ /datum/antagonist/nukeop - name = ROLE_NUCLEAR_OPERATIVE + name = ROLE_OPERATIVE roundend_category = "syndicate operatives" //just in case antagpanel_category = ANTAG_GROUP_SYNDICATE - job_rank = ROLE_OPERATIVE + pref_flag = ROLE_OPERATIVE antag_hud_name = "synd" antag_moodlet = /datum/mood_event/focused show_to_ghosts = TRUE @@ -12,10 +12,10 @@ /// Which nukie team are we on? 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. var/send_to_spawnpoint = TRUE + + var/job_type = /datum/job/nuclear_operative /// The DEFAULT outfit we will give to players granted this datum var/nukeop_outfit = /datum/outfit/syndicate @@ -23,6 +23,7 @@ /// In the preview icon, the nukies who are behind the leader 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. var/nuke_icon_state = "nuclearbomb_base" @@ -40,6 +41,7 @@ give_alias() forge_objectives() . = ..() + owner.set_assigned_role(SSjob.get_job_type(job_type)) equip_op() if(send_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) 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() return nuke_team @@ -78,24 +81,19 @@ /datum/antagonist/nukeop/create_team(datum/team/nuclear/new_team) if(!new_team) - if(!always_new_team) - for(var/datum/antagonist/nukeop/N in GLOB.antagonists) - if(!N.owner) - stack_trace("Antagonist datum without owner in GLOB.antagonists: [N]") - continue - if(N.nuke_team) - nuke_team = N.nuke_team - return - nuke_team = new /datum/team/nuclear - nuke_team.update_objectives() - assign_nuke() //This is bit ugly + // Find the first leader to join up + for(var/datum/antagonist/nukeop/leader/leader in GLOB.antagonists) + if(leader.nuke_team) + nuke_team = leader.nuke_team + return + // Otherwise make a new team entirely + nuke_team = new /datum/team/nuclear() return if(!istype(new_team)) stack_trace("Wrong team type passed to [type] initialization.") nuke_team = new_team /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) 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)].") @@ -158,42 +156,21 @@ else 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() if(nuke_team?.syndicate_name) var/mob/living/carbon/human/human_to_rename = owner.current 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/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 - var/number = 1 - number = nuke_team.members.Find(owner) - owner.current.real_name = "[nuke_team.syndicate_name] Operative #[number]" + var/number = nuke_team?.members.Find(owner) || 1 + owner.current.fully_replace_character_name(null, "[nuke_team.syndicate_name] Operative #[number]") /datum/antagonist/nukeop/proc/memorize_code() - if(nuke_team && nuke_team.tracked_nuke && nuke_team.memorized_code) - antag_memory += "[nuke_team.tracked_nuke] 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: [nuke_team.memorized_code]") - else - to_chat(owner, "Unfortunately the syndicate was unable to provide you with nuclear authorization code.") + antag_memory += "[nuke_team.tracked_nuke] 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: [nuke_team.memorized_code]") /// Actually moves our nukie to where they should be /datum/antagonist/nukeop/proc/move_to_spawnpoint() @@ -212,3 +189,8 @@ team_number = nuke_team.members.Find(owner) 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 diff --git a/code/modules/antagonists/nukeop/datums/operative_leader.dm b/code/modules/antagonists/nukeop/datums/operative_leader.dm index 1af9f1d28c4..a15a0dec31a 100644 --- a/code/modules/antagonists/nukeop/datums/operative_leader.dm +++ b/code/modules/antagonists/nukeop/datums/operative_leader.dm @@ -1,29 +1,31 @@ /datum/antagonist/nukeop/leader name = "Nuclear Operative Leader" nukeop_outfit = /datum/outfit/syndicate/leader - always_new_team = TRUE /// Randomly chosen honorific, for distinction var/title /// The nuclear challenge remote we will spawn this player with. var/challengeitem = /obj/item/nuclear_challenge /datum/antagonist/nukeop/leader/memorize_code() - ..() - if(nuke_team?.memorized_code) - var/obj/item/paper/nuke_code_paper = new - nuke_code_paper.add_raw_text("The nuclear authorization code is: [nuke_team.memorized_code]") - nuke_code_paper.name = "nuclear bomb code" - var/mob/living/carbon/human/H = owner.current - if(!istype(H)) - nuke_code_paper.forceMove(get_turf(H)) - else - H.put_in_hands(nuke_code_paper, TRUE) - H.update_icons() + . = ..() + var/obj/item/paper/nuke_code_paper = new(get_turf(owner.current)) + nuke_code_paper.add_raw_text("The nuclear authorization code is: [nuke_team.memorized_code]") + nuke_code_paper.name = "nuclear bomb code" + nuke_code_paper.update_appearance() + owner.current.put_in_hands(nuke_code_paper) + +/datum/antagonist/nukeop/leader/give_alias() + title ||= pick("Czar", "Boss", "Commander", "Chief", "Kingpin", "Director", "Overlord") + . = ..() + 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() play_stinger() - to_chat(owner, "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.") - to_chat(owner, "If you feel you are not up to this task, give your ID and radio to another operative.") + to_chat(owner, "You are the Syndicate [title] for this mission. You are responsible for guiding your team.") + to_chat(owner, "If you feel you are not up to this task, trade your headset with another operative.") if(!CONFIG_GET(flag/disable_warops)) to_chat(owner, "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.") owner.announce_objectives() @@ -59,3 +61,9 @@ newname = randomname 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() diff --git a/code/modules/antagonists/nukeop/datums/operative_lone.dm b/code/modules/antagonists/nukeop/datums/operative_lone.dm index 3993c2d49a7..b074016890a 100644 --- a/code/modules/antagonists/nukeop/datums/operative_lone.dm +++ b/code/modules/antagonists/nukeop/datums/operative_lone.dm @@ -1,22 +1,13 @@ /datum/antagonist/nukeop/lone name = "Lone Operative" - always_new_team = TRUE send_to_spawnpoint = FALSE //Handled by event nukeop_outfit = /datum/outfit/syndicate/full/loneop preview_outfit = /datum/outfit/nuclear_operative preview_outfit_behind = null nuke_icon_state = null -/datum/antagonist/nukeop/lone/assign_nuke() - if(nuke_team && !nuke_team.tracked_nuke) - nuke_team.memorized_code = random_nukecode() - var/obj/machinery/nuclearbomb/selfdestruct/nuke = locate() in SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/selfdestruct) - if(nuke) - 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 +/datum/antagonist/nukeop/lone/create_team(datum/team/nuclear/new_team) + if(new_team) + return ..() + // Lone ops always get a solo team solely because a lot of nukie code is on the team + nuke_team = new /datum/team/nuclear/loneop() diff --git a/code/modules/antagonists/nukeop/datums/operative_team.dm b/code/modules/antagonists/nukeop/datums/operative_team.dm index 07ce1e327fb..4eab9341d48 100644 --- a/code/modules/antagonists/nukeop/datums/operative_team.dm +++ b/code/modules/antagonists/nukeop/datums/operative_team.dm @@ -13,6 +13,13 @@ ..() 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() var/list/parts = list() parts += span_header("[syndicate_name] Operatives:") @@ -129,14 +136,9 @@ /datum/team/nuclear/proc/rename_team(new_name) syndicate_name = new_name name = "[syndicate_name] Team" - for(var/I in members) - var/datum/mind/synd_mind = I - var/mob/living/carbon/human/human_to_rename = synd_mind.current - 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) + for(var/datum/mind/synd_mind in members) + var/datum/antagonist/nukeop/synd_datum = synd_mind.has_antag_datum(/datum/antagonist/nukeop) + synd_datum?.give_alias() /datum/team/nuclear/proc/admin_spawn_reinforcement(mob/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")) -/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() 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 @@ -317,5 +313,42 @@ ..() 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_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 diff --git a/code/modules/antagonists/obsessed/obsessed.dm b/code/modules/antagonists/obsessed/obsessed.dm index 7cb03b8b3a0..80fba86d14d 100644 --- a/code/modules/antagonists/obsessed/obsessed.dm +++ b/code/modules/antagonists/obsessed/obsessed.dm @@ -8,12 +8,12 @@ name = "Obsessed" show_in_antagpanel = TRUE antagpanel_category = ANTAG_GROUP_CREW - job_rank = ROLE_OBSESSED + pref_flag = ROLE_OBSESSED show_to_ghosts = TRUE antag_hud_name = "obsessed" show_name_in_check_antagonists = TRUE 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. suicide_cry = "FOR MY LOVE!!" preview_outfit = /datum/outfit/obsessed @@ -31,10 +31,9 @@ show_name_in_check_antagonists = TRUE antagpanel_category = ANTAG_GROUP_CREW show_in_roundend = FALSE - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST silent = TRUE can_elimination_hijack = ELIMINATION_PREVENT - antag_flags = FLAG_FAKE_ANTAG /datum/antagonist/obsessed/admin_add(datum/mind/new_owner,mob/admin) var/mob/living/carbon/C = new_owner.current @@ -190,7 +189,7 @@ /datum/objective/assassinate/obsessed/update_explanation_text() ..() 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 message_admins("WARNING! [ADMIN_LOOKUPFLW(owner)] obsessed objectives forged without an obsession!") explanation_text = "Free Objective" diff --git a/code/modules/antagonists/paradox_clone/paradox_clone.dm b/code/modules/antagonists/paradox_clone/paradox_clone.dm index 960cf7f59ec..c5100bed01b 100644 --- a/code/modules/antagonists/paradox_clone/paradox_clone.dm +++ b/code/modules/antagonists/paradox_clone/paradox_clone.dm @@ -1,7 +1,7 @@ /datum/antagonist/paradox_clone name = "\improper Paradox Clone" roundend_category = "Paradox Clone" - job_rank = ROLE_PARADOX_CLONE + pref_flag = ROLE_PARADOX_CLONE antagpanel_category = ANTAG_GROUP_PARADOX antag_hud_name = "paradox_clone" show_to_ghosts = TRUE @@ -28,17 +28,6 @@ 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() original_ref = null return ..() @@ -94,7 +83,7 @@ if(!target?.current) explanation_text = "Free Objective" 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. /obj/effect/bluespace_stream diff --git a/code/modules/antagonists/pirate/pirate.dm b/code/modules/antagonists/pirate/pirate.dm index 4b0d3199fbb..926fe0e6468 100644 --- a/code/modules/antagonists/pirate/pirate.dm +++ b/code/modules/antagonists/pirate/pirate.dm @@ -1,6 +1,6 @@ /datum/antagonist/pirate name = "\improper Space Pirate" - job_rank = ROLE_TRAITOR + pref_flag = ROLE_TRAITOR roundend_category = "space pirates" antagpanel_category = ANTAG_GROUP_PIRATES show_in_antagpanel = FALSE diff --git a/code/modules/antagonists/pirate/pirate_event.dm b/code/modules/antagonists/pirate/pirate_event.dm deleted file mode 100644 index 5ecefa3094d..00000000000 --- a/code/modules/antagonists/pirate/pirate_event.dm +++ /dev/null @@ -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 diff --git a/code/modules/antagonists/pyro_slime/pyro_slime.dm b/code/modules/antagonists/pyro_slime/pyro_slime.dm index 49b4c8b3286..a30250b51aa 100644 --- a/code/modules/antagonists/pyro_slime/pyro_slime.dm +++ b/code/modules/antagonists/pyro_slime/pyro_slime.dm @@ -5,6 +5,7 @@ show_in_antagpanel = FALSE show_name_in_check_antagonists = TRUE show_to_ghosts = TRUE + pref_flag = ROLE_PYROCLASTIC_SLIME /datum/antagonist/pyro_slime/on_gain() forge_objectives() diff --git a/code/modules/antagonists/revolution/enemy_of_the_state.dm b/code/modules/antagonists/revolution/enemy_of_the_state.dm index d835655b7ef..9b73e9719e5 100644 --- a/code/modules/antagonists/revolution/enemy_of_the_state.dm +++ b/code/modules/antagonists/revolution/enemy_of_the_state.dm @@ -23,7 +23,7 @@ /datum/antagonist/enemy_of_the_state/on_gain() owner.add_memory(/datum/memory/revolution_rev_defeat) - owner.special_role = "exiled headrev" + // LAZYADD(owner.special_statuses, "Exiled Head Revolutionary") forge_objectives() . = ..() diff --git a/code/modules/antagonists/revolution/revolution.dm b/code/modules/antagonists/revolution/revolution.dm index c3269dc4ca3..5f538904073 100644 --- a/code/modules/antagonists/revolution/revolution.dm +++ b/code/modules/antagonists/revolution/revolution.dm @@ -2,7 +2,7 @@ name = "\improper Revolutionary" roundend_category = "revolutionaries" // if by some miracle revolutionaries without revolution happen antagpanel_category = "Revolution" - job_rank = ROLE_REV + pref_flag = ROLE_REV antag_moodlet = /datum/mood_event/revolution antag_hud_name = "rev" suicide_cry = "VIVA LA REVOLUTION!!" @@ -55,14 +55,9 @@ /datum/antagonist/rev/on_gain() . = ..() - create_objectives() equip_rev() 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() . = ..() 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) if(!new_team) - //For now only one revolution at a time - for(var/datum/antagonist/rev/head/H in GLOB.antagonists) - if(!H.owner) - 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() + GLOB.revolution_handler ||= new() + rev_team = GLOB.revolution_handler.revs + GLOB.revolution_handler.start_revolution() return if(!istype(new_team)) stack_trace("Wrong team type passed to [type] initialization.") @@ -88,12 +76,6 @@ /datum/antagonist/rev/get_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 /datum/antagonist/rev/proc/promote() var/old_team = rev_team @@ -171,7 +153,7 @@ /datum/antagonist/rev/head name = "\improper Head Revolutionary" antag_hud_name = "rev_head" - job_rank = ROLE_REV_HEAD + pref_flag = ROLE_REV_HEAD preview_outfit = /datum/outfit/revolutionary 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_antag_datum(/datum/antagonist/rev,rev_team) - rev_mind.special_role = ROLE_REV return TRUE /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) if(deconverter == DECONVERTER_BORGED) message_admins("[ADMIN_LOOKUPFLW(owner.current)] has been borged while being a [name]") - owner.special_role = null if(iscarbon(owner.current) && deconverter) var/mob/living/carbon/formerrev = owner.current formerrev.Unconscious(10 SECONDS) @@ -382,40 +362,19 @@ /datum/team/revolution name = "\improper Revolution" - /// Maximum number of headrevs 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. - 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. - var/list/ex_revs = list() + var/list/datum/mind/ex_revs = list() - /// The objective of the heads of staff, aka to kill the headrevs. - var/list/datum/objective/mutiny/heads_objective = list() - -/// Proc called on periodic timer. -/// 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) +/// Saves all current headrevs and revs +/datum/team/revolution/proc/save_members() + ex_headrevs = get_head_revolutionaries() + ex_revs = members - ex_headrevs /// Returns a list of all headrevs. /datum/team/revolution/proc/get_head_revolutionaries() @@ -427,129 +386,39 @@ 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. /// 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() - if(SSticker.HasRoundStarted()) - 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() + var/list/datum/mind/head_revolutionaries = get_head_revolutionaries() - if(head_revolutionaries.len < max_headrevs && head_revolutionaries.len < round(heads.len - ((8 - sec.len) / 3))) - 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()) + if(length(head_revolutionaries) >= headrev_cap()) return - . = STATION_VICTORY - - SSshuttle.clearHostileEnvironment(src) - - // 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, "Former head revolutionary") - else - LAZYADD(rev_mind.special_statuses, "Former revolutionary") - - 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)) + var/list/datum/mind/promotable = list() + var/list/datum/mind/monkey_promotable = list() + for(var/datum/mind/khrushchev as anything in members - head_revolutionaries) + if(!can_be_headrev(khrushchev)) continue - add_memory_in_range(real_headrev, 5, /datum/memory/revolution_rev_victory, protagonist = real_headrev) - -/// Handles effects of revs losing, such as making ex-headrevs unrevivable and setting up head of staff memories. -/datum/team/revolution/proc/defeat_effects() - // If the revolution was quelled, make rev heads unable to be revived through pods - for (var/datum/mind/rev_head as anything in 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 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") + 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)) + continue + if(ismonkey(khrushchev.current)) + monkey_promotable += khrushchev + else + promotable += khrushchev + if(!length(promotable) && length(monkey_promotable)) + promotable = monkey_promotable + if(!length(promotable)) + return + var/datum/mind/new_leader = pick(promotable) + var/datum/antagonist/rev/rev = new_leader.has_antag_datum(/datum/antagonist/rev) + rev.promote() /// Mutates the ticker to report that the revs have won /datum/team/revolution/proc/round_result(finished) @@ -595,13 +464,13 @@ if(headrevs.len) var/list/headrev_part = list() 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("
") if(revs.len) var/list/rev_part = list() 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("
") var/list/heads = SSjob.get_all_heads() diff --git a/code/modules/antagonists/revolution/revolution_handler.dm b/code/modules/antagonists/revolution/revolution_handler.dm new file mode 100644 index 00000000000..d3bdbf97269 --- /dev/null +++ b/code/modules/antagonists/revolution/revolution_handler.dm @@ -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 diff --git a/code/modules/antagonists/sentient_creature/sentient_creature.dm b/code/modules/antagonists/sentient_creature/sentient_creature.dm index 9fbafdfe2cc..48b149574f9 100644 --- a/code/modules/antagonists/sentient_creature/sentient_creature.dm +++ b/code/modules/antagonists/sentient_creature/sentient_creature.dm @@ -2,7 +2,7 @@ name = "\improper Sentient Creature" show_in_antagpanel = FALSE show_in_roundend = FALSE - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST ui_name = "AntagInfoSentient" /datum/antagonist/sentient_creature/get_preview_icon() diff --git a/code/modules/antagonists/shade/shade_minion.dm b/code/modules/antagonists/shade/shade_minion.dm index 4b06452c4b7..4d7790e320a 100644 --- a/code/modules/antagonists/shade/shade_minion.dm +++ b/code/modules/antagonists/shade/shade_minion.dm @@ -10,7 +10,7 @@ show_in_roundend = FALSE silent = TRUE ui_name = "AntagInfoShade" - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_SKIP_GLOBAL_LIST /// Name of this shade's master. var/master_name = "nobody?" diff --git a/code/modules/antagonists/space_dragon/space_dragon.dm b/code/modules/antagonists/space_dragon/space_dragon.dm index aff05bf9b8d..6bac3723ef7 100644 --- a/code/modules/antagonists/space_dragon/space_dragon.dm +++ b/code/modules/antagonists/space_dragon/space_dragon.dm @@ -2,7 +2,7 @@ name = "\improper Space Dragon" roundend_category = "space dragons" antagpanel_category = ANTAG_GROUP_LEVIATHANS - job_rank = ROLE_SPACE_DRAGON + pref_flag = ROLE_SPACE_DRAGON show_in_antagpanel = FALSE show_name_in_check_antagonists = TRUE show_to_ghosts = TRUE @@ -66,12 +66,10 @@ /datum/antagonist/space_dragon/on_gain() forge_objectives() rift_ability = new() - owner.special_role = ROLE_SPACE_DRAGON owner.set_assigned_role(SSjob.get_job_type(/datum/job/space_dragon)) return ..() /datum/antagonist/space_dragon/on_removal() - owner.special_role = null owner.set_assigned_role(SSjob.get_job_type(/datum/job/unassigned)) return ..() diff --git a/code/modules/antagonists/space_ninja/space_ninja.dm b/code/modules/antagonists/space_ninja/space_ninja.dm index 6a7cb56c913..1858bb59a2a 100644 --- a/code/modules/antagonists/space_ninja/space_ninja.dm +++ b/code/modules/antagonists/space_ninja/space_ninja.dm @@ -1,7 +1,7 @@ /datum/antagonist/ninja name = "\improper Space Ninja" antagpanel_category = ANTAG_GROUP_NINJAS - job_rank = ROLE_NINJA + pref_flag = ROLE_NINJA antag_hud_name = "ninja" hijack_speed = 1 show_name_in_check_antagonists = TRUE @@ -124,13 +124,18 @@ owner.current.add_quirk(/datum/quirk/freerunning) owner.current.add_quirk(/datum/quirk/light_step) owner.current.mind.set_assigned_role(SSjob.get_job_type(/datum/job/space_ninja)) - owner.current.mind.special_role = ROLE_NINJA operative.mind.active = TRUE // BUBBER EDIT END return ..() /datum/antagonist/ninja/admin_add(datum/mind/new_owner,mob/admin) new_owner.set_assigned_role(SSjob.get_job_type(/datum/job/space_ninja)) - new_owner.special_role = ROLE_NINJA new_owner.add_antag_datum(src) message_admins("[key_name_admin(admin)] has ninja'ed [key_name_admin(new_owner)].") log_admin("[key_name(admin)] has ninja'ed [key_name(new_owner)].") + +/datum/antagonist/ninja/on_respawn(mob/new_character) + equip_space_ninja() + var/turf/spawnpoint = find_space_spawn() + if(spawnpoint) + new_character.forceMove(spawnpoint) + return TRUE diff --git a/code/modules/antagonists/spy/spy.dm b/code/modules/antagonists/spy/spy.dm index c4d1b3445ed..e5fceec73c1 100644 --- a/code/modules/antagonists/spy/spy.dm +++ b/code/modules/antagonists/spy/spy.dm @@ -3,7 +3,7 @@ roundend_category = "spies" antagpanel_category = "Spy" antag_hud_name = "spy" - job_rank = ROLE_SPY + pref_flag = ROLE_SPY antag_moodlet = /datum/mood_event/focused hijack_speed = 1 ui_name = "AntagInfoSpy" diff --git a/code/modules/antagonists/survivalist/survivalist.dm b/code/modules/antagonists/survivalist/survivalist.dm index 8bb77ab71c0..64d943c0fc6 100644 --- a/code/modules/antagonists/survivalist/survivalist.dm +++ b/code/modules/antagonists/survivalist/survivalist.dm @@ -14,7 +14,6 @@ objectives += survive /datum/antagonist/survivalist/on_gain() - owner.special_role = "survivalist" forge_objectives() . = ..() diff --git a/code/modules/antagonists/syndicate_monkey/syndicate_monkey.dm b/code/modules/antagonists/syndicate_monkey/syndicate_monkey.dm index 7fb61f6caae..b1be0600694 100644 --- a/code/modules/antagonists/syndicate_monkey/syndicate_monkey.dm +++ b/code/modules/antagonists/syndicate_monkey/syndicate_monkey.dm @@ -4,7 +4,7 @@ show_in_roundend = TRUE show_in_antagpanel = TRUE show_name_in_check_antagonists = TRUE - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_SKIP_GLOBAL_LIST show_to_ghosts = TRUE /// The antagonist's master, used for objective var/mob/living/monky_master diff --git a/code/modules/antagonists/traitor/components/demoraliser.dm b/code/modules/antagonists/traitor/components/demoraliser.dm index ee44527728c..3d0ddf1fe3d 100644 --- a/code/modules/antagonists/traitor/components/demoraliser.dm +++ b/code/modules/antagonists/traitor/components/demoraliser.dm @@ -52,7 +52,7 @@ return - if (is_special_character(viewer)) + if (viewer.is_antag()) to_chat(viewer, span_notice("[moods.antag_notification]")) viewer.add_mood_event(moods.mood_category, moods.antag_mood) else if (viewer.mind.assigned_role.departments_bitflags & (DEPARTMENT_BITFLAG_SECURITY|DEPARTMENT_BITFLAG_COMMAND)) diff --git a/code/modules/antagonists/traitor/contractor/contract_teammate.dm b/code/modules/antagonists/traitor/contractor/contract_teammate.dm index 484d8196ddf..c60a6e9f242 100644 --- a/code/modules/antagonists/traitor/contractor/contract_teammate.dm +++ b/code/modules/antagonists/traitor/contractor/contract_teammate.dm @@ -1,7 +1,7 @@ /// Support unit gets it's own very basic antag datum for admin logging. /datum/antagonist/traitor/contractor_support name = "Contractor Support Unit" - job_rank = ROLE_CONTRACTOR_SUPPORT + pref_flag = ROLE_CONTRACTOR_SUPPORT employer = "Contractor Support Unit" show_in_roundend = FALSE give_objectives = TRUE diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index 4b7c3a97c58..ccbb27cb634 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -7,7 +7,7 @@ name = "\improper Traitor" roundend_category = "traitors" antagpanel_category = "Traitor" - job_rank = ROLE_TRAITOR + pref_flag = ROLE_TRAITOR antag_moodlet = /datum/mood_event/focused antag_hud_name = "traitor" hijack_speed = 0.5 //10 seconds per hijack stage by default @@ -52,8 +52,6 @@ src.give_objectives = give_objectives /datum/antagonist/traitor/on_gain() - owner.special_role = job_rank - if(give_uplink) owner.give_uplink(silent = TRUE, antag_datum = src) @@ -99,7 +97,6 @@ uplink_handler.can_replace_objectives = null uplink_handler.replace_objectives = null owner.take_uplink() - owner.special_role = null return ..() /// Returns true if we're allowed to assign ourselves a new objective @@ -345,3 +342,8 @@ #undef FLAVOR_FACTION_SYNDICATE #undef FLAVOR_FACTION_NANOTRASEN + +/datum/antagonist/traitor/on_respawn(mob/new_character) + SSjob.equip_rank(new_character, new_character.mind.assigned_role, new_character.client) + new_character.mind.give_uplink(silent = TRUE, antag_datum = src) + return TRUE diff --git a/code/modules/antagonists/valentines/valentine.dm b/code/modules/antagonists/valentines/valentine.dm index 086c50827f5..d528dae144b 100644 --- a/code/modules/antagonists/valentines/valentine.dm +++ b/code/modules/antagonists/valentines/valentine.dm @@ -2,12 +2,10 @@ name = "\improper Valentine" roundend_category = "valentines" //there's going to be a ton of them so put them in separate category show_in_antagpanel = FALSE - prevent_roundtype_conversion = FALSE suicide_cry = "FOR MY LOVE!!" ui_name = null // Not 'true' antags, this disables certain interactions that assume the owner is a baddie - antag_flags = FLAG_FAKE_ANTAG - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST /// Reference to our date's mind VAR_FINAL/datum/mind/date diff --git a/code/modules/antagonists/voidwalker/voidwalker.dm b/code/modules/antagonists/voidwalker/voidwalker.dm index ba3c2f15c57..a21d5d777d3 100644 --- a/code/modules/antagonists/voidwalker/voidwalker.dm +++ b/code/modules/antagonists/voidwalker/voidwalker.dm @@ -2,7 +2,7 @@ /datum/antagonist/voidwalker name = "\improper Voidwalker" antagpanel_category = ANTAG_GROUP_ABOMINATIONS - job_rank = ROLE_VOIDWALKER + pref_flag = ROLE_VOIDWALKER show_in_antagpanel = TRUE antagpanel_category = "Voidwalker" show_name_in_check_antagonists = TRUE diff --git a/code/modules/antagonists/wishgranter/wishgranter.dm b/code/modules/antagonists/wishgranter/wishgranter.dm index 09e498e5d14..e492a3d70bb 100644 --- a/code/modules/antagonists/wishgranter/wishgranter.dm +++ b/code/modules/antagonists/wishgranter/wishgranter.dm @@ -11,7 +11,6 @@ objectives += hijack /datum/antagonist/wishgranter/on_gain() - owner.special_role = "Avatar of the Wish Granter" forge_objectives() . = ..() give_powers() diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/summons.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/summons.dm index 737a3d73403..869e44cabaa 100644 --- a/code/modules/antagonists/wizard/equipment/spellbook_entries/summons.dm +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/summons.dm @@ -20,11 +20,8 @@ There is a good chance that they will shoot each other first." /datum/spellbook_entry/summon/guns/can_be_purchased() - // Summon Guns requires 98 threat. - if(SSdynamic.threat_level < MINIMUM_THREAT_FOR_RITUALS) - return FALSE - // Also must be config enabled - return !CONFIG_GET(flag/no_summon_guns) + // Must be a high chaos round + Also must be config enabled + return SSdynamic.current_tier.tier == DYNAMIC_TIER_HIGH && !CONFIG_GET(flag/no_summon_guns) /datum/spellbook_entry/summon/guns/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy = TRUE) summon_guns(user, 10) @@ -37,11 +34,8 @@ why they aren't to be trusted with it at the same time." /datum/spellbook_entry/summon/magic/can_be_purchased() - // Summon Magic requires 98 threat. - if(SSdynamic.threat_level < MINIMUM_THREAT_FOR_RITUALS) - return FALSE - // Also must be config enabled - return !CONFIG_GET(flag/no_summon_magic) + // Must be a high chaos round + Also must be config enabled + return SSdynamic.current_tier.tier == DYNAMIC_TIER_HIGH && !CONFIG_GET(flag/no_summon_magic) /datum/spellbook_entry/summon/magic/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy = TRUE) summon_magic(user, 10) @@ -57,11 +51,8 @@ limit = 5 // Each purchase can intensify it. /datum/spellbook_entry/summon/events/can_be_purchased() - // Summon Events requires 98 threat. - if(SSdynamic.threat_level < MINIMUM_THREAT_FOR_RITUALS) - return FALSE - // Also, must be config enabled - return !CONFIG_GET(flag/no_summon_events) + // Must be a high chaos round + Also must be config enabled + return SSdynamic.current_tier.tier == DYNAMIC_TIER_HIGH && !CONFIG_GET(flag/no_summon_events) /datum/spellbook_entry/summon/events/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy = TRUE) summon_events(user) @@ -135,7 +126,7 @@ return ..() /datum/spellbook_entry/summon/specific_spell/can_be_purchased() - if(SSdynamic.threat_level < MINIMUM_THREAT_FOR_RITUALS) + if(SSdynamic.current_tier.tier != DYNAMIC_TIER_HIGH) return FALSE if(GLOB.mass_teaching) return FALSE diff --git a/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm b/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm index a69492bab16..422fb0ec896 100644 --- a/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm +++ b/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm @@ -66,7 +66,7 @@ return if(user.mind != owner) - if(user.mind?.special_role == ROLE_WIZARD_APPRENTICE) + if(IS_WIZARD_APPRENTICE(user)) to_chat(user, span_warning("If you got caught sneaking a peek from your teacher's spellbook, you'd likely be expelled from the Wizard Academy. Better not.")) else to_chat(user, span_warning("[src] does not recognize you as its owner and refuses to open!")) diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/armageddon.dm b/code/modules/antagonists/wizard/grand_ritual/finales/armageddon.dm index 3716a684c6a..54fcc79fa0d 100644 --- a/code/modules/antagonists/wizard/grand_ritual/finales/armageddon.dm +++ b/code/modules/antagonists/wizard/grand_ritual/finales/armageddon.dm @@ -49,9 +49,9 @@ var/obj/energy_ball/tesla = new (current_location) tesla.energy = 200 if (DOOM_METEORS) - var/datum/dynamic_ruleset/roundstart/meteor/meteors = new() - meteors.meteordelay = 0 - SSdynamic.execute_roundstart_rule(meteors) // Meteors will continue until morale is crushed. + GLOB.meteor_mode ||= new() + GLOB.meteor_mode.meteordelay = 0 + GLOB.meteor_mode.start_meteor() priority_announce("Meteors have been detected on collision course with the station.", "Meteor Alert", ANNOUNCER_METEORS) #undef DOOM_SINGULARITY diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/grand_ritual_finale.dm b/code/modules/antagonists/wizard/grand_ritual/finales/grand_ritual_finale.dm index 1ceb21b72eb..a1bf16ff790 100644 --- a/code/modules/antagonists/wizard/grand_ritual/finales/grand_ritual_finale.dm +++ b/code/modules/antagonists/wizard/grand_ritual/finales/grand_ritual_finale.dm @@ -73,7 +73,7 @@ show_in_antagpanel = FALSE antagpanel_category = ANTAG_GROUP_CREW show_name_in_check_antagonists = TRUE - count_against_dynamic_roll_chance = FALSE + antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST silent = TRUE /// Give everyone magic items, its so simple it feels pointless to give it its own file diff --git a/code/modules/antagonists/wizard/slaughter_antag.dm b/code/modules/antagonists/wizard/slaughter_antag.dm index 9fe4b37e39e..effd5d2abf6 100644 --- a/code/modules/antagonists/wizard/slaughter_antag.dm +++ b/code/modules/antagonists/wizard/slaughter_antag.dm @@ -3,7 +3,7 @@ roundend_category = "demons" show_name_in_check_antagonists = TRUE ui_name = "AntagInfoDemon" - job_rank = ROLE_ALIEN + pref_flag = ROLE_ALIEN show_in_antagpanel = FALSE show_to_ghosts = TRUE antagpanel_category = ANTAG_GROUP_WIZARDS diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm index db9203fdf49..29c40421280 100644 --- a/code/modules/antagonists/wizard/wizard.dm +++ b/code/modules/antagonists/wizard/wizard.dm @@ -5,7 +5,7 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) name = "\improper Space Wizard" roundend_category = "wizards/witches" antagpanel_category = ANTAG_GROUP_WIZARDS - job_rank = ROLE_WIZARD + pref_flag = ROLE_WIZARD antag_hud_name = "wizard" antag_moodlet = /datum/mood_event/focused hijack_speed = 0.5 @@ -457,3 +457,8 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) parts += printplayerlist(members - master_wizard.owner) return "
[parts.Join("
")]
" + +/datum/antagonist/wizard/on_respawn(mob/new_character) + new_character.forceMove(pick(GLOB.wizardstart)) + equip_wizard() + return TRUE diff --git a/code/modules/antagonists/xeno/xeno.dm b/code/modules/antagonists/xeno/xeno.dm index da9c36de1b0..3f045a87ec5 100644 --- a/code/modules/antagonists/xeno/xeno.dm +++ b/code/modules/antagonists/xeno/xeno.dm @@ -17,10 +17,9 @@ /datum/antagonist/xeno name = "\improper Xenomorph" - job_rank = ROLE_ALIEN + pref_flag = ROLE_ALIEN show_in_antagpanel = FALSE antagpanel_category = ANTAG_GROUP_XENOS - prevent_roundtype_conversion = FALSE show_to_ghosts = TRUE var/datum/team/xeno/xeno_team @@ -161,7 +160,6 @@ mind.add_antag_datum(/datum/antagonist/xeno) mind.set_assigned_role(SSjob.get_job_type(/datum/job/xenomorph)) - mind.special_role = ROLE_ALIEN /mob/living/carbon/alien/on_wabbajacked(mob/living/new_mob) . = ..() @@ -171,7 +169,6 @@ return mind.remove_antag_datum(/datum/antagonist/xeno) mind.set_assigned_role(SSjob.get_job_type(/datum/job/unassigned)) - mind.special_role = null #undef CAPTIVE_XENO_DEAD #undef CAPTIVE_XENO_FAIL diff --git a/code/modules/bitrunning/antagonists/_parent.dm b/code/modules/bitrunning/antagonists/_parent.dm index 8bd061d72a1..5a00026f996 100644 --- a/code/modules/bitrunning/antagonists/_parent.dm +++ b/code/modules/bitrunning/antagonists/_parent.dm @@ -4,7 +4,7 @@ /datum/antagonist/bitrunning_glitch name = "Generic Bitrunning Glitch" antagpanel_category = ANTAG_GROUP_GLITCH - job_rank = ROLE_GLITCH + pref_flag = ROLE_GLITCH preview_outfit = /datum/outfit/cyber_police show_in_roundend = FALSE show_in_antagpanel = FALSE diff --git a/code/modules/bitrunning/antagonists/ghost_role.dm b/code/modules/bitrunning/antagonists/ghost_role.dm index 3bf88e16dfb..2114382d552 100644 --- a/code/modules/bitrunning/antagonists/ghost_role.dm +++ b/code/modules/bitrunning/antagonists/ghost_role.dm @@ -1,7 +1,7 @@ /datum/antagonist/domain_ghost_actor name = "Virtual Domain Actor" antagpanel_category = ANTAG_GROUP_GLITCH - job_rank = ROLE_GLITCH + pref_flag = ROLE_GLITCH show_to_ghosts = TRUE suicide_cry = "FATAL ERROR" ui_name = "AntagInfoGlitch" diff --git a/code/modules/bitrunning/event.dm b/code/modules/bitrunning/event.dm index f71a2777f68..f3c3d0e0cc2 100644 --- a/code/modules/bitrunning/event.dm +++ b/code/modules/bitrunning/event.dm @@ -6,7 +6,6 @@ ) category = EVENT_CATEGORY_INVASION description = "Causes a short term antagonist to spawn in the virtual domain." - dynamic_should_hijack = FALSE min_players = 1 max_occurrences = 0 typepath = /datum/round_event/ghost_role/bitrunning_glitch diff --git a/code/modules/bitrunning/server/threats.dm b/code/modules/bitrunning/server/threats.dm index 3e1d6252f5c..d8d79aee766 100644 --- a/code/modules/bitrunning/server/threats.dm +++ b/code/modules/bitrunning/server/threats.dm @@ -118,7 +118,6 @@ var/datum/mind/antag_mind = new_mob.mind antag_mind.add_antag_datum(chosen_role) - antag_mind.special_role = ROLE_GLITCH antag_mind.set_assigned_role(SSjob.get_job_type(/datum/job/bitrunning_glitch)) playsound(new_mob, 'sound/effects/magic/ethereal_exit.ogg', 50, vary = TRUE) diff --git a/code/modules/client/preferences/middleware/antags.dm b/code/modules/client/preferences/middleware/antags.dm index 9c15f74efdc..2e69288adf7 100644 --- a/code/modules/client/preferences/middleware/antags.dm +++ b/code/modules/client/preferences/middleware/antags.dm @@ -1,10 +1,10 @@ -// Antagonists that don't have a dynamic ruleset, but do have a preference +/// Antagonists that don't have a dynamic ruleset, but do have a preference GLOBAL_LIST_INIT(non_ruleset_antagonists, list( - ROLE_GLITCH = /datum/antagonist/bitrunning_glitch, - ROLE_FUGITIVE = /datum/antagonist/fugitive, - ROLE_LONE_OPERATIVE = /datum/antagonist/nukeop/lone, - ROLE_SENTIENCE = /datum/antagonist/sentient_creature, - )) + ROLE_GLITCH = /datum/antagonist/bitrunning_glitch, + ROLE_FUGITIVE = /datum/antagonist/fugitive, + ROLE_LONE_OPERATIVE = /datum/antagonist/nukeop/lone, + ROLE_SENTIENCE = /datum/antagonist/sentient_creature, +)) /datum/preference_middleware/antags action_delegations = list( @@ -67,20 +67,9 @@ GLOBAL_LIST_INIT(non_ruleset_antagonists, list( /datum/preference_middleware/antags/proc/get_antag_bans() var/list/antag_bans = list() - for (var/datum/dynamic_ruleset/dynamic_ruleset as anything in subtypesof(/datum/dynamic_ruleset)) - var/antag_flag = initial(dynamic_ruleset.antag_flag) - var/antag_flag_override = initial(dynamic_ruleset.antag_flag_override) - - if (isnull(antag_flag)) - continue - - if (is_banned_from(preferences.parent.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE))) - antag_bans += serialize_antag_name(antag_flag) - - for(var/antag_key in GLOB.non_ruleset_antagonists) - var/datum/antagonist/antag = GLOB.non_ruleset_antagonists[antag_key] - var/antag_flag = initial(antag.job_rank) - if(is_banned_from(preferences.parent.ckey, list(antag_flag, ROLE_SYNDICATE))) + var/is_banned_from_all = is_banned_from(preferences.parent.ckey, ROLE_SYNDICATE) + for (var/antag_flag in get_all_antag_flags()) + if (is_banned_from_all || is_banned_from(preferences.parent.ckey, antag_flag)) antag_bans += serialize_antag_name(antag_flag) return antag_bans @@ -90,29 +79,8 @@ GLOBAL_LIST_INIT(non_ruleset_antagonists, list( return var/list/antag_days_left = list() - - for (var/datum/dynamic_ruleset/dynamic_ruleset as anything in subtypesof(/datum/dynamic_ruleset)) - var/antag_flag = initial(dynamic_ruleset.antag_flag) - var/antag_flag_override = initial(dynamic_ruleset.antag_flag_override) - - if (isnull(antag_flag)) - continue - - var/days_needed = preferences.parent?.get_remaining_days( - GLOB.special_roles[antag_flag_override || antag_flag] - ) - - if (days_needed > 0) - antag_days_left[serialize_antag_name(antag_flag)] = days_needed - - for(var/antag_key in GLOB.non_ruleset_antagonists) - var/datum/antagonist/antag = GLOB.non_ruleset_antagonists[antag_key] - var/antag_flag = initial(antag.job_rank) - - var/days_needed = preferences.parent?.get_remaining_days( - GLOB.special_roles[antag_flag] - ) - + for (var/antag_flag in get_all_antag_flags()) + var/days_needed = preferences.parent?.get_days_to_play_antag(antag_flag) || 0 if (days_needed > 0) antag_days_left[serialize_antag_name(antag_flag)] = days_needed @@ -124,11 +92,50 @@ GLOBAL_LIST_INIT(non_ruleset_antagonists, list( if (isnull(serialized_antags)) serialized_antags = list() - for (var/special_role in GLOB.special_roles) + for (var/special_role in get_all_antag_flags()) serialized_antags[serialize_antag_name(special_role)] = special_role return serialized_antags +/** + * Returns a list of all antag flags that are available to the player + * + * So this includes stuff like traitor, wizard, fugitive, but does not include wizard apprentice or hypnotized + */ +/proc/get_all_antag_flags() as /list + var/static/list/antag_flags + if(antag_flags) + return antag_flags + + var/list/ruleset_antags = list() + for(var/datum/dynamic_ruleset/ruleset as anything in subtypesof(/datum/dynamic_ruleset)) + var/antag_flag = initial(ruleset.pref_flag) + var/jobban_flag = initial(ruleset.jobban_flag) + + if(antag_flag) + ruleset_antags |= antag_flag + if(jobban_flag) + ruleset_antags |= jobban_flag + + antag_flags = ruleset_antags | GLOB.non_ruleset_antagonists + return antag_flags + +/** + * Returns the number of days more the client's account must be to play the passed in antag + */ +/client/proc/get_days_to_play_antag(checked_antag_flag) + var/static/list/antag_time_limits + if(!antag_time_limits) + antag_time_limits = list() + for(var/datum/dynamic_ruleset/ruleset as anything in subtypesof(/datum/dynamic_ruleset)) + var/antag_flag = initial(ruleset.pref_flag) + var/config_min_days = SSdynamic.dynamic_config[initial(ruleset.config_tag)]?[NAMEOF(ruleset, minimum_required_age)] + var/min_days = isnull(config_min_days) ? initial(ruleset.minimum_required_age) : config_min_days + + antag_time_limits[antag_flag] = min_days + + return get_remaining_days(antag_time_limits[checked_antag_flag] || 0) + /// Sprites generated for the antagonists panel /datum/asset/spritesheet/antagonists name = "antagonists" @@ -141,12 +148,13 @@ GLOBAL_LIST_INIT(non_ruleset_antagonists, list( var/list/antagonists = GLOB.non_ruleset_antagonists.Copy() for (var/datum/dynamic_ruleset/ruleset as anything in subtypesof(/datum/dynamic_ruleset)) - var/datum/antagonist/antagonist_type = initial(ruleset.antag_datum) - if (isnull(antagonist_type)) + var/datum/antagonist/antagonist_type = initial(ruleset.preview_antag_datum) + var/antag_flag = initial(ruleset.pref_flag) + if (isnull(antagonist_type) || isnull(antag_flag)) continue // antag_flag is guaranteed to be unique by unit tests. - antagonists[initial(ruleset.antag_flag)] = antagonist_type + antagonists[initial(ruleset.pref_flag)] = antagonist_type var/list/generated_icons = list() diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 01d33e3ff2e..d0e4dd9c83e 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -421,7 +421,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car var/list/output = list() for (var/role in input_be_special) - if (role in GLOB.special_roles) + if (role in get_all_antag_flags()) output += role return output.len == input_be_special.len ? input_be_special : output diff --git a/code/modules/client/verbs/who.dm b/code/modules/client/verbs/who.dm index 0e2446a39d9..30fdc0b686d 100644 --- a/code/modules/client/verbs/who.dm +++ b/code/modules/client/verbs/who.dm @@ -36,7 +36,7 @@ entry += " - DEAD" else entry += " - DEAD" - if(is_special_character(client.mob)) + if(client.mob.is_antag()) entry += " - Antagonist" entry += " [ADMIN_QUE(client.mob)]" entry += " ([round(client.avgping, 1)]ms)" diff --git a/code/modules/clothing/outfits/event.dm b/code/modules/clothing/outfits/event.dm index 25b7f5665b5..50294a8ea5b 100644 --- a/code/modules/clothing/outfits/event.dm +++ b/code/modules/clothing/outfits/event.dm @@ -19,7 +19,6 @@ return user.fully_replace_character_name(user.real_name, "Santa Claus") user.mind.set_assigned_role(SSjob.get_job_type(/datum/job/santa)) - user.mind.special_role = ROLE_SANTA user.hairstyle = "Long Hair 3" user.facial_hairstyle = "Beard (Full)" diff --git a/code/modules/events/_event.dm b/code/modules/events/_event.dm index 88fea671c26..3fe4ae42c18 100644 --- a/code/modules/events/_event.dm +++ b/code/modules/events/_event.dm @@ -32,9 +32,6 @@ var/triggering //admin cancellation - /// Whether or not dynamic should hijack this event - var/dynamic_should_hijack = FALSE - /// Datum that will handle admin options for forcing the event. /// If there are no options, just leave it as an empty list. var/list/datum/event_admin_setup/admin_setup = list() @@ -86,9 +83,6 @@ if(ispath(typepath, /datum/round_event/ghost_role) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT)) return FALSE - if (dynamic_should_hijack && SSdynamic.random_event_hijacked != HIJACKED_NOTHING) - return FALSE - return TRUE /datum/round_event_control/proc/preRunEvent() diff --git a/code/modules/events/creep_awakening.dm b/code/modules/events/creep_awakening.dm deleted file mode 100644 index dc116a56d63..00000000000 --- a/code/modules/events/creep_awakening.dm +++ /dev/null @@ -1,30 +0,0 @@ -/datum/round_event_control/obsessed - name = "Obsession Awakening" - typepath = /datum/round_event/obsessed - max_occurrences = 1 - min_players = 20 - category = EVENT_CATEGORY_HEALTH - description = "A random crewmember becomes obsessed with another." - -/datum/round_event/obsessed - fakeable = FALSE - -/datum/round_event/obsessed/start() - for(var/mob/living/carbon/human/H in shuffle(GLOB.player_list)) - if(!H.client || !(ROLE_OBSESSED in H.client.prefs.be_special) || !H.client.prefs?.read_preference(/datum/preference/toggle/be_antag)) - continue - // BUBBER EDIT ADDITION BEGIN - Antag banned can't roll obsessed - if(is_banned_from(H.client.ckey, list(ROLE_SYNDICATE, BAN_ANTAGONIST))) - continue - // BUBBER EDIT ADDITION END - Antag banned can't roll obsessed - if(H.stat == DEAD) - continue - if(!(H.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) //only station jobs sans nonhuman roles, prevents ashwalkers trying to stalk with crewmembers they never met - continue - if(H.mind.has_antag_datum(/datum/antagonist/obsessed)) - continue - if(!H.get_organ_by_type(/obj/item/organ/brain)) - continue - H.gain_trauma(/datum/brain_trauma/special/obsessed) - announce_to_ghosts(H) - break diff --git a/code/modules/events/dynamic_tweak.dm b/code/modules/events/dynamic_tweak.dm new file mode 100644 index 00000000000..3c5899df018 --- /dev/null +++ b/code/modules/events/dynamic_tweak.dm @@ -0,0 +1,35 @@ +// Simple hidden event that adds a few more latejoins and midrounds to the round +// Keeps Greenshifts on their toes and prevents metagaming +/datum/round_event_control/dynamic_tweak + name = "Dynamic Tweak" + typepath = /datum/round_event/dynamic_tweak + weight = 10 + max_occurrences = 1 + earliest_start = 20 MINUTES + alert_observers = FALSE + category = EVENT_CATEGORY_INVASION + description = "Allows Dynamic to spawn another midround or latejoin. Gives some spice to Greenshifts." + +/datum/round_event_control/dynamic_tweak/New() + . = ..() + if(max_occurrences > 0) + max_occurrences += rand(-1, 1) + +/datum/round_event_control/dynamic_tweak/can_spawn_event(players_amt, allow_magic) + return ..() && SSdynamic.antag_events_enabled && !EMERGENCY_PAST_POINT_OF_NO_RETURN + +/datum/round_event/dynamic_tweak + start_when = 1 + end_when = 2 + fakeable = FALSE + +/datum/round_event/dynamic_tweak/start() + var/new_lights = rand(0, 1) + var/new_heavies = rand(1 - new_lights, 1) // guarantee a heavy if no new light + var/new_latejoins = rand(1 - new_heavies, 1) // guarantee a latejoin if no new heavy + + SSdynamic.rulesets_to_spawn[LIGHT_MIDROUND] += new_lights + SSdynamic.rulesets_to_spawn[HEAVY_MIDROUND] += new_heavies + SSdynamic.rulesets_to_spawn[LATEJOIN] += new_latejoins + + message_admins("Event: Dynamic Tweak added [new_latejoins] latejoin\s, [new_lights] light midround\s and [new_heavies] heavy midround\s.") diff --git a/code/modules/events/false_alarm.dm b/code/modules/events/false_alarm.dm index 6e5cfdc61a1..f862c0be102 100644 --- a/code/modules/events/false_alarm.dm +++ b/code/modules/events/false_alarm.dm @@ -12,7 +12,7 @@ if(!.) return . - if(!length(gather_false_events())) + if(!length(get_potential_false_alarm())) return FALSE return TRUE @@ -26,44 +26,65 @@ /datum/round_event/falsealarm/announce(fake) if(fake) //What are you doing return - var/players_amt = get_active_player_count(alive_check = 1, afk_check = 1, human_check = 1) - var/events_list = gather_false_events(players_amt) - var/datum/round_event_control/event_control - if(forced_type) - event_control = forced_type - else - event_control = pick(events_list) - if(event_control) - var/datum/round_event/Event = new event_control.typepath() - message_admins("False Alarm: [Event]") - Event.kill() //do not process this event - no starts, no ticks, no ends - Event.announce(TRUE) //just announce it like it's happening + var/players_amt = get_active_player_count(alive_check = TRUE, afk_check = TRUE, human_check = TRUE) + var/picked_trigger = forced_type + if(ispath(forced_type, /datum/dynamic_ruleset/midround)) + picked_trigger = new forced_type() -/proc/gather_false_events(players_amt) + var/list/event_pool = get_potential_false_alarm() + + while(length(event_pool) && isnull(picked_trigger)) + var/potential_trigger = pick_n_take(event_pool) + if(istype(potential_trigger, /datum/round_event_control)) + var/datum/round_event_control/event_control = potential_trigger + if(event_control.can_spawn_event(players_amt)) + picked_trigger = event_control + break + + else if(ispath(potential_trigger, /datum/dynamic_ruleset/midround)) + var/datum/dynamic_ruleset/midround/ruleset = new potential_trigger(SSdynamic.get_config()) + if(ruleset.can_be_selected(players_amt)) + picked_trigger = ruleset + break + qdel(ruleset) + + else + stack_trace("Unknown false alarm candidate type: [potential_trigger || "null"]") + + if(istype(picked_trigger, /datum/round_event_control)) + var/datum/round_event_control/event_control = picked_trigger + var/datum/round_event/fake_event = new event_control.typepath() + message_admins("False Alarm: [fake_event]") + fake_event.kill() //do not process this event - no starts, no ticks, no ends + fake_event.announce(TRUE) //just announce it like it's happening + + else if(istype(picked_trigger, /datum/dynamic_ruleset/midround)) + var/datum/dynamic_ruleset/midround/ruleset = picked_trigger + message_admins("False Alarm: [ruleset]") + ruleset.false_alarm() + qdel(ruleset) + +/proc/get_potential_false_alarm() . = list() - for(var/datum/round_event_control/E in SSevents.control) - if(istype(E, /datum/round_event_control/falsealarm)) + for(var/datum/round_event_control/controller as anything in SSevents.control) + if(istype(controller, /datum/round_event_control/falsealarm)) continue - if(!E.can_spawn_event(players_amt)) - continue - - var/datum/round_event/event = E.typepath + var/datum/round_event/event = controller.typepath if(!initial(event.fakeable)) continue - . += E + . += controller + + for(var/datum/dynamic_ruleset/midround/midround as anything in subtypesof(/datum/dynamic_ruleset/midround)) + if(!initial(midround.false_alarm_able)) + continue + . += midround /datum/event_admin_setup/listed_options/false_alarm normal_run_option = "Random Fake Event" /datum/event_admin_setup/listed_options/false_alarm/get_list() - var/list/possible_types = list() - for(var/datum/round_event_control/event_control in SSevents.control) - var/datum/round_event/event = event_control.typepath - if(!initial(event.fakeable)) - continue - possible_types += event_control - return possible_types + return get_potential_false_alarm() /datum/event_admin_setup/listed_options/false_alarm/apply_to_event(datum/round_event/falsealarm/event) event.forced_type = chosen diff --git a/code/modules/events/ghost_role/abductor.dm b/code/modules/events/ghost_role/abductor.dm deleted file mode 100644 index 9eec92365d9..00000000000 --- a/code/modules/events/ghost_role/abductor.dm +++ /dev/null @@ -1,43 +0,0 @@ -/datum/round_event_control/abductor - name = "Abductors" - typepath = /datum/round_event/ghost_role/abductor - weight = 10 - max_occurrences = 1 - min_players = 20 - dynamic_should_hijack = TRUE - category = EVENT_CATEGORY_INVASION - description = "One or more abductor teams spawns, and they plan to experiment on the crew." - -/datum/round_event/ghost_role/abductor - minimum_required = 2 - role_name = "abductor team" - fakeable = FALSE //Nothing to fake here - -/datum/round_event/ghost_role/abductor/spawn_role() - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_ABDUCTOR, role = ROLE_ABDUCTOR, alert_pic = /obj/item/melee/baton/abductor, role_name_text = role_name, amount_to_pick = 2) - - if(length(candidates) < 2) - return NOT_ENOUGH_PLAYERS - - SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_ABDUCTOR_SHIPS) - // BUBBER CHANGE: NO ABDUCTOR PREFS - var/mob/living/carbon/human/agent = new - var/mob/living/carbon/human/scientist = new - - agent.PossessByPlayer(candidates[1].ckey) - scientist.PossessByPlayer(candidates[2].ckey) - // BUBBER CHANGE END - - var/datum/team/abductor_team/T = new - if(T.team_number > ABDUCTOR_MAX_TEAMS) - return MAP_ERROR - - scientist.log_message("has been selected as [T.name] abductor scientist.", LOG_GAME) - agent.log_message("has been selected as [T.name] abductor agent.", LOG_GAME) - - scientist.mind.add_antag_datum(/datum/antagonist/abductor/scientist, T) - agent.mind.add_antag_datum(/datum/antagonist/abductor/agent, T) - - spawned_mobs += list(agent, scientist) - - return SUCCESSFUL_SPAWN diff --git a/code/modules/events/ghost_role/alien_infestation.dm b/code/modules/events/ghost_role/alien_infestation.dm deleted file mode 100644 index d02b0055f65..00000000000 --- a/code/modules/events/ghost_role/alien_infestation.dm +++ /dev/null @@ -1,82 +0,0 @@ -/datum/round_event_control/alien_infestation - name = "Alien Infestation" - typepath = /datum/round_event/ghost_role/alien_infestation - weight = 5 - - min_players = 10 - - dynamic_should_hijack = TRUE - category = EVENT_CATEGORY_ENTITIES - description = "A xenomorph larva spawns on a random vent." - -/datum/round_event_control/alien_infestation/can_spawn_event(players_amt, allow_magic = FALSE) - . = ..() - if(!.) - return . - - for(var/mob/living/carbon/alien/A in GLOB.player_list) - if(A.stat != DEAD) - return FALSE - -/datum/round_event/ghost_role/alien_infestation - announce_when = 400 - - minimum_required = 1 - role_name = "alien larva" - - // 50% chance of being incremented by one - var/spawncount = 1 - fakeable = TRUE - - -/datum/round_event/ghost_role/alien_infestation/setup() - announce_when = rand(announce_when, announce_when + 50) - if(prob(50)) - spawncount++ - -/datum/round_event/ghost_role/alien_infestation/announce(fake) - var/living_aliens = FALSE - for(var/mob/living/carbon/alien/A in GLOB.player_list) - if(A.stat != DEAD) - living_aliens = TRUE - - if(living_aliens || fake) - priority_announce("Unidentified lifesigns detected coming aboard [station_name()]. Secure any exterior access, including ducting and ventilation.", "Lifesign Alert", ANNOUNCER_ALIENS) - - -/datum/round_event/ghost_role/alien_infestation/spawn_role() - var/list/vents = list() - for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/atmospherics/components/unary/vent_pump)) - if(QDELETED(temp_vent)) - continue - if(is_station_level(temp_vent.loc.z) && !temp_vent.welded) - var/datum/pipeline/temp_vent_parent = temp_vent.parents[1] - if(!temp_vent_parent) - continue//no parent vent - //Stops Aliens getting stuck in small networks. - //See: Security, Virology - if(temp_vent_parent.other_atmos_machines.len > 20) - vents += temp_vent - - if(!vents.len) - message_admins("An event attempted to spawn an alien but no suitable vents were found. Shutting down.") - return MAP_ERROR - - var/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_ALIEN, role = ROLE_ALIEN, alert_pic = /mob/living/carbon/alien/larva, role_name_text = role_name) - - if(!length(candidates)) - return NOT_ENOUGH_PLAYERS - - while(spawncount > 0 && vents.len && candidates.len) - var/obj/vent = pick_n_take(vents) - var/mob/dead/observer/selected = pick_n_take(candidates) - var/mob/living/carbon/alien/larva/new_xeno = new(vent.loc) - new_xeno.PossessByPlayer(selected.key) - new_xeno.move_into_vent(vent) - - spawncount-- - message_admins("[ADMIN_LOOKUPFLW(new_xeno)] has been made into an alien by an event.") - new_xeno.log_message("was spawned as an alien by an event.", LOG_GAME) - spawned_mobs += new_xeno - - return SUCCESSFUL_SPAWN diff --git a/code/modules/events/ghost_role/blob.dm b/code/modules/events/ghost_role/blob.dm deleted file mode 100644 index 097620646d8..00000000000 --- a/code/modules/events/ghost_role/blob.dm +++ /dev/null @@ -1,44 +0,0 @@ -/datum/round_event_control/blob - name = "Blob" - typepath = /datum/round_event/ghost_role/blob - weight = 10 - max_occurrences = 1 - - min_players = 20 - - dynamic_should_hijack = TRUE - category = EVENT_CATEGORY_ENTITIES - description = "Spawns a new blob overmind." - -/datum/round_event_control/blob/can_spawn_event(players, allow_magic = FALSE) - if(EMERGENCY_PAST_POINT_OF_NO_RETURN) // no blobs if the shuttle is past the point of no return - return FALSE - - return ..() - -/datum/round_event/ghost_role/blob - announce_chance = 0 - role_name = "blob overmind" - fakeable = TRUE - -/datum/round_event/ghost_role/blob/announce(fake) - if(!fake) - return //the mob itself handles this. - priority_announce("Confirmed outbreak of level 5 biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", ANNOUNCER_OUTBREAK5) - -/datum/round_event/ghost_role/blob/spawn_role() - if(!GLOB.blobstart.len) - return MAP_ERROR - var/icon/blob_icon = icon('icons/mob/nonhuman-player/blob.dmi', icon_state = "blob_core") - blob_icon.Blend("#9ACD32", ICON_MULTIPLY) - blob_icon.Blend(icon('icons/mob/nonhuman-player/blob.dmi', "blob_core_overlay"), ICON_OVERLAY) - var/image/blob_image = image(blob_icon) - var/mob/chosen_one = SSpolling.poll_ghost_candidates(check_jobban = ROLE_BLOB, role = ROLE_BLOB, alert_pic = blob_image, role_name_text = role_name, amount_to_pick = 1, chat_text_border_icon = blob_image) - if(isnull(chosen_one)) - return NOT_ENOUGH_PLAYERS - var/mob/dead/observer/new_blob = chosen_one - var/mob/eye/blob/BC = new_blob.become_overmind() - spawned_mobs += BC - message_admins("[ADMIN_LOOKUPFLW(BC)] has been made into a blob overmind by an event.") - BC.log_message("was spawned as a blob overmind by an event.", LOG_GAME) - return SUCCESSFUL_SPAWN diff --git a/code/modules/events/ghost_role/changeling_event.dm b/code/modules/events/ghost_role/changeling_event.dm deleted file mode 100644 index ce34aaa07fa..00000000000 --- a/code/modules/events/ghost_role/changeling_event.dm +++ /dev/null @@ -1,29 +0,0 @@ -/* -* Changeling midround spawn event. Takes a ghost volunteer and stuffs them into a changeling with their own identity and a flesh space suit. -* They arrive via a meateor, which collides with the station. They are expected to find their own way into the station by whatever means necessary. -* The midround changeling experience is, by nature, more difficult than playing as a roundstart crew changeling. -* -*/ - -/datum/round_event_control/changeling - name = "Changeling Meteor" - typepath = /datum/round_event/ghost_role/changeling - weight = 8 - max_occurrences = 3 - min_players = 20 - dynamic_should_hijack = TRUE - category = EVENT_CATEGORY_ENTITIES - description = "A meteor containing a changeling is summoned and thrown at the exterior of the station." - -/datum/round_event/ghost_role/changeling - minimum_required = 1 - role_name = "space changeling" - fakeable = FALSE - -/datum/round_event/ghost_role/changeling/spawn_role() - var/mob/chosen_one = SSpolling.poll_ghost_candidates(check_jobban = ROLE_CHANGELING, role = ROLE_CHANGELING_MIDROUND, alert_pic = /obj/item/melee/arm_blade, role_name_text = role_name, amount_to_pick = 1) - if(isnull(chosen_one)) - return NOT_ENOUGH_PLAYERS - spawned_mobs += generate_changeling_meteor(chosen_one) - if(spawned_mobs) - return SUCCESSFUL_SPAWN diff --git a/code/modules/events/ghost_role/fugitive_event.dm b/code/modules/events/ghost_role/fugitive_event.dm deleted file mode 100644 index e08304b9925..00000000000 --- a/code/modules/events/ghost_role/fugitive_event.dm +++ /dev/null @@ -1,192 +0,0 @@ -#define TEAM_BACKSTORY_SIZE 4 - -/datum/round_event_control/fugitives - name = "Spawn Fugitives" - typepath = /datum/round_event/ghost_role/fugitives - max_occurrences = 1 - min_players = 20 - earliest_start = 30 MINUTES //deadchat sink, lets not even consider it early on. - category = EVENT_CATEGORY_INVASION - description = "Fugitives will hide on the station, followed by hunters." - map_flags = EVENT_SPACE_ONLY - -/datum/round_event/ghost_role/fugitives - minimum_required = 1 - role_name = "fugitive" - fakeable = FALSE - -/datum/round_event/ghost_role/fugitives/spawn_role() - var/turf/landing_turf = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE) - if(isnull(landing_turf)) - return MAP_ERROR - var/list/possible_backstories = list() - var/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_FUGITIVE, role = ROLE_FUGITIVE, alert_pic = /obj/item/card/id/advanced/prisoner, jump_target = landing_turf) - - if(!length(candidates)) - return NOT_ENOUGH_PLAYERS - - if(length(candidates) < TEAM_BACKSTORY_SIZE || prob(30 - (length(candidates) * 2))) //Solo backstories are always considered if a larger backstory cannot be filled out. Otherwise, it's a rare chance that gets rarer if more people sign up. - possible_backstories += list(FUGITIVE_BACKSTORY_WALDO, FUGITIVE_BACKSTORY_INVISIBLE) //less common as it comes with magicks and is kind of immershun shattering - - if(length(candidates) >= TEAM_BACKSTORY_SIZE)//group refugees - possible_backstories += list(FUGITIVE_BACKSTORY_PRISONER, FUGITIVE_BACKSTORY_CULTIST, FUGITIVE_BACKSTORY_SYNTH) - - var/backstory = pick(possible_backstories) - var/member_size = 3 - var/leader - switch(backstory) - if(FUGITIVE_BACKSTORY_SYNTH) - leader = pick_n_take(candidates) - if(FUGITIVE_BACKSTORY_WALDO, FUGITIVE_BACKSTORY_INVISIBLE) - member_size = 0 //solo refugees have no leader so the member_size gets bumped to one a bit later - var/list/members = list() - var/list/spawned_mobs = list() - if(isnull(leader)) - member_size++ //if there is no leader role, then the would be leader is a normal member of the team. - - for(var/i in 1 to member_size) - members += pick_n_take(candidates) - - for(var/mob/dead/selected in members) - var/mob/living/carbon/human/S = gear_fugitive(selected, landing_turf, backstory) - spawned_mobs += S - if(!isnull(leader)) - gear_fugitive_leader(leader, landing_turf, backstory) - - //after spawning - playsound(src, 'sound/items/weapons/emitter.ogg', 50, TRUE) - new /obj/item/storage/toolbox/mechanical(landing_turf) //so they can actually escape maint - var/hunter_backstory = pick( - HUNTER_PACK_COPS, - HUNTER_PACK_RUSSIAN, - HUNTER_PACK_BOUNTY, - HUNTER_PACK_PSYKER, - HUNTER_PACK_MI13, - ) - addtimer(CALLBACK(src, PROC_REF(check_spawn_hunters), hunter_backstory, 10 MINUTES), 1 MINUTES) - role_name = "fugitive hunter" - return SUCCESSFUL_SPAWN - -/datum/round_event/ghost_role/fugitives/proc/gear_fugitive(mob/dead/selected, turf/landing_turf, backstory) //spawns normal fugitive - var/datum/mind/player_mind = new /datum/mind(selected.key) - player_mind.active = TRUE - var/mob/living/carbon/human/S = new(landing_turf) - player_mind.transfer_to(S) - player_mind.set_assigned_role(SSjob.get_job_type(/datum/job/fugitive)) - player_mind.special_role = ROLE_FUGITIVE - player_mind.add_antag_datum(/datum/antagonist/fugitive) - var/datum/antagonist/fugitive/fugitiveantag = player_mind.has_antag_datum(/datum/antagonist/fugitive) - fugitiveantag.greet(backstory) - - switch(backstory) - if(FUGITIVE_BACKSTORY_PRISONER) - S.equipOutfit(/datum/outfit/prisoner) - if(FUGITIVE_BACKSTORY_CULTIST) - S.equipOutfit(/datum/outfit/yalp_cultist) - if(FUGITIVE_BACKSTORY_WALDO) - S.equipOutfit(/datum/outfit/waldo) - if(FUGITIVE_BACKSTORY_SYNTH) - S.equipOutfit(/datum/outfit/synthetic) - if(FUGITIVE_BACKSTORY_INVISIBLE) - S.equipOutfit(/datum/outfit/invisible_man) - message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a Fugitive by an event.") - S.log_message("was spawned as a Fugitive by an event.", LOG_GAME) - spawned_mobs += S - return S - -///special spawn for one member. it can be used for a special mob or simply to give one normal member special items. -/datum/round_event/ghost_role/fugitives/proc/gear_fugitive_leader(mob/dead/leader, turf/landing_turf, backstory) - var/datum/mind/player_mind = new /datum/mind(leader.key) - player_mind.active = TRUE - //if you want to add a fugitive with a special leader in the future, make this switch with the backstory - var/mob/living/carbon/human/S = gear_fugitive(leader, landing_turf, backstory) - var/obj/item/choice_beacon/augments/A = new(landing_turf) - S.put_in_hands(A) - new /obj/item/autosurgeon(landing_turf) - -/datum/round_event/ghost_role/fugitives/proc/check_spawn_hunters(backstory, remaining_time) - //if the emergency shuttle has been called, spawn hunters now to give them a chance - if(remaining_time == 0 || !EMERGENCY_IDLE_OR_RECALLED) - spawn_hunters(backstory) - return - addtimer(CALLBACK(src, PROC_REF(check_spawn_hunters), backstory, remaining_time - 1 MINUTES), 1 MINUTES) - -/datum/round_event/ghost_role/fugitives/proc/spawn_hunters(backstory) - var/list/candidates = SSpolling.poll_ghost_candidates("Do you wish to be considered for a group of [span_notice(backstory)]?", check_jobban = ROLE_FUGITIVE_HUNTER, alert_pic = /obj/machinery/sleeper, role_name_text = backstory) - shuffle_inplace(candidates) - - var/datum/map_template/shuttle/hunter/ship - switch(backstory) - if(HUNTER_PACK_COPS) - ship = new /datum/map_template/shuttle/hunter/space_cop - if(HUNTER_PACK_RUSSIAN) - ship = new /datum/map_template/shuttle/hunter/russian - if(HUNTER_PACK_BOUNTY) - ship = new /datum/map_template/shuttle/hunter/bounty - if(HUNTER_PACK_PSYKER) - ship = new /datum/map_template/shuttle/hunter/psyker - if(HUNTER_PACK_MI13) - ship = new/datum/map_template/shuttle/hunter/mi13_foodtruck - - 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/placement_turf = locate(x,y,z) - if(!placement_turf) - CRASH("Fugitive Hunters (Created from fugitive event) found no turf to load in") - if(!ship.load(placement_turf)) - CRASH("Loading [backstory] ship failed!") - - for(var/turf/shuttle_turf in ship.get_affected_turfs(placement_turf)) - for(var/obj/effect/mob_spawn/ghost_role/human/fugitive/spawner in shuttle_turf) - if(length(candidates)) - var/mob/our_candidate = candidates[1] - var/mob/spawned_mob = spawner.create_from_ghost(our_candidate) - candidates -= our_candidate - notify_ghosts( - "[spawner.prompt_name] has awoken: [spawned_mob]!", - source = spawned_mob, - header = "Come look!", - ) - else - notify_ghosts( - "[spawner.prompt_name] spawner has been created!", - source = spawner, - header = "Spawn Here!", - ) - - var/list/announcement_text_list = list() - var/announcement_title = "" - switch(backstory) - if(HUNTER_PACK_COPS) - announcement_text_list += "Attention Crew of [GLOB.station_name], this is the Police. A wanted criminal has been reported taking refuge on your station." - announcement_text_list += "We have a warrant from the SSC authorities to take them into custody. Officers have been dispatched to your location." - announcement_text_list += "We demand your cooperation in bringing this criminal to justice." - announcement_title += "Spacepol Command" - if(HUNTER_PACK_RUSSIAN) - announcement_text_list += "Zdraviya zhelaju, [GLOB.station_name] crew. We are coming to your station." - announcement_text_list += "There is a criminal aboard. We will arrest them and return them to the gulag. That's good, yes?" - announcement_title += "Russian Freighter" - if(HUNTER_PACK_BOUNTY) - announcement_text_list += "[GLOB.station_name]. One of our bounty marks has ended up on your station. We will be arriving to collect shortly." - announcement_text_list += "Let's make this quick. If you don't want trouble, stay the hell out of our way." - announcement_title += "Unregistered Signal" - if(HUNTER_PACK_PSYKER) - announcement_text_list += "HEY, CAN YOU HEAR US? We're coming to your station. There's a bad guy down there, really bad guy. We need to arrest them." - announcement_text_list += "We're also offering fortune telling services out of the front door if you have paying customers." - announcement_title += "Fortune-Telling Entertainment Shuttle" - if(HUNTER_PACK_MI13) - announcement_text_list += "Illegal intrusion detected in the crew monitoring network. Central Command has been informed." - announcement_text_list += "Please report any suspicious individuals or behaviour to your local security team." - announcement_title += "Nanotrasen Intrusion Countermeasures Electronics" - if(!length(announcement_text_list)) - announcement_text_list += "Unidentified ship detected near the station." - stack_trace("Fugitive hunter announcement was unable to generate an announcement text based on backstory: [backstory]") - - if(!length(announcement_title)) - announcement_title += "Unknown Signal" - stack_trace("Fugitive hunter announcement was unable to generate an announcement title based on backstory: [backstory]") - - priority_announce(jointext(announcement_text_list, " "), announcement_title) - -#undef TEAM_BACKSTORY_SIZE diff --git a/code/modules/events/ghost_role/morph_event.dm b/code/modules/events/ghost_role/morph_event.dm deleted file mode 100644 index b9841283cee..00000000000 --- a/code/modules/events/ghost_role/morph_event.dm +++ /dev/null @@ -1,35 +0,0 @@ -/datum/round_event_control/morph - name = "Spawn Morph" - typepath = /datum/round_event/ghost_role/morph - weight = 0 - max_occurrences = 1 - category = EVENT_CATEGORY_ENTITIES - description = "Spawns a hungry shapeshifting blobby creature." - min_wizard_trigger_potency = 4 - max_wizard_trigger_potency = 7 - -/datum/round_event/ghost_role/morph - minimum_required = 1 - role_name = "morphling" - -/datum/round_event/ghost_role/morph/spawn_role() - var/mob/chosen_one = SSpolling.poll_ghost_candidates(check_jobban = ROLE_ALIEN, role = ROLE_ALIEN, alert_pic = /mob/living/basic/morph, role_name_text = "morph", amount_to_pick = 1) - if(isnull(chosen_one)) - return NOT_ENOUGH_PLAYERS - var/datum/mind/player_mind = new /datum/mind(chosen_one.key) - player_mind.active = TRUE - - var/turf/spawn_loc = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE) - if(isnull(spawn_loc)) - return MAP_ERROR - - var/mob/living/basic/morph/corpus_accipientis = new(spawn_loc) - player_mind.transfer_to(corpus_accipientis) - player_mind.set_assigned_role(SSjob.get_job_type(/datum/job/morph)) - player_mind.special_role = ROLE_MORPH - player_mind.add_antag_datum(/datum/antagonist/morph) - SEND_SOUND(corpus_accipientis, sound('sound/effects/magic/mutate.ogg')) - message_admins("[ADMIN_LOOKUPFLW(corpus_accipientis)] has been made into a morph by an event.") - corpus_accipientis.log_message("was spawned as a morph by an event.", LOG_GAME) - spawned_mobs += corpus_accipientis - return SUCCESSFUL_SPAWN diff --git a/code/modules/events/ghost_role/nightmare.dm b/code/modules/events/ghost_role/nightmare.dm deleted file mode 100644 index 9f894c237d4..00000000000 --- a/code/modules/events/ghost_role/nightmare.dm +++ /dev/null @@ -1,38 +0,0 @@ -/datum/round_event_control/nightmare - name = "Spawn Nightmare" - typepath = /datum/round_event/ghost_role/nightmare - max_occurrences = 1 - min_players = 20 - dynamic_should_hijack = TRUE - category = EVENT_CATEGORY_ENTITIES - description = "Spawns a nightmare, aiming to darken the station." - min_wizard_trigger_potency = 6 - max_wizard_trigger_potency = 7 - -/datum/round_event/ghost_role/nightmare - minimum_required = 1 - role_name = "nightmare" - fakeable = FALSE - -/datum/round_event/ghost_role/nightmare/spawn_role() - var/mob/chosen_one = SSpolling.poll_ghost_candidates(check_jobban = ROLE_ALIEN, role = ROLE_NIGHTMARE, role_name_text = role_name, amount_to_pick = 1) - if(isnull(chosen_one)) - return NOT_ENOUGH_PLAYERS - var/datum/mind/player_mind = new /datum/mind(chosen_one.key) - player_mind.active = TRUE - - var/turf/spawn_loc = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE) - if(isnull(spawn_loc)) - return MAP_ERROR - - var/mob/living/carbon/human/S = new (spawn_loc) - player_mind.transfer_to(S) - player_mind.set_assigned_role(SSjob.get_job_type(/datum/job/nightmare)) - player_mind.special_role = ROLE_NIGHTMARE - player_mind.add_antag_datum(/datum/antagonist/nightmare) - S.set_species(/datum/species/shadow/nightmare) - playsound(S, 'sound/effects/magic/ethereal_exit.ogg', 50, TRUE, -1) - message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a Nightmare by an event.") - S.log_message("was spawned as a Nightmare by an event.", LOG_GAME) - spawned_mobs += S - return SUCCESSFUL_SPAWN diff --git a/code/modules/events/ghost_role/operative.dm b/code/modules/events/ghost_role/operative.dm index c7ad41d001d..5ea7b39f872 100644 --- a/code/modules/events/ghost_role/operative.dm +++ b/code/modules/events/ghost_role/operative.dm @@ -6,6 +6,9 @@ category = EVENT_CATEGORY_INVASION description = "A single nuclear operative assaults the station." +/datum/round_event_control/operative/can_spawn_event(players_amt, allow_magic) + return ..() && SSdynamic.antag_events_enabled + /datum/round_event/ghost_role/operative minimum_required = 1 role_name = "lone operative" @@ -23,7 +26,6 @@ operative.dna.update_dna_identity() var/datum/mind/Mind = new /datum/mind(chosen_one.key) Mind.set_assigned_role(SSjob.get_job_type(/datum/job/lone_operative)) - Mind.special_role = ROLE_LONE_OPERATIVE Mind.active = TRUE Mind.transfer_to(operative) if(!operative.client?.prefs.read_preference(/datum/preference/toggle/nuke_ops_species)) diff --git a/code/modules/events/ghost_role/revenant_event.dm b/code/modules/events/ghost_role/revenant_event.dm deleted file mode 100644 index 7b18a868e62..00000000000 --- a/code/modules/events/ghost_role/revenant_event.dm +++ /dev/null @@ -1,61 +0,0 @@ -#define REVENANT_SPAWN_THRESHOLD 20 - -/datum/round_event_control/revenant - name = "Spawn Revenant" // Did you mean 'griefghost'? - typepath = /datum/round_event/ghost_role/revenant - weight = 7 - max_occurrences = 1 - min_players = 5 - dynamic_should_hijack = TRUE - category = EVENT_CATEGORY_ENTITIES - description = "Spawns an angry, soul sucking ghost." - min_wizard_trigger_potency = 4 - max_wizard_trigger_potency = 7 - - -/datum/round_event/ghost_role/revenant - var/ignore_mobcheck = FALSE - role_name = "revenant" - -/datum/round_event/ghost_role/revenant/New(my_processing = TRUE, new_ignore_mobcheck = FALSE) - ..() - ignore_mobcheck = new_ignore_mobcheck - -/datum/round_event/ghost_role/revenant/spawn_role() - if(!ignore_mobcheck) - var/deadMobs = 0 - for(var/mob/M in GLOB.dead_mob_list) - deadMobs++ - if(deadMobs < REVENANT_SPAWN_THRESHOLD) - message_admins("Event attempted to spawn a revenant, but there were only [deadMobs]/[REVENANT_SPAWN_THRESHOLD] dead mobs.") - return WAITING_FOR_SOMETHING - - var/mob/chosen_one = SSpolling.poll_ghost_candidates(check_jobban = ROLE_REVENANT, role = ROLE_REVENANT, alert_pic = /mob/living/basic/revenant, amount_to_pick = 1) - if(isnull(chosen_one)) - return NOT_ENOUGH_PLAYERS - var/list/spawn_locs = list() - for(var/mob/living/carbon/human/L in GLOB.dead_mob_list) //look for any harvestable bodies - var/turf/T = get_turf(L) - if(T && is_station_level(T.z)) - spawn_locs += T - if(!spawn_locs.len || spawn_locs.len < 15) //look for any morgue trays, crematoriums, ect if there weren't alot of dead bodies on the station to pick from - for(var/obj/structure/bodycontainer/bc in GLOB.bodycontainers) - var/turf/T = get_turf(bc) - if(T && is_station_level(T.z)) - spawn_locs += T - if(!spawn_locs.len) //If we can't find any valid spawnpoints, try the carp spawns - spawn_locs += find_space_spawn() - if(!spawn_locs.len) //If we can't find either, just spawn the revenant at the player's location - spawn_locs += get_turf(chosen_one) - if(!spawn_locs.len) //If we can't find THAT, then just give up and cry - return MAP_ERROR - - var/mob/living/basic/revenant/revvie = new(pick(spawn_locs)) - revvie.PossessByPlayer(chosen_one.key) - message_admins("[ADMIN_LOOKUPFLW(revvie)] has been made into a revenant by an event.") - revvie.log_message("was spawned as a revenant by an event.", LOG_GAME) - spawned_mobs += revvie - qdel(chosen_one) - return SUCCESSFUL_SPAWN - -#undef REVENANT_SPAWN_THRESHOLD diff --git a/code/modules/events/ghost_role/slaughter_event.dm b/code/modules/events/ghost_role/slaughter_event.dm deleted file mode 100644 index f7af4c22d1a..00000000000 --- a/code/modules/events/ghost_role/slaughter_event.dm +++ /dev/null @@ -1,36 +0,0 @@ -/datum/round_event_control/slaughter - name = "Spawn Slaughter Demon" - typepath = /datum/round_event/ghost_role/slaughter - weight = 1 //Very rare - max_occurrences = 1 - earliest_start = 1 HOURS - min_players = 20 - dynamic_should_hijack = TRUE - category = EVENT_CATEGORY_ENTITIES - description = "Spawns a slaughter demon, to hunt by travelling through pools of blood." - min_wizard_trigger_potency = 6 - max_wizard_trigger_potency = 7 - -/datum/round_event/ghost_role/slaughter - minimum_required = 1 - role_name = "slaughter demon" - -/datum/round_event/ghost_role/slaughter/spawn_role() - var/mob/chosen_one = SSpolling.poll_ghost_candidates(check_jobban = ROLE_ALIEN, role = ROLE_ALIEN, alert_pic = /mob/living/basic/demon/slaughter, role_name_text = role_name, amount_to_pick = 1) - if(isnull(chosen_one)) - return NOT_ENOUGH_PLAYERS - var/datum/mind/player_mind = new /datum/mind(chosen_one.key) - player_mind.active = TRUE - - var/spawn_location = find_space_spawn() - if(!spawn_location) - return MAP_ERROR //This sends an error message further up. - var/mob/living/basic/demon/slaughter/spawned = new(spawn_location) - new /obj/effect/dummy/phased_mob(spawn_location, spawned) - - player_mind.transfer_to(spawned) - - message_admins("[ADMIN_LOOKUPFLW(spawned)] has been made into a slaughter demon by an event.") - spawned.log_message("was spawned as a slaughter demon by an event.", LOG_GAME) - spawned_mobs += spawned - return SUCCESSFUL_SPAWN diff --git a/code/modules/events/ghost_role/space_dragon.dm b/code/modules/events/ghost_role/space_dragon.dm deleted file mode 100644 index 6fac4fe1edf..00000000000 --- a/code/modules/events/ghost_role/space_dragon.dm +++ /dev/null @@ -1,35 +0,0 @@ -/datum/round_event_control/space_dragon - name = "Spawn Space Dragon" - typepath = /datum/round_event/ghost_role/space_dragon - weight = 7 - max_occurrences = 1 - min_players = 20 - dynamic_should_hijack = TRUE - category = EVENT_CATEGORY_ENTITIES - description = "Spawns a space dragon, which will try to take over the station." - min_wizard_trigger_potency = 6 - max_wizard_trigger_potency = 7 - -/datum/round_event/ghost_role/space_dragon - minimum_required = 1 - role_name = "Space Dragon" - announce_when = 10 - -/datum/round_event/ghost_role/space_dragon/announce(fake) - priority_announce("A large organic energy flux has been recorded near [station_name()], please stand by.", "Lifesign Alert") - -/datum/round_event/ghost_role/space_dragon/spawn_role() - var/mob/chosen_one = SSpolling.poll_ghost_candidates(check_jobban = ROLE_SPACE_DRAGON, role = ROLE_SPACE_DRAGON, alert_pic = /mob/living/basic/space_dragon, amount_to_pick = 1) - if(isnull(chosen_one)) - return NOT_ENOUGH_PLAYERS - var/spawn_location = find_space_spawn() - if(isnull(spawn_location)) - return MAP_ERROR - var/mob/living/basic/space_dragon/dragon = new(spawn_location) - dragon.PossessByPlayer(chosen_one.key) - dragon.mind.add_antag_datum(/datum/antagonist/space_dragon) - playsound(dragon, 'sound/effects/magic/ethereal_exit.ogg', 50, TRUE, -1) - message_admins("[ADMIN_LOOKUPFLW(dragon)] has been made into a Space Dragon by an event.") - dragon.log_message("was spawned as a Space Dragon by an event.", LOG_GAME) - spawned_mobs += dragon - return SUCCESSFUL_SPAWN diff --git a/code/modules/events/ghost_role/space_ninja.dm b/code/modules/events/ghost_role/space_ninja.dm deleted file mode 100644 index 99f7cbc9973..00000000000 --- a/code/modules/events/ghost_role/space_ninja.dm +++ /dev/null @@ -1,65 +0,0 @@ -/datum/round_event_control/space_ninja - name = "Spawn Space Ninja" - typepath = /datum/round_event/ghost_role/space_ninja - max_occurrences = 1 - weight = 10 - earliest_start = 20 MINUTES - min_players = 20 - dynamic_should_hijack = TRUE - category = EVENT_CATEGORY_INVASION - description = "A space ninja infiltrates the station." - -/datum/round_event/ghost_role/space_ninja - minimum_required = 1 - role_name = "Space Ninja" - -/datum/round_event/ghost_role/space_ninja/spawn_role() - var/spawn_location = find_space_spawn() - if(isnull(spawn_location)) - return MAP_ERROR - - //selecting a candidate player - var/mob/chosen_one = SSpolling.poll_ghost_candidates(check_jobban = ROLE_NINJA, role = ROLE_NINJA, alert_pic = /obj/item/energy_katana, jump_target = spawn_location, role_name_text = "space ninja", amount_to_pick = 1) - if(isnull(chosen_one)) - return NOT_ENOUGH_PLAYERS - //spawn the ninja and assign the candidate - /// BUBBER EDIT START - var/mob/living/carbon/human/ninja = create_space_ninja(spawn_location) - ninja.PossessByPlayer(chosen_one.key) - ninja.mind.add_antag_datum(/datum/antagonist/ninja) - spawned_mobs += ninja - if(!isprotean(ninja)) - var/loadme = tgui_input_list(ninja, "Do you wish to load your character slot?", "Load Character?", list("Yes!", "No, I want to be random!"), default = "No, I want to be random!", timeout = 60 SECONDS) - var/codename - if(loadme == "Yes!") - ninja.client?.prefs?.safe_transfer_prefs_to(ninja) - codename = tgui_input_text(ninja.client, "What should your codename be?", "Agent Name", "[pick("Master", "Legendary", "Agent", "Shinobi", "Ninja")] [ninja.dna.species.name]", 42, FALSE, TRUE, 300 SECONDS) - codename ? codename : (codename = "[pick("Master", "Legendary", "Agent", "Shinobi", "Ninja")] [ninja.dna.species.name]") - ninja.name = codename - ninja.real_name = codename - ninja.dna.update_dna_identity() - else - ninja.randomize_human_appearance(~(RANDOMIZE_NAME|RANDOMIZE_SPECIES)) - ninja.dna.update_dna_identity() - - var/obj/item/mod/control/ninjamod = locate(isprotean(ninja) ? /obj/item/mod/control/pre_equipped/protean : /obj/item/mod/control/pre_equipped/ninja) in ninja.contents - var/obj/item/mod/module/dna_lock/reinforced/ninja_dna_lock = locate(/obj/item/mod/module/dna_lock/reinforced) in ninjamod.contents - ninja_dna_lock.on_use() - /// BUBBER EDIT END - - message_admins("[ADMIN_LOOKUPFLW(ninja)] has been made into a space ninja by an event.") - ninja.log_message("was spawned as a ninja by an event.", LOG_GAME) - - return SUCCESSFUL_SPAWN - - -//=======//NINJA CREATION PROCS//=======// - -/proc/create_space_ninja(spawn_loc) - var/mob/living/carbon/human/new_ninja = new(spawn_loc) -// new_ninja.randomize_human_appearance(~(RANDOMIZE_NAME|RANDOMIZE_SPECIES)) //SKYRAT EDIT: Player Prefs Ninjas - var/new_name = "[pick(GLOB.ninja_titles)] [pick(GLOB.ninja_names)]" - new_ninja.name = new_name - new_ninja.real_name = new_name -// new_ninja.dna.update_dna_identity() //SKYRAT EDIT: Player Prefs Ninjas - return new_ninja diff --git a/code/modules/events/holiday/vday.dm b/code/modules/events/holiday/vday.dm index 2d8a3bc5496..a0033e7e9cc 100644 --- a/code/modules/events/holiday/vday.dm +++ b/code/modules/events/holiday/vday.dm @@ -53,13 +53,11 @@ /datum/round_event/valentines/proc/forge_valentines_objective(mob/living/lover, mob/living/date) var/datum/antagonist/valentine/valentine = new() valentine.date = date.mind - lover.mind.special_role = "valentine" lover.mind.add_antag_datum(valentine) //These really should be teams but i can't be assed to incorporate third wheels right now /datum/round_event/valentines/proc/forge_third_wheel(mob/living/sad_one, mob/living/date_one, mob/living/date_two) var/datum/antagonist/valentine/third_wheel/third_wheel = new() third_wheel.date = pick(date_one.mind, date_two.mind) - sad_one.mind.special_role = "valentine" sad_one.mind.add_antag_datum(third_wheel) /datum/round_event/valentines/start() diff --git a/code/modules/events/spider_infestation.dm b/code/modules/events/spider_infestation.dm deleted file mode 100644 index ab518fd3af1..00000000000 --- a/code/modules/events/spider_infestation.dm +++ /dev/null @@ -1,36 +0,0 @@ -/datum/round_event_control/spider_infestation - name = "Spider Infestation" - typepath = /datum/round_event/spider_infestation - weight = 10 - max_occurrences = 1 - min_players = 20 - dynamic_should_hijack = TRUE - category = EVENT_CATEGORY_ENTITIES - description = "Spawns spider eggs, ready to hatch." - min_wizard_trigger_potency = 5 - max_wizard_trigger_potency = 7 - -/datum/round_event/spider_infestation - announce_when = 400 - var/spawncount = 2 - -/datum/round_event/spider_infestation/setup() - announce_when = rand(announce_when, announce_when + 50) - -/datum/round_event/spider_infestation/announce(fake) - priority_announce("Unidentified lifesigns detected coming aboard [station_name()]. Secure any exterior access, including ducting and ventilation.", "Lifesign Alert", ANNOUNCER_ALIENS) - -/datum/round_event/spider_infestation/start() - create_midwife_eggs(spawncount) - -/proc/create_midwife_eggs(amount) - while(amount > 0) - var/turf/spawn_loc = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE) - if(isnull(spawn_loc)) - return //Admins will have already been notified of the spawning failure at this point - var/obj/effect/mob_spawn/ghost_role/spider/midwife/new_eggs = new (spawn_loc) - new_eggs.amount_grown = 98 - amount-- - log_game("Midwife spider eggs were spawned via an event.") - return TRUE - diff --git a/code/modules/events/wizard/imposter.dm b/code/modules/events/wizard/imposter.dm index 7e1e4c7847c..bd24cabae3e 100644 --- a/code/modules/events/wizard/imposter.dm +++ b/code/modules/events/wizard/imposter.dm @@ -32,7 +32,6 @@ imposter.wiz_team = master.wiz_team master.wiz_team.add_member(imposter) I.mind.add_antag_datum(imposter) - I.mind.special_role = "imposter" I.log_message("is an imposter!", LOG_ATTACK, color="red") //? SEND_SOUND(I, sound('sound/effects/magic.ogg')) announce_to_ghosts(I) diff --git a/code/modules/jobs/job_exp.dm b/code/modules/jobs/job_exp.dm index 1807ad006ff..83de5030dfc 100644 --- a/code/modules/jobs/job_exp.dm +++ b/code/modules/jobs/job_exp.dm @@ -160,7 +160,7 @@ GLOBAL_PROTECT(exp_to_update) if(isobserver(mob)) play_records[EXP_TYPE_GHOST] = minutes - else if(isliving(mob)) + else if(isliving(mob) && !isnull(mob.mind)) var/mob/living/living_mob = mob var/list/mob_exp_list = living_mob.get_exp_list(minutes) if(!length(mob_exp_list)) diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index f549041725e..75a272f34a0 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -681,3 +681,7 @@ /datum/job/proc/after_latejoin_spawn(mob/living/spawning) SHOULD_CALL_PARENT(TRUE) SEND_GLOBAL_SIGNAL(COMSIG_GLOB_JOB_AFTER_LATEJOIN_SPAWN, src, spawning) + +/// Called when a mob that has this job is admin respawned +/datum/job/proc/on_respawn(mob/new_character) + SSjob.equip_rank(new_character, new_character.mind.assigned_role, new_character.client) diff --git a/code/modules/jobs/job_types/ai.dm b/code/modules/jobs/job_types/ai.dm index 04f4e959e21..cafbf2d276b 100644 --- a/code/modules/jobs/job_types/ai.dm +++ b/code/modules/jobs/job_types/ai.dm @@ -110,3 +110,6 @@ /datum/job/ai/get_radio_information() return "Prefix your message with :b to speak with cyborgs and other AIs." + +/datum/job/ai/on_respawn(mob/new_character) + new_character.AIize() diff --git a/code/modules/jobs/job_types/antagonists/clown_operative.dm b/code/modules/jobs/job_types/antagonists/clown_operative.dm deleted file mode 100644 index 7c1a333da2c..00000000000 --- a/code/modules/jobs/job_types/antagonists/clown_operative.dm +++ /dev/null @@ -1,2 +0,0 @@ -/datum/job/clown_operative - title = ROLE_CLOWN_OPERATIVE diff --git a/code/modules/jobs/job_types/antagonists/nuclear_operative.dm b/code/modules/jobs/job_types/antagonists/nuclear_operative.dm index 6fbfdd50994..f2e38bf27d7 100644 --- a/code/modules/jobs/job_types/antagonists/nuclear_operative.dm +++ b/code/modules/jobs/job_types/antagonists/nuclear_operative.dm @@ -1,8 +1,27 @@ /datum/job/nuclear_operative - title = ROLE_NUCLEAR_OPERATIVE + title = ROLE_OPERATIVE /datum/job/nuclear_operative/get_roundstart_spawn_point() return pick(GLOB.nukeop_start) /datum/job/nuclear_operative/get_latejoin_spawn_point() return pick(GLOB.nukeop_start) + +/datum/job/nuclear_operative/leader + +/datum/job/nuclear_operative/leader/get_roundstart_spawn_point() + return pick(GLOB.nukeop_leader_start) + +/datum/job/nuclear_operative/leader/get_latejoin_spawn_point() + return pick(GLOB.nukeop_leader_start) + +/datum/job/nuclear_operative/clown_operative + title = ROLE_CLOWN_OPERATIVE + +/datum/job/nuclear_operative/clown_operative/leader + +/datum/job/nuclear_operative/clown_operative/leader/get_roundstart_spawn_point() + return pick(GLOB.nukeop_leader_start) + +/datum/job/nuclear_operative/clown_operative/leader/get_latejoin_spawn_point() + return pick(GLOB.nukeop_leader_start) diff --git a/code/modules/jobs/job_types/captain.dm b/code/modules/jobs/job_types/captain.dm index 6424b7986b3..c8e140ab145 100644 --- a/code/modules/jobs/job_types/captain.dm +++ b/code/modules/jobs/job_types/captain.dm @@ -44,7 +44,7 @@ /obj/item/skillchip/sabrage = 5, ) - job_flags = STATION_JOB_FLAGS | HEAD_OF_STAFF_JOB_FLAGS + job_flags = STATION_JOB_FLAGS | HEAD_OF_STAFF_JOB_FLAGS | JOB_ANTAG_PROTECTED rpg_title = "Star Duke" human_authority = JOB_AUTHORITY_HUMANS_ONLY diff --git a/code/modules/jobs/job_types/cyborg.dm b/code/modules/jobs/job_types/cyborg.dm index 389d9a9067e..b2c863645e1 100644 --- a/code/modules/jobs/job_types/cyborg.dm +++ b/code/modules/jobs/job_types/cyborg.dm @@ -51,3 +51,6 @@ /datum/job/cyborg/get_radio_information() return "Prefix your message with :b to speak with other cyborgs and AI." + +/datum/job/cyborg/on_respawn(mob/new_character) + new_character.Robotize(TRUE) diff --git a/code/modules/jobs/job_types/detective.dm b/code/modules/jobs/job_types/detective.dm index 53543b91b21..d467bebac4c 100644 --- a/code/modules/jobs/job_types/detective.dm +++ b/code/modules/jobs/job_types/detective.dm @@ -41,7 +41,7 @@ family_heirlooms = list(/obj/item/reagent_containers/cup/glass/bottle/whiskey) rpg_title = "Thiefcatcher" //I guess they caught them all rip thief... - job_flags = STATION_JOB_FLAGS + job_flags = STATION_JOB_FLAGS | JOB_ANTAG_PROTECTED job_tone = "objection" diff --git a/code/modules/jobs/job_types/head_of_security.dm b/code/modules/jobs/job_types/head_of_security.dm index 4a5b6566da9..470fdeb242c 100644 --- a/code/modules/jobs/job_types/head_of_security.dm +++ b/code/modules/jobs/job_types/head_of_security.dm @@ -35,7 +35,7 @@ family_heirlooms = list(/obj/item/book/manual/wiki/security_space_law) rpg_title = "Guard Leader" - job_flags = STATION_JOB_FLAGS | HEAD_OF_STAFF_JOB_FLAGS + job_flags = STATION_JOB_FLAGS | HEAD_OF_STAFF_JOB_FLAGS | JOB_ANTAG_PROTECTED human_authority = JOB_AUTHORITY_HUMANS_ONLY diff --git a/code/modules/jobs/job_types/prisoner.dm b/code/modules/jobs/job_types/prisoner.dm index 574bbc3d67c..f0e0aa8047f 100644 --- a/code/modules/jobs/job_types/prisoner.dm +++ b/code/modules/jobs/job_types/prisoner.dm @@ -24,7 +24,7 @@ family_heirlooms = list(/obj/item/pen/blue) rpg_title = "Defeated Miniboss" - job_flags = STATION_JOB_FLAGS | JOB_CANNOT_OPEN_SLOTS & ~JOB_REOPEN_ON_ROUNDSTART_LOSS + job_flags = STATION_JOB_FLAGS | JOB_CANNOT_OPEN_SLOTS | JOB_ANTAG_PROTECTED & ~JOB_REOPEN_ON_ROUNDSTART_LOSS /datum/job/prisoner/New() . = ..() diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index 72c29679a0e..8f602e9c0f4 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -44,7 +44,7 @@ JOB_SECURITY_OFFICER_SUPPLY, JOB_SECURITY_OFFICER_SCIENCE, ) - job_flags = STATION_JOB_FLAGS + job_flags = STATION_JOB_FLAGS | JOB_ANTAG_PROTECTED GLOBAL_LIST_INIT(available_depts, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, SEC_DEPT_SCIENCE, SEC_DEPT_SUPPLY)) diff --git a/code/modules/jobs/job_types/station_trait/bridge_assistant.dm b/code/modules/jobs/job_types/station_trait/bridge_assistant.dm index 562ec695d8a..5088fa87b3a 100644 --- a/code/modules/jobs/job_types/station_trait/bridge_assistant.dm +++ b/code/modules/jobs/job_types/station_trait/bridge_assistant.dm @@ -32,7 +32,7 @@ ) rpg_title = "Royal Guard" allow_bureaucratic_error = FALSE - job_flags = STATION_JOB_FLAGS | STATION_TRAIT_JOB_FLAGS + job_flags = STATION_JOB_FLAGS | STATION_TRAIT_JOB_FLAGS | JOB_ANTAG_PROTECTED human_authority = JOB_AUTHORITY_NON_HUMANS_ALLOWED /datum/job/bridge_assistant/after_spawn(mob/living/spawned, client/player_client) diff --git a/code/modules/jobs/job_types/station_trait/cargo_gorilla.dm b/code/modules/jobs/job_types/station_trait/cargo_gorilla.dm index 87363bf9b7b..4f0af0d2d06 100644 --- a/code/modules/jobs/job_types/station_trait/cargo_gorilla.dm +++ b/code/modules/jobs/job_types/station_trait/cargo_gorilla.dm @@ -16,7 +16,7 @@ ) rpg_title = "Beast of Burden" allow_bureaucratic_error = FALSE - job_flags = STATION_TRAIT_JOB_FLAGS | JOB_ANNOUNCE_ARRIVAL | JOB_NEW_PLAYER_JOINABLE | JOB_EQUIP_RANK + job_flags = STATION_TRAIT_JOB_FLAGS | JOB_ANNOUNCE_ARRIVAL | JOB_NEW_PLAYER_JOINABLE | JOB_EQUIP_RANK |JOB_ANTAG_BLACKLISTED /datum/job/cargo_gorilla/get_roundstart_spawn_point() if (length(GLOB.gorilla_start)) @@ -47,4 +47,4 @@ to_chat(spawned, span_boldnotice("You are Cargorilla, a pacifist friend of the station and carrier of freight.")) to_chat(spawned, span_notice("You can pick up crates by clicking on them, and drop them by clicking on the ground.")) - spawned.mind.special_role = "Cargorilla" + LAZYADD(spawned.mind.special_roles, "Cargorilla") diff --git a/code/modules/jobs/job_types/station_trait/human_ai.dm b/code/modules/jobs/job_types/station_trait/human_ai.dm index f0626d11152..34520067e3b 100644 --- a/code/modules/jobs/job_types/station_trait/human_ai.dm +++ b/code/modules/jobs/job_types/station_trait/human_ai.dm @@ -39,7 +39,7 @@ rpg_title = "Omnissiah" random_spawns_possible = FALSE allow_bureaucratic_error = FALSE - job_flags = STATION_JOB_FLAGS | STATION_TRAIT_JOB_FLAGS + job_flags = STATION_JOB_FLAGS | STATION_TRAIT_JOB_FLAGS | JOB_ANTAG_PROTECTED human_authority = JOB_AUTHORITY_NON_HUMANS_ALLOWED //we can safely assume NT doesn't care what species AIs are made of, much less if they can't even afford an AI. /datum/job/human_ai/get_roundstart_spawn_point() diff --git a/code/modules/jobs/job_types/station_trait/veteran_advisor.dm b/code/modules/jobs/job_types/station_trait/veteran_advisor.dm index 1b247189a43..3172155dc96 100644 --- a/code/modules/jobs/job_types/station_trait/veteran_advisor.dm +++ b/code/modules/jobs/job_types/station_trait/veteran_advisor.dm @@ -35,7 +35,7 @@ ) rpg_title = "Royal Advisor" allow_bureaucratic_error = FALSE - job_flags = STATION_JOB_FLAGS | STATION_TRAIT_JOB_FLAGS + job_flags = STATION_JOB_FLAGS | STATION_TRAIT_JOB_FLAGS | JOB_ANTAG_PROTECTED /datum/job/veteran_advisor/get_default_roundstart_spawn_point() for(var/obj/effect/landmark/start/spawn_point as anything in GLOB.start_landmarks_list) diff --git a/code/modules/jobs/job_types/warden.dm b/code/modules/jobs/job_types/warden.dm index bf42b9c033f..87cf40e00f8 100644 --- a/code/modules/jobs/job_types/warden.dm +++ b/code/modules/jobs/job_types/warden.dm @@ -40,7 +40,7 @@ /obj/item/storage/box/lethalshot = 5 ) rpg_title = "Jailor" - job_flags = STATION_JOB_FLAGS | JOB_BOLD_SELECT_TEXT + job_flags = STATION_JOB_FLAGS | JOB_BOLD_SELECT_TEXT | JOB_ANTAG_PROTECTED /datum/outfit/job/warden name = "Warden" diff --git a/code/modules/lost_crew/recovered_crew.dm b/code/modules/lost_crew/recovered_crew.dm index 2d5181a0d4e..b4018b42e50 100644 --- a/code/modules/lost_crew/recovered_crew.dm +++ b/code/modules/lost_crew/recovered_crew.dm @@ -2,9 +2,9 @@ /datum/antagonist/recovered_crew name = "\improper Recovered Crew" show_in_antagpanel = TRUE - job_rank = ROLE_RECOVERED_CREW + pref_flag = ROLE_RECOVERED_CREW show_name_in_check_antagonists = TRUE show_to_ghosts = FALSE silent = TRUE - block_midrounds = FALSE + antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST show_in_roundend = FALSE diff --git a/code/modules/meteors/meteor_mode_controller.dm b/code/modules/meteors/meteor_mode_controller.dm new file mode 100644 index 00000000000..d19d5522bb7 --- /dev/null +++ b/code/modules/meteors/meteor_mode_controller.dm @@ -0,0 +1,31 @@ +/// Global that controls meteor mode +GLOBAL_DATUM(meteor_mode, /datum/meteor_mode_controller) + +/// All this datum does is regularly spawn meteors until the round ends. +/datum/meteor_mode_controller + /// Delay before meteors start falling + var/meteordelay = 5 MINUTES + /// Every [x] minutes, more meteors will spawn + var/rampupdelta = 5 + +/datum/meteor_mode_controller/proc/start_meteor() + if(datum_flags & DF_ISPROCESSING) + return + START_PROCESSING(SSprocessing, src) + +/datum/meteor_mode_controller/process(seconds_per_tick) + if(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) diff --git a/code/modules/meteors/meteor_spawning.dm b/code/modules/meteors/meteor_spawning.dm index 17e47467535..4b852f9613a 100644 --- a/code/modules/meteors/meteor_spawning.dm +++ b/code/modules/meteors/meteor_spawning.dm @@ -77,10 +77,7 @@ * Arguments: * * candidate - The mob (player) to be transformed into a changeling and meteored. */ -/proc/generate_changeling_meteor(mob/dead/selected) - var/datum/mind/player_mind = new(selected.key) - player_mind.active = TRUE - +/proc/generate_changeling_meteor(datum/mind/player_mind) var/turf/picked_start if (SSmapping.is_planetary()) @@ -101,8 +98,7 @@ new_changeling.forceMove(changeling_meteor) //Place our payload inside of its vessel - player_mind.transfer_to(new_changeling) - player_mind.special_role = ROLE_CHANGELING_MIDROUND + player_mind.transfer_to(new_changeling, force_key_move = TRUE) player_mind.add_antag_datum(/datum/antagonist/changeling/space) SEND_SOUND(new_changeling, 'sound/effects/magic/mutate.ogg') message_admins("[ADMIN_LOOKUPFLW(new_changeling)] has been made into a space changeling by an event.") diff --git a/code/modules/mining/lavaland/mining_loot/megafauna/the_thing.dm b/code/modules/mining/lavaland/mining_loot/megafauna/the_thing.dm index eefd66ca35b..dd4ea13b351 100644 --- a/code/modules/mining/lavaland/mining_loot/megafauna/the_thing.dm +++ b/code/modules/mining/lavaland/mining_loot/megafauna/the_thing.dm @@ -61,11 +61,9 @@ QDEL_NULL(radio) connected_ai = null -/obj/item/organ/brain/cybernetic/ai/proc/cancel_rolls(mob/living/source, datum/mind/mind, datum/antagonist/antagonist) +/obj/item/organ/brain/cybernetic/ai/proc/cancel_rolls(mob/living/source, datum/mind/mind, antag_flag) SIGNAL_HANDLER - if(ispath(antagonist, /datum/antagonist/malf_ai)) - return - return CANCEL_ROLL + return antag_flag == ROLE_MALF ? NONE : CANCEL_ROLL /obj/item/organ/brain/cybernetic/ai/proc/get_status_tab_item(mob/living/source, list/items) SIGNAL_HANDLER diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index fc6a70f1785..6bf2227118a 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -266,14 +266,8 @@ GLOB.joined_player_list += character.ckey - if(CONFIG_GET(flag/allow_latejoin_antagonists) && humanc) //Borgs aren't allowed to be antags. Will need to be tweaked if we get true latejoin ais. - if(SSshuttle.emergency) - switch(SSshuttle.emergency.mode) - if(SHUTTLE_RECALL, SHUTTLE_IDLE) - SSdynamic.make_antag_chance(humanc) - if(SHUTTLE_CALL) - if(SSshuttle.emergency.timeLeft(1) > initial(SSshuttle.emergency_call_time)*0.5) - SSdynamic.make_antag_chance(humanc) + if(CONFIG_GET(flag/allow_latejoin_antagonists) && !EMERGENCY_PAST_POINT_OF_NO_RETURN && humanc) //Borgs aren't allowed to be antags. Will need to be tweaked if we get true latejoin ais. + SSdynamic.on_latejoin(humanc) if((job.job_flags & JOB_ASSIGN_QUIRKS) && humanc && CONFIG_GET(flag/roundstart_traits)) SSquirks.AssignQuirks(humanc, humanc.client) @@ -343,23 +337,24 @@ // Prevents "antag rolling" by setting antag prefs on, all jobs to never, and "return to lobby if preferences not available" // Doing so would previously allow you to roll for antag, then send you back to lobby if you didn't get an antag role // This also does some admin notification and logging as well, as well as some extra logic to make sure things don't go wrong -/mob/dead/new_player/proc/check_preferences() +/mob/dead/new_player/proc/check_job_preferences(warn = TRUE) if(!client) return FALSE //Not sure how this would get run without the mob having a client, but let's just be safe. if(client.prefs.read_preference(/datum/preference/choiced/jobless_role) != RETURNTOLOBBY) return TRUE // If they have antags enabled, they're potentially doing this on purpose instead of by accident. Notify admins if so. - var/has_antags = FALSE - if(client.prefs.be_special.len > 0) - has_antags = TRUE + var/has_antags = length(client.prefs.be_special) > 0 if(client.prefs.job_preferences.len == 0) - if(!ineligible_for_roles) - to_chat(src, span_danger("You have no jobs enabled, along with return to lobby if job is unavailable. This makes you ineligible for any round start role, please update your job preferences.")) - ineligible_for_roles = TRUE + if(warn) + to_chat(src, span_danger("You have no jobs enabled, along with return to lobby if job is unavailable. \ + This makes you ineligible for any round start role, please update your job preferences.")) ready = PLAYER_NOT_READY if(has_antags) - log_admin("[src.ckey] has no jobs enabled, return to lobby if job is unavailable enabled and [client.prefs.be_special.len] antag preferences enabled. The player has been forcefully returned to the lobby.") - message_admins("[src.ckey] has no jobs enabled, return to lobby if job is unavailable enabled and [client.prefs.be_special.len] antag preferences enabled. This is an old antag rolling technique. The player has been asked to update their job preferences and has been forcefully returned to the lobby.") + log_admin("[src.ckey] has no jobs enabled, return to lobby if job is unavailable enabled and [client.prefs.be_special.len] \ + antag preferences enabled. The player has been forcefully returned to the lobby.") + message_admins("[src.ckey] has no jobs enabled, return to lobby if job is unavailable enabled and [client.prefs.be_special.len] \ + antag preferences enabled. This is an old antag rolling technique. The player has been asked to update their job preferences \ + and has been forcefully returned to the lobby.") return FALSE //This is the only case someone should actually be completely blocked from antag rolling as well return TRUE diff --git a/code/modules/mob/living/basic/drone/interaction.dm b/code/modules/mob/living/basic/drone/interaction.dm index b0ef90c7a53..b50cea1f773 100644 --- a/code/modules/mob/living/basic/drone/interaction.dm +++ b/code/modules/mob/living/basic/drone/interaction.dm @@ -157,7 +157,7 @@ to_chat(src, "Your onboard antivirus has initiated lockdown. Motor servos are impaired, ventilation access is denied, and your display reports that you are hacked to all nearby.") hacked = TRUE set_shy(FALSE) - mind.special_role = "hacked drone" + LAZYADD(mind.special_roles, "Hacked Drone") REMOVE_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT) speed = 1 //gotta go slow message_admins("[ADMIN_LOOKUPFLW(src)] became a hacked drone hellbent on destroying the station!") @@ -173,7 +173,7 @@ to_chat(src, "Having been restored, your onboard antivirus reports the all-clear and you are able to perform all actions again.") hacked = FALSE set_shy(initial(shy)) - mind.special_role = null + LAZYREMOVE(mind.special_roles, "Hacked Drone") ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT) speed = initial(speed) message_admins("[ADMIN_LOOKUPFLW(src)], a hacked drone, was restored to factory defaults!") diff --git a/code/modules/mob/living/basic/space_fauna/demon/demon.dm b/code/modules/mob/living/basic/space_fauna/demon/demon.dm index ca574cefc5a..76591aa4912 100644 --- a/code/modules/mob/living/basic/space_fauna/demon/demon.dm +++ b/code/modules/mob/living/basic/space_fauna/demon/demon.dm @@ -69,7 +69,6 @@ return // we weren't built for this proc to run mind.set_assigned_role(SSjob.get_job_type(/datum/job/slaughter_demon)) - mind.special_role = ROLE_SLAUGHTER_DEMON mind.add_antag_datum(antag_type) SEND_SOUND(src, 'sound/effects/magic/demon_dies.ogg') diff --git a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm index 2f8c4318802..f69dfaf2b2b 100644 --- a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm +++ b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm @@ -126,7 +126,6 @@ generated_objectives_and_spells = TRUE mind.set_assigned_role(SSjob.get_job_type(/datum/job/revenant)) - mind.special_role = ROLE_REVENANT SEND_SOUND(src, sound('sound/effects/ghost.ogg')) mind.add_antag_datum(/datum/antagonist/revenant) return TRUE diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index bea69cc45ce..760c783cbf2 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -1070,7 +1070,6 @@ /mob/living/carbon/human/get_exp_list(minutes) . = ..() - if(mind.assigned_role.title in SSjob.name_occupations) .[mind.assigned_role.title] = minutes diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index b03e31f1063..320a12e16e2 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -2688,8 +2688,11 @@ GLOBAL_LIST_EMPTY(fire_appearances) /mob/living/proc/get_exp_list(minutes) var/list/exp_list = list() - if(mind && mind.special_role && !(mind.datum_flags & DF_VAR_EDITED)) - exp_list[mind.special_role] = minutes + if(!(mind.datum_flags & DF_VAR_EDITED)) + for(var/datum/antagonist/antag as anything in mind?.antag_datums) + var/flag_to_check = antag.jobban_flag || antag.pref_flag + if(flag_to_check) + exp_list[flag_to_check] = minutes if(mind.assigned_role.title in GLOB.exp_specialmap[EXP_TYPE_SPECIAL]) exp_list[mind.assigned_role.title] = minutes diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index a98c5c09cec..4401a56cbcd 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -25,7 +25,7 @@ if((target_ai.mind && target_ai.mind.active) || SSticker.current_state == GAME_STATE_SETTING_UP) target_ai.mind.transfer_to(src) - if(mind.special_role) + if(is_antag()) to_chat(src, span_userdanger("You have been installed as an AI! ")) to_chat(src, span_danger("You must obey your silicon laws above all else. Your objectives will consider you to be dead.")) if(!mind.has_ever_been_ai) @@ -1081,11 +1081,7 @@ /mob/living/silicon/ai/get_exp_list(minutes) . = ..() - - var/datum/job/ai/ai_job_ref = SSjob.get_job_type(/datum/job/ai) - - .[ai_job_ref.title] = minutes - + .[/datum/job/ai::title] = minutes /mob/living/silicon/ai/GetVoice() . = ..() diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 1862a773e9d..a01e575b07b 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -1051,10 +1051,7 @@ /mob/living/silicon/robot/get_exp_list(minutes) . = ..() - - var/datum/job/cyborg/cyborg_job_ref = SSjob.get_job_type(/datum/job/cyborg) - - .[cyborg_job_ref.title] = minutes + .[/datum/job/cyborg::title] = minutes /mob/living/silicon/robot/proc/untip_roleplay() to_chat(src, span_notice("Your frustration has empowered you! You can now right yourself faster!")) diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index d86b739d646..c95b5545e49 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -222,39 +222,22 @@ return " \[[real_name]\]" return "" -// moved out of admins.dm because things other than admin procs were calling this. /** - * Returns TRUE if the game has started and we're either an AI with a 0th law, or we're someone with a special role/antag datum - * If allow_fake_antags is set to FALSE, Valentines, ERTs, and any such roles with FLAG_FAKE_ANTAG won't pass. -*/ -/proc/is_special_character(mob/M, allow_fake_antags = FALSE) - if(!SSticker.HasRoundStarted()) - return FALSE - if(!istype(M)) - return FALSE - if(iscyborg(M)) //as a borg you're now beholden to your laws rather than greentext - return FALSE - - - // Returns TRUE if AI has a zeroth law *and* either has a special role *or* an antag datum. - if(isAI(M)) - var/mob/living/silicon/ai/A = M - return (A.laws?.zeroth && (A.mind?.special_role || !isnull(M.mind?.antag_datums))) - - if(M.mind?.special_role) - return TRUE - - // Turns 'faker' to TRUE if the antag datum is fake. If it's not fake, returns TRUE directly. - var/faker = FALSE - for(var/datum/antagonist/antag_datum as anything in M.mind?.antag_datums) - if((antag_datum.antag_flags & FLAG_FAKE_ANTAG)) - faker = TRUE - else + * Checks if this mob is an antag + * By default excludes antags like Valentines, which are "fake antags" + */ +/mob/proc/is_antag(blacklisted_antag_flags = ANTAG_FAKE) + for(var/datum/antagonist/antag_datum as anything in mind?.antag_datums) + if(!blacklisted_antag_flags || !(antag_datum.antag_flags & blacklisted_antag_flags)) return TRUE - // If 'faker' was assigned TRUE in the above loop and the argument 'allow_fake_antags' is set to TRUE, this passes. - // Else, return FALSE. - return (faker && allow_fake_antags) + return FALSE + +/mob/living/silicon/robot/is_antag(blacklisted_antag_flags) + return FALSE + +/mob/living/silicon/ai/is_antag(blacklisted_antag_flags) + return ..() && !!(laws?.zeroth) // AIs only count as antags if they have a zeroth law (apparently) /** * Fancy notifications for ghosts @@ -365,16 +348,12 @@ if(usr) log_admin("[key_name(usr)] has offered control of ([key_name(M)]) to ghosts.") message_admins("[key_name_admin(usr)] has offered control of ([ADMIN_LOOKUPFLW(M)]) to ghosts") - var/poll_message = "Do you want to play as [span_danger(M.real_name)]?" - if(M.mind) - poll_message = "[poll_message] Job: [span_notice(M.mind.assigned_role.title)]." - if(M.mind.special_role) - poll_message = "[poll_message] Status: [span_boldnotice(M.mind.special_role)]." - else - var/datum/antagonist/A = M.mind.has_antag_datum(/datum/antagonist/) - if(A) - poll_message = "[poll_message] Status: [span_boldnotice(A.name)]." - var/mob/chosen_one = SSpolling.poll_ghosts_for_target(poll_message, check_jobban = ROLE_PAI, poll_time = 10 SECONDS, checked_target = M, alert_pic = M, role_name_text = "ghost control") + var/whomst = span_danger(M.real_name) + if(M.mind && !is_unassigned_job(M.mind?.assigned_role)) + whomst += "Job: [span_notice(M.mind.assigned_role.title)]." + if(length(M.mind?.get_special_roles())) + whomst += "Status: [span_boldnotice(english_list(M.mind?.get_special_roles()))]." + var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to play as [whomst]?", check_jobban = ROLE_PAI, poll_time = 10 SECONDS, checked_target = M, alert_pic = M, role_name_text = "ghost control") if(chosen_one) to_chat(M, "Your mob has been taken over by a ghost!") @@ -411,13 +390,17 @@ /mob/proc/get_policy_keywords() . = list() . += "[type]" - if(mind) - if(mind.assigned_role.policy_index) - . += mind.assigned_role.policy_index - . += mind.assigned_role.title //A bit redunant, but both title and policy index are used - . += mind.special_role //In case there's something special leftover, try to avoid - for(var/datum/antagonist/antag_datum as anything in mind.antag_datums) - . += "[antag_datum.type]" + if(isnull(mind)) + return + if(mind.assigned_role.policy_index) + . += mind.assigned_role.policy_index + . += mind.assigned_role.title //A bit redunant, but both title and policy index are used + for(var/datum/antagonist/antag_datum as anything in mind.antag_datums) + . += "[antag_datum.type]" + if(antag_datum.pref_flag) + . += antag_datum.pref_flag + if(antag_datum.jobban_flag) + . += antag_datum.jobban_flag ///Can the mob see reagents inside of containers? /mob/proc/can_see_reagents() diff --git a/code/modules/mob/mob_lists.dm b/code/modules/mob/mob_lists.dm index f43f82ac246..4e50a4d8595 100644 --- a/code/modules/mob/mob_lists.dm +++ b/code/modules/mob/mob_lists.dm @@ -94,13 +94,13 @@ ///Adds the cliented mob reference to the list of living player-mobs. If the mob is an antag, it adds it to the list of living antag player-mobs. /mob/proc/add_to_current_living_players() GLOB.alive_player_list |= src - if(mind && (mind.special_role || length(mind.antag_datums))) + if(is_antag(NONE)) add_to_current_living_antags() ///Removes the mob reference from the list of living player-mobs. If the mob is an antag, it removes it from the list of living antag player-mobs. /mob/proc/remove_from_current_living_players() GLOB.alive_player_list -= src - if(LAZYLEN(mind?.antag_datums)) + if(is_antag(NONE)) remove_from_current_living_antags() @@ -110,9 +110,10 @@ return for (var/datum/antagonist/antagonist in mind.antag_datums) - if (antagonist.count_against_dynamic_roll_chance) - GLOB.current_living_antags |= src - return + if (antagonist.antag_flags & ANTAG_SKIP_GLOBAL_LIST) + continue + GLOB.current_living_antags |= src + return ///Removes the mob reference from the list of living antag player-mobs. /mob/proc/remove_from_current_living_antags() diff --git a/code/modules/modular_computers/computers/item/pda.dm b/code/modules/modular_computers/computers/item/pda.dm index cd81857ed84..108750d2a10 100644 --- a/code/modules/modular_computers/computers/item/pda.dm +++ b/code/modules/modular_computers/computers/item/pda.dm @@ -209,7 +209,7 @@ if(from_message_menu) log_bomber(null, null, target, "'s tablet exploded as [target.p_they()] tried to open their tablet message menu because of a recent tablet bomb.") else - log_bomber(bomber, "successfully tablet-bombed", target, "as [target.p_they()] tried to reply to a rigged tablet message [bomber && !is_special_character(bomber) ? "(SENT BY NON-ANTAG)" : ""]") + log_bomber(bomber, "successfully tablet-bombed", target, "as [target.p_they()] tried to reply to a rigged tablet message [bomber?.is_antag() ? "" : "(SENT BY NON-ANTAG)"]") if (ismob(loc)) var/mob/loc_mob = loc diff --git a/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm index 3600fb7129a..6292be024b7 100644 --- a/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm +++ b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm @@ -645,7 +645,7 @@ // Log in the talk log source.log_talk(message, subtle ? LOG_SUBTLER : LOG_PDA, tag="[shell_addendum][rigged ? "Rigged" : ""] PDA: [computer.saved_identification] to [signal.format_target()]") // BUBBER EDIT CHANGE - SUBTLE MESSAGES - Original: source.log_talk(message, LOG_PDA, tag="[shell_addendum][rigged ? "Rigged" : ""] PDA: [computer.saved_identification] to [signal.format_target()]") if(rigged) - log_bomber(sender, "sent a rigged PDA message (Name: [fake_name]. Job: [fake_job]) to [english_list(stringified_targets)] [!is_special_character(sender) ? "(SENT BY NON-ANTAG)" : ""]") + log_bomber(sender, "sent a rigged PDA message (Name: [fake_name]. Job: [fake_job]) to [english_list(stringified_targets)] [sender.is_antag() ? "" : "(SENT BY NON-ANTAG)"]") /** BUBBER EDIT CHANGE BEGIN - SUBTLE MESSAGES - Original: * var/ghost_message = span_game_say("[span_name(signal.format_sender())] [rigged ? "(as [span_name(fake_name)]) Rigged " : ""]PDA Message --> [span_name("[signal.format_target()]")]: \"[signal.format_message()]\"") diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm index 503fe9d8d13..b7c761e49bc 100644 --- a/code/modules/projectiles/projectile/magic.dm +++ b/code/modules/projectiles/projectile/magic.dm @@ -367,16 +367,12 @@ /obj/projectile/magic/wipe/proc/possession_test(mob/living/carbon/target) var/datum/brain_trauma/special/imaginary_friend/trapped_owner/trauma = target.gain_trauma(/datum/brain_trauma/special/imaginary_friend/trapped_owner) - var/poll_message = "Do you want to play as [span_danger(target.real_name)]?" - if(target.mind) - poll_message = "[poll_message] Job:[span_notice(target.mind.assigned_role.title)]." - if(target.mind && target.mind.special_role) - poll_message = "[poll_message] Status:[span_boldnotice(target.mind.special_role)]." - else if(target.mind) - var/datum/antagonist/A = target.mind.has_antag_datum(/datum/antagonist/) - if(A) - poll_message = "[poll_message] Status:[span_boldnotice(A.name)]." - var/mob/chosen_one = SSpolling.poll_ghosts_for_target(poll_message, check_jobban = ROLE_PAI, poll_time = 10 SECONDS, checked_target = target, alert_pic = target, role_name_text = "bolt of possession") + var/whomst = span_danger(target.real_name) + if(!is_unassigned_job(target.mind?.assigned_role)) + whomst += "Job: [span_notice(target.mind.assigned_role.title)]." + if(length(target.mind?.get_special_roles())) + whomst += "Status: [span_boldnotice(english_list(target.mind.get_special_roles()))]." + var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to play as [whomst]?", check_jobban = ROLE_PAI, poll_time = 10 SECONDS, checked_target = target, alert_pic = target, role_name_text = "bolt of possession") if(target.stat == DEAD)//boo. return if(chosen_one) diff --git a/code/modules/research/server.dm b/code/modules/research/server.dm index 5048163ea92..70c37d3d066 100644 --- a/code/modules/research/server.dm +++ b/code/modules/research/server.dm @@ -158,7 +158,7 @@ if(!tool.tool_behaviour) return ..() // Only antags are given the training and knowledge to disassemble this thing. - if(!is_special_character(user)) + if(!user.is_antag()) if(user.combat_mode) return ITEM_INTERACT_SKIP_TO_ATTACK balloon_alert(user, "you can't find an obvious maintenance hatch!") diff --git a/code/modules/shuttle/shuttle_events/humans.dm b/code/modules/shuttle/shuttle_events/humans.dm index 99f4006d5bb..3598542c16b 100644 --- a/code/modules/shuttle/shuttle_events/humans.dm +++ b/code/modules/shuttle/shuttle_events/humans.dm @@ -98,7 +98,7 @@ remove_from_list_when_spawned = TRUE self_destruct_when_empty = TRUE - role_type = ROLE_NUCLEAR_OPERATIVE + role_type = ROLE_OPERATIVE /datum/outfit/shuttle_nukie name = "Shuttle Nuclear Operative" diff --git a/code/modules/unit_tests/dynamic_ruleset_sanity.dm b/code/modules/unit_tests/dynamic_ruleset_sanity.dm index 552959220c8..b42b34f99df 100644 --- a/code/modules/unit_tests/dynamic_ruleset_sanity.dm +++ b/code/modules/unit_tests/dynamic_ruleset_sanity.dm @@ -1,24 +1,13 @@ -/// Verifies that roundstart dynamic rulesets are setup properly without external configuration. -/datum/unit_test/dynamic_roundstart_ruleset_sanity - -/datum/unit_test/dynamic_roundstart_ruleset_sanity/Run() - for (var/datum/dynamic_ruleset/roundstart/ruleset as anything in subtypesof(/datum/dynamic_ruleset/roundstart)) - var/has_scaling_cost = initial(ruleset.scaling_cost) - var/is_lone = initial(ruleset.flags) & (LONE_RULESET | HIGH_IMPACT_RULESET) - - if (has_scaling_cost && is_lone) - TEST_FAIL("[ruleset] has a scaling_cost, but is also a lone/highlander ruleset.") - else if (!has_scaling_cost && !is_lone) - TEST_FAIL("[ruleset] has no scaling cost, but is also not a lone/highlander ruleset.") +/// Verifies that dynamic rulesets are setup properly without external configuration. +/datum/unit_test/dynamic_ruleset_sanity +/datum/unit_test/dynamic_ruleset_sanity/Run() for (var/datum/dynamic_ruleset/midround/ruleset as anything in subtypesof(/datum/dynamic_ruleset/midround)) - if(initial(ruleset.abstract_type) == ruleset) - if(initial(ruleset.weight)) - TEST_FAIL("[ruleset] is abstract and should never run, it should also have 0 weight set.") + if(!initial(ruleset.config_tag)) continue - var/midround_ruleset_style = initial(ruleset.midround_ruleset_style) - if (midround_ruleset_style != MIDROUND_RULESET_STYLE_HEAVY && midround_ruleset_style != MIDROUND_RULESET_STYLE_LIGHT) - TEST_FAIL("[ruleset] has an invalid midround_ruleset_style, it should be MIDROUND_RULESET_STYLE_HEAVY or MIDROUND_RULESET_STYLE_LIGHT") + var/midround_ruleset_style = initial(ruleset.midround_type) + if (midround_ruleset_style != HEAVY_MIDROUND && midround_ruleset_style != LIGHT_MIDROUND) + TEST_FAIL("[ruleset] has an invalid midround_ruleset_style, it should be HEAVY_MIDROUND or LIGHT_MIDROUND") /// Verifies that dynamic rulesets have unique antag_flag. /datum/unit_test/dynamic_unique_antag_flags @@ -27,13 +16,17 @@ var/list/known_antag_flags = list() for (var/datum/dynamic_ruleset/ruleset as anything in subtypesof(/datum/dynamic_ruleset)) - if (isnull(initial(ruleset.antag_datum))) + if(!initial(ruleset.config_tag)) continue - var/antag_flag = initial(ruleset.antag_flag) + var/antag_flag = initial(ruleset.pref_flag) + // null antag flag is valid for rulesets with no associated preferecne if (isnull(antag_flag)) - TEST_FAIL("[ruleset] has a null antag_flag!") + // however if you set preview_antag_datum, it is assumed you do have a preference, and thus should have a flag + if (initial(ruleset.preview_antag_datum)) + TEST_FAIL("[ruleset] sets preview_antag_datum, but has no pref_flag! \ + If you want to use a preview antag datum, you must set a pref_flag.") continue if (antag_flag in known_antag_flags) diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_clownoperativemidround.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_clownoperativemidround.png new file mode 100644 index 0000000000000000000000000000000000000000..d61c10e74871c60a24a3573afb470553fdf7996a GIT binary patch literal 2004 zcmYk7cU05K7RP^5r76-;5m*a6q_>Ai$It>H8c>iPiXwpp5yDbJD25FpWoZF1=u?91 z0s=~12~ANBB%wnHH8fEJ1jr#FATO)?+;`5*-1%qjz32P=%nTL^v6m1%DGC69grmci zYrN?H*E}l3Tj@BY2`}7UwzOz_d3Hx`hWw;nWCN&nF*ExUz0`178JCY!1Nzv^0wlwCL@_r z(J)m+u;2|#c|mzUS8V{_r=aY74{!hg;q#7Htli?u7-c>PH+M<&>M%_k9GYSrE*K_| zXDr77+sa_4JeELw$Lai70cr8P3#GT=>X`I?=?lHWMsS;RL!0wyLjnc4DsRW1OUb4V z%6Xt(V?ei0iOPIzU<|WDH%dxo64uuV!HKwywX%Vk?dV&#$kUfreF4x_ng5M~49vE! zX)A-|DBO2FaSuI8ImU9`!(9 zCM1n)p9gQhU)dJpXzg}PA~WhEm>+7LTiYw(YDLVL%1P#A=dLU;Pr7yY&`rxq!5yvp zI}qUQO`W~Au&}qDeHrzbYe=pLFT5PS1)sM#9HKU&T8Xup-lsN{e`4|96540JZgw7s zV^3iw1W+5qeXWjVr|)@cU4ET`1cNT+xfj_}$DV;C{Mo*#qcpRf@)wzs)Uh!B6vkvw z`0P=JfqwR#kse`1E1VZuxDg0&JJS7vFFh(hFc7s26;ANZE{1c%IYM+OZ|uqkM0+RnI6zT zRmz_j%EL#*Gja&*p4d-#xH%_dU=q}xq}e9cU&pQ04nA?95EFjT(^{T2_AUK0QAvzR zt$j$L)I!1^(2vwX`ylS_Xx?6L6RIO|hfmV4_0CP7`BF*bibdsZA5ZwR)wY1g@eJU&0~c%di5w($`P%wD)m2H z-_|ZRKKJ#E=^H>3Qb-Hv1vr=Vhzc077uYj$s(D>6ZlpAinFPW*+`Zs=+J5g_rt zH#LHziGg2UUQ50_iO4Jk8Z?xGlv6<~RI#0OlH@%QM+ydV5`;trez~Q8XxnoAhQ;Fqe_TJn6qZb{|b!V%$}d8rhA0PQv=o~#X?J}2VHR24cCyXC_9gr2JsJj!G7FFU z<<37WchE@9fOr^Zb-(aldUPSuJ|B@$mBn_aalP0|^}3sv;Z4S`-b+iEHFmAyHrf{l zAh?98@AY}l(bA>Pm1g#3c-f>?B&2I4?H3JyFqZRL+*Oul0q(7`ULIFBAN_N;E2-m4 zLz;Y(Qk&-BV9%_`Dad(qn1R~Wzi<6>S3~)Or{M$LV@xYW3VN#3Z;-zJWHCnw#j(9l ziCS9GvP^%xI$TZeGnAm!i^7tOfLREZM3!+zYnrHGGp4EIvl@D+l4xoh)?GK6NS&ZD6m>_ zi6G_U5VU5iycfK_O(TYhAzra?HTF}G-PYJ^9Va_J79UUSxKvjd^K!WHIo_O*_!X;V z1ZQ&1+*7yI<2C5X=^9ozk!b6he$vy5X7Yxb{;KTK8L5o5j>+ zt>cf$%5uEU;-07;8~0#yYd;-lG{V;n<>%(@1pt9~GfR}O|L&F<>f81wX#-j5{$7e% zsfEIOrRiB23S5e8Qfg6pV{W!ruMbX+5Kl2jW*U9Xyg@T+M{7@%zOMJ08W!f(53F zxz@y#obyr-0~0>9Ye-qp++)EkdROkvC4s}|$aKfyv)qVSu+I7P@r$h6;HdMtsas4f z5r5!4XTlAx&Y$Xs*M$g!{04g0+*Hb^-cC*Uxe&@3ADJYh|2!SN0&0Wc z()wEg9J>qln3|ZC-g)(6ihg|?vqv?9oE-LZg@3$m zE_j7xQkt+eG{;jd&?%Hg#Ky-bK2@+@t_&KtvA-MuDNZr(hDw0d*{U^c6d$MV{psZ#BTH+O3D7a=1p!|%ki|-b0QD!EKBW*m zC2WB<`lXU*N?454VdrvBkFdc1X|SyX3zTnHfSJ^Qr`wAW9&N?sxrbr z?tvI7H_XO;9RmPqV@FK1ejJ^>&Q+*}$5p=tCSG&%&LjF!*h|j+A~{GI7BDw15^wf^ zwD`*X>su{XQw#GKTBQfCn-Qx{z55zmGdO^dl5X>QnrNvm0)brW3>_7f&LY)Lt9#-| zu3gf$!wMwkT{|~PYEmQ#9^9XEn(8aobK3xrBSJ~ON_S)8!_2d$VZ;rHRftRRBXiie zw>d|5QImw~iSHjrAilzhnL?^}_tPkLV#qTrOXQyMDjenBiH>m2NjNmcn3#LcsstBs zIi+TL^^Z2A(1YK^mKGO9L-FploSx2S9C}LH6tiCbk~UC=$a7iVOOoQomS!bg*dY5~ zD;}Wa@X2+~2ODjlluIE~xahC-%A;RbO;oT*GplC#+avXfPwEuPtYwHx#fUHMYcW33 z#IyvcvG#S0XPZyZOzjh;N@Kh9#D_0q^q&tHtHSHC$pc49~4;lHSCMK zQNft#^;=&zod|4d^kFAt^$qBd*K{F zK!Wz9Rkip=1($0SZ=@g)hI*}r|Akhaonc~Ri|Mr{AEey6wSBPO6*&IMJIqsA2LsY)mn?$J{TU?Z;n)m?7nt7Ksu;y8D+eK? z{sE^8i|dJ_GZ1)=N*FDZS?0TS12m)OP^A#`!|!ngvrEC&htKznE4I_Wq{Hk(oL2Iv zK6OoF#`^*ef$yi1CZA&tM$+svmfmMqtQ85Z?U9kh)4O9oA&K&39YKO~OcOrPVfssp zsuXg7m%Hde%o|$$4$&}<%6K$8w6Yt#BHOpMwmcVrelb%i)btnvVkrRufI;d=!ePx& zDB7OzFOsQ|1`VZXaiPD%W{trqVzHxrZixDqSv3sykIklMQ0NT{*yECYJI?VBcUWHGfNsD0I-cMfQw>M#a2a*e<1qd zL6|LUj+UMDbe0{^mu#=uU$(6s?Is@7nD zk^XuRLY^#in}fB2)n(_$7`=XlKf?bO`S$F@)>q2q27G|=>aDyP#6oL z;6ybQ+con0Bj!W#A1p+o?J_pLqr40yrxXYK(l76GYsee^%ptfwt}TJ3H?E<|8wzcy zMneN{LiLwEWGY)U&SM-8(DoVLwVUo=!#_jL9&>egj*b*gNMD`!`?zxg`Y; + +type DynamicConfig = Record; + +type typePath = string; + +type Player = { + key: string; +}; + +type RulesetReport = RulesetType & { + index: number; + selected_players: Player[]; + hidden: BooleanLike; +}; + +type RulesetType = { + name: string; + id: string; + typepath: typePath; + admin_disabled: BooleanLike; +}; + +type Data = { + current_tier?: { + number: number; + name: string; + }; + ruleset_count?: RulesetCount; + full_config?: DynamicConfig; + queued_rulesets: RulesetReport[]; + active_rulesets: RulesetReport[]; + all_rulesets: Record; + time_until_lights: number; + time_until_heavies: number; + time_until_latejoins: number; + time_until_next_midround: number; + time_until_next_latejoin: number; + failed_latejoins: number; + light_midround_chance: number; + heavy_midround_chance: number; + latejoin_chance: number; + roundstarted: BooleanLike; + config_even_enabled: BooleanLike; + light_chance_maxxed: BooleanLike; + heavy_chance_maxxed: BooleanLike; + latejoin_chance_maxxed: BooleanLike; + next_dynamic_tick: number; + antag_events_enabled: BooleanLike; +}; + +function formatTime(seconds: number): string { + seconds /= 10; + if (seconds < 0) { + return 'never'; + } + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.round(seconds % 60); + + return `${hours}h ${minutes}m ${secs}s`; +} + +function getPlayerString(players: Player[]): string { + if (players.length === 0) { + return 'No one'; + } else if (players.length === 1) { + return players[0].key; + } else if (players.length === 2) { + return `${players[0].key} and ${players[1].key}`; + } + let playerString = ''; + for (let i = 0; i < players.length; i++) { + playerString += players[i].key; + if (i < players.length - 1) { + playerString += ', '; + } + if (i === players.length - 2) { + playerString += 'and '; + } + } + return playerString; +} + +function readableRulesesetCategory(ruleset_category: string): string { + // Replace underlines with spaces and auto-capitalize first letter of every word + return ruleset_category + .replace(/_/g, ' ') + .split(' ') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +const StatusPanel = () => { + const { data, act } = useBackend(); + const { + current_tier, + ruleset_count, + time_until_lights, + time_until_heavies, + time_until_latejoins, + time_until_next_midround, + time_until_next_latejoin, + failed_latejoins, + light_midround_chance, + heavy_midround_chance, + latejoin_chance, + roundstarted, + light_chance_maxxed, + heavy_chance_maxxed, + latejoin_chance_maxxed, + next_dynamic_tick, + } = data; + + if (!current_tier) { + return ( + + + + + + ); + } + + return ( + + + + {current_tier.number} ({current_tier.name}) + + {!roundstarted && ( + + )} + + {ruleset_count && + Object.entries(ruleset_count).map(([name, count]) => ( + + + {count} + {(name !== 'roundstart' || !roundstarted) && ( + + + + + + ) : ( + <> + + + + + {time_until_next_midround > 0 + ? formatTime(time_until_next_midround) + : `Next dynamic tick (${formatTime(next_dynamic_tick)})`} + + + + + + + + + + + + + {light_midround_chance}% + + + + + + + + + + )} + {time_until_heavies > 0 ? ( + + + + {formatTime(time_until_heavies)} + + + + + + + ) : ( + <> + + + + + {time_until_next_midround > 0 + ? formatTime(time_until_next_midround) + : `Next dynamic tick (${formatTime(next_dynamic_tick)})`} + + + + + + + + + + + + + {heavy_midround_chance}% + + + + + + + + + + )} + {time_until_latejoins > 0 ? ( + + + + {formatTime(time_until_latejoins)} + + + + + + + ) : ( + <> + + + + + {time_until_next_latejoin + ? formatTime(time_until_next_latejoin) + : 'Next latejoin'} + + + + + + + + + + + + + {latejoin_chance}% ({failed_latejoins} failed attempts) + + + + + + + + + + )} + + ); +}; + +// This just reports the entire config +const ConfigPanel = () => { + const { data, act } = useBackend(); + const { full_config } = data; + // Config given to us is basically just a big json object + // Future TODO make this a whole functional config editor + + if (!full_config) { + return ( + + No config loaded - refer to repo defaults for reference. + + ); + } + + const configKeys = Object.keys(full_config); + const [shownConfig, setShownConfig] = useState(configKeys[0]); + + return ( + + + + This is the current config read by the dynamic system when running + rulesets. If you want to edit these values temporarily, you can do so + via View Variables. (Note: Editing tier configs has no effect after + roundstart) + + + + setShownConfig(key)} + /> + + +
+ + {Object.entries(full_config[shownConfig]).map( + ([config_name, config]) => ( + + + {JSON.stringify(config)} + + + ), + )} + +
+
+
+ ); +}; + +// This is where you can see queued rulesets, active rulesets, and trigger new ones +const RulesetsPanel = () => { + const { data, act } = useBackend(); + const { all_rulesets, queued_rulesets, active_rulesets, roundstarted } = data; + + const any_admin_disabled = Object.values(all_rulesets).some((ruleset_list) => + ruleset_list.some((ruleset) => ruleset.admin_disabled), + ); + const all_admin_disabled = Object.values(all_rulesets).every((ruleset_list) => + ruleset_list.every((ruleset) => ruleset.admin_disabled), + ); + + const [searchText, setSearchText] = useState(''); + const searchFilter = createSearch( + searchText, + (ruleset: RulesetType) => ruleset.name, + ); + + return ( + + +
+ + {queued_rulesets.length === 0 ? ( + + No rulesets queued. + + ) : ( + queued_rulesets.map((ruleset) => ( + +
+
+ + +
+ + {active_rulesets.length === 0 ? ( + + No rulesets active. + + ) : ( + active_rulesets.map((ruleset) => ( + + + + {ruleset.name} ({ruleset.id}) + + + + act('hide_ruleset', { + ruleset_index: ruleset.index, + }) + } + /> + + +
+ Selected: {getPlayerString(ruleset.selected_players)} +
+
+ )) + )} +
+
+
+ + +
+ + + + + } + > + + {Object.entries(all_rulesets).map( + ([ruleset_category, ruleset_list]) => ( + + + +

{readableRulesesetCategory(ruleset_category)}

+
+ {ruleset_list + .filter(searchFilter) + .sort((a, b) => (a.name > b.name ? 1 : -1)) + .map((ruleset, index) => ( + + + {ruleset_category === 'roundstart' || + ruleset_category === 'latejoin' ? ( + +
+
+
+ ); +}; + +enum TABS { + Status = 'Status', + Rulesets = 'Rulesets', + Config = 'Config', +} + +export const DynamicAdmin = () => { + const { act, data } = useBackend(); + const { config_even_enabled, antag_events_enabled } = data; + + // disable config tab if config_even_enabled is false + const tabs_filtered = Object.keys(TABS).filter( + (tab) => tab !== TABS.Config || config_even_enabled, + ); + + const [currentTab, setCurrentTab] = useState(tabs_filtered[0]); + + let componentShown; + + switch (currentTab) { + case TABS.Status: + componentShown = ; + break; + case TABS.Config: + componentShown = ; + break; + case TABS.Rulesets: + componentShown = ; + break; + default: + componentShown = ; + } + + return ( + + +
+ act('toggle_antag_events')} + > + Antag Events + + + + } + > + + {tabs_filtered.map((tab) => ( + setCurrentTab(tab)} + > + {tab} + + ))} + + {componentShown} +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/DynamicTester.tsx b/tgui/packages/tgui/interfaces/DynamicTester.tsx new file mode 100644 index 00000000000..af86fff955b --- /dev/null +++ b/tgui/packages/tgui/interfaces/DynamicTester.tsx @@ -0,0 +1,145 @@ +import { useState } from 'react'; +import { + Box, + NumberInput, + Section, + Stack, + Table, + Tabs, + Tooltip, +} from 'tgui-core/components'; + +import { useBackend } from '../backend'; +import { Window } from '../layouts'; + +type RulesetReport = { + name: string; + weight: number; + max_candidates: number; + min_candidates: number; + comment: string | null; +}; + +type Data = { + tier: number; + num_players: number; + roundstart_ruleset_report: RulesetReport[]; + midround_ruleset_report: RulesetReport[]; +}; + +enum TABS { + Roundstart = 'Roundstart', + Midrounds = 'Midrounds', +} + +export const DynamicTester = () => { + const { data, act } = useBackend(); + const { + tier, + num_players, + roundstart_ruleset_report, + midround_ruleset_report, + } = data; + + const [tab, setTab] = useState(Object.keys(TABS)[0]); + + const ruleset_report = + tab === TABS.Roundstart + ? roundstart_ruleset_report + : midround_ruleset_report; + + const total_weight = ruleset_report.reduce( + (acc, report) => acc + report.weight, + 0, + ); + const rulesets_with_weight_percentages = ruleset_report.map((report) => { + const percentage = Math.round((report.weight / total_weight) * 100); + return { + ...report, + percentage, + }; + }); + + return ( + + +
+ + + Tier: + act('set_tier', { tier: e })} + /> + + + Number of players: + act('set_num_players', { num_players: e })} + /> + + + + + {Object.keys(TABS).map((tabName) => ( + setTab(tabName)} + > + {tabName} + + ))} + + + + + + Ruleset + Weight + Odds + Max Antags + Min Antags + + {rulesets_with_weight_percentages.map((report) => ( + + {report.comment ? ( + + + + {report.name} + + + + ) : ( + {report.name} + )} + {report.weight} + {report.percentage}% + {report.max_candidates} + {report.min_candidates} + + ))} +
+
+
+
+
+
+ ); +};