diff --git a/_maps/RandomRuins/SpaceRuins/garbagetruck2.dmm b/_maps/RandomRuins/SpaceRuins/garbagetruck2.dmm index 1a97e8073331..1bfbbc792af8 100644 --- a/_maps/RandomRuins/SpaceRuins/garbagetruck2.dmm +++ b/_maps/RandomRuins/SpaceRuins/garbagetruck2.dmm @@ -102,8 +102,6 @@ "kp" = ( /obj/structure/safe, /obj/item/stamp/syndicate, -/obj/item/traitor_bug, -/obj/item/eyesnatcher, /obj/item/stack/spacecash/c1000, /turf/open/floor/plating, /area/ruin/space/has_grav/garbagetruck/medicalwaste) diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 0f15e9cd271c..b784f5de90b6 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -233,6 +233,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)) @@ -282,6 +284,8 @@ GLOBAL_LIST_INIT(human_invader_antagonists, list( #define DESTROY_AI_PROB(denominator) (100 / denominator) /// If the destroy AI objective doesn't roll, chance that we'll get a maroon instead. If this prob fails, they will get a generic assassinate objective instead. #define MAROON_PROB 30 +/// Probability that any job related objective is picked +#define JOB_PROB 40 /// How many telecrystals a normal traitor starts with #define TELECRYSTALS_DEFAULT 20 @@ -342,8 +346,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_global_object.dm b/code/__DEFINES/dcs/signals/signals_global_object.dm index 452be16f8391..dd3ab4455011 100644 --- a/code/__DEFINES/dcs/signals/signals_global_object.dm +++ b/code/__DEFINES/dcs/signals/signals_global_object.dm @@ -1,6 +1,6 @@ /// signals from globally accessible objects -///from SSJob when DivideOccupations is called +///from SSJob when divide_occupations is called #define COMSIG_OCCUPATIONS_DIVIDED "occupations_divided" ///from SSsun when the sun changes position : (azimuth) diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm index dc2f80927a3e..72b5add560c4 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm @@ -122,7 +122,7 @@ ///Applied preferences to a human #define COMSIG_HUMAN_PREFS_APPLIED "human_prefs_applied" -///Whenever EquipRanked is called, called after job is set +///Whenever equip_ranked is called, called after job is set #define COMSIG_JOB_RECEIVED "job_received" ///from /datum/species/handle_fire. Called when the human is set on fire and burning clothes and stuff #define COMSIG_HUMAN_BURNING "human_burning" 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 d148d80c5f0c..5ddd39fc708e 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm @@ -76,6 +76,9 @@ #define COMSIG_MOB_MIND_TRANSFERRED_OUT_OF "mob_mind_transferred_out_of" /// From /mob/proc/ghostize() Called when a mob sucessfully 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_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. #define COMSIG_MOB_MIND_SET_HOLY_ROLE "mob_mind_set_holy_role" diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 9a3d0055de0b..13783e8682d4 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -270,6 +270,8 @@ #define COMSIG_ITEM_ATTEMPT_TC_REIMBURSE "item_attempt_tc_reimburse" ///Called when a holoparasite/guardiancreator is used. #define COMSIG_TRAITOR_ITEM_USED(type) "traitor_item_used_[type]" +/// Called after an item is refunded +#define COMSIG_ITEM_TC_REIMBURSED "item_tc_reimbursed" // /obj/item/clothing signals diff --git a/code/__DEFINES/dcs/signals/signals_subsystem.dm b/code/__DEFINES/dcs/signals/signals_subsystem.dm index 076fc1d3798b..bc85747b5c08 100644 --- a/code/__DEFINES/dcs/signals/signals_subsystem.dm +++ b/code/__DEFINES/dcs/signals/signals_subsystem.dm @@ -23,3 +23,6 @@ #define COMSIG_ADDED_POINT_OF_INTEREST "added_point_of_interest" /// Sent from base of /datum/controller/subsystem/points_of_interest/proc/on_poi_element_removed : (atom/old_poi) #define COMSIG_REMOVED_POINT_OF_INTEREST "removed_point_of_interest" + +/// Send after config is loaded but before picking roundstart rulesets +#define COMSIG_DYNAMIC_PRE_ROUNDSTART "dynamic_pre_roundstart" diff --git a/code/__DEFINES/dcs/signals/signals_traitor.dm b/code/__DEFINES/dcs/signals/signals_traitor.dm index 4290b25b8009..0f80221dfefd 100644 --- a/code/__DEFINES/dcs/signals/signals_traitor.dm +++ b/code/__DEFINES/dcs/signals/signals_traitor.dm @@ -1,30 +1,5 @@ -/// Called when the hack_comm_console objective is completed. -#define COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED "!traitor_objective_completed" - /// Called whenever the uplink handler receives any sort of update. Used by uplinks to update their UI. No arguments passed #define COMSIG_UPLINK_HANDLER_ON_UPDATE "uplink_handler_on_update" -/// Sent from the uplink handler when the traitor uses the syndicate uplink beacon to order a replacement uplink. -#define COMSIG_UPLINK_HANDLER_REPLACEMENT_ORDERED "uplink_handler_replacement_ordered" - -/// Called before the traitor objective is generated -#define COMSIG_TRAITOR_OBJECTIVE_PRE_GENERATE "traitor_objective_pre_generate" - #define COMPONENT_TRAITOR_OBJECTIVE_ABORT_GENERATION (1<<0) -/// Called whenever the traitor objective is completed -#define COMSIG_TRAITOR_OBJECTIVE_COMPLETED "traitor_objective_completed" -/// Called whenever the traitor objective is failed -#define COMSIG_TRAITOR_OBJECTIVE_FAILED "traitor_objective_failed" - -/// Called when a traitor bug is planted in an area -#define COMSIG_TRAITOR_BUG_PLANTED_GROUND "traitor_bug_planted_area" -/// Called when a traitor bug is planted -#define COMSIG_TRAITOR_BUG_PLANTED_OBJECT "traitor_bug_planted_object" -/// Called before a traitor bug is planted, where it can still be overrided -#define COMSIG_TRAITOR_BUG_PRE_PLANTED_OBJECT "traitor_bug_planted_pre_object" - #define COMPONENT_FORCE_PLACEMENT (1<<0) - #define COMPONENT_FORCE_FAIL_PLACEMENT (1<<1) - -/// Called when a machine a traitor has booby trapped triggers its payload -#define COMSIG_TRAITOR_MACHINE_TRAP_TRIGGERED "traitor_machine_trap_triggered" /// Called when a device a traitor has planted effects someone's mood. Pass the mind of the viewer. #define COMSIG_DEMORALISING_EVENT "traitor_demoralise_event" diff --git a/code/__DEFINES/dcs/signals/signals_transform.dm b/code/__DEFINES/dcs/signals/signals_transform.dm index a70e4c0b1963..68a13f824a6b 100644 --- a/code/__DEFINES/dcs/signals/signals_transform.dm +++ b/code/__DEFINES/dcs/signals/signals_transform.dm @@ -8,3 +8,6 @@ #define COMSIG_TRANSFORMING_ON_TRANSFORM "transforming_on_transform" /// Return COMPONENT_NO_DEFAULT_MESSAGE to prevent the transforming component from displaying the default transform message / sound. #define COMPONENT_NO_DEFAULT_MESSAGE (1<<0) + +/// From /datum/component/transforming/proc/on_transform_end(obj/item/source, mob/user): (mob/source, obj/item/transforming, active) +#define COMSIG_MOB_TRANSFORMING_ITEM "mob_transforming_item" diff --git a/code/__DEFINES/dynamic.dm b/code/__DEFINES/dynamic.dm deleted file mode 100644 index 07c833cdc494..000000000000 --- a/code/__DEFINES/dynamic.dm +++ /dev/null @@ -1,40 +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" diff --git a/code/__DEFINES/jobs.dm b/code/__DEFINES/jobs.dm index 8419bd0902af..397b420b6bda 100644 --- a/code/__DEFINES/jobs.dm +++ b/code/__DEFINES/jobs.dm @@ -191,7 +191,7 @@ #define JOB_ANNOUNCE_ARRIVAL (1<<0) /// Whether the mob is added to the crew manifest. #define JOB_CREW_MANIFEST (1<<1) -/// Whether the mob is equipped through SSjob.EquipRank() on spawn. +/// Whether the mob is equipped through SSjob.equip_rank() on spawn. #define JOB_EQUIP_RANK (1<<2) /// Whether the job is considered a regular crew member of the station. Equipment such as AI and cyborgs not included. #define JOB_CREW_MEMBER (1<<3) @@ -213,6 +213,10 @@ #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) /// Combination flag for jobs which are considered regular crew members of the station. #define STATION_JOB_FLAGS (JOB_ANNOUNCE_ARRIVAL|JOB_CREW_MANIFEST|JOB_EQUIP_RANK|JOB_CREW_MEMBER|JOB_NEW_PLAYER_JOINABLE|JOB_REOPEN_ON_ROUNDSTART_LOSS|JOB_ASSIGN_QUIRKS|JOB_CAN_BE_INTERN) diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm index 59f34a9555e0..b7f59978806a 100644 --- a/code/__DEFINES/logging.dm +++ b/code/__DEFINES/logging.dm @@ -148,6 +148,7 @@ #define LOG_CATEGORY_GAME_TRAITOR "game-traitor" #define LOG_CATEGORY_GAME_VOTE "game-vote" #define LOG_CATEGORY_GAME_WHISPER "game-whisper" +#define LOG_CATEGORY_GAME_GHOST_POLLS "game-ghost-polls" // HREF categories #define LOG_CATEGORY_HREF "href" diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index 45b77f74457d..d574c5817b60 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -87,6 +87,7 @@ #define TGUI_LAYOUT_LIST "list" //Job preferences levels +#define JP_ANY 0 #define JP_LOW 1 #define JP_MEDIUM 2 #define JP_HIGH 3 diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index 09ba57c4ad36..5ce4067f0492 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -30,6 +30,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_SENTIENT_DISEASE "Sentient Disease" @@ -64,18 +65,20 @@ #define ROLE_REV "Revolutionary" #define ROLE_REVENANT "Revenant" #define ROLE_SENTIENCE "Sentience Potion Spawn" +#define ROLE_EVIL_CLONE "Evil Clone" +/// This flag specifically is used as a generic catch-all antag ban #define ROLE_SYNDICATE "Syndicate" #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" #define ROLE_SLAUGHTER_DEMON "Slaughter Demon" #define ROLE_WIZARD_APPRENTICE "apprentice" #define ROLE_SYNDICATE_MONKEY "Syndicate Monkey Agent" +#define ROLE_CONTRACTOR_SUPPORT "Contractor Support Unit" //Spawner roles #define ROLE_ANCIENT_CREW "Ancient Crew" @@ -112,55 +115,6 @@ #define ROLE_CYBER_TAC "Cyber Tac" #define ROLE_NETGUARDIAN "NetGuardian Prime" -/// This defines the antagonists you can operate with in the settings. -/// Keys are the antagonist, values are the number of days since the player's -/// first connection in order to play. -GLOBAL_LIST_INIT(special_roles, list( - // Roundstart - ROLE_BROTHER = 0, - ROLE_CHANGELING = 0, - ROLE_CLOWN_OPERATIVE = 14, - ROLE_CULTIST = 14, - ROLE_HERETIC = 0, - ROLE_MALF = 0, - ROLE_OPERATIVE = 14, - ROLE_REV_HEAD = 14, - ROLE_TRAITOR = 0, - ROLE_WIZARD = 14, - - // 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_SENTIENT_DISEASE = 0, - ROLE_SLEEPER_AGENT = 0, - ROLE_SPACE_DRAGON = 0, - ROLE_SPIDER = 0, - ROLE_WIZARD_MIDROUND = 14, - - // 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, -)) - //Job defines for what happens when you fail to qualify for any job during job selection #define BEOVERFLOW 1 #define BERANDOMJOB 2 diff --git a/code/__DEFINES/uplink.dm b/code/__DEFINES/uplink.dm index 76c1f2265a59..3e75278132a5 100644 --- a/code/__DEFINES/uplink.dm +++ b/code/__DEFINES/uplink.dm @@ -9,8 +9,19 @@ /// This item is purchasable to clown ops #define UPLINK_CLOWN_OPS (1 << 2) -/// This item is purchasable to infiltrators (midround traitors) -#define UPLINK_INFILTRATORS (1 << 3) +/// Can be randomly given to spies for their bounties +#define UPLINK_SPY (1 << 3) + +#define UPLINK_LONE_OP (1 << 4) + +/// A blanket define for an item being purchasable by all types of nukie +#define UPLINK_ALL_SYNDIE_OPS (UPLINK_NUKE_OPS | UPLINK_LONE_OP | UPLINK_CLOWN_OPS) + +/// A blanket define for an item being purchasable by all operatives that spawn at the nukie firebase +#define UPLINK_FIREBASE_OPS (UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) + +/// A define that excludes clown ops from the regular nukeop gear lineup +#define UPLINK_SERIOUS_OPS (UPLINK_NUKE_OPS | UPLINK_LONE_OP) /// Progression gets turned into a user-friendly form. This is just an abstract equation that makes progression not too large. #define DISPLAY_PROGRESSION(time) round(time/60, 0.01) @@ -31,3 +42,6 @@ /// Minimal cost for an item to be eligible for a discount #define TRAITOR_DISCOUNT_MIN_PRICE 4 + +/// The standard minimum player count for "don't spawn this item on low population rounds" +#define TRAITOR_POPULATION_LOWPOP 20 diff --git a/code/__HELPERS/_string_lists.dm b/code/__HELPERS/_string_lists.dm index 4d4a7ff94c84..c8a074cd07b9 100644 --- a/code/__HELPERS/_string_lists.dm +++ b/code/__HELPERS/_string_lists.dm @@ -20,8 +20,8 @@ GLOBAL_VAR(string_filename_current_key) CRASH("strings list not found: [STRING_DIRECTORY]/[filepath], index=[key]") /proc/strings(filepath as text, key as text, directory = STRING_DIRECTORY) - if(IsAdminAdvancedProcCall()) - return + // if(IsAdminAdvancedProcCall()) + // return filepath = sanitize_filepath(filepath) load_strings_file(filepath, directory) @@ -34,8 +34,8 @@ GLOBAL_VAR(string_filename_current_key) return pick_list(GLOB.string_filename_current_key, group1) /proc/load_strings_file(filepath, directory = STRING_DIRECTORY) - if(IsAdminAdvancedProcCall()) - return + // if(IsAdminAdvancedProcCall()) + // return GLOB.string_filename_current_key = filepath if(filepath in GLOB.string_cache) diff --git a/code/__HELPERS/events.dm b/code/__HELPERS/events.dm index 9b5f939de15e..58d91c8caa8a 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 1a1c8e3ef5be..701763ea483f 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -153,10 +153,9 @@ 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 - for(var/i = 1; i <= GLOB.player_list.len; i++) - var/mob/player_mob = GLOB.player_list[i] +/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 if(alive_check && player_mob.stat) @@ -171,9 +170,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) @@ -181,7 +184,7 @@ //First we spawn a dude. var/mob/living/carbon/human/new_character = new//The mob being spawned. - SSjob.SendToLateJoin(new_character) + SSjob.send_to_late_join(new_character) ghost_player.client.prefs.safe_transfer_prefs_to(new_character) new_character.dna.update_dna_identity() diff --git a/code/__HELPERS/logging/dynamic.dm b/code/__HELPERS/logging/dynamic.dm index 488aeaec3be7..70e3d8731633 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/game.dm b/code/__HELPERS/logging/game.dm index fd0fb533d312..c1103f73a3c2 100644 --- a/code/__HELPERS/logging/game.dm +++ b/code/__HELPERS/logging/game.dm @@ -30,3 +30,6 @@ /proc/log_vote(text, list/data) logger.Log(LOG_CATEGORY_GAME_VOTE, text, data) +/// Logging for ghost polls +/proc/log_ghost_poll(text, list/data) + logger.Log(LOG_CATEGORY_GAME_GHOST_POLLS, text, data) diff --git a/code/__HELPERS/logging/manifest.dm b/code/__HELPERS/logging/manifest.dm index f4e7d16e4e1e..42d7ef724abf 100644 --- a/code/__HELPERS/logging/manifest.dm +++ b/code/__HELPERS/logging/manifest.dm @@ -1,6 +1,7 @@ /// 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 )) diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index 8c83f6d5d432..9021d4a6b19c 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -485,17 +485,21 @@ GLOBAL_LIST_INIT(skin_tone_names, list( . += borg //Returns a list of AI's -/proc/active_ais(check_mind=FALSE, z = null) +/proc/active_ais(check_mind = FALSE, z = null, skip_syndicate = FALSE, only_syndicate = FALSE) . = list() for(var/mob/living/silicon/ai/ai as anything in GLOB.ai_list) if(ai.stat == DEAD) continue if(ai.control_disabled) continue - if(check_mind) - if(!ai.mind) - continue - if(z && !(z == ai.z) && (!is_station_level(z) || !is_station_level(ai.z))) //if a Z level was specified, AND the AI is not on the same level, AND either is off the station... + var/syndie_ai = istype(ai, /mob/living/silicon/ai/weak_syndie) + if(skip_syndicate && syndie_ai) + continue + if(only_syndicate && !syndie_ai) + continue + if(check_mind && !ai.mind) + continue + if(!isnull(z) && z != ai.z && (!is_station_level(z) || !is_station_level(ai.z))) //if a Z level was specified, AND the AI is not on the same level, AND either is off the station... continue . += ai diff --git a/code/__HELPERS/priority_announce.dm b/code/__HELPERS/priority_announce.dm index 76814458ae55..5e2d5f9be6e8 100644 --- a/code/__HELPERS/priority_announce.dm +++ b/code/__HELPERS/priority_announce.dm @@ -91,7 +91,7 @@ else GLOB.news_network.submit_article(text, "[command_name()] Update", NEWSCASTER_STATION_ANNOUNCEMENTS , null) -/proc/print_command_report(text = "", title = null, announce=TRUE) +/proc/print_command_report(text = "", title = null, announce = TRUE, contains_advanced_html = FALSE) if(!title) title = "Classified [command_name()] Update" @@ -107,7 +107,7 @@ message.title = title message.content = text - SScommunications.send_message(message) + GLOB.communications_controller.send_message(message, contains_advanced_html = contains_advanced_html) /** * Sends a minor annoucement to players. diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index 8078526d744a..4e6054a19ab2 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -353,15 +353,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/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index 21fc2cb196af..a2b8dc10a88c 100644 --- a/code/controllers/configuration/entries/game_options.dm +++ b/code/controllers/configuration/entries/game_options.dm @@ -52,41 +52,11 @@ /datum/config_entry/flag/disable_warops -/datum/config_entry/number/traitor_scaling_coeff //how much does the amount of players get divided by to determine traitors - default = 6 - integer = FALSE - min_val = 0 - -/datum/config_entry/number/brother_scaling_coeff //how many players per brother team - default = 25 - integer = FALSE - min_val = 0 - -/// Determines the ideal player count for maximum progression per minute. -/datum/config_entry/number/traitor_ideal_player_count - default = 20 - min_val = 1 - /// Determines how fast traitors scale in general. /datum/config_entry/number/traitor_scaling_multiplier default = 1 min_val = 0.01 -/// Determines how many potential objectives a traitor can have. -/datum/config_entry/number/maximum_potential_objectives - default = 6 - min_val = 1 - -/datum/config_entry/number/changeling_scaling_coeff //how much does the amount of players get divided by to determine changelings - default = 6 - integer = FALSE - min_val = 0 - -/datum/config_entry/number/ecult_scaling_coeff //how much does the amount of players get divided by to determine e_cult - default = 6 - integer = FALSE - min_val = 0 - /datum/config_entry/number/security_scaling_coeff //how much does the amount of players get divided by to determine open security officer positions default = 8 integer = FALSE @@ -146,6 +116,10 @@ /datum/config_entry/flag/no_summon_events //Allowed /datum/config_entry/flag/no_intercept_report //Whether or not to send a communications intercept report roundstart. This may be overridden by gamemodes. + default = FALSE + +/datum/config_entry/flag/no_dynamic_report //dynamic's status is not reported in the roundstart report + default = TRUE /datum/config_entry/number/arrivals_shuttle_dock_window //Time from when a player late joins on the arrivals shuttle to when the shuttle docks on the station default = 55 @@ -174,6 +148,9 @@ /datum/config_entry/string/alert_delta default = "Destruction of the station is imminent. All crew are instructed to obey all instructions given by heads of staff. Any violations of these orders can be punished by death. This is not a drill." +/datum/config_entry/flag/roundstart_blue_alert + default = FALSE + /datum/config_entry/flag/revival_pod_plants /datum/config_entry/number/revival_brain_life diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index d4e05b06c345..fbc098d45436 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -118,6 +118,10 @@ /// log emotes /datum/config_entry/flag/log_emote +/// log ghost polling +/datum/config_entry/flag/log_ghost_poll + default = TRUE + /// log economy actions /datum/config_entry/flag/log_econ diff --git a/code/controllers/subsystem/blackbox.dm b/code/controllers/subsystem/blackbox.dm index 3579ab472968..844adc95f57b 100644 --- a/code/controllers/subsystem/blackbox.dm +++ b/code/controllers/subsystem/blackbox.dm @@ -347,7 +347,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, diff --git a/code/controllers/subsystem/communications.dm b/code/controllers/subsystem/communications.dm deleted file mode 100644 index 5d2d5a2e7f40..000000000000 --- a/code/controllers/subsystem/communications.dm +++ /dev/null @@ -1,64 +0,0 @@ -#define COMMUNICATION_COOLDOWN (30 SECONDS) -#define COMMUNICATION_COOLDOWN_AI (30 SECONDS) -#define COMMUNICATION_COOLDOWN_MEETING (5 MINUTES) - -SUBSYSTEM_DEF(communications) - name = "Communications" - flags = SS_NO_INIT | SS_NO_FIRE - - COOLDOWN_DECLARE(silicon_message_cooldown) - COOLDOWN_DECLARE(nonsilicon_message_cooldown) - - /// Are we trying to send a cross-station message that contains soft-filtered words? If so, flip to TRUE to extend the time admins have to cancel the message. - var/soft_filtering = FALSE - - /// A list of footnote datums, to be added to the bottom of the roundstart command report. - var/list/command_report_footnotes = list() - /// A counter of conditions that are blocking the command report from printing. Counter incremements up for every blocking condition, and de-incrememnts when it is complete. - var/block_command_report = 0 - /// Has a special xenomorph egg been delivered? - var/xenomorph_egg_delivered = FALSE - /// The location where the special xenomorph egg was planted - var/area/captivity_area - -/datum/controller/subsystem/communications/proc/can_announce(mob/living/user, is_silicon) - if(is_silicon && COOLDOWN_FINISHED(src, silicon_message_cooldown)) - return TRUE - else if(!is_silicon && COOLDOWN_FINISHED(src, nonsilicon_message_cooldown)) - return TRUE - else - return FALSE - -/datum/controller/subsystem/communications/proc/make_announcement(mob/living/user, is_silicon, input, syndicate, list/players) - if(!can_announce(user, is_silicon)) - return FALSE - if(is_silicon) - minor_announce(html_decode(input),"[user.name] announces:", players = players) - COOLDOWN_START(src, silicon_message_cooldown, COMMUNICATION_COOLDOWN_AI) - else - var/list/message_data = user.treat_message(input) - if(syndicate) - priority_announce(html_decode(message_data["message"]), null, 'sound/misc/announce_syndi.ogg', ANNOUNCEMENT_TYPE_SYNDICATE, has_important_message = TRUE, players = players, color_override = "red") - else - priority_announce(html_decode(message_data["message"]), null, 'sound/misc/announce.ogg', ANNOUNCEMENT_TYPE_CAPTAIN, has_important_message = TRUE, players = players) - COOLDOWN_START(src, nonsilicon_message_cooldown, COMMUNICATION_COOLDOWN) - user.log_talk(input, LOG_SAY, tag="priority announcement") - message_admins("[ADMIN_LOOKUPFLW(user)] has made a priority announcement.") - -/datum/controller/subsystem/communications/proc/send_message(datum/comm_message/sending,print = TRUE,unique = FALSE) - for(var/obj/machinery/computer/communications/C in GLOB.shuttle_caller_list) - if(!(C.machine_stat & (BROKEN|NOPOWER)) && is_station_level(C.z)) - if(unique) - C.add_message(sending) - else //We copy the message for each console, answers and deletions won't be shared - var/datum/comm_message/M = new(sending.title,sending.content,sending.possible_answers.Copy()) - C.add_message(M) - if(print) - var/obj/item/paper/printed_paper = new /obj/item/paper(C.loc) - printed_paper.name = "paper - '[sending.title]'" - printed_paper.add_raw_text(sending.content) - printed_paper.update_appearance() - -#undef COMMUNICATION_COOLDOWN -#undef COMMUNICATION_COOLDOWN_AI -#undef COMMUNICATION_COOLDOWN_MEETING diff --git a/code/controllers/subsystem/dynamic/__dynamic_defines.dm b/code/controllers/subsystem/dynamic/__dynamic_defines.dm new file mode 100644 index 000000000000..b22e7ff0798f --- /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 000000000000..2ca153493399 --- /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 000000000000..3d1fe57f17e6 --- /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 60fd3cba20d3..6fd7fd289720 100644 --- a/code/controllers/subsystem/dynamic/dynamic.dm +++ b/code/controllers/subsystem/dynamic/dynamic.dm @@ -1,1057 +1,717 @@ -#define FAKE_GREENSHIFT_FORM_CHANCE 15 -#define FAKE_REPORT_CHANCE 8 -#define PULSAR_REPORT_CHANCE 8 -#define REPORT_NEG_DIVERGENCE -15 -#define REPORT_POS_DIVERGENCE 15 - -// 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) - SUBSYSTEM_DEF(dynamic) name = "Dynamic" flags = SS_NO_INIT - wait = 1 SECONDS - - // Threat logging vars - /// The "threat cap", threat shouldn't normally go above this and is used in ruleset calculations - var/threat_level = 0 - - /// 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 - - /// 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/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 annoucement is sent out? - var/waittime_l = 600 - - /// What is the higher bound of when the roundstart annoucement 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 - - /// The amount of threat shown on the piece of paper. - /// Can differ from the actual threat amount. - var/shown_threat - - 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 ? - 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.") + wait = 5 MINUTES + + // 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() + + // 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 + +/datum/controller/subsystem/dynamic/fire(resumed) + if(!COOLDOWN_FINISHED(src, midround_cooldown) || EMERGENCY_PAST_POINT_OF_NO_RETURN) 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) - return - if(threatadd > 0) - create_threat(threatadd, threat_log, "[worldtime2text()]: increased by [key_name(usr)]") - else - spend_midround_budget(-threatadd, threat_log, "[worldtime2text()]: decreased by [key_name(usr)]") - else if (href_list["injectlate"]) - latejoin_injection_cooldown = 0 - forced_injection = TRUE - message_admins("[key_name(usr)] forced a latejoin injection.") - else if (href_list["injectmid"]) - 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, light_ruleset_start)) + if(try_spawn_midround(LIGHT_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) + + if(COOLDOWN_FINISHED(src, heavy_ruleset_start)) + if(try_spawn_midround(HEAVY_MIDROUND)) 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"]) - return - admin_panel() // Refreshes the window +/datum/controller/subsystem/dynamic/proc/get_config() + if(!length(dynamic_config)) + load_config() + return dynamic_config -// 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 +/** + * 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 - SSticker.mode_result = "undefined" + // 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].") - // 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 + 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]) - if(SSsupermatter_cascade.cascade_initiated) - SSticker.news_report = SUPERMATTER_CASCADE + return TRUE - // Only show this one if we have nothing better to show - if(EMERGENCY_ESCAPED_OR_ENDGAMED && !SSticker.news_report) - SSticker.news_report = SSshuttle.emergency?.is_hijacked() ? SHUTTLE_HIJACK : STATION_EVACUATED +/datum/controller/subsystem/dynamic/proc/load_config() + PRIVATE_PROC(TRUE) -/datum/controller/subsystem/dynamic/proc/send_intercept() - if(SScommunications.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) + if(!CONFIG_GET(flag/dynamic_config_enabled)) return - . = "Nanotrasen Department of Intelligence Threat Advisory, Spinward Sector, TCD [time2text(world.realtime, "DDD, MMM DD")], [CURRENT_STATION_YEAR]:
" - . += generate_advisory_level() - - 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) - continue - min_threat = min(ruleset.cost, min_threat) - - var/greenshift = GLOB.dynamic_forced_extended || (threat_level < min_threat && shown_threat < min_threat) //if both shown and real threat are below any ruleset, its extended time - 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(SScommunications.command_report_footnotes)) - var/footnote_pile = "" - - for(var/datum/command_footnote/footnote in SScommunications.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 - - return . - -/// Generate the advisory level depending on the shown threat level. -/datum/controller/subsystem/dynamic/proc/generate_advisory_level() - var/advisory_string = "" - if (prob(PULSAR_REPORT_CHANCE)) - if(HAS_TRAIT(SSstation, STATION_TRAIT_BANANIUM_SHIPMENTS)) - advisory_string += "Advisory Level: Clown Planet
" - advisory_string += "Your sector's advisory level is Clown Planet! Our bike horns have picked up on a large bananium stash. Clowns show a large influx of clowns on your station. We highly advice you to slip any threats to keep Honkotrasen assets within the Banana Sector. The Department advises defending chemistry from any clowns that are trying to make baldium or space lube." - return advisory_string - - advisory_string += "Advisory Level: Pulsar Star
" - advisory_string += "Your sector's advisory level is Pulsar Star. A large unknown electromagnetic field has stormed through nearby surveillance equipment. No surveillance data has been able to be obtained showing no credible threats to Nanotrasen assets within the Spinward Sector. The Department advises maintaining high alert against potential threats, regardless of a lack of information." - return advisory_string - - switch(round(shown_threat)) - if(0) - advisory_string += "Advisory Level: White Dwarf
" - advisory_string += "Your sector's advisory level is White Dwarf. Our surveillors have ruled out any and all potential risks known in our database, ruling out the loss of our assets in the Spinward Sector. We advise a lower level of security, alongside distributing ressources on potential profit." - if(1 to 19) - var/show_core_territory = (GLOB.current_living_antags.len > 0) - if (prob(FAKE_GREENSHIFT_FORM_CHANCE)) - show_core_territory = !show_core_territory - - if (show_core_territory) - advisory_string += "Advisory Level: Blue Star
" - advisory_string += "Your sector's advisory level is Blue Star. At this threat advisory, the risk of attacks on Nanotrasen assets within the sector is minor, but cannot be ruled out entirely. Remain vigilant." - else - advisory_string += "Advisory Level: Green Star
" - advisory_string += "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." - if(20 to 39) - 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(40 to 65) - advisory_string += "Advisory Level: Orange Star
" - advisory_string += "Your sector's advisory level is Orange Star. Upon reviewing your sector's intelligence, the Department has determined that the risk of enemy activity is moderate to severe. At this advisory, we recommend maintaining a higher degree of security and alertness, and vigilance against threats that may (or will) arise." - 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 comms 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 as always, we advise remaining 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!") + 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 - if(!check_rights(R_ADMIN)) - return + dynamic_config = json_decode(result["content"]) - var/list/out = list("Threat Log
Starting Threat: [threat_level]
") +/// 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/entry in threat_log) - if(istext(entry)) - out += "[entry]
" + 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 - out += "Remaining threat/threat_level: [mid_round_budget]/[threat_level]" + 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)) - usr << browse(HTML_SKELETON_TITLE("Threat Log", out.Join()), "window=threatlog;size=700x500") + rulesets_to_spawn[category] = rand(low_end, high_end) + base_rulesets_to_spawn[category] = rulesets_to_spawn[category] -/// 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]") +/// 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 - threat_level = lorentz_to_amount(threat_curve_centre, threat_curve_width, calculated_max_threat) + 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 - 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].") + tier_weighted[tier_datum] = tier_weight - peaceful_percentage = (threat_level/max_threat_level)*100 + set_tier(pick_weight(tier_weighted), roundstart_population) -/// 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 + 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] -/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)") + 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_threat", + "dynamic_tier", 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, - ), + "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, ), ) - return TRUE - -/datum/controller/subsystem/dynamic/proc/setup_shown_threat() - if (prob(FAKE_REPORT_CHANCE)) - shown_threat = rand(1, 100) - else - shown_threat = clamp(threat_level + rand(REPORT_NEG_DIVERGENCE, REPORT_POS_DIVERGENCE), 0, 100) - -/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_shown_threat() - 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.DivideOccupations(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(" | ")]") - else - roundstart_pop_ready++ - candidates.Add(player) - SSjob.ResetOccupations() - 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 - - 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 += "[worldtime2text()]: [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() - 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, TYPE_PROC_REF(/datum/controller/subsystem/dynamic/, execute_roundstart_rule), rule), rule.delay) +/// Gets a weighted list of roundstart rulesets +/datum/controller/subsystem/dynamic/proc/get_roundstart_rulesets(list/antag_candidates) + PRIVATE_PROC(TRUE) - if (!CONFIG_GET(flag/no_intercept_report)) - addtimer(CALLBACK(src, PROC_REF(send_intercept)), rand(waittime_l, waittime_h)) + 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) - 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 + return rulesets -/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_boldannounce("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_boldannounce("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_boldannounce("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) == "") +/// 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 - if (initial(ruleset_type.weight) == 0) - 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) - var/ruleset = new ruleset_type - configure_ruleset(ruleset) - rulesets += ruleset + // clean up unused rulesets + QDEL_LIST(rulesets_weighted) + return picked_rulesets - return 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) -/// 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 + else if(prob(15)) + shown_tier = clamp(current_tier.tier + pick(-1, 1), DYNAMIC_TIER_LOW, DYNAMIC_TIER_HIGH) - 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 + 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 - if (cost > round_start_budget_left) - drafted_rules[ruleset] = null - continue + return null - 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 += "[worldtime2text()]: 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 +/** + * 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) - if(!new_rule) + 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 - 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)) +/// 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 - // 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, "[worldtime2text()]: 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 + // 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 - if(!forced_injection) - if(latejoin_injection_cooldown >= world.time) - return - if(!prob(latejoin_roll_chance)) - return + 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 - var/was_forced = forced_injection - 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)) +/** + * 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 - 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.") + 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 - 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) + if(COOLDOWN_FINISHED(src, latejoin_ruleset_start) && COOLDOWN_FINISHED(src, latejoin_cooldown)) + if(try_spawn_latejoin(latejoiner)) + return /** - * 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. + * 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/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) +/datum/controller/subsystem/dynamic/proc/try_spawn_latejoin(mob/living/carbon/human/latejoiner) - 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.") + 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 -/// 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 - -/// 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) +/// Gets a weighted list of latejoin rulesets +/datum/controller/subsystem/dynamic/proc/get_latejoin_rulesets(player_count) + PRIVATE_PROC(TRUE) -/// 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"] + 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) - if(isnull(cost)) //0 is valid so check for null specifically - return + return rulesets - 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]") +/** + * 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]") - GLOB.dynamic_station_traits[station_trait] = cost + queued_rulesets += new ruleset_typepath(dynamic_config) -/// 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) +/** + * 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 -/// 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) + 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) -/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]." +/** + * 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 -/// 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) + 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 -/// 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) + chance += 100 - (200 * (num_dead / (num_alive + num_dead))) + if(num_antags < 0) + chance += 50 -#define MAXIMUM_DYN_DISTANCE 5 + return chance /** - * Returns the comulative distribution of threat centre and width, and a random location of -0.5 to 0.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. + * Gets the chance of a latejoin ruleset being selected */ -/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.GetJob(quitter.job) - if(!job || !(job.job_flags & JOB_REOPEN_ON_ROUNDSTART_LOSS)) +/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 + +/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 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 + + if(SSsupermatter_cascade.cascade_initiated) + SSticker.news_report = SUPERMATTER_CASCADE + + // Only show this one if we have nothing better to show + if(EMERGENCY_ESCAPED_OR_ENDGAMED && !SSticker.news_report) + SSticker.news_report = SSshuttle.emergency?.is_hijacked() ? SHUTTLE_HIJACK : STATION_EVACUATED + +/// 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 + + 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 + + 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 - if(!include_command && job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) + + 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" + + data += "\n" + qdel(tier) + + for(var/ruleset_type in subtypesof(/datum/dynamic_ruleset)) + var/datum/dynamic_ruleset/ruleset = new ruleset_type() + if(!ruleset.config_tag) + qdel(ruleset) 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 - -#undef FAKE_REPORT_CHANCE -#undef FAKE_GREENSHIFT_FORM_CHANCE -#undef PULSAR_REPORT_CHANCE -#undef REPORT_NEG_DIVERGENCE -#undef REPORT_POS_DIVERGENCE + + 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 + 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) + + var/filepath = "[global.config.directory]/dynamic.toml" + fdel(file(filepath)) + text2file(data, filepath) + return TRUE +#endif diff --git a/code/controllers/subsystem/dynamic/dynamic_admin.dm b/code/controllers/subsystem/dynamic/dynamic_admin.dm new file mode 100644 index 000000000000..669781aeac40 --- /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)*/ GLOB.admin_state + +/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)*/GLOB.admin_state) + 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 7577cbcd84f6..000000000000 --- 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 16bd56a73031..000000000000 --- 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 - serialized["shown_threat"] = shown_threat - - var/list/serialized_snapshots = list() - for (var/datum/dynamic_snapshot/snapshot as anything in snapshots) - serialized_snapshots += list(snapshot.to_list()) - serialized["snapshots"] = serialized_snapshots - - rustg_file_write(json_encode(serialized), "[GLOB.log_directory]/dynamic.json") - -/// Creates a new snapshot with the given rulesets chosen, and writes to the JSON output. -/datum/controller/subsystem/dynamic/proc/new_snapshot(datum/dynamic_ruleset/ruleset_chosen) - var/datum/dynamic_snapshot/new_snapshot = new - - new_snapshot.remaining_threat = mid_round_budget - new_snapshot.time = world.time - new_snapshot.alive_players = GLOB.alive_player_list.len - new_snapshot.dead_players = GLOB.dead_player_list.len - new_snapshot.observers = GLOB.current_observers_list.len - new_snapshot.total_players = new_snapshot.alive_players + new_snapshot.dead_players + new_snapshot.observers - new_snapshot.alive_antags = GLOB.current_living_antags.len - new_snapshot.ruleset_chosen = new /datum/dynamic_snapshot_ruleset(ruleset_chosen) - - LAZYADD(snapshots, new_snapshot) - - update_log() 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 968037b9fa2e..000000000000 --- 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 (!forced_injection && next_midround_injection() > world.time) - return - - if (GLOB.dynamic_forced_extended) - return - - if (EMERGENCY_PAST_POINT_OF_NO_RETURN) - return - - var/spawn_heavy = prob(get_heavy_midround_injection_chance()) - - last_midround_injection_attempt = world.time - next_midround_injection = null - forced_injection = FALSE - - log_dynamic_and_announce("A midround ruleset is rolling, and will be [spawn_heavy ? "HEAVY" : "LIGHT"].") - - random_event_hijacked = HIJACKED_NOTHING - - var/list/drafted_heavies = list() - var/list/drafted_lights = list() - - for (var/datum/dynamic_ruleset/midround/ruleset in midround_rules) - if (ruleset.weight == 0) - log_dynamic("FAIL: [ruleset] has a weight of 0") - continue - - if (!ruleset.acceptable(GLOB.alive_player_list.len, threat_level)) - var/ruleset_forced = GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED - if (ruleset_forced == RULESET_NOT_FORCED) - log_dynamic("FAIL: [ruleset] is not acceptable with the current parameters. Alive players: [GLOB.alive_player_list.len], threat level: [threat_level]") - else - log_dynamic("FAIL: [ruleset] was disabled.") - continue - - if (mid_round_budget < ruleset.cost) - log_dynamic("FAIL: [ruleset] is too expensive, and cannot be bought. Midround budget: [mid_round_budget], ruleset cost: [ruleset.cost]") - continue - - if (ruleset.minimum_round_time > world.time - SSticker.round_start_time) - log_dynamic("FAIL: [ruleset] is trying to run too early. Minimum round time: [ruleset.minimum_round_time], current round time: [world.time - SSticker.round_start_time]") - continue - - // If admins have disabled dynamic from picking from the ghost pool - if(istype(ruleset, /datum/dynamic_ruleset/midround/from_ghosts) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT)) - log_dynamic("FAIL: [ruleset] is a from_ghosts ruleset, but ghost roles are disabled") - continue - - ruleset.trim_candidates() - ruleset.load_templates() - if (!ruleset.ready()) - log_dynamic("FAIL: [ruleset] is not ready()") - continue - - var/ruleset_is_heavy = (ruleset.midround_ruleset_style == MIDROUND_RULESET_STYLE_HEAVY) - if (ruleset_is_heavy) - drafted_heavies[ruleset] = ruleset.get_weight() - else - drafted_lights[ruleset] = ruleset.get_weight() - - var/heavy_light_log_count = "[drafted_heavies.len] heavies / [drafted_lights.len] lights" - - log_dynamic("Rolling [spawn_heavy ? "HEAVY" : "LIGHT"]... [heavy_light_log_count]") - - if (spawn_heavy && drafted_heavies.len > 0 && pick_midround_rule(drafted_heavies, "heavy rulesets")) - return - else if (drafted_lights.len > 0 && pick_midround_rule(drafted_lights, "light rulesets")) - if (spawn_heavy) - log_dynamic_and_announce("A heavy ruleset was intended to roll, but there weren't any available. [heavy_light_log_count]") - else - log_dynamic_and_announce("No midround rulesets could be drafted. ([heavy_light_log_count])") - -/// Gets the chance for a heavy ruleset midround injection, the dry_run argument is only used for forced injection. -/datum/controller/subsystem/dynamic/proc/get_heavy_midround_injection_chance(dry_run) - var/chance_modifier = 1 - var/next_midround_roll = next_midround_injection() - SSticker.round_start_time - - if (random_event_hijacked != HIJACKED_NOTHING) - chance_modifier += (hijacked_random_event_injection_chance_modifier / 100) - - if (GLOB.current_living_antags.len == 0) - chance_modifier += 0.5 - - if (GLOB.dead_player_list.len > GLOB.alive_player_list.len) - chance_modifier -= 0.3 - - var/heavy_coefficient = CLAMP01((next_midround_roll - midround_light_upper_bound) / (midround_heavy_lower_bound - midround_light_upper_bound)) - - return 100 * (heavy_coefficient * max(1, chance_modifier)) 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 000000000000..d780e2fa3446 --- /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 000000000000..5c1f6a3dc540 --- /dev/null +++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm @@ -0,0 +1,1149 @@ +/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/camera/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/simple_animal/hostile/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/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/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/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/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(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/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 000000000000..bf00d7f746b7 --- /dev/null +++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm @@ -0,0 +1,432 @@ +/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/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 b5f84176d594..000000000000 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets.dm +++ /dev/null @@ -1,285 +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 - -/datum/dynamic_ruleset/New() - // Rulesets can be instantiated more than once, such as when an admin clicks - // "Execute Midround Ruleset". Thus, it would be wrong to perform any - // side effects here. Dynamic rulesets should be stateless anyway. - SHOULD_NOT_OVERRIDE(TRUE) - - ..() - -/datum/dynamic_ruleset/roundstart // One or more of those drafted at roundstart - ruletype = ROUNDSTART_RULESET - -// Can be drafted when a player joins the server -/datum/dynamic_ruleset/latejoin - ruletype = LATEJOIN_RULESET - -/// By default, a rule is acceptable if it satisfies the threat level/population requirements. -/// If your rule has extra checks, such as counting security officers, do that in ready() instead -/datum/dynamic_ruleset/proc/acceptable(population = 0, threat_level = 0) - var/ruleset_forced = GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED - if (ruleset_forced != RULESET_NOT_FORCED) - if (ruleset_forced == RULESET_FORCE_ENABLED) - return TRUE - else - log_dynamic("FAIL: [src] was disabled in admin panel.") - return FALSE - - if(!is_valid_population(population)) - var/range = maximum_players > 0 ? "([minimum_players] - [maximum_players])" : "(minimum: [minimum_players])" - log_dynamic("FAIL: [src] failed acceptable: min/max players out of range [range] vs population ([population])") - return FALSE - - if (!is_valid_threat(population, threat_level)) - log_dynamic("FAIL: [src] failed acceptable: threat_level ([threat_level]) < requirement ([requirements[indice_pop]])") - return FALSE - - return TRUE - -/// Returns true if we have enough players to run -/datum/dynamic_ruleset/proc/is_valid_population(population) - if(minimum_players > population) - return FALSE - if(maximum_players > 0 && population > maximum_players) - return FALSE - return TRUE - -/// Sets the current threat indices and returns true if we're inside of them -/datum/dynamic_ruleset/proc/is_valid_threat(population, threat_level) - pop_per_requirement = pop_per_requirement > 0 ? pop_per_requirement : SSdynamic.pop_per_requirement - indice_pop = min(requirements.len,round(population/pop_per_requirement)+1) - return threat_level >= requirements[indice_pop] - -/// When picking rulesets, if dynamic picks the same one multiple times, it will "scale up". -/// However, doing this blindly would result in lowpop rounds (think under 10 people) where over 80% of the crew is antags! -/// This function is here to ensure the antag ratio is kept under control while scaling up. -/// Returns how much threat to actually spend in the end. -/datum/dynamic_ruleset/proc/scale_up(population, max_scale) - if (!scaling_cost) - return 0 - - var/antag_fraction = 0 - for(var/_ruleset in (SSdynamic.executed_rules + list(src))) // we care about the antags we *will* assign, too - var/datum/dynamic_ruleset/ruleset = _ruleset - antag_fraction += ((1 + ruleset.scaled_times) * ruleset.get_antag_cap(population)) / SSdynamic.roundstart_pop_ready - - for(var/i in 1 to max_scale) - if(antag_fraction < 0.25) - scaled_times += 1 - antag_fraction += get_antag_cap(population) / SSdynamic.roundstart_pop_ready // we added new antags, gotta update the % - - return scaled_times * scaling_cost - -/// Returns what the antag cap with the given population is. -/datum/dynamic_ruleset/proc/get_antag_cap(population) - if (isnum(antag_cap)) - return antag_cap - - return CEILING(population / antag_cap["denominator"], 1) + (antag_cap["offset"] || 0) - -/// This is called if persistent variable is true everytime SSTicker ticks. -/datum/dynamic_ruleset/proc/rule_process() - return - -/// Called on pre_setup for roundstart rulesets. -/// Do everything you need to do before job is assigned here. -/// IMPORTANT: ASSIGN special_role HERE -/datum/dynamic_ruleset/proc/pre_execute() - return TRUE - -/// Called on post_setup on roundstart and when the rule executes on midround and latejoin. -/// Give your candidates or assignees equipment and antag datum here. -/datum/dynamic_ruleset/proc/execute() - for(var/datum/mind/M in assigned) - M.add_antag_datum(antag_datum) - GLOB.pre_setup_antags -= M - return TRUE - -/// Rulesets can be reused, so when we're done setting one up we want to wipe its memory of the people it was selecting over -/// This isn't Destroy we aren't deleting it here, rulesets free when nothing holds a ref. This is just to prevent hung refs. -/datum/dynamic_ruleset/proc/forget_startup() - SHOULD_CALL_PARENT(TRUE) - candidates = list() - assigned = list() - antag_datum = null - -/// Here you can perform any additional checks you want. (such as checking the map etc) -/// Remember that on roundstart no one knows what their job is at this point. -/// IMPORTANT: If ready() returns TRUE, that means pre_execute() or execute() should never fail! -/datum/dynamic_ruleset/proc/ready(forced = 0) - return check_candidates() - -/// This should always be called before ready is, to ensure that the ruleset can locate map/template based landmarks as needed -/datum/dynamic_ruleset/proc/load_templates() - for(var/template in ruleset_lazy_templates) - SSmapping.lazy_load_template(template) - -/// Runs from gamemode process() if ruleset fails to start, like delayed rulesets not getting valid candidates. -/// This one only handles refunding the threat, override in ruleset to clean up the rest. -/datum/dynamic_ruleset/proc/clean_up() - SSdynamic.refund_threat(cost + (scaled_times * scaling_cost)) - SSdynamic.threat_log += "[worldtime2text()]: [ruletype] [name] refunded [cost + (scaled_times * scaling_cost)]. Failed to execute." - -/// Gets weight of the ruleset -/// Note that this decreases weight if repeatable is TRUE and repeatable_weight_decrease is higher than 0 -/// Note: If you don't want repeatable rulesets to decrease their weight use the weight variable directly -/datum/dynamic_ruleset/proc/get_weight() - if(repeatable && weight > 1 && repeatable_weight_decrease > 0) - for(var/datum/dynamic_ruleset/DR in SSdynamic.executed_rules) - if(istype(DR, type)) - weight = max(weight-repeatable_weight_decrease,1) - return weight - -/// Checks if there are enough candidates to run, and logs otherwise -/datum/dynamic_ruleset/proc/check_candidates() - if (required_candidates <= candidates.len) - return TRUE - - log_dynamic("FAIL: [src] does not have enough candidates ([required_candidates] needed, [candidates.len] found)") - return FALSE - -/// Here you can remove candidates that do not meet your requirements. -/// This means if their job is not correct or they have disconnected you can remove them from candidates here. -/// Usually this does not need to be changed unless you need some specific requirements from your candidates. -/datum/dynamic_ruleset/proc/trim_candidates() - return - -/// Set mode_result and news report here. -/// Only called if ruleset is flagged as HIGH_IMPACT_RULESET -/datum/dynamic_ruleset/proc/round_result() - -////////////////////////////////////////////// -// // -// ROUNDSTART RULESETS // -// // -////////////////////////////////////////////// - -/// Checks if candidates are connected and if they are banned or don't want to be the antagonist. -/datum/dynamic_ruleset/roundstart/trim_candidates() - for(var/mob/dead/new_player/candidate_player in candidates) - var/client/candidate_client = GET_CLIENT(candidate_player) - if (!candidate_client || !candidate_player.mind) // Are they connected? - candidates.Remove(candidate_player) - continue - - if(candidate_client.get_remaining_days(minimum_required_age) > 0) - candidates.Remove(candidate_player) - continue - - if(candidate_player.mind.special_role) // We really don't want to give antag to an antag. - candidates.Remove(candidate_player) - continue - - if (!((antag_preference || antag_flag) in candidate_client.prefs.be_special)) - candidates.Remove(candidate_player) - continue - - if (is_banned_from(candidate_player.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE))) - candidates.Remove(candidate_player) - continue - - // If this ruleset has exclusive_roles set, we want to only consider players who have those - // job prefs enabled and are eligible to play that job. Otherwise, continue as before. - if(length(exclusive_roles)) - var/exclusive_candidate = FALSE - for(var/role in exclusive_roles) - var/datum/job/job = SSjob.GetJob(role) - - if((role in candidate_client.prefs.job_preferences) && SSjob.check_job_eligibility(candidate_player, job, "Dynamic Roundstart TC", add_job_to_log = TRUE) == JOB_AVAILABLE) - exclusive_candidate = TRUE - break - - // If they didn't have any of the required job prefs enabled or were banned from all enabled prefs, - // they're not eligible for this antag type. - if(!exclusive_candidate) - candidates.Remove(candidate_player) - -/// Do your checks if the ruleset is ready to be executed here. -/// Should ignore certain checks if forced is TRUE -/datum/dynamic_ruleset/roundstart/ready(population, forced = FALSE) - return ..() 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 d853876fed14..000000000000 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets_latejoin.dm +++ /dev/null @@ -1,254 +0,0 @@ -////////////////////////////////////////////// -// // -// LATEJOIN RULESETS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/latejoin/trim_candidates() - for(var/mob/P in candidates) - if(!P.client || !P.mind || is_unassigned_job(P.mind.assigned_role)) // Are they connected? - candidates.Remove(P) - else if (P.client.get_remaining_days(minimum_required_age) > 0) - candidates.Remove(P) - else if(P.mind.assigned_role.title in restricted_roles) // Does their job allow for it? - candidates.Remove(P) - else if((exclusive_roles.len > 0) && !(P.mind.assigned_role.title in exclusive_roles)) // Is the rule exclusive to their job? - candidates.Remove(P) - else if (!((antag_preference || antag_flag) in P.client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE))) - candidates.Remove(P) - -/datum/dynamic_ruleset/latejoin/ready(forced = 0) - if (forced) - return ..() - - var/job_check = 0 - if (enemy_roles.len > 0) - for (var/mob/M in GLOB.alive_player_list) - if (M.stat == DEAD) - continue // Dead players cannot count as opponents - if (M.mind && (M.mind.assigned_role.title in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role.title in restricted_roles))) - job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it - - var/threat = round(SSdynamic.threat_level/10) - var/ruleset_forced = (GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED) == RULESET_FORCE_ENABLED - if (!ruleset_forced && job_check < required_enemies[threat]) - log_dynamic("FAIL: [src] is not ready, because there are not enough enemies: [required_enemies[threat]] needed, [job_check] found") - return FALSE - - return ..() - -/datum/dynamic_ruleset/latejoin/execute() - var/mob/M = pick(candidates) - assigned += M.mind - M.mind.special_role = antag_flag - M.mind.add_antag_datum(antag_datum) - return TRUE - -////////////////////////////////////////////// -// // -// SYNDICATE TRAITORS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/latejoin/infiltrator - name = "Syndicate Infiltrator" - antag_datum = /datum/antagonist/traitor/infiltrator - antag_flag = ROLE_SYNDICATE_INFILTRATOR - antag_flag_override = ROLE_TRAITOR - protected_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_PERSONNEL, - JOB_HEAD_OF_SECURITY, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ) - required_candidates = 1 - weight = 11 - cost = 5 - requirements = list(5,5,5,5,5,5,5,5,5,5) - repeatable = TRUE - -////////////////////////////////////////////// -// // -// REVOLUTIONARY PROVOCATEUR // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/latejoin/provocateur - name = "Provocateur" - persistent = TRUE - antag_datum = /datum/antagonist/rev/head - antag_flag = ROLE_PROVOCATEUR - antag_flag_override = ROLE_REV_HEAD - restricted_roles = list( - JOB_AI, - JOB_CAPTAIN, - JOB_CHIEF_ENGINEER, - JOB_CHIEF_MEDICAL_OFFICER, - JOB_CYBORG, - JOB_DETECTIVE, - JOB_HEAD_OF_PERSONNEL, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_QUARTERMASTER, - JOB_RESEARCH_DIRECTOR, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - enemy_roles = list( - JOB_AI, - JOB_CYBORG, - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_SECURITY, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 1 - delay = 1 MINUTES // Prevents rule start while head is offstation. - cost = 10 - requirements = list(101,101,70,40,30,20,20,20,20,20) - flags = HIGH_IMPACT_RULESET - blocking_rules = list(/datum/dynamic_ruleset/roundstart/revs) - var/required_heads_of_staff = 3 - var/finished = FALSE - var/datum/team/revolution/revolution - -/datum/dynamic_ruleset/latejoin/provocateur/ready(forced=FALSE) - if (forced) - required_heads_of_staff = 1 - if(!..()) - return FALSE - var/head_check = 0 - for(var/mob/player in GLOB.alive_player_list) - if (player.mind.assigned_role.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_TRAIT(M, TRAIT_MINDSHIELD)) - return TRUE - return FALSE - -/datum/dynamic_ruleset/latejoin/provocateur/round_result() - revolution.round_result(finished) - -////////////////////////////////////////////// -// // -// HERETIC SMUGGLER // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/latejoin/heretic_smuggler - name = "Heretic Smuggler" - antag_datum = /datum/antagonist/heretic - antag_flag = ROLE_HERETIC_SMUGGLER - antag_flag_override = ROLE_HERETIC - protected_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_PERSONNEL, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ) - required_candidates = 1 - weight = 8 - cost = 6 - requirements = list(101,101,50,10,10,10,10,10,10,10) - repeatable = TRUE - -/datum/dynamic_ruleset/latejoin/heretic_smuggler/execute() - var/mob/picked_mob = pick(candidates) - assigned += picked_mob.mind - picked_mob.mind.special_role = antag_flag - var/datum/antagonist/heretic/new_heretic = picked_mob.mind.add_antag_datum(antag_datum) - - // Heretics passively gain influence over time. - // As a consequence, latejoin heretics start out at a massive - // disadvantage if the round's been going on for a while. - // Let's give them some influence points when they arrive. - new_heretic.knowledge_points += round((world.time - SSticker.round_start_time) / new_heretic.passive_gain_timer) - // BUT let's not give smugglers a million points on arrival. - // Limit it to four missed passive gain cycles (4 points). - new_heretic.knowledge_points = min(new_heretic.knowledge_points, 5) - - return TRUE - -/// Ruleset for latejoin changelings -/datum/dynamic_ruleset/latejoin/stowaway_changeling - name = "Stowaway Changeling" - antag_datum = /datum/antagonist/changeling - antag_flag = ROLE_STOWAWAY_CHANGELING - antag_flag_override = ROLE_CHANGELING - protected_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_PERSONNEL, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ) - required_candidates = 1 - weight = 2 - cost = 12 - requirements = list(101,101,60,50,40,20,20,10,10,10) - repeatable = TRUE - -/datum/dynamic_ruleset/latejoin/stowaway_changeling/execute() - var/mob/picked_mob = pick(candidates) - assigned += picked_mob.mind - picked_mob.mind.special_role = antag_flag - picked_mob.mind.add_antag_datum(antag_datum) - return TRUE 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 7d9ee99c136f..000000000000 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm +++ /dev/null @@ -1,933 +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 - -/datum/dynamic_ruleset/midround/from_ghosts/check_candidates() - var/dead_count = dead_players.len + list_observers.len - if (required_candidates <= dead_count) - return TRUE - - log_dynamic("FAIL: [src], a from_ghosts ruleset, did not have enough dead candidates: [required_candidates] needed, [dead_count] found") - - return FALSE - -/datum/dynamic_ruleset/midround/trim_candidates() - living_players = trim_list(GLOB.alive_player_list) - living_antags = trim_list(GLOB.current_living_antags) - dead_players = trim_list(GLOB.dead_player_list) - list_observers = trim_list(GLOB.current_observers_list) - -/datum/dynamic_ruleset/midround/proc/trim_list(list/to_trim = list()) - var/list/trimmed_list = to_trim.Copy() - for(var/mob/creature in trimmed_list) - if (!istype(creature, required_type)) - trimmed_list.Remove(creature) - continue - if (isnull(creature.client)) // Are they connected? - trimmed_list.Remove(creature) - continue - if(creature.client.get_remaining_days(minimum_required_age) > 0) - trimmed_list.Remove(creature) - continue - if (!((antag_preference || antag_flag) in creature.client.prefs.be_special)) - trimmed_list.Remove(creature) - continue - if (is_banned_from(creature.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE))) - trimmed_list.Remove(creature) - continue - - if (isnull(creature.mind)) - continue - - if (restrict_ghost_roles && (creature.mind.assigned_role.title in GLOB.exp_specialmap[EXP_TYPE_SPECIAL])) // Are they playing a ghost role? - trimmed_list.Remove(creature) - continue - if (creature.mind.assigned_role.title in restricted_roles) // Does their job allow it? - trimmed_list.Remove(creature) - continue - if (length(exclusive_roles) && !(creature.mind.assigned_role.title in exclusive_roles)) // Is the rule exclusive to their job? - trimmed_list.Remove(creature) - continue - if(HAS_TRAIT(creature, TRAIT_MIND_TEMPORARILY_GONE)) // are they out of body? - trimmed_list.Remove(creature) - continue - if(HAS_TRAIT(creature, TRAIT_TEMPORARY_BODY)) // are they an avatar? - trimmed_list.Remove(creature) - continue - return trimmed_list - -// You can then for example prompt dead players in execute() to join as strike teams or whatever -// Or autotator someone - -// IMPORTANT, since /datum/dynamic_ruleset/midround may accept candidates from both living, dead, and even antag players -// subtype your midround with /from_ghosts or /from_living to get candidate checking. Or check yourself by subtyping from neither -/datum/dynamic_ruleset/midround/ready(forced = FALSE) - if (forced) - return TRUE - - var/job_check = 0 - if (enemy_roles.len > 0) - for (var/mob/M in GLOB.alive_player_list) - if (M.stat == DEAD || !M.client) - continue // Dead/disconnected players cannot count as opponents - if (M.mind && (M.mind.assigned_role.title in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role.title in restricted_roles))) - job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it - - var/threat = round(SSdynamic.threat_level/10) - var/ruleset_forced = (GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED) == RULESET_FORCE_ENABLED - if (!ruleset_forced && job_check < required_enemies[threat]) - log_dynamic("FAIL: [src] is not ready, because there are not enough enemies: [required_enemies[threat]] needed, [job_check] found") - return FALSE - - return TRUE - -/datum/dynamic_ruleset/midround/from_ghosts/execute() - var/list/possible_candidates = list() - possible_candidates.Add(dead_players) - possible_candidates.Add(list_observers) - send_applications(possible_candidates) - if(assigned.len > 0) - return TRUE - else - return FALSE - -/// This sends a poll to ghosts if they want to be a ghost spawn from a ruleset. -/datum/dynamic_ruleset/midround/from_ghosts/proc/send_applications(list/possible_volunteers = list()) - if (possible_volunteers.len <= 0) // This shouldn't happen, as ready() should return FALSE if there is not a single valid candidate - message_admins("Possible volunteers was 0. This shouldn't appear, because of ready(), unless you forced it!") - return - - SSdynamic.log_dynamic_and_announce("Polling [possible_volunteers.len] players to apply for the [name] ruleset.") - candidates = SSpolling.poll_ghost_candidates("Looking for volunteers to become [antag_flag] for [name]", check_jobban = antag_flag_override, role = antag_flag || antag_flag_override, poll_time = 30 SECONDS, pic_source = /obj/structure/sign/poster/contraband/syndicate_recruitment, 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) - finish_setup(new_character, i) - notify_ghosts( - "[applicant.name] has been picked for the ruleset [name]!", - source = new_character, - ) - -/datum/dynamic_ruleset/midround/from_ghosts/proc/generate_ruleset_body(mob/applicant) - var/mob/living/carbon/human/new_character = make_body(applicant) - new_character.dna.remove_all_mutations() - return new_character - -/datum/dynamic_ruleset/midround/from_ghosts/proc/finish_setup(mob/new_character, index) - var/datum/antagonist/new_role = new antag_datum() - setup_role(new_role) - new_character.mind.add_antag_datum(new_role) - new_character.mind.special_role = antag_flag - -/datum/dynamic_ruleset/midround/from_ghosts/proc/setup_role(datum/antagonist/new_role) - return - -/// Fired when there are no valid candidates. Will spawn a sleeper agent or latejoin traitor. -/datum/dynamic_ruleset/midround/from_ghosts/proc/attempt_replacement() - var/datum/dynamic_ruleset/midround/from_living/autotraitor/sleeper_agent = new - - SSdynamic.configure_ruleset(sleeper_agent) - - if (!SSdynamic.picking_specific_rule(sleeper_agent)) - return - - SSdynamic.picking_specific_rule(/datum/dynamic_ruleset/latejoin/infiltrator) - -///subtype to handle checking players -/datum/dynamic_ruleset/midround/from_living - weight = 0 - abstract_type = /datum/dynamic_ruleset/midround/from_living - -/datum/dynamic_ruleset/midround/from_living/ready(forced) - if(!check_candidates()) - return FALSE - return ..() - - -/// Midround Traitor Ruleset (From Living) -/datum/dynamic_ruleset/midround/from_living/autotraitor - name = "Syndicate Sleeper Agent" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/traitor/infiltrator/sleeper_agent - antag_flag = ROLE_SLEEPER_AGENT - antag_flag_override = ROLE_TRAITOR - protected_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_PERSONNEL, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ROLE_POSITRONIC_BRAIN, - ) - required_candidates = 1 - weight = 35 - cost = 3 - requirements = list(3,3,3,3,3,3,3,3,3,3) - repeatable = TRUE - -/datum/dynamic_ruleset/midround/from_living/autotraitor/trim_candidates() - ..() - candidates = living_players - for(var/mob/living/player in candidates) - if(issilicon(player)) // Your assigned role doesn't change when you are turned into a silicon. - candidates -= player - else if(is_centcom_level(player.z)) - candidates -= player // We don't autotator people in CentCom - else if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0)) - candidates -= player // We don't autotator people with roles already - -/datum/dynamic_ruleset/midround/from_living/autotraitor/execute() - var/mob/M = pick(candidates) - assigned += M - candidates -= M - var/datum/antagonist/traitor/infiltrator/sleeper_agent/newTraitor = new - M.mind.add_antag_datum(newTraitor) - message_admins("[ADMIN_LOOKUPFLW(M)] was selected by the [name] ruleset and has been made into a midround traitor.") - log_dynamic("[key_name(M)] was selected by the [name] ruleset and has been made into a midround traitor.") - return TRUE - -/// Midround Malf AI Ruleset (From Living) -/datum/dynamic_ruleset/midround/malf - name = "Malfunctioning AI" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/malf_ai - antag_flag = ROLE_MALF_MIDROUND - antag_flag_override = ROLE_MALF - enemy_roles = list( - JOB_CHEMIST, - JOB_CHIEF_ENGINEER, - JOB_HEAD_OF_SECURITY, - JOB_RESEARCH_DIRECTOR, - JOB_SCIENTIST, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - exclusive_roles = list(JOB_AI) - required_enemies = list(4,4,4,4,4,4,2,2,2,0) - required_candidates = 1 - minimum_players = 25 - weight = 2 - cost = 10 - required_type = /mob/living/silicon/ai - blocking_rules = list(/datum/dynamic_ruleset/roundstart/malf_ai) - -/datum/dynamic_ruleset/midround/malf/trim_candidates() - ..() - candidates = living_players - for(var/mob/living/player in candidates) - if(!isAI(player)) - candidates -= player - continue - - if(is_centcom_level(player.z)) - candidates -= player - continue - - if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0)) - candidates -= player - -/datum/dynamic_ruleset/midround/malf/execute() - if(!candidates || !candidates.len) - return FALSE - var/mob/living/silicon/ai/new_malf_ai = pick_n_take(candidates) - assigned += new_malf_ai.mind - var/datum/antagonist/malf_ai/malf_antag_datum = new - new_malf_ai.mind.special_role = antag_flag - new_malf_ai.mind.add_antag_datum(malf_antag_datum) - if(prob(MALF_ION_PROB)) - priority_announce("Ion storm detected near the station. Please check all AI-controlled equipment for errors.", "Anomaly Alert", ANNOUNCER_IONSTORM) - if(prob(REPLACE_LAW_WITH_ION_PROB)) - new_malf_ai.replace_random_law(generate_ion_law(), list(LAW_INHERENT, LAW_SUPPLIED, LAW_ION), LAW_ION) - else - new_malf_ai.add_ion_law(generate_ion_law()) - return TRUE - -/// Midround Wizard Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/wizard - name = "Wizard" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/wizard - antag_flag = ROLE_WIZARD_MIDROUND - antag_flag_override = ROLE_WIZARD - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 1 - cost = 10 - requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED - flags = HIGH_IMPACT_RULESET - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_WIZARDDEN) - -/datum/dynamic_ruleset/midround/from_ghosts/wizard/ready(forced = FALSE) - if(!check_candidates()) - return FALSE - if(!length(GLOB.wizardstart)) - log_admin("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") - message_admins("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/wizard/finish_setup(mob/new_character, index) - ..() - new_character.forceMove(pick(GLOB.wizardstart)) - -/// Midround Nuclear Operatives Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/nuclear - name = "Nuclear Assault" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_flag = ROLE_OPERATIVE_MIDROUND - antag_flag_override = ROLE_OPERATIVE - antag_datum = /datum/antagonist/nukeop - enemy_roles = list( - JOB_AI, - JOB_CYBORG, - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_SECURITY, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - required_enemies = list(3,3,3,3,3,2,1,1,0,0) - required_candidates = 5 - weight = 5 - cost = 7 - minimum_round_time = 70 MINUTES - requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE) - flags = HIGH_IMPACT_RULESET - - var/list/operative_cap = list(2,2,3,3,4,5,5,5,5,5) - -/datum/dynamic_ruleset/midround/from_ghosts/nuclear/acceptable(population=0, threat_level=0) - if (locate(/datum/dynamic_ruleset/roundstart/nuclear) in SSdynamic.executed_rules) - return FALSE // Unavailable if nuke ops were already sent at roundstart - indice_pop = min(operative_cap.len, round(living_players.len/5)+1) - required_candidates = operative_cap[indice_pop] - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/nuclear/ready(forced = FALSE) - if (!check_candidates()) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_applications() - var/mob/leader = get_most_experienced(assigned, ROLE_NUCLEAR_OPERATIVE) - if(leader) - assigned.Remove(leader) - assigned.Insert(1, leader) - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_setup(mob/new_character, index) - new_character.mind.set_assigned_role(SSjob.GetJobType(/datum/job/nuclear_operative)) - new_character.mind.special_role = ROLE_NUCLEAR_OPERATIVE - if(index == 1) - var/datum/antagonist/nukeop/leader/leader_antag_datum = new() - new_character.mind.add_antag_datum(leader_antag_datum) - return - return ..() - -/// Midround Blob Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/blob - name = "Blob" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/blob - antag_flag = ROLE_BLOB - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - minimum_round_time = 35 MINUTES - weight = 3 - cost = 8 - minimum_players = 25 - repeatable = TRUE - -/datum/dynamic_ruleset/midround/from_ghosts/blob/generate_ruleset_body(mob/applicant) - var/body = applicant.become_overmind() - return body - -/// Midround Blob Infection Ruleset (From Living) -/datum/dynamic_ruleset/midround/from_living/blob_infection - name = "Blob Infection" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/blob/infection - antag_flag = ROLE_BLOB_INFECTION - antag_flag_override = ROLE_BLOB - protected_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ROLE_POSITRONIC_BRAIN, - ) - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - minimum_round_time = 35 MINUTES - weight = 3 - cost = 10 - minimum_players = 25 - repeatable = TRUE - -/datum/dynamic_ruleset/midround/from_living/blob_infection/trim_candidates() - ..() - candidates = living_players - for(var/mob/living/player as anything in candidates) - var/turf/player_turf = get_turf(player) - if(!player_turf || !is_station_level(player_turf.z)) - candidates -= player - continue - - if(player.mind && (player.mind.special_role || length(player.mind.antag_datums) > 0)) - candidates -= player - -/datum/dynamic_ruleset/midround/from_living/blob_infection/execute() - if(!candidates || !candidates.len) - return FALSE - var/mob/living/carbon/human/blob_antag = pick_n_take(candidates) - assigned += blob_antag.mind - blob_antag.mind.special_role = antag_flag - return ..() - -/// Midround Xenomorph Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/xenomorph - name = "Alien Infestation" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/xeno - antag_flag = ROLE_ALIEN - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - minimum_round_time = 40 MINUTES - weight = 5 - cost = 10 - minimum_players = 25 - repeatable = TRUE - var/list/vents = list() - -/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/forget_startup() - vents = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/execute() - // 50% chance of being incremented by one - required_candidates += prob(50) - var/list/vent_pumps = SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/atmospherics/components/unary/vent_pump) - for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent as anything in vent_pumps) - if(QDELETED(temp_vent)) - continue - if(is_station_level(temp_vent.loc.z) && !temp_vent.welded) - var/datum/pipeline/temp_vent_parent = temp_vent.parents[1] - if(!temp_vent_parent) - continue // No parent vent - // Stops Aliens getting stuck in small networks. - // See: Security, Virology - if(temp_vent_parent.other_atmos_machines.len > 20) - vents += temp_vent - if(!vents.len) - return FALSE - . = ..() - -/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/generate_ruleset_body(mob/applicant) - var/obj/vent = pick_n_take(vents) - var/mob/living/carbon/alien/larva/new_xeno = new(vent.loc) - new_xeno.key = applicant.key - new_xeno.move_into_vent(vent) - message_admins("[ADMIN_LOOKUPFLW(new_xeno)] has been made into an alien by the midround ruleset.") - log_dynamic("[key_name(new_xeno)] was spawned as an alien by the midround ruleset.") - return new_xeno - -/// Midround Nightmare Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/nightmare - name = "Nightmare" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/nightmare - antag_flag = ROLE_NIGHTMARE - antag_flag_override = ROLE_ALIEN - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 3 - cost = 5 - minimum_players = 15 - repeatable = TRUE - -/datum/dynamic_ruleset/midround/from_ghosts/nightmare/acceptable(population = 0, threat_level = 0) - var/turf/spawn_loc = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE) //Checks if there's a single safe, dark tile on station. - if(!spawn_loc) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/nightmare/generate_ruleset_body(mob/applicant) - var/datum/mind/player_mind = new /datum/mind(applicant.key) - player_mind.active = TRUE - - var/mob/living/carbon/human/new_nightmare = new (find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE)) - player_mind.transfer_to(new_nightmare) - player_mind.set_assigned_role(SSjob.GetJobType(/datum/job/nightmare)) - player_mind.special_role = ROLE_NIGHTMARE - player_mind.add_antag_datum(/datum/antagonist/nightmare) - new_nightmare.set_species(/datum/species/shadow/nightmare) - - playsound(new_nightmare, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1) - message_admins("[ADMIN_LOOKUPFLW(new_nightmare)] has been made into a Nightmare by the midround ruleset.") - log_dynamic("[key_name(new_nightmare)] was spawned as a Nightmare by the midround ruleset.") - return new_nightmare - -/// Midround Space Dragon Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/space_dragon - name = "Space Dragon" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/space_dragon - antag_flag = ROLE_SPACE_DRAGON - antag_flag_override = ROLE_SPACE_DRAGON - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 4 - cost = 7 - minimum_players = 25 - repeatable = TRUE - var/list/spawn_locs = list() - -/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/forget_startup() - spawn_locs = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/execute() - for(var/obj/effect/landmark/carpspawn/C in GLOB.landmarks_list) - spawn_locs += (C.loc) - if(!spawn_locs.len) - message_admins("No valid spawn locations found, aborting...") - return MAP_ERROR - . = ..() - -/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/generate_ruleset_body(mob/applicant) - var/datum/mind/player_mind = new /datum/mind(applicant.key) - player_mind.active = TRUE - - var/mob/living/basic/space_dragon/S = new (pick(spawn_locs)) - player_mind.transfer_to(S) - player_mind.add_antag_datum(/datum/antagonist/space_dragon) - - playsound(S, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1) - message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a Space Dragon by the midround ruleset.") - log_dynamic("[key_name(S)] was spawned as a Space Dragon by the midround ruleset.") - priority_announce("A large organic energy flux has been recorded near of [station_name()], please stand-by.", "Lifesign Alert") - return S - -/datum/dynamic_ruleset/midround/from_ghosts/abductors - name = "Abductors" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/abductor - antag_flag = ROLE_ABDUCTOR - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 2 - required_applicants = 2 - weight = 4 - cost = 7 - minimum_players = 25 - repeatable = TRUE - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_ABDUCTOR_SHIPS) - - var/datum/team/abductor_team/new_team - -/datum/dynamic_ruleset/midround/from_ghosts/abductors/forget_startup() - new_team = null - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/abductors/ready(forced = FALSE) - if (required_candidates > (dead_players.len + list_observers.len)) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/abductors/finish_setup(mob/new_character, index) - if (index == 1) // Our first guy is the scientist. We also initialize the team here as well since this should only happen once per pair of abductors. - new_team = new - if(new_team.team_number > ABDUCTOR_MAX_TEAMS) - return MAP_ERROR - var/datum/antagonist/abductor/scientist/new_role = new - new_character.mind.add_antag_datum(new_role, new_team) - else // Our second guy is the agent, team is already created, don't need to make another one. - var/datum/antagonist/abductor/agent/new_role = new - new_character.mind.add_antag_datum(new_role, new_team) - -/// Midround Space Ninja Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/space_ninja - name = "Space Ninja" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/ninja - antag_flag = ROLE_NINJA - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 4 - cost = 8 - minimum_players = 30 - repeatable = TRUE - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY) // I mean, no one uses the nets anymore but whateva - - var/list/spawn_locs = list() - -/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/forget_startup() - spawn_locs = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/execute() - for(var/obj/effect/landmark/carpspawn/carp_spawn in GLOB.landmarks_list) - if(!isturf(carp_spawn.loc)) - stack_trace("Carp spawn found not on a turf: [carp_spawn.type] on [isnull(carp_spawn.loc) ? "null" : carp_spawn.loc.type]") - continue - spawn_locs += carp_spawn.loc - if(!spawn_locs.len) - message_admins("No valid spawn locations found, aborting...") - return MAP_ERROR - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/generate_ruleset_body(mob/applicant) - var/mob/living/carbon/human/ninja = create_space_ninja(pick(spawn_locs)) - ninja.key = applicant.key - ninja.mind.add_antag_datum(/datum/antagonist/ninja) - - message_admins("[ADMIN_LOOKUPFLW(ninja)] has been made into a Space Ninja by the midround ruleset.") - log_dynamic("[key_name(ninja)] was spawned as a Space Ninja by the midround ruleset.") - return ninja - -/// Midround Spiders Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/spiders - name = "Spiders" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_flag = ROLE_SPIDER - required_type = /mob/dead/observer - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 0 - weight = 3 - cost = 8 - minimum_players = 27 - repeatable = TRUE - var/spawncount = 2 - -/datum/dynamic_ruleset/midround/spiders/execute() - create_midwife_eggs(spawncount) - return ..() - -/// Midround Revenant Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/revenant - name = "Revenant" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/revenant - antag_flag = ROLE_REVENANT - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 4 - cost = 5 - minimum_players = 15 - repeatable = TRUE - var/dead_mobs_required = 20 - var/need_extra_spawns_value = 15 - var/list/spawn_locs = list() - -/datum/dynamic_ruleset/midround/from_ghosts/revenant/forget_startup() - spawn_locs = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/revenant/acceptable(population=0, threat_level=0) - if(GLOB.dead_mob_list.len < dead_mobs_required) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/revenant/execute() - for(var/mob/living/corpse in GLOB.dead_mob_list) //look for any dead bodies - var/turf/corpse_turf = get_turf(corpse) - if(corpse_turf && is_station_level(corpse_turf.z)) - spawn_locs += corpse_turf - if(!spawn_locs.len || spawn_locs.len < need_extra_spawns_value) //look for any morgue trays, crematoriums, ect if there weren't alot of dead bodies on the station to pick from - for(var/obj/structure/bodycontainer/corpse_container in GLOB.bodycontainers) - var/turf/container_turf = get_turf(corpse_container) - if(container_turf && is_station_level(container_turf.z)) - spawn_locs += container_turf - if(!spawn_locs.len) //If we can't find any valid spawnpoints, try the carp spawns - for(var/obj/effect/landmark/carpspawn/carp_spawnpoint in GLOB.landmarks_list) - if(isturf(carp_spawnpoint.loc)) - spawn_locs += carp_spawnpoint.loc - if(!spawn_locs.len) //If we can't find THAT, then just give up and cry - return FALSE - . = ..() - -/datum/dynamic_ruleset/midround/from_ghosts/revenant/generate_ruleset_body(mob/applicant) - var/mob/living/basic/revenant/revenant = new(pick(spawn_locs)) - revenant.key = applicant.key - message_admins("[ADMIN_LOOKUPFLW(revenant)] has been made into a revenant by the midround ruleset.") - log_game("[key_name(revenant)] was spawned as a revenant by the midround ruleset.") - return revenant - -/// Midround Sentient Disease Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/sentient_disease - name = "Sentient Disease" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_datum = /datum/antagonist/disease - antag_flag = ROLE_SENTIENT_DISEASE - required_candidates = 1 - minimum_players = 25 - weight = 4 - cost = 8 - repeatable = TRUE - -/datum/dynamic_ruleset/midround/from_ghosts/sentient_disease/generate_ruleset_body(mob/applicant) - var/mob/camera/disease/virus = new /mob/camera/disease(SSmapping.get_station_center()) - virus.key = applicant.key - INVOKE_ASYNC(virus, TYPE_PROC_REF(/mob/camera/disease, pick_name)) - message_admins("[ADMIN_LOOKUPFLW(virus)] has been made into a sentient disease by the midround ruleset.") - log_game("[key_name(virus)] was spawned as a sentient disease by the midround ruleset.") - return virus - -/// Midround Space Pirates Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/pirates - name = "Space Pirates" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_flag = "Space Pirates" - required_type = /mob/dead/observer - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 0 - weight = 3 - cost = 8 - minimum_players = 20 - repeatable = TRUE - -/datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat_level=0) - if (SSmapping.is_planetary() || GLOB.light_pirate_gangs.len == 0) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/pirates/execute() - send_pirate_threat(GLOB.light_pirate_gangs) - return ..() - -/// Dangerous Space Pirates ruleset -/datum/dynamic_ruleset/midround/dangerous_pirates - name = "Dangerous Space Pirates" - midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY - antag_flag = "Space Pirates" - required_type = /mob/dead/observer - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 0 - weight = 3 - cost = 8 - minimum_players = 25 - repeatable = TRUE - -/datum/dynamic_ruleset/midround/dangerous_pirates/acceptable(population=0, threat_level=0) - if (SSmapping.is_planetary() || GLOB.heavy_pirate_gangs.len == 0) - return FALSE - return ..() - -/datum/dynamic_ruleset/midround/dangerous_pirates/execute() - send_pirate_threat(GLOB.heavy_pirate_gangs) - return ..() - -/// Midround Obsessed Ruleset (From Living) -/datum/dynamic_ruleset/midround/from_living/obsessed - name = "Obsessed" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/obsessed - antag_flag = ROLE_OBSESSED - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ROLE_POSITRONIC_BRAIN, - ) - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 4 - cost = 3 // Doesn't have the same impact on rounds as revenants, dragons, sentient disease (10) or syndicate infiltrators (5). - repeatable = TRUE - -/datum/dynamic_ruleset/midround/from_living/obsessed/trim_candidates() - ..() - candidates = living_players - for(var/mob/living/carbon/human/candidate in candidates) - if( \ - !candidate.get_organ_by_type(/obj/item/organ/brain) \ - || candidate.mind.has_antag_datum(/datum/antagonist/obsessed) \ - || candidate.stat == DEAD \ - || !(ROLE_OBSESSED in candidate.client?.prefs?.be_special) \ - || !candidate.mind.assigned_role \ - ) - candidates -= candidate - -/datum/dynamic_ruleset/midround/from_living/obsessed/execute() - var/mob/living/carbon/human/obsessed = pick_n_take(candidates) - obsessed.gain_trauma(/datum/brain_trauma/special/obsessed) - message_admins("[ADMIN_LOOKUPFLW(obsessed)] has been made Obsessed by the midround ruleset.") - log_game("[key_name(obsessed)] was made Obsessed by the midround ruleset.") - return TRUE - -/// Midround Space Changeling Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/changeling_midround - name = "Space Changeling" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/changeling/space - antag_flag = ROLE_CHANGELING_MIDROUND - antag_flag_override = ROLE_CHANGELING - required_type = /mob/dead/observer - required_enemies = list(2,2,1,1,1,1,1,0,0,0) - required_candidates = 1 - weight = 3 - cost = 7 - minimum_players = 15 - repeatable = TRUE - -/datum/dynamic_ruleset/midround/from_ghosts/changeling_midround/generate_ruleset_body(mob/applicant) - var/body = generate_changeling_meteor(applicant) - message_admins("[ADMIN_LOOKUPFLW(body)] has been made into a space changeling by the midround ruleset.") - log_dynamic("[key_name(body)] was spawned as a space changeling by the midround ruleset.") - return body - -/// Midround Paradox Clone Ruleset (From Ghosts) -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone - name = "Paradox Clone" - midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT - antag_datum = /datum/antagonist/paradox_clone - antag_flag = ROLE_PARADOX_CLONE - enemy_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_SECURITY, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - required_enemies = list(2, 2, 1, 1, 1, 1, 1, 0, 0, 0) - required_candidates = 1 - weight = 4 - cost = 3 - repeatable = TRUE - var/list/possible_spawns = list() ///places the antag can spawn - -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/forget_startup() - possible_spawns = list() - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/execute() - possible_spawns += find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE) - if(!possible_spawns.len) - return MAP_ERROR - return ..() - -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/generate_ruleset_body(mob/applicant) - var/datum/mind/player_mind = new /datum/mind(applicant.key) - player_mind.active = TRUE - - var/mob/living/carbon/human/clone_victim = find_original() - var/mob/living/carbon/human/clone = clone_victim.make_full_human_copy(pick(possible_spawns)) - player_mind.transfer_to(clone) - - var/datum/antagonist/paradox_clone/new_datum = player_mind.add_antag_datum(/datum/antagonist/paradox_clone) - new_datum.original_ref = WEAKREF(clone_victim.mind) - new_datum.setup_clone() - - playsound(clone, 'sound/weapons/zapbang.ogg', 30, TRUE) - new /obj/item/storage/toolbox/mechanical(clone.loc) //so they dont get stuck in maints - - message_admins("[ADMIN_LOOKUPFLW(clone)] has been made into a Paradox Clone by the midround ruleset.") - clone.log_message("was spawned as a Paradox Clone of [key_name(clone)] by the midround ruleset.", LOG_GAME) - - return clone - -/** - * Trims through GLOB.player_list and finds a target - * Returns a single human victim, if none is possible then returns null. - */ -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/proc/find_original() - var/list/possible_targets = list() - - for(var/mob/living/carbon/human/player in GLOB.player_list) - if(!player.client || !player.mind || player.stat) - continue - if(!(player.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) - continue - possible_targets += player - - if(possible_targets.len) - return pick(possible_targets) - return FALSE - -#undef MALF_ION_PROB -#undef REPLACE_LAW_WITH_ION_PROB 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 290b71161625..000000000000 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm +++ /dev/null @@ -1,701 +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) - . = ..() - var/num_traitors = get_antag_cap(population) * (scaled_times + 1) - for (var/i = 1 to num_traitors) - if(candidates.len <= 0) - break - var/mob/M = pick_n_take(candidates) - assigned += M.mind - M.mind.special_role = ROLE_TRAITOR - M.mind.restricted_roles = restricted_roles - GLOB.pre_setup_antags += M.mind - return TRUE - -////////////////////////////////////////////// -// // -// MALFUNCTIONING AI // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/malf_ai - name = "Malfunctioning AI" - antag_flag = ROLE_MALF - antag_datum = /datum/antagonist/malf_ai - minimum_required_age = 14 - exclusive_roles = list(JOB_AI) - required_candidates = 1 - weight = 3 - cost = 18 - requirements = list(101,101,101,80,60,50,30,20,10,10) - antag_cap = 1 - flags = HIGH_IMPACT_RULESET - -/datum/dynamic_ruleset/roundstart/malf_ai/ready(forced) - var/datum/job/ai_job = SSjob.GetJobType(/datum/job/ai) - - // If we're not forced, we're going to make sure we can actually have an AI in this shift, - if(!forced && min(ai_job.total_positions - ai_job.current_positions, ai_job.spawn_positions) <= 0) - log_dynamic("FAIL: [src] could not run, because there is nobody who wants to be an AI") - return FALSE - - return ..() - -/datum/dynamic_ruleset/roundstart/malf_ai/pre_execute(population) - . = ..() - - var/datum/job/ai_job = SSjob.GetJobType(/datum/job/ai) - // Maybe a bit too pedantic, but there should never be more malf AIs than there are available positions, spawn positions or antag cap allocations. - var/num_malf = min(get_antag_cap(population), min(ai_job.total_positions - ai_job.current_positions, ai_job.spawn_positions)) - for (var/i in 1 to num_malf) - if(candidates.len <= 0) - break - var/mob/new_malf = pick_n_take(candidates) - assigned += new_malf.mind - new_malf.mind.special_role = ROLE_MALF - GLOB.pre_setup_antags += new_malf.mind - // We need an AI for the malf roundstart ruleset to execute. This means that players who get selected as malf AI get priority, because antag selection comes before role selection. - LAZYADDASSOC(SSjob.dynamic_forced_occupations, new_malf, "AI") - return TRUE - -////////////////////////////////////////// -// // -// BLOOD BROTHERS // -// // -////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/traitorbro - name = "Blood Brothers" - antag_flag = ROLE_BROTHER - antag_datum = /datum/antagonist/brother - protected_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ) - weight = 5 - cost = 8 - scaling_cost = 15 - requirements = list(40,30,30,20,20,15,15,15,10,10) - antag_cap = 1 - -/datum/dynamic_ruleset/roundstart/traitorbro/pre_execute(population) - . = ..() - - for (var/_ in 1 to get_antag_cap(population) * (scaled_times + 1)) - var/mob/candidate = pick_n_take(candidates) - if (isnull(candidate)) - break - - assigned += candidate.mind - candidate.mind.restricted_roles = restricted_roles - 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) - var/datum/team/brother_team/team = new - team.add_member(mind) - team.forge_brother_objectives() - mind.add_antag_datum(/datum/antagonist/brother, team) - GLOB.pre_setup_antags -= mind - - return TRUE - -////////////////////////////////////////////// -// // -// CHANGELINGS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/changeling - name = "Changelings" - antag_flag = ROLE_CHANGELING - antag_datum = /datum/antagonist/changeling - protected_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ) - required_candidates = 1 - weight = 3 - cost = 16 - scaling_cost = 10 - requirements = list(70,70,60,50,40,20,20,10,10,10) - antag_cap = list("denominator" = 29) - -/datum/dynamic_ruleset/roundstart/changeling/pre_execute(population) - . = ..() - var/num_changelings = get_antag_cap(population) * (scaled_times + 1) - for (var/i = 1 to num_changelings) - if(candidates.len <= 0) - break - var/mob/M = pick_n_take(candidates) - assigned += M.mind - M.mind.restricted_roles = restricted_roles - M.mind.special_role = ROLE_CHANGELING - GLOB.pre_setup_antags += M.mind - return TRUE - -/datum/dynamic_ruleset/roundstart/changeling/execute() - for(var/datum/mind/changeling in assigned) - var/datum/antagonist/changeling/new_antag = new antag_datum() - changeling.add_antag_datum(new_antag) - GLOB.pre_setup_antags -= changeling - return TRUE - -////////////////////////////////////////////// -// // -// HERETICS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/heretics - name = "Heretics" - antag_flag = ROLE_HERETIC - antag_datum = /datum/antagonist/heretic - protected_roles = list( - JOB_CAPTAIN, - JOB_DETECTIVE, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - restricted_roles = list( - JOB_AI, - JOB_CYBORG, - ) - required_candidates = 1 - weight = 3 - cost = 10 - scaling_cost = 9 - requirements = list(101,101,60,30,30,25,20,15,10,10) - antag_cap = list("denominator" = 24) - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_HERETIC_SACRIFICE) - - -/datum/dynamic_ruleset/roundstart/heretics/pre_execute(population) - . = ..() - var/num_ecult = get_antag_cap(population) * (scaled_times + 1) - - for (var/i = 1 to num_ecult) - if(candidates.len <= 0) - break - var/mob/picked_candidate = pick_n_take(candidates) - assigned += picked_candidate.mind - picked_candidate.mind.restricted_roles = restricted_roles - picked_candidate.mind.special_role = ROLE_HERETIC - GLOB.pre_setup_antags += picked_candidate.mind - return TRUE - -/datum/dynamic_ruleset/roundstart/heretics/execute() - - for(var/c in assigned) - var/datum/mind/cultie = c - var/datum/antagonist/heretic/new_antag = new antag_datum() - cultie.add_antag_datum(new_antag) - GLOB.pre_setup_antags -= cultie - - return TRUE - - -////////////////////////////////////////////// -// // -// WIZARDS // -// // -////////////////////////////////////////////// - -// Dynamic is a wonderful thing that adds wizards to every round and then adds even more wizards during the round. -/datum/dynamic_ruleset/roundstart/wizard - name = "Wizard" - antag_flag = ROLE_WIZARD - antag_datum = /datum/antagonist/wizard - flags = HIGH_IMPACT_RULESET - minimum_required_age = 14 - restricted_roles = list( - JOB_CAPTAIN, - JOB_HEAD_OF_SECURITY, - ) // Just to be sure that a wizard getting picked won't ever imply a Captain or HoS not getting drafted - required_candidates = 1 - weight = 2 - cost = 20 - requirements = list(90,90,90,80,60,40,30,20,10,10) - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_WIZARDDEN) - -/datum/dynamic_ruleset/roundstart/wizard/ready(forced = FALSE) - if(!check_candidates()) - return FALSE - if(!length(GLOB.wizardstart)) - log_admin("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") - message_admins("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") - return FALSE - return ..() - -/datum/dynamic_ruleset/roundstart/wizard/round_result() - for(var/datum/antagonist/wizard/wiz in GLOB.antagonists) - var/mob/living/real_wiz = wiz.owner?.current - if(isnull(real_wiz)) - continue - - var/turf/wiz_location = get_turf(real_wiz) - // If this wiz is alive AND not in an away level, then we know not all wizards are dead and can leave entirely - if(considered_alive(wiz.owner) && wiz_location && !is_away_level(wiz_location.z)) - return - - SSticker.news_report = WIZARD_KILLED - -/datum/dynamic_ruleset/roundstart/wizard/pre_execute() - . = ..() - if(GLOB.wizardstart.len == 0) - return FALSE - var/mob/M = pick_n_take(candidates) - if (M) - assigned += M.mind - M.mind.set_assigned_role(SSjob.GetJobType(/datum/job/space_wizard)) - M.mind.special_role = ROLE_WIZARD - - return TRUE - -/datum/dynamic_ruleset/roundstart/wizard/execute() - for(var/datum/mind/M in assigned) - M.current.forceMove(pick(GLOB.wizardstart)) - M.add_antag_datum(new antag_datum()) - return TRUE - -////////////////////////////////////////////// -// // -// BLOOD CULT // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/bloodcult - name = "Blood Cult" - antag_flag = ROLE_CULTIST - antag_datum = /datum/antagonist/cult - minimum_required_age = 14 - restricted_roles = list( - JOB_AI, - JOB_CAPTAIN, - JOB_CHAPLAIN, - JOB_CYBORG, - JOB_DETECTIVE, - JOB_HEAD_OF_PERSONNEL, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - required_candidates = 2 - weight = 3 - cost = 20 - requirements = list(100,90,80,60,40,30,10,10,10,10) - flags = HIGH_IMPACT_RULESET - antag_cap = list("denominator" = 20, "offset" = 1) - var/datum/team/cult/main_cult - -/datum/dynamic_ruleset/roundstart/bloodcult/ready(population, forced = FALSE) - required_candidates = get_antag_cap(population) - return ..() - -/datum/dynamic_ruleset/roundstart/bloodcult/pre_execute(population) - . = ..() - var/cultists = get_antag_cap(population) - for(var/cultists_number = 1 to cultists) - if(candidates.len <= 0) - break - var/mob/M = pick_n_take(candidates) - assigned += M.mind - M.mind.special_role = ROLE_CULTIST - M.mind.restricted_roles = restricted_roles - GLOB.pre_setup_antags += M.mind - return TRUE - -/datum/dynamic_ruleset/roundstart/bloodcult/execute() - main_cult = new - for(var/datum/mind/M in assigned) - var/datum/antagonist/cult/new_cultist = new antag_datum() - new_cultist.cult_team = main_cult - new_cultist.give_equipment = TRUE - M.add_antag_datum(new_cultist) - GLOB.pre_setup_antags -= M - main_cult.setup_objectives() - return TRUE - -/datum/dynamic_ruleset/roundstart/bloodcult/round_result() - if(main_cult.check_cult_victory()) - SSticker.mode_result = "win - cult win" - SSticker.news_report = CULT_SUMMON - return - - SSticker.mode_result = "loss - staff stopped the cult" - - if(main_cult.size_at_maximum == 0) - CRASH("Cult team existed with a size_at_maximum of 0 at round end!") - - // If more than a certain ratio of our cultists have escaped, give the "cult escape" resport. - // Otherwise, give the "cult failure" report. - var/ratio_to_be_considered_escaped = 0.5 - var/escaped_cultists = 0 - for(var/datum/mind/escapee as anything in main_cult.members) - if(considered_escaped(escapee)) - escaped_cultists++ - - SSticker.news_report = (escaped_cultists / main_cult.size_at_maximum) >= ratio_to_be_considered_escaped ? CULT_ESCAPE : CULT_FAILURE - -////////////////////////////////////////////// -// // -// NUCLEAR OPERATIVES // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/nuclear - name = "Nuclear Emergency" - antag_flag = ROLE_OPERATIVE - antag_datum = /datum/antagonist/nukeop - var/datum/antagonist/antag_leader_datum = /datum/antagonist/nukeop/leader - minimum_required_age = 14 - restricted_roles = list( - JOB_CAPTAIN, - JOB_HEAD_OF_SECURITY, - ) // Just to be sure that a nukie getting picked won't ever imply a Captain or HoS not getting drafted - required_candidates = 5 - weight = 3 - cost = 20 - requirements = list(90,90,90,80,60,40,30,20,10,10) - flags = HIGH_IMPACT_RULESET - antag_cap = list("denominator" = 18, "offset" = 1) - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE) - var/required_role = ROLE_NUCLEAR_OPERATIVE - var/datum/team/nuclear/nuke_team - -/datum/dynamic_ruleset/roundstart/nuclear/ready(population, forced = FALSE) - required_candidates = get_antag_cap(population) - return ..() - -/datum/dynamic_ruleset/roundstart/nuclear/pre_execute(population) - . = ..() - // If ready() did its job, candidates should have 5 or more members in it - var/operatives = get_antag_cap(population) - for(var/operatives_number = 1 to operatives) - if(candidates.len <= 0) - break - var/mob/M = pick_n_take(candidates) - assigned += M.mind - M.mind.set_assigned_role(SSjob.GetJobType(/datum/job/nuclear_operative)) - M.mind.special_role = ROLE_NUCLEAR_OPERATIVE - return TRUE - -/datum/dynamic_ruleset/roundstart/nuclear/execute() - var/datum/mind/most_experienced = get_most_experienced(assigned, required_role) - if(!most_experienced) - most_experienced = assigned[1] - var/datum/antagonist/nukeop/leader/leader = most_experienced.add_antag_datum(antag_leader_datum) - nuke_team = leader.nuke_team - for(var/datum/mind/assigned_player in assigned) - if(assigned_player == most_experienced) - continue - var/datum/antagonist/nukeop/new_op = new antag_datum() - assigned_player.add_antag_datum(new_op) - return TRUE - -/datum/dynamic_ruleset/roundstart/nuclear/round_result() - var/result = nuke_team.get_result() - switch(result) - if(NUKE_RESULT_FLUKE) - SSticker.mode_result = "loss - syndicate nuked - disk secured" - SSticker.news_report = NUKE_SYNDICATE_BASE - if(NUKE_RESULT_NUKE_WIN) - SSticker.mode_result = "win - syndicate nuke" - SSticker.news_report = STATION_DESTROYED_NUKE - if(NUKE_RESULT_NOSURVIVORS) - SSticker.mode_result = "halfwin - syndicate nuke - did not evacuate in time" - SSticker.news_report = STATION_DESTROYED_NUKE - if(NUKE_RESULT_WRONG_STATION) - SSticker.mode_result = "halfwin - blew wrong station" - SSticker.news_report = NUKE_MISS - if(NUKE_RESULT_WRONG_STATION_DEAD) - SSticker.mode_result = "halfwin - blew wrong station - did not evacuate in time" - SSticker.news_report = NUKE_MISS - if(NUKE_RESULT_CREW_WIN_SYNDIES_DEAD) - SSticker.mode_result = "loss - evacuation - disk secured - syndi team dead" - SSticker.news_report = OPERATIVES_KILLED - if(NUKE_RESULT_CREW_WIN) - SSticker.mode_result = "loss - evacuation - disk secured" - SSticker.news_report = OPERATIVES_KILLED - if(NUKE_RESULT_DISK_LOST) - SSticker.mode_result = "halfwin - evacuation - disk not secured" - SSticker.news_report = OPERATIVE_SKIRMISH - if(NUKE_RESULT_DISK_STOLEN) - SSticker.mode_result = "halfwin - detonation averted" - SSticker.news_report = OPERATIVE_SKIRMISH - else - SSticker.mode_result = "halfwin - interrupted" - SSticker.news_report = OPERATIVE_SKIRMISH - -////////////////////////////////////////////// -// // -// REVS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/revs - name = "Revolution" - persistent = TRUE - antag_flag = ROLE_REV_HEAD - antag_flag_override = ROLE_REV_HEAD - antag_datum = /datum/antagonist/rev/head - minimum_required_age = 14 - restricted_roles = list( - JOB_AI, - JOB_CAPTAIN, - JOB_CHIEF_ENGINEER, - JOB_CHIEF_MEDICAL_OFFICER, - JOB_CYBORG, - JOB_DETECTIVE, - JOB_HEAD_OF_PERSONNEL, - JOB_HEAD_OF_SECURITY, - JOB_PRISONER, - JOB_QUARTERMASTER, - JOB_RESEARCH_DIRECTOR, - JOB_SECURITY_OFFICER, - JOB_WARDEN, - ) - required_candidates = 3 - weight = 3 - delay = 7 MINUTES - cost = 20 - requirements = list(101,101,70,40,30,20,10,10,10,10) - antag_cap = 3 - flags = HIGH_IMPACT_RULESET - blocking_rules = list(/datum/dynamic_ruleset/latejoin/provocateur) - // I give up, just there should be enough heads with 35 players... - minimum_players = 35 - var/datum/team/revolution/revolution - var/finished = FALSE - -/datum/dynamic_ruleset/roundstart/revs/pre_execute(population) - . = ..() - var/max_candidates = get_antag_cap(population) - for(var/i = 1 to max_candidates) - if(candidates.len <= 0) - break - var/mob/M = pick_n_take(candidates) - assigned += M.mind - M.mind.restricted_roles = restricted_roles - M.mind.special_role = antag_flag - GLOB.pre_setup_antags += M.mind - return TRUE - -/datum/dynamic_ruleset/roundstart/revs/execute() - revolution = new() - for(var/datum/mind/M in assigned) - GLOB.pre_setup_antags -= M - if(check_eligible(M)) - var/datum/antagonist/rev/head/new_head = new antag_datum() - new_head.give_flash = TRUE - new_head.give_hud = TRUE - new_head.remove_clumsy = TRUE - M.add_antag_datum(new_head,revolution) - else - assigned -= M - log_dynamic("[ruletype] [name] discarded [M.name] from head revolutionary due to ineligibility.") - if(revolution.members.len) - revolution.update_objectives() - revolution.update_rev_heads() - SSshuttle.registerHostileEnvironment(revolution) - return TRUE - log_dynamic("[ruletype] [name] failed to get any eligible headrevs. Refunding [cost] threat.") - return FALSE - -/datum/dynamic_ruleset/roundstart/revs/clean_up() - qdel(revolution) - ..() - -/datum/dynamic_ruleset/roundstart/revs/rule_process() - var/winner = revolution.process_victory() - if (isnull(winner)) - return - - finished = winner - - if(winner == REVOLUTION_VICTORY) - GLOB.revolutionary_win = TRUE - - return RULESET_STOP_PROCESSING - -/// Checks for revhead loss conditions and other antag datums. -/datum/dynamic_ruleset/roundstart/revs/proc/check_eligible(datum/mind/M) - var/turf/T = get_turf(M.current) - if(!considered_afk(M) && considered_alive(M) && is_station_level(T.z) && !M.antag_datums?.len && !HAS_TRAIT(M, TRAIT_MINDSHIELD)) - return TRUE - return FALSE - -/datum/dynamic_ruleset/roundstart/revs/round_result() - revolution.round_result(finished) - -// Admin only rulesets. The threat requirement is 101 so it is not possible to roll them. - -////////////////////////////////////////////// -// // -// EXTENDED // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/extended - name = "Extended" - antag_flag = null - antag_datum = null - restricted_roles = list() - required_candidates = 0 - weight = 3 - cost = 0 - requirements = list(101,101,101,101,101,101,101,101,101,101) - flags = LONE_RULESET - -/datum/dynamic_ruleset/roundstart/extended/pre_execute() - . = ..() - message_admins("Starting a round of extended.") - log_game("Starting a round of extended.") - SSdynamic.spend_roundstart_budget(SSdynamic.round_start_budget) - SSdynamic.spend_midround_budget(SSdynamic.mid_round_budget) - SSdynamic.threat_log += "[worldtime2text()]: Extended ruleset set threat to 0." - return TRUE - -////////////////////////////////////////////// -// // -// CLOWN OPS // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/nuclear/clown_ops - name = "Clown Operatives" - antag_datum = /datum/antagonist/nukeop/clownop - antag_flag = ROLE_CLOWN_OPERATIVE - antag_flag_override = ROLE_OPERATIVE - antag_leader_datum = /datum/antagonist/nukeop/leader/clownop - requirements = list(101,101,101,101,101,101,101,101,101,101) - required_role = ROLE_CLOWN_OPERATIVE - -/datum/dynamic_ruleset/roundstart/nuclear/clown_ops/pre_execute() - . = ..() - if(!.) - return - - var/list/nukes = SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/syndicate) - for(var/obj/machinery/nuclearbomb/syndicate/nuke as anything in nukes) - new /obj/machinery/nuclearbomb/syndicate/bananium(nuke.loc) - qdel(nuke) - - for(var/datum/mind/clowns in assigned) - clowns.set_assigned_role(SSjob.GetJobType(/datum/job/clown_operative)) - clowns.special_role = ROLE_CLOWN_OPERATIVE - -////////////////////////////////////////////// -// // -// METEOR // -// // -////////////////////////////////////////////// - -/datum/dynamic_ruleset/roundstart/meteor - name = "Meteor" - persistent = TRUE - required_candidates = 0 - weight = 3 - cost = 0 - requirements = list(101,101,101,101,101,101,101,101,101,101) - flags = LONE_RULESET - var/meteordelay = 2000 - var/nometeors = FALSE - var/rampupdelta = 5 - -/datum/dynamic_ruleset/roundstart/meteor/rule_process() - if(nometeors || meteordelay > world.time - SSticker.round_start_time) - return - - var/list/wavetype = GLOB.meteors_normal - var/meteorminutes = (world.time - SSticker.round_start_time - meteordelay) / 10 / 60 - - if (prob(meteorminutes)) - wavetype = GLOB.meteors_threatening - - if (prob(meteorminutes/2)) - wavetype = GLOB.meteors_catastrophic - - var/ramp_up_final = clamp(round(meteorminutes/rampupdelta), 1, 10) - - spawn_meteors(ramp_up_final, wavetype) - -/// Ruleset for Nations -/datum/dynamic_ruleset/roundstart/nations - name = "Nations" - required_candidates = 0 - weight = 0 //admin only (and for good reason) - cost = 0 - flags = LONE_RULESET | ONLY_RULESET - -/datum/dynamic_ruleset/roundstart/nations/execute() - . = ..() - //notably assistant is not in this list to prevent the round turning into BARBARISM instantly, and silicon is in this list for UN - var/list/department_types = list( - /datum/job_department/silicon, //united nations - /datum/job_department/cargo, - /datum/job_department/engineering, - /datum/job_department/medical, - /datum/job_department/science, - /datum/job_department/security, - /datum/job_department/service, - ) - - for(var/department_type in department_types) - create_separatist_nation(department_type, announcement = FALSE, dangerous = FALSE, message_admins = FALSE) - - GLOB.round_default_lawset = /datum/ai_laws/united_nations diff --git a/code/controllers/subsystem/dynamic/dynamic_testing.dm b/code/controllers/subsystem/dynamic/dynamic_testing.dm new file mode 100644 index 000000000000..2b6730a0f0bc --- /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)*/ GLOB.admin_state + +/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 994f2e3f5de7..000000000000 --- 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 ec21626945ea..000000000000 --- a/code/controllers/subsystem/dynamic/readme.md +++ /dev/null @@ -1,194 +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 f22ce3315740..000000000000 --- a/code/controllers/subsystem/dynamic/ruleset_picking.dm +++ /dev/null @@ -1,139 +0,0 @@ -#define ADMIN_CANCEL_MIDROUND_TIME (10 SECONDS) - -/// -/// -/** - * From a list of rulesets, returns one based on weight and availability. - * Mutates the list that is passed into it to remove invalid rules. - * - * * max_allowed_attempts - Allows you to configure how many times the proc will attempt to pick a ruleset before giving up. - */ -/datum/controller/subsystem/dynamic/proc/pick_ruleset(list/drafted_rules, max_allowed_attempts = INFINITY) - if (only_ruleset_executed) - log_dynamic("FAIL: only_ruleset_executed") - return null - - if(!length(drafted_rules)) - log_dynamic("FAIL: pick ruleset supplied with an empty list of drafted rules.") - return null - - var/attempts = 0 - while (attempts < max_allowed_attempts) - attempts++ - var/datum/dynamic_ruleset/rule = pick_weight(drafted_rules) - if (!rule) - var/list/leftover_rules = list() - for (var/leftover_rule in drafted_rules) - leftover_rules += "[leftover_rule]" - - log_dynamic("FAIL: No rulesets left to pick. Leftover rules: [leftover_rules.Join(", ")]") - return null - - if (check_blocking(rule.blocking_rules, executed_rules)) - log_dynamic("FAIL: [rule] can't execute as another rulset is blocking it.") - drafted_rules -= rule - if(drafted_rules.len <= 0) - return null - continue - else if ( - rule.flags & HIGH_IMPACT_RULESET \ - && threat_level < GLOB.dynamic_stacking_limit \ - && GLOB.dynamic_no_stacking \ - && high_impact_ruleset_executed \ - ) - log_dynamic("FAIL: [rule] can't execute as a high impact ruleset was already executed.") - drafted_rules -= rule - if(drafted_rules.len <= 0) - return null - continue - - return rule - - return null - -/// Executes a random midround ruleset from the list of drafted rules. -/datum/controller/subsystem/dynamic/proc/pick_midround_rule(list/drafted_rules, description) - log_dynamic("Rolling [drafted_rules.len] [description]") - - var/datum/dynamic_ruleset/rule = pick_ruleset(drafted_rules) - if (isnull(rule)) - return null - - current_midround_rulesets = drafted_rules - rule - - midround_injection_timer_id = addtimer( - CALLBACK(src, PROC_REF(execute_midround_rule), rule), \ - ADMIN_CANCEL_MIDROUND_TIME, \ - TIMER_STOPPABLE, \ - ) - - log_dynamic("[rule] ruleset executing...") - message_admins("DYNAMIC: Executing midround ruleset [rule] in [DisplayTimeText(ADMIN_CANCEL_MIDROUND_TIME)]. \ - CANCEL | \ - SOMETHING ELSE") - - return rule - -/// Fired after admins do not cancel a midround injection. -/datum/controller/subsystem/dynamic/proc/execute_midround_rule(datum/dynamic_ruleset/rule) - current_midround_rulesets = null - midround_injection_timer_id = null - if (!rule.repeatable) - midround_rules = remove_from_list(midround_rules, rule.type) - addtimer(CALLBACK(src, PROC_REF(execute_midround_latejoin_rule), rule), rule.delay) - -/// Mainly here to facilitate delayed rulesets. All midround/latejoin rulesets are executed with a timered callback to this proc. -/datum/controller/subsystem/dynamic/proc/execute_midround_latejoin_rule(sent_rule) - var/datum/dynamic_ruleset/rule = sent_rule - spend_midround_budget(rule.cost, threat_log, "[worldtime2text()]: [rule.ruletype] [rule.name]") - rule.pre_execute(GLOB.alive_player_list.len) - if (rule.execute()) - log_dynamic("Injected a [rule.ruletype] ruleset [rule.name].") - if(rule.flags & HIGH_IMPACT_RULESET) - high_impact_ruleset_executed = TRUE - else if(rule.flags & ONLY_RULESET) - only_ruleset_executed = TRUE - if(rule.ruletype == LATEJOIN_RULESET) - var/mob/M = pick(rule.candidates) - message_admins("[key_name(M)] joined the station, and was selected by the [rule.name] ruleset.") - log_dynamic("[key_name(M)] joined the station, and was selected by the [rule.name] ruleset.") - executed_rules += rule - if (rule.persistent) - current_rules += rule - new_snapshot(rule) - rule.forget_startup() - return TRUE - rule.forget_startup() - rule.clean_up() - stack_trace("The [rule.ruletype] rule \"[rule.name]\" failed to execute.") - return FALSE - -/// Fired when an admin cancels the current midround injection. -/datum/controller/subsystem/dynamic/proc/admin_cancel_midround(mob/user, timer_id) - if (midround_injection_timer_id != timer_id || !deltimer(midround_injection_timer_id)) - to_chat(user, span_notice("Too late!")) - return - - log_admin("[key_name(user)] cancelled the next midround injection.") - message_admins("[key_name(user)] cancelled the next midround injection.") - midround_injection_timer_id = null - current_midround_rulesets = null - -/// Fired when an admin requests a different midround injection. -/datum/controller/subsystem/dynamic/proc/admin_different_midround(mob/user, timer_id) - if (midround_injection_timer_id != timer_id || !deltimer(midround_injection_timer_id)) - to_chat(user, span_notice("Too late!")) - return - - midround_injection_timer_id = null - - if (isnull(current_midround_rulesets) || current_midround_rulesets.len == 0) - log_admin("[key_name(user)] asked for a different midround injection, but there were none left.") - message_admins("[key_name(user)] asked for a different midround injection, but there were none left.") - return - - log_admin("[key_name(user)] asked for a different midround injection.") - message_admins("[key_name(user)] asked for a different midround injection.") - pick_midround_rule(current_midround_rulesets, "different midround rulesets") - -#undef ADMIN_CANCEL_MIDROUND_TIME diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm index f12ec3f288cb..0b8e96f275d1 100644 --- a/code/controllers/subsystem/job.dm +++ b/code/controllers/subsystem/job.dm @@ -24,7 +24,7 @@ SUBSYSTEM_DEF(job) var/list/unassigned = list() //Players who need jobs var/initial_players_to_assign = 0 //used for checking against population caps - // Whether to run DivideOccupations pure so that there are no side-effects from calling it other than + // Whether to run divide_occupations pure so that there are no side-effects from calling it other than // a player's assigned_role being set to some value. var/run_divide_occupation_pure = FALSE @@ -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. @@ -90,7 +92,7 @@ SUBSYSTEM_DEF(job) setup_job_lists() job_config_datum_singletons = generate_config_singletons() // we set this up here regardless in case someone wants to use the verb to generate the config file. if(!length(all_occupations)) - SetupOccupations() + setup_occupations() if(CONFIG_GET(flag/load_jobs_from_txt)) load_jobs_from_config() set_overflow_role(CONFIG_GET(string/overflow_job)) // this must always go after load_jobs_from_config() due to how the legacy systems operate, this always takes precedent. @@ -110,9 +112,9 @@ SUBSYSTEM_DEF(job) return overflow_jobs /datum/controller/subsystem/job/proc/set_overflow_role(new_overflow_role) - var/datum/job/new_overflow = ispath(new_overflow_role) ? GetJobType(new_overflow_role) : GetJob(new_overflow_role) + var/datum/job/new_overflow = ispath(new_overflow_role) ? get_job_type(new_overflow_role) : get_job(new_overflow_role) if(!new_overflow) - JobDebug("Failed to set new overflow role: [new_overflow_role]") + job_debug("Failed to set new overflow role: [new_overflow_role]") CRASH("set_overflow_role failed | new_overflow_role: [isnull(new_overflow_role) ? "null" : new_overflow_role]") var/cap = CONFIG_GET(number/overflow_cap) @@ -123,17 +125,17 @@ SUBSYSTEM_DEF(job) if(new_overflow.type == overflow_role) return - var/datum/job/old_overflow = GetJobType(overflow_role) + var/datum/job/old_overflow = get_job_type(overflow_role) old_overflow.allow_bureaucratic_error = initial(old_overflow.allow_bureaucratic_error) old_overflow.spawn_positions = initial(old_overflow.spawn_positions) old_overflow.total_positions = initial(old_overflow.total_positions) if(!(initial(old_overflow.job_flags) & JOB_CANNOT_OPEN_SLOTS)) old_overflow.job_flags &= ~JOB_CANNOT_OPEN_SLOTS overflow_role = new_overflow.type - JobDebug("Overflow role set to : [new_overflow.type]") + job_debug("Overflow role set to : [new_overflow.type]") -/datum/controller/subsystem/job/proc/SetupOccupations() +/datum/controller/subsystem/job/proc/setup_occupations() name_occupations = list() type_occupations = list() @@ -203,20 +205,20 @@ SUBSYSTEM_DEF(job) return TRUE -/datum/controller/subsystem/job/proc/GetJob(rank) +/datum/controller/subsystem/job/proc/get_job(rank) if(!length(all_occupations)) - SetupOccupations() + setup_occupations() return name_occupations[rank] -/datum/controller/subsystem/job/proc/GetJobType(jobtype) +/datum/controller/subsystem/job/proc/get_job_type(jobtype) RETURN_TYPE(/datum/job) if(!length(all_occupations)) - SetupOccupations() + setup_occupations() return type_occupations[jobtype] /datum/controller/subsystem/job/proc/get_department_type(department_type) if(!length(all_occupations)) - SetupOccupations() + setup_occupations() return joinable_departments_by_type[department_type] /** @@ -228,89 +230,89 @@ SUBSYSTEM_DEF(job) * * latejoin - Set to TRUE if this is a latejoin role assignment. * * do_eligibility_checks - Set to TRUE to conduct all job eligibility tests and reject on failure. Set to FALSE if job eligibility has been tested elsewhere and they can be safely skipped. */ -/datum/controller/subsystem/job/proc/AssignRole(mob/dead/new_player/player, datum/job/job, latejoin = FALSE, do_eligibility_checks = TRUE) - JobDebug("Running AR, Player: [player], Job: [isnull(job) ? "null" : job], LateJoin: [latejoin]") +/datum/controller/subsystem/job/proc/assign_role(mob/dead/new_player/player, datum/job/job, latejoin = FALSE, do_eligibility_checks = TRUE) + job_debug("Running AR, Player: [player], Job: [isnull(job) ? "null" : job], LateJoin: [latejoin]") if(!player?.mind || !job) - JobDebug("AR has failed, player has no mind or job is null, Player: [player], Rank: [isnull(job) ? "null" : job.type]") + job_debug("AR has failed, player has no mind or job is null, Player: [player], Rank: [isnull(job) ? "null" : job.type]") return FALSE if(do_eligibility_checks && (check_job_eligibility(player, job, "AR", add_job_to_log = TRUE) != JOB_AVAILABLE)) return FALSE - JobDebug("Player: [player] is now Rank: [job.title], JCP:[job.current_positions], JPL:[latejoin ? job.total_positions : job.spawn_positions]") + job_debug("Player: [player] is now Rank: [job.title], JCP:[job.current_positions], JPL:[latejoin ? job.total_positions : job.spawn_positions]") player.mind.set_assigned_role(job) unassigned -= player job.current_positions++ return TRUE -/datum/controller/subsystem/job/proc/FindOccupationCandidates(datum/job/job, level) - JobDebug("Running FOC, Job: [job], Level: [job_priority_level_to_string(level)]") +/datum/controller/subsystem/job/proc/find_occupation_candidates(datum/job/job, level = 0) + job_debug("FOC: Now running, Job: [job], Level: [job_priority_level_to_string(level)]") var/list/candidates = list() for(var/mob/dead/new_player/player in unassigned) if(!player) - JobDebug("FOC player no longer exists.") + job_debug("FOC: Player no longer exists.") continue + if(!player.client) - JobDebug("FOC player client no longer exists, Player: [player]") + job_debug("FOC: Player client no longer exists, Player: [player]") continue + // Initial screening check. Does the player even have the job enabled, if they do - Is it at the correct priority level? var/player_job_level = player.client?.prefs.job_preferences[job.title] if(isnull(player_job_level)) - JobDebug("FOC player job not enabled, Player: [player]") + job_debug("FOC: Player job not enabled, Player: [player]") continue - else if(player_job_level != level) - JobDebug("FOC player job enabled at wrong level, Player: [player], TheirLevel: [job_priority_level_to_string(player_job_level)], ReqLevel: [job_priority_level_to_string(level)]") + + if(level && (player_job_level != level)) + job_debug("FOC: Player job enabled at wrong level, Player: [player], TheirLevel: [job_priority_level_to_string(player_job_level)], ReqLevel: [job_priority_level_to_string(level)]") continue - // This check handles its own output to JobDebug. + // This check handles its own output to job_debug. if(check_job_eligibility(player, job, "FOC", add_job_to_log = FALSE) != JOB_AVAILABLE) continue // They have the job enabled, at this priority level, with no restrictions applying to them. - JobDebug("FOC pass, Player: [player], Level: [job_priority_level_to_string(level)]") + job_debug("FOC: Player eligible, Player: [player], Level: [job_priority_level_to_string(level)]") candidates += player return candidates - -/datum/controller/subsystem/job/proc/GiveRandomJob(mob/dead/new_player/player) - JobDebug("GRJ Giving random job, Player: [player]") +/datum/controller/subsystem/job/proc/give_random_job(mob/dead/new_player/player) + job_debug("GRJ: Giving random job, Player: [player]") . = FALSE for(var/datum/job/job as anything in shuffle(joinable_occupations)) if(QDELETED(player)) - JobDebug("GRJ player is deleted, aborting") + job_debug("GRJ: Player is deleted, aborting") break if((job.current_positions >= job.spawn_positions) && job.spawn_positions != -1) - JobDebug("GRJ job lacks spawn positions to be eligible, Player: [player], Job: [job]") + job_debug("GRJ: Job lacks spawn positions to be eligible, Player: [player], Job: [job]") continue - if(istype(job, GetJobType(overflow_role))) // We don't want to give him assistant, that's boring! - JobDebug("GRJ skipping overflow role, Player: [player], Job: [job]") + if(istype(job, get_job_type(overflow_role))) // We don't want to give him assistant, that's boring! + job_debug("GRJ: Skipping overflow role, Player: [player], Job: [job]") continue if(job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) //If you want a command position, select it! - JobDebug("GRJ skipping command role, Player: [player], Job: [job]") + job_debug("GRJ: Skipping command role, Player: [player], Job: [job]") continue - // This check handles its own output to JobDebug. + // This check handles its own output to job_debug. if(check_job_eligibility(player, job, "GRJ", add_job_to_log = TRUE) != JOB_AVAILABLE) continue - if(AssignRole(player, job, do_eligibility_checks = FALSE)) - JobDebug("GRJ Random job given, Player: [player], Job: [job]") + if(assign_role(player, job, do_eligibility_checks = FALSE)) + job_debug("GRJ: Random job given, Player: [player], Job: [job]") return TRUE - JobDebug("GRJ Player eligible but AssignRole failed, Player: [player], Job: [job]") - + job_debug("GRJ: Player eligible but assign_role failed, Player: [player], Job: [job]") -/datum/controller/subsystem/job/proc/ResetOccupations() - JobDebug("Occupations reset.") +/datum/controller/subsystem/job/proc/reset_occupations() + job_debug("Occupations reset.") for(var/mob/dead/new_player/player as anything in GLOB.new_player_list) if(!player?.mind) continue - player.mind.set_assigned_role(GetJobType(/datum/job/unassigned)) - player.mind.special_role = null - SetupOccupations() + player.mind.set_assigned_role(get_job_type(/datum/job/unassigned)) + setup_occupations() unassigned = list() if(CONFIG_GET(flag/load_jobs_from_txt)) // Any errors with the configs has already been said, we don't need to repeat them here. @@ -318,13 +320,11 @@ SUBSYSTEM_DEF(job) set_overflow_role(overflow_role) return - -/** - * Will try to select a head, ignoring ALL non-head preferences for every level until. - * - * Basically tries to ensure there is at least one head in every shift if anyone has that job preference enabled at all. +/* + * Forces a random Head of Staff role to be assigned to a random eligible player. + * Returns TRUE if a player was selected and assigned the role. FALSE otherwise. */ -/datum/controller/subsystem/job/proc/FillHeadPosition() +/datum/controller/subsystem/job/proc/force_one_head_assignment() var/datum/job_department/command_department = get_department_type(/datum/job_department/command) if(!command_department) return FALSE @@ -332,237 +332,265 @@ SUBSYSTEM_DEF(job) for(var/datum/job/job as anything in command_department.department_jobs) if((job.current_positions >= job.total_positions) && job.total_positions != -1) continue - var/list/candidates = FindOccupationCandidates(job, level) + var/list/candidates = find_occupation_candidates(job, level) if(!candidates.len) continue var/mob/dead/new_player/candidate = pick(candidates) - // Eligibility checks done as part of FindOccupationCandidates. - if(AssignRole(candidate, job, do_eligibility_checks = FALSE)) + // Eligibility checks done as part of find_occupation_candidates. + if(assign_role(candidate, job, do_eligibility_checks = FALSE)) return TRUE return FALSE /** * Attempts to fill out all possible head positions for players with that job at a a given job priority level. + * Returns the number of Head positions assigned. * * Arguments: - * * level - One of the JP_LOW, JP_MEDIUM or JP_HIGH defines. Attempts to find candidates with head jobs at this priority only. + * * level - One of the JP_LOW, JP_MEDIUM, JP_HIGH or JP_ANY defines. Attempts to find candidates with head jobs at that priority only. */ -/datum/controller/subsystem/job/proc/CheckHeadPositions(level) +/datum/controller/subsystem/job/proc/fill_all_head_positions_at_priority(level) + . = 0 var/datum/job_department/command_department = get_department_type(/datum/job_department/command) + if(!command_department) - return + return . + for(var/datum/job/job as anything in command_department.department_jobs) if((job.current_positions >= job.total_positions) && job.total_positions != -1) continue - var/list/candidates = FindOccupationCandidates(job, level) + + var/list/candidates = find_occupation_candidates(job, level) if(!candidates.len) continue + var/mob/dead/new_player/candidate = pick(candidates) - // Eligibility checks done as part of FindOccupationCandidates - AssignRole(candidate, job, do_eligibility_checks = FALSE) + + // Eligibility checks done as part of find_occupation_candidates() above. + if(!assign_role(candidate, job, do_eligibility_checks = FALSE)) + continue + + .++ + + if((job.current_positions >= job.spawn_positions) && job.spawn_positions != -1) + job_debug("JOBS: Command Job is now full, Job: [job], Positions: [job.current_positions], Limit: [job.spawn_positions]") /// Attempts to fill out all available AI positions. /datum/controller/subsystem/job/proc/fill_ai_positions() - var/datum/job/ai_job = GetJob(JOB_AI) + var/datum/job/ai_job = get_job(JOB_AI) if(!ai_job) return // In byond for(in to) loops, the iteration is inclusive so we need to stop at ai_job.total_positions - 1 for(var/i in ai_job.current_positions to ai_job.total_positions - 1) for(var/level in level_order) var/list/candidates = list() - candidates = FindOccupationCandidates(ai_job, level) + candidates = find_occupation_candidates(ai_job, level) if(candidates.len) var/mob/dead/new_player/candidate = pick(candidates) - // Eligibility checks done as part of FindOccupationCandidates - if(AssignRole(candidate, GetJobType(/datum/job/ai), do_eligibility_checks = FALSE)) + // Eligibility checks done as part of find_occupation_candidates + if(assign_role(candidate, get_job_type(/datum/job/ai), do_eligibility_checks = FALSE)) break -/** Proc DivideOccupations +/** Proc divide_occupations * fills var "assigned_role" for all ready players. * This proc must not have any side effect besides of modifying "assigned_role". **/ -/datum/controller/subsystem/job/proc/DivideOccupations(pure = FALSE, allow_all = FALSE) +/datum/controller/subsystem/job/proc/divide_occupations(pure = FALSE, allow_all = FALSE) //Setup new player list and get the jobs list - JobDebug("Running DO, allow_all = [allow_all], pure = [pure]") + job_debug("DO: Running, allow_all = [allow_all], pure = [pure]") run_divide_occupation_pure = pure 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 = unassigned.len + initial_players_to_assign = length(unassigned) - JobDebug("DO, Len: [unassigned.len]") + job_debug("DO: Player count to assign roles to: [initial_players_to_assign]") //Scale number of open security officer slots to population setup_officer_positions() //Jobs will have fewer access permissions if the number of players exceeds the threshold defined in game_options.txt - var/mat = CONFIG_GET(number/minimal_access_threshold) - if(mat) - if(mat > unassigned.len) + var/min_access_threshold = CONFIG_GET(number/minimal_access_threshold) + if(min_access_threshold) + if(min_access_threshold > initial_players_to_assign) CONFIG_SET(flag/jobs_have_minimal_access, FALSE) else CONFIG_SET(flag/jobs_have_minimal_access, TRUE) - //Shuffle players and jobs - unassigned = shuffle(unassigned) + //Shuffle player list. + shuffle_inplace(unassigned) - HandleFeedbackGathering() + handle_feedback_gathering() - // Dynamic has picked a ruleset that requires enforcing some jobs before others. - JobDebug("DO, Assigning Priority Positions: [length(dynamic_forced_occupations)]") + // Assign any priority positions before all other standard job selections. + job_debug("DO: Assigning priority positions") assign_priority_positions() + job_debug("DO: Priority assignment complete") + + // The overflow role has limitless slots, plus having the Overflow box ticked in prefs should (with one exception) set the priority to JP_HIGH. + // So everyone with overflow enabled will get that job. Thus we can assign it immediately to all players that have it enabled. + job_debug("DO: Assigning early overflow roles") + assign_all_overflow_positions() + job_debug("DO: Early overflow roles assigned.") + + // At this point we can assume the following: + // From assign_priority_positions() + // 1. If possible, any necessary job roles to allow Dynamic rulesets to execute (such as an AI for malf AI) are satisfied. + // 2. All Head of Staff roles with any player pref set to JP_HIGH are filled out. + // 3. If any player not selected by the above has any Head of Staff preference enabled at any JP_ level, there is at least one Head of Staff. + // + // From assign_all_overflow_positions() + // 4. Anyone with the overflow role enabled has been given the overflow role. + + // Copy the joinable occupation list and filter out ineligible occupations due to above job assignments. + var/list/available_occupations = joinable_occupations.Copy() + var/datum/job_department/command_department = get_department_type(/datum/job_department/command) - //People who wants to be the overflow role, sure, go on. - JobDebug("DO, Running Overflow Check 1") - var/datum/job/overflow_datum = GetJobType(overflow_role) - var/list/overflow_candidates = FindOccupationCandidates(overflow_datum, JP_LOW) - JobDebug("AC1, Candidates: [overflow_candidates.len]") - for(var/mob/dead/new_player/player in overflow_candidates) - JobDebug("AC1 pass, Player: [player]") - // Eligibility checks done as part of FindOccupationCandidates - AssignRole(player, GetJobType(overflow_role), do_eligibility_checks = FALSE) - overflow_candidates -= player - JobDebug("DO, AC1 end") - - //Select one head - JobDebug("DO, Running Head Check") - FillHeadPosition() - JobDebug("DO, Head Check end") - - // Fill out any remaining AI positions. - JobDebug("DO, Running AI Check") - fill_ai_positions() - JobDebug("DO, AI Check end") + for(var/datum/job/job in available_occupations) + // Make sure the job isn't filled. If it is, remove it from the list so it doesn't get checked. + if((job.current_positions >= job.spawn_positions) && job.spawn_positions != -1) + job_debug("DO: Job is now filled, Job: [job], Current: [job.current_positions], Limit: [job.spawn_positions]") + available_occupations -= job + continue - //Other jobs are now checked - JobDebug("DO, Running standard job assignment") - // New job giving system by Donkie - // This will cause lots of more loops, but since it's only done once it shouldn't really matter much at all. - // Hopefully this will add more randomness and fairness to job giving. + // Command jobs are handled via fill_all_head_positions_at_priority(...) + // Remove these jobs from the list of available occupations to prevent multiple players being assigned to the same + // limited role without constantly having to iterate over the available_occupations list and re-check them. + if(job in command_department?.department_jobs) + available_occupations -= job + + job_debug("DO: Running standard job assignment") - // Loop through all levels from high to low - var/list/shuffledoccupations = shuffle(joinable_occupations) for(var/level in level_order) - //Check the head jobs first each level - CheckHeadPositions(level) + job_debug("JOBS: Filling in head roles, Level: [job_priority_level_to_string(level)]") + // Fill the head jobs first each level + fill_all_head_positions_at_priority(level) // Loop through all unassigned players for(var/mob/dead/new_player/player in unassigned) if(!allow_all) - if(PopcapReached()) - RejectPlayer(player) - - // Loop through all jobs - for(var/datum/job/job in shuffledoccupations) // SHUFFLE ME BABY - if(!job) - JobDebug("FOC invalid/null job in occupations, Player: [player], Job: [job]") - shuffledoccupations -= job - continue + if(popcap_reached()) + job_debug("JOBS: Popcap reached, trying to reject player: [player]") + try_reject_player(player) - // Make sure the job isn't filled. If it is, remove it from the list so it doesn't get checked again. - if((job.current_positions >= job.spawn_positions) && job.spawn_positions != -1) - JobDebug("FOC job filled and not overflow, Player: [player], Job: [job], Current: [job.current_positions], Limit: [job.spawn_positions]") - shuffledoccupations -= job - continue + job_debug("JOBS: Finding a job for player: [player], at job priority pref: [job_priority_level_to_string(level)]") + // Loop through all jobs and build a list of jobs this player could be eligible for. + var/list/possible_jobs = list() + for(var/datum/job/job in available_occupations) // Filter any job that doesn't fit the current level. var/player_job_level = player.client?.prefs.job_preferences[job.title] if(isnull(player_job_level)) - JobDebug("FOC player job not enabled, Player: [player]") + job_debug("JOBS: Job not enabled, Job: [job]") continue - else if(player_job_level != level) - JobDebug("FOC player job enabled but at different level, Player: [player], TheirLevel: [job_priority_level_to_string(player_job_level)], ReqLevel: [job_priority_level_to_string(level)]") + if(player_job_level != level) + job_debug("JOBS: Job enabled at different priority pref, Job: [job], TheirLevel: [job_priority_level_to_string(player_job_level)], ReqLevel: [job_priority_level_to_string(level)]") continue - if(check_job_eligibility(player, job, "DO", add_job_to_log = TRUE) != JOB_AVAILABLE) + if(check_job_eligibility(player, job, "JOBS", add_job_to_log = TRUE) != JOB_AVAILABLE) continue - JobDebug("DO pass, Player: [player], Level:[level], Job:[job.title]") - AssignRole(player, job, do_eligibility_checks = FALSE) - unassigned -= player - break + possible_jobs += job + + // If there are no possible jobs for them at this priority, skip them. + if(!length(possible_jobs)) + job_debug("JOBS: Player not eligible for any available jobs at this priority level: [player]") + continue + + // Otherwise, pick one of those jobs at random. + var/datum/job/picked_job = pick(possible_jobs) - JobDebug("DO, Ending standard job assignment") + job_debug("JOBS: Now assigning role to player: [player], Job:[picked_job.title]") + assign_role(player, picked_job, do_eligibility_checks = FALSE) + if((picked_job.current_positions >= picked_job.spawn_positions) && picked_job.spawn_positions != -1) + job_debug("JOBS: Job is now full, Job: [picked_job], Positions: [picked_job.current_positions], Limit: [picked_job.spawn_positions]") + available_occupations -= picked_job - JobDebug("DO, Handle unassigned.") - // Hand out random jobs to the people who didn't get any in the last check - // Also makes sure that they got their preference correct + job_debug("DO: Ending standard job assignment") + + job_debug("DO: Handle unassigned") + // For any players that didn't get a job, fall back on their pref setting for what to do. for(var/mob/dead/new_player/player in unassigned) - HandleUnassigned(player, allow_all) - JobDebug("DO, Ending handle unassigned.") + handle_unassigned(player, allow_all) + job_debug("DO: Ending handle unassigned") - JobDebug("DO, Handle unrejectable unassigned") + job_debug("DO: Handle unrejectable unassigned") //Mop up people who can't leave. for(var/mob/dead/new_player/player in unassigned) //Players that wanted to back out but couldn't because they're antags (can you feel the edge case?) - if(!GiveRandomJob(player)) - if(!AssignRole(player, GetJobType(overflow_role))) //If everything is already filled, make them an assistant - JobDebug("DO, Forced antagonist could not be assigned any random job or the overflow role. DivideOccupations failed.") - JobDebug("---------------------------------------------------") + if(!give_random_job(player)) + if(!assign_role(player, get_job_type(overflow_role))) //If everything is already filled, make them an assistant + job_debug("DO: Forced antagonist could not be assigned any random job or the overflow role. divide_occupations failed.") + job_debug("---------------------------------------------------") run_divide_occupation_pure = FALSE return FALSE //Living on the edge, the forced antagonist couldn't be assigned to overflow role (bans, client age) - just reroll - JobDebug("DO, Ending handle unrejectable unassigned") + job_debug("DO: Ending handle unrejectable unassigned") - JobDebug("All divide occupations tasks completed.") - JobDebug("---------------------------------------------------") + job_debug("All divide occupations tasks completed.") + job_debug("---------------------------------------------------") run_divide_occupation_pure = FALSE return TRUE //We couldn't find a job from prefs for this guy. -/datum/controller/subsystem/job/proc/HandleUnassigned(mob/dead/new_player/player, allow_all = FALSE) +/datum/controller/subsystem/job/proc/handle_unassigned(mob/dead/new_player/player, allow_all = FALSE) var/jobless_role = player.client.prefs.read_preference(/datum/preference/choiced/jobless_role) if(!allow_all) - if(PopcapReached()) - RejectPlayer(player) + if(popcap_reached()) + job_debug("HU: Popcap reached, trying to reject player: [player]") + try_reject_player(player) return switch (jobless_role) if (BEOVERFLOW) - var/datum/job/overflow_role_datum = GetJobType(overflow_role) + var/datum/job/overflow_role_datum = get_job_type(overflow_role) + + if((overflow_role_datum.current_positions >= overflow_role_datum.spawn_positions) && overflow_role_datum.spawn_positions != -1) + job_debug("HU: Overflow role player cap reached, trying to reject: [player]") + try_reject_player(player) + return if(check_job_eligibility(player, overflow_role_datum, debug_prefix = "HU", add_job_to_log = TRUE) != JOB_AVAILABLE) - RejectPlayer(player) + job_debug("HU: Player cannot be overflow, trying to reject: [player]") + try_reject_player(player) return - if(!AssignRole(player, overflow_role_datum, do_eligibility_checks = FALSE)) - RejectPlayer(player) + if(!assign_role(player, overflow_role_datum, do_eligibility_checks = FALSE)) + job_debug("HU: Player could not be assigned overflow role, trying to reject: [player]") + try_reject_player(player) return if (BERANDOMJOB) - if(!GiveRandomJob(player)) - RejectPlayer(player) + if(!give_random_job(player)) + job_debug("HU: Player cannot be given a random job, trying to reject: [player]") + try_reject_player(player) return if (RETURNTOLOBBY) - RejectPlayer(player) + job_debug("HU: Player unable to be assigned job, return to lobby enabled: [player]") + try_reject_player(player) return else //Something gone wrong if we got here. - var/message = "HU: [player] fell through handling unassigned" - JobDebug(message) - log_game(message) - message_admins(message) - RejectPlayer(player) + job_debug("HU: [player] has an invalid jobless_role var: [jobless_role]") + log_game("[player] has an invalid jobless_role var: [jobless_role]") + message_admins("[player] has an invalid jobless_role, this shouldn't happen.") + try_reject_player(player) //Gives the player the stuff he should have with his rank -/datum/controller/subsystem/job/proc/EquipRank(mob/living/equipping, datum/job/job, client/player_client) +/datum/controller/subsystem/job/proc/equip_rank(mob/living/equipping, datum/job/job, client/player_client) equipping.job = job.title SEND_SIGNAL(equipping, COMSIG_JOB_RECEIVED, job) equipping.mind?.set_assigned_role_with_greeting(job, player_client) - equipping.on_job_equipping(job, player_client) - job.announce_job(equipping) if(player_client?.holder) - if(CONFIG_GET(flag/auto_deadmin_players) || (player_client.prefs?.toggles & DEADMIN_ALWAYS)) + if(/*CONFIG_GET(flag/auto_deadmin_always) || */(player_client.prefs?.toggles & DEADMIN_ALWAYS)) player_client.holder.auto_deadmin() else handle_auto_deadmin_roles(player_client, job.title) @@ -572,7 +600,7 @@ SUBSYSTEM_DEF(job) /datum/controller/subsystem/job/proc/handle_auto_deadmin_roles(client/C, rank) if(!C?.holder) return TRUE - var/datum/job/job = GetJob(rank) + var/datum/job/job = get_job(rank) var/timegate_expired = FALSE // allow only forcing deadminning in the first X seconds of the round if auto_deadmin_timegate is set in config @@ -590,7 +618,7 @@ SUBSYSTEM_DEF(job) return C.holder.auto_deadmin() /datum/controller/subsystem/job/proc/setup_officer_positions() - var/datum/job/J = SSjob.GetJob(JOB_SECURITY_OFFICER) + var/datum/job/J = SSjob.get_job(JOB_SECURITY_OFFICER) if(!J) CRASH("setup_officer_positions(): Security officer job is missing") @@ -598,7 +626,7 @@ SUBSYSTEM_DEF(job) if(ssc > 0) if(J.spawn_positions > 0) var/officer_positions = min(12, max(J.spawn_positions, round(unassigned.len / ssc))) //Scale between configured minimum and 12 officers - JobDebug("Setting open security officer positions to [officer_positions]") + job_debug("SOP: Setting open security officer positions to [officer_positions]") J.total_positions = officer_positions J.spawn_positions = officer_positions @@ -614,7 +642,7 @@ SUBSYSTEM_DEF(job) else //We ran out of spare locker spawns! break -/datum/controller/subsystem/job/proc/HandleFeedbackGathering() +/datum/controller/subsystem/job/proc/handle_feedback_gathering() for(var/datum/job/job as anything in joinable_occupations) var/high = 0 //high var/medium = 0 //medium @@ -622,6 +650,7 @@ SUBSYSTEM_DEF(job) var/never = 0 //never var/banned = 0 //banned var/young = 0 //account too young + var/newbie = 0 //exp too low for(var/i in GLOB.new_player_list) var/mob/dead/new_player/player = i if(!(player.ready == PLAYER_READY_TO_PLAY && player.mind && is_unassigned_job(player.mind.assigned_role))) @@ -633,7 +662,7 @@ SUBSYSTEM_DEF(job) young++ continue if(job.required_playtime_remaining(player.client)) - young++ + newbie++ continue switch(player.client.prefs.job_preferences[job.title]) if(JP_HIGH) @@ -650,8 +679,9 @@ SUBSYSTEM_DEF(job) SSblackbox.record_feedback("nested tally", "job_preferences", never, list("[job.title]", "never")) SSblackbox.record_feedback("nested tally", "job_preferences", banned, list("[job.title]", "banned")) SSblackbox.record_feedback("nested tally", "job_preferences", young, list("[job.title]", "young")) + SSblackbox.record_feedback("nested tally", "job_preferences", newbie, list("[job.title]", "newbie")) -/datum/controller/subsystem/job/proc/PopcapReached() +/datum/controller/subsystem/job/proc/popcap_reached() var/hpc = CONFIG_GET(number/hard_popcap) var/epc = CONFIG_GET(number/extreme_popcap) if(hpc || epc) @@ -660,16 +690,17 @@ SUBSYSTEM_DEF(job) return 1 return 0 -/datum/controller/subsystem/job/proc/RejectPlayer(mob/dead/new_player/player) - if(player.mind && player.mind.special_role) - return - if(PopcapReached()) - JobDebug("Popcap overflow Check observer located, Player: [player]") - JobDebug("Player rejected :[player]") +/datum/controller/subsystem/job/proc/try_reject_player(mob/dead/new_player/player) + 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 if(!run_divide_occupation_pure) - to_chat(player, "You have failed to qualify for any job you desired.") - player.unready() + to_chat(player, span_infoplain("You have failed to qualify for any job you desired.")) + player.ready = PLAYER_NOT_READY /datum/controller/subsystem/job/Recover() @@ -677,10 +708,10 @@ SUBSYSTEM_DEF(job) var/oldjobs = SSjob.all_occupations sleep(2 SECONDS) for (var/datum/job/job as anything in oldjobs) - INVOKE_ASYNC(src, PROC_REF(RecoverJob), job) + INVOKE_ASYNC(src, PROC_REF(recover_job), job) -/datum/controller/subsystem/job/proc/RecoverJob(datum/job/J) - var/datum/job/newjob = GetJob(J.title) +/datum/controller/subsystem/job/proc/recover_job(datum/job/J) + var/datum/job/newjob = get_job(J.title) if (!istype(newjob)) return newjob.total_positions = J.total_positions @@ -689,15 +720,21 @@ SUBSYSTEM_DEF(job) /atom/proc/JoinPlayerHere(mob/joining_mob, buckle) // By default, just place the mob on the same turf as the marker or whatever. - joining_mob.forceMove(get_turf(src)) + // Set joining_mob as the new mob so subtypes can use it as a proper mob. + if(ispath(joining_mob)) + joining_mob = new joining_mob(get_turf(src)) + else + joining_mob.forceMove(get_turf(src)) + return joining_mob /obj/structure/chair/JoinPlayerHere(mob/joining_mob, buckle) - . = ..() + var/mob/created_joining_mob = ..() // Placing a mob in a chair will attempt to buckle it, or else fall back to default. - if(buckle && isliving(joining_mob)) - buckle_mob(joining_mob, FALSE, FALSE) + if(buckle && isliving(created_joining_mob)) + buckle_mob(created_joining_mob, FALSE, FALSE) + return created_joining_mob -/datum/controller/subsystem/job/proc/SendToLateJoin(mob/M, buckle = TRUE) +/datum/controller/subsystem/job/proc/send_to_late_join(mob/M, buckle = TRUE) var/atom/destination if(M.mind && !is_unassigned_job(M.mind.assigned_role) && length(GLOB.jobspawn_overrides[M.mind.assigned_role.title])) //We're doing something special today. destination = pick(GLOB.jobspawn_overrides[M.mind.assigned_role.title]) @@ -732,17 +769,6 @@ SUBSYSTEM_DEF(job) stack_trace("Unable to find last resort spawn point.") return GET_ERROR_ROOM -///Lands specified mob at a random spot in the hallways -/datum/controller/subsystem/job/proc/DropLandAtRandomHallwayPoint(mob/living/living_mob) - var/turf/spawn_turf = get_safe_random_station_turf(typesof(/area/station/hallway)) - - if(!spawn_turf) - SendToLateJoin(living_mob) - else - var/obj/structure/closet/supplypod/centcompod/toLaunch = new() - living_mob.forceMove(toLaunch) - new /obj/effect/pod_landingzone(spawn_turf, toLaunch) - /// Returns a list of minds of all heads of staff who are alive /datum/controller/subsystem/job/proc/get_living_heads() . = list() @@ -777,7 +803,7 @@ SUBSYSTEM_DEF(job) if(sec.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY) . += sec -/datum/controller/subsystem/job/proc/JobDebug(message) +/datum/controller/subsystem/job/proc/job_debug(message) log_job_debug(message) /// Builds various lists of jobs based on station, centcom and additional jobs with icons associated with them. @@ -842,12 +868,54 @@ SUBSYSTEM_DEF(job) safe_code_timer_id = null safe_code_request_loc = null -/// Blindly assigns the required roles to every player in the dynamic_forced_occupations list. +/// 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() - for(var/mob/new_player in dynamic_forced_occupations) - // Eligibility checks already carried out as part of the dynamic ruleset trim_candidates proc.area - // However no guarantee of game state between then and now, so don't skip eligibility checks on AssignRole. - AssignRole(new_player, GetJob(dynamic_forced_occupations[new_player])) + 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_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.") + var/head_count = fill_all_head_positions_at_priority(JP_HIGH) + + // If nobody has JP_HIGH on a Head role, try to force at least one Head of Staff so every shift has the best chance + // of having at least one leadership role. + if(head_count == 0) + force_one_head_assignment() + + // Fill out all AI positions. + job_debug("APP: Filling all AI positions") + fill_ai_positions() + +/datum/controller/subsystem/job/proc/assign_all_overflow_positions() + job_debug("OVRFLW: Assigning all overflow roles.") + job_debug("OVRFLW: This shift's overflow role: [overflow_role]") + var/datum/job/overflow_datum = get_job_type(overflow_role) + + // When the Overflow role changes for any reason, this allows players to set otherwise invalid job priority pref states. + // So if Assistant is the "usual" Overflow but it gets changed to Clown for a shift, players can set the Assistant role's priorities + // to JP_MEDIUM and JP_LOW. When the "usual" Overflow role comes back, it returns to an On option in the prefs menu but still + // keeps its old JP_MEDIUM or JP_LOW value in the background. + + // Due to this prefs quirk, we actually don't want to find JP_HIGH candidates as it may exclude people with abnormal pref states that + // appear normal from the UI. By passing in JP_ANY, it will return all players that have the overflow job pref (which should be a toggle) + // set to any level. + var/list/overflow_candidates = find_occupation_candidates(overflow_datum, JP_ANY) + job_debug("OVRFLW: Attempting to assign the overflow role to [length(overflow_candidates)] players.") + for(var/mob/dead/new_player/player in overflow_candidates) + if((overflow_datum.current_positions >= overflow_datum.spawn_positions) && overflow_datum.spawn_positions != -1) + job_debug("OVRFLW: Overflow role cap reached, role only assigned to [overflow_datum.current_positions] players.") + job_debug("OVRFLW: Overflow Job is now full, Job: [overflow_datum], Positions: [overflow_datum.current_positions], Limit: [overflow_datum.spawn_positions]") + return + + // Eligibility checks done as part of find_occupation_candidates, so skip them. + assign_role(player, get_job_type(overflow_role), do_eligibility_checks = FALSE) + job_debug("OVRFLW: Assigned overflow to player: [player]") + job_debug("OVRFLW: All overflow roles assigned.") /// Takes a job priority #define such as JP_LOW and gets its string representation for logging. /datum/controller/subsystem/job/proc/job_priority_level_to_string(priority) @@ -865,40 +933,41 @@ SUBSYSTEM_DEF(job) * Arguments: * * player - The player to check for job eligibility. * * possible_job - The job to check for eligibility against. - * * debug_prefix - Logging prefix for the JobDebug log entries. For example, GRJ during GiveRandomJob or DO during DivideOccupations. + * * debug_prefix - Logging prefix for the job_debug log entries. For example, GRJ during give_random_job or DO during divide_occupations. * * add_job_to_log - If TRUE, appends the job type to the log entry. If FALSE, does not. Set to FALSE when check is part of iterating over players for a specific job, set to TRUE when check is part of iterating over jobs for a specific player and you don't want extra log entry spam. */ /datum/controller/subsystem/job/proc/check_job_eligibility(mob/dead/new_player/player, datum/job/possible_job, debug_prefix = "", add_job_to_log = FALSE) if(!player.mind) - JobDebug("[debug_prefix] player has no mind, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") + job_debug("[debug_prefix]: Player has no mind, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") return JOB_UNAVAILABLE_GENERIC - if(possible_job.title in player.mind.restricted_roles) - JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_ANTAG_INCOMPAT, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") + 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 if(!possible_job.player_old_enough(player.client)) - JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_ACCOUNTAGE, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") + job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_ACCOUNTAGE, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") return JOB_UNAVAILABLE_ACCOUNTAGE var/required_playtime_remaining = possible_job.required_playtime_remaining(player.client) if(required_playtime_remaining) - JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_PLAYTIME, possible_job.title)], Player: [player], MissingTime: [required_playtime_remaining][add_job_to_log ? ", Job: [possible_job]" : ""]") + job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_PLAYTIME, possible_job.title)], Player: [player], MissingTime: [required_playtime_remaining][add_job_to_log ? ", Job: [possible_job]" : ""]") return JOB_UNAVAILABLE_PLAYTIME // Run the banned check last since it should be the rarest check to fail and can access the database. if(is_banned_from(player.ckey, possible_job.title)) - JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_BANNED, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") + job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_BANNED, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_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) - JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_AGE)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") + 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 // Need to recheck the player exists after is_banned_from since it can query the DB which may sleep. if(QDELETED(player)) - JobDebug("[debug_prefix] player is qdeleted, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") + job_debug("[debug_prefix]: Player is qdeleted, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]") return JOB_UNAVAILABLE_GENERIC return JOB_AVAILABLE diff --git a/code/controllers/subsystem/polling.dm b/code/controllers/subsystem/polling.dm index 279c6f7bad70..27433c242f8a 100644 --- a/code/controllers/subsystem/polling.dm +++ b/code/controllers/subsystem/polling.dm @@ -2,7 +2,7 @@ SUBSYSTEM_DEF(polling) name = "Polling" flags = SS_BACKGROUND | SS_NO_INIT wait = 1 SECONDS - runlevels = RUNLEVEL_GAME + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME /// List of polls currently ongoing, to be checked on next fire() var/list/datum/candidate_poll/currently_polling /// Number of polls performed since the start @@ -27,12 +27,16 @@ SUBSYSTEM_DEF(polling) * * ignore_category: Optional, A poll category. If a candidate has this category in their ignore list, they won't be polled. * * flash_window: If TRUE, the candidate's window will flash when they're polled. * * list/group: A list of candidates to poll. - * * pic_source: Optional, An /atom or an /image to display on the poll alert. + * * alert_pic: Optional, An /atom or an /image to display on the poll alert. + * * jump_target: An /atom to teleport/jump to, if alert_pic is an /atom defaults to that. * * role_name_text: Optional, A string to display in logging / the (default) question. If null, the role name will be used. * * list/custom_response_messages: Optional, A list of strings to use as responses to the poll. If null, the default responses will be used. see __DEFINES/polls.dm for valid keys to use. * * start_signed_up: If TRUE, all candidates will start signed up for the poll, making it opt-out rather than opt-in. + * * amount_to_pick: Lets you pick candidates and return a single mob or list of mobs that were chosen. + * * chat_text_border_icon: Object or path to make an icon of to decorate the chat announcement. + * * announce_chosen: Whether we should announce the chosen candidates in chat. This is ignored unless amount_to_pick is greater than 0. * - * Returns a list of all mobs who signed up for the poll. + * Returns a list of all mobs who signed up for the poll, OR, in the case that amount_to_pick is equal to 1 the singular mob/null if no available candidates. */ /datum/controller/subsystem/polling/proc/poll_candidates( question, @@ -42,28 +46,36 @@ SUBSYSTEM_DEF(polling) ignore_category = null, flash_window = TRUE, list/group = null, - pic_source, + alert_pic, + jump_target, role_name_text, list/custom_response_messages, start_signed_up = FALSE, + amount_to_pick = 0, + chat_text_border_icon, + announce_chosen = TRUE, ) - RETURN_TYPE(/list/mob) if(group.len == 0) - return list() + return if(role && !role_name_text) role_name_text = role if(role_name_text && !question) - question = "Do you want to play as [full_capitalize(role_name_text)]?" + question = "Do you want to play as [span_notice(role_name_text)]?" if(!question) question = "Do you want to play as a special role?" - log_game("Polling candidates [role_name_text ? "for [role_name_text]" : "\"[question]\""] for [DisplayTimeText(poll_time)] seconds") + log_ghost_poll("Candidate poll started.", data = list( + "role name" = role_name_text, + "poll question" = question, + "poll duration" = DisplayTimeText(poll_time), + )) // Start firing total_polls++ - var/jumpable = isatom(pic_source) ? pic_source : null + if(isnull(jump_target) && isatom(alert_pic)) + jump_target = alert_pic - var/datum/candidate_poll/new_poll = new(role_name_text, question, poll_time, ignore_category, jumpable, custom_response_messages) + var/datum/candidate_poll/new_poll = new(role_name_text, question, poll_time, ignore_category, jump_target, custom_response_messages) LAZYADD(currently_polling, new_poll) var/category = "[new_poll.poll_key]_poll_alert" @@ -72,7 +84,7 @@ SUBSYSTEM_DEF(polling) if(!candidate_mob.client) continue // Universal opt-out for all players. - if((!candidate_mob.client.prefs.read_preference(/datum/preference/toggle/ghost_roles))) + if(!candidate_mob.client.prefs.read_preference(/datum/preference/toggle/ghost_roles)) continue // Opt-out for admins whom are currently adminned. if((!candidate_mob.client.prefs.read_preference(/datum/preference/toggle/ghost_roles_as_admin)) && candidate_mob.client.holder) @@ -122,80 +134,146 @@ SUBSYSTEM_DEF(polling) // Image to display var/image/poll_image - if(pic_source) - if(!ispath(pic_source)) - var/atom/the_pic_source = pic_source - var/old_layer = the_pic_source.layer - var/old_plane = the_pic_source.plane - the_pic_source.plane = poll_alert_button.plane - the_pic_source.layer = FLOAT_LAYER - poll_alert_button.add_overlay(the_pic_source) - the_pic_source.layer = old_layer - the_pic_source.plane = old_plane - else - poll_image = image(pic_source, layer = FLOAT_LAYER) + if(ispath(alert_pic, /atom) || isatom(alert_pic)) + poll_image = new /mutable_appearance(alert_pic) + poll_image.pixel_z = 0 + else if(!isnull(alert_pic)) + poll_image = alert_pic else - // Just use a generic image - poll_image = image('icons/effects/effects.dmi', icon_state = "static", layer = FLOAT_LAYER) + poll_image = image('icons/effects/effects.dmi', icon_state = "static") if(poll_image) + poll_image.layer = FLOAT_LAYER poll_image.plane = poll_alert_button.plane poll_alert_button.add_overlay(poll_image) // Chat message var/act_jump = "" - if(isatom(pic_source) && isobserver(candidate_mob)) - act_jump = "\[Teleport\]" - var/act_signup = "\[[start_signed_up ? "Opt out" : "Sign Up"]\]" + var/custom_link_style_start = "" + var/custom_link_style_end = "style='color:DodgerBlue;font-weight:bold;-dm-text-outline: 1px black'" + if(isatom(alert_pic) && isobserver(candidate_mob)) + act_jump = "[custom_link_style_start]\[Teleport\]" + var/act_signup = "[custom_link_style_start]\[[start_signed_up ? "Opt out" : "Sign Up"]\]" var/act_never = "" if(ignore_category) - act_never = "\[Never For This Round\]" + act_never = "[custom_link_style_start]\[Never For This Round\]" + + if(!duplicate_message_check(alert_poll)) //Only notify people once. They'll notice if there are multiple and we don't want to spam people. + + // ghost poll prompt sound handling + // var/polling_sound_volume = candidate_mob.client?.prefs.read_preference(/datum/preference/numeric/sound_ghost_poll_prompt_volume) + SEND_SOUND(candidate_mob, sound('sound/misc/prompt1.ogg', volume = 50)) + + var/surrounding_icon + if(chat_text_border_icon) + var/image/surrounding_image + if(!ispath(chat_text_border_icon)) + var/mutable_appearance/border_image = chat_text_border_icon + surrounding_image = border_image + else + surrounding_image = image(chat_text_border_icon) + surrounding_icon = icon2html(surrounding_image, candidate_mob, extra_classes = "bigicon") + var/final_message = examine_block("[surrounding_icon] [span_ooc(question)] [surrounding_icon]\n[act_jump] [act_signup] [act_never]") + to_chat(candidate_mob, final_message) - SEND_SOUND(candidate_mob, 'sound/misc/notice2.ogg') - to_chat(candidate_mob, span_boldnotice(examine_block("Now looking for candidates [role_name_text ? "to play as \an [role_name_text]." : "\"[question]\""] [act_jump] [act_signup] [act_never]"))) // Start processing it so it updates visually the timer START_PROCESSING(SSprocessing, poll_alert_button) // Sleep until the time is up UNTIL(new_poll.finished) - return new_poll.signed_up - -/datum/controller/subsystem/polling/proc/poll_ghost_candidates(question, role, check_jobban, poll_time = 30 SECONDS, ignore_category = null, flashwindow = TRUE, pic_source, role_name_text) + if(!amount_to_pick) + return new_poll.signed_up + if (!length(new_poll.signed_up)) + return null + for(var/pick in 1 to amount_to_pick) + // There may be less people signed up than amount_to_pick + // pick_n_take returns the default return value of null if passed an empty list, so just break in that case rather than adding null to the list. + if(!length(new_poll.signed_up)) + break + new_poll.chosen_candidates += pick_n_take(new_poll.signed_up) + if(announce_chosen) + new_poll.announce_chosen(group) + if(new_poll.chosen_candidates.len == 1) + var/chosen_one = pick(new_poll.chosen_candidates) + return chosen_one + return new_poll.chosen_candidates + +/datum/controller/subsystem/polling/proc/poll_ghost_candidates( + question, + role, + check_jobban, + poll_time = 30 SECONDS, + ignore_category = null, + flashwindow = TRUE, + alert_pic, + jump_target, + role_name_text, + list/custom_response_messages, + start_signed_up = FALSE, + amount_to_pick = 0, + chat_text_border_icon, + announce_chosen = TRUE, +) var/list/candidates = list() if(!(GLOB.ghost_role_flags & GHOSTROLE_STATION_SENTIENCE)) - return candidates - + return for(var/mob/dead/observer/ghost_player in GLOB.player_list) candidates += ghost_player - return poll_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, candidates, pic_source, role_name_text) - -/datum/controller/subsystem/polling/proc/poll_ghost_candidates_for_mob(question, role, check_jobban, poll_time = 30 SECONDS, mob/target_mob, ignore_category = null, flashwindow = TRUE, pic_source, role_name_text) - var/static/list/mob/currently_polling_mobs = list() - - if(currently_polling_mobs.Find(target_mob)) - return list() - - currently_polling_mobs += target_mob +#ifdef TESTING + for(var/mob/dude in GLOB.player_list) + candidates |= dude +#endif - var/list/possible_candidates = poll_ghost_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, pic_source, role_name_text) + return poll_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, candidates, alert_pic, jump_target, role_name_text, custom_response_messages, start_signed_up, amount_to_pick, chat_text_border_icon, announce_chosen) - currently_polling_mobs -= target_mob - if(!target_mob || QDELETED(target_mob) || !target_mob.loc) - return list() - - return possible_candidates - -/datum/controller/subsystem/polling/proc/poll_ghost_candidates_for_mobs(question, role, check_jobban, poll_time = 30 SECONDS, list/mobs, ignore_category = null, flashwindow = TRUE, pic_source, role_name_text) - var/list/candidate_list = poll_ghost_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, pic_source, role_name_text) - - for(var/mob/potential_mob as anything in mobs) - if(QDELETED(potential_mob) || !potential_mob.loc) - mobs -= potential_mob - - if(!length(mobs)) +/datum/controller/subsystem/polling/proc/poll_ghosts_for_target( + question, + role, + check_jobban, + poll_time = 30 SECONDS, + atom/movable/checked_target, + ignore_category = null, + flashwindow = TRUE, + alert_pic, + jump_target, + role_name_text, + list/custom_response_messages, + start_signed_up = FALSE, + chat_text_border_icon, + announce_chosen = TRUE, +) + var/static/list/atom/movable/currently_polling_targets = list() + if(currently_polling_targets.Find(checked_target)) + return + currently_polling_targets += checked_target + var/mob/chosen_one = poll_ghost_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, alert_pic, jump_target, role_name_text, custom_response_messages, start_signed_up, amount_to_pick = 1, chat_text_border_icon = chat_text_border_icon, announce_chosen = announce_chosen) + currently_polling_targets -= checked_target + if(!checked_target || QDELETED(checked_target) || !checked_target.loc) + return null + return chosen_one + +/datum/controller/subsystem/polling/proc/poll_ghosts_for_targets( + question, + role, + check_jobban, + poll_time = 30 SECONDS, + list/checked_targets, + ignore_category = null, + flashwindow = TRUE, + alert_pic, + jump_target, + role_name_text, + list/custom_response_messages, + start_signed_up = FALSE, + chat_text_border_icon, +) + var/list/candidate_list = poll_ghost_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, alert_pic, jump_target, role_name_text, custom_response_messages, start_signed_up, chat_text_border_icon = chat_text_border_icon) + for(var/atom/movable/potential_target as anything in checked_targets) + if(QDELETED(potential_target) || !potential_target.loc) + checked_targets -= potential_target + if(!length(checked_targets)) return list() - return candidate_list /datum/controller/subsystem/polling/proc/is_eligible(mob/potential_candidate, role, check_jobban, the_ignore_category) @@ -204,15 +282,14 @@ 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) - if(is_banned_from(potential_candidate.ckey, list(check_jobban, ROLE_SYNDICATE))) + if(is_banned_from(potential_candidate.ckey, list(ROLE_SYNDICATE) + check_jobban)) return FALSE return TRUE @@ -222,18 +299,20 @@ SUBSYSTEM_DEF(polling) // Trim players who aren't eligible anymore var/length_pre_trim = length(finishing_poll.signed_up) finishing_poll.trim_candidates() - log_game("Candidate poll [finishing_poll.role ? "for [finishing_poll.role]" : "\"[finishing_poll.question]\""] finished. [length_pre_trim] players signed up, [length(finishing_poll.signed_up)] after trimming") + + log_ghost_poll("Candidate poll completed.", data = list( + "role name" = finishing_poll.role, + "poll question" = finishing_poll.question, + "signed up count" = length_pre_trim, + "trimmed candidate count" = length(finishing_poll.signed_up) + )) + finishing_poll.finished = TRUE // Take care of updating the remaining screen alerts if a similar poll is found, or deleting them. if(length(finishing_poll.alert_buttons)) - var/polls_of_same_type_left = FALSE - for(var/datum/candidate_poll/running_poll as anything in currently_polling) - if(running_poll.poll_key == finishing_poll.poll_key && running_poll.time_left() > 0) - polls_of_same_type_left = TRUE - break for(var/atom/movable/screen/alert/poll_alert/alert as anything in finishing_poll.alert_buttons) - if(polls_of_same_type_left) + if(duplicate_message_check(finishing_poll)) alert.update_stacks_overlay() else alert.owner.clear_alert("[finishing_poll.poll_key]_poll_alert") @@ -242,12 +321,19 @@ SUBSYSTEM_DEF(polling) QDEL_IN(finishing_poll, 0.5 SECONDS) /datum/controller/subsystem/polling/stat_entry(msg) - msg += "Active: [length(currently_polling)] | Total: [total_polls]" + msg = "\n Active: [length(currently_polling)] | Total: [total_polls]" var/datum/candidate_poll/soonest_to_complete = get_next_poll_to_finish() if(soonest_to_complete) msg += " | Next: [DisplayTimeText(soonest_to_complete.time_left())] ([length(soonest_to_complete.signed_up)] candidates)" return ..() +///Is there a multiple of the given event type running right now? +/datum/controller/subsystem/polling/proc/duplicate_message_check(datum/candidate_poll/poll_to_check) + for(var/datum/candidate_poll/running_poll as anything in currently_polling) + if((running_poll.poll_key == poll_to_check.poll_key && running_poll != poll_to_check) && running_poll.time_left() > 0) + return TRUE + return FALSE + /datum/controller/subsystem/polling/proc/get_next_poll_to_finish() var/lowest_time_left = INFINITY var/next_poll_to_finish diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 165a068a9547..896153e0d099 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -234,7 +234,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 @@ -245,17 +245,17 @@ SUBSYSTEM_DEF(ticker) CHECK_TICK //Configure mode and assign player to antagonists var/can_continue = FALSE - can_continue = SSdynamic.pre_setup() //Choose antagonists + can_continue = SSdynamic.select_roundstart_antagonists() //Choose antagonists CHECK_TICK SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PRE_JOBS_ASSIGNED, src) - can_continue = can_continue && SSjob.DivideOccupations() //Distribute jobs + can_continue = can_continue && SSjob.divide_occupations() //Distribute jobs CHECK_TICK if(!GLOB.Debug2) if(!can_continue) log_game("Game failed pre_setup") to_chat(world, "Error setting up game. Reverting to pre-game lobby.") - SSjob.ResetOccupations() + SSjob.reset_occupations() return FALSE else message_admins(span_notice("DEBUG: Bypassing prestart checks...")) @@ -311,7 +311,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) + GLOB.start_state = new /datum/station_state() GLOB.start_state.count() @@ -338,7 +368,7 @@ 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!")) @@ -355,6 +385,92 @@ SUBSYSTEM_DEF(ticker) SSvote.initiate_vote(/datum/vote/round_chaos, "the server", forced = TRUE) +/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()) @@ -440,7 +556,7 @@ SUBSYSTEM_DEF(ticker) continue var/datum/job/player_assigned_role = new_player_living.mind.assigned_role if(player_assigned_role.job_flags & JOB_EQUIP_RANK) - SSjob.EquipRank(new_player_living, player_assigned_role, new_player_mob.client) + SSjob.equip_rank(new_player_living, player_assigned_role, new_player_mob.client) player_assigned_role.after_roundstart_spawn(new_player_living, new_player_mob.client) if(picked_spare_id_candidate == new_player_mob) captainless = FALSE diff --git a/code/controllers/subsystem/traitor.dm b/code/controllers/subsystem/traitor.dm index dad7284216dc..599fd91e5785 100644 --- a/code/controllers/subsystem/traitor.dm +++ b/code/controllers/subsystem/traitor.dm @@ -13,81 +13,35 @@ SUBSYSTEM_DEF(traitor) /// A list of all uplink items var/list/uplink_items = list() - /// File to load configurations from. - var/configuration_path = "config/traitor_objective.json" - /// Global configuration data that gets applied to each objective when it is created. - /// Basic objective format - /// '/datum/traitor_objective/path/to/objective': { - /// "global_progression_influence_intensity": 0 - /// } - var/configuration_data = list() - /// The coefficient multiplied by the current_global_progression for new joining traitors to calculate their progression var/newjoin_progression_coeff = 1 - /// The current progression that all traitors should be at in the round + /// The current progression that all traitors should be at in the round, you can't have less than this var/current_global_progression = 0 - /// The amount of deviance from the current global progression before you start getting 2x the current scaling or no scaling at all - /// Also affects objectives, so -50% progress reduction or 50% progress boost. - var/progression_scaling_deviance = 20 MINUTES /// The current uplink handlers being managed var/list/datum/uplink_handler/uplink_handlers = list() - /// The current scaling per minute of progression. Has a maximum value of 1 MINUTES. + /// The current scaling per minute of progression. var/current_progression_scaling = 1 MINUTES - /// Used to handle the probability of getting an objective. - var/datum/traitor_category_handler/category_handler - /// The current debug handler for objectives. Used for debugging objectives - var/datum/traitor_objective_debug/traitor_debug_panel - /// Used by the debug menu, decides whether newly created objectives should generate progression and telecrystals. Do not modify for non-debug purposes. - var/generate_objectives = TRUE - /// Objectives that have been completed by type. Used for limiting objectives. - var/list/taken_objectives_by_type = list() - /// A list of all existing objectives by type - var/list/all_objectives_by_type = list() /datum/controller/subsystem/traitor/Initialize() - category_handler = new() - traitor_debug_panel = new(category_handler) - + current_progression_scaling = 1 MINUTES * CONFIG_GET(number/traitor_scaling_multiplier) for(var/theft_item in subtypesof(/datum/objective_item/steal)) new theft_item - - if(fexists(configuration_path)) - var/list/data = json_decode(file2text(file(configuration_path))) - for(var/typepath in data) - var/actual_typepath = text2path(typepath) - if(!actual_typepath) - log_world("[configuration_path] has an invalid type ([typepath]) that doesn't exist in the codebase! Please correct or remove [typepath]") - configuration_data[actual_typepath] = data[typepath] return SS_INIT_SUCCESS /datum/controller/subsystem/traitor/fire(resumed) - var/player_count = length(GLOB.alive_player_list) - // Has a maximum of 1 minute, however the value can be lower if there are lower players than the ideal - // player count for a traitor to be threatening. Rounds to the nearest 10% of a minute to prevent weird - // values from appearing in the UI. Traitor scaling multiplier bypasses the limit and only multiplies the end value. - // from all of our calculations. - current_progression_scaling = max(min( - (player_count / CONFIG_GET(number/traitor_ideal_player_count)) * 1 MINUTES, - 1 MINUTES - ), 0.1 MINUTES) * CONFIG_GET(number/traitor_scaling_multiplier) + var/previous_progression = current_global_progression + current_global_progression = (STATION_TIME_PASSED()) * CONFIG_GET(number/traitor_scaling_multiplier) + var/progression_increment = current_global_progression - previous_progression - var/progression_scaling_delta = (wait / (1 MINUTES)) * current_progression_scaling - var/previous_global_progression = current_global_progression - - current_global_progression += progression_scaling_delta for(var/datum/uplink_handler/handler in uplink_handlers) if(!handler.has_progression || QDELETED(handler)) uplink_handlers -= handler - var/deviance = (previous_global_progression - handler.progression_points) / progression_scaling_deviance - if(abs(deviance) < 0.01) - // If deviance is less than 1%, just set them to the current global progression + if(handler.progression_points < current_global_progression) + // If we got unsynced somehow, just set them to the current global progression // Prevents problems with precision errors. handler.progression_points = current_global_progression else - var/amount_to_give = progression_scaling_delta + (progression_scaling_delta * deviance) - amount_to_give = clamp(amount_to_give, 0, progression_scaling_delta * 2) - handler.progression_points += amount_to_give - handler.update_objectives() + handler.progression_points += progression_increment // Should only really happen if an admin is messing with an individual's progression value handler.on_update() /datum/controller/subsystem/traitor/proc/register_uplink_handler(datum/uplink_handler/uplink_handler) @@ -101,22 +55,3 @@ SUBSYSTEM_DEF(traitor) /datum/controller/subsystem/traitor/proc/uplink_handler_deleted(datum/uplink_handler/uplink_handler) SIGNAL_HANDLER uplink_handlers -= uplink_handler - -/datum/controller/subsystem/traitor/proc/on_objective_taken(datum/traitor_objective/objective) - if(!istype(objective)) - return - - add_objective_to_list(objective, taken_objectives_by_type) - -/datum/controller/subsystem/traitor/proc/get_taken_count(datum/traitor_objective/objective_type) - return length(taken_objectives_by_type[objective_type]) - - -/datum/controller/subsystem/traitor/proc/add_objective_to_list(datum/traitor_objective/objective, list/objective_list) - var/datum/traitor_objective/current_type = objective.type - while(current_type != /datum/traitor_objective) - if(!objective_list[current_type]) - objective_list[current_type] = list(objective) - else - objective_list[current_type] += objective - current_type = type2parent(current_type) diff --git a/code/datums/ai_laws/ai_laws.dm b/code/datums/ai_laws/ai_laws.dm index 9b9f9c628411..d93966195a1f 100644 --- a/code/datums/ai_laws/ai_laws.dm +++ b/code/datums/ai_laws/ai_laws.dm @@ -234,11 +234,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/brain_damage/creepy_trauma.dm b/code/datums/brain_damage/creepy_trauma.dm index 8b4921c24315..88904110fa6a 100644 --- a/code/datums/brain_damage/creepy_trauma.dm +++ b/code/datums/brain_damage/creepy_trauma.dm @@ -67,7 +67,8 @@ /datum/brain_trauma/special/obsessed/on_lose() ..() - owner.mind.remove_antag_datum(/datum/antagonist/obsessed) + if (owner.mind.remove_antag_datum(/datum/antagonist/obsessed)) + owner.mind.add_antag_datum(/datum/antagonist/former_obsessed) owner.clear_mood_event("creeping") if(obsession) log_game("[key_name(owner)] is no longer obsessed with [key_name(obsession)].") diff --git a/code/datums/brain_damage/imaginary_friend.dm b/code/datums/brain_damage/imaginary_friend.dm index 18e74abdeba9..ea07db402fc1 100644 --- a/code/datums/brain_damage/imaginary_friend.dm +++ b/code/datums/brain_damage/imaginary_friend.dm @@ -153,11 +153,11 @@ for(var/job in appearance_from_prefs.job_preferences) var/this_pref = appearance_from_prefs.job_preferences[job] if(this_pref > highest_pref) - appearance_job = SSjob.GetJob(job) + appearance_job = SSjob.get_job(job) highest_pref = this_pref if(!appearance_job) - appearance_job = SSjob.GetJob(JOB_ASSISTANT) + appearance_job = SSjob.get_job(JOB_ASSISTANT) if(istype(appearance_job, /datum/job/ai)) human_image = icon('icons/mob/silicon/ai.dmi', icon_state = resolve_ai_icon(appearance_from_prefs.read_preference(/datum/preference/choiced/ai_core_display)), dir = SOUTH) diff --git a/code/datums/brain_damage/split_personality.dm b/code/datums/brain_damage/split_personality.dm index db7eace1af27..589996a025f6 100644 --- a/code/datums/brain_damage/split_personality.dm +++ b/code/datums/brain_damage/split_personality.dm @@ -212,10 +212,9 @@ /datum/brain_trauma/severe/split_personality/brainwashing/get_ghost() set waitfor = FALSE - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as [owner.real_name]'s brainwashed mind?", poll_time = 7.5 SECONDS, target_mob = stranger_backseat, pic_source = owner, role_name_text = "brainwashed mind") - if(LAZYLEN(candidates)) - var/mob/dead/observer/C = pick(candidates) - stranger_backseat.key = C.key + var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to play as [span_danger("[owner.real_name]'s")] brainwashed mind?", poll_time = 7.5 SECONDS, checked_target = stranger_backseat, alert_pic = owner, role_name_text = "brainwashed mind") + if(chosen_one) + stranger_backseat.key = chosen_one.key else qdel(src) diff --git a/code/datums/candidate_poll.dm b/code/datums/candidate_poll.dm index e1baf19c5cc0..a2fcf62676ed 100644 --- a/code/datums/candidate_poll.dm +++ b/code/datums/candidate_poll.dm @@ -28,8 +28,16 @@ POLL_RESPONSE_TOO_LATE_TO_UNREGISTER = "It's too late to unregister yourself, selection has already begun!", POLL_RESPONSE_UNREGISTERED = "You have been unregistered as a candidate for %ROLE%. You can sign up again before the poll ends.", ) + var/list/chosen_candidates = list() -/datum/candidate_poll/New(polled_role, polled_question, poll_duration, poll_ignoring_category, poll_jumpable, list/custom_response_messages) +/datum/candidate_poll/New( + polled_role, + polled_question, + poll_duration, + poll_ignoring_category, + poll_jumpable, + list/custom_response_messages = list(), +) role = polled_role question = polled_question duration = poll_duration @@ -37,9 +45,9 @@ jump_to_me = poll_jumpable signed_up = list() time_started = world.time - poll_key = "[question]_[role || "0"]" - for(var/custom_message in custom_response_messages) - response_messages[custom_message] = custom_response_messages[custom_message] + poll_key = "[question]_[role ? role : "0"]" + if(custom_response_messages.len) + response_messages = custom_response_messages for(var/individual_message in response_messages) response_messages[individual_message] = replacetext(response_messages[individual_message], "%ROLE%", role) return ..() @@ -70,6 +78,13 @@ return FALSE signed_up += candidate + + log_ghost_poll("Player [candidate.key] signed candidate poll", data = list( + "player key" = candidate.key, + "role name" = role, + "poll question" = question, + )) + if(!silent) to_chat(candidate, span_notice(response_messages[POLL_RESPONSE_SIGNUP])) // Sign them up for any other polls with the same mob type @@ -94,6 +109,13 @@ return FALSE signed_up -= candidate + + log_ghost_poll("Player [candidate.key] removed from poll candidacy", data = list( + "player key" = candidate.key, + "role name" = role, + "poll question" = question, + )) + if(!silent) to_chat(candidate, span_danger(response_messages[POLL_RESPONSE_UNREGISTERED])) @@ -124,3 +146,13 @@ /datum/candidate_poll/proc/time_left() return duration - (world.time - time_started) + + +/// Print to chat which candidate was selected +/datum/candidate_poll/proc/announce_chosen(list/poll_recipients) + if(!length(chosen_candidates)) + return + for(var/mob/chosen in chosen_candidates) + var/client/chosen_client = chosen.client + for(var/mob/poll_recipient as anything in poll_recipients) + to_chat(poll_recipient, span_ooc("[isobserver(poll_recipient) ? FOLLOW_LINK(poll_recipient, chosen_client.mob) : null][span_warning(" [full_capitalize(role)] Poll: ")][key_name(chosen_client, include_name = FALSE)] was selected.")) diff --git a/code/datums/communications.dm b/code/datums/communications.dm new file mode 100644 index 000000000000..91ccc8369374 --- /dev/null +++ b/code/datums/communications.dm @@ -0,0 +1,166 @@ +#define COMMUNICATION_COOLDOWN (30 SECONDS) +#define COMMUNICATION_COOLDOWN_AI (30 SECONDS) + +GLOBAL_DATUM_INIT(communications_controller, /datum/communciations_controller, new) + +/datum/communciations_controller + COOLDOWN_DECLARE(silicon_message_cooldown) + COOLDOWN_DECLARE(nonsilicon_message_cooldown) + + /// Are we trying to send a cross-station message that contains soft-filtered words? If so, flip to TRUE to extend the time admins have to cancel the message. + var/soft_filtering = FALSE + + /// The main content of the roundstart report + /// If nothing is set, it will pick a random flavor report + var/command_report_main_content = "" + /// A list of footnote datums, to be added to the bottom of the roundstart command report. + var/list/command_report_footnotes = list() + /// A counter of conditions that are blocking the command report from printing. Counter incremements up for every blocking condition, and de-incrememnts when it is complete. + var/block_command_report = 0 + /// Has a special xenomorph egg been delivered? + var/xenomorph_egg_delivered = FALSE + /// 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 + else if(!is_silicon && COOLDOWN_FINISHED(src, nonsilicon_message_cooldown)) + return TRUE + else + return FALSE + +/datum/communciations_controller/proc/make_announcement(mob/living/user, is_silicon, input, syndicate, list/players) + if(!can_announce(user, is_silicon)) + return FALSE + if(is_silicon) + minor_announce(html_decode(input),"[user.name] announces:", players = players) + COOLDOWN_START(src, silicon_message_cooldown, COMMUNICATION_COOLDOWN_AI) + else + var/list/message_data = user.treat_message(input) + if(syndicate) + priority_announce(html_decode(message_data["message"]), null, 'sound/misc/announce_syndi.ogg', ANNOUNCEMENT_TYPE_SYNDICATE, has_important_message = TRUE, players = players, color_override = "red") + else + priority_announce(html_decode(message_data["message"]), null, 'sound/misc/announce.ogg', ANNOUNCEMENT_TYPE_CAPTAIN, has_important_message = TRUE, players = players) + COOLDOWN_START(src, nonsilicon_message_cooldown, COMMUNICATION_COOLDOWN) + user.log_talk(input, LOG_SAY, tag="priority announcement") + message_admins("[ADMIN_LOOKUPFLW(user)] has made a priority announcement.") + +/datum/communciations_controller/proc/send_message(datum/comm_message/sending,print = TRUE,unique = FALSE, contains_advanced_html = FALSE) + for(var/obj/machinery/computer/communications/C in GLOB.shuttle_caller_list) + if(!(C.machine_stat & (BROKEN|NOPOWER)) && is_station_level(C.z)) + if(unique) + C.add_message(sending) + else //We copy the message for each console, answers and deletions won't be shared + var/datum/comm_message/M = new(sending.title,sending.content,sending.possible_answers.Copy()) + C.add_message(M) + if(print) + var/obj/item/paper/printed_paper = new /obj/item/paper(C.loc) + printed_paper.name = "paper - '[sending.title]'" + printed_paper.add_raw_text("[sending.content]", advanced_html = contains_advanced_html) + printed_paper.color = "#deebff" + 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 + + . = "" + . += "

" + . += "

[command_name()], TCD [time2text(world.realtime, "DDD, MMM DD")], [CURRENT_STATION_YEAR]


" + . += command_report_main_content || get_main_report_content() + if(CONFIG_GET(flag/no_dynamic_report)) + if(isnull(greenshift)) + greenshift = SSdynamic.current_tier.tier == 0 + else + 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:

" + . += dynamic_report + + SSstation.generate_station_goals(greenshift ? INFINITY : CONFIG_GET(number/station_goal_budget)) + + var/list/station_goal_strings = list() + if(greenshift) + station_goal_strings += "All special orders have been authorized for the shift. \ + Feel free to pick one your crew wishes to specialize in - you are not expected to complete them all." + + else + for(var/datum/station_goal/station_goal as anything in SSstation.get_station_goals()) + station_goal.on_report() + station_goal_strings += station_goal.get_report() + + if(length(station_goal_strings) > 0) // if we have any special orders to report, add them in + . += "

Special Orders for [station_name()]:

" + . += station_goal_strings.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, contains_advanced_html = TRUE) + 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(CONFIG_GET(flag/roundstart_blue_alert)) + 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, + ) + else + priority_announce( + "A summary of the station's situation has been copied and printed to all communications consoles.", + "Security Report", + SSstation.announcer.get_rand_report_sound(), + ) + +#endif + + return . + +/// Return a random flavor/meme report to use in the command report +/datum/communciations_controller/proc/get_main_report_content() + if(istype(SSstation.announcer, /datum/centcom_announcer/intern)) + return pick_list_replacements("flavor_reports.json", "intern_reports") + return pick_list_replacements("flavor_reports.json", "reports") + +#undef COMMUNICATION_COOLDOWN +#undef COMMUNICATION_COOLDOWN_AI diff --git a/code/datums/components/crafting/structures.dm b/code/datums/components/crafting/structures.dm index c4a9b48ec36b..aadc142561d5 100644 --- a/code/datums/components/crafting/structures.dm +++ b/code/datums/components/crafting/structures.dm @@ -60,17 +60,3 @@ /obj/item/stack/cable_coil = 10, ) category = CAT_STRUCTURE - -/datum/crafting_recipe/syndicate_uplink_beacon - name = "Syndicate Uplink Beacon" - result = /obj/structure/syndicate_uplink_beacon - tool_behaviors = list(TOOL_SCREWDRIVER) - time = 6 SECONDS - reqs = list( - /obj/item/stack/sheet/iron = 5, - /obj/item/stack/cable_coil = 5, - /obj/item/beacon = 1, - /obj/item/stack/ore/bluespace_crystal = 1, - ) - category = CAT_STRUCTURE - crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_MUST_BE_LEARNED diff --git a/code/datums/components/ghost_direct_control.dm b/code/datums/components/ghost_direct_control.dm index a131a2d3ca72..05460efa096d 100644 --- a/code/datums/components/ghost_direct_control.dm +++ b/code/datums/components/ghost_direct_control.dm @@ -16,8 +16,11 @@ /datum/component/ghost_direct_control/Initialize( ban_type = ROLE_SENTIENCE, role_name = null, + poll_question = null, poll_candidates = TRUE, + poll_announce_chosen = TRUE, poll_length = 10 SECONDS, + poll_chat_border_icon = null, poll_ignore_key = POLL_IGNORE_SENTIENCE_POTION, assumed_control_message = null, datum/callback/extra_control_checks, @@ -36,7 +39,7 @@ LAZYADD(GLOB.joinable_mobs[format_text("[initial(mob_parent.name)]")], mob_parent) if (poll_candidates) - INVOKE_ASYNC(src, PROC_REF(request_ghost_control), role_name || "[parent]", poll_length, poll_ignore_key) + INVOKE_ASYNC(src, PROC_REF(request_ghost_control), poll_question, role_name || "[parent]", poll_length, poll_ignore_key, poll_announce_chosen, poll_chat_border_icon) /datum/component/ghost_direct_control/RegisterWithParent() . = ..() @@ -70,23 +73,25 @@ examine_text += span_boldnotice("You could take control of this mob by clicking on it.") /// Send out a request for a brain -/datum/component/ghost_direct_control/proc/request_ghost_control(role_name, poll_length, poll_ignore_key) - if (!(GLOB.ghost_role_flags & GHOSTROLE_SPAWNER)) +/datum/component/ghost_direct_control/proc/request_ghost_control(poll_question, role_name, poll_length, poll_ignore_key, poll_announce_chosen, poll_chat_border_icon) + if(!(GLOB.ghost_role_flags & GHOSTROLE_SPAWNER)) return awaiting_ghosts = TRUE - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates( - question = "Do you want to play as [role_name]?", + var/mob/chosen_one = SSpolling.poll_ghosts_for_target( + question = poll_question, check_jobban = ban_type, - role = ban_type, poll_time = poll_length, + checked_target = parent, ignore_category = poll_ignore_key, - pic_source = parent, + alert_pic = parent, role_name_text = role_name, + chat_text_border_icon = poll_chat_border_icon, + announce_chosen = poll_announce_chosen, ) awaiting_ghosts = FALSE - if (!LAZYLEN(candidates)) + if(isnull(chosen_one)) return - assume_direct_control(pick(candidates)) + assume_direct_control(chosen_one) /// A ghost clicked on us, they want to get in this body /datum/component/ghost_direct_control/proc/on_ghost_clicked(mob/our_mob, mob/dead/observer/hopeful_ghost) diff --git a/code/datums/components/transforming.dm b/code/datums/components/transforming.dm index d800ee041f7b..9f006ddb0f1b 100644 --- a/code/datums/components/transforming.dm +++ b/code/datums/components/transforming.dm @@ -154,7 +154,8 @@ toggle_active(source) if(!(SEND_SIGNAL(source, COMSIG_TRANSFORMING_ON_TRANSFORM, user, active) & COMPONENT_NO_DEFAULT_MESSAGE)) default_transform_message(source, user) - + if(!isnull(user)) + SEND_SIGNAL(user, COMSIG_MOB_TRANSFORMING_ITEM, source, active) if(isnum(transform_cooldown_time)) COOLDOWN_START(src, transform_cooldown, transform_cooldown_time) if(user) diff --git a/code/datums/components/uplink.dm b/code/datums/components/uplink.dm index 9f56bc3f6f68..c6f57a5169ff 100644 --- a/code/datums/components/uplink.dm +++ b/code/datums/components/uplink.dm @@ -66,8 +66,6 @@ RegisterSignal(parent, COMSIG_RADIO_NEW_MESSAGE, PROC_REF(new_message)) else if(istype(parent, /obj/item/pen)) RegisterSignal(parent, COMSIG_PEN_ROTATED, PROC_REF(pen_rotation)) - else if(istype(parent, /obj/item/uplink/replacement)) - RegisterSignal(parent, COMSIG_MOVABLE_HEAR, PROC_REF(on_heard)) if(owner) src.owner = owner @@ -81,7 +79,6 @@ src.active = enabled if(!uplink_handler_override) uplink_handler = new() - uplink_handler.has_objectives = FALSE uplink_handler.uplink_flag = uplink_flag uplink_handler.telecrystals = starting_tc uplink_handler.has_progression = has_progression @@ -89,7 +86,6 @@ else uplink_handler = uplink_handler_override RegisterSignal(uplink_handler, COMSIG_UPLINK_HANDLER_ON_UPDATE, PROC_REF(handle_uplink_handler_update)) - RegisterSignal(uplink_handler, COMSIG_UPLINK_HANDLER_REPLACEMENT_ORDERED, PROC_REF(handle_uplink_replaced)) if(!lockable) active = TRUE locked = FALSE @@ -100,28 +96,6 @@ SIGNAL_HANDLER SStgui.update_uis(src) -/// When a new uplink is made via the syndicate beacon it locks all lockable uplinks and destroys replacement uplinks -/datum/component/uplink/proc/handle_uplink_replaced() - SIGNAL_HANDLER - if(lockable) - lock_uplink() - if(!istype(parent, /obj/item/uplink/replacement)) - return - var/obj/item/uplink_item = parent - do_sparks(number = 3, cardinal_only = FALSE, source = uplink_item) - uplink_item.visible_message(span_warning("The [uplink_item] suddenly combusts!"), vision_distance = COMBAT_MESSAGE_RANGE) - new /obj/effect/decal/cleanable/ash(get_turf(uplink_item)) - qdel(uplink_item) - -/// Adds telecrystals to the uplink. It is bad practice to use this outside of the component itself. -/datum/component/uplink/proc/add_telecrystals(telecrystals_added) - set_telecrystals(uplink_handler.telecrystals + telecrystals_added) - -/// Sets the telecrystals of the uplink. It is bad practice to use this outside of the component itself. -/datum/component/uplink/proc/set_telecrystals(new_telecrystal_amount) - uplink_handler.telecrystals = new_telecrystal_amount - uplink_handler.on_update() - /datum/component/uplink/InheritComponent(datum/component/uplink/uplink) lockable |= uplink.lockable active |= uplink.active @@ -194,13 +168,10 @@ var/list/data = list() data["telecrystals"] = uplink_handler.telecrystals data["progression_points"] = uplink_handler.progression_points - data["current_expected_progression"] = SStraitor.current_global_progression - data["maximum_active_objectives"] = uplink_handler.maximum_active_objectives - data["progression_scaling_deviance"] = SStraitor.progression_scaling_deviance + data["joined_population"] = length(GLOB.joined_player_list) data["current_progression_scaling"] = SStraitor.current_progression_scaling - data["maximum_potential_objectives"] = uplink_handler.maximum_potential_objectives - if(uplink_handler.has_objectives) + if(uplink_handler.primary_objectives) var/list/primary_objectives = list() for(var/datum/objective/task as anything in uplink_handler.primary_objectives) var/list/task_data = list() @@ -211,24 +182,6 @@ task_data["task_text"] = task.explanation_text primary_objectives += list(task_data) - var/list/potential_objectives = list() - for(var/index in 1 to uplink_handler.potential_objectives.len) - var/datum/traitor_objective/objective = uplink_handler.potential_objectives[index] - var/list/objective_data = objective.uplink_ui_data(user) - objective_data["id"] = index - potential_objectives += list(objective_data) - - var/list/active_objectives = list() - for(var/index in 1 to uplink_handler.active_objectives.len) - var/datum/traitor_objective/objective = uplink_handler.active_objectives[index] - var/list/objective_data = objective.uplink_ui_data(user) - objective_data["id"] = index - active_objectives += list(objective_data) - - data["primary_objectives"] = primary_objectives - data["potential_objectives"] = potential_objectives - data["active_objectives"] = active_objectives - data["completed_final_objective"] = uplink_handler.final_objective var/list/stock_list = uplink_handler.item_stock.Copy() var/list/extra_purchasable_stock = list() @@ -251,6 +204,7 @@ "restricted_roles" = item.restricted_roles, "restricted_species" = item.restricted_species, "progression_minimum" = item.progression_minimum, + "population_minimum" = item.population_minimum, "ref" = REF(item), )) @@ -269,7 +223,6 @@ var/list/data = list() data["uplink_flag"] = uplink_handler.uplink_flag data["has_progression"] = uplink_handler.has_progression - data["has_objectives"] = uplink_handler.has_objectives data["lockable"] = lockable data["assigned_role"] = uplink_handler.assigned_role data["assigned_species"] = uplink_handler.assigned_species @@ -312,47 +265,9 @@ if("renegotiate_objectives") uplink_handler.replace_objectives?.Invoke() SStgui.update_uis(src) - - if(!uplink_handler.has_objectives) - return TRUE - - if(uplink_handler.owner?.current != ui.user || !uplink_handler.can_take_objectives) - return TRUE - - switch(action) - if("regenerate_objectives") - uplink_handler.generate_objectives() - return TRUE - - var/list/objectives - switch(action) - if("start_objective") - objectives = uplink_handler.potential_objectives - if("objective_act", "finish_objective", "objective_abort") - objectives = uplink_handler.active_objectives - - if(!objectives) - return - - var/objective_index = round(text2num(params["index"])) - if(objective_index < 1 || objective_index > length(objectives)) - return TRUE - var/datum/traitor_objective/objective = objectives[objective_index] - - // Objective actions - switch(action) - if("start_objective") - uplink_handler.take_objective(ui.user, objective) - if("objective_act") - uplink_handler.ui_objective_act(ui.user, objective, params["objective_action"]) - if("finish_objective") - if(!objective.finish_objective(ui.user)) - return - uplink_handler.complete_objective(objective) - if("objective_abort") - uplink_handler.abort_objective(objective) return TRUE + /// Proc that locks uplinks /datum/component/uplink/proc/lock_uplink() active = FALSE @@ -498,17 +413,6 @@ return returnable_code -/// Proc that unlocks a locked replacement uplink when it hears the unlock code from their datum -/datum/component/uplink/proc/on_heard(datum/source, list/hearing_args) - SIGNAL_HANDLER - if(!locked) - return - if(!findtext(hearing_args[HEARING_RAW_MESSAGE], unlock_code)) - return - var/atom/replacement_uplink = parent - locked = FALSE - replacement_uplink.balloon_alert_to_viewers("beep", vision_distance = COMBAT_MESSAGE_RANGE) - /datum/component/uplink/proc/failsafe(atom/source) if(!parent) return diff --git a/code/datums/diseases/transformation.dm b/code/datums/diseases/transformation.dm index c0734025a8e0..c510ce0ca855 100644 --- a/code/datums/diseases/transformation.dm +++ b/code/datums/diseases/transformation.dm @@ -84,20 +84,18 @@ /datum/disease/transformation/proc/replace_banned_player(mob/living/new_mob) // This can run well after the mob has been transferred, so need a handle on the new mob to kill it if needed. set waitfor = FALSE - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as [affected_mob.real_name]?", check_jobban = bantype, role = bantype, poll_time = 5 SECONDS, target_mob = affected_mob, pic_source = affected_mob, role_name_text = "transformation victim") - if(LAZYLEN(candidates)) - var/mob/dead/observer/C = pick(candidates) + var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to play as [span_notice(affected_mob.real_name)]?", check_jobban = bantype, role = bantype, poll_time = 5 SECONDS, checked_target = affected_mob, alert_pic = affected_mob, role_name_text = "transformation victim") + if(chosen_one) to_chat(affected_mob, span_userdanger("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(C)] has taken control of ([key_name_admin(affected_mob)]) to replace a jobbanned player.") + message_admins("[key_name_admin(chosen_one)] has taken control of ([key_name_admin(affected_mob)]) to replace a jobbanned player.") affected_mob.ghostize(FALSE) - affected_mob.key = C.key + affected_mob.key = chosen_one.key else to_chat(new_mob, span_userdanger("Your mob has been claimed by death! Appeal your job ban if you want to avoid this in the future!")) new_mob.investigate_log("has been killed because there was no one to replace them as a job-banned player.", INVESTIGATE_DEATHS) new_mob.death() if (!QDELETED(new_mob)) new_mob.ghostize(can_reenter_corpse = FALSE) - new_mob.key = null /datum/disease/transformation/jungle_flu name = "Jungle Flu" diff --git a/code/datums/elements/art.dm b/code/datums/elements/art.dm index 81d388aa94af..d5a642c23d0b 100644 --- a/code/datums/elements/art.dm +++ b/code/datums/elements/art.dm @@ -74,7 +74,7 @@ var/datum/job_department/hater_department = SSjob.get_department_type(hater_department_type) for(var/datum/job/hater_job as anything in hater_department.department_jobs) haters += hater_job.title - var/datum/job/quartermaster/fucking_quartermaster = SSjob.GetJobType(/datum/job/quartermaster) + var/datum/job/quartermaster/fucking_quartermaster = SSjob.get_job_type(/datum/job/quartermaster) haters += fucking_quartermaster.title if(!(user.mind.assigned_role.title in haters)) diff --git a/code/datums/elements/uplink_reimburse.dm b/code/datums/elements/uplink_reimburse.dm index 3ff182ec2314..2753fdcf495a 100644 --- a/code/datums/elements/uplink_reimburse.dm +++ b/code/datums/elements/uplink_reimburse.dm @@ -22,7 +22,7 @@ RegisterSignal(target, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) RegisterSignal(target, COMSIG_ITEM_ATTEMPT_TC_REIMBURSE, PROC_REF(reimburse)) RegisterSignal(target,COMSIG_TRAITOR_ITEM_USED(target.type), PROC_REF(used)) - + /datum/element/uplink_reimburse/Detach(datum/target) UnregisterSignal(target, list(COMSIG_ATOM_EXAMINE, COMSIG_TRAITOR_ITEM_USED(target.type), COMSIG_ITEM_ATTEMPT_TC_REIMBURSE)) @@ -47,10 +47,12 @@ to_chat(user, span_notice("You tap [uplink_comp.uplink_handler] with [refund_item], and a moment after [refund_item] disappears in a puff of red smoke!")) do_sparks(2, source = uplink_comp.uplink_handler) - uplink_comp.add_telecrystals(refundable_tc) + uplink_comp.uplink_handler.add_telecrystals(refundable_tc) + SEND_SIGNAL(refund_item, COMSIG_ITEM_TC_REIMBURSED) qdel(refund_item) + /// If the item is used, it needs to no longer be refundable /datum/element/uplink_reimburse/proc/used(datum/target) SIGNAL_HANDLER - + Detach(target) diff --git a/code/datums/id_trim/jobs.dm b/code/datums/id_trim/jobs.dm index 6e9341f57f35..9865ede38073 100644 --- a/code/datums/id_trim/jobs.dm +++ b/code/datums/id_trim/jobs.dm @@ -24,7 +24,7 @@ /datum/id_trim/job/New() if(ispath(job)) - job = SSjob.GetJobType(job) + job = SSjob.get_job_type(job) if(isnull(job_changes)) job_changes = SSmapping.config.job_changes @@ -205,30 +205,6 @@ ) job = /datum/job/botanist -/datum/id_trim/job/bridge_assistant - assignment = "Bridge Assistant" - trim_state = "trim_assistant" - department_color = COLOR_COMMAND_BLUE - subdepartment_color = COLOR_COMMAND_BLUE - sechud_icon_state = SECHUD_BRIDGE_ASSISTANT - minimal_access = list( - ACCESS_COMMAND, - ACCESS_EVA, - ACCESS_GATEWAY, - ACCESS_MAINT_TUNNELS, - ACCESS_RC_ANNOUNCE, - ACCESS_TELEPORTER, - ACCESS_WEAPONS, - ) - extra_access = list() - template_access = list( - ACCESS_CAPTAIN, - ACCESS_CHANGE_IDS, - ) - job = /datum/job/bridge_assistant - honorifics = list("Underling", "Assistant", "Mate") - honorific_positions = HONORIFIC_POSITION_FIRST | HONORIFIC_POSITION_LAST | HONORIFIC_POSITION_FIRST_FULL - /datum/id_trim/job/captain assignment = "Captain" intern_alt_name = "Captain-in-Training" @@ -1154,7 +1130,7 @@ if(CONFIG_GET(number/depsec_access_level) == POPULATION_SCALED_ACCESS) var/minimal_security_officers = 3 // We do not spawn in any more lockers if there are 5 or less security officers, so let's keep it lower than that number. - var/datum/job/J = SSjob.GetJob(JOB_SECURITY_OFFICER) + var/datum/job/J = SSjob.get_job(JOB_SECURITY_OFFICER) if((J.spawn_positions - minimal_security_officers) <= 0) access |= elevated_access diff --git a/code/datums/mind/_mind.dm b/code/datums/mind/_mind.dm index 61b30a1e2802..1c58d9847770 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 @@ -90,7 +88,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) @@ -105,7 +105,7 @@ /datum/mind/New(_key) key = _key init_known_skills() - set_assigned_role(SSjob.GetJobType(/datum/job/unassigned)) // Unassigned by default. + set_assigned_role(SSjob.get_job_type(/datum/job/unassigned)) // Unassigned by default. /datum/mind/Destroy() SSticker.minds -= src @@ -125,7 +125,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 @@ -249,7 +249,7 @@ var/new_role = input("Select new role", "Assigned role", assigned_role.title) as null|anything in sort_list(SSjob.name_occupations) if(isnull(new_role)) return - var/datum/job/new_job = SSjob.GetJob(new_role) + var/datum/job/new_job = SSjob.get_job(new_role) if (!new_job) to_chat(usr, span_warning("Job not found.")) return @@ -406,41 +406,6 @@ message_admins("[key_name_admin(usr)] has unemag'ed [ai]'s Cyborgs.") log_admin("[key_name(usr)] has unemag'ed [ai]'s Cyborgs.") - else if(href_list["edit_obj_tc"]) - var/datum/traitor_objective/objective = locate(href_list["edit_obj_tc"]) - if(!istype(objective)) - return - var/telecrystal = input("Set new telecrystal reward for [objective.name]","Syndicate uplink", objective.telecrystal_reward) as null | num - if(isnull(telecrystal)) - return - objective.telecrystal_reward = telecrystal - message_admins("[key_name_admin(usr)] changed [objective]'s telecrystal reward count to [telecrystal].") - log_admin("[key_name(usr)] changed [objective]'s telecrystal reward count to [telecrystal].") - else if(href_list["edit_obj_pr"]) - var/datum/traitor_objective/objective = locate(href_list["edit_obj_pr"]) - if(!istype(objective)) - return - var/progression = input("Set new progression reward for [objective.name]","Syndicate uplink", objective.progression_reward) as null | num - if(isnull(progression)) - return - objective.progression_reward = progression - message_admins("[key_name_admin(usr)] changed [objective]'s progression reward count to [progression].") - log_admin("[key_name(usr)] changed [objective]'s progression reward count to [progression].") - else if(href_list["fail_objective"]) - var/datum/traitor_objective/objective = locate(href_list["fail_objective"]) - if(!istype(objective)) - return - var/performed = objective.objective_state == OBJECTIVE_STATE_INACTIVE? "skipped" : "failed" - message_admins("[key_name_admin(usr)] forcefully [performed] [objective].") - log_admin("[key_name(usr)] forcefully [performed] [objective].") - objective.fail_objective() - else if(href_list["succeed_objective"]) - var/datum/traitor_objective/objective = locate(href_list["succeed_objective"]) - if(!istype(objective)) - return - message_admins("[key_name_admin(usr)] forcefully succeeded [objective].") - log_admin("[key_name(usr)] forcefully succeeded [objective].") - objective.succeed_objective() else if (href_list["common"]) switch(href_list["common"]) if("undress") @@ -471,26 +436,6 @@ uplink.uplink_handler.progression_points = progression message_admins("[key_name_admin(usr)] changed [current]'s progression point count to [progression].") log_admin("[key_name(usr)] changed [current]'s progression point count to [progression].") - uplink.uplink_handler.update_objectives() - uplink.uplink_handler.generate_objectives() - if("give_objective") - if(!check_rights(R_FUN)) - return - var/datum/component/uplink/uplink = find_syndicate_uplink() - if(!uplink || !uplink.uplink_handler) - return - var/list/all_objectives = subtypesof(/datum/traitor_objective) - var/objective_typepath = tgui_input_list(usr, "Select objective", "Select objective", all_objectives) - if(!objective_typepath) - return - var/datum/traitor_objective/objective = uplink.uplink_handler.try_add_objective(objective_typepath, force = TRUE) - if(objective) - message_admins("[key_name_admin(usr)] gave [current] a traitor objective ([objective_typepath]).") - log_admin("[key_name(usr)] gave [current] a traitor objective ([objective_typepath]).") - else - to_chat(usr, span_warning("Failed to generate the objective!")) - message_admins("[key_name_admin(usr)] failed to give [current] a traitor objective ([objective_typepath]).") - log_admin("[key_name(usr)] failed to give [current] a traitor objective ([objective_typepath]).") if("uplink") var/datum/antagonist/traitor/traitor_datum = has_antag_datum(/datum/antagonist/traitor) if(!give_uplink(antag_datum = traitor_datum || null)) diff --git a/code/datums/mind/antag.dm b/code/datums/mind/antag.dm index 647821935cff..f0eff835f892 100644 --- a/code/datums/mind/antag.dm +++ b/code/datums/mind/antag.dm @@ -60,37 +60,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() var/list/Mob_Contents = current.get_contents() for(var/obj/item/I in Mob_Contents) @@ -220,7 +189,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.")) @@ -267,33 +236,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.GetJobType(/datum/job/space_wizard)) - special_role = ROLE_WIZARD + set_assigned_role(SSjob.get_job_type(/datum/job/space_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 eb622cc5af54..0e9a40f7f639 100644 --- a/code/datums/mind/initialization.dm +++ b/code/datums/mind/initialization.dm @@ -21,17 +21,16 @@ //AI /mob/living/silicon/ai/mind_initialize() . = ..() - mind.set_assigned_role(SSjob.GetJobType(/datum/job/ai)) + mind.set_assigned_role(SSjob.get_job_type(/datum/job/ai)) //BORG /mob/living/silicon/robot/mind_initialize() . = ..() - mind.set_assigned_role(SSjob.GetJobType(/datum/job/cyborg)) + mind.set_assigned_role(SSjob.get_job_type(/datum/job/cyborg)) //PAI /mob/living/silicon/pai/mind_initialize() . = ..() - mind.set_assigned_role(SSjob.GetJobType(/datum/job/personal_ai)) - mind.special_role = "" + mind.set_assigned_role(SSjob.get_job_type(/datum/job/personal_ai)) diff --git a/code/datums/records/manifest.dm b/code/datums/records/manifest.dm index 2e9f504e71de..d1927c87f2a1 100644 --- a/code/datums/records/manifest.dm +++ b/code/datums/records/manifest.dm @@ -32,7 +32,7 @@ GLOBAL_DATUM_INIT(manifest, /datum/manifest, new) var/name = target.name var/rank = target.rank // user-visible job var/trim = target.trim // internal jobs by trim type - var/datum/job/job = SSjob.GetJob(trim) + var/datum/job/job = SSjob.get_job(trim) if(!job || !(job.job_flags & JOB_CREW_MANIFEST) || !LAZYLEN(job.departments_list)) // In case an unlawful custom rank is added. var/list/misc_list = manifest_out[DEPARTMENT_UNASSIGNED] misc_list[++misc_list.len] = list( diff --git a/code/datums/station_traits/_station_trait.dm b/code/datums/station_traits/_station_trait.dm index 757dcb21d8b7..6bfdaa01a175 100644 --- a/code/datums/station_traits/_station_trait.dm +++ b/code/datums/station_traits/_station_trait.dm @@ -31,8 +31,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 overriden by config if set - var/threat_reduction = 0 /// Trait should not be instantiated in a round if its type matches this type var/abstract_type = /datum/station_trait @@ -41,8 +39,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(sign_up_button) GLOB.lobby_station_traits += src if(trait_processes) @@ -52,8 +48,9 @@ GLOBAL_LIST_EMPTY(lobby_station_traits) /datum/station_trait/Destroy() SSstation.station_traits -= src - GLOB.dynamic_station_traits.Remove(src) destroy_lobby_buttons() + GLOB.lobby_station_traits -= src + REMOVE_TRAIT(SSstation, trait_to_give, STATION_TRAIT) return ..() /// Returns the type of info the centcom report has on this trait, if any. diff --git a/code/datums/station_traits/job_traits.dm b/code/datums/station_traits/job_traits.dm index dc207d17e59a..70fe2f443d1b 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)) @@ -61,7 +50,7 @@ if (isnull(signee) || !signee.client || !signee.mind || signee.ready != PLAYER_READY_TO_PLAY) LAZYREMOVE(lobby_candidates, signee) - var/datum/job/our_job = SSjob.GetJobType(job_to_add) + var/datum/job/our_job = SSjob.get_job_type(job_to_add) while(length(lobby_candidates) && position_amount > 0) var/mob/dead/new_player/picked_player = pick_n_take(lobby_candidates) picked_player.mind.set_assigned_role(our_job) @@ -71,7 +60,7 @@ lobby_candidates = null /datum/station_trait/job/can_display_lobby_button(client/player) - var/datum/job/our_job = SSjob.GetJobType(job_to_add) + var/datum/job/our_job = SSjob.get_job_type(job_to_add) return our_job.player_old_enough(player) && ..() /// Adds a gorilla to the cargo department, replacing the sloth and the mech @@ -80,7 +69,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() @@ -107,73 +95,14 @@ if(GLOB.cargo_ripley) qdel(GLOB.cargo_ripley) -/datum/station_trait/job/bridge_assistant - name = "Bridge Assistant" - button_desc = "Sign up to become the Bridge Assistant and watch over the Bridge." - 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() - . = ..() - RegisterSignal(SSatoms, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(add_coffeemaker)) - -/datum/station_trait/job/bridge_assistant/on_lobby_button_update_overlays(atom/movable/screen/lobby/button/sign_up/lobby_button, list/overlays) - . = ..() - overlays += "bridge_assistant" - -/// Creates a coffeemaker in the bridge, if we don't have one yet. -/datum/station_trait/job/bridge_assistant/proc/add_coffeemaker(datum/source) - SIGNAL_HANDLER - var/area/bridge = GLOB.areas_by_type[/area/station/command/bridge] - if(isnull(bridge)) //no bridge, what will he assist? - return - var/list/possible_coffeemaker_positions = list(/area/station/command/bridge, /area/station/command/meeting_room) - var/list/coffeemakers = SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/coffeemaker) - for(var/obj/machinery/coffeemaker as anything in coffeemakers) //don't spawn a coffeemaker if there is already one on the bridge - if(is_type_in_list(get_area(coffeemaker), possible_coffeemaker_positions)) - return - var/list/tables = list() - for(var/turf/area_turf as anything in bridge.get_turfs_from_all_zlevels()) - var/obj/structure/table/table = locate() in area_turf - if(isnull(table)) - continue - if(area_turf.is_blocked_turf(ignore_atoms = list(table))) //don't spawn a coffeemaker on a fax machine or smth - continue - tables += table - if(!length(tables)) - return - var/picked_table = pick_n_take(tables) - var/picked_turf = get_turf(picked_table) - if(length(tables)) - var/another_table = pick(tables) - for(var/obj/thing_on_table in picked_turf) //if there's paper bins or other shit on the table, get that off - if(thing_on_table == picked_table) - continue - if(HAS_TRAIT(thing_on_table, TRAIT_WALLMOUNTED) || (thing_on_table.flags_1 & ON_BORDER_1) || thing_on_table.layer < TABLE_LAYER) - continue - if(thing_on_table.invisibility || !thing_on_table.alpha || !thing_on_table.mouse_opacity) - continue - thing_on_table.forceMove(get_turf(another_table)) - new /obj/machinery/coffeemaker/impressa(picked_turf) - new /obj/item/reagent_containers/cup/coffeepot(picked_turf) - new /obj/item/storage/box/coffeepack(picked_turf) - /datum/station_trait/job/veteran_advisor name = "Veteran Advisor" button_desc = "Sign up to become a DISABLED but hard boiled Veteran Advisor of Nanotrasen Security Force. Advise HoS and Captain, train Officers, all while fighting your PTSD." 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 /datum/station_trait/job/veteran_advisor/on_lobby_button_update_overlays(atom/movable/screen/lobby/button/sign_up/lobby_button, list/overlays) . = ..() overlays += "veteran_advisor" - -#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 498f120fe7fa..ff41592dfb4d 100644 --- a/code/datums/station_traits/negative_traits.dm +++ b/code/datums/station_traits/negative_traits.dm @@ -556,7 +556,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 = 5 MINUTES diff --git a/code/datums/station_traits/positive_traits.dm b/code/datums/station_traits/positive_traits.dm index 539cdf2027ff..7d5031c28aec 100644 --- a/code/datums/station_traits/positive_traits.dm +++ b/code/datums/station_traits/positive_traits.dm @@ -234,7 +234,6 @@ /datum/job/warden = /obj/item/organ/cyberimp/eyes/hud/security, // NON-MODULE CHANGE /datum/job/asset_protection = /obj/item/organ/eyes/robotic/thermals, - /datum/job/bridge_assistant = /obj/item/organ/eyes/robotic, /datum/job/bridge_officer = /obj/item/organ/eyes/robotic, /datum/job/noble_ambassador = /obj/item/organ/cyberimp/chest/nutriment/plus, /datum/job/ordnance_tech = /obj/item/organ/cyberimp/arm/toolset, diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm index fbf1dc1c29ea..d0632112a0f6 100644 --- a/code/game/gamemodes/objective.dm +++ b/code/game/gamemodes/objective.dm @@ -226,7 +226,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list /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]." + explanation_text = "Assassinate [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())]." else explanation_text = "Free objective." @@ -276,7 +276,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list /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." @@ -298,7 +298,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list /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." @@ -329,7 +329,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list /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." @@ -353,7 +353,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list /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." @@ -378,7 +378,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list /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." @@ -394,7 +394,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list /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/gamemodes/objective_items.dm b/code/game/gamemodes/objective_items.dm index e0fea12a0a88..d366ed78757c 100644 --- a/code/game/gamemodes/objective_items.dm +++ b/code/game/gamemodes/objective_items.dm @@ -1,7 +1,55 @@ +GLOBAL_DATUM_INIT(steal_item_handler, /datum/objective_item_handler, new()) + /proc/add_item_to_steal(source, type) GLOB.steal_item_handler.objectives_by_path[type] += source return type +/// Holds references to information about all of the items you might need to steal for objectives +/datum/objective_item_handler + var/list/list/objectives_by_path + var/generated_items = FALSE + +/datum/objective_item_handler/New() + . = ..() + objectives_by_path = list() + for(var/datum/objective_item/item as anything in subtypesof(/datum/objective_item)) + objectives_by_path[initial(item.targetitem)] = list() + RegisterSignal(SSatoms, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(save_items)) + RegisterSignal(SSdcs, COMSIG_GLOB_NEW_ITEM, PROC_REF(new_item_created)) + +/datum/objective_item_handler/proc/new_item_created(datum/source, obj/item/item) + SIGNAL_HANDLER + // if(HAS_TRAIT(item, TRAIT_ITEM_OBJECTIVE_BLOCKED)) + // return + if(!generated_items) + item.add_stealing_item_objective() + return + var/typepath = item.add_stealing_item_objective() + if(typepath != null) + register_item(item, typepath) + +/// Registers all items that are potentially stealable and removes ones that aren't. +/// We still need to do things this way because on mapload, items may not be on the station until everything has finished loading. +/datum/objective_item_handler/proc/save_items() + SIGNAL_HANDLER + for(var/obj/item/typepath as anything in objectives_by_path) + var/list/obj_by_path_cache = objectives_by_path[typepath].Copy() + for(var/obj/item/object as anything in obj_by_path_cache) + register_item(object, typepath) + generated_items = TRUE + +/datum/objective_item_handler/proc/register_item(atom/object, typepath) + var/turf/place = get_turf(object) + if(!place || !is_station_level(place.z)) + objectives_by_path[typepath] -= object + return + RegisterSignal(object, COMSIG_QDELETING, PROC_REF(remove_item)) + +/datum/objective_item_handler/proc/remove_item(atom/source) + SIGNAL_HANDLER + for(var/typepath in objectives_by_path) + objectives_by_path[typepath] -= source + //Contains the target item datums for Steal objectives. /datum/objective_item /// How the item is described in the objective diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm index 8471fd7a0675..cd8c66d44a84 100644 --- a/code/game/machinery/computer/communications.dm +++ b/code/game/machinery/computer/communications.dm @@ -341,7 +341,7 @@ if (!message) return - SScommunications.soft_filtering = FALSE + GLOB.communications_controller.soft_filtering = FALSE var/list/hard_filter_result = is_ic_filtered(message) if(hard_filter_result) tgui_alert(usr, "Your message contains: (\"[hard_filter_result[CHAT_FILTER_INDEX_WORD]]\"), which is not allowed on this server.") @@ -353,7 +353,7 @@ return message_admins("[ADMIN_LOOKUPFLW(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\". They may be using a disallowed term for a cross-station message. Increasing delay time to reject.\n\n Message: \"[html_encode(message)]\"") log_admin_private("[key_name(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\". They may be using a disallowed term for a cross-station message. Increasing delay time to reject.\n\n Message: \"[message]\"") - SScommunications.soft_filtering = TRUE + GLOB.communications_controller.soft_filtering = TRUE playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE) @@ -364,13 +364,13 @@ GLOB.admins, span_adminnotice( \ "CROSS-SECTOR MESSAGE (OUTGOING): [ADMIN_LOOKUPFLW(usr)] is about to send \ - the following message to [destination] (will autoapprove in [SScommunications.soft_filtering ? DisplayTimeText(EXTENDED_CROSS_SECTOR_CANCEL_TIME) : DisplayTimeText(CROSS_SECTOR_CANCEL_TIME)]): \ + the following message to [destination] (will autoapprove in [GLOB.communications_controller.soft_filtering ? DisplayTimeText(EXTENDED_CROSS_SECTOR_CANCEL_TIME) : DisplayTimeText(CROSS_SECTOR_CANCEL_TIME)]): \ REJECT
\ [html_encode(message)]" \ ) ) - send_cross_comms_message_timer = addtimer(CALLBACK(src, PROC_REF(send_cross_comms_message), usr, destination, message), SScommunications.soft_filtering ? EXTENDED_CROSS_SECTOR_CANCEL_TIME : CROSS_SECTOR_CANCEL_TIME, TIMER_STOPPABLE) + send_cross_comms_message_timer = addtimer(CALLBACK(src, PROC_REF(send_cross_comms_message), usr, destination, message), GLOB.communications_controller.soft_filtering ? EXTENDED_CROSS_SECTOR_CANCEL_TIME : CROSS_SECTOR_CANCEL_TIME, TIMER_STOPPABLE) COOLDOWN_START(src, important_action_cooldown, IMPORTANT_ACTION_COOLDOWN) if ("setState") @@ -500,7 +500,7 @@ var/network_name = CONFIG_GET(string/cross_comms_network) if(network_name) payload["network"] = network_name - if(SScommunications.soft_filtering) + if(GLOB.communications_controller.soft_filtering) payload["is_filtered"] = TRUE send2otherserver(html_decode(station_name()), message, "Comms_Console", destination == "all" ? null : list(destination), additional_data = payload) @@ -508,7 +508,7 @@ usr.log_talk(message, LOG_SAY, tag = "message to the other server") message_admins("[ADMIN_LOOKUPFLW(usr)] has sent a message to the other server\[s].") deadchat_broadcast(" has sent an outgoing message to the other station(s).", "[usr.real_name]", usr, message_type = DEADCHAT_ANNOUNCEMENT) - SScommunications.soft_filtering = FALSE // set it to false at the end of the proc to ensure that everything prior reads as intended + GLOB.communications_controller.soft_filtering = FALSE // set it to false at the end of the proc to ensure that everything prior reads as intended /obj/machinery/computer/communications/ui_data(mob/user) var/list/data = list( @@ -670,7 +670,7 @@ return deltimer(send_cross_comms_message_timer) - SScommunications.soft_filtering = FALSE + GLOB.communications_controller.soft_filtering = FALSE send_cross_comms_message_timer = null log_admin("[key_name(usr)] has cancelled the outgoing cross-comms message.") @@ -743,7 +743,7 @@ /obj/machinery/computer/communications/proc/make_announcement(mob/living/user) var/is_ai = issilicon(user) - if(!SScommunications.can_announce(user, is_ai)) + if(!GLOB.communications_controller.can_announce(user, is_ai)) to_chat(user, span_alert("Intercomms recharging. Please stand by.")) return var/input = tgui_input_text(user, "Message to announce to the station crew", "Announcement") @@ -771,7 +771,7 @@ input += "\n\n - [authorize_job] [authorize_name]" var/list/players = get_communication_players() - SScommunications.make_announcement(user, is_ai, input, syndicate || (obj_flags & EMAGGED), players) + GLOB.communications_controller.make_announcement(user, is_ai, input, syndicate || (obj_flags & EMAGGED), players) deadchat_broadcast(" made a priority announcement from [span_name("[get_area_name(usr, TRUE)]")].", span_name("[user.real_name]"), user, message_type=DEADCHAT_ANNOUNCEMENT) /obj/machinery/computer/communications/proc/get_communication_players() @@ -815,7 +815,6 @@ #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 @@ -859,7 +858,7 @@ */ /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) @@ -874,11 +873,6 @@ 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) @@ -886,59 +880,29 @@ 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, - ) - 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", + /datum/dynamic_ruleset/midround/pirates/heavy, ) - 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", ) + SSdynamic.force_run_midround(/datum/dynamic_ruleset/midround/from_ghosts/fugitives) - force_event_after(/datum/round_event_control/fugitives, "[hacker] hacking a communications console", rand(20 SECONDS, 1 MINUTES)) - - if(HACK_THREAT) // Force an unfavorable situation on the crew + if(HACK_SLEEPER) // Trigger one or multiple sleeper agents with the crew (or for latejoining 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.", + "Attention crew, it appears that someone on your station has hijacked your telecommunications and broadcasted an unknown signal.", "[command_name()] High-Priority Update", ) - - for(var/mob/crew_member as anything in GLOB.player_list) - if(!is_station_level(crew_member.z)) - continue - shake_camera(crew_member, 15, 1) - - SSdynamic.unfavorable_situation() - - if(HACK_SLEEPER) // Trigger one or multiple sleeper agents with the crew (or for latejoining crew) - 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", - ) + 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/computer/crew.dm b/code/game/machinery/computer/crew.dm index 0dcf15a4a8c8..c53bff0b7380 100644 --- a/code/game/machinery/computer/crew.dm +++ b/code/game/machinery/computer/crew.dm @@ -166,7 +166,7 @@ GLOBAL_DATUM_INIT(crewmonitor, /datum/crewmonitor, new) SIGNAL_HANDLER for(var/datum/job/jobtype as anything in subtypesof(/datum/job)) - var/datum/job/job = SSjob.GetJobType(jobtype) + var/datum/job/job = SSjob.get_job_type(jobtype) if(isnull(job)) continue var/job_prio = isnum(job.crewmonitor_priority) ? job.crewmonitor_priority : jobs[job.title] diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm index 7fa0f24d628b..e51059657126 100644 --- a/code/game/machinery/porta_turret/portable_turret.dm +++ b/code/game/machinery/porta_turret/portable_turret.dm @@ -573,7 +573,7 @@ DEFINE_BITFIELD(turret_flags, list( // If we aren't shooting heads then return a threatcount of 0 if (!(turret_flags & TURRET_FLAG_SHOOT_HEADS)) - var/datum/job/apparent_job = SSjob.GetJob(perp.get_assignment()) + var/datum/job/apparent_job = SSjob.get_job(perp.get_assignment()) if(apparent_job?.job_flags & JOB_HEAD_OF_STAFF) return 0 diff --git a/code/game/machinery/wishgranter.dm b/code/game/machinery/wishgranter.dm index 7d03fc8efaeb..6bfbb57d4116 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_ectoplasm.dm b/code/game/objects/effects/anomalies/anomalies_ectoplasm.dm index 29a1d9b3009b..51a033f515f9 100644 --- a/code/game/objects/effects/anomalies/anomalies_ectoplasm.dm +++ b/code/game/objects/effects/anomalies/anomalies_ectoplasm.dm @@ -178,7 +178,7 @@ candidate_list += GLOB.current_observers_list candidate_list += GLOB.dead_player_list - var/list/candidates = SSpolling.poll_candidates("Would you like to participate in a spooky ghost swarm? (Warning: you will not be able to return to your body!)", check_jobban = ROLE_SENTIENCE, poll_time = 10 SECONDS, group = candidate_list, pic_source = src, role_name_text = "ghost swarm") + var/list/candidates = SSpolling.poll_candidates("Would you like to participate in a spooky ghost swarm? (Warning: you will not be able to return to your body!)", check_jobban = ROLE_SENTIENCE, poll_time = 10 SECONDS, group = candidate_list, alert_pic = src, role_name_text = "ghost swarm") for(var/mob/dead/observer/candidate_ghost as anything in candidates) var/mob/living/basic/ghost/swarm/new_ghost = new(get_turf(src)) ghosts_spawned += new_ghost diff --git a/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm b/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm index 87221eeec9fa..b814d914b657 100644 --- a/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm +++ b/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm @@ -6,6 +6,8 @@ /// How many seconds between each gas release var/releasedelay = 10 aSignal = /obj/item/assembly/signaler/anomaly/pyro + /// Delay before spawning a slime + var/poll_time = 5 SECONDS /obj/effect/anomaly/pyro/Initialize(mapload, new_lifespan, drops_core) . = ..() @@ -39,13 +41,11 @@ var/datum/action/innate/slime/reproduce/repro_action = new repro_action.Grant(pyro) - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as a pyroclastic anomaly slime?", check_jobban = ROLE_SENTIENCE, poll_time = 10 SECONDS, target_mob = pyro, ignore_category = POLL_IGNORE_PYROSLIME, pic_source = pyro, role_name_text = "pyroclastic anomaly slime") - if(!LAZYLEN(candidates)) + var/mob/dead/observer/chosen = SSpolling.poll_ghosts_for_target(check_jobban = ROLE_SENTIENCE, poll_time = poll_time, checked_target = src, ignore_category = POLL_IGNORE_PYROSLIME, alert_pic = src, role_name_text = "pyroclastic anomaly slime") + if(isnull(chosen)) return - var/mob/dead/observer/chosen = pick(candidates) pyro.key = chosen.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) diff --git a/code/game/objects/effects/poster_motivational.dm b/code/game/objects/effects/poster_motivational.dm index 1d5fa6b6ad2c..0e9444190af2 100644 --- a/code/game/objects/effects/poster_motivational.dm +++ b/code/game/objects/effects/poster_motivational.dm @@ -17,8 +17,8 @@ department_grab.quirk_poster_department = quirk_poster_department /// 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, params) - if(!is_special_character(user) || !HAS_TRAIT(user, TRAIT_POSTERBOY) || !istype(postertool, /obj/item/toy/crayon)) +/obj/item/poster/quirk/attackby(obj/item/postertool, mob/user, list/modifiers) + 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/effects/spawners/xeno_egg_delivery.dm b/code/game/objects/effects/spawners/xeno_egg_delivery.dm index b09a37f14b12..eb5bb62df5c5 100644 --- a/code/game/objects/effects/spawners/xeno_egg_delivery.dm +++ b/code/game/objects/effects/spawners/xeno_egg_delivery.dm @@ -25,5 +25,5 @@ /obj/structure/alien/egg/delivery/Initialize(mapload) . = ..() - SScommunications.xenomorph_egg_delivered = TRUE - SScommunications.captivity_area = get_area(src) + GLOB.communications_controller.xenomorph_egg_delivered = TRUE + GLOB.communications_controller.captivity_area = get_area(src) diff --git a/code/game/objects/items/cards_ids.dm b/code/game/objects/items/cards_ids.dm index d1d4856823b6..e523fe8350b0 100644 --- a/code/game/objects/items/cards_ids.dm +++ b/code/game/objects/items/cards_ids.dm @@ -133,7 +133,7 @@ /obj/item/card/id/Initialize(mapload) . = ..() - var/datum/bank_account/blank_bank_account = new("Unassigned", SSjob.GetJobType(/datum/job/unassigned), player_account = FALSE) + var/datum/bank_account/blank_bank_account = new("Unassigned", SSjob.get_job_type(/datum/job/unassigned), player_account = FALSE) registered_account = blank_bank_account registered_account.replaceable = TRUE @@ -1302,7 +1302,7 @@ registered_name = "Emergency Response Intern" trim = /datum/id_trim/centcom/ert -/obj/item/card/id/advanced/centcom/ert +/obj/item/card/id/advanced/centcom/ert/commander registered_name = JOB_ERT_COMMANDER trim = /datum/id_trim/centcom/ert/commander @@ -1398,7 +1398,7 @@ /obj/item/card/id/advanced/debug/Initialize(mapload) . = ..() registered_account = SSeconomy.get_dep_account(ACCOUNT_CAR) - registered_account.account_job = SSjob.GetJobType(/datum/job/admin) // so we can actually use this account without being filtered as a "departmental" card + registered_account.account_job = SSjob.get_job_type(/datum/job/admin) // so we can actually use this account without being filtered as a "departmental" card /obj/item/card/id/advanced/prisoner name = "prisoner ID card" diff --git a/code/game/objects/items/devices/aicard_evil.dm b/code/game/objects/items/devices/aicard_evil.dm index 2e8e23ec5fee..5bfff8bfcf61 100644 --- a/code/game/objects/items/devices/aicard_evil.dm +++ b/code/game/objects/items/devices/aicard_evil.dm @@ -55,7 +55,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.set_hologram_appearance(mutable_appearance('icons/mob/silicon/ai.dmi', "xeno_queen")) //good enough diff --git a/code/game/objects/items/dice.dm b/code/game/objects/items/dice.dm index 66f29dfb7906..cdaa2564970e 100644 --- a/code/game/objects/items/dice.dm +++ b/code/game/objects/items/dice.dm @@ -428,11 +428,19 @@ var/mob/living/carbon/human/human_servant = new(drop_location()) do_smoke(0, holder = src, location = drop_location()) - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as [user.real_name]'s Servant?", check_jobban = ROLE_WIZARD, role = ROLE_WIZARD, poll_time = 5 SECONDS, target_mob = human_servant, pic_source = user, role_name_text = "dice servant") - if(LAZYLEN(candidates)) - var/mob/dead/observer/candidate = pick(candidates) - message_admins("[ADMIN_LOOKUPFLW(candidate)] was spawned as Dice Servant") - human_servant.key = candidate.key + var/mob/dead/observer/chosen_one = SSpolling.poll_ghosts_for_target( + "Do you want to play as [user.real_name]'s Servant?", + check_jobban = ROLE_WIZARD, + role = ROLE_WIZARD, + poll_time = 5 SECONDS, + checked_target = human_servant, + jump_target = human_servant, + alert_pic = user, + role_name_text = "dice servant", + ) + if(!isnull(chosen_one)) + message_admins("[ADMIN_LOOKUPFLW(chosen_one)] was spawned as Dice Servant") + human_servant.key = chosen_one.key human_servant.equipOutfit(/datum/outfit/butler) var/datum/mind/servant_mind = new /datum/mind() diff --git a/code/game/objects/items/dna_probe.dm b/code/game/objects/items/dna_probe.dm index b25a3158ded2..6df7a3bcfd34 100644 --- a/code/game/objects/items/dna_probe.dm +++ b/code/game/objects/items/dna_probe.dm @@ -146,51 +146,6 @@ return TRUE return FALSE -#define CARP_MIX_DNA_TIMER (15 SECONDS) - -///Used for scanning carps, and then turning yourself into one. -/obj/item/dna_probe/carp_scanner - name = "Carp DNA Sampler" - desc = "Can be used to take chemical and genetic samples of animals." - ///Whether we have Carp DNA - var/carp_dna_loaded = FALSE - -/obj/item/dna_probe/carp_scanner/examine_more(mob/user) - . = ..() - if(IS_TRAITOR(user)) - . = list(span_notice("Using this on a Space Carp will harvest its DNA. Use it in-hand once complete to mutate it with yourself.")) - -/obj/item/dna_probe/carp_scanner/scan_dna(atom/target, mob/user) - if(istype(target, /mob/living/basic/carp)) - carp_dna_loaded = TRUE - playsound(src, 'sound/misc/compiler-stage2.ogg', 50) - balloon_alert(user, "dna scanned") - else - return ..() - -/obj/item/dna_probe/carp_scanner/valid_scan_target(atom/target) - if (istype(target, /mob/living/basic/carp)) - return TRUE - return ..() - -/obj/item/dna_probe/carp_scanner/attack_self(mob/user, modifiers) - . = ..() - if(!is_special_character(user)) - return - if(!carp_dna_loaded) - to_chat(user, span_notice("Space carp DNA is required to use the self-mutation mechanism!")) - return - to_chat(user, span_notice("You pull out the needle from [src] and flip the switch, and start injecting yourself with it.")) - if(!do_after(user, CARP_MIX_DNA_TIMER)) - return - var/mob/living/basic/space_dragon/new_dragon = user.change_mob_type(/mob/living/basic/space_dragon, location = loc, delete_old_mob = TRUE) - new_dragon.add_filter("anger_glow", 3, list("type" = "outline", "color" = COLOR_CARP_RIFT_RED, "size" = 5)) - new_dragon.add_movespeed_modifier(/datum/movespeed_modifier/dragon_rage) - priority_announce("A large organic energy flux has been recorded near of [station_name()], please stand-by.", "Lifesign Alert") - qdel(src) - -#undef CARP_MIX_DNA_TIMER - #undef DNA_PROBE_SCAN_PLANTS #undef DNA_PROBE_SCAN_ANIMALS #undef DNA_PROBE_SCAN_HUMANS diff --git a/code/game/objects/items/religion.dm b/code/game/objects/items/religion.dm index 40d17f415b2b..60f83e29f357 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 93c94033cb2a..be5d494e39a4 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/voice/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/stacks/telecrystal.dm b/code/game/objects/items/stacks/telecrystal.dm index b62e0503de78..0429354d6c04 100644 --- a/code/game/objects/items/stacks/telecrystal.dm +++ b/code/game/objects/items/stacks/telecrystal.dm @@ -23,7 +23,7 @@ var/datum/component/uplink/hidden_uplink = uplink.GetComponent(/datum/component/uplink) if(hidden_uplink) - hidden_uplink.add_telecrystals(amount) + hidden_uplink.uplink_handler.add_telecrystals(amount) use(amount) to_chat(user, span_notice("You press [src] onto yourself and charge your hidden uplink.")) return ITEM_INTERACT_SUCCESS diff --git a/code/game/objects/items/storage/boxes/clothes_boxes.dm b/code/game/objects/items/storage/boxes/clothes_boxes.dm index a136f4664173..c1a2cee3010a 100644 --- a/code/game/objects/items/storage/boxes/clothes_boxes.dm +++ b/code/game/objects/items/storage/boxes/clothes_boxes.dm @@ -47,11 +47,6 @@ new /obj/item/clothing/head/syndicatefake(src) new /obj/item/clothing/suit/syndicatefake(src) -/obj/item/storage/box/syndie_kit/space_dragon/PopulateContents() - new /obj/item/dna_probe/carp_scanner(src) - new /obj/item/clothing/suit/hooded/carp_costume/spaceproof/old(src) - new /obj/item/clothing/mask/gas/carp(src) - /obj/item/storage/box/deputy name = "box of deputy armbands" desc = "To be issued to those authorized to act as deputy of security." diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm index 7d4d834fea9f..bc950cd5f229 100644 --- a/code/game/objects/items/storage/uplink_kits.dm +++ b/code/game/objects/items/storage/uplink_kits.dm @@ -757,13 +757,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/game/objects/structures/syndicate_uplink_beacon.dm b/code/game/objects/structures/syndicate_uplink_beacon.dm deleted file mode 100644 index abeb7e0d0808..000000000000 --- a/code/game/objects/structures/syndicate_uplink_beacon.dm +++ /dev/null @@ -1,114 +0,0 @@ -/// Device that traitors can craft in order to be sent a new, undisguised uplink -/obj/structure/syndicate_uplink_beacon - name = "suspicious beacon" - icon = 'icons/obj/machines/telecomms.dmi' - icon_state = "relay_traitor" - desc = "This ramshackle device seems capable of recieving and sending signals for some nefarious purpose." - density = TRUE - anchored = TRUE - /// Traitor's code that they speak into the radio - var/uplink_code = "" - /// weakref to person who is going to use the beacon to get a replacement uplink - var/datum/weakref/owner - /// while constructed the teleport beacon is still active - var/obj/item/beacon/teleport_beacon - /// Radio that the device needs to listen to the codeword from the traitor - var/obj/item/radio/listening_radio - /// prevents traitor from activating teleport_beacon proc too much in a small period of time - COOLDOWN_DECLARE(beacon_cooldown) - -/obj/structure/syndicate_uplink_beacon/Initialize(mapload) - . = ..() - register_context() - - var/static/list/tool_behaviors = list( - TOOL_SCREWDRIVER = list( - SCREENTIP_CONTEXT_RMB = "Deconstruct", - ), - ) - AddElement(/datum/element/contextual_screentip_tools, tool_behaviors) - listening_radio = new(src) - listening_radio.canhear_range = 0 - teleport_beacon = new(src) - -/obj/structure/syndicate_uplink_beacon/attack_hand(mob/living/user, list/modifiers) - if(!IS_TRAITOR(user)) - balloon_alert(user, "don't know how to use!") - return - if(IS_WEAKREF_OF(owner, user)) - balloon_alert(user, "already synchronized to you!") - return - if(owner != null) - balloon_alert(user, "already claimed!") - return - var/datum/looping_sound/typing/typing_sounds = new(src, start_immediately = TRUE) - balloon_alert(user, "synchronizing...") - if(!do_after(user = user, delay = 3 SECONDS, target = src, interaction_key = REF(src), hidden = TRUE)) - typing_sounds.stop() - return - typing_sounds.stop() - probe_traitor(user) - -/obj/structure/syndicate_uplink_beacon/screwdriver_act_secondary(mob/living/user, obj/item/tool) - tool.play_tool_sound(src) - balloon_alert(user, "deconstructing...") - if (!do_after(user, 5 SECONDS, target = src, hidden = TRUE)) - return FALSE - var/turf/beacon_tile = get_turf(src) - new /obj/item/stack/sheet/iron/five(beacon_tile) - new /obj/item/stack/cable_coil/five(beacon_tile) - teleport_beacon.forceMove(beacon_tile) - teleport_beacon = null - qdel(src) - return TRUE - -/obj/structure/syndicate_uplink_beacon/Destroy() - QDEL_NULL(listening_radio) - if(teleport_beacon) - QDEL_NULL(teleport_beacon) - return ..() - -/// Proc reads the user, sets radio to the correct frequency and starts to listen for the replacement uplink code -/obj/structure/syndicate_uplink_beacon/proc/probe_traitor(mob/living/user) - owner = WEAKREF(user) - var/datum/antagonist/traitor/traitor_datum = user.mind.has_antag_datum(/datum/antagonist/traitor) - - uplink_code = traitor_datum.replacement_uplink_code - listening_radio.set_frequency(traitor_datum.replacement_uplink_frequency) - become_hearing_sensitive() - -/obj/structure/syndicate_uplink_beacon/Hear(atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods, message_range) - if(ismob(speaker) || radio_freq != listening_radio.get_frequency()) - return - if(!findtext(raw_message, uplink_code)) - return - teleport_uplink() - -/// Proc uses owners uplink handler to create replacement uplink and then lock or destroy their other uplinks -/obj/structure/syndicate_uplink_beacon/proc/teleport_uplink() - if(!COOLDOWN_FINISHED(src, beacon_cooldown)) - return - COOLDOWN_START(src, beacon_cooldown, 10 MINUTES) - - var/mob/living/resolved_owner = owner.resolve() - if(isnull(resolved_owner)) - return - - var/datum/antagonist/traitor/traitor_datum = resolved_owner.mind.has_antag_datum(/datum/antagonist/traitor) - if(isnull(traitor_datum)) - return - - var/datum/uplink_handler/uplink_handler = traitor_datum.uplink_handler - - SEND_SIGNAL(uplink_handler, COMSIG_UPLINK_HANDLER_REPLACEMENT_ORDERED) - new /obj/item/uplink/replacement(get_turf(src), /*owner = */resolved_owner, /*tc_amount = */0, /*uplink_handler_override = */uplink_handler) - flick("relay_traitor_activate", src) - do_sparks(number = 5, cardinal_only = FALSE, source = src) - log_traitor("[key_name(resolved_owner)] acquired a replacement uplink via the syndicate uplink beacon.") - -// Adds screentips -/obj/structure/syndicate_uplink_beacon/add_context(atom/source, list/context, obj/item/held_item, mob/user) - if(held_item || !IS_TRAITOR(user)) - return NONE - context[SCREENTIP_CONTEXT_LMB] = "Synchronize with beacon" - return CONTEXTUAL_SCREENTIP_SET diff --git a/code/game/say.dm b/code/game/say.dm index 8b0601c9c550..d157ed720dba 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -320,7 +320,7 @@ GLOBAL_LIST_INIT(freqtospan, list( //HACKY VIRTUALSPEAKER STUFF BEYOND THIS POINT //these exist mostly to deal with the AIs hrefs and job stuff. -/atom/movable/proc/GetJob() //Get a job, you lazy butte +/atom/movable/proc/get_job() //Get a job, you lazy butte /atom/movable/proc/GetSource() @@ -367,7 +367,7 @@ INITIALIZE_IMMEDIATE(/atom/movable/virtualspeaker) else // Unidentifiable mob job = "Unknown" -/atom/movable/virtualspeaker/GetJob() +/atom/movable/virtualspeaker/get_job() return job /atom/movable/virtualspeaker/GetSource() diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 571e23d3a6b8..0ab6120f5526 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -20,19 +20,9 @@ return var/dat - if(SSticker.current_state <= GAME_STATE_PREGAME) - dat += "(Manage Dynamic Rulesets)
" - dat += "(Force Roundstart Rulesets)
" - 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)
" - dat += "
" + + dat += "Dynamic Panel
" + dat += "
" dat += {" Create Object
Quick Create Object
@@ -105,134 +95,6 @@ ADMIN_VERB(spawn_cargo, R_SPAWN, "Spawn Cargo", "Spawn a cargo crate.", ADMIN_CA log_admin("[key_name(user)] spawned cargo pack [chosen] at [AREACOORD(user.mob)]") 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) @@ -292,14 +154,16 @@ ADMIN_VERB(create_or_modify_area, R_DEBUG, "Create Or Modify Area", "Create of m return TRUE -/client/proc/adminGreet(logout) - if(SSticker.HasRoundStarted()) - var/string - if(logout && CONFIG_GET(flag/announce_admin_logout)) - string = pick( - "Admin logout: [key_name(src)]") - else if(!logout && CONFIG_GET(flag/announce_admin_login) && (prefs.toggles & ANNOUNCE_LOGIN)) - string = pick( - "Admin login: [key_name(src)]") - if(string) - message_admins("[string]") +/// Sends a message to adminchat when anyone with a holder logs in or logs out. +/// Is dependent on admin preferences and configuration settings, which means that this proc can fire without sending a message. +/client/proc/adminGreet(logout = FALSE) + if(!SSticker.HasRoundStarted()) + return + + if(logout && CONFIG_GET(flag/announce_admin_logout)) + message_admins("Admin logout: [key_name(src)]") + return + + 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/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index f772e562da22..2f390ea4c328 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -521,7 +521,7 @@ ADMIN_VERB(spawn_debug_full_crew, R_DEBUG, "Spawn Debug Full Crew", "Creates a f // Then, spawn a human and slap a person into it. var/number_made = 0 for(var/rank in SSjob.name_occupations) - var/datum/job/job = SSjob.GetJob(rank) + var/datum/job/job = SSjob.get_job(rank) // JOB_CREW_MEMBER is all jobs that pretty much aren't silicon if(!(job.job_flags & JOB_CREW_MEMBER)) @@ -533,7 +533,7 @@ ADMIN_VERB(spawn_debug_full_crew, R_DEBUG, "Spawn Debug Full Crew", "Creates a f new_guy.mind.name = "[rank] Dummy" // Assign the rank to the new player dummy. - if(!SSjob.AssignRole(new_guy, job, do_eligibility_checks = FALSE)) + if(!SSjob.assign_role(new_guy, job, do_eligibility_checks = FALSE)) qdel(new_guy) to_chat(user, "[rank] wasn't able to be spawned.") continue @@ -545,7 +545,7 @@ ADMIN_VERB(spawn_debug_full_crew, R_DEBUG, "Spawn Debug Full Crew", "Creates a f qdel(new_guy) // Then equip up the human with job gear. - SSjob.EquipRank(character, job) + SSjob.equip_rank(character, job) job.after_latejoin_spawn(character) // Finally, ensure the minds are tracked and in the manifest. diff --git a/code/modules/admin/antag_panel.dm b/code/modules/admin/antag_panel.dm index b1ce37fdec26..5862cab8ac6d 100644 --- a/code/modules/admin/antag_panel.dm +++ b/code/modules/admin/antag_panel.dm @@ -74,18 +74,29 @@ GLOBAL_VAR(antag_prototypes) break 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) result += "No body!" if(current && HAS_TRAIT(current, TRAIT_MINDSHIELD)) - result += "Mindshielded" - //Move these to mob + result += span_green("Mindshielded") + // if(current && HAS_MIND_TRAIT(current, TRAIT_UNCONVERTABLE)) + // result += span_green("Unconvertable") + 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 += "Emagged" - return result.Join(" | ") + roles += "Emagged" + return roles /datum/mind/proc/traitor_panel() if(!SSticker.HasRoundStarted()) @@ -98,12 +109,11 @@ GLOBAL_VAR(antag_prototypes) var/out = "[name][(current && (current.real_name != name))?" (as [current.real_name])":""]
" out += "Mind currently owned by key: [key] [active?"(synced)":"(not synced)"]
" 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() @@ -165,7 +175,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 @@ -193,14 +203,10 @@ GLOBAL_VAR(antag_prototypes) var/uplink_info = "Uplink:" var/datum/component/uplink/U = find_syndicate_uplink() if(U) - if(!U.uplink_handler.has_objectives) - uplink_info += "take" if (check_rights(R_FUN, 0)) uplink_info += ", [U.uplink_handler.telecrystals] TC" if(U.uplink_handler.has_progression) uplink_info += ", [U.uplink_handler.progression_points] PR" - if(U.uplink_handler.has_objectives) - uplink_info += ", Force Give Objective" else uplink_info += ", [U.uplink_handler.telecrystals] TC" if(U.uplink_handler.has_progression) diff --git a/code/modules/admin/check_antagonists.dm b/code/modules/admin/check_antagonists.dm index 30071504c919..2931bdfad682 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/fun_balloon.dm b/code/modules/admin/fun_balloon.dm index 4d74f5425608..b65f72f8f4d7 100644 --- a/code/modules/admin/fun_balloon.dm +++ b/code/modules/admin/fun_balloon.dm @@ -87,14 +87,14 @@ if (!possessable.ckey && possessable.stat == CONSCIOUS) // Only assign ghosts to living, non-occupied mobs! bodies += possessable - var/list/candidates = SSpolling.poll_ghost_candidates_for_mobs( - question = "Would you like to be [group_name]?", + var/list/candidates = SSpolling.poll_ghosts_for_targets( + question = "Would you like to be [span_notice(group_name)]?", role = ROLE_SENTIENCE, check_jobban = ROLE_SENTIENCE, poll_time = 10 SECONDS, - mobs = bodies, + checked_targets = bodies, ignore_category = POLL_IGNORE_SHUTTLE_DENIZENS, - pic_source = src, + alert_pic = src, role_name_text = "sentience fun balloon", ) diff --git a/code/modules/admin/player_panel.dm b/code/modules/admin/player_panel.dm index a56fe9daf8da..00f93ac84bc5 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 d282ea47307e..3bf4c22df318 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -67,9 +67,7 @@ edit_rights_topic(href_list) else if(href_list["gamemode_panel"]) - if(!check_rights(R_ADMIN)) - return - SSdynamic.admin_panel() + dynamic_panel(usr) else if(href_list["call_shuttle"]) if(!check_rights(R_ADMIN)) @@ -388,122 +386,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 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 @@ -1600,12 +1482,6 @@ else return - else if(href_list["force_war"]) - if(!check_rights(R_ADMIN)) - return - var/obj/item/nuclear_challenge/button = locate(href_list["force_war"]) - button.force_war() - else if(href_list["give_reinforcement"]) var/datum/team/nuclear/nuketeam = locate(href_list["give_reinforcement"]) in GLOB.antagonist_teams nuketeam.admin_spawn_reinforcement(usr) diff --git a/code/modules/admin/verbs/admin.dm b/code/modules/admin/verbs/admin.dm index 6e0c7f68e563..629423e713fa 100644 --- a/code/modules/admin/verbs/admin.dm +++ b/code/modules/admin/verbs/admin.dm @@ -32,7 +32,7 @@ ADMIN_VERB(unprison, R_ADMIN, "UnPrison", ADMIN_VERB_NO_DESCRIPTION, ADMIN_CATEG tgui_alert(user, "[prisoner.name] is not prisoned.") return - SSjob.SendToLateJoin(prisoner) + SSjob.send_to_late_join(prisoner) message_admins("[key_name_admin(user)] has unprisoned [key_name_admin(prisoner)]") log_admin("[key_name(user)] has unprisoned [key_name(prisoner)]") BLACKBOX_LOG_ADMIN_VERB("Unprison") diff --git a/code/modules/admin/verbs/adminevents.dm b/code/modules/admin/verbs/adminevents.dm index f947f3e118b1..c21aaccc202c 100644 --- a/code/modules/admin/verbs/adminevents.dm +++ b/code/modules/admin/verbs/adminevents.dm @@ -261,23 +261,31 @@ ADMIN_VERB(run_weather, R_FUN, "Run Weather", "Triggers specific weather on the log_admin("[key_name(user)] started weather of type [weather_type] on the z-level [z_level].") BLACKBOX_LOG_ADMIN_VERB("Run Weather") -ADMIN_VERB(command_report_footnote, R_ADMIN, "Command Report Footnote", "Adds a footnote to the roundstart command report.", ADMIN_CATEGORY_EVENTS) +ADMIN_VERB(command_report_footnote, R_FUN, "Command Report Footnote", "Adds a footnote to the roundstart command report.", ADMIN_CATEGORY_EVENTS) var/datum/command_footnote/command_report_footnote = new /datum/command_footnote() - SScommunications.block_command_report += 1 //Add a blocking condition to the counter until the inputs are done. + GLOB.communications_controller.block_command_report += 1 //Add a blocking condition to the counter until the inputs are done. - command_report_footnote.message = tgui_input_text(user, "This message will be attached to the bottom of the roundstart threat report. Be sure to delay the roundstart report if you need extra time.", "P.S.") + command_report_footnote.message = tgui_input_text( + user, + "This message will be attached to the bottom of the roundstart threat report.", + "P.S.", + ) if(!command_report_footnote.message) - SScommunications.block_command_report -= 1 + GLOB.communications_controller.block_command_report -= 1 qdel(command_report_footnote) return - command_report_footnote.signature = tgui_input_text(user, "Whose signature will appear on this footnote?", "Also sign here, here, aaand here.") + command_report_footnote.signature = tgui_input_text( + user, + "Whose signature will appear on this footnote?", + "Also sign here, here, aaand here.", + ) if(!command_report_footnote.signature) command_report_footnote.signature = "Classified" - SScommunications.command_report_footnotes += command_report_footnote - SScommunications.block_command_report-- + GLOB.communications_controller.command_report_footnotes += command_report_footnote + GLOB.communications_controller.block_command_report -= 1 message_admins("[user] has added a footnote to the command report: [command_report_footnote.message], signed [command_report_footnote.signature]") @@ -285,6 +293,16 @@ ADMIN_VERB(command_report_footnote, R_ADMIN, "Command Report Footnote", "Adds a var/message var/signature +ADMIN_VERB(command_report_content, R_FUN, "Command Report Content", "Sets the main content of the roundstart command report.", ADMIN_CATEGORY_EVENTS) + GLOB.communications_controller.block_command_report += 1 + GLOB.communications_controller.command_report_main_content = tgui_input_text( + user, + "This message will be the main content of the roundstart command report, above the threat report (if enabled).", + "To Whom It May Concern", + ) + GLOB.communications_controller.block_command_report -= 1 + message_admins("[key_name_admin(user)] has [GLOB.communications_controller.command_report_main_content ? "set" : "cleared"] the main content of the roundstart command report.") + ADMIN_VERB(delay_command_report, R_FUN, "Delay Command Report", "Prevents the roundstart command report from being sent; or forces it to send it delayed.", ADMIN_CATEGORY_EVENTS) - SScommunications.block_command_report = !SScommunications.block_command_report - message_admins("[key_name_admin(user)] has [(SScommunications.block_command_report ? "delayed" : "sent")] the roundstart command report.") + GLOB.communications_controller.block_command_report = !GLOB.communications_controller.block_command_report + message_admins("[key_name_admin(user)] has [(GLOB.communications_controller.block_command_report ? "delayed" : "sent")] the roundstart command report.") diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm index 104b6fc1874d..10e53400c3b4 100644 --- a/code/modules/admin/verbs/admingame.dm +++ b/code/modules/admin/verbs/admingame.dm @@ -190,7 +190,7 @@ ADMIN_VERB(respawn_character, R_ADMIN, "Respawn Character", "Respawn a player th if(findtext(G_found.real_name,"monkey")) if(tgui_alert(user,"This character appears to have been a monkey. Would you like to respawn them as such?",,list("Yes","No")) == "Yes") var/mob/living/carbon/human/species/monkey/new_monkey = new - SSjob.SendToLateJoin(new_monkey) + SSjob.send_to_late_join(new_monkey) G_found.mind.transfer_to(new_monkey) //be careful when doing stuff like this! I've already checked the mind isn't in use new_monkey.key = G_found.key to_chat(new_monkey, "You have been fully respawned. Enjoy the game.", confidential = TRUE) @@ -202,7 +202,7 @@ ADMIN_VERB(respawn_character, R_ADMIN, "Respawn Character", "Respawn a player th //Ok, it's not a monkey. So, spawn a human. var/mob/living/carbon/human/new_character = new//The mob being spawned. - SSjob.SendToLateJoin(new_character) + SSjob.send_to_late_join(new_character) var/datum/record/locked/record_found //Referenced to later to either randomize or not randomize the character. if(G_found.mind && !G_found.mind.active) //mind isn't currently in use by someone/something @@ -225,7 +225,7 @@ ADMIN_VERB(respawn_character, R_ADMIN, "Respawn Character", "Respawn a player th else new_character.mind_initialize() if(is_unassigned_job(new_character.mind.assigned_role)) - new_character.mind.set_assigned_role(SSjob.GetJobType(SSjob.overflow_role)) + new_character.mind.set_assigned_role(SSjob.get_job_type(SSjob.overflow_role)) new_character.key = G_found.key @@ -242,36 +242,17 @@ ADMIN_VERB(respawn_character, R_ADMIN, "Respawn Character", "Respawn a player th //Now for special roles and equipment. var/datum/antagonist/traitor/traitordatum = new_character.mind.has_antag_datum(/datum/antagonist/traitor) if(traitordatum) - SSjob.EquipRank(new_character, new_character.mind.assigned_role, new_character.client) + SSjob.equip_rank(new_character, new_character.mind.assigned_role, new_character.client) new_character.mind.give_uplink(silent = TRUE, antag_datum = traitordatum) - 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)) - - 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.EquipRank(new_character, new_character.mind.assigned_role, new_character.client)//Or we simply equip them. + 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 + + 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 26e46a89c70f..4ec275fc48f5 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -1017,9 +1017,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/admin/verbs/commandreport.dm b/code/modules/admin/verbs/commandreport.dm index f714d430ab00..d05206994fd2 100644 --- a/code/modules/admin/verbs/commandreport.dm +++ b/code/modules/admin/verbs/commandreport.dm @@ -147,7 +147,7 @@ ADMIN_VERB(create_command_report, R_ADMIN, "Create Command Report", "Create a co priority_announce(command_report_content, subheader == ""? null : subheader, report_sound, has_important_message = TRUE, color_override = chosen_color) if(!announce_contents || print_report) - print_command_report(command_report_content, "[announce_contents ? "" : "Classified "][command_name] Update", !announce_contents) + print_command_report(command_report_content, "[announce_contents ? "" : "Classified "][command_name] Update", !announce_contents, contains_advanced_html = TRUE) change_command_name(original_command_name) diff --git a/code/modules/admin/verbs/ert.dm b/code/modules/admin/verbs/ert.dm index 697f5953be95..0d1f33bc3e36 100644 --- a/code/modules/admin/verbs/ert.dm +++ b/code/modules/admin/verbs/ert.dm @@ -121,7 +121,7 @@ var/list/spawnpoints = GLOB.emergencyresponseteamspawn var/index = 0 - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates("Do you wish to be considered for [ertemplate.polldesc]?", check_jobban = "deathsquad", pic_source = /obj/item/card/id/advanced/centcom/ert, role_name_text = "emergency response team") + var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates("Do you wish to be considered for [span_notice(ertemplate.polldesc)]?", check_jobban = "deathsquad", alert_pic = /obj/item/card/id/advanced/centcom/ert/commander, role_name_text = "emergency response team") var/teamSpawned = FALSE // This list will take priority over spawnpoints if not empty @@ -242,7 +242,7 @@ ert_antag.random_names = ertemplate.random_names ert_operative.mind.add_antag_datum(ert_antag,ert_team) - ert_operative.mind.set_assigned_role(SSjob.GetJobType(ert_antag.ert_job_path)) + ert_operative.mind.set_assigned_role(SSjob.get_job_type(ert_antag.ert_job_path)) // future melbert todo - tie to ert jobs, make ert jobs not one datum ert_operative.mind.set_level(/datum/skill/electronics, SKILL_LEVEL_MASTER, silent = TRUE) ert_operative.mind.set_level(/datum/skill/eva, SKILL_LEVEL_MASTER, silent = TRUE) diff --git a/code/modules/admin/verbs/secrets.dm b/code/modules/admin/verbs/secrets.dm index 366e957f1bbb..9e0180b192ed 100644 --- a/code/modules/admin/verbs/secrets.dm +++ b/code/modules/admin/verbs/secrets.dm @@ -88,7 +88,7 @@ ADMIN_VERB(secrets, R_NONE, "Secrets", "Abuse harder than you ever have before w if("infinite_sec") if(!is_debugger) return - var/datum/job/sec_job = SSjob.GetJobType(/datum/job/security_officer) + var/datum/job/sec_job = SSjob.get_job_type(/datum/job/security_officer) sec_job.total_positions = -1 sec_job.spawn_positions = -1 message_admins("[key_name_admin(holder)] has removed the cap on security officers.") @@ -406,7 +406,7 @@ ADMIN_VERB(secrets, R_NONE, "Secrets", "Abuse harder than you ever have before w var/list/candidates = list() if (prefs["offerghosts"]["value"] == "Yes") - candidates = SSpolling.poll_ghost_candidates(replacetext(prefs["ghostpoll"]["value"], "%TYPE%", initial(pathToSpawn.name)), check_jobban = ROLE_TRAITOR, pic_source = pathToSpawn, role_name_text = "portal storm") + candidates = SSpolling.poll_ghost_candidates(replacetext(prefs["ghostpoll"]["value"], "%TYPE%", initial(pathToSpawn.name)), check_jobban = ROLE_TRAITOR, alert_pic = pathToSpawn, role_name_text = "portal storm") if (prefs["playersonly"]["value"] == "Yes" && length(candidates) < prefs["minplayers"]["value"]) message_admins("Not enough players signed up to create a portal storm, the minimum was [prefs["minplayers"]["value"]] and the number of signups [length(candidates)]") @@ -523,7 +523,7 @@ ADMIN_VERB(secrets, R_NONE, "Secrets", "Abuse harder than you ever have before w if(teamsize <= 0) return FALSE - candidates = SSpolling.poll_ghost_candidates("Do you wish to be considered for a Nanotrasen emergency response drone?", check_jobban = ROLE_DRONE, pic_source = /mob/living/basic/drone/classic, role_name_text = "nanotrasen emergency response drone") + candidates = SSpolling.poll_ghost_candidates("Do you wish to be considered for a Nanotrasen emergency response drone?", check_jobban = ROLE_DRONE, alert_pic = /mob/living/basic/drone/classic, role_name_text = "nanotrasen emergency response drone") if(length(candidates) == 0) return FALSE @@ -638,7 +638,6 @@ ADMIN_VERB(secrets, R_NONE, "Secrets", "Abuse harder than you ever have before w assign_admin_objective_and_antag(player, antag_datum) var/datum/uplink_handler/uplink = antag_datum.uplink_handler uplink.has_progression = FALSE - uplink.has_objectives = FALSE if(ROLE_CHANGELING) var/datum/antagonist/changeling/antag_datum = new antag_datum.give_objectives = keep_generic_objecives diff --git a/code/modules/admin/view_variables/admin_delete.dm b/code/modules/admin/view_variables/admin_delete.dm index 5ef04b351cce..1d5b621b70d5 100644 --- a/code/modules/admin/view_variables/admin_delete.dm +++ b/code/modules/admin/view_variables/admin_delete.dm @@ -20,6 +20,9 @@ var/turf/T = D T.ScrapeAway() else + if(ismob(D)) + var/mob/M = D + M.ghostize(FALSE) vv_update_display(D, "deleted", VV_MSG_DELETED) qdel(D) if(!QDELETED(D)) diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index c679fd45dfa1..be6c266f3071 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 @@ -61,6 +55,8 @@ GLOBAL_LIST_EMPTY(antagonists) var/default_custom_objective = "Cause chaos on the space station." /// Whether we give a hardcore random bonus for greentexting as this antagonist while playing hardcore random var/hardcore_random_bonus = FALSE + /// A path to the audio stinger that plays upon gaining this datum. + var/stinger_sound //ANTAG UI @@ -74,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 @@ -175,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. @@ -198,7 +188,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 @@ -266,9 +256,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) /** @@ -285,7 +280,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. @@ -293,13 +288,12 @@ GLOBAL_LIST_EMPTY(antagonists) /datum/antagonist/proc/replace_banned_player() set waitfor = FALSE - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as a [name]?", check_jobban = "[name]", role = job_rank, poll_time = 5 SECONDS, target_mob = owner.current, pic_source = owner.current, role_name_text = name) - if(LAZYLEN(candidates)) - var/mob/dead/observer/C = pick(candidates) + 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(C)] has taken control of ([key_name_admin(owner)]) to replace a jobbanned player.") + message_admins("[key_name_admin(chosen_one)] has taken control of ([key_name_admin(owner)]) to replace a jobbanned player.") owner.current.ghostize(FALSE) - owner.current.key = C.key + owner.current.key = chosen_one.key /** * Called by the remove_antag_datum() and remove_all_antag_datums() mind procs for the antag datum to handle its own removal and deletion. @@ -323,6 +317,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) // Remove HUDs that they should no longer see @@ -340,6 +335,14 @@ GLOBAL_LIST_EMPTY(antagonists) /datum/antagonist/proc/greet() if(!silent) to_chat(owner.current, span_big("You are \the [src].")) + play_stinger() + +/// Plays the antag stinger sound, if we have one +/datum/antagonist/proc/play_stinger() + if(isnull(stinger_sound)) + return + + owner.current.playsound_local(get_turf(owner.current), stinger_sound, 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE) /** * Proc that sends fluff or instructional messages to the player when they lose this antag datum. @@ -442,8 +445,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 @@ -597,3 +600,7 @@ GLOBAL_LIST_EMPTY(antagonists) owner.announce_objectives() #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 6ccaa80e55fd..de3ad9865e75 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 4998f7e198b2..b608563067a8 100644 --- a/code/modules/antagonists/_common/antag_spawner.dm +++ b/code/modules/antagonists/_common/antag_spawner.dm @@ -55,16 +55,15 @@ /obj/item/antag_spawner/contract/proc/poll_for_student(mob/living/carbon/human/teacher, apprentice_school) balloon_alert(teacher, "contacting apprentice...") polling = TRUE - var/list/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as a wizard's [apprentice_school] apprentice?", check_jobban = ROLE_WIZARD, role = ROLE_WIZARD, poll_time = 15 SECONDS, target_mob = src, pic_source = teacher, role_name_text = "wizard apprentice") + var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to play as [span_danger("[teacher]'s")] [span_notice("[apprentice_school] apprentice")]?", check_jobban = ROLE_WIZARD, role = ROLE_WIZARD, poll_time = 15 SECONDS, checked_target = src, alert_pic = /obj/item/clothing/head/wizard/red, jump_target = src, role_name_text = "wizard apprentice", chat_text_border_icon = /obj/item/clothing/head/wizard/red) polling = FALSE - if(!LAZYLEN(candidates)) + if(isnull(chosen_one)) to_chat(teacher, span_warning("Unable to reach your apprentice! You can either attack the spellbook with the contract to refund your points, or wait and try again later.")) return if(QDELETED(src) || used) return used = TRUE - var/mob/dead/observer/student = pick(candidates) - spawn_antag(student.client, get_turf(src), apprentice_school, teacher.mind) + spawn_antag(chosen_one.client, get_turf(src), apprentice_school, teacher.mind) /obj/item/antag_spawner/contract/spawn_antag(client/C, turf/T, kind, datum/mind/user) new /obj/effect/particle_effect/fluid/smoke(T) @@ -84,8 +83,7 @@ app.wiz_team = master_wizard.wiz_team master_wizard.wiz_team.add_member(app_mind) app_mind.add_antag_datum(app) - app_mind.set_assigned_role(SSjob.GetJobType(/datum/job/wizard_apprentice)) - app_mind.special_role = ROLE_WIZARD_APPRENTICE + app_mind.set_assigned_role(SSjob.get_job_type(/datum/job/wizard_apprentice)) SEND_SOUND(M, sound('sound/effects/magic.ogg')) ///////////BORGS AND OPERATIVES @@ -101,7 +99,7 @@ icon_state = "nukietalkie" var/borg_to_spawn /// 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 outfit given to plasmaman operatives @@ -134,13 +132,12 @@ return to_chat(user, span_notice("You activate [src] and wait for confirmation.")) - var/list/nuke_candidates = SSpolling.poll_ghost_candidates("Do you want to play as a syndicate [borg_to_spawn ? "[LOWER_TEXT(borg_to_spawn)] cyborg":"operative"]?", check_jobban = ROLE_OPERATIVE, role = ROLE_OPERATIVE, poll_time = 15 SECONDS, ignore_category = POLL_IGNORE_SYNDICATE, pic_source = src, role_name_text = "syndicate [borg_to_spawn ? "[borg_to_spawn] cyborg":"operative"]") - if(LAZYLEN(nuke_candidates)) + var/mob/chosen_one = SSpolling.poll_ghost_candidates("Do you want to play as a reinforcement [special_role_name]?", check_jobban = ROLE_OPERATIVE, role = ROLE_OPERATIVE, poll_time = 15 SECONDS, ignore_category = POLL_IGNORE_SYNDICATE, alert_pic = src, role_name_text = special_role_name, amount_to_pick = 1) + if(chosen_one) if(QDELETED(src) || !check_usability(user)) return used = TRUE - var/mob/dead/observer/G = pick(nuke_candidates) - spawn_antag(G.client, get_turf(src), "nukeop", user.mind) + spawn_antag(chosen_one.client, get_turf(src), "nukeop", user.mind) do_sparks(4, TRUE, src) qdel(src) else @@ -148,7 +145,6 @@ /obj/item/antag_spawner/nuke_ops/spawn_antag(client/our_client, turf/T, kind, datum/mind/user) var/mob/living/carbon/human/nukie = new() - var/obj/structure/closet/supplypod/pod = setup_pod() our_client.prefs.safe_transfer_prefs_to(nukie, is_antag = TRUE) nukie.ckey = our_client.key var/datum/mind/op_mind = nukie.mind @@ -159,12 +155,13 @@ antag_datum = new() antag_datum.send_to_spawnpoint = FALSE - antag_datum.nukeop_outfit = use_subtypes ? pick(subtypesof(outfit)) : outfit var/datum/antagonist/nukeop/creator_op = user.has_antag_datum(/datum/antagonist/nukeop, TRUE) - op_mind.add_antag_datum(antag_datum, creator_op ? creator_op.get_team() : null) - op_mind.special_role = special_role_name + op_mind.add_antag_datum(antag_datum, creator_op?.get_team()) + LAZYADD(op_mind.special_roles, special_role_name) + + var/obj/structure/closet/supplypod/pod = setup_pod() nukie.forceMove(pod) new /obj/effect/pod_landingzone(get_turf(src), pod) @@ -227,10 +224,8 @@ borg.key = C.key - var/datum/antagonist/nukeop/new_borg = new() - new_borg.send_to_spawnpoint = FALSE - borg.mind.add_antag_datum(new_borg,creator_op.nuke_team) - borg.mind.special_role = "Syndicate Cyborg" + borg.mind.add_antag_datum(antag_datum, creator_op?.get_team()) + LAZYADD(borg.mind.special_roles, special_role_name) borg.forceMove(pod) new /obj/effect/pod_landingzone(get_turf(src), pod) @@ -252,14 +247,13 @@ return if(used) return - var/list/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as a [initial(demon_type.name)]?", check_jobban = ROLE_ALIEN, role = ROLE_ALIEN, poll_time = 5 SECONDS, target_mob = src, pic_source = src, role_name_text = initial(demon_type.name)) - if(LAZYLEN(candidates)) + var/mob/chosen_one = SSpolling.poll_ghosts_for_target(check_jobban = ROLE_ALIEN, role = ROLE_ALIEN, poll_time = 5 SECONDS, checked_target = src, alert_pic = demon_type, jump_target = src, role_name_text = initial(demon_type.name)) + if(chosen_one) if(used || QDELETED(src)) return used = TRUE - var/mob/dead/observer/summoned = pick(candidates) - user.log_message("has summoned forth the [initial(demon_type.name)] (played by [key_name(summoned)]) using a [name].", LOG_GAME) // has to be here before we create antag otherwise we can't get the ckey of the demon - spawn_antag(summoned.client, get_turf(src), initial(demon_type.name), user.mind) + user.log_message("has summoned forth the [initial(demon_type.name)] (played by [key_name(chosen_one)]) using a [name].", LOG_GAME) // has to be here before we create antag otherwise we can't get the ckey of the demon + spawn_antag(chosen_one.client, get_turf(src), initial(demon_type.name), user.mind) to_chat(user, shatter_msg) to_chat(user, veil_msg) playsound(user.loc, 'sound/effects/glassbr1.ogg', 100, TRUE) @@ -332,15 +326,22 @@ return to_chat(user, span_notice("You activate [src] and wait for confirmation.")) - var/list/baddie_candidates = SSpolling.poll_ghost_candidates("Do you want to play as a [role_to_play]?", check_jobban = poll_role_check, role = poll_role_check, poll_time = 10 SECONDS, ignore_category = poll_ignore_category, pic_source = src, role_name_text = role_to_play) - if(!LAZYLEN(baddie_candidates)) + var/mob/chosen_one = SSpolling.poll_ghost_candidates( + check_jobban = poll_role_check, + role = poll_role_check, + poll_time = 10 SECONDS, + ignore_category = poll_ignore_category, + alert_pic = src, + role_name_text = role_to_play, + amount_to_pick = 1 + ) + if(isnull(chosen_one)) to_chat(user, span_warning(fail_text)) return if(QDELETED(src) || !check_usability(user)) return used = TRUE - var/mob/dead/observer/ghostie = pick(baddie_candidates) - spawn_antag(ghostie.client, get_turf(src), user) + spawn_antag(chosen_one.client, get_turf(src), user) do_sparks(4, TRUE, src) qdel(src) @@ -366,13 +367,28 @@ human_mob.set_species(species_type) 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) spawned_mob.forceMove(pod) new /obj/effect/pod_landingzone(get_turf(src), pod) +/obj/item/antag_spawner/loadout/contractor + name = "contractor support beacon" + desc = "A beacon sold to the most prestigeous syndicate members, a single-use radio for calling immediate backup." + icon = 'icons/obj/devices/voice.dmi' + icon_state = "nukietalkie" + outfit = /datum/outfit/contractor_partner + use_subtypes = FALSE + antag_datum = /datum/antagonist/traitor/contractor_support + poll_ignore_category = ROLE_TRAITOR + role_to_play = ROLE_CONTRACTOR_SUPPORT + +/obj/item/antag_spawner/loadout/contractor/do_special_things(mob/living/carbon/human/contractor_support, mob/user) + to_chat(contractor_support, "\n[span_alertwarning("[user.real_name] is your superior. Follow any, and all orders given by them. You're here to support their mission only.")]") + to_chat(contractor_support, "[span_alertwarning("Should they perish, or be otherwise unavailable, you're to assist other active agents in this mission area to the best of your ability.")]") + /obj/item/antag_spawner/loadout/monkey_man name = "monkey agent beacon" desc = "Call up some backup from ARC for monkey mayhem." @@ -417,4 +433,3 @@ internals_slot = NONE belt = /obj/item/lighter/skull r_hand = /obj/item/food/grown/banana - diff --git a/code/modules/antagonists/abductor/abductee/abductee.dm b/code/modules/antagonists/abductor/abductee/abductee.dm index f1e657a558ea..1fd5dc4c74a9 100644 --- a/code/modules/antagonists/abductor/abductee/abductee.dm +++ b/code/modules/antagonists/abductor/abductee/abductee.dm @@ -9,6 +9,7 @@ roundend_category = "abductees" antagpanel_category = ANTAG_GROUP_ABDUCTORS antag_hud_name = "abductee" + stinger_sound = 'sound/ambience/antag/abductee.ogg' /datum/antagonist/abductee/on_gain() give_objective() diff --git a/code/modules/antagonists/abductor/abductor.dm b/code/modules/antagonists/abductor/abductor.dm index df0a93b4f07c..493af969824b 100644 --- a/code/modules/antagonists/abductor/abductor.dm +++ b/code/modules/antagonists/abductor/abductor.dm @@ -2,11 +2,13 @@ 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 suicide_cry = "FOR THE MOTHERSHIP!!" // They can't even talk but y'know + stinger_sound = 'sound/ambience/antag/ayylien.ogg' + var/datum/team/abductor_team/team var/sub_role var/outfit @@ -69,15 +71,13 @@ return team /datum/antagonist/abductor/on_gain() - owner.set_assigned_role(SSjob.GetJobType(role_job)) - owner.special_role = ROLE_ABDUCTOR + owner.set_assigned_role(SSjob.get_job_type(role_job)) objectives += team.objectives finalize_abductor() ADD_TRAIT(owner, TRAIT_ABDUCTOR_TRAINING, ABDUCTOR_ANTAGONIST) return ..() /datum/antagonist/abductor/on_removal() - owner.special_role = null REMOVE_TRAIT(owner, TRAIT_ABDUCTOR_TRAINING, ABDUCTOR_ANTAGONIST) return ..() diff --git a/code/modules/antagonists/abductor/machinery/experiment.dm b/code/modules/antagonists/abductor/machinery/experiment.dm index b5254310c6b3..72bf6ae2f40a 100644 --- a/code/modules/antagonists/abductor/machinery/experiment.dm +++ b/code/modules/antagonists/abductor/machinery/experiment.dm @@ -190,7 +190,7 @@ H.forceMove(console.pad.teleport_target) return //Area not chosen / It's not safe area - teleport to arrivals - SSjob.SendToLateJoin(H, FALSE) + SSjob.send_to_late_join(H, FALSE) return /obj/machinery/abductor/experiment/update_icon_state() diff --git a/code/modules/antagonists/ashwalker/ashwalker.dm b/code/modules/antagonists/ashwalker/ashwalker.dm index 827d929b0fbb..588b1606b84c 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 8560300698f8..ec1f702a107b 100644 --- a/code/modules/antagonists/battlecruiser/battlecruiser.dm +++ b/code/modules/antagonists/battlecruiser/battlecruiser.dm @@ -19,7 +19,8 @@ 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/ambience/antag/ops.ogg' /// Team to place the crewmember on. var/datum/team/battlecruiser/battlecruiser_team @@ -38,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 07ca14586e1f..9f9ae93749fb 100644 --- a/code/modules/antagonists/blob/blob_antag.dm +++ b/code/modules/antagonists/blob/blob_antag.dm @@ -4,8 +4,9 @@ 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/ambience/antag/blobalert.ogg' /// Action to release a blob infection var/datum/action/innate/blobpop/pop_action /// Initial points for a human blob @@ -156,7 +157,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 = ..() @@ -184,5 +185,3 @@ /obj/effect/dummy/phased_mob/can_blob_attack() return FALSE - - diff --git a/code/modules/antagonists/brainwashing/brainwashing.dm b/code/modules/antagonists/brainwashing/brainwashing.dm index a689bebd9dd1..95680cac25a6 100644 --- a/code/modules/antagonists/brainwashing/brainwashing.dm +++ b/code/modules/antagonists/brainwashing/brainwashing.dm @@ -29,13 +29,14 @@ /datum/antagonist/brainwashed name = "\improper Brainwashed Victim" - job_rank = ROLE_BRAINWASHED + pref_flag = ROLE_BRAINWASHED + stinger_sound = 'sound/ambience/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 2b60dcdca89f..202b0bd4ec97 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 @@ -9,11 +9,13 @@ suicide_cry = "FOR MY BROTHER!!" antag_moodlet = /datum/mood_event/focused hardcore_random_bonus = TRUE + stinger_sound = 'sound/ambience/antag/tatoralert.ogg' VAR_PRIVATE datum/team/brother_team/team /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.") @@ -24,31 +26,44 @@ /datum/antagonist/brother/on_gain() objectives += team.objectives - owner.special_role = special_role finalize_brother() - var/is_first_brother = team.members.len == 1 - team.brothers_left -= 1 + if (team.brothers_left <= 0) + return ..() + + var/mob/living/carbon/carbon_owner = owner.current + if (!istype(carbon_owner)) + return ..() - if (is_first_brother || team.brothers_left > 0) - var/mob/living/carbon/carbon_owner = owner.current - if (istype(carbon_owner)) - carbon_owner.equip_conspicuous_item(new /obj/item/assembly/flash) - carbon_owner.AddComponentFrom(REF(src), /datum/component/can_flash_from_behind) - RegisterSignal(carbon_owner, COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON, PROC_REF(on_mob_successful_flashed_carbon)) + grant_conversion_skills() + carbon_owner.equip_conspicuous_item(new /obj/item/assembly/flash) - if (!is_first_brother) - to_chat(carbon_owner, span_boldwarning("The Syndicate have higher expectations from you than others. They have granted you an extra flash to convert one other person.")) + var/is_first_brother = team.members.len == 1 + if (!is_first_brother) + to_chat(carbon_owner, span_boldwarning("The Syndicate have higher expectations from you than others. They have granted you an extra flash to convert one other person.")) return ..() /datum/antagonist/brother/on_removal() - owner.special_role = null - owner.RemoveComponentSource(REF(src), /datum/component/can_flash_from_behind) - UnregisterSignal(owner, COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON) - + remove_conversion_skills() return ..() +/// Give us the ability to add another brother +/datum/antagonist/brother/proc/grant_conversion_skills() + var/mob/living/carbon/carbon_owner = owner.current + if (!istype(carbon_owner)) + return + carbon_owner.AddComponentFrom(REF(src), /datum/component/can_flash_from_behind) + RegisterSignal(carbon_owner, COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON, PROC_REF(on_mob_successful_flashed_carbon)) + +/// Take away the ability to add more brothers +/datum/antagonist/brother/proc/remove_conversion_skills() + if (isnull(owner.current)) + return + var/mob/living/carbon/carbon_owner = owner.current + carbon_owner.RemoveComponentSource(REF(src), /datum/component/can_flash_from_behind) + UnregisterSignal(carbon_owner, COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON) + /datum/antagonist/brother/proc/on_mob_successful_flashed_carbon(mob/living/source, mob/living/carbon/flashed, obj/item/assembly/flash/flash) SIGNAL_HANDLER @@ -67,7 +82,7 @@ flashed.balloon_alert(source, "[flashed.p_theyre()] loyal to someone else!") return - if (HAS_TRAIT(flashed, TRAIT_MINDSHIELD) || flashed.mind.assigned_role?.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY) + if (HAS_TRAIT(flashed, TRAIT_MINDSHIELD)) flashed.balloon_alert(source, "[flashed.p_they()] resist!") return @@ -141,7 +156,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 5257e7d4242b..7fd3be635cc2 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/ling antag_hud_name = "changeling" hijack_speed = 0.5 @@ -14,6 +14,7 @@ can_assign_self_objectives = TRUE default_custom_objective = "Consume the station's most valuable genomes." hardcore_random_bonus = TRUE + stinger_sound = 'sound/ambience/antag/ling_alert.ogg' /// Whether to give this changeling objectives or not var/give_objectives = TRUE /// Weather we assign objectives which compete with other lings @@ -1029,7 +1030,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 c44c1b66cd3d..9114886df295 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 167708ff826c..9448835fc64f 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.GetJobType(/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)].") @@ -34,18 +34,18 @@ 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" roundend_category = "clown operatives" antagpanel_category = ANTAG_GROUP_CLOWNOPS nukeop_outfit = /datum/outfit/syndicate/clownop/leader - challengeitem = /obj/item/nuclear_challenge/clownops suicide_cry = "HAPPY BIRTHDAY!!" /datum/antagonist/nukeop/leader/clownop/apply_innate_effects(mob/living/mob_override) diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm index 4f1000c62c54..edf923ee423c 100644 --- a/code/modules/antagonists/cult/cult.dm +++ b/code/modules/antagonists/cult/cult.dm @@ -5,8 +5,9 @@ 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/ambience/antag/bloodcult/bloodcult_gain.ogg' ///The vote ability Cultists have to elect someone to be the leader. var/datum/action/innate/cult/mastervote/vote_ability diff --git a/code/modules/antagonists/cult/cult_comms.dm b/code/modules/antagonists/cult/cult_comms.dm index ef0cc722aa3e..f78c1d73e8ca 100644 --- a/code/modules/antagonists/cult/cult_comms.dm +++ b/code/modules/antagonists/cult/cult_comms.dm @@ -127,7 +127,7 @@ if(B.current && B.current != Nominee && !B.current.incapacitated()) SEND_SOUND(B.current, 'sound/magic/exit_blood.ogg') asked_cultists += B.current - var/list/yes_voters = SSpolling.poll_candidates("[Nominee] seeks to lead your cult, do you support [Nominee.p_them()]?", poll_time = 30 SECONDS, group = asked_cultists, pic_source = Nominee, role_name_text = "cult master") + var/list/yes_voters = SSpolling.poll_candidates("[Nominee] seeks to lead your cult, do you support [Nominee.p_them()]?", poll_time = 30 SECONDS, group = asked_cultists, alert_pic = Nominee, role_name_text = "cult master") if(QDELETED(Nominee) || Nominee.incapacitated()) team.cult_vote_called = FALSE for(var/datum/mind/B in team.members) diff --git a/code/modules/antagonists/cult/runes.dm b/code/modules/antagonists/cult/runes.dm index 265be82b7614..7d55146de95b 100644 --- a/code/modules/antagonists/cult/runes.dm +++ b/code/modules/antagonists/cult/runes.dm @@ -303,7 +303,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_cultitalic("Your blood pulses. Your head throbs. The world goes red. \ @@ -740,13 +739,12 @@ GLOBAL_VAR_INIT(narsie_summon_count, 0) if(!mob_to_revive.client || mob_to_revive.client.is_afk()) set waitfor = FALSE - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as a [mob_to_revive.real_name], an inactive blood cultist?", check_jobban = ROLE_CULTIST, role = ROLE_CULTIST, poll_time = 5 SECONDS, target_mob = mob_to_revive, pic_source = mob_to_revive) - if(LAZYLEN(candidates)) - var/mob/dead/observer/C = pick(candidates) + var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to play as [span_danger(mob_to_revive.real_name)], an [span_notice("inactive blood cultist")]?", check_jobban = ROLE_CULTIST, role = ROLE_CULTIST, poll_time = 5 SECONDS, checked_target = mob_to_revive, alert_pic = mob_to_revive, role_name_text = "inactive cultist") + if(chosen_one) to_chat(mob_to_revive.mind, "Your physical form has been taken over by another soul due to your inactivity! Ahelp if you wish to regain your form.") - message_admins("[key_name_admin(C)] has taken control of ([key_name_admin(mob_to_revive)]) to replace an AFK player.") + message_admins("[key_name_admin(chosen_one)] has taken control of ([key_name_admin(mob_to_revive)]) to replace an AFK player.") mob_to_revive.ghostize(FALSE) - mob_to_revive.key = C.key + mob_to_revive.key = chosen_one.key else fail_invoke() return @@ -1174,7 +1172,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/disease/disease_datum.dm b/code/modules/antagonists/disease/disease_datum.dm index 17364feec559..79dd95f93a46 100644 --- a/code/modules/antagonists/disease/disease_datum.dm +++ b/code/modules/antagonists/disease/disease_datum.dm @@ -6,8 +6,7 @@ var/disease_name = "" /datum/antagonist/disease/on_gain() - owner.set_assigned_role(SSjob.GetJobType(/datum/job/sentient_disease)) - owner.special_role = ROLE_SENTIENT_DISEASE + owner.set_assigned_role(SSjob.get_job_type(/datum/job/sentient_disease)) var/datum/objective/O = new /datum/objective/disease_infect() O.owner = owner objectives += O diff --git a/code/modules/antagonists/ert/ert.dm b/code/modules/antagonists/ert/ert.dm index 92ff1b1b5e54..073bafd61fb0 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 new file mode 100644 index 000000000000..a5dad0b9112e --- /dev/null +++ b/code/modules/antagonists/evil_clone/evil_clone.dm @@ -0,0 +1,67 @@ +/// Antag datum associated with the experimental cloner +/datum/antagonist/evil_clone + name = "\improper Evil Clone" + stinger_sound = 'sound/ambience/antag/hypnotized.ogg' + pref_flag = ROLE_EVIL_CLONE + roundend_category = "evil clones" + show_in_antagpanel = TRUE + antagpanel_category = ANTAG_GROUP_CREW + show_name_in_check_antagonists = TRUE + antag_flags = ANTAG_SKIP_GLOBAL_LIST + +/datum/antagonist/evil_clone/on_gain() + if (owner.current) + name = "[owner.current.real_name] Prime" + forge_objectives() + return ..() + +/datum/antagonist/evil_clone/greet() + if(silent) + return + play_stinger() + var/mob/living/current_mob = owner.current + if (current_mob) + to_chat(current_mob, span_big("You are [current_mob.real_name].")) + to_chat(current_mob, span_hypnophrase("You are the only [current_mob.real_name].")) + to_chat(current_mob, span_boldwarning("Anyone else pretending to be [current_mob.real_name] must be punished.")) + owner.announce_objectives() + +/datum/antagonist/evil_clone/forge_objectives() + var/datum/objective/accept_no_substitutes/objective = new + objective.owner = owner + objective.set_target_name(owner.current?.real_name) + objectives += objective + +/// Kill everyone with the same name as you +/datum/objective/accept_no_substitutes + name = "kill all clones" + explanation_text = "Ensure that nobody with a particular name that you don't remember remains alive." + admin_grantable = TRUE + /// What name do we want to expunge? + var/target_name + +/// Set the name to check for +/datum/objective/accept_no_substitutes/proc/set_target_name(new_name) + target_name = new_name + explanation_text = "Ensure that nobody with the name [target_name] remains alive." + +/datum/objective/accept_no_substitutes/check_completion() + if (!target_name) + return FALSE // Well we forgot to check for a name + + for (var/mob/living/someone as anything in GLOB.player_list) // We will generously not include people who logged out or ghosted + if (!istype(someone)) + continue + if (someone.stat == DEAD) + continue + if (someone == owner.current) + continue + if (someone.real_name == target_name) + return FALSE + + return TRUE + +/datum/objective/accept_no_substitutes/admin_edit(mob/admin) + admin_simple_target_pick(admin) + if (target.current) + set_target_name(target.current.real_name) diff --git a/code/modules/antagonists/fugitive/fugitive.dm b/code/modules/antagonists/fugitive/fugitive.dm index 7f2cc4a9f3d5..e42782bbf74f 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 b75cba69e528..510cb281ecab 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 7133066f8569..32f624c65054 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 9cd05062583f..f68540292b37 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!!" @@ -26,6 +26,7 @@ can_assign_self_objectives = TRUE default_custom_objective = "Turn a department into a testament for your dark knowledge." hardcore_random_bonus = TRUE + stinger_sound = 'sound/ambience/antag/ecult_op.ogg' /// Whether we give this antagonist objectives on gain. var/give_objectives = TRUE /// Whether we've ascended! (Completed one of the final rituals) diff --git a/code/modules/antagonists/heretic/heretic_knowledge.dm b/code/modules/antagonists/heretic/heretic_knowledge.dm index 7e6177d8b90d..42b8b18bad69 100644 --- a/code/modules/antagonists/heretic/heretic_knowledge.dm +++ b/code/modules/antagonists/heretic/heretic_knowledge.dm @@ -547,14 +547,13 @@ animate(summoned, 10 SECONDS, alpha = 155) message_admins("A [summoned.name] is being summoned by [ADMIN_LOOKUPFLW(user)] in [ADMIN_COORDJMP(summoned)].") - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as a [summoned.name]?", check_jobban = ROLE_HERETIC, poll_time = 10 SECONDS, target_mob = summoned, ignore_category = poll_ignore_define, pic_source = summoned, role_name_text = summoned.name) - if(!LAZYLEN(candidates)) + var/mob/picked_candidate = SSpolling.poll_ghosts_for_target(check_jobban = ROLE_HERETIC, poll_time = 10 SECONDS, checked_target = summoned, ignore_category = poll_ignore_define, alert_pic = summoned, role_name_text = summoned.name) + if(isnull(picked_candidate)) loc.balloon_alert(user, "ritual failed, no ghosts!") animate(summoned, 0.5 SECONDS, alpha = 0) QDEL_IN(summoned, 0.6 SECONDS) return FALSE - var/mob/dead/observer/picked_candidate = pick(candidates) // Ok let's make them an interactable mob now, since we got a ghost summoned.alpha = 255 REMOVE_TRAIT(summoned, TRAIT_NO_TRANSFORM, REF(src)) diff --git a/code/modules/antagonists/heretic/heretic_monsters.dm b/code/modules/antagonists/heretic/heretic_monsters.dm index 3f3dd3203572..2bcbe31ee259 100644 --- a/code/modules/antagonists/heretic/heretic_monsters.dm +++ b/code/modules/antagonists/heretic/heretic_monsters.dm @@ -4,17 +4,14 @@ 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 + stinger_sound = 'sound/ambience/antag/ecult_op.ogg' /// Our master (a heretic)'s mind. var/datum/mind/master -/datum/antagonist/heretic_monster/on_gain() - . = ..() - owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/ecult_op.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)//subject to change - /datum/antagonist/heretic_monster/on_removal() if(!silent) if(master?.current) diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm index 9b3e38e38477..085204fb3b42 100644 --- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm @@ -176,15 +176,13 @@ if(!soon_to_be_ghoul.mind || !soon_to_be_ghoul.client) message_admins("[ADMIN_LOOKUPFLW(user)] is creating a voiceless dead of a body with no player.") - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as a [soon_to_be_ghoul.real_name], a voiceless dead?", check_jobban = ROLE_HERETIC, role = ROLE_HERETIC, poll_time = 5 SECONDS, target_mob = soon_to_be_ghoul, pic_source = soon_to_be_ghoul, role_name_text = "voiceless dead") - if(!LAZYLEN(candidates)) + var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to play as [span_danger(soon_to_be_ghoul.real_name)], a [span_notice("voiceless dead")]?", check_jobban = ROLE_HERETIC, role = ROLE_HERETIC, poll_time = 5 SECONDS, checked_target = soon_to_be_ghoul, alert_pic = mutable_appearance('icons/mob/human/human.dmi', "husk"), jump_target = soon_to_be_ghoul, role_name_text = "voiceless dead") + if(isnull(chosen_one)) loc.balloon_alert(user, "ritual failed, no ghosts!") return FALSE - - var/mob/dead/observer/chosen_candidate = pick(candidates) - message_admins("[key_name_admin(chosen_candidate)] has taken control of ([key_name_admin(soon_to_be_ghoul)]) to replace an AFK player.") + message_admins("[key_name_admin(chosen_one)] has taken control of ([key_name_admin(soon_to_be_ghoul)]) to replace an AFK player.") soon_to_be_ghoul.ghostize(FALSE) - soon_to_be_ghoul.key = chosen_candidate.key + soon_to_be_ghoul.key = chosen_one.key selected_atoms -= soon_to_be_ghoul make_ghoul(user, soon_to_be_ghoul) diff --git a/code/modules/antagonists/heretic/structures/lock_final.dm b/code/modules/antagonists/heretic/structures/lock_final.dm index 8cb6c06f3cb0..759bc8aa55e3 100644 --- a/code/modules/antagonists/heretic/structures/lock_final.dm +++ b/code/modules/antagonists/heretic/structures/lock_final.dm @@ -37,7 +37,7 @@ /// Ask ghosts if they want to make some noise /obj/structure/lock_tear/proc/poll_ghosts() - var/list/candidates = SSpolling.poll_ghost_candidates("Would you like to be a random eldritch monster attacking the crew?", check_jobban = ROLE_SENTIENCE, role = ROLE_SENTIENCE, poll_time = 10 SECONDS, ignore_category = POLL_IGNORE_HERETIC_MONSTER, pic_source = src, role_name_text = "eldritch monster") + var/list/candidates = SSpolling.poll_ghost_candidates("Would you like to be a random [span_notice("eldritch monster")] attacking the crew?", check_jobban = ROLE_SENTIENCE, role = ROLE_SENTIENCE, poll_time = 10 SECONDS, ignore_category = POLL_IGNORE_HERETIC_MONSTER, alert_pic = src, role_name_text = "eldritch monster") while(LAZYLEN(candidates)) var/mob/dead/observer/candidate = pick_n_take(candidates) ghost_to_monster(candidate, should_ask = FALSE) diff --git a/code/modules/antagonists/highlander/highlander.dm b/code/modules/antagonists/highlander/highlander.dm index 491d55d8f2d0..2ba710b2ffd4 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_DESENSITIZED, @@ -39,7 +39,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 4f1f49aa3be7..1634b2b84a72 100644 --- a/code/modules/antagonists/hypnotized/hypnotized.dm +++ b/code/modules/antagonists/hypnotized/hypnotized.dm @@ -1,14 +1,15 @@ /// Antag datum associated with the hypnosis brain trauma, used for displaying objectives and antag hud /datum/antagonist/hypnotized name = "\improper Hypnotized Victim" - job_rank = ROLE_HYPNOTIZED + stinger_sound = 'sound/ambience/antag/hypnotized.ogg' + 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 silent = TRUE //not actually silent, because greet will be called by the trauma anyway. /// Brain trauma associated with this antag datum diff --git a/code/modules/antagonists/malf_ai/malf_ai.dm b/code/modules/antagonists/malf_ai/malf_ai.dm index fc25fd11a72d..bbb8d7a77ee5 100644 --- a/code/modules/antagonists/malf_ai/malf_ai.dm +++ b/code/modules/antagonists/malf_ai/malf_ai.dm @@ -5,11 +5,12 @@ 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 default_custom_objective = "Make sure your precious crew are incapable of ever, ever leaving you." + stinger_sound = 'sound/ambience/antag/malf.ogg' ///the name of the antag flavor this traitor has. var/employer ///assoc list of strings set up after employer is given @@ -30,12 +31,15 @@ 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() + if(!employer) + employer = pick(GLOB.ai_employers) - if(finalize_antag) // NON-MODULE CHANGE - finalize_antag() + malfunction_flavor = strings(MALFUNCTION_FLAVOR_FILE, employer) + + add_law_zero() + owner.current.grant_language(/datum/language/codespeak, source = LANGUAGE_MALF) return ..() @@ -46,8 +50,6 @@ malf_ai.remove_malf_abilities() QDEL_NULL(malf_ai.malf_picker) - owner.special_role = null - return ..() /// Generates a complete set of malf AI objectives up to the traitor objective limit. @@ -262,6 +264,7 @@ /datum/antagonist/malf_ai/infected name = "Infected AI" employer = "Infected AI" + stinger_sound = null ///The player, to who is this AI slaved var/datum/mind/boss diff --git a/code/modules/antagonists/nightmare/nightmare.dm b/code/modules/antagonists/nightmare/nightmare.dm index 58417b2a9664..d8a1f18a3010 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 new file mode 100644 index 000000000000..5db8db895ccb --- /dev/null +++ b/code/modules/antagonists/nukeop/datums/operative.dm @@ -0,0 +1,206 @@ +/datum/antagonist/nukeop + name = ROLE_OPERATIVE + roundend_category = "syndicate operatives" //just in case + antagpanel_category = ANTAG_GROUP_SYNDICATE + pref_flag = ROLE_OPERATIVE + antag_hud_name = "synd" + antag_moodlet = /datum/mood_event/focused + show_to_ghosts = TRUE + hijack_speed = 2 //If you can't take out the station, take the shuttle instead. + suicide_cry = "FOR THE SYNDICATE!!" + stinger_sound = 'sound/ambience/antag/ops.ogg' + + /// Which nukie team are we on? + var/datum/team/nuclear/nuke_team + /// 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 + + preview_outfit = /datum/outfit/nuclear_operative_elite + + /// 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" + + /// The amount of discounts that the team get + var/discount_team_amount = 5 + /// The amount of limited discounts that the team get + var/discount_limited_amount = 10 + +/datum/antagonist/nukeop/greet() + play_stinger() + to_chat(owner, span_big("You are a [nuke_team ? nuke_team.syndicate_name : "syndicate"] agent!")) + owner.announce_objectives() + +/datum/antagonist/nukeop/on_gain() + give_alias() + forge_objectives() + . = ..() + owner.set_assigned_role(SSjob.get_job_type(job_type)) + equip_op() + if(send_to_spawnpoint) + move_to_spawnpoint() + // grant extra TC for the people who start in the nukie base ie. not the lone op + var/extra_tc = CEILING(GLOB.joined_player_list.len/5, 5) + var/datum/component/uplink/uplink = owner.find_syndicate_uplink() + if (uplink) + uplink.uplink_handler.add_telecrystals(extra_tc) + var/datum/component/uplink/uplink = owner.find_syndicate_uplink() + if(uplink) + var/datum/team/nuclear/nuke_team = get_team() + if(!nuke_team.team_discounts) + var/list/uplink_items = list() + for(var/datum/uplink_item/item as anything in SStraitor.uplink_items) + if(item.item && !item.cant_discount && (item.purchasable_from & uplink.uplink_handler.uplink_flag) && item.cost > 1) + uplink_items += item + nuke_team.team_discounts = list() + nuke_team.team_discounts += create_uplink_sales(discount_team_amount, /datum/uplink_category/discount_team_gear, -1, uplink_items) + nuke_team.team_discounts += create_uplink_sales(discount_limited_amount, /datum/uplink_category/limited_discount_team_gear, 1, uplink_items) + uplink.uplink_handler.extra_purchasable += nuke_team.team_discounts + + if(nuke_team?.tracked_nuke && nuke_team?.memorized_code) + memorize_code() + + // future melbert todo - tie to nukeop jobs + owner.set_level(/datum/skill/electronics, SKILL_LEVEL_MASTER, silent = TRUE) + owner.set_level(/datum/skill/eva, SKILL_LEVEL_MASTER, silent = TRUE) + owner.set_level(/datum/skill/firearms, SKILL_LEVEL_MASTER, silent = TRUE) + owner.set_level(/datum/skill/first_aid, SKILL_LEVEL_MASTER, silent = TRUE) + owner.set_level(/datum/skill/athletics, SKILL_LEVEL_EXPERT, silent = TRUE) + owner.set_level(/datum/skill/mechanics, SKILL_LEVEL_MASTER, silent = TRUE) + owner.set_level(/datum/skill/surgery, SKILL_LEVEL_MASTER, silent = TRUE) + owner.set_level(/datum/skill/piloting, SKILL_LEVEL_EXPERT, silent = TRUE) + +/datum/antagonist/nukeop/get_team() + return nuke_team + +/datum/antagonist/nukeop/apply_innate_effects(mob/living/mob_override) + add_team_hud(mob_override || owner.current, /datum/antagonist/nukeop) + +/datum/antagonist/nukeop/forge_objectives() + if(nuke_team) + objectives |= nuke_team.objectives + +/datum/antagonist/nukeop/leader/get_spawnpoint() + return pick(GLOB.nukeop_leader_start) + +/datum/antagonist/nukeop/create_team(datum/team/nuclear/new_team) + if(!new_team) + // 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.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)].") + +/datum/antagonist/nukeop/get_admin_commands() + . = ..() + .["Send to base"] = CALLBACK(src, PROC_REF(admin_send_to_base)) + .["Tell code"] = CALLBACK(src, PROC_REF(admin_tell_code)) + +/datum/antagonist/nukeop/get_preview_icon() + if (!preview_outfit) + return null + + var/icon/final_icon = render_preview_outfit(preview_outfit) + + if (!isnull(preview_outfit_behind)) + var/icon/teammate = render_preview_outfit(preview_outfit_behind) + teammate.Blend(rgb(128, 128, 128, 128), ICON_MULTIPLY) + + final_icon.Blend(teammate, ICON_UNDERLAY, -ICON_SIZE_X / 4, 0) + final_icon.Blend(teammate, ICON_UNDERLAY, ICON_SIZE_X / 4, 0) + + if (!isnull(nuke_icon_state)) + var/icon/nuke = icon('icons/obj/machines/nuke.dmi', nuke_icon_state) + nuke.Shift(SOUTH, 6) + final_icon.Blend(nuke, ICON_OVERLAY) + + return finish_preview_icon(final_icon) + +/datum/antagonist/nukeop/proc/equip_op() + if(!ishuman(owner.current)) + return + + var/mob/living/carbon/human/operative = owner.current + ADD_TRAIT(operative, TRAIT_NOFEAR_HOLDUPS, INNATE_TRAIT) + + if(!nukeop_outfit) // this variable is null in instances where an antagonist datum is granted via enslaving the mind (/datum/mind/proc/enslave_mind_to_creator), like in golems. + return + + // If our nuke_ops_species pref is set to TRUE, (or we have no client) make us a human + if(isnull(operative.client) || operative.client.prefs.read_preference(/datum/preference/toggle/nuke_ops_species)) + operative.set_species(/datum/species/human) + + operative.equip_species_outfit(nukeop_outfit) + + return TRUE + +/datum/antagonist/nukeop/proc/admin_send_to_base(mob/admin) + owner.current.forceMove(pick(GLOB.nukeop_start)) + +/datum/antagonist/nukeop/proc/admin_tell_code(mob/admin) + var/code + for (var/obj/machinery/nuclearbomb/bombue as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb)) + if (length(bombue.r_code) <= 5 && bombue.r_code != initial(bombue.r_code)) + code = bombue.r_code + break + if (code) + antag_memory += "Syndicate Nuclear Bomb Code: [code]
" + to_chat(owner.current, "The nuclear authorization code is: [code]") + else + to_chat(admin, span_danger("No valid nuke found!")) + +/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(null, chosen_name) + else + 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() + 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() + // Ensure that the nukiebase is loaded, and wait for it if required + SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NUKIEBASE) + var/turf/destination = get_spawnpoint() + owner.current.forceMove(destination) + if(!owner.current.onSyndieBase()) + message_admins("[ADMIN_LOOKUPFLW(owner.current)] is a NUKE OP and move_to_spawnpoint put them somewhere that isn't the syndie base, help please.") + stack_trace("Nuke op move_to_spawnpoint resulted in a location not on the syndicate base. (Was moved to: [destination])") + +/// Gets the position we spawn at +/datum/antagonist/nukeop/proc/get_spawnpoint() + var/team_number = 1 + if(nuke_team) + 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 new file mode 100644 index 000000000000..0b4a5b0ea0e5 --- /dev/null +++ b/code/modules/antagonists/nukeop/datums/operative_leader.dm @@ -0,0 +1,62 @@ +/datum/antagonist/nukeop/leader + name = "Nuclear Operative Leader" + nukeop_outfit = /datum/outfit/syndicate/leader + /// Randomly chosen honorific, for distinction + var/title + +/datum/antagonist/nukeop/leader/memorize_code() + . = ..() + 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 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() + +/datum/antagonist/nukeop/leader/on_gain() + . = ..() + addtimer(CALLBACK(src, PROC_REF(nuketeam_name_assign)), 0.1 SECONDS) + +/datum/antagonist/nukeop/leader/proc/nuketeam_name_assign() + if(!nuke_team) + return + nuke_team.rename_team(ask_name()) + +/datum/antagonist/nukeop/leader/proc/ask_name() + var/randomname = pick(GLOB.last_names) + var/newname = tgui_input_text( + owner.current, + "You are the nuclear operative [title]. Please choose a last name for your family.", + "Name change", + randomname, + max_length = MAX_NAME_LEN, + ) + if (!newname) + newname = randomname + else + newname = reject_bad_name(newname) + if(!newname) + 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 new file mode 100644 index 000000000000..b074016890aa --- /dev/null +++ b/code/modules/antagonists/nukeop/datums/operative_lone.dm @@ -0,0 +1,13 @@ +/datum/antagonist/nukeop/lone + name = "Lone Operative" + 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/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 new file mode 100644 index 000000000000..1604ac65f6ab --- /dev/null +++ b/code/modules/antagonists/nukeop/datums/operative_team.dm @@ -0,0 +1,334 @@ +#define SPAWN_AT_BASE "Nuke base" +#define SPAWN_AT_INFILTRATOR "Infiltrator" + +/datum/team/nuclear + var/syndicate_name + var/obj/machinery/nuclearbomb/tracked_nuke + var/core_objective = /datum/objective/nuclear + var/memorized_code + var/list/team_discounts + var/datum/weakref/war_button_ref + +/datum/team/nuclear/New() + ..() + 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:") + + switch(get_result()) + if(NUKE_RESULT_FLUKE) + parts += "Humiliating Syndicate Defeat" + parts += "The crew of [station_name()] gave [syndicate_name] operatives back their bomb! The syndicate base was destroyed! Next time, don't lose the nuke!" + if(NUKE_RESULT_NUKE_WIN) + parts += "Syndicate Major Victory!" + parts += "[syndicate_name] operatives have destroyed [station_name()]!" + if(NUKE_RESULT_NOSURVIVORS) + parts += "Total Annihilation!" + parts += "[syndicate_name] operatives destroyed [station_name()] but did not leave the area in time and got caught in the explosion. Next time, don't lose the disk!" + if(NUKE_RESULT_WRONG_STATION) + parts += "Crew Minor Victory!" + parts += "[syndicate_name] operatives secured the authentication disk but blew up something that wasn't [station_name()]. Next time, don't do that!" + if(NUKE_RESULT_WRONG_STATION_DEAD) + parts += "[syndicate_name] operatives have earned Darwin Award!" + parts += "[syndicate_name] operatives blew up something that wasn't [station_name()] and got caught in the explosion. Next time, don't do that!" + if(NUKE_RESULT_HIJACK_DISK) + parts += "Syndicate Miniscule Victory!" + parts += "[syndicate_name] operatives failed to destroy [station_name()], but they managed to secure the disk and hijack the emergency shuttle, causing it to land on the syndicate base. Good job?" + if(NUKE_RESULT_HIJACK_NO_DISK) + parts += "Syndicate Insignificant Victory!" + parts += "[syndicate_name] operatives failed to destroy [station_name()] or secure the disk, but they managed to hijack the emergency shuttle, causing it to land on the syndicate base. Good job?" + if(NUKE_RESULT_CREW_WIN_SYNDIES_DEAD) + parts += "Crew Major Victory!" + parts += "The Research Staff has saved the disk and killed the [syndicate_name] Operatives" + if(NUKE_RESULT_CREW_WIN) + parts += "Crew Major Victory!" + parts += "The Research Staff has saved the disk and stopped the [syndicate_name] Operatives!" + if(NUKE_RESULT_DISK_LOST) + parts += "Neutral Victory!" + parts += "The Research Staff failed to secure the authentication disk but did manage to kill most of the [syndicate_name] Operatives!" + if(NUKE_RESULT_DISK_STOLEN) + parts += "Syndicate Minor Victory!" + parts += "[syndicate_name] operatives survived the assault but did not achieve the destruction of [station_name()]. Next time, don't lose the disk!" + else + parts += "Neutral Victory" + parts += "Mission aborted!" + + var/text = span_header("
The syndicate operatives were:") + var/purchases = "" + var/TC_uses = 0 + LAZYINITLIST(GLOB.uplink_purchase_logs_by_key) + for(var/I in members) + var/datum/mind/syndicate = I + var/datum/uplink_purchase_log/H = GLOB.uplink_purchase_logs_by_key[syndicate.key] + if(H) + TC_uses += H.total_spent + purchases += H.generate_render(show_key = FALSE) + text += printplayerlist(members) + text += "
" + text += "(Syndicates used [TC_uses] TC) [purchases]" + if(TC_uses == 0 && GLOB.station_was_nuked && !are_all_operatives_dead()) + text += "[icon2html('icons/ui_icons/antags/badass.dmi', world, "badass")]" + + parts += text + + return "
[parts.Join("
")]
" + +/datum/team/nuclear/antag_listing_name() + if(syndicate_name) + return "[syndicate_name] Syndicates" + else + return "Syndicates" + +/datum/team/nuclear/antag_listing_entry() + var/disk_report = "Nuclear Disk(s)
" + disk_report += "" + for(var/obj/item/disk/nuclear/N in SSpoints_of_interest.real_nuclear_disks) + disk_report += "" + disk_report += "
[N.name], " + var/atom/disk_loc = N.loc + while(!isturf(disk_loc)) + if(ismob(disk_loc)) + var/mob/M = disk_loc + disk_report += "carried by [M.real_name] " + if(isobj(disk_loc)) + var/obj/O = disk_loc + disk_report += "in \a [O.name] " + disk_loc = disk_loc.loc + disk_report += "in [disk_loc.loc] at ([disk_loc.x], [disk_loc.y], [disk_loc.z])FLW
" + + var/post_report + post_report += "\n\[Send Reinforcement\]" + + var/final_report = ..() + final_report += disk_report + final_report += post_report + return final_report + +/datum/team/nuclear/proc/rename_team(new_name) + syndicate_name = new_name + name = "[syndicate_name] Team" + 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)) + return + + var/infil_or_nukebase = tgui_alert( + admin, + "Spawn them at the nuke base, or in the Infiltrator?", + "Where to reinforce?", + list(SPAWN_AT_BASE, SPAWN_AT_INFILTRATOR, "Cancel"), + ) + + if(!infil_or_nukebase || infil_or_nukebase == "Cancel") + return + + var/tc_to_spawn = tgui_input_number(admin, "How much TC to spawn with?", "TC", 0, 100) + + var/mob/chosen_one = SSpolling.poll_ghost_candidates( + check_jobban = ROLE_OPERATIVE, + role = ROLE_OPERATIVE, + poll_time = 30 SECONDS, + ignore_category = POLL_IGNORE_SYNDICATE, + alert_pic = /obj/structure/sign/poster/contraband/gorlex_recruitment, + role_name_text = "emergency syndicate reinforcement", + amount_to_pick = 1, + ) + + if(isnull(chosen_one)) + tgui_alert(admin, "No candidates found.", "Recruitment Shortage", list("OK")) + return + + + var/turf/spawn_loc + if(infil_or_nukebase == SPAWN_AT_INFILTRATOR) + var/area/spawn_in + // Prioritize EVA then hallway, if neither can be found default to the first area we can find + for(var/area_type in list(/area/shuttle/syndicate/eva, /area/shuttle/syndicate/hallway, /area/shuttle/syndicate)) + spawn_in = locate(area_type) in GLOB.areas // I'd love to use areas_by_type but the Infiltrator is a unique area + if(spawn_in) + break + + var/list/turf/options = list() + for(var/turf/open/open_turf in spawn_in?.get_turfs_from_all_zlevels()) + if(open_turf.is_blocked_turf()) + continue + options += open_turf + + if(length(options)) + spawn_loc = pick(options) + else + infil_or_nukebase = SPAWN_AT_BASE + + if(infil_or_nukebase == SPAWN_AT_BASE) + spawn_loc = pick(GLOB.nukeop_start) + + var/mob/living/carbon/human/nukie = new(spawn_loc) + chosen_one.client.prefs.safe_transfer_prefs_to(nukie, is_antag = TRUE) + nukie.key = chosen_one.key + + var/datum/antagonist/nukeop/antag_datum = new() + antag_datum.send_to_spawnpoint = FALSE + antag_datum.nukeop_outfit = /datum/outfit/syndicate/reinforcement + + nukie.mind.add_antag_datum(antag_datum, src) + + var/datum/component/uplink/uplink = nukie.mind.find_syndicate_uplink() + uplink?.uplink_handler.set_telecrystals(tc_to_spawn) + + // add some pizzazz + do_sparks(4, FALSE, spawn_loc) + new /obj/effect/temp_visual/teleport_abductor/syndi_teleporter(spawn_loc) + playsound(spawn_loc, SFX_SPARKS, 50, TRUE) + playsound(spawn_loc, 'sound/effects/phasein.ogg', 50, TRUE) + + tgui_alert(admin, "Reinforcement spawned at [infil_or_nukebase] with [tc_to_spawn].", "Reinforcements have arrived", list("God speed")) + +/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 + if(SSshuttle.emergency.mode == SHUTTLE_ESCAPE) + if(!SSshuttle.emergency.is_in_shuttle_bounds(nuke_disk)) + return FALSE + //If shuttle escaped check if it's on centcom side + else if(SSshuttle.emergency.mode == SHUTTLE_ENDGAME) + if(!nuke_disk.onCentCom()) + return FALSE + else //Otherwise disk is safe when on station + var/turf/disk_turf = get_turf(nuke_disk) + if(!disk_turf || !is_station_level(disk_turf.z)) + return FALSE + return TRUE + +/datum/team/nuclear/proc/are_all_operatives_dead() + for(var/datum/mind/operative_mind as anything in members) + if(ishuman(operative_mind.current) && (operative_mind.current.stat != DEAD)) + return FALSE + return TRUE + +/datum/team/nuclear/proc/get_result() + var/shuttle_evacuated = EMERGENCY_ESCAPED_OR_ENDGAMED + var/shuttle_landed_base = SSshuttle.emergency.is_hijacked() + var/disk_rescued = is_disk_rescued() + var/syndies_didnt_escape = !is_infiltrator_docked_at_syndiebase() + var/team_is_dead = are_all_operatives_dead() + var/station_was_nuked = GLOB.station_was_nuked + var/station_nuke_source = GLOB.station_nuke_source + + // The nuke detonated on the syndicate base + if(station_nuke_source == DETONATION_HIT_SYNDIE_BASE) + return NUKE_RESULT_FLUKE + + // The station was nuked + if(station_was_nuked) + // The station was nuked and the infiltrator failed to escape + if(syndies_didnt_escape) + return NUKE_RESULT_NOSURVIVORS + // The station was nuked and the infiltrator escaped, and the nuke ops won + else + return NUKE_RESULT_NUKE_WIN + + // The station was not nuked, but something was + else if(station_nuke_source && !disk_rescued) + // The station was not nuked, but something was, and the syndicates didn't escape it + if(syndies_didnt_escape) + return NUKE_RESULT_WRONG_STATION_DEAD + // The station was not nuked, but something was, and the syndicates returned to their base + else + return NUKE_RESULT_WRONG_STATION + + // Nuke didn't blow, but nukies somehow hijacked the emergency shuttle to land at the base anyways. + else if(shuttle_landed_base) + if(disk_rescued) + return NUKE_RESULT_HIJACK_DISK + else + return NUKE_RESULT_HIJACK_NO_DISK + + // No nuke went off, the station rescued the disk + else if(disk_rescued) + // No nuke went off, the shuttle left, and the team is dead + if(shuttle_evacuated && team_is_dead) + return NUKE_RESULT_CREW_WIN_SYNDIES_DEAD + // No nuke went off, but the nuke ops survived + else + return NUKE_RESULT_CREW_WIN + + // No nuke went off, but the disk was left behind + else + // No nuke went off, the disk was left, but all the ops are dead + if(team_is_dead) + return NUKE_RESULT_DISK_LOST + // No nuke went off, the disk was left, there are living ops, but the shuttle left successfully + else if(shuttle_evacuated) + return NUKE_RESULT_DISK_STOLEN + + CRASH("[type] - got an undefined / unexpected result.") + +/// Returns whether or not syndicate operatives escaped. +/proc/is_infiltrator_docked_at_syndiebase() + var/obj/docking_port/mobile/infiltrator/infiltrator_port = SSshuttle.getShuttle("syndicate") + + var/datum/lazy_template/nukie_base/nukie_template = GLOB.lazy_templates[LAZY_TEMPLATE_KEY_NUKIEBASE] + if(!nukie_template) + return FALSE // if its not even loaded, cant be docked + + for(var/datum/turf_reservation/loaded_area as anything in nukie_template.reservations) + var/infiltrator_turf = get_turf(infiltrator_port) + if(infiltrator_turf in loaded_area.reserved_turfs) + return TRUE + return FALSE + +// /datum/team/nuclear/add_member(datum/mind/new_member) +// ..() +// 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/nukeop/equipment/nuclear_challenge.dm b/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm deleted file mode 100644 index 734025e9a370..000000000000 --- a/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm +++ /dev/null @@ -1,199 +0,0 @@ -#define CHALLENGE_TELECRYSTALS 280 -#define CHALLENGE_TIME_LIMIT (5 MINUTES) -#define CHALLENGE_SHUTTLE_DELAY (25 MINUTES) // 25 minutes, so the ops have at least 5 minutes before the shuttle is callable. - -GLOBAL_LIST_EMPTY(jam_on_wardec) - -/obj/item/nuclear_challenge - name = "Declaration of War (Challenge Mode)" - icon = 'icons/obj/devices/voice.dmi' - icon_state = "nukietalkie" - inhand_icon_state = "nukietalkie" - lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi' - righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi' - desc = "Use to send a declaration of hostilities to the target, delaying your shuttle departure for 20 minutes while they prepare for your assault. \ - Such a brazen move will attract the attention of powerful benefactors within the Syndicate, who will supply your team with a massive amount of bonus telecrystals. \ - Must be used within five minutes, or your benefactors will lose interest." - var/declaring_war = FALSE - var/uplink_type = /obj/item/uplink/nuclear - -/obj/item/nuclear_challenge/attack_self(mob/living/user) - if(!check_allowed(user)) - return - - declaring_war = TRUE - var/are_you_sure = tgui_alert(user, "Consult your team carefully before you declare war on [station_name()]. Are you sure you want to alert the enemy crew? You have [DisplayTimeText(CHALLENGE_TIME_LIMIT - world.time - SSticker.round_start_time)] to decide.", "Declare war?", list("Yes", "No")) - declaring_war = FALSE - - if(!check_allowed(user)) - return - - if(are_you_sure != "Yes") - to_chat(user, span_notice("On second thought, the element of surprise isn't so bad after all.")) - return - - var/war_declaration = "A syndicate fringe group has declared their intent to utterly destroy [station_name()] with a nuclear device, and dares the crew to try and stop them." - - declaring_war = TRUE - var/custom_threat = tgui_alert(user, "Do you want to customize your declaration?", "Customize?", list("Yes", "No")) - declaring_war = FALSE - - if(!check_allowed(user)) - return - - if(custom_threat == "Yes") - declaring_war = TRUE - war_declaration = tgui_input_text(user, "Insert your custom declaration", "Declaration", multiline = TRUE, encode = FALSE) - declaring_war = FALSE - - if(!check_allowed(user) || !war_declaration) - return - - war_was_declared(user, memo = war_declaration) - -///Admin only proc to bypass checks and force a war declaration. Button on antag panel. -/obj/item/nuclear_challenge/proc/force_war() - var/are_you_sure = tgui_alert(usr, "Are you sure you wish to force a war declaration?[GLOB.player_list.len < CHALLENGE_MIN_PLAYERS ? " Note, the player count is under the required limit." : ""]", "Declare war?", list("Yes", "No")) - - if(are_you_sure != "Yes") - return - - var/war_declaration = "A syndicate fringe group has declared their intent to utterly destroy [station_name()] with a nuclear device, and dares the crew to try and stop them." - - var/custom_threat = tgui_alert(usr, "Do you want to customize the declaration?", "Customize?", list("Yes", "No")) - - if(custom_threat == "Yes") - war_declaration = tgui_input_text(usr, "Insert your custom declaration", "Declaration", multiline = TRUE, encode = FALSE) - - if(!war_declaration) - tgui_alert(usr, "Invalid war declaration.", "Poor Choice of Words") - return - - for(var/obj/item/circuitboard/computer/syndicate_shuttle/board as anything in GLOB.syndicate_shuttle_boards) - if(board.challenge) - tgui_alert(usr, "War has already been declared!", "War Was Declared") - return - - war_was_declared(memo = war_declaration) - -/obj/item/nuclear_challenge/proc/war_was_declared(mob/living/user, memo) - priority_announce( - text = memo, - title = "Declaration of War", - sound = 'sound/machines/alarm.ogg', - has_important_message = TRUE, - sender_override = "Nuclear Operative Outpost", - color_override = "red", - ) - if(user) - to_chat(user, "You've attracted the attention of powerful forces within the syndicate. \ - A bonus bundle of telecrystals has been granted to your team. Great things await you if you complete the mission.") - - distribute_tc() - CONFIG_SET(number/shuttle_refuel_delay, max(CONFIG_GET(number/shuttle_refuel_delay), CHALLENGE_SHUTTLE_DELAY)) - SSblackbox.record_feedback("amount", "nuclear_challenge_mode", 1) - - for(var/obj/item/circuitboard/computer/syndicate_shuttle/board as anything in GLOB.syndicate_shuttle_boards) - board.challenge = TRUE - - for(var/obj/machinery/computer/camera_advanced/shuttle_docker/dock as anything in GLOB.jam_on_wardec) - dock.jammed = TRUE - - qdel(src) - -/obj/item/nuclear_challenge/proc/distribute_tc() - var/list/orphans = list() - var/list/uplinks = list() - - for (var/datum/mind/M in get_antag_minds(/datum/antagonist/nukeop)) - if (iscyborg(M.current)) - continue - var/datum/component/uplink/uplink = M.find_syndicate_uplink() - if (!uplink) - orphans += M.current - continue - uplinks += uplink - - var/tc_to_distribute = CHALLENGE_TELECRYSTALS - var/tc_per_nukie = round(tc_to_distribute / (length(orphans)+length(uplinks))) - - for (var/datum/component/uplink/uplink in uplinks) - uplink.add_telecrystals(tc_per_nukie) - tc_to_distribute -= tc_per_nukie - - for (var/mob/living/L in orphans) - var/TC = new /obj/item/stack/telecrystal(L.drop_location(), tc_per_nukie) - to_chat(L, span_warning("Your uplink could not be found so your share of the team's bonus telecrystals has been bluespaced to your [L.put_in_hands(TC) ? "hands" : "feet"].")) - tc_to_distribute -= tc_per_nukie - - if (tc_to_distribute > 0) // What shall we do with the remainder... - for (var/mob/living/basic/carp/pet/cayenne/C in GLOB.mob_living_list) - if (C.stat != DEAD) - var/obj/item/stack/telecrystal/TC = new(C.drop_location(), tc_to_distribute) - TC.throw_at(get_step(C, C.dir), 3, 3) - C.visible_message(span_notice("[C] coughs up a half-digested telecrystal"),span_notice("You cough up a half-digested telecrystal!")) - break - - -/obj/item/nuclear_challenge/proc/check_allowed(mob/living/user) - if(declaring_war) - to_chat(user, span_boldwarning("You are already in the process of declaring war! Make your mind up.")) - return FALSE - if(GLOB.player_list.len < CHALLENGE_MIN_PLAYERS) - to_chat(user, span_boldwarning("The enemy crew is too small to be worth declaring war on.")) - return FALSE - if(!user.onSyndieBase()) - to_chat(user, span_boldwarning("You have to be at your base to use this.")) - return FALSE - if(world.time - SSticker.round_start_time > CHALLENGE_TIME_LIMIT) - to_chat(user, span_boldwarning("It's too late to declare hostilities. Your benefactors are already busy with other schemes. You'll have to make do with what you have on hand.")) - return FALSE - for(var/obj/item/circuitboard/computer/syndicate_shuttle/board as anything in GLOB.syndicate_shuttle_boards) - if(board.moved) - to_chat(user, span_boldwarning("The shuttle has already been moved! You have forfeit the right to declare war.")) - return FALSE - if(board.challenge) - to_chat(user, span_boldwarning("War has already been declared!")) - return FALSE - return TRUE - -/obj/item/nuclear_challenge/clownops - uplink_type = /obj/item/uplink/clownop - -/// Subtype that does nothing but plays the war op message. Intended for debugging -/obj/item/nuclear_challenge/literally_just_does_the_message - name = "\"Declaration of War\"" - desc = "It's a Syndicate Declaration of War thing-a-majig, but it only plays the loud sound and message. Nothing else." - var/admin_only = TRUE - -/obj/item/nuclear_challenge/literally_just_does_the_message/check_allowed(mob/living/user) - if(admin_only && !check_rights_for(user.client, R_SPAWN|R_FUN|R_DEBUG)) - to_chat(user, span_hypnophrase("You shouldn't have this!")) - return FALSE - - return TRUE - -/obj/item/nuclear_challenge/literally_just_does_the_message/war_was_declared(mob/living/user, memo) -#ifndef TESTING - // Reminder for our friends the admins - var/are_you_sure = tgui_alert(user, "Last second reminder that fake war declarations is a horrible idea and yes, \ - this does the whole shebang, so be careful what you're doing.", "Don't do it", list("I'm sure", "You're right")) - if(are_you_sure != "I'm sure") - return -#endif - - priority_announce( - text = memo, - title = "Declaration of War", - sound = 'sound/machines/alarm.ogg', - has_important_message = TRUE, - sender_override = "Nuclear Operative Outpost", - color_override = "red", - ) - -/obj/item/nuclear_challenge/literally_just_does_the_message/distribute_tc() - return - -#undef CHALLENGE_TELECRYSTALS -#undef CHALLENGE_TIME_LIMIT -#undef CHALLENGE_SHUTTLE_DELAY diff --git a/code/modules/antagonists/nukeop/nukeop.dm b/code/modules/antagonists/nukeop/nukeop.dm deleted file mode 100644 index 1de07d23a1b6..000000000000 --- a/code/modules/antagonists/nukeop/nukeop.dm +++ /dev/null @@ -1,647 +0,0 @@ -/datum/antagonist/nukeop - name = ROLE_NUCLEAR_OPERATIVE - roundend_category = "syndicate operatives" //just in case - antagpanel_category = ANTAG_GROUP_SYNDICATE - job_rank = ROLE_OPERATIVE - antag_hud_name = "synd" - antag_moodlet = /datum/mood_event/focused - show_to_ghosts = TRUE - hijack_speed = 2 //If you can't take out the station, take the shuttle instead. - suicide_cry = "FOR THE SYNDICATE!!" - /// 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 - /// The DEFAULT outfit we will give to players granted this datum - var/nukeop_outfit = /datum/outfit/syndicate - - preview_outfit = /datum/outfit/nuclear_operative_elite - - /// 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" - - /// The amount of discounts that the team get - var/discount_team_amount = 5 - /// The amount of limited discounts that the team get - var/discount_limited_amount = 10 - -/datum/antagonist/nukeop/proc/equip_op() - if(!ishuman(owner.current)) - return - - var/mob/living/carbon/human/operative = owner.current - ADD_TRAIT(operative, TRAIT_NOFEAR_HOLDUPS, INNATE_TRAIT) - ADD_TRAIT(operative, TRAIT_DESENSITIZED, INNATE_TRAIT) - - if(!nukeop_outfit) // this variable is null in instances where an antagonist datum is granted via enslaving the mind (/datum/mind/proc/enslave_mind_to_creator), like in golems. - return - - // If our nuke_ops_species pref is set to TRUE, (or we have no client) make us a human - if(isnull(operative.client) || operative.client.prefs.read_preference(/datum/preference/toggle/nuke_ops_species)) - operative.set_species(/datum/species/human) - - operative.equip_species_outfit(nukeop_outfit) - - return TRUE - -/datum/antagonist/nukeop/greet() - owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/ops.ogg',100,0, use_reverb = FALSE) - to_chat(owner, span_big("You are a [nuke_team ? nuke_team.syndicate_name : "syndicate"] agent!")) - owner.announce_objectives() - -/datum/antagonist/nukeop/on_gain() - give_alias() - forge_objectives() - owner.current.add_personality(/datum/personality/callous) - . = ..() - equip_op() - if(send_to_spawnpoint) - move_to_spawnpoint() - // grant extra TC for the people who start in the nukie base ie. not the lone op - var/extra_tc = CEILING(GLOB.joined_player_list.len/5, 5) - var/datum/component/uplink/uplink = owner.find_syndicate_uplink() - if (uplink) - uplink.add_telecrystals(extra_tc) - - var/datum/component/uplink/uplink = owner.find_syndicate_uplink() - if(uplink) - var/datum/team/nuclear/nuke_team = get_team() - if(!nuke_team.team_discounts) - var/list/uplink_items = list() - for(var/datum/uplink_item/item as anything in SStraitor.uplink_items) - if(item.item && !item.cant_discount && (item.purchasable_from & uplink.uplink_handler.uplink_flag) && item.cost > 1) - uplink_items += item - nuke_team.team_discounts = list() - nuke_team.team_discounts += create_uplink_sales(discount_team_amount, /datum/uplink_category/discount_team_gear, -1, uplink_items) - nuke_team.team_discounts += create_uplink_sales(discount_limited_amount, /datum/uplink_category/limited_discount_team_gear, 1, uplink_items) - uplink.uplink_handler.extra_purchasable += nuke_team.team_discounts - - memorize_code() - -/datum/antagonist/nukeop/get_team() - return nuke_team - -/datum/antagonist/nukeop/apply_innate_effects(mob/living/mob_override) - add_team_hud(mob_override || owner.current, /datum/antagonist/nukeop) - // future melbert todo - tie to nukeop jobs - owner.set_level(/datum/skill/electronics, SKILL_LEVEL_MASTER, silent = TRUE) - owner.set_level(/datum/skill/eva, SKILL_LEVEL_MASTER, silent = TRUE) - owner.set_level(/datum/skill/firearms, SKILL_LEVEL_MASTER, silent = TRUE) - owner.set_level(/datum/skill/first_aid, SKILL_LEVEL_MASTER, silent = TRUE) - owner.set_level(/datum/skill/athletics, SKILL_LEVEL_EXPERT, silent = TRUE) - owner.set_level(/datum/skill/mechanics, SKILL_LEVEL_MASTER, silent = TRUE) - owner.set_level(/datum/skill/surgery, SKILL_LEVEL_MASTER, silent = TRUE) - owner.set_level(/datum/skill/piloting, SKILL_LEVEL_EXPERT, silent = TRUE) - -/datum/antagonist/nukeop/proc/assign_nuke() - if(nuke_team && !nuke_team.tracked_nuke) - nuke_team.memorized_code = random_nukecode() - var/obj/machinery/nuclearbomb/syndicate/nuke = locate() in SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/syndicate) - 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 - 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 - else - stack_trace("Syndicate nuke not found during nuke team creation.") - nuke_team.memorized_code = null - -/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) - else - var/number = 1 - number = nuke_team.members.Find(owner) - owner.current.real_name = "[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.") - -/datum/antagonist/nukeop/forge_objectives() - if(nuke_team) - objectives |= nuke_team.objectives - -/// Actually moves our nukie to where they should be -/datum/antagonist/nukeop/proc/move_to_spawnpoint() - // Ensure that the nukiebase is loaded, and wait for it if required - SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NUKIEBASE) - var/turf/destination = get_spawnpoint() - owner.current.forceMove(destination) - if(!owner.current.onSyndieBase()) - message_admins("[ADMIN_LOOKUPFLW(owner.current)] is a NUKE OP and move_to_spawnpoint put them somewhere that isn't the syndie base, help please.") - stack_trace("Nuke op move_to_spawnpoint resulted in a location not on the syndicate base. (Was moved to: [destination])") - -/// Gets the position we spawn at -/datum/antagonist/nukeop/proc/get_spawnpoint() - var/team_number = 1 - if(nuke_team) - team_number = nuke_team.members.Find(owner) - - return GLOB.nukeop_start[((team_number - 1) % GLOB.nukeop_start.len) + 1] - -/datum/antagonist/nukeop/leader/get_spawnpoint() - return pick(GLOB.nukeop_leader_start) - -/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 - 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.GetJobType(/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)].") - -/datum/antagonist/nukeop/get_admin_commands() - . = ..() - .["Send to base"] = CALLBACK(src, PROC_REF(admin_send_to_base)) - .["Tell code"] = CALLBACK(src, PROC_REF(admin_tell_code)) - -/datum/antagonist/nukeop/proc/admin_send_to_base(mob/admin) - owner.current.forceMove(pick(GLOB.nukeop_start)) - -/datum/antagonist/nukeop/proc/admin_tell_code(mob/admin) - var/code - for (var/obj/machinery/nuclearbomb/bombue as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb)) - if (length(bombue.r_code) <= 5 && bombue.r_code != initial(bombue.r_code)) - code = bombue.r_code - break - if (code) - antag_memory += "Syndicate Nuclear Bomb Code: [code]
" - to_chat(owner.current, "The nuclear authorization code is: [code]") - else - to_chat(admin, span_danger("No valid nuke found!")) - -/datum/antagonist/nukeop/get_preview_icon() - if (!preview_outfit) - return null - - var/icon/final_icon = render_preview_outfit(preview_outfit) - - if (!isnull(preview_outfit_behind)) - var/icon/teammate = render_preview_outfit(preview_outfit_behind) - teammate.Blend(rgb(128, 128, 128, 128), ICON_MULTIPLY) - - final_icon.Blend(teammate, ICON_UNDERLAY, -world.icon_size / 4, 0) - final_icon.Blend(teammate, ICON_UNDERLAY, world.icon_size / 4, 0) - - if (!isnull(nuke_icon_state)) - var/icon/nuke = icon('icons/obj/machines/nuke.dmi', nuke_icon_state) - nuke.Shift(SOUTH, 6) - final_icon.Blend(nuke, ICON_OVERLAY) - - return finish_preview_icon(final_icon) - -/datum/outfit/nuclear_operative - name = "Nuclear Operative (Preview only)" - - back = /obj/item/mod/control/pre_equipped/empty/syndicate - uniform = /obj/item/clothing/under/syndicate - -/datum/outfit/nuclear_operative_elite - name = "Nuclear Operative (Elite, Preview only)" - - back = /obj/item/mod/control/pre_equipped/empty/elite - uniform = /obj/item/clothing/under/syndicate - l_hand = /obj/item/modular_computer/pda/nukeops - r_hand = /obj/item/shield/energy - -/datum/outfit/nuclear_operative_elite/post_equip(mob/living/carbon/human/H, visualsOnly) - var/obj/item/shield/energy/shield = locate() in H.held_items - shield.icon_state = "[shield.base_icon_state]1" - H.update_held_items() - -/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() - -/datum/antagonist/nukeop/leader/greet() - owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/ops.ogg',100,0, use_reverb = FALSE) - 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.") - 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() - -/datum/antagonist/nukeop/leader/on_gain() - . = ..() - if(!CONFIG_GET(flag/disable_warops)) - var/mob/living/carbon/human/leader = owner.current - var/obj/item/war_declaration = new challengeitem(leader.drop_location()) - leader.put_in_hands(war_declaration) - nuke_team.war_button_ref = WEAKREF(war_declaration) - addtimer(CALLBACK(src, PROC_REF(nuketeam_name_assign)), 1) - -/datum/antagonist/nukeop/leader/proc/nuketeam_name_assign() - if(!nuke_team) - return - nuke_team.rename_team(ask_name()) - -/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) - -/datum/antagonist/nukeop/leader/proc/ask_name() - var/randomname = pick(GLOB.last_names) - var/newname = tgui_input_text(owner.current, "You are the nuclear operative [title]. Please choose a last name for your family.", "Name change", randomname, MAX_NAME_LEN) - if (!newname) - newname = randomname - else - newname = reject_bad_name(newname) - if(!newname) - newname = randomname - - return capitalize(newname) - -/datum/antagonist/nukeop/lone - name = "Lone Operative" - always_new_team = TRUE - send_to_spawnpoint = FALSE //Handled by event - nukeop_outfit = /datum/outfit/syndicate/full - 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/reinforcement - show_in_antagpanel = FALSE - send_to_spawnpoint = FALSE - nukeop_outfit = /datum/outfit/syndicate/reinforcement - -/datum/team/nuclear - var/syndicate_name - var/obj/machinery/nuclearbomb/tracked_nuke - var/core_objective = /datum/objective/nuclear - var/memorized_code - var/list/team_discounts - var/datum/weakref/war_button_ref - -/datum/team/nuclear/New() - ..() - syndicate_name = syndicate_name() - -/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 - if(SSshuttle.emergency.mode == SHUTTLE_ESCAPE) - if(!SSshuttle.emergency.is_in_shuttle_bounds(nuke_disk)) - return FALSE - //If shuttle escaped check if it's on centcom side - else if(SSshuttle.emergency.mode == SHUTTLE_ENDGAME) - if(!nuke_disk.onCentCom()) - return FALSE - else //Otherwise disk is safe when on station - var/turf/disk_turf = get_turf(nuke_disk) - if(!disk_turf || !is_station_level(disk_turf.z)) - return FALSE - return TRUE - -/datum/team/nuclear/proc/are_all_operatives_dead() - for(var/datum/mind/operative_mind as anything in members) - if(ishuman(operative_mind.current) && (operative_mind.current.stat != DEAD)) - return FALSE - return TRUE - -/datum/team/nuclear/proc/get_result() - var/shuttle_evacuated = EMERGENCY_ESCAPED_OR_ENDGAMED - var/shuttle_landed_base = SSshuttle.emergency.is_hijacked() - var/disk_rescued = is_disk_rescued() - var/syndies_didnt_escape = !is_infiltrator_docked_at_syndiebase() - var/team_is_dead = are_all_operatives_dead() - var/station_was_nuked = GLOB.station_was_nuked - var/station_nuke_source = GLOB.station_nuke_source - - // The nuke detonated on the syndicate base - if(station_nuke_source == DETONATION_HIT_SYNDIE_BASE) - return NUKE_RESULT_FLUKE - - // The station was nuked - if(station_was_nuked) - // The station was nuked and the infiltrator failed to escape - if(syndies_didnt_escape) - return NUKE_RESULT_NOSURVIVORS - // The station was nuked and the infiltrator escaped, and the nuke ops won - else - return NUKE_RESULT_NUKE_WIN - - // The station was not nuked, but something was - else if(station_nuke_source && !disk_rescued) - // The station was not nuked, but something was, and the syndicates didn't escape it - if(syndies_didnt_escape) - return NUKE_RESULT_WRONG_STATION_DEAD - // The station was not nuked, but something was, and the syndicates returned to their base - else - return NUKE_RESULT_WRONG_STATION - - // Nuke didn't blow, but nukies somehow hijacked the emergency shuttle to land at the base anyways. - else if(shuttle_landed_base) - if(disk_rescued) - return NUKE_RESULT_HIJACK_DISK - else - return NUKE_RESULT_HIJACK_NO_DISK - - // No nuke went off, the station rescued the disk - else if(disk_rescued) - // No nuke went off, the shuttle left, and the team is dead - if(shuttle_evacuated && team_is_dead) - return NUKE_RESULT_CREW_WIN_SYNDIES_DEAD - // No nuke went off, but the nuke ops survived - else - return NUKE_RESULT_CREW_WIN - - // No nuke went off, but the disk was left behind - else - // No nuke went off, the disk was left, but all the ops are dead - if(team_is_dead) - return NUKE_RESULT_DISK_LOST - // No nuke went off, the disk was left, there are living ops, but the shuttle left successfully - else if(shuttle_evacuated) - return NUKE_RESULT_DISK_STOLEN - - CRASH("[type] - got an undefined / unexpected result.") - -/datum/team/nuclear/roundend_report() - var/list/parts = list() - parts += "[syndicate_name] Operatives:" - - switch(get_result()) - if(NUKE_RESULT_FLUKE) - parts += "Humiliating Syndicate Defeat" - parts += "The crew of [station_name()] gave [syndicate_name] operatives back their bomb! The syndicate base was destroyed! Next time, don't lose the nuke!" - if(NUKE_RESULT_NUKE_WIN) - parts += "Syndicate Major Victory!" - parts += "[syndicate_name] operatives have destroyed [station_name()]!" - if(NUKE_RESULT_NOSURVIVORS) - parts += "Total Annihilation!" - parts += "[syndicate_name] operatives destroyed [station_name()] but did not leave the area in time and got caught in the explosion. Next time, don't lose the disk!" - if(NUKE_RESULT_WRONG_STATION) - parts += "Crew Minor Victory!" - parts += "[syndicate_name] operatives secured the authentication disk but blew up something that wasn't [station_name()]. Next time, don't do that!" - if(NUKE_RESULT_WRONG_STATION_DEAD) - parts += "[syndicate_name] operatives have earned Darwin Award!" - parts += "[syndicate_name] operatives blew up something that wasn't [station_name()] and got caught in the explosion. Next time, don't do that!" - if(NUKE_RESULT_HIJACK_DISK) - parts += "Syndicate Miniscule Victory!" - parts += "[syndicate_name] operatives failed to destroy [station_name()], but they managed to secure the disk and hijack the emergency shuttle, causing it to land on the syndicate base. Good job?" - if(NUKE_RESULT_HIJACK_NO_DISK) - parts += "Syndicate Insignificant Victory!" - parts += "[syndicate_name] operatives failed to destroy [station_name()] or secure the disk, but they managed to hijack the emergency shuttle, causing it to land on the syndicate base. Good job?" - if(NUKE_RESULT_CREW_WIN_SYNDIES_DEAD) - parts += "Crew Major Victory!" - parts += "The Research Staff has saved the disk and killed the [syndicate_name] Operatives" - if(NUKE_RESULT_CREW_WIN) - parts += "Crew Major Victory!" - parts += "The Research Staff has saved the disk and stopped the [syndicate_name] Operatives!" - if(NUKE_RESULT_DISK_LOST) - parts += "Neutral Victory!" - parts += "The Research Staff failed to secure the authentication disk but did manage to kill most of the [syndicate_name] Operatives!" - if(NUKE_RESULT_DISK_STOLEN) - parts += "Syndicate Minor Victory!" - parts += "[syndicate_name] operatives survived the assault but did not achieve the destruction of [station_name()]. Next time, don't lose the disk!" - else - parts += "Neutral Victory" - parts += "Mission aborted!" - - var/text = "
The syndicate operatives were:" - var/purchases = "" - var/TC_uses = 0 - LAZYINITLIST(GLOB.uplink_purchase_logs_by_key) - for(var/I in members) - var/datum/mind/syndicate = I - var/datum/uplink_purchase_log/H = GLOB.uplink_purchase_logs_by_key[syndicate.key] - if(H) - TC_uses += H.total_spent - purchases += H.generate_render(show_key = FALSE) - text += printplayerlist(members) - text += "
" - text += "(Syndicates used [TC_uses] TC) [purchases]" - if(TC_uses == 0 && GLOB.station_was_nuked && !are_all_operatives_dead()) - text += "[icon2html('icons/ui_icons/antags/badass.dmi', world, "badass")]" - - parts += text - - return "
[parts.Join("
")]
" - -/datum/team/nuclear/antag_listing_name() - if(syndicate_name) - return "[syndicate_name] Syndicates" - else - return "Syndicates" - -/datum/team/nuclear/antag_listing_entry() - var/disk_report = "Nuclear Disk(s)
" - disk_report += "" - for(var/obj/item/disk/nuclear/N in SSpoints_of_interest.real_nuclear_disks) - disk_report += "" // Non-module change : 516 byond:// - disk_report += "
[N.name], " - var/atom/disk_loc = N.loc - while(!isturf(disk_loc)) - if(ismob(disk_loc)) - var/mob/M = disk_loc - disk_report += "carried by [M.real_name] " // Non-module change : 516 byond:// - if(isobj(disk_loc)) - var/obj/O = disk_loc - disk_report += "in \a [O.name] " - disk_loc = disk_loc.loc - disk_report += "in [disk_loc.loc] at ([disk_loc.x], [disk_loc.y], [disk_loc.z])FLW
" - - var/post_report - - var/war_declared = FALSE - for(var/obj/item/circuitboard/computer/syndicate_shuttle/board as anything in GLOB.syndicate_shuttle_boards) - if(board.challenge) - war_declared = TRUE - - var/force_war_button = "" - - if(war_declared) - post_report += "War declared." - force_war_button = "\[Force war\]" - else - post_report += "War not declared." - var/obj/item/nuclear_challenge/war_button = war_button_ref?.resolve() - if(war_button) - force_war_button = "\[Force war\]" // Non-module change : 516 byond:// - else - force_war_button = "\[Cannot declare war, challenge button missing!\]" - - post_report += "\n[force_war_button]" - post_report += "\n\[Send Reinforcement\]" // Non-module change : 516 byond:// - - var/final_report = ..() - final_report += disk_report - final_report += post_report - return final_report - -#define SPAWN_AT_BASE "Nuke base" -#define SPAWN_AT_INFILTRATOR "Infiltrator" - -/datum/team/nuclear/proc/admin_spawn_reinforcement(mob/admin) - if(!check_rights_for(admin.client, R_ADMIN)) - return - - var/infil_or_nukebase = tgui_alert( - admin, - "Spawn them at the nuke base, or in the Infiltrator?", - "Where to reinforce?", - list(SPAWN_AT_BASE, SPAWN_AT_INFILTRATOR, "Cancel"), - ) - - if(!infil_or_nukebase || infil_or_nukebase == "Cancel") - return - - var/tc_to_spawn = tgui_input_number(admin, "How much TC to spawn with?", "TC", 0, 100) - - var/list/nuke_candidates = SSpolling.poll_ghost_candidates( - "Do you want to play as an emergency syndicate reinforcement?", - check_jobban = ROLE_OPERATIVE, - role = ROLE_OPERATIVE, - poll_time = 30 SECONDS, - ignore_category = POLL_IGNORE_SYNDICATE, - pic_source = /obj/structure/sign/poster/contraband/gorlex_recruitment, - role_name_text = "syndicate reinforcement", - ) - - nuke_candidates -= admin // may be easy to fat-finger say yes. so just don't - - if(!length(nuke_candidates)) - tgui_alert(admin, "No candidates found.", "Recruitment Shortage", list("OK")) - return - - - var/turf/spawn_loc - if(infil_or_nukebase == SPAWN_AT_INFILTRATOR) - var/area/spawn_in - // Prioritize EVA then hallway, if neither can be found default to the first area we can find - for(var/area_type in list(/area/shuttle/syndicate/eva, /area/shuttle/syndicate/hallway, /area/shuttle/syndicate)) - spawn_in = locate(area_type) in GLOB.areas // I'd love to use areas_by_type but the Infiltrator is a unique area - if(spawn_in) - break - - var/list/turf/options = list() - for(var/turf/open/open_turf in spawn_in?.get_turfs_from_all_zlevels()) - if(open_turf.is_blocked_turf()) - continue - options += open_turf - - if(length(options)) - spawn_loc = pick(options) - else - infil_or_nukebase = SPAWN_AT_BASE - - if(infil_or_nukebase == SPAWN_AT_BASE) - spawn_loc = pick(GLOB.nukeop_start) - - var/mob/dead/observer/picked = pick(nuke_candidates) - var/mob/living/carbon/human/nukie = new(spawn_loc) - picked.client.prefs.safe_transfer_prefs_to(nukie, is_antag = TRUE) - nukie.key = picked.key - - var/datum/antagonist/nukeop/antag_datum = new() - antag_datum.send_to_spawnpoint = FALSE - antag_datum.nukeop_outfit = /datum/outfit/syndicate/reinforcement - - nukie.mind.add_antag_datum(antag_datum, src) - - var/datum/component/uplink/uplink = nukie.mind.find_syndicate_uplink() - uplink?.set_telecrystals(tc_to_spawn) - - // add some pizzazz - do_sparks(4, FALSE, spawn_loc) - new /obj/effect/temp_visual/teleport_abductor/syndi_teleporter(spawn_loc) - playsound(spawn_loc, SFX_SPARKS, 50, TRUE) - playsound(spawn_loc, 'sound/effects/phasein.ogg', 50, TRUE) - - tgui_alert(admin, "Reinforcement spawned at [infil_or_nukebase] with [tc_to_spawn].", "Reinforcements have arrived", list("God speed")) - -#undef SPAWN_AT_BASE -#undef SPAWN_AT_INFILTRATOR - -/// Returns whether or not syndicate operatives escaped. -/proc/is_infiltrator_docked_at_syndiebase() - var/obj/docking_port/mobile/infiltrator/infiltrator_port = SSshuttle.getShuttle("syndicate") - - var/datum/lazy_template/nukie_base/nukie_template = GLOB.lazy_templates[LAZY_TEMPLATE_KEY_NUKIEBASE] - if(!nukie_template) - return FALSE // if its not even loaded, cant be docked - - for(var/datum/turf_reservation/loaded_area as anything in nukie_template.reservations) - var/infiltrator_turf = get_turf(infiltrator_port) - if(infiltrator_turf in loaded_area.reserved_turfs) - return TRUE - return FALSE diff --git a/code/modules/antagonists/nukeop/outfits.dm b/code/modules/antagonists/nukeop/outfits.dm index e58507d31648..9aa08dcdbaa6 100644 --- a/code/modules/antagonists/nukeop/outfits.dm +++ b/code/modules/antagonists/nukeop/outfits.dm @@ -79,6 +79,10 @@ /obj/item/pen/edagger = 1, ) +/datum/outfit/syndicate/full/loneop + name = "Syndicate Operative - Full Kit (Loneop)" + uplink_type = /obj/item/uplink/loneop + /datum/outfit/syndicate/full/plasmaman name = "Syndicate Operative - Full Kit (Plasmaman)" back = /obj/item/mod/control/pre_equipped/nuclear/plasmaman @@ -163,3 +167,22 @@ shoes = /obj/item/clothing/shoes/laceup glasses = /obj/item/clothing/glasses/sunglasses/big faction = "MI13" + +/datum/outfit/nuclear_operative + name = "Nuclear Operative (Preview only)" + + back = /obj/item/mod/control/pre_equipped/empty/syndicate + uniform = /obj/item/clothing/under/syndicate + +/datum/outfit/nuclear_operative_elite + name = "Nuclear Operative (Elite, Preview only)" + + back = /obj/item/mod/control/pre_equipped/empty/elite + uniform = /obj/item/clothing/under/syndicate + l_hand = /obj/item/modular_computer/pda/nukeops + r_hand = /obj/item/shield/energy + +/datum/outfit/nuclear_operative_elite/post_equip(mob/living/carbon/human/H, visuals_only) + var/obj/item/shield/energy/shield = locate() in H.held_items + shield.icon_state = "[shield.base_icon_state]1" + H.update_held_items() diff --git a/code/modules/antagonists/obsessed/obsessed.dm b/code/modules/antagonists/obsessed/obsessed.dm index 6fb2267904a6..9595463c1ebb 100644 --- a/code/modules/antagonists/obsessed/obsessed.dm +++ b/code/modules/antagonists/obsessed/obsessed.dm @@ -2,18 +2,30 @@ 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 hardcore_random_bonus = TRUE + stinger_sound = 'sound/ambience/antag/creepalert.ogg' var/datum/brain_trauma/special/obsessed/trauma +/// Dummy antag datum that will show the cured obsessed to admins +/datum/antagonist/former_obsessed + name = "Former Obsessed" + show_in_antagpanel = FALSE + show_name_in_check_antagonists = TRUE + antagpanel_category = ANTAG_GROUP_CREW + show_in_roundend = FALSE + antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST + silent = TRUE + can_elimination_hijack = ELIMINATION_PREVENT + /datum/antagonist/obsessed/admin_add(datum/mind/new_owner,mob/admin) var/mob/living/carbon/C = new_owner.current if(!istype(C)) @@ -169,7 +181,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 e809e8cecbf0..e9c07c581e68 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 ..() @@ -52,7 +41,7 @@ kill.update_explanation_text() objectives += kill - owner.set_assigned_role(SSjob.GetJobType(/datum/job/paradox_clone)) + owner.set_assigned_role(SSjob.get_job_type(/datum/job/paradox_clone)) //clone doesnt show up on message lists var/obj/item/modular_computer/pda/messenger = locate() in owner.current @@ -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 5f21449163e3..4ee393f396c1 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 f3c6655a2757..000000000000 --- 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) - SScommunications.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 pirate crew of [chosen_gang.name]?", check_jobban = ROLE_TRAITOR, pic_source = /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 aed278d261d9..d5a70ecb402c 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 90a6431d428b..417ee0d543ea 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 b22a4ec0a7e5..d70511038ab6 100644 --- a/code/modules/antagonists/revolution/revolution.dm +++ b/code/modules/antagonists/revolution/revolution.dm @@ -2,10 +2,11 @@ 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!!" + stinger_sound = 'sound/ambience/antag/revolutionary_tide.ogg' var/datum/team/revolution/rev_team /// When this antagonist is being de-antagged, this is the source. Can be a mob (for mindshield/blunt force trauma) or a #define string. @@ -56,14 +57,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!")) @@ -72,16 +68,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.") @@ -90,12 +79,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 @@ -173,7 +156,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 @@ -297,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() @@ -337,7 +319,9 @@ 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) deconversion_source = deconverter owner.remove_antag_datum(type) @@ -374,40 +358,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() - - /// 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() + var/list/datum/mind/ex_revs = list() - 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() @@ -419,129 +382,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() - - 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 + var/list/datum/mind/head_revolutionaries = get_head_revolutionaries() -/// 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) @@ -586,14 +459,14 @@ if(headrevs.len) var/list/headrev_part = list() - headrev_part += "The head revolutionaries were:" - headrev_part += printplayerlist(headrevs, !check_rev_victory()) + headrev_part += span_header("The head revolutionaries were:") + headrev_part += printplayerlist(headrevs, GLOB.revolution_handler.result != REVOLUTION_VICTORY) result += headrev_part.Join("
") if(revs.len) var/list/rev_part = list() - rev_part += "The revolutionaries were:" - rev_part += printplayerlist(revs, !check_rev_victory()) + rev_part += span_header("The revolutionaries were:") + 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 000000000000..1b68205e36ea --- /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 d1197265ced5..79008f082398 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 4b06452c4b79..4d7790e320a6 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 9c4d418426be..2725e7cba6d8 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,13 +66,11 @@ /datum/antagonist/space_dragon/on_gain() forge_objectives() rift_ability = new() - owner.special_role = ROLE_SPACE_DRAGON - owner.set_assigned_role(SSjob.GetJobType(/datum/job/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.GetJobType(/datum/job/unassigned)) + owner.set_assigned_role(SSjob.get_job_type(/datum/job/unassigned)) return ..() /datum/antagonist/space_dragon/apply_innate_effects(mob/living/mob_override) diff --git a/code/modules/antagonists/space_ninja/space_ninja.dm b/code/modules/antagonists/space_ninja/space_ninja.dm index 2d49138fc450..c0f3db8b1186 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 @@ -122,13 +122,18 @@ equip_space_ninja(owner.current) owner.current.add_quirk(/datum/quirk/freerunning, announce = FALSE) owner.current.add_quirk(/datum/quirk/light_step, announce = FALSE) - owner.current.mind.set_assigned_role(SSjob.GetJobType(/datum/job/space_ninja)) - owner.current.mind.special_role = ROLE_NINJA + owner.current.mind.set_assigned_role(SSjob.get_job_type(/datum/job/space_ninja)) return ..() /datum/antagonist/ninja/admin_add(datum/mind/new_owner,mob/admin) - new_owner.set_assigned_role(SSjob.GetJobType(/datum/job/space_ninja)) - new_owner.special_role = ROLE_NINJA + new_owner.set_assigned_role(SSjob.get_job_type(/datum/job/space_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/survivalist/survivalist.dm b/code/modules/antagonists/survivalist/survivalist.dm index 2480b186600a..cef6a8f61f02 100644 --- a/code/modules/antagonists/survivalist/survivalist.dm +++ b/code/modules/antagonists/survivalist/survivalist.dm @@ -11,7 +11,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 eae5d558a7cd..84ec707a8688 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/README.md b/code/modules/antagonists/traitor/README.md deleted file mode 100644 index 360d1bd54ebf..000000000000 --- a/code/modules/antagonists/traitor/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Progression Traitor Balance Guide - -This guide will explain how the values for progression traitor works, how to balance progression traitors and what you should NOT do when balancing. -This guide will only explain progression values. - -## Definitions - -- Progression points OR Progression - A currency that controls what uplink items a player can purchase and what objectives they have accessible to them. Gained passively or by completing objectives and has diminishing returns as it strays from the expected progression -- Expected Progression - A global value that increments by a value of 1 minute every minute, representing the 'time' that a player should be at if they had not completed any objectives. -- Objectives - An activity or job that a player can take for rewards such as TC and progression points. -- Player - The user(s) that are playing as the antagonist in this new system. -- Expected deviance - The amount of deviance that can be expected from the minimum and maximum progressions. Usually calculated by `progression_scaling_deviance` + `progression_scaling_deviance` * `global_progression_deviance_required` (explained further down) - -## How it works - -This section will explain how the entire balance system works. This is an overview of the entire system. - -### Progression - -Progression points is passively given to a player, and are represented as minutes (or time values) in code. The round has its own 'expected progression', which is the progression value that you'd normally have if you hadn't completed any objectives whatsoever. This is the baseline progression that all players will be at unless they're a latejoiner, and it acts as the basis for determining how much progression points a player should get over time and the cost of objectives for a specific player, if they deviate too much from this value. The idea is that they will slowly drift back towards the expected progression if they do nothing and it becomes harder for them to progress as they deviate further from the expected progression. The amount that is passively given can also vary depending on how many players there are, so that at lower populations, expected progression rises more slowly. - -### Objectives - -Objectives are worth a certain amount of progression points, determined by the code. However, this can be scaled to be less if the player taking them is ahead of the expected progression. This scales exponentially, so that as a player deviates further from the expected progression, the reward diminishes exponentially, up to a reduced value of 90%. The similar thing happens in the opposite direction, with people who are lower than the expected progression getting more progression than usual for completing objectives. - -## How to balance - -### The traitor subsystem -- `newjoin_progression_coeff` - The coefficient multiplied by the expected progression for new joining traitors to calculate their starting progression, so that they don't start from scratch -- `progression_scaling_deviance` - The value that the entire system revolves around. This determines how your disadvantages are calculated, if you stray from the expected progression. Having a progression value that is `progression_scaling_deviance` minutes off of the expected progression means that you won't get any progression at all, and if objectives were configured to suit this, you'd have the highest reduction you can possibly get. From the expected progression to this value, it scales linearly and it also works in the opposite direction. -- `current_progression_scaling` - Defined at compile time, this determines how fast expected progression scales. So if you have it set to 0.5 MINUTES, it'll take twice as long to unlock uplink items and new objectives. -- `CONFIG:TRAITOR_IDEAL_PLAYER_COUNT` - The ideal player count before expected progression stops increasing. If the living player list gets below this value, the current progression scaling will be multiplied by player_count/traitor_ideal_player_count. In essence, this makes it so that progression scales more slowly when there isn't a lot of people alive. - -If you want to balance how fast the system progresses, you should look at modifying `current_progression_scaling`. If you want to balance how far someone should be allowed to deviate, you should look at modifying `progression-scaling-deviance` - -### Objectives -- `progression_minimum` - The minimum number of progression points required before this objective can show up as a potential objective -- `progression_maximum` - The maximum number of progression points before this objective stops showing up as a potential objective, used to prevent roundstart objectives from showing up during the late game. -- `progression_reward` - The progression reward you get from completing an objective. This is the base value, and can also be a two element list of numbers if you want it to be random. This value is then scaled depending on whether a player is ahead or behind the expected progression -- `global_progression_influence_intensity` - Determines how influential expected progression will affect the progression reward of this objective. Set to 0 to disable. -- `global_progression_deviance_required` - Determines how much deviance is required before the scaling kicks in, to give objectives more leeway so that at the `progression_scaling_deviance`, it doesn't scale to 90% immediately. -- `progression_cost_coeff_deviance` - This determines the randomness of the progression reward, to prevent all of the scaling from looking the same. Becomes a lot less significant as the scaling variable gets closer to 1. - -If you want to balance the expected timeframe an objective should be available, you should look at changing the `progression_minimum` or `progression_maximum`. If you want to balance how much objectives reward, you may want to look at modifying `progression_reward`. If you want to look at balancing the cost of an objective depending on the expected progression, you may want to look at `global_progression_influence_intensity`. If you want to look at decreasing or increasing the deviance allowed before objectives become worthless progression-wise, you may want to look at modifying `global_progression_deviance_required` - -### Uplink Items -- `progression_minimum` - The minimum number of progression points required to purchase this uplink item. - -## What NOT to do when balancing - -### Overcompensate - -You do not want to overcompensate variables such as `progression_minimum` and `progression_maximum`. Such values need to be an accurate representation of roughly around the time a player should unlock the objective or uplink item. progression_scaling_deviance is supposed to represents the limit that a casual player can be at before it becomes significantly harder for them to progress throughout. You should expect people to be within `progression_scaling_deviance` + `progression_scaling_deviance` * `global_progression_deviance_required`. (Assuming `progression_scaling_deviance` is 20 minutes and `progression_scaling_deviance_required` is 0.5, 20 + 0.5 * 20 = 30; this gives us a value of 30 minutes). This is the expected deviance. - -### Reward large amounts of progression points - -Progression points are passively gained, so rewarding large amounts of progression points will let people bypass the scaling as they'll immediately jump to an absurd value. A good rule of thumb is to always keep the reward within or below the expected deviance. - diff --git a/code/modules/antagonists/traitor/balance_helper.dm b/code/modules/antagonists/traitor/balance_helper.dm deleted file mode 100644 index b2a9661bfeb5..000000000000 --- a/code/modules/antagonists/traitor/balance_helper.dm +++ /dev/null @@ -1,109 +0,0 @@ -ADMIN_VERB(debug_traitor_objectives, R_DEBUG, "Debug Traitor Objectives", "Verify functionality of traitor goals.", ADMIN_CATEGORY_DEBUG) - SStraitor.traitor_debug_panel?.ui_interact(user.mob) - -/datum/traitor_objective_debug - var/list/all_objectives - -/datum/traitor_objective_debug/New(datum/traitor_category_handler/category_handler) - . = ..() - all_objectives = list() - for(var/datum/traitor_objective_category/category as anything in category_handler.all_categories) - var/list/generated_list = list() - var/list/current_list = category.objectives - for(var/value in category.objectives) - if(islist(value)) - generated_list += list(list( - "objectives" = recursive_list_generate(value), - "weight" = current_list[value] - )) - else - generated_list += list(generate_objective_data(value, current_list[value])) - all_objectives += list(list( - "name" = category.name, - "objectives" = generated_list, - "weight" = category.weight, - )) - -/datum/traitor_objective_debug/proc/recursive_list_generate(list/to_check) - var/list/generated_list = list() - for(var/value in to_check) - if(islist(value)) - generated_list += list(list( - "objectives" = recursive_list_generate(value), - "weight" = to_check[value] - )) - else - generated_list += list(generate_objective_data(value, to_check[value])) - return generated_list - -/datum/traitor_objective_debug/proc/generate_objective_data(datum/traitor_objective/objective_type, weight) - // Need to set this to false before we create the new objective to prevent init from fucking it up - SStraitor.generate_objectives = FALSE - var/datum/traitor_objective/objective = new objective_type() - var/list/return_data = list( - "name" = objective.name, - "description" = objective.description, - "progression_minimum" = objective.progression_minimum, - "progression_maximum" = objective.progression_maximum, - "global_progression" = objective.global_progression_deviance_required, - "global_progression_limit_coeff" = objective.global_progression_limit_coeff, - "global_progression_influence_intensity" = objective.global_progression_influence_intensity, - "progression_reward" = objective.progression_reward, - "telecrystal_reward" = objective.telecrystal_reward, - "telecrystal_penalty" = objective.telecrystal_penalty, - "weight" = weight, - "type" = objective.type, - ) - qdel(objective) - SStraitor.generate_objectives = TRUE - return return_data - -/datum/traitor_objective_debug/ui_interact(mob/user, datum/tgui/ui) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "TraitorObjectiveDebug") - ui.open() - -/datum/traitor_objective_debug/ui_data(mob/user) - var/list/data = list() - data["current_progression"] = SStraitor.current_global_progression - var/list/handlers = SStraitor.uplink_handlers - var/list/handler_data = list() - for(var/datum/uplink_handler/handler as anything in handlers) - var/total_progression_from_objectives = 0 - for(var/datum/traitor_objective/objective as anything in handler.completed_objectives) - if(objective.objective_state != OBJECTIVE_STATE_COMPLETED) - continue - total_progression_from_objectives += objective.progression_reward - handler_data += list(list( - "player" = handler.owner?.key, - "progression_points" = handler.progression_points, - "total_progression_from_objectives" = total_progression_from_objectives - )) - data["player_data"] = handler_data - return data - -/datum/traitor_objective_debug/ui_static_data(mob/user) - var/list/data = list() - data["objective_data"] = all_objectives - data["progression_scaling_deviance"] = SStraitor.progression_scaling_deviance - return data - -/datum/traitor_objective_debug/ui_state(mob/user) - return GLOB.admin_state - -/datum/traitor_objective_debug/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) - . = ..() - if(.) - return - - switch(action) - if("set_current_expected_progression") - SStraitor.current_global_progression = text2num(params["new_expected_progression"]) - return TRUE - if("generate_json") - var/temp_file = file("data/TraitorObjectiveDownloadTempFile") - fdel(temp_file) - WRITE_FILE(temp_file, all_objectives) - DIRECT_OUTPUT(ui.user, ftp(temp_file, "TraitorObjectiveData.json")) - return TRUE diff --git a/code/modules/antagonists/traitor/components/demoraliser.dm b/code/modules/antagonists/traitor/components/demoraliser.dm index ee44527728c0..3d0ddf1fe3d5 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/components/traitor_objective_helpers.dm b/code/modules/antagonists/traitor/components/traitor_objective_helpers.dm deleted file mode 100644 index 6bf5ce5d117a..000000000000 --- a/code/modules/antagonists/traitor/components/traitor_objective_helpers.dm +++ /dev/null @@ -1,54 +0,0 @@ -/// Helper component that registers signals on an object -/// This is not necessary to use and gives little control over the conditions -/datum/component/traitor_objective_register - dupe_mode = COMPONENT_DUPE_ALLOWED - - /// The target to apply the succeed/fail signals onto - var/datum/target - /// Signals to listen out for to automatically succeed the objective - var/succeed_signals - /// Signals to listen out for to automatically fail the objective. - var/fail_signals - /// Whether failing has a penalty - var/penalty = 0 - -/datum/component/traitor_objective_register/Initialize(datum/target, succeed_signals, fail_signals, penalty) - . = ..() - if(!istype(parent, /datum/traitor_objective)) - return COMPONENT_INCOMPATIBLE - src.target = target - src.succeed_signals = succeed_signals - src.fail_signals = fail_signals - src.penalty = penalty - -/datum/component/traitor_objective_register/RegisterWithParent() - if(succeed_signals) - RegisterSignals(target, succeed_signals, PROC_REF(on_success)) - if(fail_signals) - RegisterSignals(target, fail_signals, PROC_REF(on_fail)) - RegisterSignals(parent, list(COMSIG_TRAITOR_OBJECTIVE_COMPLETED, COMSIG_TRAITOR_OBJECTIVE_FAILED), PROC_REF(delete_self)) - -/datum/component/traitor_objective_register/UnregisterFromParent() - if(target) - if(succeed_signals) - UnregisterSignal(target, succeed_signals) - if(fail_signals) - UnregisterSignal(target, fail_signals) - UnregisterSignal(parent, list( - COMSIG_TRAITOR_OBJECTIVE_COMPLETED, - COMSIG_TRAITOR_OBJECTIVE_FAILED - )) - -/datum/component/traitor_objective_register/proc/on_fail(datum/traitor_objective/source) - SIGNAL_HANDLER - var/datum/traitor_objective/objective = parent - objective.fail_objective(penalty) - -/datum/component/traitor_objective_register/proc/on_success() - SIGNAL_HANDLER - var/datum/traitor_objective/objective = parent - objective.succeed_objective() - -/datum/component/traitor_objective_register/proc/delete_self() - SIGNAL_HANDLER - qdel(src) diff --git a/code/modules/antagonists/traitor/components/traitor_objective_limit_per_time.dm b/code/modules/antagonists/traitor/components/traitor_objective_limit_per_time.dm deleted file mode 100644 index ce72e1227de0..000000000000 --- a/code/modules/antagonists/traitor/components/traitor_objective_limit_per_time.dm +++ /dev/null @@ -1,41 +0,0 @@ -/// Helper component to track events on -/datum/component/traitor_objective_limit_per_time - dupe_mode = COMPONENT_DUPE_HIGHLANDER - - /// The maximum time that an objective will be considered for. Set to -1 to accept any time. - var/time_period = 0 - /// The maximum amount of objectives that can be active or recently active at one time - var/maximum_objectives = 0 - /// The typepath which we check for - var/typepath - -/datum/component/traitor_objective_limit_per_time/Initialize(typepath, time_period, maximum_objectives) - . = ..() - if(!istype(parent, /datum/traitor_objective)) - return COMPONENT_INCOMPATIBLE - src.time_period = time_period - src.maximum_objectives = maximum_objectives - src.typepath = typepath - if(!typepath) - src.typepath = parent.type - -/datum/component/traitor_objective_limit_per_time/RegisterWithParent() - RegisterSignal(parent, COMSIG_TRAITOR_OBJECTIVE_PRE_GENERATE, PROC_REF(handle_generate)) - -/datum/component/traitor_objective_limit_per_time/UnregisterFromParent() - UnregisterSignal(parent, COMSIG_TRAITOR_OBJECTIVE_PRE_GENERATE) - - -/datum/component/traitor_objective_limit_per_time/proc/handle_generate(datum/traitor_objective/source, datum/mind/owner, list/potential_duplicates) - SIGNAL_HANDLER - var/datum/uplink_handler/handler = source.handler - if(!handler) - return - var/count = 0 - for(var/datum/traitor_objective/objective as anything in handler.potential_duplicate_objectives[typepath]) - if(time_period != -1 && objective.objective_state != OBJECTIVE_STATE_INACTIVE && (world.time - objective.time_of_completion) > time_period) - continue - count++ - - if(count >= maximum_objectives) - return COMPONENT_TRAITOR_OBJECTIVE_ABORT_GENERATION diff --git a/code/modules/antagonists/traitor/components/traitor_objective_mind_tracker.dm b/code/modules/antagonists/traitor/components/traitor_objective_mind_tracker.dm deleted file mode 100644 index 01d1febb9ac7..000000000000 --- a/code/modules/antagonists/traitor/components/traitor_objective_mind_tracker.dm +++ /dev/null @@ -1,40 +0,0 @@ -/// Helper component to track events on -/datum/component/traitor_objective_mind_tracker - dupe_mode = COMPONENT_DUPE_ALLOWED - - /// The target to track - var/datum/mind/target - /// Signals to listen out for mapped to procs to call - var/list/signals - /// Current registered target - var/mob/current_registered_target - -/datum/component/traitor_objective_mind_tracker/Initialize(datum/target, signals) - . = ..() - if(!istype(parent, /datum/traitor_objective)) - return COMPONENT_INCOMPATIBLE - src.target = target - src.signals = signals - -/datum/component/traitor_objective_mind_tracker/RegisterWithParent() - RegisterSignal(target, COMSIG_MIND_TRANSFERRED, PROC_REF(handle_mind_transferred)) - RegisterSignal(target, COMSIG_QDELETING, PROC_REF(delete_self)) - RegisterSignals(parent, list(COMSIG_TRAITOR_OBJECTIVE_COMPLETED, COMSIG_TRAITOR_OBJECTIVE_FAILED), PROC_REF(delete_self)) - handle_mind_transferred(target) - -/datum/component/traitor_objective_mind_tracker/UnregisterFromParent() - UnregisterSignal(target, COMSIG_MIND_TRANSFERRED) - if(target.current) - parent.UnregisterSignal(target.current, signals) - -/datum/component/traitor_objective_mind_tracker/proc/handle_mind_transferred(datum/source, mob/previous_body) - SIGNAL_HANDLER - if(current_registered_target) - parent.UnregisterSignal(current_registered_target, signals) - - for(var/signal in signals) - parent.RegisterSignal(target.current, signal, signals[signal]) - -/datum/component/traitor_objective_mind_tracker/proc/delete_self() - SIGNAL_HANDLER - qdel(src) diff --git a/code/modules/antagonists/traitor/contractor/contract_teammate.dm b/code/modules/antagonists/traitor/contractor/contract_teammate.dm index c97af9cfc812..c62e8aee7942 100644 --- a/code/modules/antagonists/traitor/contractor/contract_teammate.dm +++ b/code/modules/antagonists/traitor/contractor/contract_teammate.dm @@ -1,35 +1,8 @@ -///Spawns a contractor partner to a spawning user, with a given key to assign to the new player. -/proc/spawn_contractor_partner(mob/living/user, key) - var/mob/living/carbon/human/partner = new() - var/datum/outfit/contractor_partner/partner_outfit = new() - - partner_outfit.equip(partner) - - var/obj/structure/closet/supplypod/arrival_pod = new(null, /datum/pod_style/syndicate) // Non-module change : removed upstream - arrival_pod.explosionSize = list(0,0,0,1) - arrival_pod.bluespace = TRUE - - var/turf/free_location = find_obstruction_free_location(2, user) - - // We really want to send them - if we can't find a nice location just land it on top of them. - if (!free_location) - free_location = get_turf(user) - - partner.forceMove(arrival_pod) - partner.ckey = key - - /// We give a reference to the mind that'll be the support unit - var/datum/antagonist/traitor/contractor_support/new_datum = partner.mind.add_antag_datum(/datum/antagonist/traitor/contractor_support) - - to_chat(partner, "\n[span_alertwarning("[user.real_name] is your superior. Follow any, and all orders given by them. You're here to support their mission only.")]") - to_chat(partner, "[span_alertwarning("Should they perish, or be otherwise unavailable, you're to assist other active agents in this mission area to the best of your ability.")]") - - new /obj/effect/pod_landingzone(free_location, arrival_pod) - return new_datum - /// Support unit gets it's own very basic antag datum for admin logging. /datum/antagonist/traitor/contractor_support name = "Contractor Support Unit" + pref_flag = ROLE_CONTRACTOR_SUPPORT + employer = "Contractor Support Unit" show_in_roundend = FALSE give_objectives = TRUE give_uplink = FALSE diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index c545d3dae532..9a71a5c3e51e 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -1,13 +1,13 @@ ///all the employers that are syndicate #define FLAVOR_FACTION_SYNDICATE "syndicate" -///all the employers that are nanotrasen +///all the employers that are Nanotrasen #define FLAVOR_FACTION_NANOTRASEN "nanotrasen" /datum/antagonist/traitor 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 @@ -17,24 +17,19 @@ can_assign_self_objectives = TRUE default_custom_objective = "Perform an overcomplicated heist on valuable Nanotrasen assets." hardcore_random_bonus = TRUE + stinger_sound = 'sound/ambience/antag/tatoralert.ogg' ///The flag of uplink that this traitor is supposed to have. var/uplink_flag_given = UPLINK_TRAITORS var/give_objectives = TRUE - /// Whether to give secondary objectives to the traitor, which aren't necessary but can be completed for a progression and TC boost. - var/give_secondary_objectives = TRUE var/should_give_codewords = TRUE ///give this traitor an uplink? var/give_uplink = TRUE - /// Code that allows traitor to get a replacement uplink - var/replacement_uplink_code = "" - /// Radio frequency that traitor must speak on to get a replacement uplink - var/replacement_uplink_frequency = "" ///if TRUE, this traitor will always get hijacking as their final objective var/is_hijacker = FALSE - ///the name of the antag flavor this traitor has. + ///the name of the antag flavor this traitor has, set in Traitor's setup if not preset. var/employer ///assoc list of strings set up after employer is given @@ -46,44 +41,19 @@ /// The uplink handler that this traitor belongs to. var/datum/uplink_handler/uplink_handler - var/uplink_sale_count = 3 + var/uplink_sales_min = 4 + var/uplink_sales_max = 6 ///the final objective the traitor has to accomplish, be it escaping, hijacking, or just martyrdom. var/datum/objective/ending_objective -/datum/antagonist/traitor/infiltrator - // Used to denote traitors who have joined midround and therefore have no access to secondary objectives. - // Progression elements are best left to the roundstart antagonists - // There will still be a timelock on uplink items - name = "\improper Infiltrator" - give_secondary_objectives = FALSE - uplink_flag_given = UPLINK_TRAITORS | UPLINK_INFILTRATORS - -/datum/antagonist/traitor/infiltrator/sleeper_agent - name = "\improper Syndicate Sleeper Agent" - /datum/antagonist/traitor/New(give_objectives = TRUE) . = ..() src.give_objectives = give_objectives /datum/antagonist/traitor/on_gain() - owner.special_role = job_rank - - // NON-MODULE CHANGE: ADV TRAITORS - if(give_objectives) - forge_traitor_objectives() - - if(finalize_antag) - finalize_antag() - - return ..() - // NON-MODULE CHANGE END - -/* Moved to finalize_antags() - if(give_uplink) owner.give_uplink(silent = TRUE, antag_datum = src) - generate_replacement_codes() var/datum/component/uplink/uplink = owner.find_syndicate_uplink() uplink_ref = WEAKREF(uplink) @@ -97,10 +67,6 @@ uplink_handler.has_progression = TRUE SStraitor.register_uplink_handler(uplink_handler) - if(give_secondary_objectives) - uplink_handler.has_objectives = TRUE - uplink_handler.generate_objectives() - uplink_handler.can_replace_objectives = CALLBACK(src, PROC_REF(can_change_objectives)) uplink_handler.replace_objectives = CALLBACK(src, PROC_REF(submit_player_objective)) @@ -109,14 +75,14 @@ var/list/uplink_items = list() for(var/datum/uplink_item/item as anything in SStraitor.uplink_items) - if(item.item && !item.cant_discount && (item.purchasable_from & uplink_handler.uplink_flag) && item.cost > 1) + if(item.item && !item.cant_discount && (item.purchasable_from & uplink_handler.uplink_flag) && item.cost >= TRAITOR_DISCOUNT_MIN_PRICE) if(!length(item.restricted_roles) && !length(item.restricted_species)) uplink_items += item continue if((uplink_handler.assigned_role in item.restricted_roles) || (uplink_handler.assigned_species in item.restricted_species)) uplink_items += item continue - uplink_handler.extra_purchasable += create_uplink_sales(uplink_sale_count, /datum/uplink_category/discounts, 1, uplink_items) + uplink_handler.extra_purchasable += create_uplink_sales(rand(uplink_sales_min, uplink_sales_max), /datum/uplink_category/discounts, -1, uplink_items) if(give_objectives) forge_traitor_objectives() @@ -124,98 +90,37 @@ pick_employer() - owner.teach_crafting_recipe(/datum/crafting_recipe/syndicate_uplink_beacon) - - owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/tatoralert.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE) - return ..() -*/ + /datum/antagonist/traitor/on_removal() if(!isnull(uplink_handler)) - uplink_handler.has_objectives = FALSE uplink_handler.can_replace_objectives = null uplink_handler.replace_objectives = null + owner.take_uplink() return ..() -/datum/antagonist/traitor/proc/traitor_objective_to_html(datum/traitor_objective/to_display) - var/string = "[to_display.name]" - if(to_display.objective_state == OBJECTIVE_STATE_ACTIVE || to_display.objective_state == OBJECTIVE_STATE_INACTIVE) - string += " [to_display.telecrystal_reward] TC" - string += " [to_display.progression_reward] PR" - else - string += ", [to_display.telecrystal_reward] TC" - string += ", [to_display.progression_reward] PR" - if(to_display.objective_state == OBJECTIVE_STATE_ACTIVE && !istype(to_display, /datum/traitor_objective/ultimate)) - string += " Fail this objective" - string += " Succeed this objective" - if(to_display.objective_state == OBJECTIVE_STATE_INACTIVE) - string += " Dispose of this objective" - - if(to_display.skipped) - string += " - Skipped" - else if(to_display.objective_state == OBJECTIVE_STATE_FAILED) - string += " - Failed" - else if(to_display.objective_state == OBJECTIVE_STATE_INVALID) - string += " - Invalidated" - else if(to_display.objective_state == OBJECTIVE_STATE_COMPLETED) - string += " - Succeeded" - - return string - -/datum/antagonist/traitor/antag_panel_objectives() - var/result = ..() - if(!uplink_handler) - return result - result += "Traitor specific objectives
" - result += "Concluded Objectives:
" - for(var/datum/traitor_objective/objective as anything in uplink_handler.completed_objectives) - result += "[traitor_objective_to_html(objective)]
" - if(!length(uplink_handler.completed_objectives)) - result += "EMPTY
" - result += "Ongoing Objectives:
" - for(var/datum/traitor_objective/objective as anything in uplink_handler.active_objectives) - result += "[traitor_objective_to_html(objective)]
" - if(!length(uplink_handler.active_objectives)) - result += "EMPTY
" - result += "Potential Objectives:
" - for(var/datum/traitor_objective/objective as anything in uplink_handler.potential_objectives) - result += "[traitor_objective_to_html(objective)]
" - if(!length(uplink_handler.potential_objectives)) - result += "EMPTY
" - result += "Force add objective
" - return result - /// Returns true if we're allowed to assign ourselves a new objective /datum/antagonist/traitor/proc/can_change_objectives() return can_assign_self_objectives -/// proc that generates the traitors replacement uplink code and radio frequency -/datum/antagonist/traitor/proc/generate_replacement_codes() - replacement_uplink_code = "[pick(GLOB.phonetic_alphabet)] [rand(10,99)]" - replacement_uplink_frequency = sanitize_frequency(rand(MIN_UNUSED_FREQ, MAX_FREQ), free = FALSE, syndie = FALSE) - -/datum/antagonist/traitor/on_removal() - owner.special_role = null - owner.forget_crafting_recipe(/datum/crafting_recipe/syndicate_uplink_beacon) - return ..() - /datum/antagonist/traitor/proc/pick_employer() - var/faction = prob(75) ? FLAVOR_FACTION_SYNDICATE : FLAVOR_FACTION_NANOTRASEN - var/list/possible_employers = list() - - possible_employers.Add(GLOB.syndicate_employers, GLOB.nanotrasen_employers) - - if(istype(ending_objective, /datum/objective/hijack)) - possible_employers -= GLOB.normal_employers - else //escape or martyrdom - possible_employers -= GLOB.hijack_employers - - switch(faction) - if(FLAVOR_FACTION_SYNDICATE) - possible_employers -= GLOB.nanotrasen_employers - if(FLAVOR_FACTION_NANOTRASEN) - possible_employers -= GLOB.syndicate_employers - employer = pick(possible_employers) + if(!employer) + var/faction = prob(75) ? FLAVOR_FACTION_SYNDICATE : FLAVOR_FACTION_NANOTRASEN + var/list/possible_employers = list() + + possible_employers.Add(GLOB.syndicate_employers, GLOB.nanotrasen_employers) + + if(istype(ending_objective, /datum/objective/hijack)) + possible_employers -= GLOB.normal_employers + else //escape or martyrdom + possible_employers -= GLOB.hijack_employers + + switch(faction) + if(FLAVOR_FACTION_SYNDICATE) + possible_employers -= GLOB.nanotrasen_employers + if(FLAVOR_FACTION_NANOTRASEN) + possible_employers -= GLOB.syndicate_employers + employer = pick(possible_employers) traitor_flavor = strings(TRAITOR_FLAVOR_FILE, employer) /// Generates a complete set of traitor objectives up to the traitor objective limit, including non-generic objectives such as martyr and hijack. @@ -227,11 +132,14 @@ objective_count++ var/objective_limit = CONFIG_GET(number/traitor_objectives_amount) - + var/datum/objective/job_objective = forge_job_objective() // for(in...to) loops iterate inclusively, so to reach objective_limit we need to loop to objective_limit - 1 // This does not give them 1 fewer objectives than intended. for(var/i in objective_count to objective_limit - 1) - objectives += forge_single_generic_objective() + var/generated = forge_single_generic_objective(job_objective) + if(generated == job_objective) + job_objective = null + objectives += generated /** * ## forge_ending_objective @@ -261,9 +169,12 @@ ending_objective.owner = owner objectives += ending_objective -/datum/antagonist/traitor/proc/forge_single_generic_objective() +/datum/antagonist/traitor/proc/forge_single_generic_objective(job_objective) + if(prob(JOB_PROB) && job_objective) + return job_objective + if(prob(KILL_PROB)) - var/list/active_ais = active_ais() + var/list/active_ais = active_ais(skip_syndicate = TRUE) if(active_ais.len && prob(DESTROY_AI_PROB(GLOB.joined_player_list.len))) var/datum/objective/destroy/destroy_objective = new() destroy_objective.owner = owner @@ -286,6 +197,11 @@ steal_objective.find_target() return steal_objective +/datum/antagonist/traitor/proc/forge_job_objective() + var/datum/objective/job_objective = owner.assigned_role.generate_traitor_objective() // can return null + job_objective?.owner = owner + return job_objective + /datum/antagonist/traitor/apply_innate_effects(mob/living/mob_override) . = ..() var/mob/living/datum_owner = mob_override || owner.current @@ -296,13 +212,18 @@ datum_owner.AddComponent(/datum/component/codeword_hearing, GLOB.syndicate_code_response_regex, "red", src) /datum/antagonist/traitor/remove_innate_effects(mob/living/mob_override) - . = ..() var/mob/living/datum_owner = mob_override || owner.current handle_clown_mutation(datum_owner, removing = FALSE) for(var/datum/component/codeword_hearing/component as anything in datum_owner.GetComponents(/datum/component/codeword_hearing)) component.delete_if_from_source(src) +/datum/antagonist/traitor/submit_player_objective(retain_existing, retain_escape, force) + . = ..() + if (!.) + return + owner.current.playsound_local(get_turf(owner.current), 'sound/traitor/final_objective.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE) + /datum/antagonist/traitor/ui_static_data(mob/user) var/datum/component/uplink/uplink = uplink_ref?.resolve() var/list/data = list() @@ -313,12 +234,11 @@ data["theme"] = traitor_flavor["ui_theme"] data["code"] = uplink?.unlock_code data["failsafe_code"] = uplink?.failsafe_code - data["replacement_code"] = replacement_uplink_code - data["replacement_frequency"] = format_frequency(replacement_uplink_frequency) data["intro"] = traitor_flavor["introduction"] data["allies"] = traitor_flavor["allies"] data["goal"] = traitor_flavor["goal"] data["has_uplink"] = uplink ? TRUE : FALSE + data["given_uplink"] = give_uplink if(uplink) data["uplink_intro"] = traitor_flavor["uplink"] data["uplink_unlock_info"] = uplink.unlock_text @@ -352,9 +272,6 @@ traitor_won = FALSE objectives_text += "
Objective #[count]: [objective.explanation_text] [objective.get_roundend_success_suffix()]" count++ - if(uplink_handler.final_objective) - objectives_text += "
[span_greentext("[traitor_won ? "Additionally" : "However"], the final objective \"[uplink_handler.final_objective]\" was completed!")]" - traitor_won = TRUE result += "
[owner.name] [traitor_flavor["roundend_report"]]" @@ -367,10 +284,8 @@ result += objectives_text - if(uplink_handler) - if (uplink_handler.contractor_hub) - result += contractor_round_end() - result += "
The traitor had a total of [DISPLAY_PROGRESSION(uplink_handler.progression_points)] Reputation and [uplink_handler.telecrystals] Unused Telecrystals." + if(uplink_handler && uplink_handler.contractor_hub) + result += contractor_round_end() var/special_role_text = LOWER_TEXT(name) @@ -420,7 +335,7 @@ r_hand = /obj/item/gun/energy/recharge/ebow shoes = /obj/item/clothing/shoes/magboots/advance -/datum/outfit/traitor/post_equip(mob/living/carbon/human/H, visualsOnly) +/datum/outfit/traitor/post_equip(mob/living/carbon/human/H, visuals_only) var/obj/item/melee/energy/sword/sword = locate() in H.held_items if(sword.flags_1 & INITIALIZED_1) sword.attack_self() @@ -433,3 +348,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/traitor/objective_category.dm b/code/modules/antagonists/traitor/objective_category.dm deleted file mode 100644 index 5484d2937985..000000000000 --- a/code/modules/antagonists/traitor/objective_category.dm +++ /dev/null @@ -1,68 +0,0 @@ -/// The traitor category handler. This is where the probability of all objectives are managed. -/datum/traitor_category_handler - var/list/datum/traitor_objective_category/all_categories = list() - -/datum/traitor_category_handler/New() - . = ..() - for(var/type in subtypesof(/datum/traitor_objective_category)) - var/datum/traitor_objective_category/category = new type() - if(length(category.objectives)) - all_categories += category - else - // Category should just get autoGC'd here if they don't have any length, this may not be necessary - qdel(category) - -/datum/traitor_category_handler/proc/objective_valid(datum/traitor_objective/objective_path, progression_points) - if(initial(objective_path.abstract_type) == objective_path) - return FALSE - if(progression_points < initial(objective_path.progression_minimum)) - return FALSE - if(progression_points > initial(objective_path.progression_maximum)) - return FALSE - return TRUE - -/datum/traitor_category_handler/proc/get_possible_objectives(progression_points) - var/list/valid_objectives = list() - for(var/datum/traitor_objective_category/category as anything in all_categories) - var/list/category_list = list() - for(var/value in category.objectives) - if(islist(value)) - var/list/objective_category = filter_invalid_objective_list(value, progression_points) - if(!length(objective_category)) - continue - category_list[objective_category] = category.objectives[value] - else - if(!objective_valid(value, progression_points)) - continue - category_list[value] = category.objectives[value] - if(!length(category_list)) - continue - valid_objectives[category_list] = category.weight - - return valid_objectives - -/datum/traitor_category_handler/proc/filter_invalid_objective_list(list/objectives, progression_points) - var/list/filtered_objectives = list() - for(var/value in objectives) - if(islist(value)) - var/list/result = filter_invalid_objective_list(value, progression_points) - if(!length(result)) - continue - filtered_objectives[result] = objectives[value] - else - if(!objective_valid(value, progression_points)) - continue - filtered_objectives[value] = objectives[value] - return filtered_objectives - -/// The objective category. -/// Used to group up entire objectives into 1 weight objects to prevent having a -/// higher chance of getting an objective due to an increased number of different objective subtypes. -/// These are nothing but informational holders and will have no other purpose. -/datum/traitor_objective_category - /// Name of the category, unused but may help in the future - var/name = "generic category" - /// Assoc list of objectives by type mapped to their weight. Can also contain lists of objectives mapped to weight - var/list/objectives = list() - /// The weight of the category. How likely this category is to be chosen. - var/weight = OBJECTIVE_WEIGHT_DEFAULT diff --git a/code/modules/antagonists/traitor/objectives/abstract/target_player.dm b/code/modules/antagonists/traitor/objectives/abstract/target_player.dm deleted file mode 100644 index c08f8cd46ee3..000000000000 --- a/code/modules/antagonists/traitor/objectives/abstract/target_player.dm +++ /dev/null @@ -1,33 +0,0 @@ -/** - * The point of this datum is to act as a means to group target player objectives - * Not all 'target player' objectives have to be under this subtype, it's only used if you don't want duplicates among the current - * children types under this type. - */ -/datum/traitor_objective/target_player - abstract_type = /datum/traitor_objective/target_player - - progression_minimum = 30 MINUTES - - // The code below is for limiting how often you can get this objective. You will get this objective at a maximum of maximum_objectives_in_period every objective_period - /// The objective period at which we consider if it is an 'objective'. Set to 0 to accept all objectives. - var/objective_period = 15 MINUTES - /// The maximum number of objectives we can get within this period. - var/maximum_objectives_in_period = 4 - - /// The target that we need to target. - var/mob/living/target - -/datum/traitor_objective/target_player/Destroy(force) - set_target(null) - return ..() - -/datum/traitor_objective/target_player/proc/set_target(mob/living/new_target) - if(target) - UnregisterSignal(target, COMSIG_QDELETING) - target = new_target - if(target) - RegisterSignal(target, COMSIG_QDELETING, PROC_REF(target_deleted)) - -/datum/traitor_objective/target_player/proc/target_deleted(datum/source) - SIGNAL_HANDLER - set_target(null) diff --git a/code/modules/antagonists/traitor/objectives/assassination.dm b/code/modules/antagonists/traitor/objectives/assassination.dm deleted file mode 100644 index 5d5cdbd1981f..000000000000 --- a/code/modules/antagonists/traitor/objectives/assassination.dm +++ /dev/null @@ -1,205 +0,0 @@ -/datum/traitor_objective_category/assassinate_kidnap - name = "Assassination/Kidnap" - objectives = list( - list( - /datum/traitor_objective/target_player/assassinate/calling_card = 1, - /datum/traitor_objective/target_player/assassinate/calling_card/heads_of_staff = 1, - ) = 1, - list( - list( - /datum/traitor_objective/target_player/kidnapping/common = 20, - /datum/traitor_objective/target_player/kidnapping/common/assistant = 1, - ) = 4, - /datum/traitor_objective/target_player/kidnapping/uncommon = 3, - /datum/traitor_objective/target_player/kidnapping/rare = 2, - /datum/traitor_objective/target_player/kidnapping/captain = 1 - ) = 1, - ) - -/datum/traitor_objective/target_player/assassinate - name = "Assassinate %TARGET% the %JOB TITLE%" - description = "Simply kill your target to accomplish this objective." - - abstract_type = /datum/traitor_objective/target_player/assassinate - - progression_minimum = 30 MINUTES - - /** - * Makes the objective only set heads as targets when true, and block them from being targets when false. - * This also blocks the objective from generating UNTIL the un-heads_of_staff version (WHICH SHOULD BE A DIRECT PARENT) is completed. - * example: calling card objective, you kill someone, you unlock the chance to roll a head of staff target version of calling card. - */ - var/heads_of_staff = FALSE - - duplicate_type = /datum/traitor_objective/target_player - -/datum/traitor_objective/target_player/assassinate/supported_configuration_changes() - . = ..() - . += NAMEOF(src, objective_period) - . += NAMEOF(src, maximum_objectives_in_period) - -/datum/traitor_objective/target_player/assassinate/calling_card - name = "Assassinate %TARGET% the %JOB TITLE%, and plant a calling card" - description = "Kill your target and plant a calling card in the pockets of your victim. If your calling card gets destroyed before you are able to plant it, this objective will fail." - progression_reward = 2 MINUTES - telecrystal_reward = list(1, 2) - - var/obj/item/paper/calling_card/card - -/datum/traitor_objective/target_player/assassinate/calling_card/heads_of_staff - progression_reward = 4 MINUTES - telecrystal_reward = list(2, 3) - - heads_of_staff = TRUE - -/datum/traitor_objective/target_player/assassinate/calling_card/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!card) - buttons += add_ui_button("", "Pressing this will materialize a calling card, which you must plant to succeed.", "paper-plane", "summon_card") - return buttons - -/datum/traitor_objective/target_player/assassinate/calling_card/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("summon_card") - if(card) - return - card = new(user.drop_location()) - user.put_in_hands(card) - card.balloon_alert(user, "the card materializes in your hand") - RegisterSignal(card, COMSIG_ITEM_EQUIPPED, PROC_REF(on_card_planted)) - AddComponent(/datum/component/traitor_objective_register, card, \ - succeed_signals = null, \ - fail_signals = list(COMSIG_QDELETING), \ - penalty = TRUE) - -/datum/traitor_objective/target_player/assassinate/calling_card/proc/on_card_planted(datum/source, mob/living/equipper, slot) - SIGNAL_HANDLER - if(equipper != target) - return //your target please - if(equipper.stat != DEAD) - return //kill them please - if(!(slot & (ITEM_SLOT_LPOCKET|ITEM_SLOT_RPOCKET))) - return //in their pockets please - succeed_objective() - -/datum/traitor_objective/target_player/assassinate/calling_card/ungenerate_objective() - . = ..() //unsets kill target - if(card) - UnregisterSignal(card, COMSIG_ITEM_EQUIPPED) - card = null - -/datum/traitor_objective/target_player/assassinate/calling_card/target_deleted() - //you cannot plant anything on someone who is gone gone, so even if this happens after you're still liable to fail - fail_objective(penalty_cost = telecrystal_penalty) - -/datum/traitor_objective/target_player/assassinate/New(datum/uplink_handler/handler) - . = ..() - AddComponent(/datum/component/traitor_objective_limit_per_time, \ - /datum/traitor_objective/target_player, \ - time_period = objective_period, \ - maximum_objectives = maximum_objectives_in_period \ - ) - -/datum/traitor_objective/target_player/assassinate/generate_objective(datum/mind/generating_for, list/possible_duplicates) - - var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things - for(var/datum/objective/task as anything in handler.primary_objectives) - if(!istype(task.target, /datum/mind)) - continue - already_targeting += task.target //Removing primary objective kill targets from the list - - var/parent_type = type2parent(type) - //don't roll head of staff types if you haven't completed the normal version - if(heads_of_staff && !handler.get_completion_count(parent_type)) - // Locked if they don't have any of the risky bug room objective completed - return FALSE - - var/list/possible_targets = list() - var/try_target_late_joiners = FALSE - if(generating_for.late_joiner) - try_target_late_joiners = TRUE - for(var/datum/mind/possible_target as anything in get_crewmember_minds()) - if(possible_target in already_targeting) - continue - var/target_area = get_area(possible_target.current) - if(possible_target == generating_for) - continue - if(!ishuman(possible_target.current)) - continue - if(possible_target.current.stat == DEAD) - continue - var/datum/antagonist/traitor/traitor = possible_target.has_antag_datum(/datum/antagonist/traitor) - if(traitor && traitor.uplink_handler.telecrystals >= 0) - continue - if(!HAS_TRAIT(SSstation, STATION_TRAIT_LATE_ARRIVALS) && istype(target_area, /area/shuttle/arrival)) - continue - //removes heads of staff from being targets from non heads of staff assassinations, and vice versa - if(heads_of_staff) - if(!(possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF)) - continue - else - if((possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF)) - continue - possible_targets += possible_target - for(var/datum/traitor_objective/target_player/objective as anything in possible_duplicates) - possible_targets -= objective.target - if(try_target_late_joiners) - var/list/all_possible_targets = possible_targets.Copy() - for(var/datum/mind/possible_target as anything in all_possible_targets) - if(!possible_target.late_joiner) - possible_targets -= possible_target - if(!possible_targets.len) - possible_targets = all_possible_targets - special_target_filter(possible_targets) - if(!possible_targets.len) - return FALSE //MISSION FAILED, WE'LL GET EM NEXT TIME - - var/datum/mind/target_mind = pick(possible_targets) - set_target(target_mind.current) - replace_in_name("%TARGET%", target.real_name) - replace_in_name("%JOB TITLE%", target_mind.assigned_role.title) - RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_death)) - return TRUE - -/datum/traitor_objective/target_player/assassinate/ungenerate_objective() - UnregisterSignal(target, COMSIG_LIVING_DEATH) - set_target(null) - -///proc for checking for special states that invalidate a target -/datum/traitor_objective/target_player/assassinate/proc/special_target_filter(list/possible_targets) - return - -/datum/traitor_objective/target_player/assassinate/target_deleted() - if(objective_state == OBJECTIVE_STATE_INACTIVE) - //don't take an objective target of someone who is already obliterated - fail_objective() - return ..() - -/datum/traitor_objective/target_player/assassinate/proc/on_target_death() - SIGNAL_HANDLER - if(objective_state == OBJECTIVE_STATE_INACTIVE) - //don't take an objective target of someone who is already dead - fail_objective() - -/obj/item/paper/calling_card - name = "calling card" - icon_state = "syndicate_calling_card" - color = "#ff5050" - show_written_words = FALSE - default_raw_text = {" - **Death to Nanotrasen.**

- - Only through the inviolable cooperation of corporations known as The Syndicate, can Nanotrasen and its autocratic tyrants be silenced. - The outcries of Nanotrasen's employees are squelched by the suffocating iron grip of their leaders. If you read this, and understand - why we fight, then you need only to look where Nanotrasen doesn't want you to find us to join our cause. Any number of our companies - may be fighting with your interests in mind.

- - SELF: They fight for the protection and freedom of silicon life all across the galaxy.

- - Tiger Cooperative: They fight for religious freedom and their righteous concoctions.

- - Waffle Corporation: They fight for the return of healthy corporate competition, snuffed out by Nanotrasen's monopoly.

- - Animal Rights Consortium: They fight for nature and the right for all biological life to exist. - "} diff --git a/code/modules/antagonists/traitor/objectives/demoralise_assault.dm b/code/modules/antagonists/traitor/objectives/demoralise_assault.dm deleted file mode 100644 index fe26864e4fc1..000000000000 --- a/code/modules/antagonists/traitor/objectives/demoralise_assault.dm +++ /dev/null @@ -1,129 +0,0 @@ -/datum/traitor_objective_category/demoralise - name = "Demoralise Crew" - objectives = list( - /datum/traitor_objective/target_player/assault = 1, - /datum/traitor_objective/destroy_item/demoralise = 1, - ) - weight = OBJECTIVE_WEIGHT_UNLIKELY - -/datum/traitor_objective/target_player/assault - name = "Assault %TARGET% the %JOB TITLE%" - description = "%TARGET% has been identified as a potential future agent. \ - Pick a fight and give them a good beating. \ - %COUNT% hits should reduce their morale and have them questioning their loyalties. \ - Try not to kill them just yet, we may want to recruit them in the future." - - abstract_type = /datum/traitor_objective/target_player - duplicate_type = /datum/traitor_objective/target_player - - progression_minimum = 0 MINUTES - progression_maximum = 30 MINUTES - progression_reward = list(4 MINUTES, 8 MINUTES) - telecrystal_reward = list(0, 1) - - /// Min attacks required to pass the objective. Picked at random between this and max. - var/min_attacks_required = 2 - /// Max attacks required to pass the objective. Picked at random between this and min. - var/max_attacks_required = 5 - /// The random number picked for the number of required attacks to pass this objective. - var/attacks_required = 0 - /// Total number of successful attacks recorded. - var/attacks_inflicted = 0 - -/datum/traitor_objective/target_player/assault/on_objective_taken(mob/user) - . = ..() - - target.AddElement(/datum/element/relay_attackers) - RegisterSignal(target, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked)) - -/datum/traitor_objective/target_player/assault/proc/on_attacked(mob/source, mob/living/attacker, attack_flags) - SIGNAL_HANDLER - - // Only care about attacks from the objective's owner. - if(attacker != handler.owner.current) - return - - // We want some sort of damaging attack to trigger this, rather than shoves and non-lethals. - if(!(attack_flags & ATTACKER_DAMAGING_ATTACK)) - return - - attacks_inflicted++ - - if(attacks_inflicted == attacks_required) - succeed_objective() - -/datum/traitor_objective/target_player/assault/ungenerate_objective() - UnregisterSignal(target, COMSIG_ATOM_WAS_ATTACKED) - UnregisterSignal(target, COMSIG_LIVING_DEATH) - set_target(null) - -/datum/traitor_objective/target_player/assault/generate_objective(datum/mind/generating_for, list/possible_duplicates) - var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things - for(var/datum/objective/task as anything in handler.primary_objectives) - if(!istype(task.target, /datum/mind)) - continue - already_targeting += task.target //Removing primary objective kill targets from the list - - var/list/possible_targets = list() - - for(var/datum/mind/possible_target as anything in get_crewmember_minds()) - if(possible_target in already_targeting) - continue - - if(possible_target == generating_for) - continue - - if(!ishuman(possible_target.current)) - continue - - if(possible_target.current.stat == DEAD) - continue - - if(possible_target.has_antag_datum(/datum/antagonist/traitor)) - continue - - possible_targets += possible_target - - for(var/datum/traitor_objective/target_player/objective as anything in possible_duplicates) - possible_targets -= objective.target?.mind - - if(generating_for.late_joiner) - var/list/all_possible_targets = possible_targets.Copy() - for(var/datum/mind/possible_target as anything in all_possible_targets) - if(!possible_target.late_joiner) - possible_targets -= possible_target - if(!possible_targets.len) - possible_targets = all_possible_targets - - if(!possible_targets.len) - return FALSE - - var/datum/mind/target_mind = pick(possible_targets) - - set_target(target_mind.current) - replace_in_name("%TARGET%", target.real_name) - replace_in_name("%JOB TITLE%", target_mind.assigned_role.title) - - attacks_required = rand(min_attacks_required, max_attacks_required) - replace_in_name("%COUNT%", attacks_required) - - RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_death)) - - return TRUE - -/datum/traitor_objective/target_player/assault/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(attacks_required > attacks_inflicted) - buttons += add_ui_button("[attacks_required - attacks_inflicted]", "This tells you how many more times you have to attack the target player to succeed.", "hand-rock-o", "none") - return buttons - -/datum/traitor_objective/target_player/assault/target_deleted() - //don't take an objective target of someone who is already obliterated - fail_objective() - return ..() - -/datum/traitor_objective/target_player/assault/proc/on_target_death() - SIGNAL_HANDLER - - //don't take an objective target of someone who is already dead - fail_objective() diff --git a/code/modules/antagonists/traitor/objectives/destroy_heirloom.dm b/code/modules/antagonists/traitor/objectives/destroy_heirloom.dm deleted file mode 100644 index 0d62ecece5f2..000000000000 --- a/code/modules/antagonists/traitor/objectives/destroy_heirloom.dm +++ /dev/null @@ -1,151 +0,0 @@ -/datum/traitor_objective_category/destroy_heirloom - name = "Destroy Heirloom" - objectives = list( - list( - // There's about 16 jobs in common, so assistant has a 1/21 chance of getting chosen. - /datum/traitor_objective/destroy_heirloom/common = 20, - /datum/traitor_objective/destroy_heirloom/common/assistant = 1, - ) = 4, - /datum/traitor_objective/destroy_heirloom/uncommon = 3, - /datum/traitor_objective/destroy_heirloom/rare = 2, - /datum/traitor_objective/destroy_heirloom/captain = 1 - ) - -/datum/traitor_objective/destroy_heirloom - name = "Destroy %ITEM%, the family heirloom that belongs to %TARGET% the %JOB TITLE%" - description = "%TARGET% has been on our shitlist for a while and we want to show them we mean business. Find their %ITEM% and destroy it." - - abstract_type = /datum/traitor_objective/destroy_heirloom - - /// The jobs that this objective is targeting. - var/list/target_jobs - /// the item we need to destroy - var/obj/item/target_item - /// the owner of the item we need to destroy - var/datum/mind/target_mind - - // The code below is for limiting how often you can get this objective. You will get this objective at a maximum of maximum_objectives_in_period every objective_period - /// The objective period at which we consider if it is an 'objective'. Set to 0 to accept all objectives. - var/objective_period = 10 MINUTES - /// The maximum number of objectives that can be taken in this period. - var/maximum_objectives_in_period = 2 - - duplicate_type = /datum/traitor_objective/destroy_heirloom - -/datum/traitor_objective/destroy_heirloom/common - /// 30 minutes in, syndicate won't care about common heirlooms anymore - progression_minimum = 0 MINUTES - progression_maximum = 30 MINUTES - progression_reward = list(8 MINUTES, 12 MINUTES) - telecrystal_reward = list(1, 2) - target_jobs = list( - // Medical - /datum/job/doctor, - /datum/job/virologist, - /datum/job/paramedic, - /datum/job/psychologist, - /datum/job/chemist, - // Service - /datum/job/clown, - /datum/job/botanist, - /datum/job/janitor, - /datum/job/mime, - /datum/job/lawyer, - // Cargo - /datum/job/cargo_technician, - // Science - /datum/job/geneticist, - /datum/job/scientist, - /datum/job/roboticist, - // Engineering - /datum/job/station_engineer, - /datum/job/atmospheric_technician, - ) - -/// This is only for assistants, because the syndies are a lot less likely to give a shit about what an assistant does, so they're a lot less likely to appear -/datum/traitor_objective/destroy_heirloom/common/assistant - target_jobs = list( - /datum/job/assistant - ) - -/datum/traitor_objective/destroy_heirloom/uncommon - /// 45 minutes in, syndicate won't care about uncommon heirlooms anymore - progression_minimum = 0 MINUTES - progression_maximum = 45 MINUTES - progression_reward = list(8 MINUTES, 12 MINUTES) - telecrystal_reward = list(1, 2) - target_jobs = list( - // Cargo - /datum/job/shaft_miner, - // Service - /datum/job/chaplain, - /datum/job/bartender, - /datum/job/cook, - /datum/job/curator, - ) - -/datum/traitor_objective/destroy_heirloom/rare - progression_minimum = 15 MINUTES - /// 60 minutes in, syndicate won't care about rare heirlooms anymore - progression_maximum = 60 MINUTES - progression_reward = list(10 MINUTES, 14 MINUTES) - telecrystal_reward = list(2, 3) - target_jobs = list( - // Security - /datum/job/security_officer, - /datum/job/warden, - /datum/job/detective, - // Heads of staff - /datum/job/head_of_personnel, - /datum/job/chief_medical_officer, - /datum/job/research_director, - /datum/job/quartermaster, - ) - -/datum/traitor_objective/destroy_heirloom/captain - progression_minimum = 30 MINUTES - progression_reward = list(10 MINUTES, 14 MINUTES) - telecrystal_reward = 4 - target_jobs = list( - /datum/job/head_of_security, - /datum/job/captain - ) - -/datum/traitor_objective/destroy_heirloom/New(datum/uplink_handler/handler) - . = ..() - AddComponent(/datum/component/traitor_objective_limit_per_time, \ - /datum/traitor_objective/destroy_heirloom, \ - time_period = objective_period, \ - maximum_objectives = maximum_objectives_in_period \ - ) - -/datum/traitor_objective/destroy_heirloom/generate_objective(datum/mind/generating_for, list/possible_duplicates) - var/list/possible_targets = list() - for(var/datum/mind/possible_target as anything in get_crewmember_minds()) - if(possible_target == generating_for) - continue - if(!ishuman(possible_target.current)) - continue - var/datum/quirk/item_quirk/family_heirloom/quirk = locate() in possible_target.current.quirks - if(!quirk || !quirk.heirloom.resolve()) - continue - if(!(possible_target.assigned_role.type in target_jobs)) - continue - possible_targets += possible_target - for(var/datum/traitor_objective/destroy_heirloom/objective as anything in possible_duplicates) - possible_targets -= objective.target_mind - if(!length(possible_targets)) - return FALSE - target_mind = pick(possible_targets) - AddComponent(/datum/component/traitor_objective_register, target_mind.current, fail_signals = list(COMSIG_QDELETING)) - var/datum/quirk/item_quirk/family_heirloom/quirk = locate() in target_mind.current.quirks - target_item = quirk.heirloom.resolve() - AddComponent(/datum/component/traitor_objective_register, target_item, succeed_signals = list(COMSIG_QDELETING)) - replace_in_name("%TARGET%", target_mind.name) - replace_in_name("%JOB TITLE%", target_mind.assigned_role.title) - replace_in_name("%ITEM%", target_item.name) - return TRUE - -/datum/traitor_objective/destroy_heirloom/ungenerate_objective() - target_item = null - target_mind = null diff --git a/code/modules/antagonists/traitor/objectives/destroy_item.dm b/code/modules/antagonists/traitor/objectives/destroy_item.dm deleted file mode 100644 index ec247eac9edf..000000000000 --- a/code/modules/antagonists/traitor/objectives/destroy_item.dm +++ /dev/null @@ -1,109 +0,0 @@ -/datum/traitor_objective/destroy_item - name = "Steal %ITEM% and destroy it" - description = "Find %ITEM% and destroy it using any means necessary. We can't allow the crew to have %ITEM% as it conflicts with our interests." - - var/list/possible_items = list() - /// The current target item that we are stealing. - var/datum/objective_item/steal/target_item - /// Any special equipment that may be needed - var/list/special_equipment - /// Items that are currently tracked and will succeed this objective when destroyed. - var/list/tracked_items = list() - - abstract_type = /datum/traitor_objective/destroy_item - -/datum/traitor_objective/destroy_item/low_risk - progression_minimum = 10 MINUTES - progression_maximum = 35 MINUTES - progression_reward = list(5 MINUTES, 10 MINUTES) - telecrystal_reward = 1 - - possible_items = list( - /datum/objective_item/steal/traitor/bartender_shotgun, - /datum/objective_item/steal/traitor/fireaxe, - /datum/objective_item/steal/traitor/nullrod, - /datum/objective_item/steal/traitor/big_crowbar, - ) - -/datum/traitor_objective/destroy_item/very_risky - progression_minimum = 40 MINUTES - progression_reward = 15 MINUTES - telecrystal_reward = list(6, 9) - - possible_items = list( - /datum/objective_item/steal/blackbox, - ) - -/// Super early-game destroy objective intended to be items easily tided that the crew tends to value. -/datum/traitor_objective/destroy_item/demoralise - description = "Find %ITEM% and destroy it using any means necessary. \ - We believe this luxury item is important for crew morale. \ - Destruction of this item will help our recruitment efforts." - - progression_minimum = 0 MINUTES - progression_maximum = 10 MINUTES - progression_reward = list(4 MINUTES, 8 MINUTES) - telecrystal_reward = list(0, 1) - - possible_items = list( - /datum/objective_item/steal/traitor/rpd, - /datum/objective_item/steal/traitor/space_law, - /datum/objective_item/steal/traitor/granted_stamp, - /datum/objective_item/steal/traitor/denied_stamp, - /datum/objective_item/steal/traitor/lizard_plush, - /datum/objective_item/steal/traitor/moth_plush, - /datum/objective_item/steal/traitor/insuls, - ) - -/datum/traitor_objective/destroy_item/generate_objective(datum/mind/generating_for, list/possible_duplicates) - for(var/datum/traitor_objective/destroy_item/objective as anything in possible_duplicates) - possible_items -= objective.target_item.type - while(length(possible_items)) - var/datum/objective_item/steal/target = pick_n_take(possible_items) - target = new target() - if(!target.valid_objective_for(list(generating_for), require_owner = TRUE)) - qdel(target) - continue - target_item = target - break - if(!target_item) - return FALSE - if(target_item.exists_on_map) - var/list/items = GLOB.steal_item_handler.objectives_by_path[target_item.targetitem] - for(var/obj/item/item as anything in items) - AddComponent(/datum/component/traitor_objective_register, item, succeed_signals = list(COMSIG_QDELETING)) - tracked_items += item - if(length(target_item.special_equipment)) - special_equipment = target_item.special_equipment - replace_in_name("%ITEM%", target_item.name) - AddComponent(/datum/component/traitor_objective_mind_tracker, generating_for, \ - signals = list(COMSIG_MOB_EQUIPPED_ITEM = PROC_REF(on_item_pickup))) - return TRUE - -/datum/traitor_objective/destroy_item/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(special_equipment) - buttons += add_ui_button("", "Pressing this will summon any extra special equipment you may need for the mission.", "tools", "summon_gear") - return buttons - -/datum/traitor_objective/destroy_item/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("summon_gear") - if(!special_equipment) - return - for(var/item in special_equipment) - var/obj/item/new_item = new item(user.drop_location()) - user.put_in_hands(new_item) - user.balloon_alert(user, "the equipment materializes in your hand") - special_equipment = null - -/datum/traitor_objective/destroy_item/proc/on_item_pickup(datum/source, obj/item/item, slot) - SIGNAL_HANDLER - if(istype(item, target_item.targetitem) && !(item in tracked_items)) - AddComponent(/datum/component/traitor_objective_register, item, succeed_signals = list(COMSIG_QDELETING)) - tracked_items += item - -/datum/traitor_objective/destroy_item/ungenerate_objective() - tracked_items.Cut() - return ..() diff --git a/code/modules/antagonists/traitor/objectives/eyesnatching.dm b/code/modules/antagonists/traitor/objectives/eyesnatching.dm deleted file mode 100644 index 1b59b4cd48d6..000000000000 --- a/code/modules/antagonists/traitor/objectives/eyesnatching.dm +++ /dev/null @@ -1,240 +0,0 @@ -/datum/traitor_objective_category/eyesnatching - name = "Eyesnatching" - objectives = list( - /datum/traitor_objective/target_player/eyesnatching = 1, - /datum/traitor_objective/target_player/eyesnatching/heads = 1, - ) - weight = OBJECTIVE_WEIGHT_UNLIKELY - -/datum/traitor_objective/target_player/eyesnatching - name = "Steal the eyes of %TARGET% the %JOB TITLE%" - description = "%TARGET% messed with the wrong people. Steal their eyes to teach them a lesson. You will be provided an experimental eyesnatcher device to aid you in your mission." - - progression_minimum = 10 MINUTES - - progression_reward = list(4 MINUTES, 8 MINUTES) - telecrystal_reward = list(1, 2) - - /// If we're targeting heads of staff or not - var/heads_of_staff = FALSE - /// Have we already spawned an eyesnatcher - var/spawned_eyesnatcher = FALSE - - duplicate_type = /datum/traitor_objective/target_player - -/datum/traitor_objective/target_player/eyesnatching/supported_configuration_changes() - . = ..() - . += NAMEOF(src, objective_period) - . += NAMEOF(src, maximum_objectives_in_period) - -/datum/traitor_objective/target_player/eyesnatching/New(datum/uplink_handler/handler) - . = ..() - AddComponent(/datum/component/traitor_objective_limit_per_time, \ - /datum/traitor_objective/target_player, \ - time_period = objective_period, \ - maximum_objectives = maximum_objectives_in_period \ - ) - -/datum/traitor_objective/target_player/eyesnatching/heads - progression_reward = list(6 MINUTES, 12 MINUTES) - telecrystal_reward = list(2, 3) - - heads_of_staff = TRUE - -/datum/traitor_objective/target_player/eyesnatching/generate_objective(datum/mind/generating_for, list/possible_duplicates) - - var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things - for(var/datum/objective/task as anything in handler.primary_objectives) - if(!istype(task.target, /datum/mind)) - continue - already_targeting += task.target //Removing primary objective kill targets from the list - - var/list/possible_targets = list() - var/try_target_late_joiners = FALSE - if(generating_for.late_joiner) - try_target_late_joiners = TRUE - - for(var/datum/mind/possible_target as anything in get_crewmember_minds()) - if(possible_target == generating_for) - continue - - if(possible_target in already_targeting) - continue - - if(!ishuman(possible_target.current)) - continue - - if(possible_target.current.stat == DEAD) - continue - - if(possible_target.has_antag_datum(/datum/antagonist/traitor)) - continue - - if(!possible_target.assigned_role) - continue - - if(heads_of_staff) - if(!(possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF)) - continue - else - if(possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF) - continue - - var/mob/living/carbon/human/targets_current = possible_target.current - if(!targets_current.get_organ_by_type(/obj/item/organ/eyes)) - continue - - possible_targets += possible_target - - for(var/datum/traitor_objective/target_player/objective as anything in possible_duplicates) - possible_targets -= objective.target?.mind - - if(try_target_late_joiners) - var/list/all_possible_targets = possible_targets.Copy() - for(var/datum/mind/possible_target as anything in all_possible_targets) - if(!possible_target.late_joiner) - possible_targets -= possible_target - - if(!possible_targets.len) - possible_targets = all_possible_targets - - if(!possible_targets.len) - return FALSE //MISSION FAILED, WE'LL GET EM NEXT TIME - - var/datum/mind/target_mind = pick(possible_targets) - set_target(target_mind.current) - - replace_in_name("%TARGET%", target_mind.name) - replace_in_name("%JOB TITLE%", target_mind.assigned_role.title) - RegisterSignal(target, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(check_eye_removal)) - AddComponent(/datum/component/traitor_objective_register, target, fail_signals = list(COMSIG_QDELETING)) - return TRUE - -/datum/traitor_objective/target_player/eyesnatching/proc/check_eye_removal(datum/source, obj/item/organ/eyes/removed) - SIGNAL_HANDLER - - if(!istype(removed)) - return - - succeed_objective() - -/datum/traitor_objective/target_player/eyesnatching/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!spawned_eyesnatcher) - buttons += add_ui_button("", "Pressing this will materialize an eyesnatcher, which can be used on incapacitaded or restrained targets to forcefully remove their eyes.", "syringe", "eyesnatcher") - return buttons - -/datum/traitor_objective/target_player/eyesnatching/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("eyesnatcher") - if(spawned_eyesnatcher) - return - spawned_eyesnatcher = TRUE - var/obj/item/eyesnatcher/eyesnatcher = new(user.drop_location()) - user.put_in_hands(eyesnatcher) - eyesnatcher.balloon_alert(user, "the snatcher materializes in your hand") - -/obj/item/eyesnatcher - name = "portable eyeball extractor" - desc = "An overly complicated device that can pierce target's skull and extract their eyeballs if enough brute force is applied." - icon = 'icons/obj/medical/surgery_tools.dmi' - icon_state = "eyesnatcher" - base_icon_state = "eyesnatcher" - inhand_icon_state = "hypo" - lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi' - righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi' - throwforce = 0 - w_class = WEIGHT_CLASS_SMALL - throw_speed = 3 - throw_range = 5 - ///Whether it's been used to steal a pair of eyes already. - var/used = FALSE - -/obj/item/eyesnatcher/update_icon_state() - . = ..() - icon_state = "[base_icon_state][used ? "-used" : ""]" - -/obj/item/eyesnatcher/attack(mob/living/carbon/human/target, mob/living/user, params) - if(used || !istype(target) || !target.Adjacent(user)) //Works only once, no TK use - return ..() - - var/obj/item/organ/eyes/eyeballies = target.get_organ_slot(ORGAN_SLOT_EYES) - var/obj/item/bodypart/head/head = target.get_bodypart(BODY_ZONE_HEAD) - - if(!head || !eyeballies || target.is_eyes_covered()) - return ..() - var/eye_snatch_enthusiasm = 5 SECONDS - if(HAS_MIND_TRAIT(user, TRAIT_MORBID)) - eye_snatch_enthusiasm *= 0.7 - user.do_attack_animation(target, used_item = src) - target.visible_message( - span_warning("[user] presses [src] against [target]'s skull!"), - span_userdanger("[user] presses [src] against your skull!")) - if(!do_after(user, eye_snatch_enthusiasm, target = target, extra_checks = CALLBACK(src, PROC_REF(eyeballs_exist), eyeballies, head, target))) - return - - to_chat(target, span_userdanger("You feel something forcing its way into your skull!")) - balloon_alert(user, "applying pressure...") - if(!do_after(user, eye_snatch_enthusiasm, target = target, extra_checks = CALLBACK(src, PROC_REF(eyeballs_exist), eyeballies, head, target))) - return - - var/min_wound = head.get_wound_threshold_of_wound_type(WOUND_BLUNT, WOUND_SEVERITY_SEVERE, return_value_if_no_wound = 30, wound_source = src) - var/max_wound = head.get_wound_threshold_of_wound_type(WOUND_BLUNT, WOUND_SEVERITY_CRITICAL, return_value_if_no_wound = 50, wound_source = src) - - target.apply_damage(20, BRUTE, BODY_ZONE_HEAD, wound_bonus = rand(min_wound, max_wound + 10), attacking_item = src) - target.visible_message( - span_danger("[src] pierces through [target]'s skull, horribly mutilating their eyes!"), - span_userdanger("Something penetrates your skull, horribly mutilating your eyes! Holy fuck!"), - span_hear("You hear a sickening sound of metal piercing flesh!") - ) - eyeballies.apply_organ_damage(eyeballies.maxHealth) - target.emote("scream") - playsound(target, 'sound/effects/wounds/crackandbleed.ogg', 100) - log_combat(user, target, "cracked the skull of (eye snatching)", src) - - if(!do_after(user, eye_snatch_enthusiasm, target = target, extra_checks = CALLBACK(src, PROC_REF(eyeballs_exist), eyeballies, head, target))) - return - - if(!target.is_blind()) - to_chat(target, span_userdanger("You suddenly go blind!")) - if(prob(1)) - to_chat(target, span_notice("At least you got a new pirate-y look out of it...")) - var/obj/item/clothing/glasses/eyepatch/new_patch = new(target.loc) - target.equip_to_slot_if_possible(new_patch, ITEM_SLOT_EYES, disable_warning = TRUE) - - to_chat(user, span_notice("You successfully extract [target]'s eyeballs.")) - playsound(target, 'sound/surgery/retractor2.ogg', 100, TRUE) - playsound(target, 'sound/effects/pop.ogg', 100, TRAIT_MUTE) - eyeballies.Remove(target) - eyeballies.forceMove(get_turf(target)) - notify_ghosts( - "[target] has just had their eyes snatched!", - source = target, - header = "Ouch!", - ) - target.emote("scream") - if(prob(20)) - target.emote("cry") - used = TRUE - update_appearance(UPDATE_ICON) - -/obj/item/eyesnatcher/examine(mob/user) - . = ..() - if(used) - . += span_notice("It has been used up.") - -/obj/item/eyesnatcher/proc/eyeballs_exist(obj/item/organ/eyes/eyeballies, obj/item/bodypart/head/head, mob/living/carbon/human/target) - if(!eyeballies || QDELETED(eyeballies)) - return FALSE - if(!head || QDELETED(head)) - return FALSE - - if(eyeballies.owner != target) - return FALSE - var/obj/item/organ/eyes/eyes = target.get_organ_slot(ORGAN_SLOT_EYES) - //got different eyes or doesn't own the head... somehow - if(head.owner != target || eyes != eyeballies) - return FALSE - - return TRUE diff --git a/code/modules/antagonists/traitor/objectives/final_objective/battlecruiser.dm b/code/modules/antagonists/traitor/objectives/final_objective/battlecruiser.dm deleted file mode 100644 index 7da84f600261..000000000000 --- a/code/modules/antagonists/traitor/objectives/final_objective/battlecruiser.dm +++ /dev/null @@ -1,49 +0,0 @@ -/datum/traitor_objective/ultimate/battlecruiser - name = "Reveal Station Coordinates to nearby Syndicate Battlecruiser" - description = "Use a special upload card on a communications console to send the coordinates \ - of the station to a nearby Battlecruiser. You may want to make your syndicate status known to \ - the battlecruiser crew when they arrive - their goal will be to destroy the station." - - /// Checks whether we have sent the card to the traitor yet. - var/sent_accesscard = FALSE - /// Battlecruiser team that we get assigned to - var/datum/team/battlecruiser/team - -/datum/traitor_objective/ultimate/battlecruiser/generate_objective(datum/mind/generating_for, list/possible_duplicates) - // There's no empty space to load a battlecruiser in... - if(SSmapping.is_planetary()) - return FALSE - - return TRUE - -/datum/traitor_objective/ultimate/battlecruiser/on_objective_taken(mob/user) - . = ..() - team = new() - var/obj/machinery/nuclearbomb/selfdestruct/nuke = locate() in SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/selfdestruct) - if(nuke.r_code == NUKE_CODE_UNSET) - nuke.r_code = random_nukecode() - team.nuke = nuke - team.update_objectives() - handler.owner.add_antag_datum(/datum/antagonist/battlecruiser/ally, team) - - -/datum/traitor_objective/ultimate/battlecruiser/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!sent_accesscard) - buttons += add_ui_button("", "Pressing this will materialize an upload card, which you can use on a communication console to contact the fleet.", "phone", "card") - return buttons - -/datum/traitor_objective/ultimate/battlecruiser/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("card") - if(sent_accesscard) - return - sent_accesscard = TRUE - var/obj/item/card/emag/battlecruiser/emag_card = new() - emag_card.team = team - podspawn(list( - "target" = get_turf(user), - "style" = /datum/pod_style/syndicate, - "spawn" = emag_card, - )) diff --git a/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm b/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm deleted file mode 100644 index 47b9b3b01674..000000000000 --- a/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm +++ /dev/null @@ -1,42 +0,0 @@ -/datum/traitor_objective_category/final_objective - name = "Final Objective" - objectives = list( - /datum/traitor_objective/ultimate/romerol = 1, - /datum/traitor_objective/ultimate/battlecruiser = 1, - /datum/traitor_objective/ultimate/space_dragon = 1, - /datum/traitor_objective/ultimate/supermatter_cascade = 1, - /datum/traitor_objective/ultimate/infect_ai = 1, - /datum/traitor_objective/ultimate/dark_matteor = 1, - ) - weight = 100 - -/datum/traitor_objective/ultimate - abstract_type = /datum/traitor_objective/ultimate - progression_minimum = 140 MINUTES - needs_reward = FALSE - - var/progression_points_in_objectives = 20 MINUTES - -/// Determines if this final objective can be taken. Should be put into every final objective's generate function. -/datum/traitor_objective/ultimate/can_generate_objective(generating_for, list/possible_duplicates) - if(handler.get_completion_progression(/datum/traitor_objective) < progression_points_in_objectives) - return FALSE - if(SStraitor.get_taken_count(type) > 0) // Prevents multiple people from ever getting the same final objective. - return FALSE - if(length(possible_duplicates) > 0) - return FALSE - return TRUE - -/datum/traitor_objective/ultimate/on_objective_taken(mob/user) - . = ..() - handler.maximum_potential_objectives = 0 - for(var/datum/traitor_objective/objective as anything in handler.potential_objectives) - if(objective == src) - continue - objective.fail_objective() - user.playsound_local(get_turf(user), 'sound/traitor/final_objective.ogg', vol = 100, vary = FALSE, channel = CHANNEL_TRAITOR) - handler.final_objective = name - -/datum/traitor_objective/ultimate/uplink_ui_data(mob/user) - . = ..() - .["final_objective"] = TRUE diff --git a/code/modules/antagonists/traitor/objectives/final_objective/infect_ai.dm b/code/modules/antagonists/traitor/objectives/final_objective/infect_ai.dm deleted file mode 100644 index d47a2c6aabc5..000000000000 --- a/code/modules/antagonists/traitor/objectives/final_objective/infect_ai.dm +++ /dev/null @@ -1,56 +0,0 @@ -/datum/traitor_objective/ultimate/infect_ai - name = "Infect the station AI with an experimental virus." - description = "Infect the station AI with an experimental virus. Go to %AREA% to receive an infected law upload board \ - and use it on the AI core or a law upload console." - - ///area type the objective owner must be in to receive the law upload module - var/area/board_area_pickup - ///checker on whether we have sent the law upload module - var/sent_board = FALSE - -/datum/traitor_objective/ultimate/infect_ai/can_generate_objective(generating_for, list/possible_duplicates) - . = ..() - if(!.) - return FALSE - - for(var/mob/living/silicon/ai/ai in GLOB.ai_list) - if(ai.stat == DEAD || ai.mind?.has_antag_datum(/datum/antagonist/malf_ai) || !is_station_level(ai.z)) - continue - return TRUE - - return FALSE - -/datum/traitor_objective/ultimate/infect_ai/generate_objective(datum/mind/generating_for, list/possible_duplicates) - var/list/possible_areas = GLOB.the_station_areas.Copy() - for(var/area/possible_area as anything in possible_areas) - //remove areas too close to the destination, too obvious for our poor shmuck, or just unfair - if(istype(possible_area, /area/station/hallway) || istype(possible_area, /area/station/security)) - possible_areas -= possible_area - if(!length(possible_areas)) - return FALSE - board_area_pickup = pick(possible_areas) - replace_in_name("%AREA%", initial(board_area_pickup.name)) - return TRUE - -/datum/traitor_objective/ultimate/infect_ai/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!sent_board) - buttons += add_ui_button("", "Pressing this will call down a pod with an infected law upload board.", "wifi", "upload_board") - return buttons - -/datum/traitor_objective/ultimate/infect_ai/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("upload_board") - if(sent_board) - return - var/area/delivery_area = get_area(user) - if(delivery_area.type != board_area_pickup) - to_chat(user, span_warning("You must be in [initial(board_area_pickup.name)] to receive the infected law upload board.")) - return - sent_board = TRUE - podspawn(list( - "target" = get_turf(user), - "style" = /datum/pod_style/syndicate, - "spawn" = /obj/item/ai_module/malf, - )) diff --git a/code/modules/antagonists/traitor/objectives/final_objective/objective_dark_matteor.dm b/code/modules/antagonists/traitor/objectives/final_objective/objective_dark_matteor.dm deleted file mode 100644 index b90fdba73d03..000000000000 --- a/code/modules/antagonists/traitor/objectives/final_objective/objective_dark_matteor.dm +++ /dev/null @@ -1,83 +0,0 @@ -/datum/traitor_objective/ultimate/dark_matteor - name = "Summon a dark matter singularity to consume the station." - description = "Go to %AREA%, and receive the smuggled satellites + emag. Set up and emag the satellites, \ - after enough have been recalibrated by the emag, IT COMES. Warning: The dark matter singularity will hunt all creatures, you included." - - //this is a prototype so this progression is for all basic level kill objectives - - ///area type the objective owner must be in to receive the satellites - var/area/satellites_spawnarea_type - ///checker on whether we have sent the satellites yet. - var/sent_satellites = FALSE - -/datum/traitor_objective/ultimate/dark_matteor/can_generate_objective(generating_for, list/possible_duplicates) - . = ..() - if(!.) - return FALSE - if(SSmapping.is_planetary()) - return FALSE //meteors can't spawn on planets - return TRUE - -/datum/traitor_objective/ultimate/dark_matteor/generate_objective(datum/mind/generating_for, list/possible_duplicates) - var/list/possible_areas = GLOB.the_station_areas.Copy() - for(var/area/possible_area as anything in possible_areas) - if(!ispath(possible_area, /area/station/maintenance/solars) && !ispath(possible_area, /area/station/solars)) - possible_areas -= possible_area - if(length(possible_areas) == 0) - return FALSE - satellites_spawnarea_type = pick(possible_areas) - replace_in_name("%AREA%", initial(satellites_spawnarea_type.name)) - return TRUE - -/datum/traitor_objective/ultimate/dark_matteor/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!sent_satellites) - buttons += add_ui_button("", "Pressing this will call down a pod with the smuggled satellites.", "satellite", "satellite") - return buttons - -/datum/traitor_objective/ultimate/dark_matteor/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("satellite") - if(sent_satellites) - return - var/area/delivery_area = get_area(user) - if(delivery_area.type != satellites_spawnarea_type) - to_chat(user, span_warning("You must be in [initial(satellites_spawnarea_type.name)] to receive the smuggled satellites.")) - return - sent_satellites = TRUE - podspawn(list( - "target" = get_turf(user), - "style" = /datum/pod_style/syndicate, - "spawn" = /obj/structure/closet/crate/engineering/smuggled_meteor_shields, - )) - -/obj/structure/closet/crate/engineering/smuggled_meteor_shields - -/obj/structure/closet/crate/engineering/smuggled_meteor_shields/PopulateContents() - ..() - for(var/i in 1 to 11) - new /obj/machinery/satellite/meteor_shield(src) - new /obj/item/card/emag/meteor_shield_recalibrator(src) - new /obj/item/paper/dark_matteor_summoning(src) - -/obj/item/paper/dark_matteor_summoning - name = "notes - dark matter meteor summoning" - default_raw_text = {" - Summoning a dark matter meteor.
-
-
- Operative, this crate contains 10+1 spare meteor shield satellites stolen from NT’s supply lines. Your mission is to - deploy them in space near the station and recalibrate them with the provided emag. Be careful: you need a 30 second - cooldown between each hack, and NT will detect your interference after seven recalibrations. That means you - have at least 5 minutes of work and 1 minute of resistance.
-
- This is a high-risk operation. You’ll need backup, fortification, and determination. The reward? - A spectacular dark matter singularity that will wipe out the station.
-
- **Death to Nanotrasen.** -"} - -/obj/item/card/emag/meteor_shield_recalibrator - name = "cryptographic satellite recalibrator" - desc = "It's a cryptographic sequencer that has been tuned to recalibrate meteor shields quicker and with less risk of frying them." diff --git a/code/modules/antagonists/traitor/objectives/final_objective/romerol.dm b/code/modules/antagonists/traitor/objectives/final_objective/romerol.dm deleted file mode 100644 index 09edc4b0c739..000000000000 --- a/code/modules/antagonists/traitor/objectives/final_objective/romerol.dm +++ /dev/null @@ -1,46 +0,0 @@ -/datum/traitor_objective/ultimate/romerol - name = "Spread the experimental bioterror agent Romerol by calling a droppod down at %AREA%" - description = "Go to %AREA%, and receive the bioterror agent. Spread it to the crew, \ - and watch then raise from the dead as mindless killing machines. Warning: The undead will attack you too." - - //this is a prototype so this progression is for all basic level kill objectives - - ///area type the objective owner must be in to receive the romerol - var/area/romerol_spawnarea_type - ///checker on whether we have sent the romerol yet. - var/sent_romerol = FALSE - -/datum/traitor_objective/ultimate/romerol/generate_objective(datum/mind/generating_for, list/possible_duplicates) - var/list/possible_areas = GLOB.the_station_areas.Copy() - for(var/area/possible_area as anything in possible_areas) - //remove areas too close to the destination, too obvious for our poor shmuck, or just unfair - if(ispath(possible_area, /area/station/hallway) || ispath(possible_area, /area/station/security)) - possible_areas -= possible_area - if(length(possible_areas) == 0) - return FALSE - romerol_spawnarea_type = pick(possible_areas) - replace_in_name("%AREA%", initial(romerol_spawnarea_type.name)) - return TRUE - -/datum/traitor_objective/ultimate/romerol/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!sent_romerol) - buttons += add_ui_button("", "Pressing this will call down a pod with the biohazard kit.", "biohazard", "romerol") - return buttons - -/datum/traitor_objective/ultimate/romerol/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("romerol") - if(sent_romerol) - return - var/area/delivery_area = get_area(user) - if(delivery_area.type != romerol_spawnarea_type) - to_chat(user, span_warning("You must be in [initial(romerol_spawnarea_type.name)] to receive the bioterror agent.")) - return - sent_romerol = TRUE - podspawn(list( - "target" = get_turf(user), - "style" = /datum/pod_style/syndicate, - "spawn" = /obj/item/storage/box/syndie_kit/romerol, - )) diff --git a/code/modules/antagonists/traitor/objectives/final_objective/space_dragon.dm b/code/modules/antagonists/traitor/objectives/final_objective/space_dragon.dm deleted file mode 100644 index 4d39412653ed..000000000000 --- a/code/modules/antagonists/traitor/objectives/final_objective/space_dragon.dm +++ /dev/null @@ -1,49 +0,0 @@ -/datum/traitor_objective/ultimate/space_dragon - name = "Find a Space Carp and mutate their DNA with your own using a DNA harvester we will drop pod at %AREA%" - description = "Go to %AREA%, and receive the Carp DNA scanner. Use it on any Space Carp to harvest its DNA. \ - From there, use it on yourself, to mutate your own DNA with it and become a Space Dragon. \ - Don't worry about finding one, I'm sure there'll have a wave of carp right when you need it." - - ///Area type the objective owner must be in to receive the DNA extractor. - var/area/dna_scanner_spawnarea_type - ///Whether the DNA extraction kit was sent already. - var/received_dna_scanner = FALSE - -/datum/traitor_objective/ultimate/space_dragon/on_objective_taken(mob/user) - . = ..() - force_event(/datum/round_event_control/carp_migration, "[handler.owner]'s final objective") - -/datum/traitor_objective/ultimate/space_dragon/generate_objective(datum/mind/generating_for, list/possible_duplicates) - var/list/possible_areas = GLOB.the_station_areas.Copy() - for(var/area/possible_area as anything in possible_areas) - //remove areas too close to the destination, too obvious for our poor shmuck, or just unfair - if(ispath(possible_area, /area/station/hallway) || ispath(possible_area, /area/station/security)) - possible_areas -= possible_area - if(length(possible_areas) == 0) - return FALSE - dna_scanner_spawnarea_type = pick(possible_areas) - replace_in_name("%AREA%", initial(dna_scanner_spawnarea_type.name)) - return TRUE - -/datum/traitor_objective/ultimate/space_dragon/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!received_dna_scanner) - buttons += add_ui_button("", "Pressing this will call down a pod with the DNA extraction kit.", "biohazard", "carp_dna") - return buttons - -/datum/traitor_objective/ultimate/space_dragon/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("carp_dna") - if(received_dna_scanner) - return - var/area/delivery_area = get_area(user) - if(delivery_area.type != dna_scanner_spawnarea_type) - to_chat(user, span_warning("You must be in [initial(dna_scanner_spawnarea_type.name)] to receive the DNA extraction kit.")) - return - received_dna_scanner = TRUE - podspawn(list( - "target" = get_turf(user), - "style" = /datum/pod_style/syndicate, // Non-module change : upstream killed this lol - "spawn" = /obj/item/storage/box/syndie_kit/space_dragon, - )) diff --git a/code/modules/antagonists/traitor/objectives/final_objective/supermatter_cascade.dm b/code/modules/antagonists/traitor/objectives/final_objective/supermatter_cascade.dm deleted file mode 100644 index 2e9396c90b07..000000000000 --- a/code/modules/antagonists/traitor/objectives/final_objective/supermatter_cascade.dm +++ /dev/null @@ -1,57 +0,0 @@ -/datum/traitor_objective/ultimate/supermatter_cascade - name = "Destroy the station by causing a crystallizing resonance cascade" - description = "Destroy the station by causing a supermatter cascade. Go to %AREA% to retrieve the destabilizing crystal \ - and use it on the supermatter." - - ///area type the objective owner must be in to receive the destabilizing crystal - var/area/dest_crystal_area_pickup - ///checker on whether we have sent the crystal yet. - var/sent_crystal = FALSE - -/datum/traitor_objective/ultimate/supermatter_cascade/can_generate_objective(generating_for, list/possible_duplicates) - . = ..() - if(!.) - return FALSE - - if(isnull(GLOB.main_supermatter_engine)) - return FALSE - var/obj/machinery/power/supermatter_crystal/engine/crystal = locate() in GLOB.main_supermatter_engine - if(!is_station_level(crystal.z) && !is_mining_level(crystal.z)) - return FALSE - - return TRUE - -/datum/traitor_objective/ultimate/supermatter_cascade/generate_objective(datum/mind/generating_for, list/possible_duplicates) - var/list/possible_areas = GLOB.the_station_areas.Copy() - for(var/area/possible_area as anything in possible_areas) - //remove areas too close to the destination, too obvious for our poor shmuck, or just unfair - if(ispath(possible_area, /area/station/hallway) || ispath(possible_area, /area/station/security)) - possible_areas -= possible_area - if(length(possible_areas) == 0) - return FALSE - dest_crystal_area_pickup = pick(possible_areas) - replace_in_name("%AREA%", initial(dest_crystal_area_pickup.name)) - return TRUE - -/datum/traitor_objective/ultimate/supermatter_cascade/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!sent_crystal) - buttons += add_ui_button("", "Pressing this will call down a pod with the supermatter cascade kit.", "biohazard", "destabilizing_crystal") - return buttons - -/datum/traitor_objective/ultimate/supermatter_cascade/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("destabilizing_crystal") - if(sent_crystal) - return - var/area/delivery_area = get_area(user) - if(delivery_area.type != dest_crystal_area_pickup) - to_chat(user, span_warning("You must be in [initial(dest_crystal_area_pickup.name)] to receive the supermatter cascade kit.")) - return - sent_crystal = TRUE - podspawn(list( - "target" = get_turf(user), - "style" = /datum/pod_style/syndicate, - "spawn" = /obj/item/destabilizing_crystal, - )) diff --git a/code/modules/antagonists/traitor/objectives/hack_comm_console.dm b/code/modules/antagonists/traitor/objectives/hack_comm_console.dm deleted file mode 100644 index 93323e4e15f0..000000000000 --- a/code/modules/antagonists/traitor/objectives/hack_comm_console.dm +++ /dev/null @@ -1,53 +0,0 @@ -/datum/traitor_objective_category/hack_comm_console - name = "Hack Communication Console" - objectives = list( - /datum/traitor_objective/hack_comm_console = 1, - ) - -/datum/traitor_objective/hack_comm_console - name = "Hack a communication console to summon an unknown threat to the station" - description = "Right click on a communication console to begin the hacking process. Once started, the AI will know that you are hacking a communication console, so be ready to run or have yourself disguised to prevent being caught. This objective will invalidate itself if another traitor completes it first." - - progression_minimum = 60 MINUTES - progression_reward = list(30 MINUTES, 40 MINUTES) - telecrystal_reward = list(7, 12) - - var/progression_objectives_minimum = 20 MINUTES - -/datum/traitor_objective/hack_comm_console/can_generate_objective(datum/mind/generating_for, list/possible_duplicates) - if(SStraitor.get_taken_count(/datum/traitor_objective/hack_comm_console) > 0) - return FALSE - if(handler.get_completion_progression(/datum/traitor_objective) < progression_objectives_minimum) - return FALSE - return TRUE - -/datum/traitor_objective/hack_comm_console/generate_objective(datum/mind/generating_for, list/possible_duplicates) - AddComponent(/datum/component/traitor_objective_mind_tracker, generating_for, \ - signals = list(COMSIG_LIVING_UNARMED_ATTACK = PROC_REF(on_unarmed_attack))) - RegisterSignal(SSdcs, COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED, PROC_REF(on_global_obj_completed)) - return TRUE - -/datum/traitor_objective/hack_comm_console/ungenerate_objective() - UnregisterSignal(SSdcs, COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED) - -/datum/traitor_objective/hack_comm_console/proc/on_global_obj_completed(datum/source, datum/traitor_objective/objective) - SIGNAL_HANDLER - if(istype(objective, /datum/traitor_objective/hack_comm_console)) - fail_objective() - -/datum/traitor_objective/hack_comm_console/proc/on_unarmed_attack(mob/user, obj/machinery/computer/communications/target, proximity_flag, modifiers) - SIGNAL_HANDLER - if(!proximity_flag) - return - if(!modifiers[RIGHT_CLICK]) - return - if(!istype(target)) - return - INVOKE_ASYNC(src, PROC_REF(begin_hack), user, target) - return COMPONENT_CANCEL_ATTACK_CHAIN - -/datum/traitor_objective/hack_comm_console/proc/begin_hack(mob/user, obj/machinery/computer/communications/target) - if(!target.try_hack_console(user)) - return - - succeed_objective() diff --git a/code/modules/antagonists/traitor/objectives/infect.dm b/code/modules/antagonists/traitor/objectives/infect.dm deleted file mode 100644 index 459e8f54a0d7..000000000000 --- a/code/modules/antagonists/traitor/objectives/infect.dm +++ /dev/null @@ -1,176 +0,0 @@ -/datum/traitor_objective_category/infect - name = "Infect with Disease" - objectives = list( - /datum/traitor_objective/target_player/infect = 1, - ) - -/datum/traitor_objective/target_player/infect - name = "Infect %TARGET% the %JOB TITLE%" - description = "Infect your target with the experimental Hereditary Manifold Sickness." - - progression_minimum = 30 MINUTES - - progression_reward = list(8 MINUTES, 14 MINUTES) - telecrystal_reward = 1 - - duplicate_type = /datum/traitor_objective/target_player/infect - - /// if TRUE, can only target heads of staff - /// if FALSE, CANNOT target heads of staff - var/heads_of_staff = FALSE - /// if TRUE, the injector item has been bestowed upon the player - var/injector_given = FALSE - -/datum/traitor_objective/target_player/infect/supported_configuration_changes() - . = ..() - . += NAMEOF(src, objective_period) - . += NAMEOF(src, maximum_objectives_in_period) - -/datum/traitor_objective/target_player/infect/can_generate_objective(generating_for, list/possible_duplicates) - if(length(possible_duplicates) > 0) - return FALSE - return ..() - -/datum/traitor_objective/target_player/infect/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!injector_given) - buttons += add_ui_button("", "Pressing this will materialize a EHMS autoinjector into your hand, which you must inject into the target to succeed.", "syringe", "summon_pen") - return buttons - -/datum/traitor_objective/target_player/infect/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("summon_pen") - if(injector_given) - return - injector_given = TRUE - var/obj/item/reagent_containers/hypospray/medipen/manifoldinjector/ehms = new(user.drop_location()) - user.put_in_hands(ehms) - ehms.balloon_alert(user, "the injector materializes in your hand") - RegisterSignal(ehms, COMSIG_EHMS_INJECTOR_INJECTED, PROC_REF(on_injected)) - AddComponent(/datum/component/traitor_objective_register, ehms, \ - succeed_signals = null, \ - fail_signals = list(COMSIG_QDELETING), \ - penalty = TRUE) - -/datum/traitor_objective/target_player/infect/proc/on_injected(datum/source, mob/living/user, mob/living/injected) - SIGNAL_HANDLER - if(injected != target) - fail_objective() - return - if(injected == target) - succeed_objective() - return - -/datum/traitor_objective/target_player/infect/generate_objective(datum/mind/generating_for, list/possible_duplicates) - - var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things - for(var/datum/objective/task as anything in handler.primary_objectives) - if(!istype(task.target, /datum/mind)) - continue - already_targeting += task.target //Removing primary objective kill targets from the list - - var/parent_type = type2parent(type) - //don't roll head of staff types if you haven't completed the normal version - if(heads_of_staff && !handler.get_completion_count(parent_type)) - // Locked if they don't have any of the risky bug room objective completed - return FALSE - - var/list/possible_targets = list() - var/try_target_late_joiners = FALSE - if(generating_for.late_joiner) - try_target_late_joiners = TRUE - for(var/datum/mind/possible_target as anything in get_crewmember_minds()) - if(possible_target in already_targeting) - continue - var/target_area = get_area(possible_target.current) - if(possible_target == generating_for) - continue - if(!ishuman(possible_target.current)) - continue - if(possible_target.current.stat == DEAD) - continue - var/datum/antagonist/traitor/traitor = possible_target.has_antag_datum(/datum/antagonist/traitor) - if(traitor && traitor.uplink_handler.telecrystals >= 0) - continue - var/mob/living/carbon/human/targets_current = possible_target.current - var/datum/disease/chronic_illness/illness = locate() in targets_current.diseases - if(illness) - continue - if(!HAS_TRAIT(SSstation, STATION_TRAIT_LATE_ARRIVALS) && istype(target_area, /area/shuttle/arrival)) - continue - //removes heads of staff from being targets from non heads of staff assassinations, and vice versa - if(heads_of_staff) - if(!(possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF)) - continue - else - if((possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF)) - continue - possible_targets += possible_target - for(var/datum/traitor_objective/target_player/objective as anything in possible_duplicates) - possible_targets -= objective.target - if(try_target_late_joiners) - var/list/all_possible_targets = possible_targets.Copy() - for(var/datum/mind/possible_target as anything in all_possible_targets) - if(!possible_target.late_joiner) - possible_targets -= possible_target - if(!possible_targets.len) - possible_targets = all_possible_targets - special_target_filter(possible_targets) - if(!possible_targets.len) - return FALSE //MISSION FAILED, WE'LL GET EM NEXT TIME - - var/datum/mind/target_mind = pick(possible_targets) - set_target(target_mind.current) - replace_in_name("%TARGET%", target.real_name) - replace_in_name("%JOB TITLE%", target_mind.assigned_role.title) - RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_death)) - return TRUE - -/datum/traitor_objective/target_player/infect/ungenerate_objective() - UnregisterSignal(target, COMSIG_LIVING_DEATH) - set_target(null) - -///proc for checking for special states that invalidate a target -/datum/traitor_objective/target_player/infect/proc/special_target_filter(list/possible_targets) - return - -/datum/traitor_objective/target_player/infect/target_deleted() - if(objective_state == OBJECTIVE_STATE_INACTIVE) - //don't take an objective target of someone who is already obliterated - fail_objective() - return ..() - -/datum/traitor_objective/target_player/infect/proc/on_target_death() - SIGNAL_HANDLER - if(objective_state == OBJECTIVE_STATE_INACTIVE) - //don't take an objective target of someone who is already dead - fail_objective() - -/obj/item/reagent_containers/hypospray/medipen/manifoldinjector - name = "EHMS autoinjector" - desc = "Experimental Hereditary Manifold Sickness autoinjector." - icon_state = "tbpen" - inhand_icon_state = "tbpen" - base_icon_state = "tbpen" - volume = 30 - amount_per_transfer_from_this = 30 - list_reagents = list(/datum/reagent/medicine/sansufentanyl = 20) - stealthy = TRUE - //Was the injector used on someone yet? - var/used = FALSE - -/obj/item/reagent_containers/hypospray/medipen/manifoldinjector/attack(mob/living/affected_mob, mob/living/carbon/human/user) - if(used) - return ..() - to_chat(affected_mob, span_warning("You feel someone try to inject you with something.")) - balloon_alert(user, "injecting...") - log_combat(user, affected_mob, "attempted to inject", src) - if(!do_after(user, 1.5 SECONDS, hidden = TRUE)) - balloon_alert(user, "interrupted!") - return - var/datum/disease/chronic_illness/hms = new /datum/disease/chronic_illness() - affected_mob.ForceContractDisease(hms) - used = TRUE - inject(affected_mob, user) - SEND_SIGNAL(src, COMSIG_EHMS_INJECTOR_INJECTED, user, affected_mob) diff --git a/code/modules/antagonists/traitor/objectives/kidnapping.dm b/code/modules/antagonists/traitor/objectives/kidnapping.dm deleted file mode 100644 index 3e41b7f6916e..000000000000 --- a/code/modules/antagonists/traitor/objectives/kidnapping.dm +++ /dev/null @@ -1,320 +0,0 @@ -/datum/traitor_objective/target_player/kidnapping - name = "Kidnap %TARGET% the %JOB TITLE% and deliver them to %AREA%" - description = "%TARGET% holds extremely important information regarding secret NT projects - and you'll need to kidnap and deliver them to %AREA%, where our transport pod will be waiting. \ - If %TARGET% is delivered alive, you will be rewarded with an additional %TC% telecrystals." - - abstract_type = /datum/traitor_objective/target_player/kidnapping - - /// The jobs that this objective is targeting. - var/list/target_jobs - /// Area that the target needs to be delivered to - var/area/dropoff_area - /// Have we called the pod yet? - var/pod_called = FALSE - /// How much TC do we get from sending the target alive - var/alive_bonus = 0 - /// All stripped targets belongings (weakrefs) - var/list/target_belongings = list() - - duplicate_type = /datum/traitor_objective/target_player - -/datum/traitor_objective/target_player/kidnapping/supported_configuration_changes() - . = ..() - . += NAMEOF(src, objective_period) - . += NAMEOF(src, maximum_objectives_in_period) - -/datum/traitor_objective/target_player/kidnapping/New(datum/uplink_handler/handler) - . = ..() - AddComponent(/datum/component/traitor_objective_limit_per_time, \ - /datum/traitor_objective/target_player, \ - time_period = objective_period, \ - maximum_objectives = maximum_objectives_in_period \ - ) - -/datum/traitor_objective/target_player/kidnapping/common - progression_minimum = 0 MINUTES - progression_maximum = 30 MINUTES - progression_reward = list(2 MINUTES, 4 MINUTES) - telecrystal_reward = list(1, 2) - target_jobs = list( - // Cargo - /datum/job/cargo_technician, - // Engineering - /datum/job/atmospheric_technician, - /datum/job/station_engineer, - // Medical - /datum/job/chemist, - /datum/job/doctor, - /datum/job/psychologist, - /datum/job/virologist, - // Science - /datum/job/geneticist, - /datum/job/roboticist, - /datum/job/scientist, - // Service - /datum/job/bartender, - /datum/job/botanist, - /datum/job/chaplain, - /datum/job/clown, - /datum/job/curator, - /datum/job/janitor, - /datum/job/lawyer, - /datum/job/mime, - ) - alive_bonus = 2 - -/datum/traitor_objective/target_player/kidnapping/common/assistant - progression_minimum = 0 MINUTES - progression_maximum = 15 MINUTES - target_jobs = list( - /datum/job/assistant - ) - -/datum/traitor_objective/target_player/kidnapping/uncommon //Hard to fish out targets - progression_minimum = 0 MINUTES - progression_maximum = 45 MINUTES - progression_reward = list(4 MINUTES, 8 MINUTES) - telecrystal_reward = list(1, 2) - - target_jobs = list( - // Cargo - /datum/job/shaft_miner, - // Medical - /datum/job/paramedic, - // Service - /datum/job/cook, - ) - alive_bonus = 3 - -/datum/traitor_objective/target_player/kidnapping/rare - progression_minimum = 15 MINUTES - progression_maximum = 60 MINUTES - progression_reward = list(8 MINUTES, 12 MINUTES) - telecrystal_reward = list(2, 3) - target_jobs = list( - // Heads of staff - /datum/job/chief_engineer, - /datum/job/chief_medical_officer, - /datum/job/head_of_personnel, - /datum/job/research_director, - /datum/job/quartermaster, - // Security - /datum/job/detective, - /datum/job/security_officer, - /datum/job/warden, - ) - alive_bonus = 4 - -/datum/traitor_objective/target_player/kidnapping/captain - progression_minimum = 30 MINUTES - progression_reward = list(12 MINUTES, 16 MINUTES) - telecrystal_reward = list(2, 3) - target_jobs = list( - /datum/job/captain, - /datum/job/head_of_security, - ) - alive_bonus = 5 - -/datum/traitor_objective/target_player/kidnapping/generate_objective(datum/mind/generating_for, list/possible_duplicates) - - var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things - for(var/datum/objective/task as anything in handler.primary_objectives) - if(!istype(task.target, /datum/mind)) - continue - already_targeting += task.target //Removing primary objective kill targets from the list - - var/list/possible_targets = list() - for(var/datum/mind/possible_target as anything in get_crewmember_minds()) - if(possible_target == generating_for) - continue - - if(possible_target in already_targeting) - continue - - if(!ishuman(possible_target.current)) - continue - - if(possible_target.current.stat == DEAD) - continue - - if(HAS_TRAIT(possible_target, TRAIT_HAS_BEEN_KIDNAPPED)) - continue - - if(possible_target.has_antag_datum(/datum/antagonist/traitor)) - continue - - if(!(possible_target.assigned_role.type in target_jobs)) - continue - - possible_targets += possible_target - - for(var/datum/traitor_objective/target_player/objective as anything in possible_duplicates) - if(!objective.target) //the old objective was already completed. - continue - possible_targets -= objective.target.mind - - if(!length(possible_targets)) - return FALSE - - var/datum/mind/target_mind = pick(possible_targets) - set_target(target_mind.current) - AddComponent(/datum/component/traitor_objective_register, target, fail_signals = list(COMSIG_QDELETING)) - var/list/possible_areas = GLOB.the_station_areas.Copy() - for(var/area/possible_area as anything in possible_areas) - if(ispath(possible_area, /area/station/hallway) || ispath(possible_area, /area/station/security) || initial(possible_area.outdoors)) - possible_areas -= possible_area - - dropoff_area = pick(possible_areas) - replace_in_name("%TARGET%", target_mind.name) - replace_in_name("%JOB TITLE%", target_mind.assigned_role.title) - replace_in_name("%AREA%", initial(dropoff_area.name)) - replace_in_name("%TC%", alive_bonus) - return TRUE - -/datum/traitor_objective/target_player/kidnapping/ungenerate_objective() - set_target(null) - dropoff_area = null - -/datum/traitor_objective/target_player/kidnapping/on_objective_taken(mob/user) - . = ..() - INVOKE_ASYNC(src, PROC_REF(generate_holding_area)) - -/datum/traitor_objective/target_player/kidnapping/proc/generate_holding_area() - // Let's load in the holding facility ahead of time - // even if they fail the objective it's better to get done now rather than later - SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY) - -/datum/traitor_objective/target_player/kidnapping/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!pod_called) - buttons += add_ui_button("Call Extraction Pod", "Pressing this will call down an extraction pod.", "rocket", "call_pod") - return buttons - -/datum/traitor_objective/target_player/kidnapping/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("call_pod") - if(pod_called) - return - var/area/user_area = get_area(user) - var/area/target_area = get_area(target) - - if(user_area.type != dropoff_area) - to_chat(user, span_warning("You must be in [initial(dropoff_area.name)] to call the extraction pod.")) - return - - if(target_area.type != dropoff_area) - to_chat(user, span_warning("[target.real_name] must be in [initial(dropoff_area.name)] for you to call the extraction pod.")) - return - - call_pod(user) - -/datum/traitor_objective/target_player/kidnapping/proc/call_pod(mob/living/user) - pod_called = TRUE - var/obj/structure/closet/supplypod/extractionpod/new_pod = new() - RegisterSignal(new_pod, COMSIG_ATOM_ENTERED, PROC_REF(enter_check)) - new /obj/effect/pod_landingzone(get_turf(user), new_pod) - -/datum/traitor_objective/target_player/kidnapping/proc/enter_check(obj/structure/closet/supplypod/extractionpod/source, entered_atom) - if(!istype(source)) - CRASH("Kidnapping objective's enter_check called with source being not an extraction pod: [source ? source.type : "N/A"]") - - if(!ishuman(entered_atom)) - return - - var/mob/living/carbon/human/sent_mob = entered_atom - - if(sent_mob.mind) - ADD_TRAIT(sent_mob.mind, TRAIT_HAS_BEEN_KIDNAPPED, TRAIT_GENERIC) - - for(var/obj/item/belonging in sent_mob.gather_belongings()) - if(belonging == sent_mob.get_item_by_slot(ITEM_SLOT_ICLOTHING) || belonging == sent_mob.get_item_by_slot(ITEM_SLOT_FEET)) - continue - - var/unequipped = sent_mob.transferItemToLoc(belonging) - if (!unequipped) - continue - target_belongings.Add(WEAKREF(belonging)) - - var/datum/bank_account/cargo_account = SSeconomy.get_dep_account(ACCOUNT_CAR) - - if(cargo_account) //Just in case - cargo_account.adjust_money(-min(rand(1000, 3000), cargo_account.account_balance)) //Not so much, especially for competent cargo. Plus this can't be mass-triggered like it has been done with contractors - - priority_announce("One of your crew was captured by a rival organisation - we've needed to pay their ransom to bring them back. As is policy we've taken a portion of the station's funds to offset the overall cost.", "Nanotrasen Asset Protection", has_important_message = TRUE) - - addtimer(CALLBACK(src, PROC_REF(handle_target), sent_mob), 1.5 SECONDS) - - if(sent_mob != target) - fail_objective(penalty_cost = telecrystal_penalty) - source.startExitSequence(source) - return - - if(sent_mob.stat != DEAD) - telecrystal_reward += alive_bonus - - succeed_objective() - source.startExitSequence(source) - -/datum/traitor_objective/target_player/kidnapping/proc/handle_target(mob/living/carbon/human/sent_mob) - addtimer(CALLBACK(src, PROC_REF(return_target), sent_mob), 3 MINUTES) - if(sent_mob.stat == DEAD) - return - - sent_mob.flash_act() - sent_mob.adjust_confusion(10 SECONDS) - sent_mob.adjust_dizzy(10 SECONDS) - sent_mob.set_eye_blur_if_lower(100 SECONDS) - sent_mob.dna.species.give_important_for_life(sent_mob) // so plasmamen do not get left for dead - to_chat(sent_mob, span_hypnophrase(span_reallybig("A million voices echo in your head... \"Your mind held many valuable secrets - \ - we thank you for providing them. Your value is expended, and you will be ransomed back to your station. We always get paid, \ - so it's only a matter of time before we ship you back...\""))) - -/datum/traitor_objective/target_player/kidnapping/proc/return_target(mob/living/carbon/human/sent_mob) - if(!sent_mob || QDELETED(sent_mob)) //suicided and qdeleted themselves - return - - var/list/possible_turfs = list() - - for(var/turf/open/open_turf in dropoff_area.get_turfs_from_all_zlevels()) - if(open_turf.is_blocked_turf() || isspaceturf(open_turf)) - continue - possible_turfs += open_turf - - if(!LAZYLEN(possible_turfs)) - var/turf/new_turf = get_safe_random_station_turf() - if(!new_turf) //SOMEHOW - to_chat(sent_mob, span_hypnophrase(span_reallybig("A million voices echo in your head... \"Seems where you got sent here from won't \ - be able to handle our pod... You will die here instead.\"
"))) - if (sent_mob.can_heartattack()) - sent_mob.set_heartattack(TRUE) - return - - possible_turfs += new_turf - - var/obj/structure/closet/supplypod/return_pod = new() - return_pod.bluespace = TRUE - return_pod.explosionSize = list(0,0,0,0) - return_pod.style = /datum/pod_style/syndicate // Non-module change : removed upstream - - do_sparks(8, FALSE, sent_mob) - sent_mob.visible_message(span_notice("[sent_mob] vanishes!")) - for(var/obj/item/belonging in sent_mob.gather_belongings()) - if(belonging == sent_mob.get_item_by_slot(ITEM_SLOT_ICLOTHING) || belonging == sent_mob.get_item_by_slot(ITEM_SLOT_FEET)) - continue - sent_mob.dropItemToGround(belonging) // No souvenirs, except shoes and t-shirts - - for(var/datum/weakref/belonging_ref in target_belongings) - var/obj/item/belonging = belonging_ref.resolve() - if(!belonging) - continue - belonging.forceMove(return_pod) - - sent_mob.forceMove(return_pod) - sent_mob.flash_act() - sent_mob.adjust_confusion(10 SECONDS) - sent_mob.adjust_dizzy(10 SECONDS) - sent_mob.set_eye_blur_if_lower(100 SECONDS) - sent_mob.dna.species.give_important_for_life(sent_mob) // so plasmamen do not get left for dead - - new /obj/effect/pod_landingzone(pick(possible_turfs), return_pod) diff --git a/code/modules/antagonists/traitor/objectives/kill_pet.dm b/code/modules/antagonists/traitor/objectives/kill_pet.dm deleted file mode 100644 index 21bf06eb3868..000000000000 --- a/code/modules/antagonists/traitor/objectives/kill_pet.dm +++ /dev/null @@ -1,96 +0,0 @@ -/datum/traitor_objective_category/kill_pet - name = "Kill Pet" - objectives = list( - /datum/traitor_objective/kill_pet/high_risk = 1, - list( - /datum/traitor_objective/kill_pet = 2, - /datum/traitor_objective/kill_pet/medium_risk = 1, - ) = 4, - ) - -/datum/traitor_objective/kill_pet - name = "Kill the %DEPARTMENT HEAD%'s beloved %PET%" - description = "The %DEPARTMENT HEAD% has particularly annoyed us by sending us spam emails and we want their %PET% dead to show them what happens when they cross us. " - - progression_minimum = 0 MINUTES - telecrystal_reward = list(1, 2) - progression_reward = list(3 MINUTES, 6 MINUTES) - - /// Possible heads mapped to their pet type. Can be a list of possible pets - var/list/possible_heads = list( - JOB_HEAD_OF_PERSONNEL = list( - /mob/living/basic/pet/dog/corgi/ian, - /mob/living/basic/pet/dog/corgi/puppy/ian - ), - JOB_CAPTAIN = /mob/living/basic/pet/fox/renault, - JOB_CHIEF_MEDICAL_OFFICER = /mob/living/basic/pet/cat/runtime, - JOB_CHIEF_ENGINEER = /mob/living/basic/parrot/poly, - JOB_QUARTERMASTER = list( - /mob/living/basic/gorilla/cargorilla, - /mob/living/basic/sloth/citrus, - /mob/living/basic/sloth/paperwork, - ) - ) - /// The head that we are targeting - var/datum/job/target - /// Whether or not we only take from the traitor's own department head or not. - var/limited_to_department_head = TRUE - /// The actual pet that needs to be killed - var/mob/living/target_pet - - duplicate_type = /datum/traitor_objective/kill_pet - -/datum/traitor_objective/kill_pet/medium_risk - progression_minimum = 10 MINUTES - progression_reward = list(5 MINUTES, 8 MINUTES) - limited_to_department_head = FALSE - -/datum/traitor_objective/kill_pet/high_risk - progression_minimum = 25 MINUTES - progression_reward = list(14 MINUTES, 18 MINUTES) - telecrystal_reward = list(2, 3) - - limited_to_department_head = FALSE - possible_heads = list( - JOB_HEAD_OF_SECURITY = list( - /mob/living/basic/carp/pet/lia, - /mob/living/basic/spider/giant/sgt_araneus, - ), - JOB_WARDEN = list( - /mob/living/basic/pet/dog/pug/mcgriff - ) - ) - -/datum/traitor_objective/kill_pet/generate_objective(datum/mind/generating_for, list/possible_duplicates) - var/datum/job/role = generating_for.assigned_role - for(var/datum/traitor_objective/kill_pet/objective as anything in possible_duplicates) - possible_heads -= objective.target.title - if(limited_to_department_head) - possible_heads = possible_heads & role.department_head - possible_heads -= role.title - - if(!length(possible_heads)) - return FALSE - target = SSjob.name_occupations[pick(possible_heads)] - var/pet_type = possible_heads[target.title] - if(islist(pet_type)) - for(var/type in pet_type) - target_pet = locate(type) in GLOB.mob_living_list - if(target_pet) - break - else - target_pet = locate(pet_type) in GLOB.mob_living_list - if(!target_pet) - return FALSE - if(target_pet.stat == DEAD) - return FALSE - AddComponent(/datum/component/traitor_objective_register, target_pet, \ - succeed_signals = list(COMSIG_QDELETING, COMSIG_LIVING_DEATH)) - replace_in_name("%DEPARTMENT HEAD%", target.title) - replace_in_name("%PET%", target_pet.name) - return TRUE - -/datum/traitor_objective/kill_pet/ungenerate_objective() - if(target_pet) - UnregisterSignal(target_pet, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH)) - target_pet = null diff --git a/code/modules/antagonists/traitor/objectives/locate_weakpoint.dm b/code/modules/antagonists/traitor/objectives/locate_weakpoint.dm deleted file mode 100644 index b08ae515a646..000000000000 --- a/code/modules/antagonists/traitor/objectives/locate_weakpoint.dm +++ /dev/null @@ -1,280 +0,0 @@ -/datum/traitor_objective_category/locate_weakpoint - name = "Locate And Destroy Weakpoint" - objectives = list( - /datum/traitor_objective/locate_weakpoint = 1, - ) - weight = OBJECTIVE_WEIGHT_UNLIKELY - -/datum/traitor_objective/locate_weakpoint - name = "Triangulate station's structural weakpoint and detonate an explosive charge nearby." - description = "You will be given a handheld device that you'll need to use in %AREA1% and %AREA2% in order to triangulate the station's structural weakpoint and detonate an explosive charge there. Warning: Once you start scanning either one of the areas, station's AI will be alerted." - - progression_minimum = 45 MINUTES - progression_reward = list(15 MINUTES, 20 MINUTES) - telecrystal_reward = list(3, 5) - - var/progression_objectives_minimum = 20 MINUTES - - /// Have we sent a weakpoint locator yet? - var/locator_sent = FALSE - /// Have we sent a bomb yet? - var/bomb_sent = FALSE - /// Have we located the weakpoint yet? - var/weakpoint_found = FALSE - /// Areas that need to be scanned - var/list/area/scan_areas - /// Weakpoint where the bomb should be planted - var/area/weakpoint_area - -/datum/traitor_objective/locate_weakpoint/can_generate_objective(datum/mind/generating_for, list/possible_duplicates) - if(handler.get_completion_progression(/datum/traitor_objective) < progression_objectives_minimum) - return FALSE - if(SStraitor.get_taken_count(/datum/traitor_objective/locate_weakpoint) > 0) - return FALSE - return TRUE - -/datum/traitor_objective/locate_weakpoint/generate_objective(datum/mind/generating_for, list/possible_duplicates) - scan_areas = list() - /// List of high-security areas that we pick required ones from - var/list/allowed_areas = typecacheof(list(/area/station/command, - /area/station/comms, - /area/station/engineering, - /area/station/science, - /area/station/security, - )) - - var/list/blacklisted_areas = typecacheof(list(/area/station/engineering/hallway, - /area/station/engineering/lobby, - /area/station/engineering/storage, - /area/station/science/lobby, - /area/station/science/ordnance/bomb, - /area/station/security/prison, - )) - - var/list/possible_areas = GLOB.the_station_areas.Copy() - for(var/area/possible_area as anything in possible_areas) - if(!is_type_in_typecache(possible_area, allowed_areas) || initial(possible_area.outdoors) || is_type_in_typecache(possible_area, blacklisted_areas)) - possible_areas -= possible_area - - for(var/i in 1 to 2) - scan_areas[pick_n_take(possible_areas)] = TRUE - weakpoint_area = pick_n_take(possible_areas) - - var/area/scan_area1 = scan_areas[1] - var/area/scan_area2 = scan_areas[2] - replace_in_name("%AREA1%", initial(scan_area1.name)) - replace_in_name("%AREA2%", initial(scan_area2.name)) - RegisterSignal(SSdcs, COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED, PROC_REF(on_global_obj_completed)) - return TRUE - -/datum/traitor_objective/locate_weakpoint/ungenerate_objective() - UnregisterSignal(SSdcs, COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED) - -/datum/traitor_objective/locate_weakpoint/on_objective_taken(mob/user) - . = ..() - - // We don't want multiple people being able to take weakpoint objectives if they get one available at the same time - for(var/datum/traitor_objective/locate_weakpoint/other_objective as anything in SStraitor.all_objectives_by_type[/datum/traitor_objective/locate_weakpoint]) - if(other_objective != src) - other_objective.fail_objective() - - -/datum/traitor_objective/locate_weakpoint/proc/on_global_obj_completed(datum/source, datum/traitor_objective/objective) - SIGNAL_HANDLER - if(istype(objective, /datum/traitor_objective/locate_weakpoint)) - fail_objective() - -/datum/traitor_objective/locate_weakpoint/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!locator_sent) - buttons += add_ui_button("", "Pressing this will materialize a weakpoint locator in your hand.", "globe", "locator") - if(weakpoint_found && !bomb_sent) - buttons += add_ui_button("", "Pressing this will materialize an ES8 explosive charge in your hand.", "bomb", "shatter_charge") - return buttons - -/datum/traitor_objective/locate_weakpoint/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("locator") - if(locator_sent) - return - locator_sent = TRUE - var/obj/item/weakpoint_locator/locator = new(user.drop_location(), src) - user.put_in_hands(locator) - locator.balloon_alert(user, "the weakpoint locator materializes in your hand") - - if("shatter_charge") - if(bomb_sent) - return - bomb_sent = TRUE - var/obj/item/grenade/c4/es8/bomb = new(user.drop_location(), src) - user.put_in_hands(bomb) - bomb.balloon_alert(user, "the ES8 charge materializes in your hand") - -/datum/traitor_objective/locate_weakpoint/proc/weakpoint_located() - description = "Structural weakpoint has been located in %AREA%. Detonate an ES8 explosive charge there to create a shockwave that will severely damage the station." - replace_in_name("%AREA%", initial(weakpoint_area.name)) - weakpoint_found = TRUE - -/datum/traitor_objective/locate_weakpoint/proc/create_shockwave(center_x, center_y, center_z) - var/turf/epicenter = locate(center_x, center_y, center_z) - var/lowpop = (length(GLOB.clients) <= CONFIG_GET(number/minimal_access_threshold)) - if(lowpop) - explosion(epicenter, devastation_range = 2, heavy_impact_range = 4, light_impact_range = 6, explosion_cause = src) - else - explosion(epicenter, devastation_range = 3, heavy_impact_range = 6, light_impact_range = 9, explosion_cause = src) - priority_announce( - "Attention crew, it appears that a high-power explosive charge has been detonated in your station's weakpoint, causing severe structural damage.", - "[command_name()] High-Priority Update" - ) - - succeed_objective() - -/obj/item/weakpoint_locator - name = "structural weakpoint locator" - desc = "A device that can triangulate station's structural weakpoint. It has to be used in %AREA1% and %AREA2% in order to triangulate the weakpoint. Warning: station's AI will be notified as soon as the process starts!" - icon = 'icons/obj/antags/syndicate_tools.dmi' - icon_state = "weakpoint_locator" - inhand_icon_state = "weakpoint_locator" - lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi' - righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi' - throwforce = 0 - w_class = WEIGHT_CLASS_SMALL - throw_speed = 3 - throw_range = 5 - var/datum/weakref/objective_weakref - -/obj/item/weakpoint_locator/Initialize(mapload, datum/traitor_objective/locate_weakpoint/objective) - . = ..() - objective_weakref = WEAKREF(objective) - if(!objective) - return - var/area/area1 = objective.scan_areas[1] - var/area/area2 = objective.scan_areas[2] - desc = replacetext(desc, "%AREA1%", initial(area1.name)) - desc = replacetext(desc, "%AREA2%", initial(area2.name)) - -/obj/item/weakpoint_locator/Destroy(force) - objective_weakref = null - return ..() - -/obj/item/weakpoint_locator/attack_self(mob/living/user, modifiers) - . = ..() - if(!istype(user) || loc != user || !user.mind) //No TK cheese - return - - var/datum/traitor_objective/locate_weakpoint/objective = objective_weakref.resolve() - - if(!objective || objective.objective_state == OBJECTIVE_STATE_INACTIVE) - to_chat(user, span_warning("Your time to use [src] has not come yet.")) - return - - if(objective.handler.owner != user.mind) - to_chat(user, span_warning("You have zero clue how to use [src].")) - return - - var/area/user_area = get_area(user) - if(!(user_area.type in objective.scan_areas)) - balloon_alert(user, "invalid area!") - playsound(user, 'sound/machines/buzz-sigh.ogg', 30, TRUE) - return - - if(!objective.scan_areas[user_area.type]) - balloon_alert(user, "already scanned here!") - playsound(user, 'sound/machines/buzz-sigh.ogg', 30, TRUE) - return - - user.visible_message(span_danger("[user] presses a few buttons on [src] and it starts ominously beeping!"), span_notice("You activate [src] and start scanning the area. Do not exit [get_area_name(user, TRUE)] until the scan finishes!")) - playsound(user, 'sound/machines/triple_beep.ogg', 30, TRUE) - var/alertstr = span_userdanger("Network Alert: Station network probing attempt detected[user_area?" in [get_area_name(user, TRUE)]":". Unable to pinpoint location"].") - for(var/mob/living/silicon/ai/ai_player in GLOB.player_list) - to_chat(ai_player, alertstr) - - if(!do_after(user, 30 SECONDS, src, IGNORE_USER_LOC_CHANGE | IGNORE_TARGET_LOC_CHANGE | IGNORE_HELD_ITEM | IGNORE_INCAPACITATED | IGNORE_SLOWDOWNS, extra_checks = CALLBACK(src, PROC_REF(scan_checks), user, user_area, objective), hidden = TRUE)) - playsound(user, 'sound/machines/buzz-sigh.ogg', 30, TRUE) - return - - playsound(user, 'sound/machines/ding.ogg', 100, TRUE) - objective.scan_areas[user_area.type] = FALSE - for(var/area/scan_area as anything in objective.scan_areas) - if(objective.scan_areas[scan_area]) - say("Next scanning location is [initial(scan_area.name)]") - return - - to_chat(user, span_notice("Scan finished. Structural weakpoint located in [initial(objective.weakpoint_area.name)].")) - objective.weakpoint_located() - -/obj/item/weakpoint_locator/proc/scan_checks(mob/living/user, area/user_area, datum/traitor_objective/locate_weakpoint/parent_objective) - if(get_area(user) != user_area) - return FALSE - - if(parent_objective.objective_state != OBJECTIVE_STATE_ACTIVE) - return FALSE - - var/atom/current_loc = loc - while(!isturf(current_loc) && !ismob(current_loc)) - current_loc = current_loc.loc - - if(current_loc != user) - return FALSE - - return TRUE - -/obj/item/grenade/c4/es8 - name = "ES8 explosive charge" - desc = "A high-power explosive charge designed to create a shockwave in a structural weakpoint of the station." - - icon_state = "plasticx40" - inhand_icon_state = "plasticx4" - worn_icon_state = "x4" - - boom_sizes = list(3, 6, 9) - - /// Weakref to user's objective - var/datum/weakref/objective_weakref - -/obj/item/grenade/c4/es8/Initialize(mapload, objective) - . = ..() - objective_weakref = WEAKREF(objective) - -/obj/item/grenade/c4/es8/Destroy() - objective_weakref = null - return ..() - -/obj/item/grenade/c4/es8/plant_c4(atom/bomb_target, mob/living/user) - if(!IS_TRAITOR(user)) - to_chat(user, span_warning("You can't seem to find a way to detonate the charge.")) - return FALSE - - var/datum/traitor_objective/locate_weakpoint/objective = objective_weakref.resolve() - if(!objective || objective.objective_state == OBJECTIVE_STATE_INACTIVE || objective.handler.owner != user.mind) - to_chat(user, span_warning("You don't think it would be wise to use [src].")) - return FALSE - - var/area/target_area = get_area(bomb_target) - if (target_area.type != objective.weakpoint_area) - to_chat(user, span_warning("[src] can only be detonated in [initial(objective.weakpoint_area.name)].")) - return FALSE - - if(!isfloorturf(bomb_target) && !iswallturf(bomb_target)) - to_chat(user, span_warning("[src] can only be planted on a wall or the floor!")) - return FALSE - - return ..() - -/obj/item/grenade/c4/es8/detonate(mob/living/lanced_by) - var/area/target_area = get_area(target) - var/datum/traitor_objective/locate_weakpoint/objective = objective_weakref.resolve() - - if(!objective) - return - - if (target_area.type != objective.weakpoint_area) - var/obj/item/grenade/c4/es8/new_bomb = new(target.drop_location()) - new_bomb.balloon_alert_to_viewers("invalid location!") - target.cut_overlay(plastic_overlay, TRUE) - qdel(src) - return - - objective.create_shockwave(target.x, target.y, target.z) - return ..() diff --git a/code/modules/antagonists/traitor/objectives/sabotage_machinery.dm b/code/modules/antagonists/traitor/objectives/sabotage_machinery.dm deleted file mode 100644 index 5a31c4c17397..000000000000 --- a/code/modules/antagonists/traitor/objectives/sabotage_machinery.dm +++ /dev/null @@ -1,240 +0,0 @@ -/// Datum which manages references to things we are instructed to destroy -GLOBAL_DATUM_INIT(objective_machine_handler, /datum/objective_target_machine_handler, new()) - -/// Marks a machine as a possible traitor sabotage target -/proc/add_sabotage_machine(source, typepath) - LAZYADD(GLOB.objective_machine_handler.machine_instances_by_path[typepath], source) - return typepath - -/// Traitor objective to destroy a machine the crew cares about -/datum/traitor_objective_category/sabotage_machinery - name = "Sabotage Worksite" - objectives = list( - /datum/traitor_objective/sabotage_machinery/trap = 1, - /datum/traitor_objective/sabotage_machinery/destroy = 1, - ) - -/datum/traitor_objective/sabotage_machinery - name = "Sabotage the %MACHINE%" - description = "Abstract objective holder which shouldn't appear in your uplink." - abstract_type = /datum/traitor_objective/sabotage_machinery - - /// The maximum amount of this type of objective a traitor can have, set to 0 for no limit. - var/maximum_allowed = 0 - /// The possible target machinery and the jobs tied to each one. - var/list/applicable_jobs = list() - /// The chosen job. Used to check for duplicates - var/chosen_job - -/datum/traitor_objective/sabotage_machinery/can_generate_objective(datum/mind/generating_for, list/possible_duplicates) - if(!maximum_allowed) - return TRUE - if(length(possible_duplicates) >= maximum_allowed) - return FALSE - return TRUE - -/datum/traitor_objective/sabotage_machinery/generate_objective(datum/mind/generating_for, list/possible_duplicates) - var/list/possible_jobs = applicable_jobs.Copy() - for(var/datum/traitor_objective/sabotage_machinery/objective as anything in possible_duplicates) - possible_jobs -= objective.chosen_job - for(var/available_job in possible_jobs) - var/job_machine_path = possible_jobs[available_job] - if (!length(GLOB.objective_machine_handler.machine_instances_by_path[job_machine_path])) - possible_jobs -= available_job - if(!length(possible_jobs)) - return FALSE - - chosen_job = pick(possible_jobs) - var/list/obj/machinery/possible_machines = GLOB.objective_machine_handler.machine_instances_by_path[possible_jobs[chosen_job]] - for(var/obj/machinery/machine as anything in possible_machines) - prepare_machine(machine) - - replace_in_name("%JOB%", LOWER_TEXT(chosen_job)) - replace_in_name("%MACHINE%", possible_machines[1].name) - return TRUE - -/// Marks a given machine as our target -/datum/traitor_objective/sabotage_machinery/proc/prepare_machine(obj/machinery/machine) - AddComponent(/datum/component/traitor_objective_register, machine, succeed_signals = list(COMSIG_QDELETING)) - -// Destroy machines which are in annoying locations, are annoying when destroyed, and aren't directly interacted with -/datum/traitor_objective/sabotage_machinery/destroy - name = "Destroy the %MACHINE%" - description = "Destroy the %MACHINE% to cause disarray and disrupt the operations of the %JOB%'s department." - - progression_reward = list(5 MINUTES, 10 MINUTES) - telecrystal_reward = list(3, 4) - - progression_minimum = 15 MINUTES - progression_maximum = 30 MINUTES - - applicable_jobs = list( - JOB_STATION_ENGINEER = /obj/machinery/telecomms/hub, - JOB_SCIENTIST = /obj/machinery/rnd/server, - ) - -// Rig machines which are in public locations to explode when interacted with -/datum/traitor_objective/sabotage_machinery/trap - name = "Sabotage the %MACHINE%" - description = "Destroy the %MACHINE% to cause disarray and disrupt the operations of the %JOB%'s department. If you can get another crew member to destroy the machine using the provided booby trap, you will be rewarded with an additional %PROGRESSION% reputation and %TC% telecrystals." - - progression_reward = list(2 MINUTES, 4 MINUTES) - telecrystal_reward = 0 // Only from completing the bonus objective - - progression_minimum = 0 MINUTES - progression_maximum = 10 MINUTES - - maximum_allowed = 2 - applicable_jobs = list( - JOB_CHIEF_ENGINEER = /obj/machinery/rnd/production/protolathe/department/engineering, - JOB_CHIEF_MEDICAL_OFFICER = /obj/machinery/rnd/production/techfab/department/medical, - JOB_HEAD_OF_PERSONNEL = /obj/machinery/rnd/production/techfab/department/service, - JOB_QUARTERMASTER = /obj/machinery/rnd/production/techfab/department/cargo, - JOB_RESEARCH_DIRECTOR = /obj/machinery/rnd/production/protolathe/department/science, - JOB_SHAFT_MINER = /obj/machinery/mineral/ore_redemption, - ) - - /// Bonus reward to grant if you booby trap successfully - var/bonus_tc = 2 - /// Bonus progression to grant if you booby trap successfully - var/bonus_progression = 5 MINUTES - /// Have we given out a traitor trap item? - var/traitor_trapper_given = FALSE - -/datum/traitor_objective/sabotage_machinery/trap/generate_objective(datum/mind/generating_for, list/possible_duplicates) - . = ..() - if (!.) - return FALSE - - replace_in_name("%TC%", bonus_tc) - replace_in_name("%PROGRESSION%", DISPLAY_PROGRESSION(bonus_progression)) - return TRUE - -/datum/traitor_objective/sabotage_machinery/trap/prepare_machine(obj/machinery/machine) - RegisterSignal(machine, COMSIG_TRAITOR_MACHINE_TRAP_TRIGGERED, PROC_REF(sabotage_success)) - return ..() - -/// Called when you successfully proc the booby trap, gives a bonus reward -/datum/traitor_objective/sabotage_machinery/trap/proc/sabotage_success(obj/machinery/machine) - progression_reward += bonus_progression - telecrystal_reward += bonus_tc - succeed_objective() - -/datum/traitor_objective/sabotage_machinery/trap/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(!traitor_trapper_given) - buttons += add_ui_button("", "Pressing this will materialize an explosive trap in your hand, which you can conceal within the target machine", "wifi", "summon_gear") - return buttons - -/datum/traitor_objective/sabotage_machinery/trap/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("summon_gear") - if(traitor_trapper_given) - return - traitor_trapper_given = TRUE - var/obj/item/traitor_machine_trapper/tool = new(user.drop_location()) - user.put_in_hands(tool) - tool.balloon_alert(user, "a booby trap materializes in your hand") - tool.target_machine_path = applicable_jobs[chosen_job] - -/// Item which you use on a machine to cause it to explode next time someone interacts with it -/obj/item/traitor_machine_trapper - name = "suspicious device" - desc = "It looks dangerous." - icon = 'icons/obj/weapons/grenade.dmi' - icon_state = "boobytrap" - - /// Light explosion range, to hurt the person using the machine - var/explosion_range = 3 - /// The type of object on which this can be planted on. - var/obj/machinery/target_machine_path - /// The time it takes to deploy the bomb. - var/deploy_time = 10 SECONDS - -/obj/item/traitor_machine_trapper/examine(mob/user) - . = ..() - if(!IS_TRAITOR(user)) - return - if(target_machine_path) - . += span_notice("This device must be placed by clicking on a [initial(target_machine_path.name)] with it. It can be removed with a screwdriver.") - . += span_notice("Remember, you may leave behind fingerprints on the device. Wear gloves when handling it to be safe!") - -/obj/item/traitor_machine_trapper/pre_attack(atom/target, mob/living/user, params) - . = ..() - if (. || !istype(target, target_machine_path)) - return - balloon_alert(user, "planting device...") - if(!do_after(user, delay = deploy_time, target = src, interaction_key = DOAFTER_SOURCE_PLANTING_DEVICE, hidden = TRUE)) - return TRUE - target.AddComponent(\ - /datum/component/interaction_booby_trap,\ - additional_triggers = list(COMSIG_ORM_COLLECTED_ORE),\ - on_triggered_callback = CALLBACK(src, PROC_REF(on_triggered)),\ - on_defused_callback = CALLBACK(src, PROC_REF(on_defused)),\ - ) - RegisterSignal(target, COMSIG_QDELETING, GLOBAL_PROC_REF(qdel), src) - moveToNullspace() - return TRUE - -/// Called when applied trap is triggered, mark success -/obj/item/traitor_machine_trapper/proc/on_triggered(atom/machine) - SEND_SIGNAL(machine, COMSIG_TRAITOR_MACHINE_TRAP_TRIGGERED) - qdel(src) - -/// Called when applied trap has been defused, retrieve this item from nullspace -/obj/item/traitor_machine_trapper/proc/on_defused(atom/machine, mob/defuser, obj/item/tool) - UnregisterSignal(machine, COMSIG_QDELETING) - playsound(machine, 'sound/effects/structure_stress/pop3.ogg', 100, vary = TRUE) - forceMove(get_turf(machine)) - visible_message(span_warning("A [src] falls out from the [machine]!")) - -/// Datum which manages references to things we are instructed to destroy -/datum/objective_target_machine_handler - /// Existing instances of machines organised by typepath - var/list/machine_instances_by_path = list() - -/datum/objective_target_machine_handler/New() - . = ..() - RegisterSignal(SSdcs, COMSIG_GLOB_NEW_MACHINE, PROC_REF(on_machine_created)) - RegisterSignal(SSatoms, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(finalise_valid_targets)) - -/// Adds a newly created machine to our list of machines, if we need it -/datum/objective_target_machine_handler/proc/on_machine_created(datum/source, obj/machinery/new_machine) - SIGNAL_HANDLER - new_machine.add_as_sabotage_target() - -/// Confirm that everything added to the list is a valid target, then prevent new targets from being added -/datum/objective_target_machine_handler/proc/finalise_valid_targets() - SIGNAL_HANDLER - for (var/machine_type in machine_instances_by_path) - for (var/obj/machinery/machine as anything in machine_instances_by_path[machine_type]) - var/turf/place = get_turf(machine) - if(!place || !is_station_level(place.z)) - machine_instances_by_path[machine_type] -= machine - continue - RegisterSignal(machine, COMSIG_QDELETING, PROC_REF(machine_destroyed)) - UnregisterSignal(SSdcs, COMSIG_GLOB_NEW_MACHINE) - -/datum/objective_target_machine_handler/proc/machine_destroyed(atom/machine) - SIGNAL_HANDLER - // Sadly can't do a direct typepath association because of some map helper subtypes - for (var/machine_type in machine_instances_by_path) - machine_instances_by_path[machine_type] -= machine - -// Mark valid machines as targets, add a new entry here if you add a new potential target - -/obj/machinery/telecomms/hub/add_as_sabotage_target() - return add_sabotage_machine(src, /obj/machinery/telecomms/hub) // Not always our specific type because of map helper subtypes - -/obj/machinery/rnd/server/add_as_sabotage_target() - return add_sabotage_machine(src, type) - -/obj/machinery/rnd/production/protolathe/department/add_as_sabotage_target() - return add_sabotage_machine(src, type) - -/obj/machinery/rnd/production/techfab/department/add_as_sabotage_target() - return add_sabotage_machine(src, type) - -/obj/machinery/mineral/ore_redemption/add_as_sabotage_target() - return add_sabotage_machine(src, type) diff --git a/code/modules/antagonists/traitor/objectives/steal.dm b/code/modules/antagonists/traitor/objectives/steal.dm deleted file mode 100644 index 69a665bc7c12..000000000000 --- a/code/modules/antagonists/traitor/objectives/steal.dm +++ /dev/null @@ -1,318 +0,0 @@ -/datum/traitor_objective_category/steal_item - name = "Steal Item" - objectives = list( - list( - /datum/traitor_objective/steal_item/low_risk = 1, - /datum/traitor_objective/destroy_item/low_risk = 1, - ) = 1, - /datum/traitor_objective/steal_item/somewhat_risky = 1, - list( - /datum/traitor_objective/destroy_item/very_risky = 1, - /datum/traitor_objective/steal_item/very_risky = 1, - ) = 1, - /datum/traitor_objective/steal_item/most_risky = 1 - ) - -GLOBAL_DATUM_INIT(steal_item_handler, /datum/objective_item_handler, new()) - -/datum/objective_item_handler - var/list/list/objectives_by_path - var/generated_items = FALSE - -/datum/objective_item_handler/New() - . = ..() - objectives_by_path = list() - for(var/datum/objective_item/item as anything in subtypesof(/datum/objective_item)) - objectives_by_path[initial(item.targetitem)] = list() - RegisterSignal(SSatoms, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(save_items)) - RegisterSignal(SSdcs, COMSIG_GLOB_NEW_ITEM, PROC_REF(new_item_created)) - -/datum/objective_item_handler/proc/new_item_created(datum/source, obj/item/item) - SIGNAL_HANDLER - if(!generated_items) - item.add_stealing_item_objective() - return - var/typepath = item.add_stealing_item_objective() - if(typepath != null) - register_item(item, typepath) - -/// Registers all items that are potentially stealable and removes ones that aren't. -/// We still need to do things this way because on mapload, items may not be on the station until everything has finished loading. -/datum/objective_item_handler/proc/save_items() - SIGNAL_HANDLER - for(var/obj/item/typepath as anything in objectives_by_path) - var/list/obj_by_path_cache = objectives_by_path[typepath].Copy() - for(var/obj/item/object as anything in obj_by_path_cache) - register_item(object, typepath) - generated_items = TRUE - -/datum/objective_item_handler/proc/register_item(atom/object, typepath) - var/turf/place = get_turf(object) - if(!place || !is_station_level(place.z)) - objectives_by_path[typepath] -= object - return - RegisterSignal(object, COMSIG_QDELETING, PROC_REF(remove_item)) - -/datum/objective_item_handler/proc/remove_item(atom/source) - SIGNAL_HANDLER - for(var/typepath in objectives_by_path) - objectives_by_path[typepath] -= source - -/datum/traitor_objective/steal_item - name = "Steal %ITEM% and place a schematics scanner on it." - description = "Use the button below to materialize the schematic scanner within your hand, where you'll then be able to place it on the item. Additionally, you can keep it near you and let it scan for %TIME% minutes, and you will be rewarded with %PROGRESSION% reputation and %TC% telecrystals." - - progression_minimum = 20 MINUTES - - var/list/possible_items = list() - /// The current target item that we are stealing. - var/datum/objective_item/steal/target_item - /// A list of 2 elements, which contain the range that the time will be in. Represented in minutes. - var/hold_time_required = list(5, 15) - /// The current time fulfilled around the item - var/time_fulfilled = 0 - /// The maximum distance between the bug and the objective taker for time to count as fulfilled - var/max_distance = 4 - /// The bug that will be put onto the item - var/obj/item/traitor_bug/bug - /// Any special equipment that may be needed - var/list/special_equipment - /// Telecrystal reward increase per unit of time. - var/minutes_per_telecrystal = 3 - - /// Extra TC given for holding the item for the required duration of time. - var/extra_tc = 0 - /// Extra progression given for holding the item for the required duration of time. - var/extra_progression = 0 - - abstract_type = /datum/traitor_objective/steal_item - -/datum/traitor_objective/steal_item/low_risk - progression_minimum = 10 MINUTES - progression_maximum = 35 MINUTES - progression_reward = list(5 MINUTES, 10 MINUTES) - telecrystal_reward = 0 - minutes_per_telecrystal = 6 - - possible_items = list( - /datum/objective_item/steal/traitor/cargo_budget, - /datum/objective_item/steal/traitor/clown_shoes, - /datum/objective_item/steal/traitor/lawyers_badge, - /datum/objective_item/steal/traitor/chef_moustache, - /datum/objective_item/steal/traitor/pka, - ) - -/datum/traitor_objective/steal_item/somewhat_risky - progression_minimum = 20 MINUTES - progression_maximum = 50 MINUTES - progression_reward = 10 MINUTES - telecrystal_reward = 2 - - possible_items = list( - /datum/objective_item/steal/traitor/chief_engineer_belt - ) - -/datum/traitor_objective/steal_item/very_risky - progression_minimum = 30 MINUTES - progression_reward = 15 MINUTES - telecrystal_reward = 3 - - possible_items = list( - /datum/objective_item/steal/traitor/det_revolver, - ) - -/datum/traitor_objective/steal_item/most_risky - progression_minimum = 50 MINUTES - progression_reward = 20 MINUTES - telecrystal_reward = 5 - - possible_items = list( - /datum/objective_item/steal/traitor/captain_modsuit, - /datum/objective_item/steal/traitor/captain_spare, - ) - -/datum/traitor_objective/steal_item/most_risky/generate_objective(datum/mind/generating_for, list/possible_duplicates) - if(!handler.get_completion_count(/datum/traitor_objective/steal_item/very_risky)) - return FALSE - return ..() - -/datum/traitor_objective/steal_item/generate_objective(datum/mind/generating_for, list/possible_duplicates) - for(var/datum/traitor_objective/steal_item/objective as anything in possible_duplicates) - possible_items -= objective.target_item.type - while(length(possible_items)) - var/datum/objective_item/steal/target = pick_n_take(possible_items) - target = new target() - if(!target.valid_objective_for(list(generating_for), require_owner = TRUE)) - qdel(target) - continue - target_item = target - break - if(!target_item) - return FALSE - if(length(target_item.special_equipment)) - special_equipment = target_item.special_equipment - hold_time_required = rand(hold_time_required[1], hold_time_required[2]) - extra_progression += hold_time_required * (1 MINUTES) - extra_tc += round(hold_time_required / max(minutes_per_telecrystal, 0.1)) - replace_in_name("%ITEM%", target_item.name) - replace_in_name("%TIME%", hold_time_required) - replace_in_name("%TC%", extra_tc) - replace_in_name("%PROGRESSION%", DISPLAY_PROGRESSION(extra_progression)) - return TRUE - -/datum/traitor_objective/steal_item/ungenerate_objective() - STOP_PROCESSING(SSprocessing, src) - if(bug) - UnregisterSignal(bug, list(COMSIG_TRAITOR_BUG_PLANTED_OBJECT, COMSIG_TRAITOR_BUG_PRE_PLANTED_OBJECT)) - bug = null - -/datum/traitor_objective/steal_item/generate_ui_buttons(mob/user) - var/list/buttons = list() - if(special_equipment) - buttons += add_ui_button("", "Pressing this will summon any extra special equipment you may need for the mission.", "tools", "summon_gear") - if(!bug) - buttons += add_ui_button("", "Pressing this will materialize a scanner in your hand, which you can place on the target item", "wifi", "summon_bug") - else if(bug.planted_on) - buttons += add_ui_button("[DisplayTimeText(time_fulfilled)]", "This tells you how much time you have spent around the target item after the scanner has been planted.", "clock", "none") - buttons += add_ui_button("Skip Time", "Pressing this will succeed the mission. You will not get the extra TC and progression.", "forward", "cash_out") - return buttons - -/datum/traitor_objective/steal_item/ui_perform_action(mob/living/user, action) - . = ..() - switch(action) - if("summon_bug") - if(bug) - return - bug = new(user.drop_location()) - user.put_in_hands(bug) - bug.balloon_alert(user, "the scanner materializes in your hand") - bug.target_object_type = target_item.targetitem - AddComponent(/datum/component/traitor_objective_register, bug, \ - fail_signals = list(COMSIG_QDELETING), \ - penalty = telecrystal_penalty) - RegisterSignal(bug, COMSIG_TRAITOR_BUG_PLANTED_OBJECT, PROC_REF(on_bug_planted)) - RegisterSignal(bug, COMSIG_TRAITOR_BUG_PRE_PLANTED_OBJECT, PROC_REF(handle_special_case)) - if("summon_gear") - if(!special_equipment) - return - for(var/item in special_equipment) - var/obj/item/new_item = new item(user.drop_location()) - user.put_in_hands(new_item) - user.balloon_alert(user, "the equipment materializes in your hand") - special_equipment = null - if("cash_out") - if(!bug.planted_on) - return - succeed_objective() - -/datum/traitor_objective/steal_item/process(seconds_per_tick) - var/mob/owner = handler.owner?.current - if(objective_state != OBJECTIVE_STATE_ACTIVE || !bug.planted_on) - return PROCESS_KILL - if(!owner) - fail_objective() - return PROCESS_KILL - if(get_dist(get_turf(owner), get_turf(bug)) > max_distance) - return - time_fulfilled += seconds_per_tick * (1 SECONDS) - if(time_fulfilled >= hold_time_required * (1 MINUTES)) - progression_reward += extra_progression - telecrystal_reward += extra_tc - succeed_objective() - return PROCESS_KILL - handler.on_update() - -/datum/traitor_objective/steal_item/proc/handle_special_case(obj/item/source, obj/item/target) - SIGNAL_HANDLER - if(istype(target, target_item.targetitem)) - if(!target_item.check_special_completion(target)) - return COMPONENT_FORCE_FAIL_PLACEMENT - return - - var/found = FALSE - for(var/typepath in target_item.valid_containers) - if(istype(target, typepath)) - found = TRUE - break - - if(!found) - return - - var/found_item = locate(target_item.targetitem) in target - if(!found_item || !target_item.check_special_completion(found_item)) - return COMPONENT_FORCE_FAIL_PLACEMENT - return COMPONENT_FORCE_PLACEMENT - -/datum/traitor_objective/steal_item/proc/on_bug_planted(obj/item/source, obj/item/location) - SIGNAL_HANDLER - if(objective_state == OBJECTIVE_STATE_ACTIVE) - START_PROCESSING(SSprocessing, src) - -/obj/item/traitor_bug - name = "suspicious device" - desc = "It looks dangerous." - item_flags = NOBLUDGEON - - icon = 'icons/obj/antags/syndicate_tools.dmi' - icon_state = "bug" - - /// The object on which this bug can be planted on. Has to be a type. - var/obj/target_object_type - /// The object this bug is currently planted on. - var/obj/planted_on - /// The time it takes to place this bug. - var/deploy_time = 10 SECONDS - -/obj/item/traitor_bug/Initialize(mapload) - . = ..() - ADD_TRAIT(src, TRAIT_EXAMINE_SKIP, INNATE_TRAIT) - -/obj/item/traitor_bug/examine(mob/user) - . = ..() - if(planted_on) - return - - if(IS_TRAITOR(user)) - if(target_object_type) - . += span_notice("This device must be placed by clicking on the [initial(target_object_type.name)] with it.") - . += span_notice("Remember, you may leave behind fingerprints or fibers on the device. Use soap or similar to scrub it clean to be safe!") - -/obj/item/traitor_bug/interact_with_atom(atom/movable/target, mob/living/user, list/modifiers) - if(!target_object_type || !ismovable(target)) - return NONE - if(SHOULD_SKIP_INTERACTION(target, src, user)) - return NONE - var/result = SEND_SIGNAL(src, COMSIG_TRAITOR_BUG_PRE_PLANTED_OBJECT, target) - if(!(result & COMPONENT_FORCE_PLACEMENT)) - if(result & COMPONENT_FORCE_FAIL_PLACEMENT || !istype(target, target_object_type)) - balloon_alert(user, "you can't attach this onto here!") - return ITEM_INTERACT_BLOCKING - if(!do_after(user, deploy_time, src, hidden = TRUE)) - return ITEM_INTERACT_BLOCKING - if(planted_on) - return ITEM_INTERACT_BLOCKING - forceMove(target) - target.vis_contents += src - vis_flags |= VIS_INHERIT_PLANE - planted_on = target - RegisterSignal(planted_on, COMSIG_QDELETING, PROC_REF(handle_planted_on_deletion)) - SEND_SIGNAL(src, COMSIG_TRAITOR_BUG_PLANTED_OBJECT, target) - return ITEM_INTERACT_SUCCESS - -/obj/item/traitor_bug/proc/handle_planted_on_deletion() - planted_on = null - -/obj/item/traitor_bug/Destroy() - if(planted_on) - vis_flags &= ~VIS_INHERIT_PLANE - planted_on.vis_contents -= src - return ..() - -/obj/item/traitor_bug/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) - . = ..() - if(planted_on) - vis_flags &= ~VIS_INHERIT_PLANE - planted_on.vis_contents -= src - anchored = FALSE - UnregisterSignal(planted_on, COMSIG_QDELETING) - planted_on = null diff --git a/code/modules/antagonists/traitor/traitor_objective.dm b/code/modules/antagonists/traitor/traitor_objective.dm deleted file mode 100644 index d60820c3fcee..000000000000 --- a/code/modules/antagonists/traitor/traitor_objective.dm +++ /dev/null @@ -1,264 +0,0 @@ -/// A traitor objective. Traitor objectives should not be deleted after they have been created and established, only failed. -/// If a traitor objective needs to be removed from the failed/completed objective list of their handler, then you are doing something wrong -/// and you should reconsider. When an objective is failed/completed, that is final and the only way you can change that is by refactoring the code. -/datum/traitor_objective - /// The name of the traitor objective - var/name = "traitor objective" - /// The description of the traitor objective - var/description = "this is a traitor objective" - /// The uplink handler holder to give the progression and telecrystals to. - var/datum/uplink_handler/handler - /// The minimum required progression points for this objective - var/progression_minimum = null - /// The maximum progression before this objective cannot appear anymore - var/progression_maximum = INFINITY - /// The progression that is rewarded from completing this traitor objective. Can either be a list of list(min, max) or a direct value - var/progression_reward = 0 MINUTES - /// The telecrystals that are rewarded from completing this traitor objective. Can either be a list of list(min,max) or a direct value - var/telecrystal_reward = 0 - /// TC penalty for failing an objective or cancelling it - var/telecrystal_penalty = 1 - /// The time at which this objective was first created - var/time_of_creation = 0 - /// The time at which this objective was completed - var/time_of_completion = 0 - /// The current state of this objective - var/objective_state = OBJECTIVE_STATE_INACTIVE - /// Whether this objective was forced upon by an admin. Won't get autocleared by the traitor subsystem if progression surpasses an amount - var/forced = FALSE - /// Whether this objective was skipped by going from an inactive state to a failed state. - var/skipped = FALSE - - /// Determines how influential global progression will affect this objective. Set to 0 to disable. - var/global_progression_influence_intensity = 0.1 - /// Determines how great the deviance has to be before progression starts to get reduced. - var/global_progression_deviance_required = 1 - /// Determines the minimum and maximum progression this objective can be worth as a result of being influenced by global progression - /// Should only be smaller than or equal to 1 - var/global_progression_limit_coeff = 0.6 - /// The deviance coefficient used to determine the randomness of the progression rewards. - var/progression_cost_coeff_deviance = 0.05 - /// This gets added onto the coeff when calculating the updated progression cost. Used for variability and a slight bit of randomness - var/progression_cost_coeff = 0 - /// The percentage that this objective has been increased or decreased by as a result of progression. Used by the UI - var/original_progression = 0 - /// Abstract type that won't be included as a possible objective - var/abstract_type = /datum/traitor_objective - /// The duplicate type that will be used to check for duplicates. - /// If undefined, this will either take from the abstract type or the type of the objective itself - var/duplicate_type = null - /// Used only in unit testing. Can be used to explicitly skip the progression_reward and telecrystal_reward check for non-abstract objectives. - /// Useful for final objectives as they don't need a reward. - var/needs_reward = TRUE - -/// Returns a list of variables that can be changed by config, allows for balance through configuration. -/// It is not recommended to finetweak any values of objectives on your server. -/datum/traitor_objective/proc/supported_configuration_changes() - return list( - NAMEOF(src, global_progression_influence_intensity), - NAMEOF(src, global_progression_deviance_required), - NAMEOF(src, global_progression_limit_coeff) - ) - -/// Replaces a word in the name of the proc. Also does it for the description -/datum/traitor_objective/proc/replace_in_name(replace, word) - name = replacetext(name, replace, word) - description = replacetext(description, replace, word) - -/datum/traitor_objective/New(datum/uplink_handler/handler) - . = ..() - src.handler = handler - src.time_of_creation = world.time - apply_configuration() - if(SStraitor.generate_objectives) - if(islist(telecrystal_reward)) - telecrystal_reward = rand(telecrystal_reward[1], telecrystal_reward[2]) - if(islist(progression_reward)) - progression_reward = rand(progression_reward[1], progression_reward[2]) - else - if(!islist(telecrystal_reward)) - telecrystal_reward = list(telecrystal_reward, telecrystal_reward) - if(!islist(progression_reward)) - progression_reward = list(progression_reward, progression_reward) - progression_cost_coeff = (rand()*2 - 1) * progression_cost_coeff_deviance - -/datum/traitor_objective/proc/apply_configuration() - if(!length(SStraitor.configuration_data)) - return - var/datum/traitor_objective/current_type = type - var/list/types = list() - while(current_type != /datum/traitor_objective) - types += current_type - current_type = type2parent(current_type) - types += /datum/traitor_objective - // Reverse the list direction - reverse_range(types) - var/list/supported_configurations = supported_configuration_changes() - for(var/typepath in types) - if(!(typepath in SStraitor.configuration_data)) - continue - var/list/changes = SStraitor.configuration_data[typepath] - for(var/variable in changes) - if(!(variable in supported_configurations)) - continue - vars[variable] = changes[variable] - - -/// Updates the progression reward, scaling it depending on their current progression compared against the global progression -/datum/traitor_objective/proc/update_progression_reward() - if(!SStraitor.generate_objectives) - return - progression_reward = original_progression - if(global_progression_influence_intensity <= 0) - return - var/minimum_progression = progression_reward * global_progression_limit_coeff - var/maximum_progression = progression_reward * (2-global_progression_limit_coeff) - var/deviance = (SStraitor.current_global_progression - handler.progression_points) / SStraitor.progression_scaling_deviance - if(abs(deviance) < global_progression_deviance_required) - return - if(abs(deviance) == deviance) // If it is positive - deviance = deviance - global_progression_deviance_required - else - deviance = deviance + global_progression_deviance_required - var/coeff = NUM_E ** (global_progression_influence_intensity * abs(deviance)) - 1 - if(abs(deviance) != deviance) - coeff *= -1 - - // This has less of an effect as the coeff gets nearer to -1. Is linear - coeff += progression_cost_coeff * min(max(1 - abs(coeff), 1), 0) - - - progression_reward = clamp( - progression_reward + progression_reward * coeff, - minimum_progression, - maximum_progression - ) - -/datum/traitor_objective/Destroy(force) - handler = null - return ..() - -/// Called whenever the objective is about to be generated. Bypassed by forcefully adding objectives. -/// Returning false or true will do the same as the generate_objective proc. -/datum/traitor_objective/proc/can_generate_objective(datum/mind/generating_for, list/possible_duplicates) - return TRUE - -/// Called when the objective should be generated. Should return if the objective has been successfully generated. -/// If false is returned, the objective will be removed as a potential objective for the traitor it is being generated for. -/// This is only temporary, it will run the proc again when objectives are generated for the traitor again. -/datum/traitor_objective/proc/generate_objective(datum/mind/generating_for, list/possible_duplicates) - return FALSE - -/// Used to clean up signals and stop listening to states. -/datum/traitor_objective/proc/ungenerate_objective() - return - -/datum/traitor_objective/proc/get_log_data() - return list( - "type" = type, - "owner" = handler.owner.key, - "name" = name, - "description" = description, - "telecrystal_reward" = telecrystal_reward, - "progression_reward" = progression_reward, - "original_progression" = original_progression, - "objective_state" = objective_state, - "forced" = forced, - "time_of_creation" = time_of_creation, - ) - -/// Converts the type into a useful debug string to be used for logging and debug display. -/datum/traitor_objective/proc/to_debug_string() - return "[type] (Name: [name], TC: [telecrystal_reward], Progression: [progression_reward], Time of creation: [time_of_creation])" - -/datum/traitor_objective/proc/save_objective() - SSblackbox.record_feedback("associative", "traitor_objective", 1, get_log_data()) - -/// Used to handle cleaning up the objective. -/datum/traitor_objective/proc/handle_cleanup() - time_of_completion = world.time - ungenerate_objective() - if(objective_state == OBJECTIVE_STATE_INACTIVE) - skipped = TRUE - handler.complete_objective(src) // Remove this objective immediately, no reason to keep it around. It isn't even active - -/// Used to fail objectives. Players can clear completed objectives in the UI -/datum/traitor_objective/proc/fail_objective(penalty_cost = 0, trigger_update = TRUE) - // Don't let players succeed already succeeded/failed objectives - if(objective_state != OBJECTIVE_STATE_INACTIVE && objective_state != OBJECTIVE_STATE_ACTIVE) - return - SEND_SIGNAL(src, COMSIG_TRAITOR_OBJECTIVE_FAILED) - handle_cleanup() - log_traitor("[key_name(handler.owner)] [objective_state == OBJECTIVE_STATE_INACTIVE? "missed" : "failed"] [to_debug_string()]") - if(penalty_cost) - handler.telecrystals -= penalty_cost - objective_state = OBJECTIVE_STATE_FAILED - else - objective_state = OBJECTIVE_STATE_INVALID - save_objective() - if(trigger_update) - handler.on_update() // Trigger an update to the UI - -/// Used to succeed objectives. Allows the player to cash it out in the UI. -/datum/traitor_objective/proc/succeed_objective() - // Don't let players succeed already succeeded/failed objectives - if(objective_state != OBJECTIVE_STATE_INACTIVE && objective_state != OBJECTIVE_STATE_ACTIVE) - return - SEND_SIGNAL(src, COMSIG_TRAITOR_OBJECTIVE_COMPLETED) - SEND_GLOBAL_SIGNAL(COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED, src) - handle_cleanup() - log_traitor("[key_name(handler.owner)] [objective_state == OBJECTIVE_STATE_INACTIVE? "missed" : "completed"] [to_debug_string()]") - objective_state = OBJECTIVE_STATE_COMPLETED - save_objective() - handler.on_update() // Trigger an update to the UI - -/// Called by player input, do not call directly. Validates whether the objective is finished and pays out the handler if it is. -/datum/traitor_objective/proc/finish_objective(mob/user) - switch(objective_state) - if(OBJECTIVE_STATE_FAILED, OBJECTIVE_STATE_INVALID) - user.playsound_local(get_turf(user), 'sound/traitor/objective_failed.ogg', vol = 100, vary = FALSE, channel = CHANNEL_TRAITOR) - return TRUE - if(OBJECTIVE_STATE_COMPLETED) - user.playsound_local(get_turf(user), 'sound/traitor/objective_success.ogg', vol = 100, vary = FALSE, channel = CHANNEL_TRAITOR) - completion_payout() - return TRUE - return FALSE - -/// Called when rewards should be given to the user. -/datum/traitor_objective/proc/completion_payout() - handler.progression_points += progression_reward - handler.telecrystals += telecrystal_reward - -/// Used for sending data to the uplink UI -/datum/traitor_objective/proc/uplink_ui_data(mob/user) - return list( - "name" = name, - "description" = description, - "progression_minimum" = progression_minimum, - "progression_reward" = progression_reward, - "telecrystal_reward" = telecrystal_reward, - "ui_buttons" = generate_ui_buttons(user), - "objective_state" = objective_state, - "original_progression" = original_progression, - "telecrystal_penalty" = telecrystal_penalty, - ) - -/datum/traitor_objective/proc/on_objective_taken(mob/user) - SStraitor.on_objective_taken(src) - log_traitor("[key_name(handler.owner)] has taken an objective: [to_debug_string()]") - -/// Used for generating the UI buttons for the UI. Use ui_perform_action to respond to clicks. -/datum/traitor_objective/proc/generate_ui_buttons(mob/user) - return - -/datum/traitor_objective/proc/add_ui_button(name, tooltip, icon, action) - return list(list( - "name" = name, - "tooltip" = tooltip, - "icon" = icon, - "action" = action, - )) - -/// Return TRUE to trigger a UI update -/datum/traitor_objective/proc/ui_perform_action(mob/user, action) - return TRUE diff --git a/code/modules/antagonists/traitor/uplink_handler.dm b/code/modules/antagonists/traitor/uplink_handler.dm index 78e6e2da41f7..84e186bc116f 100644 --- a/code/modules/antagonists/traitor/uplink_handler.dm +++ b/code/modules/antagonists/traitor/uplink_handler.dm @@ -20,24 +20,6 @@ var/list/item_stock = list(UPLINK_SHARED_STOCK_KITS = 1 , UPLINK_SHARED_STOCK_SURPLUS = 1) /// Extra stuff that can be purchased by an uplink, regardless of flag. var/list/extra_purchasable = list() - /// Whether this uplink handler has objectives. - var/has_objectives = TRUE - /// Whether this uplink handler can TAKE objectives. - var/can_take_objectives = TRUE - /// The maximum number of objectives that can be taken - var/maximum_active_objectives = 2 - /// The maximum number of potential objectives that can exist. - var/maximum_potential_objectives = 6 - /// Current objectives taken - var/list/active_objectives = list() - /// Potential objectives that can be taken - var/list/potential_objectives = list() - /// Objectives that have been completed. - var/list/completed_objectives = list() - /// All objectives assigned by type to handle any duplicates - var/list/potential_duplicate_objectives = list() - /// Text of the final objective, once assigned. Used for uplink data and traitor greentext. Empty string means not yet reached. - var/final_objective = "" /// Objectives that must be completed for traitor greentext. Set by the traitor datum. var/list/primary_objectives /// The role that this uplink handler is associated to. @@ -55,10 +37,6 @@ ///Reference to a contractor hub that the infiltrator can run, if they purchase it. var/datum/contractor_hub/contractor_hub -/datum/uplink_handler/New() - . = ..() - maximum_potential_objectives = CONFIG_GET(number/maximum_potential_objectives) - /datum/uplink_handler/Destroy(force) can_replace_objectives = null replace_objectives = null @@ -73,6 +51,10 @@ /datum/uplink_handler/proc/not_enough_reputation(datum/uplink_item/to_purchase) return has_progression && progression_points < to_purchase.progression_minimum +/// Checks if there are enough joined players to purchase an item +/datum/uplink_handler/proc/not_enough_population(datum/uplink_item/to_purchase) + return length(GLOB.joined_player_list) < to_purchase.population_minimum + /// Checks for uplink flags as well as items restricted to roles and species /datum/uplink_handler/proc/check_if_restricted(datum/uplink_item/to_purchase) if(!to_purchase.can_be_bought(src)) @@ -102,9 +84,15 @@ if(!check_if_restricted(to_purchase)) return FALSE + if(not_enough_reputation(to_purchase) || not_enough_population(to_purchase)) + return FALSE + + if(telecrystals < to_purchase.cost) + return FALSE + var/current_stock = item_stock[to_purchase.stock_key] var/stock = current_stock != null ? current_stock : INFINITY - if(telecrystals < to_purchase.cost || stock <= 0 || not_enough_reputation(to_purchase)) + if(stock <= 0) return FALSE return TRUE @@ -140,132 +128,11 @@ on_update() return TRUE +///Helper to add telecrystals to the uplink handler, calling set_telecrystals. +/datum/uplink_handler/proc/add_telecrystals(amount) + set_telecrystals(telecrystals + amount) -/// Generates objectives for this uplink handler -/datum/uplink_handler/proc/generate_objectives() - var/potential_objectives_left = maximum_potential_objectives - (length(potential_objectives) + length(active_objectives)) - var/list/objectives = SStraitor.category_handler.get_possible_objectives(progression_points) - if(!length(objectives)) - return - while(length(objectives) && potential_objectives_left > 0) - var/objective_typepath = pick_weight(objectives) - var/list/target_list = objectives - while(islist(objective_typepath)) - if(!length(objective_typepath)) - // Need to wrap this in a list or else it list unrolls and the list doesn't actually get removed. - // Thank you byond, very cool! - target_list -= list(objective_typepath) - break - target_list = objective_typepath - objective_typepath = pick_weight(objective_typepath) - if(islist(objective_typepath) || !objective_typepath) - continue - if(!try_add_objective(objective_typepath)) - target_list -= objective_typepath - continue - potential_objectives_left-- +///Sets how many telecrystals the uplink handler has, then updates the UI for any players watching. +/datum/uplink_handler/proc/set_telecrystals(amount) + telecrystals = amount on_update() - -/datum/uplink_handler/proc/try_add_objective(datum/traitor_objective/objective_typepath, force = FALSE) - var/datum/traitor_objective/objective = new objective_typepath(src) - var/duplicate_typepath = objective.duplicate_type - if(!duplicate_typepath) - if(objective.abstract_type != /datum/traitor_objective) - duplicate_typepath = objective.abstract_type - else - duplicate_typepath = objective_typepath - - if(!force && !objective.can_generate_objective(owner, potential_duplicate_objectives[duplicate_typepath])) - qdel(objective) - return - - var/should_abort = SEND_SIGNAL(objective, COMSIG_TRAITOR_OBJECTIVE_PRE_GENERATE, owner, potential_duplicate_objectives[duplicate_typepath]) & COMPONENT_TRAITOR_OBJECTIVE_ABORT_GENERATION - if(should_abort || !objective.generate_objective(owner, potential_duplicate_objectives[duplicate_typepath])) - qdel(objective) - return - if(!handle_duplicate(objective)) - qdel(objective) - return - objective.forced = force - log_traitor("[key_name(owner)] has received a potential objective: [objective.to_debug_string()] | Forced: [force]") - objective.original_progression = objective.progression_reward - objective.update_progression_reward() - potential_objectives += objective - SStraitor.add_objective_to_list(objective, SStraitor.all_objectives_by_type) - return objective - -/datum/uplink_handler/proc/handle_duplicate(datum/traitor_objective/potential_duplicate) - if(!istype(potential_duplicate)) - return FALSE - - var/datum/traitor_objective/current_type = potential_duplicate.type - var/list/added_types = list() - while(current_type != /datum/traitor_objective) - if(!potential_duplicate_objectives[current_type]) - potential_duplicate_objectives[current_type] = list(potential_duplicate) - else - potential_duplicate_objectives[current_type] += potential_duplicate - - added_types += current_type - current_type = type2parent(current_type) - return TRUE - -/datum/uplink_handler/proc/get_completion_count(datum/traitor_objective/type) - var/amount_completed = 0 - for(var/datum/traitor_objective/objective as anything in potential_duplicate_objectives[type]) - if(objective.objective_state == OBJECTIVE_STATE_COMPLETED) - amount_completed += 1 - return amount_completed - -/datum/uplink_handler/proc/get_completion_progression(datum/traitor_objective/type) - var/total_progression = 0 - for(var/datum/traitor_objective/objective as anything in completed_objectives) - if(objective.objective_state == OBJECTIVE_STATE_COMPLETED) - total_progression += objective.progression_reward - return total_progression - -/// Used to complete objectives, failed or successful. -/datum/uplink_handler/proc/complete_objective(datum/traitor_objective/to_remove) - if(to_remove in completed_objectives) - return - - potential_objectives -= to_remove - active_objectives -= to_remove - completed_objectives += to_remove - update_objectives() - generate_objectives() - -/// Updates the objectives on the uplink and deletes -/datum/uplink_handler/proc/update_objectives() - var/list/objectives_copy = potential_objectives + active_objectives - for(var/datum/traitor_objective/objective as anything in objectives_copy) - if(progression_points > objective.progression_maximum && !objective.forced && objective.objective_state != OBJECTIVE_STATE_ACTIVE) - objective.fail_objective(trigger_update = FALSE) - continue - objective.update_progression_reward() - -/datum/uplink_handler/proc/abort_objective(datum/traitor_objective/to_abort) - if(istype(to_abort, /datum/traitor_objective/ultimate)) - return - if(to_abort.objective_state != OBJECTIVE_STATE_ACTIVE) - return - to_abort.fail_objective(penalty_cost = to_abort.telecrystal_penalty) - -/datum/uplink_handler/proc/take_objective(mob/user, datum/traitor_objective/to_take) - if(!(to_take in potential_objectives)) - return - - user.playsound_local(get_turf(user), 'sound/traitor/objective_taken.ogg', vol = 100, vary = FALSE, channel = CHANNEL_TRAITOR) - to_take.on_objective_taken(user) - to_take.objective_state = OBJECTIVE_STATE_ACTIVE - potential_objectives -= to_take - active_objectives += to_take - on_update() - -/datum/uplink_handler/proc/ui_objective_act(mob/user, datum/traitor_objective/to_act_on, action) - if(!(to_act_on in active_objectives)) - return - if(to_act_on.objective_state != OBJECTIVE_STATE_ACTIVE) - return - - to_act_on.ui_perform_action(user, action) diff --git a/code/modules/antagonists/valentines/valentine.dm b/code/modules/antagonists/valentines/valentine.dm index 086c50827f5e..d528dae144b9 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/wishgranter/wishgranter.dm b/code/modules/antagonists/wishgranter/wishgranter.dm index fd18ffe5c1ed..6ecb9500854e 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 fe5f69fd9fa5..4d1ceb2790f2 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 f13b53b12edd..79fb2a1568f2 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 a951a5daf422..1633bb50a876 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 1ceb21b72eb7..a1bf16ff790e 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 c9aea4478c39..71d4c4669a01 100644 --- a/code/modules/antagonists/wizard/slaughter_antag.dm +++ b/code/modules/antagonists/wizard/slaughter_antag.dm @@ -2,7 +2,7 @@ name = "\improper Slaughter Demon" 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 2466f58947a8..1f3445fd12d0 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 @@ -123,7 +123,7 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) if(!owner.current) return if(!GLOB.wizardstart.len) - SSjob.SendToLateJoin(owner.current) + SSjob.send_to_late_join(owner.current) to_chat(owner, "HOT INSERTION, GO GO GO") owner.current.forceMove(pick(GLOB.wizardstart)) @@ -449,3 +449,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 eaebc61cd477..3dcbe0918ff8 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 @@ -87,7 +86,7 @@ explanation_text = "Escape from captivity." /datum/objective/escape_captivity/check_completion() - if(!istype(get_area(owner), SScommunications.captivity_area)) + if(!istype(get_area(owner), GLOB.communications_controller.captivity_area)) return TRUE /datum/objective/advance_hive @@ -146,7 +145,7 @@ if(!captive_alien || captive_alien.stat == DEAD) return CAPTIVE_XENO_DEAD - if(istype(get_area(captive_alien), SScommunications.captivity_area)) + if(istype(get_area(captive_alien), GLOB.communications_controller.captivity_area)) return CAPTIVE_XENO_FAIL return CAPTIVE_XENO_PASS @@ -155,13 +154,12 @@ /mob/living/carbon/alien/mind_initialize() ..() if(!mind.has_antag_datum(/datum/antagonist/xeno)) - if(SScommunications.xenomorph_egg_delivered && istype(get_area(src), SScommunications.captivity_area)) + if(GLOB.communications_controller.xenomorph_egg_delivered && istype(get_area(src), GLOB.communications_controller.captivity_area)) mind.add_antag_datum(/datum/antagonist/xeno/captive) else mind.add_antag_datum(/datum/antagonist/xeno) - mind.set_assigned_role(SSjob.GetJobType(/datum/job/xenomorph)) - mind.special_role = ROLE_ALIEN + mind.set_assigned_role(SSjob.get_job_type(/datum/job/xenomorph)) /mob/living/carbon/alien/on_wabbajacked(mob/living/new_mob) . = ..() @@ -170,8 +168,7 @@ if(isalien(new_mob)) return mind.remove_antag_datum(/datum/antagonist/xeno) - mind.set_assigned_role(SSjob.GetJobType(/datum/job/unassigned)) - mind.special_role = null + mind.set_assigned_role(SSjob.get_job_type(/datum/job/unassigned)) #undef CAPTIVE_XENO_DEAD #undef CAPTIVE_XENO_FAIL diff --git a/code/modules/asset_cache/assets/paper.dm b/code/modules/asset_cache/assets/paper.dm index f97d25e2d5cc..ee88f8062476 100644 --- a/code/modules/asset_cache/assets/paper.dm +++ b/code/modules/asset_cache/assets/paper.dm @@ -1,5 +1,5 @@ -/datum/asset/spritesheet/simple/paper - name = "paper" +/datum/asset/spritesheet/simple/stamps + name = "stamps" assets = list( "stamp-clown" = 'icons/stamp_icons/large_stamp-clown.png', "stamp-deny" = 'icons/stamp_icons/large_stamp-deny.png', @@ -21,3 +21,9 @@ "stamp-bo" = 'maplestation_modules/icons/stamp_icons/large_stamp-bo.png', "stamp-ap" = 'maplestation_modules/icons/stamp_icons/large_stamp-ap.png', ) + +// Contains 256x128 versions of various in game company logos +/datum/asset/simple/logos + assets = list( + "nanotrasen-logo" = 'maplestation_modules/icons/nanotrasen-logo.png', + ) diff --git a/code/modules/asset_cache/assets/uplink.dm b/code/modules/asset_cache/assets/uplink.dm index 4542a638b4bf..e236c18122a0 100644 --- a/code/modules/asset_cache/assets/uplink.dm +++ b/code/modules/asset_cache/assets/uplink.dm @@ -35,6 +35,7 @@ "restricted_roles" = item.restricted_roles, "restricted_species" = item.restricted_species, "progression_minimum" = item.progression_minimum, + "population_minimum" = item.population_minimum, "cost_override_string" = item.cost_override_string, "lock_other_purchases" = item.lock_other_purchases )) diff --git a/code/modules/bitrunning/antagonists/_parent.dm b/code/modules/bitrunning/antagonists/_parent.dm index 8bd061d72a1d..5a00026f9965 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 3bf88e16dfb2..2114382d552a 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/components/avatar_connection.dm b/code/modules/bitrunning/components/avatar_connection.dm index 801f51f5b7c9..1891a4689974 100644 --- a/code/modules/bitrunning/components/avatar_connection.dm +++ b/code/modules/bitrunning/components/avatar_connection.dm @@ -69,7 +69,7 @@ avatar.fully_replace_character_name(newname = alias) update_avatar_id() - avatar.mind.set_assigned_role(SSjob.GetJobType(/datum/job/bit_avatar)) + avatar.mind.set_assigned_role(SSjob.get_job_type(/datum/job/bit_avatar)) for(var/skill_type in old_mind.known_skills) avatar.mind.set_experience(skill_type, old_mind.get_skill_exp(skill_type), silent = TRUE) diff --git a/code/modules/bitrunning/event.dm b/code/modules/bitrunning/event.dm index 370957e2ebb0..c50ef016a515 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 d912e972289a..4c44355412bf 100644 --- a/code/modules/bitrunning/server/threats.dm +++ b/code/modules/bitrunning/server/threats.dm @@ -111,8 +111,7 @@ 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.GetJobType(/datum/job/bitrunning_glitch)) + antag_mind.set_assigned_role(SSjob.get_job_type(/datum/job/bitrunning_glitch)) playsound(new_mob, 'sound/magic/ethereal_exit.ogg', 50, vary = TRUE) message_admins("[ADMIN_LOOKUPFLW(new_mob)] has been made into virtual antagonist by an event.") diff --git a/code/modules/bitrunning/spawners.dm b/code/modules/bitrunning/spawners.dm index 81d039fad137..0ad7e454e6f2 100644 --- a/code/modules/bitrunning/spawners.dm +++ b/code/modules/bitrunning/spawners.dm @@ -18,7 +18,7 @@ ..() spawned_mob.mind.add_antag_datum(/datum/antagonist/domain_ghost_actor) - spawned_mob.mind.set_assigned_role(SSjob.GetJobType(/datum/job/bitrunning_glitch)) + spawned_mob.mind.set_assigned_role(SSjob.get_job_type(/datum/job/bitrunning_glitch)) /obj/effect/mob_spawn/ghost_role/human/virtual_domain/pirate name = "Virtual Pirate Remains" diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 2cbd9f892b06..6e9990b418a0 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -174,7 +174,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) data["character_profiles"] = create_character_profiles() data["character_preview_view"] = character_preview_view.assigned_map - data["overflow_role"] = SSjob.GetJobType(SSjob.overflow_role).title + data["overflow_role"] = SSjob.get_job_type(SSjob.overflow_role).title data["window"] = current_window data["content_unlocked"] = unlock_content diff --git a/code/modules/client/preferences/middleware/antags.dm b/code/modules/client/preferences/middleware/antags.dm index 5b107d2c0d0b..2e69288adf74 100644 --- a/code/modules/client/preferences/middleware/antags.dm +++ b/code/modules/client/preferences/middleware/antags.dm @@ -1,3 +1,11 @@ +/// 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, +)) + /datum/preference_middleware/antags action_delegations = list( "set_antags" = PROC_REF(set_antags), @@ -59,14 +67,9 @@ /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))) + 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 @@ -76,18 +79,8 @@ 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] - ) - + 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 @@ -99,11 +92,50 @@ 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" @@ -113,23 +145,16 @@ var/list/antag_icons = list() /datum/asset/spritesheet/antagonists/create_spritesheets() - // Antagonists that don't have a dynamic ruleset, but do have a preference - var/static/list/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, - ) - - var/list/antagonists = non_ruleset_antagonists.Copy() + 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/middleware/jobs.dm b/code/modules/client/preferences/middleware/jobs.dm index 0471367d2899..a0c08e9382d6 100644 --- a/code/modules/client/preferences/middleware/jobs.dm +++ b/code/modules/client/preferences/middleware/jobs.dm @@ -11,7 +11,7 @@ if (level != null && level != JP_LOW && level != JP_MEDIUM && level != JP_HIGH) return FALSE - var/datum/job/job = SSjob.GetJob(job_title) + var/datum/job/job = SSjob.get_job(job_title) if (isnull(job)) return FALSE @@ -36,7 +36,7 @@ if(new_title == base_title) new_title = null // clearing, essentially - var/datum/job/job = SSjob.GetJob(base_title) + var/datum/job/job = SSjob.get_job(base_title) if(isnull(job)) return FALSE if(new_title && !(new_title in job.get_titles(TRUE))) @@ -166,7 +166,7 @@ /datum/preference/job_titles/deserialize(input, datum/preferences/preferences) var/list/input_sanitized = list() for(var/job_title in input) - var/datum/job/job = SSjob.GetJob(job_title) + var/datum/job/job = SSjob.get_job(job_title) if(isnull(job)) continue if(!(input[job_title] in job.get_titles(TRUE))) diff --git a/code/modules/client/preferences/species_features/vampire.dm b/code/modules/client/preferences/species_features/vampire.dm index bd24acb3ab15..cc54fb3391f5 100644 --- a/code/modules/client/preferences/species_features/vampire.dm +++ b/code/modules/client/preferences/species_features/vampire.dm @@ -32,7 +32,7 @@ GLOBAL_LIST_EMPTY(vampire_houses) //find and setup the house (department) this vampire is joining var/datum/job_department/vampire_house - var/datum/job/vampire_job = SSjob.GetJob(target.job) + var/datum/job/vampire_job = SSjob.get_job(target.job) if(!vampire_job) //no job or no mind LOSERS return var/list/valid_departments = (SSjob.joinable_departments.Copy()) - list(/datum/job_department/silicon, /datum/job_department/undefined) diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 0ac47d29c92d..46d0a8644e31 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -460,7 +460,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 5b31ae49849c..796475fddb49 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/chameleon/_chameleon_action.dm b/code/modules/clothing/chameleon/_chameleon_action.dm index b822f7feabb2..4befe80fbe82 100644 --- a/code/modules/clothing/chameleon/_chameleon_action.dm +++ b/code/modules/clothing/chameleon/_chameleon_action.dm @@ -207,7 +207,7 @@ if(istype(applying_from, /datum/outfit/job)) var/datum/outfit/job/job_outfit = applying_from - var/datum/job/job_datum = SSjob.GetJobType(job_outfit.jobtype) + var/datum/job/job_datum = SSjob.get_job_type(job_outfit.jobtype) apply_job_data(job_datum) update_look(using_item_type) diff --git a/code/modules/clothing/head/mind_monkey_helmet.dm b/code/modules/clothing/head/mind_monkey_helmet.dm index 2ffdc035198f..c169e7fc74d2 100644 --- a/code/modules/clothing/head/mind_monkey_helmet.dm +++ b/code/modules/clothing/head/mind_monkey_helmet.dm @@ -49,19 +49,18 @@ playsound(src, 'sound/machines/ping.ogg', 30, TRUE) RegisterSignal(magnification, COMSIG_SPECIES_LOSS, PROC_REF(make_fall_off)) polling = TRUE - var/list/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as a mind magnified monkey?", check_jobban = ROLE_MONKEY_HELMET, poll_time = 5 SECONDS, target_mob = magnification, ignore_category = POLL_IGNORE_MONKEY_HELMET, pic_source = magnification, role_name_text = "mind-magnified monkey") + var/mob/chosen_one = SSpolling.poll_ghosts_for_target(check_jobban = ROLE_MONKEY_HELMET, poll_time = 5 SECONDS, checked_target = magnification, ignore_category = POLL_IGNORE_MONKEY_HELMET, alert_pic = magnification, role_name_text = "mind-magnified monkey") polling = FALSE if(!magnification) return - if(!candidates.len) + if(isnull(chosen_one)) UnregisterSignal(magnification, COMSIG_SPECIES_LOSS) magnification = null visible_message(span_notice("[src] falls silent and drops on the floor. Maybe you should try again later?")) playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE) user.dropItemToGround(src) return - var/mob/picked = pick(candidates) - magnification.key = picked.key + magnification.key = chosen_one.key playsound(src, 'sound/machines/microwave/microwave-end.ogg', 100, FALSE) to_chat(magnification, span_notice("You're a mind magnified monkey! Protect your helmet with your life- if you lose it, your sentience goes with it!")) var/policy = get_policy(ROLE_MONKEY_HELMET) diff --git a/code/modules/clothing/outfits/event.dm b/code/modules/clothing/outfits/event.dm index fa07b3297b28..43857f5f3cb3 100644 --- a/code/modules/clothing/outfits/event.dm +++ b/code/modules/clothing/outfits/event.dm @@ -18,8 +18,7 @@ if(visualsOnly) return user.fully_replace_character_name(user.real_name, "Santa Claus") - user.mind.set_assigned_role(SSjob.GetJobType(/datum/job/santa)) - user.mind.special_role = ROLE_SANTA + user.mind.set_assigned_role(SSjob.get_job_type(/datum/job/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 e1ff22c7a127..0b1f0d319e83 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() @@ -84,9 +81,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 0dfa87ddfa33..000000000000 --- a/code/modules/events/creep_awakening.dm +++ /dev/null @@ -1,26 +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)) - continue - 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 000000000000..3c5899df0185 --- /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 6e5cfdc61a1f..f862c0be1022 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 - -/proc/gather_false_events(players_amt) + + 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() + + 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)) + var/datum/round_event/event = controller.typepath + if(!initial(event.fakeable)) continue + . += controller - var/datum/round_event/event = E.typepath - if(!initial(event.fakeable)) + for(var/datum/dynamic_ruleset/midround/midround as anything in subtypesof(/datum/dynamic_ruleset/midround)) + if(!initial(midround.false_alarm_able)) continue - . += E + . += 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 65fe4a142f5a..000000000000 --- a/code/modules/events/ghost_role/abductor.dm +++ /dev/null @@ -1,38 +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, pic_source = /obj/item/melee/baton/abductor, role_name_text = role_name) - - if(candidates.len < 2) - return NOT_ENOUGH_PLAYERS - - SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_ABDUCTOR_SHIPS) - var/mob/living/carbon/human/agent = make_body(pick_n_take(candidates)) - var/mob/living/carbon/human/scientist = make_body(pick_n_take(candidates)) - - 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 f4078e52e658..000000000000 --- 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, pic_source = /mob/living/carbon/alien/larva, role_name_text = role_name) - - if(!candidates.len) - 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.key = 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 70640512699d..000000000000 --- 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/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_BLOB, role = ROLE_BLOB, pic_source = blob_image, role_name_text = role_name) - if(!candidates.len) - return NOT_ENOUGH_PLAYERS - var/mob/dead/observer/new_blob = pick(candidates) - var/mob/camera/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 43b4ca48af57..000000000000 --- a/code/modules/events/ghost_role/changeling_event.dm +++ /dev/null @@ -1,32 +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/list/mob/dead/observer/candidate = SSpolling.poll_ghost_candidates(check_jobban = ROLE_CHANGELING, role = ROLE_CHANGELING_MIDROUND, pic_source = /obj/item/melee/arm_blade, role_name_text = role_name) - - if(!candidate.len) - return NOT_ENOUGH_PLAYERS - - spawned_mobs += generate_changeling_meteor(pick_n_take(candidate)) - - 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 1556eb045589..000000000000 --- a/code/modules/events/ghost_role/fugitive_event.dm +++ /dev/null @@ -1,151 +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, pic_source = /obj/item/card/id/advanced/prisoner) - - 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/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, - ) - addtimer(CALLBACK(src, PROC_REF(spawn_hunters), hunter_backstory), 10 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.GetJobType(/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) - -//security team gets called in after 10 minutes of prep to find the refugees -/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 [backstory]?", check_jobban = ROLE_FUGITIVE_HUNTER, pic_source = /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 - - 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!", - ) - - priority_announce("Unidentified ship detected near the station.") - -#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 c9133863f8c0..000000000000 --- a/code/modules/events/ghost_role/morph_event.dm +++ /dev/null @@ -1,38 +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/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_ALIEN, role = ROLE_ALIEN, pic_source = /mob/living/basic/morph, role_name_text = "morph") - if(!candidates.len) - return NOT_ENOUGH_PLAYERS - - var/mob/dead/selected = pick_n_take(candidates) - - var/datum/mind/player_mind = new /datum/mind(selected.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.GetJobType(/datum/job/morph)) - player_mind.special_role = ROLE_MORPH - player_mind.add_antag_datum(/datum/antagonist/morph) - SEND_SOUND(corpus_accipientis, sound('sound/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 ffb206c476dd..000000000000 --- a/code/modules/events/ghost_role/nightmare.dm +++ /dev/null @@ -1,41 +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/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_ALIEN, role = ROLE_NIGHTMARE, role_name_text = role_name) - if(!candidates.len) - return NOT_ENOUGH_PLAYERS - - var/mob/dead/selected = pick(candidates) - - var/datum/mind/player_mind = new /datum/mind(selected.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.GetJobType(/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/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 fcea52e3c023..5ea7b39f872d 100644 --- a/code/modules/events/ghost_role/operative.dm +++ b/code/modules/events/ghost_role/operative.dm @@ -6,28 +6,26 @@ 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" fakeable = FALSE /datum/round_event/ghost_role/operative/spawn_role() - var/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_OPERATIVE, role = ROLE_LONE_OPERATIVE, pic_source = /obj/machinery/nuclearbomb) - if(!candidates.len) + var/mob/chosen_one = SSpolling.poll_ghost_candidates(check_jobban = ROLE_OPERATIVE, role = ROLE_LONE_OPERATIVE, alert_pic = /obj/machinery/nuclearbomb, amount_to_pick = 1) + if(isnull(chosen_one)) return NOT_ENOUGH_PLAYERS - - var/mob/dead/selected = pick_n_take(candidates) - var/spawn_location = find_space_spawn() if(isnull(spawn_location)) return MAP_ERROR - var/mob/living/carbon/human/operative = new(spawn_location) operative.randomize_human_appearance(~RANDOMIZE_SPECIES) operative.dna.update_dna_identity() - var/datum/mind/Mind = new /datum/mind(selected.key) - Mind.set_assigned_role(SSjob.GetJobType(/datum/job/lone_operative)) - Mind.special_role = ROLE_LONE_OPERATIVE + var/datum/mind/Mind = new /datum/mind(chosen_one.key) + Mind.set_assigned_role(SSjob.get_job_type(/datum/job/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 6cdfc2c4c9e5..000000000000 --- a/code/modules/events/ghost_role/revenant_event.dm +++ /dev/null @@ -1,65 +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/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_REVENANT, role = ROLE_REVENANT, pic_source = /mob/living/basic/revenant) - if(!candidates.len) - return NOT_ENOUGH_PLAYERS - - var/mob/dead/observer/selected = pick_n_take(candidates) - - var/list/spawn_locs = list() - - for(var/mob/living/L in GLOB.dead_mob_list) //look for any dead 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(selected) - 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.key = selected.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(selected) - return SUCCESSFUL_SPAWN - -#undef REVENANT_SPAWN_THRESHOLD diff --git a/code/modules/events/ghost_role/sentience.dm b/code/modules/events/ghost_role/sentience.dm index 8ebd30ad7b3e..53062aa48f88 100644 --- a/code/modules/events/ghost_role/sentience.dm +++ b/code/modules/events/ghost_role/sentience.dm @@ -49,14 +49,14 @@ GLOBAL_LIST_INIT(high_priority_sentience, typecacheof(list( priority_announce(sentience_report,"[command_name()] Medium-Priority Update") /datum/round_event/ghost_role/sentience/spawn_role() - var/list/mob/dead/observer/candidates - candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_SENTIENCE, role = ROLE_SENTIENCE, pic_source = /obj/item/slimepotion/slime/sentience, role_name_text = role_name) + var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_SENTIENCE, role = ROLE_SENTIENCE, alert_pic = /obj/item/slimepotion/slime/sentience, role_name_text = role_name) + if(!length(candidates)) + return NOT_ENOUGH_PLAYERS // find our chosen mob to breathe life into // Mobs have to be simple animals, mindless, on station, and NOT holograms. // prioritize starter animals that people will recognise - var/list/potential = list() var/list/hi_pri = list() diff --git a/code/modules/events/ghost_role/sentient_disease.dm b/code/modules/events/ghost_role/sentient_disease.dm index 44f78d7c08cc..1e8c5832ed90 100644 --- a/code/modules/events/ghost_role/sentient_disease.dm +++ b/code/modules/events/ghost_role/sentient_disease.dm @@ -13,14 +13,19 @@ role_name = "sentient disease" /datum/round_event/ghost_role/sentient_disease/spawn_role() - var/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_ALIEN, role = ROLE_ALIEN, pic_source = /obj/structure/sign/warning/biohazard, role_name_text = role_name) - if(!candidates.len) + var/mob/chosen_one = SSpolling.poll_ghost_candidates( + poll_time = 15 SECONDS, + check_jobban = ROLE_ALIEN, + role = ROLE_ALIEN, + alert_pic = /obj/structure/sign/warning/biohazard, + role_name_text = role_name, + amount_to_pick = 1, + ) + if(isnull(chosen_one)) return NOT_ENOUGH_PLAYERS - var/mob/dead/observer/selected = pick_n_take(candidates) - var/mob/camera/disease/virus = new /mob/camera/disease(SSmapping.get_station_center()) - virus.key = selected.key + virus.key = chosen_one.key INVOKE_ASYNC(virus, TYPE_PROC_REF(/mob/camera/disease, pick_name)) message_admins("[ADMIN_LOOKUPFLW(virus)] has been made into a sentient disease by an event.") virus.log_message("was spawned as a sentient disease by an event.", LOG_GAME) 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 f4628344d2f5..000000000000 --- a/code/modules/events/ghost_role/slaughter_event.dm +++ /dev/null @@ -1,40 +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/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_ALIEN, role = ROLE_ALIEN, pic_source = /mob/living/basic/demon/slaughter, role_name_text = role_name) - if(!candidates.len) - return NOT_ENOUGH_PLAYERS - - var/mob/dead/selected = pick_n_take(candidates) - - var/datum/mind/player_mind = new /datum/mind(selected.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) - spawned.generate_antagonist_status() - - 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 0a328f6dc8d8..000000000000 --- a/code/modules/events/ghost_role/space_dragon.dm +++ /dev/null @@ -1,41 +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/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_SPACE_DRAGON, role = ROLE_SPACE_DRAGON, pic_source = /mob/living/basic/space_dragon) - if(!candidates.len) - return NOT_ENOUGH_PLAYERS - - var/mob/dead/selected = pick(candidates) - var/key = selected.key - - var/spawn_location = find_space_spawn() - if(isnull(spawn_location)) - return MAP_ERROR - - var/mob/living/basic/space_dragon/dragon = new (spawn_location) - dragon.key = key - dragon.mind.add_antag_datum(/datum/antagonist/space_dragon) - playsound(dragon, 'sound/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 ffa28e6e1c4f..000000000000 --- a/code/modules/events/ghost_role/space_ninja.dm +++ /dev/null @@ -1,49 +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/list/candidates = SSpolling.poll_ghost_candidates(check_jobban = ROLE_NINJA, role = ROLE_NINJA, pic_source = /obj/item/energy_katana) - if(!candidates.len) - return NOT_ENOUGH_PLAYERS - - var/mob/dead/selected_candidate = pick(candidates) - var/key = selected_candidate.key - - //spawn the ninja and assign the candidate - var/mob/living/carbon/human/ninja = create_space_ninja(spawn_location) - ninja.key = key - ninja.mind.add_antag_datum(/datum/antagonist/ninja) - spawned_mobs += ninja - 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)) - 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() - return new_ninja diff --git a/code/modules/events/holiday/vday.dm b/code/modules/events/holiday/vday.dm index a8c54996d18a..f3a98984a970 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() @@ -79,7 +77,7 @@ poll_time = 30 SECONDS, flash_window = FALSE, start_signed_up = TRUE, - pic_source = /obj/item/storage/fancy/heart_box, + alert_pic = /obj/item/storage/fancy/heart_box, custom_response_messages = list( POLL_RESPONSE_SIGNUP = "You have signed up for a date!", POLL_RESPONSE_ALREADY_SIGNED = "You are already signed up for a date.", diff --git a/code/modules/events/holiday/xmas.dm b/code/modules/events/holiday/xmas.dm index cad343f7debd..35ae6a7b3b97 100644 --- a/code/modules/events/holiday/xmas.dm +++ b/code/modules/events/holiday/xmas.dm @@ -78,17 +78,14 @@ description = "Spawns santa, who shall roam the station, handing out gifts." /datum/round_event/santa - var/mob/living/carbon/human/santa //who is our santa? /datum/round_event/santa/announce(fake) priority_announce("Santa is coming to town!", "Unknown Transmission") -/datum/round_event/santa/start() - var/list/candidates = SSpolling.poll_ghost_candidates("Santa is coming to town! Do you want to be Santa?", poll_time = 15 SECONDS, pic_source = /obj/item/clothing/head/costume/santa, role_name_text = "santa") - if(LAZYLEN(candidates)) - var/mob/dead/observer/C = pick(candidates) - santa = new /mob/living/carbon/human(pick(GLOB.blobstart)) - santa.key = C.key - - var/datum/antagonist/santa/A = new - santa.mind.add_antag_datum(A) +/datum/round_event/ghost_role/santa/start() + var/mob/chosen_one = SSpolling.poll_ghost_candidates("Santa is coming to town! Do you want to be [span_notice("Santa")]?", poll_time = 15 SECONDS, alert_pic = /obj/item/clothing/head/costume/santa, role_name_text = "Santa", amount_to_pick = 1) + if(isnull(chosen_one)) + return NOT_ENOUGH_PLAYERS + var/mob/living/carbon/human/santa = new(pick(GLOB.blobstart)) + santa.key = chosen_one.key + santa.mind.add_antag_datum(/datum/antagonist/santa) diff --git a/code/modules/events/shuttle_insurance.dm b/code/modules/events/shuttle_insurance.dm index 4fce9c556b8b..4442291a9ed9 100644 --- a/code/modules/events/shuttle_insurance.dm +++ b/code/modules/events/shuttle_insurance.dm @@ -43,7 +43,7 @@ /datum/round_event/shuttle_insurance/start() insurance_message = new("Shuttle Insurance", "Hey, pal, this is the [ship_name]. Can't help but notice you're rocking a wild and crazy shuttle there with NO INSURANCE! Crazy. What if something happened to it, huh?! We've done a quick evaluation on your rates in this sector and we're offering [insurance_evaluation] to cover for your shuttle in case of any disaster.", list("Purchase Insurance.","Reject Offer.")) insurance_message.answer_callback = CALLBACK(src, PROC_REF(answered)) - SScommunications.send_message(insurance_message, unique = TRUE) + GLOB.communications_controller.send_message(insurance_message, unique = TRUE) /datum/round_event/shuttle_insurance/proc/answered() if(EMERGENCY_AT_LEAST_DOCKED) diff --git a/code/modules/events/spider_infestation.dm b/code/modules/events/spider_infestation.dm deleted file mode 100644 index ab518fd3af11..000000000000 --- 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/stray_cargo.dm b/code/modules/events/stray_cargo.dm index fee767142c40..2dc2a32d1259 100644 --- a/code/modules/events/stray_cargo.dm +++ b/code/modules/events/stray_cargo.dm @@ -160,7 +160,13 @@ var/pack_telecrystals = tgui_input_number(usr, "Please input crate's value in telecrystals.", "Set Telecrystals.", 30) if(isnull(pack_telecrystals)) return ADMIN_CANCEL_EVENT - var/list/possible_uplinks = list("Traitor" = UPLINK_TRAITORS, "Nuke Op" = UPLINK_NUKE_OPS, "Clown Op" = UPLINK_CLOWN_OPS) + var/list/possible_uplinks = list( + "Traitor" = UPLINK_TRAITORS, + "Nuke Op" = UPLINK_NUKE_OPS, + "Clown Op" = UPLINK_CLOWN_OPS, + "Lone Op" = UPLINK_LONE_OP, + //"Spy" = UPLINK_SPY, + ) var/uplink_type = tgui_input_list(usr, "Choose uplink to draw items from.", "Choose uplink type.", possible_uplinks) var/selection if(!isnull(uplink_type)) diff --git a/code/modules/events/wizard/imposter.dm b/code/modules/events/wizard/imposter.dm index 829942261427..914780d6d787 100644 --- a/code/modules/events/wizard/imposter.dm +++ b/code/modules/events/wizard/imposter.dm @@ -13,20 +13,18 @@ if(!ishuman(M.current)) continue var/mob/living/carbon/human/W = M.current - var/list/candidates = SSpolling.poll_ghost_candidates("Would you like to be an imposter wizard?", check_jobban = ROLE_WIZARD, pic_source = /obj/item/clothing/head/wizard, role_name_text = "imposter wizard") - if(!length(candidates)) + var/mob/chosen_one = SSpolling.poll_ghost_candidates("Would you like to be an [span_notice("imposter wizard")]?", check_jobban = ROLE_WIZARD, alert_pic = /obj/item/clothing/head/wizard, jump_target = W, role_name_text = "imposter wizard", amount_to_pick = 1) + if(isnull(chosen_one)) return //Sad Trombone - var/mob/dead/observer/C = pick(candidates) new /obj/effect/particle_effect/fluid/smoke(W.loc) - var/mob/living/carbon/human/I = new /mob/living/carbon/human(W.loc) W.dna.transfer_identity(I, transfer_SE=1) I.real_name = I.dna.real_name I.name = I.dna.real_name I.updateappearance(mutcolor_update=1) I.domutcheck() - I.key = C.key + I.key = chosen_one.key var/datum/antagonist/wizard/master = M.has_antag_datum(/datum/antagonist/wizard) if(!master.wiz_team) master.create_wiz_team() @@ -35,7 +33,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/events/wizard/rpgtitles.dm b/code/modules/events/wizard/rpgtitles.dm index 37eae2459f62..68df2bd27cb7 100644 --- a/code/modules/events/wizard/rpgtitles.dm +++ b/code/modules/events/wizard/rpgtitles.dm @@ -38,7 +38,7 @@ GLOBAL_DATUM(rpgtitle_controller, /datum/rpgtitle_controller) /datum/rpgtitle_controller/proc/on_crewmember_join(datum/source, mob/living/new_crewmember, rank) SIGNAL_HANDLER - var/datum/job/job = SSjob.GetJob(rank) + var/datum/job/job = SSjob.get_job(rank) //we must prepare for the mother of all strings new_crewmember.maptext_height = max(new_crewmember.maptext_height, 32) diff --git a/code/modules/jobs/job_exp.dm b/code/modules/jobs/job_exp.dm index b2c051a4e1c7..ed86ecdbfcd2 100644 --- a/code/modules/jobs/job_exp.dm +++ b/code/modules/jobs/job_exp.dm @@ -155,7 +155,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 163289c2e7f9..2d0116919247 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -425,10 +425,10 @@ if(visualsOnly) return - var/datum/job/equipped_job = SSjob.GetJobType(jobtype) + var/datum/job/equipped_job = SSjob.get_job_type(jobtype) if(!equipped_job) - equipped_job = SSjob.GetJob(equipped.job) + equipped_job = SSjob.get_job(equipped.job) var/obj/item/card/id/card = equipped.wear_id @@ -660,3 +660,11 @@ /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) + +/// This proc may be called when someone of this job is made into a traitor to create custom objectives related to the job. +/datum/job/proc/generate_traitor_objective() + return null diff --git a/code/modules/jobs/job_types/ai.dm b/code/modules/jobs/job_types/ai.dm index a19a8d1e5762..95b1c9265e73 100644 --- a/code/modules/jobs/job_types/ai.dm +++ b/code/modules/jobs/job_types/ai.dm @@ -92,3 +92,6 @@ /datum/job/ai/get_radio_information() return "Prefix your message with :[MODE_KEY_BINARY] 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 7c1a333da2c7..000000000000 --- 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 6fbfdd50994b..f2e38bf27d74 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 bac08cfd1cf6..b70962f0f6f5 100644 --- a/code/modules/jobs/job_types/captain.dm +++ b/code/modules/jobs/job_types/captain.dm @@ -45,7 +45,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" voice_of_god_power = 1.4 //Command staff has authority diff --git a/code/modules/jobs/job_types/cook.dm b/code/modules/jobs/job_types/cook.dm index cb571d0f41bd..4e2914696315 100644 --- a/code/modules/jobs/job_types/cook.dm +++ b/code/modules/jobs/job_types/cook.dm @@ -95,7 +95,7 @@ ..() if(!change_hat) return - var/datum/job/cook/other_chefs = SSjob.GetJobType(jobtype) + var/datum/job/cook/other_chefs = SSjob.get_job_type(jobtype) if(other_chefs) // If there's other Chefs, you're a Cook if(other_chefs.cooks > 0)//Cooks id_trim = /datum/id_trim/job/cook diff --git a/code/modules/jobs/job_types/cyborg.dm b/code/modules/jobs/job_types/cyborg.dm index 6b92a6c1acb3..7158b1226abc 100644 --- a/code/modules/jobs/job_types/cyborg.dm +++ b/code/modules/jobs/job_types/cyborg.dm @@ -34,3 +34,6 @@ /datum/job/cyborg/get_radio_information() return "Prefix your message with :[MODE_KEY_BINARY] 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 ad83e25123ad..cfde4602e36b 100644 --- a/code/modules/jobs/job_types/detective.dm +++ b/code/modules/jobs/job_types/detective.dm @@ -48,7 +48,7 @@ /obj/item/reagent_containers/cup/glass/flask/det, ) 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_personnel.dm b/code/modules/jobs/job_types/head_of_personnel.dm index eb723252bef4..b36f078d00ef 100644 --- a/code/modules/jobs/job_types/head_of_personnel.dm +++ b/code/modules/jobs/job_types/head_of_personnel.dm @@ -52,6 +52,45 @@ /datum/job/head_of_personnel/get_captaincy_announcement(mob/living/captain) return "Due to staffing shortages, newly promoted Acting Captain [captain.real_name] on deck!" +/datum/job/head_of_personnel/generate_traitor_objective() + var/datum/objective/assassinate/captain_replacement/promotion = new() + promotion.target = promotion.find_target() + if(isnull(promotion.target)) + qdel(promotion) + return null + + promotion.update_explanation_text() + return promotion + +/// Special assassination objective to kill the cap, take their id, and become the new captain +/datum/objective/assassinate/captain_replacement + name = "replace the captain" + admin_grantable = FALSE + +/datum/objective/assassinate/captain_replacement/update_explanation_text() + . = ..() + explanation_text = "Assassinate [target.name], the Captain, and steal [target.p_their()] ID card." + +/datum/objective/assassinate/captain_replacement/check_completion() + if(completed) + return TRUE + if(!..()) + return FALSE + + for(var/datum/mind/hop as anything in get_owners()) + if(!isliving(hop.current)) + continue + + if(locate(/obj/item/card/id/advanced/gold) in hop.current.get_all_contents()) + return TRUE + + return FALSE + +/datum/objective/assassinate/captain_replacement/find_target(dupe_search_range, list/blacklist) + for(var/datum/mind/fellow_head as anything in SSjob.get_all_heads() - blacklist) + if(is_captain_job(fellow_head.assigned_role)) + return fellow_head + return null /datum/outfit/job/hop name = "Head of Personnel" diff --git a/code/modules/jobs/job_types/head_of_security.dm b/code/modules/jobs/job_types/head_of_security.dm index aed377d3c253..27270d229a02 100644 --- a/code/modules/jobs/job_types/head_of_security.dm +++ b/code/modules/jobs/job_types/head_of_security.dm @@ -37,7 +37,7 @@ /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 voice_of_god_power = 1.4 //Command staff has authority diff --git a/code/modules/jobs/job_types/prisoner.dm b/code/modules/jobs/job_types/prisoner.dm index ed00d5a1ef19..682371c94662 100644 --- a/code/modules/jobs/job_types/prisoner.dm +++ b/code/modules/jobs/job_types/prisoner.dm @@ -25,7 +25,7 @@ /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 base_skills = list( /datum/skill/botany = SKILL_LEVEL_NOVICE, diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index 2206e4a6572b..a506c1485f58 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -45,7 +45,7 @@ /obj/item/melee/baton/security/boomerang/loaded = 1 ) rpg_title = "Guard" - job_flags = STATION_JOB_FLAGS + job_flags = STATION_JOB_FLAGS | JOB_ANTAG_PROTECTED base_skills = list( /datum/skill/eva = SKILL_LEVEL_JOURNEYMAN, diff --git a/code/modules/jobs/job_types/station_trait/bridge_assistant.dm b/code/modules/jobs/job_types/station_trait/bridge_assistant.dm deleted file mode 100644 index bce78cf55c2d..000000000000 --- a/code/modules/jobs/job_types/station_trait/bridge_assistant.dm +++ /dev/null @@ -1,87 +0,0 @@ -/datum/job/bridge_assistant - title = JOB_BRIDGE_ASSISTANT - description = "Watch over the Bridge, command its consoles, and spend your days brewing coffee for higher-ups." - auto_deadmin_role_flags = DEADMIN_POSITION_HEAD //not really a head but close enough - department_head = list(JOB_CAPTAIN) - faction = FACTION_STATION - total_positions = 0 - spawn_positions = 0 - supervisors = "the Captain, and in non-Bridge related situations the other heads" - minimal_player_age = 7 - exp_requirements = 300 - exp_required_type = EXP_TYPE_CREW - exp_granted_type = EXP_TYPE_CREW - config_tag = "BRIDGE_ASSISTANT" - - base_outfit = /datum/outfit/job/bridge_assistant - plasmaman_outfit = /datum/outfit/plasmaman/bridge_assistant - - paycheck = PAYCHECK_CREW - paycheck_department = ACCOUNT_CIV - - liver_traits = list(TRAIT_PRETENDER_ROYAL_METABOLISM) - - display_order = JOB_DISPLAY_ORDER_BRIDGE_ASSISTANT - departments_list = list(/datum/job_department/command) - - family_heirlooms = list( - // /obj/item/banner/command/mundane, - ) - - mail_goodies = list( - /obj/item/storage/fancy/cigarettes = 1, - /obj/item/pen/fountain = 1, - ) - rpg_title = "Royal Guard" - allow_bureaucratic_error = FALSE - job_flags = STATION_JOB_FLAGS | STATION_TRAIT_JOB_FLAGS - ignore_human_authority = TRUE - - base_skills = list( - /datum/skill/firearms = SKILL_LEVEL_NOVICE, - /datum/skill/first_aid = SKILL_LEVEL_NOVICE, - ) - -/datum/job/bridge_assistant/after_spawn(mob/living/spawned, client/player_client) - . = ..() - ADD_TRAIT(spawned, TRAIT_NO_TWOHANDING, JOB_TRAIT) - -/datum/job/bridge_assistant/get_roundstart_spawn_point() - var/list/chair_turfs = list() - var/list/possible_turfs = list() - var/area/bridge = GLOB.areas_by_type[/area/station/command/bridge] - if(isnull(bridge)) - return ..() //if no bridge, spawn on the arrivals shuttle (but also what the fuck) - for (var/list/zlevel_turfs as anything in bridge.get_zlevel_turf_lists()) - for (var/turf/possible_turf as anything in zlevel_turfs) - if(possible_turf.is_blocked_turf()) - continue - if(locate(/obj/structure/chair) in possible_turf) - chair_turfs += possible_turf - continue - possible_turfs += possible_turf - if(length(chair_turfs)) - return pick(chair_turfs) //prioritize turfs with a chair - if(length(possible_turfs)) - return pick(possible_turfs) //if none, just pick a random turf in the bridge - return ..() //if the bridge has no turfs, spawn on the arrivals shuttle - -/datum/outfit/job/bridge_assistant - name = "Bridge Assistant" - jobtype = /datum/job/bridge_assistant - - id_trim = /datum/id_trim/job/bridge_assistant - backpack_contents = list( - /obj/item/modular_computer/pda/bridge_assistant = 1, - ) - - uniform = /obj/item/clothing/under/trek/command/next - neck = /obj/item/clothing/neck/large_scarf/blue - belt = /obj/item/storage/belt/utility/full/inducer - ears = /obj/item/radio/headset/headset_com - glasses = /obj/item/clothing/glasses/sunglasses - gloves = /obj/item/clothing/gloves/fingerless - head = /obj/item/clothing/head/soft/black - shoes = /obj/item/clothing/shoes/laceup - l_pocket = /obj/item/gun/energy/e_gun/mini - r_pocket = /obj/item/assembly/flash/handheld 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 2e387b5c33a8..c11c931a3ef9 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/veteran_advisor.dm b/code/modules/jobs/job_types/station_trait/veteran_advisor.dm index 50607967f8ab..795e4caad7d3 100644 --- a/code/modules/jobs/job_types/station_trait/veteran_advisor.dm +++ b/code/modules/jobs/job_types/station_trait/veteran_advisor.dm @@ -37,7 +37,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 crewmonitor_priority = 18 diff --git a/code/modules/jobs/job_types/warden.dm b/code/modules/jobs/job_types/warden.dm index 04594f80a0b4..5699fcd6bbb0 100644 --- a/code/modules/jobs/job_types/warden.dm +++ b/code/modules/jobs/job_types/warden.dm @@ -42,7 +42,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 base_skills = list( /datum/skill/firearms = SKILL_LEVEL_EXPERT, diff --git a/code/modules/library/skill_learning/job_skillchips/_job.dm b/code/modules/library/skill_learning/job_skillchips/_job.dm index 4572baae4d6b..2be2e9a295ea 100644 --- a/code/modules/library/skill_learning/job_skillchips/_job.dm +++ b/code/modules/library/skill_learning/job_skillchips/_job.dm @@ -19,7 +19,7 @@ . = ..() if(isnull(job_type)) return INITIALIZE_HINT_QDEL - var/datum/job/job_datum = SSjob.GetJobType(job_type) + var/datum/job/job_datum = SSjob.get_job_type(job_type) if(!length(job_datum.base_skills)) stack_trace("Skill job skillchip on a job with no skills, USELESS. (job type: [job_type])") return INITIALIZE_HINT_QDEL diff --git a/code/modules/logging/categories/log_category_game.dm b/code/modules/logging/categories/log_category_game.dm index 1d29f288984a..f337d5f821c7 100644 --- a/code/modules/logging/categories/log_category_game.dm +++ b/code/modules/logging/categories/log_category_game.dm @@ -12,6 +12,11 @@ config_flag = /datum/config_entry/flag/log_emote master_category = /datum/log_category/game +/datum/log_category/game_ghost_polls + category = LOG_CATEGORY_GAME_GHOST_POLLS + config_flag = /datum/config_entry/flag/log_ghost_poll + master_category = /datum/log_category/game + /datum/log_category/game_internet_request category = LOG_CATEGORY_GAME_INTERNET_REQUEST config_flag = /datum/config_entry/flag/log_internet_request diff --git a/code/modules/mapfluff/ruins/spaceruin_code/caravanambush.dm b/code/modules/mapfluff/ruins/spaceruin_code/caravanambush.dm index 7884b6852264..68b95a4e8473 100644 --- a/code/modules/mapfluff/ruins/spaceruin_code/caravanambush.dm +++ b/code/modules/mapfluff/ruins/spaceruin_code/caravanambush.dm @@ -56,14 +56,6 @@ shuttleId = "caravantrade1" possible_destinations = "whiteship_away;whiteship_home;whiteship_z4;whiteship_lavaland;caravantrade1_custom;caravantrade1_ambush" -/obj/machinery/computer/camera_advanced/shuttle_docker/caravan/Initialize(mapload) - . = ..() - GLOB.jam_on_wardec += src - -/obj/machinery/computer/camera_advanced/shuttle_docker/caravan/Destroy() - GLOB.jam_on_wardec -= src - return ..() - /obj/machinery/computer/camera_advanced/shuttle_docker/caravan/trade1 name = "Small Freighter Navigation Computer" desc = "Used to designate a precise transit location for the Small Freighter." diff --git a/code/modules/mapfluff/ruins/spaceruin_code/cyborgmothership.dm b/code/modules/mapfluff/ruins/spaceruin_code/cyborgmothership.dm index cad1c8488f91..463eda50f611 100644 --- a/code/modules/mapfluff/ruins/spaceruin_code/cyborgmothership.dm +++ b/code/modules/mapfluff/ruins/spaceruin_code/cyborgmothership.dm @@ -56,14 +56,6 @@ designate_time = 100 y_offset = -11 -/obj/machinery/computer/camera_advanced/shuttle_docker/cyborg_mothership/Initialize(mapload) - . = ..() - GLOB.jam_on_wardec += src - -/obj/machinery/computer/camera_advanced/shuttle_docker/cyborg_mothership/Destroy() - GLOB.jam_on_wardec -= src - return ..() - /obj/item/disk/holodisk/ruin/cyborg_mothership name = "Blackbox Print-out #101011" desc = "A rusty holodisk containing the last moments of #101011." diff --git a/code/modules/meteors/meteor_mode_controller.dm b/code/modules/meteors/meteor_mode_controller.dm new file mode 100644 index 000000000000..d19d5522bb77 --- /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 d19d3aff0a5a..ecdb3f127134 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/magic/mutate.ogg') message_admins("[ADMIN_LOOKUPFLW(new_changeling)] has been made into a space changeling by an event.") diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index 3fe20d1bd568..af1033c57af8 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -126,7 +126,7 @@ return GENERIC_JOB_UNAVAILABLE_ERROR /mob/dead/new_player/proc/IsJobUnavailable(rank, latejoin = FALSE) - var/datum/job/job = SSjob.GetJob(rank) + var/datum/job/job = SSjob.get_job(rank) if(!(job.job_flags & JOB_NEW_PLAYER_JOINABLE)) return JOB_UNAVAILABLE_GENERIC if((job.current_positions >= job.total_positions) && job.total_positions != -1) @@ -171,9 +171,9 @@ SSticker.queued_players -= src SSticker.queue_delay = 4 - var/datum/job/job = SSjob.GetJob(rank) + var/datum/job/job = SSjob.get_job(rank) - if(!SSjob.AssignRole(src, job, TRUE)) + if(!SSjob.assign_role(src, job, TRUE)) tgui_alert(usr, "There was an unexpected error putting you into your requested job. If you cannot join with any job, you should contact an admin.") return FALSE @@ -186,7 +186,7 @@ CRASH("Failed to create a character for latejoin.") transfer_character() - SSjob.EquipRank(character, job, character.client) + SSjob.equip_rank(character, job, character.client) job.after_latejoin_spawn(character) #define IS_NOT_CAPTAIN 0 @@ -225,14 +225,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(humanc) if(job.job_flags & JOB_ASSIGN_QUIRKS) @@ -299,23 +293,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.")) unready() 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/dead/new_player/preferences_setup.dm b/code/modules/mob/dead/new_player/preferences_setup.dm index a745618f1a32..0c75bf4525cd 100644 --- a/code/modules/mob/dead/new_player/preferences_setup.dm +++ b/code/modules/mob/dead/new_player/preferences_setup.dm @@ -88,13 +88,13 @@ for(var/job in job_preferences) if(job_preferences[job] > highest_pref) - preview_job = SSjob.GetJob(job) + preview_job = SSjob.get_job(job) highest_pref = job_preferences[job] return preview_job /datum/preferences/proc/render_new_preview_appearance(mob/living/carbon/human/dummy/mannequin, show_job_clothes = TRUE) - var/datum/job/no_job = SSjob.GetJobType(/datum/job/unassigned) + var/datum/job/no_job = SSjob.get_job_type(/datum/job/unassigned) var/datum/job/preview_job = get_highest_priority_job() || no_job if(preview_job) diff --git a/code/modules/mob/living/basic/bots/_bots.dm b/code/modules/mob/living/basic/bots/_bots.dm index f6e4967cf4e8..60ec939ffe62 100644 --- a/code/modules/mob/living/basic/bots/_bots.dm +++ b/code/modules/mob/living/basic/bots/_bots.dm @@ -52,7 +52,7 @@ GLOBAL_LIST_INIT(command_strings, list( ///All initial access this bot started with. var/list/initial_access = list() ///Bot-related mode flags on the Bot indicating how they will act. BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION - var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION + var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT ///Bot-related cover flags on the Bot to deal with what has been done to their cover, including emagging. BOT_MAINTS_PANEL_OPEN | BOT_CONTROL_PANEL_OPEN | BOT_COVER_EMAGGED | BOT_COVER_HACKED var/bot_access_flags = NONE ///Small name of what the bot gets messed with when getting hacked/emagged. diff --git a/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm b/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm index 56bd95e178ed..6abee565b95e 100644 --- a/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm +++ b/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm @@ -348,7 +348,7 @@ ai_controller.set_blackboard_key(BB_CLEANBOT_EMAGGED_PHRASES, emagged_phrases) /mob/living/basic/bot/cleanbot/autopatrol - bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION + bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT /mob/living/basic/bot/cleanbot/medbay name = "Scrubs, MD" diff --git a/code/modules/mob/living/basic/bots/medbot/medbot.dm b/code/modules/mob/living/basic/bots/medbot/medbot.dm index edde350dc1ec..3f9b7c2e18aa 100644 --- a/code/modules/mob/living/basic/bots/medbot/medbot.dm +++ b/code/modules/mob/living/basic/bots/medbot/medbot.dm @@ -374,7 +374,7 @@ INVOKE_ASYNC(src, PROC_REF(speak), "Error! Surgical efficacy decreased to [round(heal_multiplier * 100)]%!") /mob/living/basic/bot/medbot/autopatrol - bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION + bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT /mob/living/basic/bot/medbot/stationary medical_mode_flags = MEDBOT_DECLARE_CRIT | MEDBOT_STATIONARY_MODE | MEDBOT_SPEAK_MODE diff --git a/code/modules/mob/living/basic/drone/extra_drone_types.dm b/code/modules/mob/living/basic/drone/extra_drone_types.dm index 927d28f0ca24..08c9278b7533 100644 --- a/code/modules/mob/living/basic/drone/extra_drone_types.dm +++ b/code/modules/mob/living/basic/drone/extra_drone_types.dm @@ -33,7 +33,7 @@ /mob/living/basic/drone/syndrone/Initialize(mapload) . = ..() var/datum/component/uplink/hidden_uplink = internal_storage.GetComponent(/datum/component/uplink) - hidden_uplink.set_telecrystals(telecrystal_count) + hidden_uplink.uplink_handler.set_telecrystals(telecrystal_count) /obj/effect/mob_spawn/ghost_role/drone/syndrone name = "syndrone shell" diff --git a/code/modules/mob/living/basic/drone/interaction.dm b/code/modules/mob/living/basic/drone/interaction.dm index debb5c7b33d7..dea5fc854299 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/guardian/guardian_creator.dm b/code/modules/mob/living/basic/guardian/guardian_creator.dm index 3f1f09275221..b88f3708ee82 100644 --- a/code/modules/mob/living/basic/guardian/guardian_creator.dm +++ b/code/modules/mob/living/basic/guardian/guardian_creator.dm @@ -52,11 +52,15 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial()) /mob/living/basic/guardian/standard, /mob/living/basic/guardian/support, ) + /// Have we been refunded? Used to prevent guardians from being created after we've been refunded + /// while avoiding scamming people if they use and then destroy us + var/was_refunded = FALSE /obj/item/guardian_creator/Initialize(mapload) . = ..() var/datum/guardian_fluff/using_theme = GLOB.guardian_themes[theme] mob_name = using_theme.name + RegisterSignal(src, COMSIG_ITEM_TC_REIMBURSED, PROC_REF(on_reimbursed)) /obj/item/guardian_creator/attack_self(mob/living/user) if(isguardian(user) && !allow_guardian) @@ -87,23 +91,32 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial()) used = TRUE to_chat(user, use_message) var/guardian_type_name = random ? "Random" : capitalize(initial(guardian_path.creator_name)) - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates( - "Do you want to play as [user.real_name]'s [guardian_type_name] [mob_name]?", + var/mob/chosen_one = SSpolling.poll_ghost_candidates( + "Do you want to play as [span_danger("[user.real_name]'s")] [span_notice("[guardian_type_name] [mob_name]")]?", check_jobban = ROLE_PAI, poll_time = 10 SECONDS, ignore_category = POLL_IGNORE_HOLOPARASITE, - pic_source = src, - role_name_text = "guardian spirit", + alert_pic = guardian_path, + jump_target = src, + role_name_text = guardian_type_name, + amount_to_pick = 1, ) - if(LAZYLEN(candidates)) - var/mob/dead/observer/candidate = pick(candidates) - spawn_guardian(user, candidate, guardian_path) + + if(was_refunded) + return + + if(chosen_one) + spawn_guardian(user, chosen_one, guardian_path) used = TRUE SEND_SIGNAL(src, COMSIG_TRAITOR_ITEM_USED(type)) else to_chat(user, failure_message) used = FALSE +/obj/item/guardian_creator/proc/on_reimbursed(datum/source) + SIGNAL_HANDLER + was_refunded = TRUE + /// Actually create our guy /obj/item/guardian_creator/proc/spawn_guardian(mob/living/user, mob/dead/candidate, guardian_path) if(QDELETED(user) || user.stat == DEAD) diff --git a/code/modules/mob/living/basic/guardian/guardian_verbs.dm b/code/modules/mob/living/basic/guardian/guardian_verbs.dm index 2f40da369f82..7c1a89132fef 100644 --- a/code/modules/mob/living/basic/guardian/guardian_verbs.dm +++ b/code/modules/mob/living/basic/guardian/guardian_verbs.dm @@ -169,20 +169,19 @@ return FALSE to_chat(owner, span_holoparasite("You attempt to reset [span_bold(chosen_guardian.real_name)]'s personality...")) - var/list/mob/dead/observer/ghost_candidates = SSpolling.poll_ghost_candidates("Do you want to play as [owner.real_name]'s [chosen_guardian.theme.name]?", check_jobban = ROLE_PAI, poll_time = 10 SECONDS, pic_source = chosen_guardian, role_name_text = chosen_guardian.theme.name) - if (!LAZYLEN(ghost_candidates)) + var/mob/chosen_one = SSpolling.poll_ghost_candidates("Do you want to play as [span_danger("[owner.real_name]'s")] [span_notice(chosen_guardian.theme.name)]?", check_jobban = ROLE_PAI, poll_time = 10 SECONDS, alert_pic = chosen_guardian, jump_target = owner, role_name_text = chosen_guardian.theme.name, amount_to_pick = 1) + if(isnull(chosen_one)) to_chat(owner, span_holoparasite("Your attempt to reset the personality of \ [span_bold(chosen_guardian.real_name)] appears to have failed... \ Looks like you're stuck with it for now.")) StartCooldown() return FALSE - var/mob/dead/observer/candidate = pick(ghost_candidates) to_chat(chosen_guardian, span_holoparasite("Your user reset you, and your body was taken over by a ghost. Looks like they weren't happy with your performance.")) to_chat(owner, span_boldholoparasite("The personality of [chosen_guardian.theme.name] has been successfully reset.")) - message_admins("[key_name_admin(candidate)] has taken control of ([ADMIN_LOOKUPFLW(chosen_guardian)])") + message_admins("[key_name_admin(chosen_one)] has taken control of ([ADMIN_LOOKUPFLW(chosen_guardian)])") chosen_guardian.ghostize(FALSE) - chosen_guardian.key = candidate.key + chosen_guardian.key = chosen_one.key COOLDOWN_START(chosen_guardian, resetting_cooldown, 5 MINUTES) chosen_guardian.guardian_rename() //give it a new color and name, to show it's a new person chosen_guardian.guardian_recolour() 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 ccecbf2cd461..22646790c37e 100644 --- a/code/modules/mob/living/basic/space_fauna/demon/demon.dm +++ b/code/modules/mob/living/basic/space_fauna/demon/demon.dm @@ -67,8 +67,7 @@ if(isnull(antag_type)) return // we weren't built for this proc to run - mind.set_assigned_role(SSjob.GetJobType(/datum/job/slaughter_demon)) - mind.special_role = ROLE_SLAUGHTER_DEMON + mind.set_assigned_role(SSjob.get_job_type(/datum/job/slaughter_demon)) mind.add_antag_datum(antag_type) SEND_SOUND(src, 'sound/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 2f5bc61b5973..35f040b3ac39 100644 --- a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm +++ b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm @@ -125,8 +125,7 @@ return TRUE generated_objectives_and_spells = TRUE - mind.set_assigned_role(SSjob.GetJobType(/datum/job/revenant)) - mind.special_role = ROLE_REVENANT + mind.set_assigned_role(SSjob.get_job_type(/datum/job/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/basic/space_fauna/revenant/revenant_items.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_items.dm index ea153b03c063..f8a3db0202a2 100644 --- a/code/modules/mob/living/basic/space_fauna/revenant/revenant_items.dm +++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_items.dm @@ -86,21 +86,11 @@ /// Handles giving the revenant a new client to control it /obj/item/ectoplasm/revenant/proc/get_new_user() message_admins("The new revenant's old client either could not be found or is in a new, living mob - grabbing a random candidate instead...") - var/list/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to be [revenant.name] (reforming)?", check_jobban = ROLE_REVENANT, role = ROLE_REVENANT, poll_time = 5 SECONDS, target_mob = revenant, pic_source = revenant) - - if(!LAZYLEN(candidates)) + var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to be [span_notice(revenant.name)] (reforming)?", check_jobban = ROLE_REVENANT, role = ROLE_REVENANT, poll_time = 5 SECONDS, checked_target = revenant, alert_pic = revenant, role_name_text = "reforming revenant", chat_text_border_icon = revenant) + if(isnull(chosen_one)) message_admins("No candidates were found for the new revenant.") inert = TRUE visible_message(span_revenwarning("[src] settles down and seems lifeless.")) qdel(revenant) return null - - var/mob/dead/observer/potential_client = pick(candidates) - if(isnull(potential_client)) - qdel(revenant) - message_admins("No candidate was found for the new revenant. Oh well!") - inert = TRUE - visible_message(span_revenwarning("[src] settles down and seems lifeless.")) - return null - - return potential_client + return chosen_one diff --git a/code/modules/mob/living/brain/posibrain.dm b/code/modules/mob/living/brain/posibrain.dm index b476299b1d8c..4959321ebdd9 100644 --- a/code/modules/mob/living/brain/posibrain.dm +++ b/code/modules/mob/living/brain/posibrain.dm @@ -139,7 +139,7 @@ GLOBAL_VAR(posibrain_notify_cooldown) brainmob.timeofdeath = transferred_user.timeofdeath brainmob.revive() if(brainmob.mind) - brainmob.mind.set_assigned_role(SSjob.GetJobType(posibrain_job_path)) + brainmob.mind.set_assigned_role(SSjob.get_job_type(posibrain_job_path)) if(transferred_user.mind) transferred_user.mind.transfer_to(brainmob) @@ -162,7 +162,7 @@ GLOBAL_VAR(posibrain_notify_cooldown) var/policy = get_policy(ROLE_POSIBRAIN) if(policy) to_chat(brainmob, policy) - brainmob.mind.set_assigned_role(SSjob.GetJobType(posibrain_job_path)) + brainmob.mind.set_assigned_role(SSjob.get_job_type(posibrain_job_path)) brainmob.revive() visible_message(new_mob_message) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 78dc6329775d..311f50549627 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -940,7 +940,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/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm index af97d4623a78..038e435b1e22 100644 --- a/code/modules/mob/living/carbon/human/human_helpers.dm +++ b/code/modules/mob/living/carbon/human/human_helpers.dm @@ -295,7 +295,7 @@ clone.age = age dna.transfer_identity(clone, transfer_SE = TRUE, transfer_species = TRUE) - clone.dress_up_as_job(SSjob.GetJob(job)) + clone.dress_up_as_job(SSjob.get_job(job)) for(var/datum/quirk/original_quircks as anything in quirks) clone.add_quirk(original_quircks.type, override_client = client, announce = FALSE) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index bf11c7d9cf4b..588bd64958c7 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -2557,8 +2557,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 @@ -2758,10 +2761,9 @@ GLOBAL_LIST_EMPTY(fire_appearances) if(isnull(guardian_client)) return else if(guardian_client == "Poll Ghosts") - var/list/candidates = SSpolling.poll_ghost_candidates("Do you want to play as an admin created Guardian Spirit of [real_name]?", check_jobban = ROLE_PAI, poll_time = 10 SECONDS, ignore_category = POLL_IGNORE_HOLOPARASITE, pic_source = src, role_name_text = "guardian spirit") - if(LAZYLEN(candidates)) - var/mob/dead/observer/candidate = pick(candidates) - guardian_client = candidate.client + var/mob/chosen_one = SSpolling.poll_ghost_candidates("Do you want to play as an admin created [span_notice("Guardian Spirit")] of [span_danger(real_name)]?", check_jobban = ROLE_PAI, poll_time = 10 SECONDS, ignore_category = POLL_IGNORE_HOLOPARASITE, alert_pic = mutable_appearance('icons/mob/nonhuman-player/guardian.dmi', "magicexample"), jump_target = src, role_name_text = "guardian spirit", amount_to_pick = 1) + if(chosen_one) + guardian_client = chosen_one.client else tgui_alert(admin, "No ghost candidates.", "Guardian Controller") return diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index f3e733207a5d..afcf49ebd4a4 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -133,7 +133,7 @@ if(target_ai.mind) 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) @@ -1156,11 +1156,7 @@ /mob/living/silicon/ai/get_exp_list(minutes) . = ..() - - var/datum/job/ai/ai_job_ref = SSjob.GetJobType(/datum/job/ai) - - .[ai_job_ref.title] = minutes - + .[/datum/job/ai::title] = minutes /mob/living/silicon/ai/get_voice(add_id_name) if(ai_voicechanger?.changing_voice) diff --git a/code/modules/mob/living/silicon/ai/ai_say.dm b/code/modules/mob/living/silicon/ai/ai_say.dm index c3b9c0267f39..255dd42cbda1 100644 --- a/code/modules/mob/living/silicon/ai/ai_say.dm +++ b/code/modules/mob/living/silicon/ai/ai_say.dm @@ -23,7 +23,7 @@ /mob/living/silicon/ai/compose_job(atom/movable/speaker, message_langs, raw_message, radio_freq) //Also includes the for AI hrefs, for convenience. - return "[radio_freq ? " (" + speaker.GetJob() + ")" : ""]" + "[speaker.GetSource() ? "" : ""]" + return "[radio_freq ? " (" + speaker.get_job() + ")" : ""]" + "[speaker.GetSource() ? "" : ""]" /mob/living/silicon/ai/try_speak(message, ignore_spam = FALSE, forced = null, filterproof = FALSE) // AIs cannot speak if silent AI is on. diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 8fa7fd9354f9..323a1519fcd1 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -1033,10 +1033,7 @@ /mob/living/silicon/robot/get_exp_list(minutes) . = ..() - - var/datum/job/cyborg/cyborg_job_ref = SSjob.GetJobType(/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/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm index fa93f7b02978..700121ae3d3f 100644 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ b/code/modules/mob/living/simple_animal/bot/bot.dm @@ -49,7 +49,7 @@ var/list/prev_access = list() ///Bot-related mode flags on the Bot indicating how they will act. BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION - var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION + var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT ///Bot-related cover flags on the Bot to deal with what has been done to their cover, including emagging. BOT_COVER_OPEN | BOT_COVER_LOCKED | BOT_COVER_EMAGGED | BOT_COVER_HACKED var/bot_cover_flags = BOT_COVER_LOCKED diff --git a/code/modules/mob/living/simple_animal/bot/honkbot.dm b/code/modules/mob/living/simple_animal/bot/honkbot.dm index 28ecc29c93e3..1a3799e250d2 100644 --- a/code/modules/mob/living/simple_animal/bot/honkbot.dm +++ b/code/modules/mob/living/simple_animal/bot/honkbot.dm @@ -11,7 +11,7 @@ radio_key = /obj/item/encryptionkey/headset_service //doesn't have security key radio_channel = RADIO_CHANNEL_SERVICE //Doesn't even use the radio anyway. bot_type = HONK_BOT - bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_AUTOPATROL | BOT_MODE_ROUNDSTART_POSSESSION + bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_AUTOPATROL hackables = "sound control systems" path_image_color = "#FF69B4" data_hud_type = TRAIT_SECURITY_HUD_ID_ONLY //show jobs diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm index a5818ebb20c3..81144a20ab47 100644 --- a/code/modules/mob/living/simple_animal/bot/mulebot.dm +++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm @@ -26,7 +26,6 @@ buckle_lying = 0 mob_size = MOB_SIZE_LARGE buckle_prevents_pull = TRUE // No pulling loaded shit - bot_mode_flags = ~BOT_MODE_ROUNDSTART_POSSESSION maints_access_required = list(ACCESS_ROBOTICS, ACCESS_CARGO) radio_key = /obj/item/encryptionkey/headset_cargo radio_channel = RADIO_CHANNEL_SUPPLY diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm index 4db34d582a6a..256828240831 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm @@ -340,7 +340,7 @@ var/mob/living/carbon/human/new_clown = user for(var/obj/item/to_strip in new_clown.get_equipped_items()) new_clown.dropItemToGround(to_strip) - new_clown.dress_up_as_job(SSjob.GetJobType(/datum/job/clown)) + new_clown.dress_up_as_job(SSjob.get_job_type(/datum/job/clown)) clowned_mob_refs += clown_ref return TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm index 4b9fe6a65827..e347ef0cf672 100644 --- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm @@ -184,10 +184,10 @@ While using this makes the system rely on OnFire, it still gives options for tim addtimer(CALLBACK(src, PROC_REF(spawn_elite)), 3 SECONDS) return visible_message(span_boldwarning("Something within [src] stirs...")) - var/list/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as a lavaland elite?", check_jobban = ROLE_SENTIENCE, role = ROLE_SENTIENCE, poll_time = 5 SECONDS, target_mob = src, ignore_category = POLL_IGNORE_LAVALAND_ELITE, pic_source = src, role_name_text = "lavaland elite") - if(candidates.len) + var/mob/chosen_one = SSpolling.poll_ghosts_for_target(check_jobban = ROLE_SENTIENCE, role = ROLE_SENTIENCE, poll_time = 5 SECONDS, checked_target = src, ignore_category = POLL_IGNORE_LAVALAND_ELITE, alert_pic = src, role_name_text = "lavaland elite") + if(chosen_one) audible_message(span_boldwarning("The stirring sounds increase in volume!")) - elitemind = pick(candidates) + elitemind = chosen_one elitemind.playsound_local(get_turf(elitemind), 'sound/effects/magic.ogg', 40, 0) to_chat(elitemind, "You have been chosen to play as a Lavaland Elite.\nIn a few seconds, you will be summoned on Lavaland as a monster to fight your activator, in a fight to the death.\n\ Your attacks can be switched using the buttons on the top left of the HUD, and used by clicking on targets or tiles similar to a gun.\n\ diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index 4d15d747bf7f..6c206813de7f 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -225,39 +225,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 + * 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 + 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))) +/mob/living/silicon/robot/is_antag(blacklisted_antag_flags) + return FALSE - 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 - 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) +/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 @@ -362,23 +345,18 @@ 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 [M.real_name]?" - if(M.mind) - poll_message = "[poll_message] Job: [M.mind.assigned_role.title]." - if(M.mind.special_role) - poll_message = "[poll_message] Status: [M.mind.special_role]." - else - var/datum/antagonist/A = M.mind.has_antag_datum(/datum/antagonist/) - if(A) - poll_message = "[poll_message] Status: [A.name]." - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob(poll_message, check_jobban = ROLE_PAI, poll_time = 10 SECONDS, target_mob = M, pic_source = M, role_name_text = "ghost control") - - if(LAZYLEN(candidates)) - var/mob/dead/observer/C = pick(candidates) + 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!") - message_admins("[key_name_admin(C)] has taken control of ([ADMIN_LOOKUPFLW(M)])") + message_admins("[key_name_admin(chosen_one)] has taken control of ([ADMIN_LOOKUPFLW(M)])") M.ghostize(FALSE) - M.key = C.key + M.key = chosen_one.key M.client?.init_verbs() return TRUE else @@ -409,13 +387,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 9fd097a1fd6a..af74e8f8dec0 100644 --- a/code/modules/mob/mob_lists.dm +++ b/code/modules/mob/mob_lists.dm @@ -96,13 +96,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() @@ -112,9 +112,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/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm index 657056a288ee..2b888b81fee1 100644 --- a/code/modules/mob/transform_procs.dm +++ b/code/modules/mob/transform_procs.dm @@ -191,11 +191,10 @@ to_chat(src, "You are job banned from cyborg! Appeal your job ban if you want to avoid this in the future!") ghostize(FALSE) - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob("Do you want to play as [src]?", check_jobban = JOB_CYBORG, poll_time = 5 SECONDS, target_mob = src, pic_source = src, role_name_text = "cyborg") - if(LAZYLEN(candidates)) - var/mob/dead/observer/chosen_candidate = pick(candidates) - message_admins("[key_name_admin(chosen_candidate)] has taken control of ([key_name_admin(src)]) to replace a jobbanned player.") - key = chosen_candidate.key + var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to play as [span_notice(name)]?", check_jobban = JOB_CYBORG, poll_time = 5 SECONDS, checked_target = src, alert_pic = src, role_name_text = "cyborg") + if(chosen_one) + message_admins("[key_name_admin(chosen_one)] has taken control of ([key_name_admin(src)]) to replace a jobbanned player.") + key = chosen_one.key //human -> alien /mob/living/carbon/human/proc/Alienize() diff --git a/code/modules/mob_spawn/ghost_roles/unused_roles.dm b/code/modules/mob_spawn/ghost_roles/unused_roles.dm index d9cdd699ade7..4ca259fa47c3 100644 --- a/code/modules/mob_spawn/ghost_roles/unused_roles.dm +++ b/code/modules/mob_spawn/ghost_roles/unused_roles.dm @@ -273,7 +273,7 @@ /obj/effect/mob_spawn/ghost_role/human/syndicatespace/special(mob/living/new_spawn) . = ..() new_spawn.grant_language(/datum/language/codespeak, source = LANGUAGE_MIND) - var/datum/job/spawn_job = SSjob.GetJobType(spawner_job_path) + var/datum/job/spawn_job = SSjob.get_job_type(spawner_job_path) var/policy = get_policy(spawn_job.policy_index) if(policy) to_chat(new_spawn, span_bold("[policy]")) diff --git a/code/modules/mob_spawn/mob_spawn.dm b/code/modules/mob_spawn/mob_spawn.dm index 0942217b16d8..b94499907570 100644 --- a/code/modules/mob_spawn/mob_spawn.dm +++ b/code/modules/mob_spawn/mob_spawn.dm @@ -245,7 +245,7 @@ spawned_mob.key = mob_possessor.key var/datum/mind/spawned_mind = spawned_mob.mind if(spawned_mind) - spawned_mob.mind.set_assigned_role_with_greeting(SSjob.GetJobType(spawner_job_path)) + spawned_mob.mind.set_assigned_role_with_greeting(SSjob.get_job_type(spawner_job_path)) spawned_mind.name = spawned_mob.real_name if(show_flavor) diff --git a/code/modules/modular_computers/computers/item/disks/virus_disk.dm b/code/modules/modular_computers/computers/item/disks/virus_disk.dm index e3eac7736f50..28ceb2da54a1 100644 --- a/code/modules/modular_computers/computers/item/disks/virus_disk.dm +++ b/code/modules/modular_computers/computers/item/disks/virus_disk.dm @@ -147,14 +147,11 @@ target_mind = pick(backup_players) hidden_uplink = target.AddComponent(/datum/component/uplink, target_mind, enabled = TRUE, starting_tc = telecrystals, has_progression = TRUE) hidden_uplink.unlock_code = unlock_code - hidden_uplink.uplink_handler.has_objectives = TRUE hidden_uplink.uplink_handler.owner = target_mind - hidden_uplink.uplink_handler.can_take_objectives = FALSE hidden_uplink.uplink_handler.progression_points = min(SStraitor.current_global_progression, current_progression) - hidden_uplink.uplink_handler.generate_objectives() SStraitor.register_uplink_handler(hidden_uplink.uplink_handler) else - hidden_uplink.add_telecrystals(telecrystals) + hidden_uplink.uplink_handler.add_telecrystals(telecrystals) telecrystals = 0 hidden_uplink.locked = FALSE hidden_uplink.active = TRUE diff --git a/code/modules/modular_computers/computers/item/pda.dm b/code/modules/modular_computers/computers/item/pda.dm index 1d3687eabf44..7d85432739f0 100644 --- a/code/modules/modular_computers/computers/item/pda.dm +++ b/code/modules/modular_computers/computers/item/pda.dm @@ -211,7 +211,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/jobmanagement.dm b/code/modules/modular_computers/file_system/programs/jobmanagement.dm index 8d4db5ffb08d..2d5184088639 100644 --- a/code/modules/modular_computers/file_system/programs/jobmanagement.dm +++ b/code/modules/modular_computers/file_system/programs/jobmanagement.dm @@ -62,7 +62,7 @@ GLOBAL_VAR_INIT(time_last_changed_position, 0) switch(action) if("PRG_open_job") var/edit_job_target = params["target"] - var/datum/job/j = SSjob.GetJob(edit_job_target) + var/datum/job/j = SSjob.get_job(edit_job_target) if(!can_edit_job(j) || !can_open_job(j)) return TRUE if(opened_positions[edit_job_target] >= 0) @@ -74,7 +74,7 @@ GLOBAL_VAR_INIT(time_last_changed_position, 0) return TRUE if("PRG_close_job") var/edit_job_target = params["target"] - var/datum/job/j = SSjob.GetJob(edit_job_target) + var/datum/job/j = SSjob.get_job(edit_job_target) if(!can_edit_job(j) || !can_close_job(j)) return TRUE //Allow instant closing without cooldown if a position has been opened before @@ -87,7 +87,7 @@ GLOBAL_VAR_INIT(time_last_changed_position, 0) return TRUE if("PRG_priority") var/priority_target = params["target"] - var/datum/job/j = SSjob.GetJob(priority_target) + var/datum/job/j = SSjob.get_job(priority_target) if(!can_edit_job(j)) return TRUE if(j.total_positions <= j.current_positions) 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 ce6cd1620ba6..e8b3824c549b 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 @@ -631,7 +631,7 @@ // Log in the talk log 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)"]") // Show it to ghosts 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/paperwork/paper.dm b/code/modules/paperwork/paper.dm index 99558d5ac500..62f4b8581089 100644 --- a/code/modules/paperwork/paper.dm +++ b/code/modules/paperwork/paper.dm @@ -66,6 +66,9 @@ ///If this paper can be selected as a candidate for a future message in a bottle when spawned outside of mapload. Doesn't affect manually doing that. var/can_become_message_in_bottle = TRUE + /// Assoc Lazylist of REF()s to mobs that are viewing the paper while holding a writing tool to what that tool's writing implement details are + VAR_FINAL/list/writers + /obj/item/paper/Initialize(mapload) . = ..() pixel_x = base_pixel_x + rand(-9, 9) @@ -503,7 +506,8 @@ /obj/item/paper/ui_assets(mob/user) return list( - get_asset_datum(/datum/asset/spritesheet/simple/paper), + get_asset_datum(/datum/asset/spritesheet/simple/stamps), + get_asset_datum(/datum/asset/simple/logos), ) /obj/item/paper/ui_interact(mob/user, datum/tgui/ui) @@ -511,8 +515,93 @@ return ui = SStgui.try_update_ui(user, src, ui) if(!ui) + /** + * these signals are for checking whether the ui viewer is holding a writing tool. + * (whether they are holding a writing tool matters for the state of the ui) + * + * we have to do this rigamarole, rather than just checking on ui_data calls, + * because if we set this ui to autoupdate, it causes weird rendering issues. + * rather than figure out why those are happening, it was easier to just turn off autoupdate. + */ + RegisterSignals(user, list( + COMSIG_MOB_UNEQUIPPED_ITEM, + COMSIG_MOB_EQUIPPED_ITEM, + COMSIG_MOB_SWAP_HANDS, + COMSIG_MOB_TRANSFORMING_ITEM, // specifically for pens + ), PROC_REF(viewer_writing_state_change)) + var/list/writing_info = get_viewer_writing_implement_details(user) + if(writing_info) + add_writer(user, writing_info, update = FALSE) + ui = new(user, src, "PaperSheet", name) ui.open() + // please see the above comment if you want to re-enable autoupdate + ui.set_autoupdate(FALSE) + +/obj/item/paper/ui_close(mob/user) + . = ..() + if(LAZYACCESS(writers, REF(user))) + remove_writer(user, update = FALSE) + UnregisterSignal(user, list( + COMSIG_MOB_UNEQUIPPED_ITEM, + COMSIG_MOB_EQUIPPED_ITEM, + COMSIG_MOB_SWAP_HANDS, + COMSIG_MOB_TRANSFORMING_ITEM, + )) + +/// Generically check if we are holding a writing tool to update our writer status +/obj/item/paper/proc/viewer_writing_state_change(mob/living/source) + SIGNAL_HANDLER + + var/list/writing_info = get_viewer_writing_implement_details(source) + if(writing_info) + if(!LAZYACCESS(writers, REF(source))) + add_writer(source, writing_info) + + else + if(LAZYACCESS(writers, REF(source))) + remove_writer(source) + +/// Add passed mob with passed writing info to the list of writers, then updates their ui +/obj/item/paper/proc/add_writer(mob/living/user, list/writing_info, update = TRUE) + PRIVATE_PROC(TRUE) + set waitfor = FALSE + + LAZYSET(writers, REF(user), writing_info) + if(update) + ui_interact(user) + +/// Remove passed mob from the list of writers, then updates their ui +/obj/item/paper/proc/remove_writer(mob/living/user, update = TRUE) + PRIVATE_PROC(TRUE) + set waitfor = FALSE + + LAZYREMOVE(writers, REF(user)) + if(update) + ui_interact(user) + +/obj/item/paper/proc/get_viewer_writing_implement_details(mob/living/user) + if(istype(loc, /obj/structure/noticeboard)) + var/obj/structure/noticeboard/noticeboard = loc + if(!noticeboard.allowed(user)) + return null + + var/obj/item/holding = user.get_active_held_item() + . = holding?.get_writing_implement_details() + + // Use a clipboard's pen, if applicable + if(istype(loc, /obj/item/clipboard)) + var/obj/item/clipboard/clipboard = loc + . ||= clipboard.pen?.get_writing_implement_details() + + return . + +/obj/item/paper/ui_data(mob/user) + var/list/data = list() + + data["held_item_details"] = LAZYACCESS(writers, REF(user)) + + return data /obj/item/paper/ui_static_data(mob/user) var/list/static_data = list() @@ -528,7 +617,7 @@ static_data["default_pen_color"] = COLOR_BLACK static_data["signature_font"] = FOUNTAIN_PEN_FONT - return static_data; + return static_data /obj/item/paper/proc/convert_to_data() var/list/data = list() @@ -567,28 +656,6 @@ name = data[LIST_PAPER_NAME] -/obj/item/paper/ui_data(mob/user) - var/list/data = list() - - var/obj/item/holding = user.get_active_held_item() - // Use a clipboard's pen, if applicable - if(istype(loc, /obj/item/clipboard)) - var/obj/item/clipboard/clipboard = loc - // This is just so you can still use a stamp if you're holding one. Otherwise, it'll - // use the clipboard's pen, if applicable. - if(!istype(holding, /obj/item/stamp) && clipboard.pen) - holding = clipboard.pen - - data["held_item_details"] = holding?.get_writing_implement_details() - - // If the paper is on an unwritable noticeboard, clear the held item details so it's read-only. - if(istype(loc, /obj/structure/noticeboard)) - var/obj/structure/noticeboard/noticeboard = loc - if(!noticeboard.allowed(user)) - data["held_item_details"] = null; - - return data - /obj/item/paper/ui_act(action, params, datum/tgui/ui) . = ..() if(.) diff --git a/code/modules/paperwork/paper_cutter.dm b/code/modules/paperwork/paper_cutter.dm index 1315ca3a81d2..0a6edf893be3 100644 --- a/code/modules/paperwork/paper_cutter.dm +++ b/code/modules/paperwork/paper_cutter.dm @@ -113,9 +113,12 @@ /obj/item/papercutter/attackby(obj/item/inserted_item, mob/user, params) if(istype(inserted_item, /obj/item/paper)) if(is_type_in_list(inserted_item, list( - /obj/item/paper/paperslip, /obj/item/paper/report, /obj/item/paper/fake_report, - /obj/item/paper/calling_card, /obj/item/paper/pamphlet, /obj/item/paper/holy_writ) - )) + /obj/item/paper/fake_report, + /obj/item/paper/holy_writ, + /obj/item/paper/pamphlet, + /obj/item/paper/paperslip, + /obj/item/paper/report, + ))) balloon_alert(user, "won't fit!") return if(stored_paper) diff --git a/code/modules/paperwork/stamps.dm b/code/modules/paperwork/stamps.dm index 9548fb04c477..55585a65e10e 100644 --- a/code/modules/paperwork/stamps.dm +++ b/code/modules/paperwork/stamps.dm @@ -22,7 +22,7 @@ return OXYLOSS /obj/item/stamp/get_writing_implement_details() - var/datum/asset/spritesheet_batched/sheet = get_asset_datum(/datum/asset/spritesheet/simple/paper) + var/datum/asset/spritesheet_batched/sheet = get_asset_datum(/datum/asset/spritesheet/simple/stamps) return list( interaction_mode = MODE_STAMPING, stamp_icon_state = icon_state, diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm index afdead3fd380..73e0f3dc8f38 100644 --- a/code/modules/projectiles/projectile/magic.dm +++ b/code/modules/projectiles/projectile/magic.dm @@ -363,28 +363,19 @@ /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 [target.real_name]?" - if(target.mind) - poll_message = "[poll_message] Job:[target.mind.assigned_role.title]." - if(target.mind && target.mind.special_role) - poll_message = "[poll_message] Status:[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:[A.name]." - var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates_for_mob(poll_message, check_jobban = ROLE_PAI, poll_time = 10 SECONDS, target_mob = target, pic_source = 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(LAZYLEN(candidates)) - var/mob/dead/observer/ghost = pick(candidates) + if(chosen_one) to_chat(target, span_boldnotice("You have been noticed by a ghost and it has possessed you!")) - var/oldkey = target.key - target.ghostize(FALSE) - target.key = ghost.key - trauma.friend.key = oldkey - trauma.friend.reset_perspective(null) - trauma.friend.Show() - trauma.friend_initialized = TRUE + var/mob/dead/observer/ghosted_target = target.ghostize(FALSE) + target.key = chosen_one.key + trauma.add_friend(ghosted_target) else to_chat(target, span_notice("Your mind has managed to go unnoticed in the spirit world.")) qdel(trauma) diff --git a/code/modules/research/server.dm b/code/modules/research/server.dm index 79a97b40a6bc..3f610fc23844 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/battlecruiser_starfury.dm b/code/modules/shuttle/battlecruiser_starfury.dm index e95ff4243f5d..a27cadacad2e 100644 --- a/code/modules/shuttle/battlecruiser_starfury.dm +++ b/code/modules/shuttle/battlecruiser_starfury.dm @@ -135,7 +135,7 @@ */ /proc/summon_battlecruiser(datum/team/battlecruiser/team) - var/list/candidates = SSpolling.poll_ghost_candidates("Do you wish to be considered for battlecruiser crew?", check_jobban = ROLE_TRAITOR, pic_source = /obj/machinery/sleeper/syndie, role_name_text = "battlecruiser crew") + var/list/candidates = SSpolling.poll_ghost_candidates("Do you wish to be considered for [span_notice("battlecruiser crew")]?", check_jobban = ROLE_TRAITOR, alert_pic = /obj/machinery/sleeper/syndie, role_name_text = "battlecruiser crew") shuffle_inplace(candidates) var/datum/map_template/ship = SSmapping.map_templates["battlecruiser_starfury.dmm"] diff --git a/code/modules/shuttle/shuttle_events/player_controlled.dm b/code/modules/shuttle/shuttle_events/player_controlled.dm deleted file mode 100644 index 77fee390a687..000000000000 --- a/code/modules/shuttle/shuttle_events/player_controlled.dm +++ /dev/null @@ -1,75 +0,0 @@ -///Mobs spawned with this one are automatically player controlled, if possible -/datum/shuttle_event/simple_spawner/player_controlled - spawning_list = list(/mob/living/basic/carp) - - ///If we cant find a ghost, do we spawn them anyway? Otherwise they go in the garbage bin - var/spawn_anyway_if_no_player = FALSE - - var/ghost_alert_string = "Would you like to be shot at the shuttle?" - - var/role_type = ROLE_SENTIENCE - -/datum/shuttle_event/simple_spawner/player_controlled/spawn_movable(spawn_type) - if(ispath(spawn_type, /mob/living)) - INVOKE_ASYNC(src, PROC_REF(try_grant_ghost_control), spawn_type) - else - ..() - -/// Attempt to grant control of a mob to ghosts before spawning it in. if spawn_anyway_if_no_player = TRUE, we spawn the mob even if there's no ghosts -/datum/shuttle_event/simple_spawner/player_controlled/proc/try_grant_ghost_control(spawn_type) - var/list/candidates = SSpolling.poll_ghost_candidates(ghost_alert_string + " (Warning: you will not be able to return to your body!)", check_jobban = role_type, poll_time = 10 SECONDS, pic_source = spawn_type, role_name_text = "shot at shuttle") - - if(!candidates.len && !spawn_anyway_if_no_player) - return - - var/mob/living/new_mob = new spawn_type (get_turf(get_spawn_turf())) - - if(candidates.len) - var/mob/dead/observer/candidate = pick(candidates) - new_mob.ckey = candidate.ckey - post_spawn(new_mob) - -///BACK FOR REVENGE!!! -/datum/shuttle_event/simple_spawner/player_controlled/alien_queen - name = "ALIEN QUEEN! (Kinda dangerous!)" - spawning_list = list(/mob/living/carbon/alien/adult/royal/queen = 1, /obj/vehicle/sealed/mecha/ripley = 1) - spawning_flags = SHUTTLE_EVENT_HIT_SHUTTLE - - event_probability = 0.2 - spawn_probability_per_process = 10 - activation_fraction = 0.5 - - spawn_anyway_if_no_player = FALSE - ghost_alert_string = "Would you like to be an alien queen shot at the shuttle?" - remove_from_list_when_spawned = TRUE - self_destruct_when_empty = TRUE - - role_type = ROLE_ALIEN - -///Spawns three player controlled carp!! Deadchats final chance to wreak havoc, probably really not that dangerous if even one person has a laser gun -/datum/shuttle_event/simple_spawner/player_controlled/carp - name = "Three player controlled carp! (Little dangerous!)" - spawning_list = list(/mob/living/basic/carp = 10, /mob/living/basic/carp/mega = 2, /mob/living/basic/carp/magic = 2, /mob/living/basic/carp/magic/chaos = 1) - spawning_flags = SHUTTLE_EVENT_HIT_SHUTTLE - - event_probability = 1 - spawn_probability_per_process = 10 - activation_fraction = 0.4 - - spawn_anyway_if_no_player = TRUE - ghost_alert_string = "Would you like to be a space carp to pester the emergency shuttle?" - remove_from_list_when_spawned = TRUE - self_destruct_when_empty = TRUE - - role_type = ROLE_SENTIENCE - - ///how many carp can we spawn max? - var/max_carp_spawns = 3 - -/datum/shuttle_event/simple_spawner/player_controlled/carp/New(obj/docking_port/mobile/port) - . = ..() - - var/list/spawning_list_copy = spawning_list.Copy() - spawning_list.Cut() - for(var/i in 1 to max_carp_spawns) - spawning_list[pick_weight(spawning_list_copy)] += 1 diff --git a/code/modules/shuttle/white_ship.dm b/code/modules/shuttle/white_ship.dm index b222fde787b0..a19492e43aa1 100644 --- a/code/modules/shuttle/white_ship.dm +++ b/code/modules/shuttle/white_ship.dm @@ -26,11 +26,3 @@ x_offset = -6 y_offset = -10 designate_time = 100 - -/obj/machinery/computer/camera_advanced/shuttle_docker/whiteship/Initialize(mapload) - . = ..() - GLOB.jam_on_wardec += src - -/obj/machinery/computer/camera_advanced/shuttle_docker/whiteship/Destroy() - GLOB.jam_on_wardec -= src - return ..() diff --git a/code/modules/spells/spell_types/self/mime_vow.dm b/code/modules/spells/spell_types/self/mime_vow.dm index bd666786b962..18744a9a4e5b 100644 --- a/code/modules/spells/spell_types/self/mime_vow.dm +++ b/code/modules/spells/spell_types/self/mime_vow.dm @@ -31,6 +31,6 @@ cast_on.log_message("broke [cast_on.p_their()] vow of silence.", LOG_GAME) cast_on.add_mood_event("vow", /datum/mood_event/broken_vow) REMOVE_TRAIT(cast_on, TRAIT_MIMING, "[type]") - var/datum/job/mime/mime_job = SSjob.GetJob(JOB_MIME) + var/datum/job/mime/mime_job = SSjob.get_job(JOB_MIME) mime_job.total_positions += 1 qdel(src) diff --git a/code/modules/station_goals/bsa.dm b/code/modules/station_goals/bsa.dm index e2feaa2bc26e..57bc857a20a1 100644 --- a/code/modules/station_goals/bsa.dm +++ b/code/modules/station_goals/bsa.dm @@ -11,11 +11,11 @@ GLOBAL_VAR_INIT(bsa_unlock, FALSE) /datum/station_goal/bluespace_cannon/get_report() return list( - "
Our military presence is inadequate in your sector.", + "Our military presence is inadequate in your sector.", "We need you to construct BSA-[rand(1,99)] Artillery position aboard your station.", "", "Base parts are available for shipping via cargo.", - "-Nanotrasen Naval Command
", + "- Nanotrasen Naval Command", ).Join("\n") /datum/station_goal/bluespace_cannon/on_report() diff --git a/code/modules/station_goals/dna_vault.dm b/code/modules/station_goals/dna_vault.dm index 2039fc1d64a5..12accfbd1154 100644 --- a/code/modules/station_goals/dna_vault.dm +++ b/code/modules/station_goals/dna_vault.dm @@ -33,7 +33,7 @@ /datum/station_goal/dna_vault/get_report() return list( - "
Our long term prediction systems indicate a 99% chance of system-wide cataclysm in the near future.", + "Our long term prediction systems indicate a 99% chance of system-wide cataclysm in the near future.", "We need you to construct a DNA Vault aboard your station.", "", "The DNA Vault needs to contain samples of:", @@ -41,7 +41,7 @@ "* [plant_count] unique non-standard plant data", "* [human_count] unique sapient humanoid DNA data", "", - "Base vault parts are available for shipping via cargo.
", + "Base vault parts are available for shipping via cargo.", ).Join("\n") diff --git a/code/modules/station_goals/meteor_shield.dm b/code/modules/station_goals/meteor_shield.dm index 84a61395a4b9..790b212da9e7 100644 --- a/code/modules/station_goals/meteor_shield.dm +++ b/code/modules/station_goals/meteor_shield.dm @@ -20,10 +20,10 @@ /datum/station_goal/station_shield/get_report() return list( - "
The station is located in a zone full of space debris.", + "The station is located in a zone full of space debris.", "We have a prototype shielding system you must deploy to reduce collision-related accidents.", "", - "You can order the satellites and control systems at cargo.
", + "You can order the satellites and control systems at cargo.", ).Join("\n") @@ -135,8 +135,6 @@ to_chat(user, span_warning("The last satellite emagged needs [DisplayTimeText(COOLDOWN_TIMELEFT(src, shared_emag_cooldown))] to recalibrate first. Emagging another so soon could damage the satellite network.")) return FALSE var/cooldown_applied = METEOR_SHIELD_EMAG_COOLDOWN - if(istype(emag_card, /obj/item/card/emag/meteor_shield_recalibrator)) - cooldown_applied /= 3 COOLDOWN_START(src, shared_emag_cooldown, cooldown_applied) obj_flags |= EMAGGED to_chat(user, span_notice("You access the satellite's debug mode and it begins emitting a strange signal, increasing the chance of meteor strikes.")) diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 1f7e96231d68..26d842a62008 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -216,7 +216,6 @@ #include "mutant_organs.dm" #include "novaflower_burn.dm" #include "nuke_cinematic.dm" -#include "objectives.dm" #include "omnitools.dm" #include "operating_table.dm" #include "orderable_items.dm" diff --git a/code/modules/unit_tests/anonymous_themes.dm b/code/modules/unit_tests/anonymous_themes.dm index a58f60eab3bd..99cd4f82802a 100644 --- a/code/modules/unit_tests/anonymous_themes.dm +++ b/code/modules/unit_tests/anonymous_themes.dm @@ -12,7 +12,7 @@ client.prefs.write_preference(GLOB.preference_entries[/datum/preference/name/real_name], "Prefs Biddle") - human.apply_prefs_job(client, SSjob.GetJobType(/datum/job/assistant)) + human.apply_prefs_job(client, SSjob.get_job_type(/datum/job/assistant)) TEST_ASSERT_NOTEQUAL(human.real_name, "Prefs Biddle", "apply_prefs_job didn't randomize human name with an anonymous theme") TEST_ASSERT_EQUAL(client.prefs.read_preference(/datum/preference/name/real_name), "Prefs Biddle", "Anonymous theme overrode original prefs") diff --git a/code/modules/unit_tests/dummy_spawn.dm b/code/modules/unit_tests/dummy_spawn.dm index ed359f962b7d..6ec59eaaa506 100644 --- a/code/modules/unit_tests/dummy_spawn.dm +++ b/code/modules/unit_tests/dummy_spawn.dm @@ -16,7 +16,7 @@ /datum/unit_test/dummy_spawn_outfit/Run() var/mob/living/carbon/human/dummy/lad = allocate(/mob/living/carbon/human/dummy) for(var/datum/job/one_two_three as anything in subtypesof(/datum/job)) - var/datum/job/can_you_hear_this = SSjob.GetJobType(one_two_three) + var/datum/job/can_you_hear_this = SSjob.get_job_type(one_two_three) if(!can_you_hear_this) log_test("\tJob type [one_two_three] could not be retrieved from SSjob") continue diff --git a/code/modules/unit_tests/dynamic_ruleset_sanity.dm b/code/modules/unit_tests/dynamic_ruleset_sanity.dm index 552959220c80..b42b34f99df3 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/objectives.dm b/code/modules/unit_tests/objectives.dm deleted file mode 100644 index 30263c0f0786..000000000000 --- a/code/modules/unit_tests/objectives.dm +++ /dev/null @@ -1,49 +0,0 @@ -/datum/unit_test/objectives_category/Run() - var/datum/traitor_category_handler/category_handler = allocate(/datum/traitor_category_handler) - var/list/objectives_that_exist = list() - for(var/datum/traitor_objective_category/category as anything in category_handler.all_categories) - for(var/value in category.objectives) - TEST_ASSERT(isnum(category.objectives[value]), "[category.type] does not have a valid format for its objectives as an objective category! ([value] requires a weight to be assigned to it)") - if(islist(value)) - recursive_check_list(category.type, value, objectives_that_exist) - else - objectives_that_exist += value - - SStraitor.generate_objectives = FALSE - for(var/datum/traitor_objective/objective_typepath as anything in subtypesof(/datum/traitor_objective)) - var/datum/traitor_objective/objective = allocate(objective_typepath) - if(objective.abstract_type == objective_typepath) - // In this case, we don't want abstract types to define values that should be defined on non-abstract types - // Nor do we want abstract types to appear in the pool of traitor objectives. - if(objective_typepath in objectives_that_exist) - TEST_FAIL("[objective_typepath] is in a traitor category and is an abstract type! Please remove it from the [/datum/traitor_objective_category].") - // Since we didn't generate the objective, the rewards are going to be in list form: (min, max) - if(!reward_is_zero(objective.progression_reward)) - TEST_FAIL("[objective_typepath] has set a progression reward as an abstract type! Please define progression rewards on non-abstract types rather than abstract types.") - // Since we didn't generate the objective, the rewards are going to be in list form: (min, max) - if(!reward_is_zero(objective.telecrystal_reward)) - TEST_FAIL("[objective_typepath] has set a telecrystal reward as an abstract type! Please define telecrystal rewards on non-abstract types rather than abstract types.") - continue - if(!(objective_typepath in objectives_that_exist)) - TEST_FAIL("[objective_typepath] is not in a traitor category and isn't an abstract type! Place it into a [/datum/traitor_objective_category] or remove it from code.") - if(objective.progression_minimum == null) - TEST_FAIL("[objective_typepath] has not defined a minimum progression level and isn't an abstract type! Please define the progression minimum variable on the datum") - if(objective.needs_reward && reward_is_zero(objective.progression_reward) && reward_is_zero(objective.telecrystal_reward)) - TEST_FAIL("[objective_typepath] has not set either a progression reward or a telecrystal reward! Please set either a telecrystal or progression reward for this objective.") - -/// Returns whether the reward specified (in format (min, max)) is zero or not. -/datum/unit_test/objectives_category/proc/reward_is_zero(list/reward) - return (reward[1] == 0 && reward[2] == 0) - -/datum/unit_test/objectives_category/Destroy() - SStraitor.generate_objectives = TRUE - return ..() - - -/datum/unit_test/objectives_category/proc/recursive_check_list(base_type, list/to_check, list/to_add_to) - for(var/value in to_check) - TEST_ASSERT(isnum(to_check[value]), "[base_type] does not have a valid format for its objectives as an objective category! ([value] requires a weight to be assigned to it)") - if(islist(value)) - recursive_check_list(base_type, value, to_add_to) - else - to_add_to += value 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 000000000000..d61c10e74871 Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_clownoperativemidround.png differ diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_nuclearoperative.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_nuclearoperative.png new file mode 100644 index 000000000000..450f465ee806 Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_nuclearoperative.png differ diff --git a/code/modules/unit_tests/security_officer_distribution.dm b/code/modules/unit_tests/security_officer_distribution.dm index e268aab709f3..05d62eeab351 100644 --- a/code/modules/unit_tests/security_officer_distribution.dm +++ b/code/modules/unit_tests/security_officer_distribution.dm @@ -68,7 +68,7 @@ var/mob/living/carbon/human/new_character = allocate(/mob/living/carbon/human/consistent) new_character.mind_initialize() - new_character.mind.set_assigned_role(SSjob.GetJobType(/datum/job/security_officer)) + new_character.mind.set_assigned_role(SSjob.get_job_type(/datum/job/security_officer)) new_player.new_character = new_character new_player.mock_client = mock_client diff --git a/code/modules/unit_tests/traitor.dm b/code/modules/unit_tests/traitor.dm index 16098a9227c4..5d82b087202f 100644 --- a/code/modules/unit_tests/traitor.dm +++ b/code/modules/unit_tests/traitor.dm @@ -1,7 +1,7 @@ /datum/unit_test/traitor/Run() var/datum/dynamic_ruleset/roundstart/traitor/traitor_ruleset = allocate(/datum/dynamic_ruleset/roundstart/traitor) var/list/possible_jobs = list() - var/list/restricted_roles = traitor_ruleset.restricted_roles + var/list/restricted_roles = traitor_ruleset.get_blacklisted_roles() for(var/datum/job/job as anything in SSjob.joinable_occupations) if(!(job.job_flags & JOB_CREW_MEMBER)) continue @@ -11,7 +11,7 @@ possible_jobs += rank for(var/job_name in possible_jobs) - var/datum/job/job = SSjob.GetJob(job_name) + var/datum/job/job = SSjob.get_job(job_name) var/mob/living/player = allocate(job.spawn_type) player.mind_initialize() var/datum/mind/mind = player.mind @@ -22,11 +22,3 @@ var/datum/antagonist/traitor/traitor = mind.add_antag_datum(/datum/antagonist/traitor) if(!traitor.uplink_handler) TEST_FAIL("[job_name] when made traitor does not have a proper uplink created when spawned in!") - for(var/datum/traitor_objective/objective_typepath as anything in subtypesof(/datum/traitor_objective)) - if(initial(objective_typepath.abstract_type) == objective_typepath) - continue - var/datum/traitor_objective/objective = allocate(objective_typepath, traitor.uplink_handler) - try - objective.generate_objective(mind, list()) - catch(var/exception/exception) - TEST_FAIL("[objective_typepath] failed to generate their objective. Reason: [exception.name] [exception.file]:[exception.line]\n[exception.desc]") diff --git a/code/modules/unit_tests/traitor_mail_content_check.dm b/code/modules/unit_tests/traitor_mail_content_check.dm index 6d14d9d1428d..b2ecfe2ef24a 100644 --- a/code/modules/unit_tests/traitor_mail_content_check.dm +++ b/code/modules/unit_tests/traitor_mail_content_check.dm @@ -5,6 +5,6 @@ var/mob/living/carbon/human/person = allocate(/mob/living/carbon/human/consistent) person.mind_initialize() var/obj/item/mail/traitor/test_mail = allocate(/obj/item/mail/traitor) - person.mind.set_assigned_role(SSjob.GetJobType(/datum/job/captain)) + person.mind.set_assigned_role(SSjob.get_job_type(/datum/job/captain)) test_mail.initialize_for_recipient(person.mind) TEST_ASSERT_EQUAL(test_mail.contents.len, 0, "/obj/item/mail/traitor should not have items after initialize_for_recipient proc!") diff --git a/code/modules/uplink/uplink_devices.dm b/code/modules/uplink/uplink_devices.dm index 4d539be433de..cad4e5569eab 100644 --- a/code/modules/uplink/uplink_devices.dm +++ b/code/modules/uplink/uplink_devices.dm @@ -68,6 +68,10 @@ var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) hidden_uplink.allow_restricted = FALSE +///A subtype used for lone ops, with some of the stuff they shouldn't/can't access removed from purchase. +/obj/item/uplink/loneop + uplink_flag = UPLINK_LONE_OP + /obj/item/uplink/clownop uplink_flag = UPLINK_CLOWN_OPS @@ -80,34 +84,6 @@ var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) hidden_uplink.name = "dusty radio" -// Uplink subtype used as replacement uplink -/obj/item/uplink/replacement - lockable_uplink = TRUE - -/obj/item/uplink/replacement/Initialize(mapload, owner, tc_amount = 10, datum/uplink_handler/uplink_handler_override = null) - . = ..() - var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) - var/mob/living/replacement_needer = owner - if(!istype(replacement_needer)) - return - var/datum/antagonist/traitor/traitor_datum = replacement_needer?.mind.has_antag_datum(/datum/antagonist/traitor) - hidden_uplink.unlock_code = traitor_datum?.replacement_uplink_code - become_hearing_sensitive() - -/obj/item/uplink/replacement/screwdriver_act_secondary(mob/living/user, obj/item/tool) - tool.play_tool_sound(src) - balloon_alert(user, "deconstructing...") - if (!do_after(user, 3 SECONDS, target = src)) - return FALSE - qdel(src) - return TRUE - -/obj/item/uplink/replacement/examine(mob/user) - . = ..() - if(!IS_TRAITOR(user)) - return - . += span_notice("You can destroy this device with a screwdriver.") - // Multitool uplink /obj/item/multitool/uplink/Initialize(mapload, owner, tc_amount = 20, datum/uplink_handler/uplink_handler_override = null) . = ..() diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm index 32783504fe87..9abc9d97ab2b 100644 --- a/code/modules/uplink/uplink_items.dm +++ b/code/modules/uplink/uplink_items.dm @@ -62,6 +62,8 @@ var/restricted_species = list() /// The minimum amount of progression needed for this item to be added to uplinks. var/progression_minimum = 0 + /// The minimum number of joined players (so not observers) needed for this item to be added to uplinks. + var/population_minimum = 0 /// Whether this purchase is visible in the purchase log. var/purchase_log_vis = TRUE // Visible in the purchase log? /// Whether this purchase is restricted or not (VR/Events related) diff --git a/code/modules/uplink/uplink_items/badass.dm b/code/modules/uplink/uplink_items/badass.dm index fd28cd708b9b..5342d65f3861 100644 --- a/code/modules/uplink/uplink_items/badass.dm +++ b/code/modules/uplink/uplink_items/badass.dm @@ -59,16 +59,11 @@ name = "Clown Costume" desc = "Nothing is more terrifying than clowns with fully automatic weaponry." item = /obj/item/storage/backpack/duffelbag/clown/syndie - purchasable_from = ALL - progression_minimum = 70 MINUTES /datum/uplink_item/badass/costumes/tactical_naptime name = "Sleepy Time Pajama Bundle" desc = "Even soldiers need to get a good nights rest. Comes with blood-red pajamas, a blankie, a hot mug of cocoa and a fuzzy friend." item = /obj/item/storage/box/syndie_kit/sleepytime - purchasable_from = ALL - progression_minimum = 90 MINUTES - cost = 4 limited_stock = 1 cant_discount = TRUE @@ -76,16 +71,12 @@ name = "Broken Chameleon Kit" desc = "A set of items that contain chameleon technology allowing you to disguise as pretty much anything on the station, and more! \ Please note that this kit did NOT pass quality control." - purchasable_from = ALL - progression_minimum = 90 MINUTES item = /obj/item/storage/box/syndie_kit/chameleon/broken /datum/uplink_item/badass/costumes/centcom_official name = "CentCom Official Costume" desc = "Ask the crew to \"inspect\" their nuclear disk and weapons system, and then when they decline, pull out a fully automatic rifle and gun down the Captain. \ Radio headset does not include encryption key. No gun included." - purchasable_from = ALL - progression_minimum = 110 MINUTES item = /obj/item/storage/box/syndie_kit/centcom_costume /datum/uplink_item/badass/stickers diff --git a/code/modules/uplink/uplink_items/contractor.dm b/code/modules/uplink/uplink_items/contractor.dm index 1364f8b088bb..68229d66c8cf 100644 --- a/code/modules/uplink/uplink_items/contractor.dm +++ b/code/modules/uplink/uplink_items/contractor.dm @@ -13,7 +13,7 @@ item = /obj/item/storage/box/syndicate/contract_kit category = /datum/uplink_category/contractor cost = 20 - purchasable_from = UPLINK_INFILTRATORS + purchasable_from = UPLINK_TRAITORS /datum/uplink_item/bundles_tc/contract_kit/purchase(mob/user, datum/uplink_handler/uplink_handler, atom/movable/source) . = ..() @@ -82,28 +82,9 @@ cost = 1 /datum/uplink_item/contractor/partner - name = "Reinforcements" - desc = "Upon purchase we'll contact available units in the area. Should there be an agent free, \ - we'll send them down to assist you immediately. If no units are free, we give a full refund." - item = /obj/effect/gibspawner/generic + name = "Contractor Reinforcement" + desc = "A reinforcement operative will be sent to aid you in your goals, \ + they are paid separately, and will not take a cut from your profits." + item = /obj/item/antag_spawner/loadout/contractor limited_stock = 1 cost = 2 - -/datum/uplink_item/contractor/partner/spawn_item(spawn_path, mob/user, datum/uplink_handler/uplink_handler, atom/movable/source) - to_chat(user, span_notice("The uplink vibrates quietly, connecting to nearby agents...")) - var/list/candidates = SSpolling.poll_ghost_candidates( - question = "Do you want to play as the Contractor Support Unit for [user.real_name]?", - check_jobban = ROLE_TRAITOR, - role = ROLE_TRAITOR, - poll_time = 10 SECONDS, - ignore_category = POLL_IGNORE_CONTRACTOR_SUPPORT, - pic_source = /obj/item/modular_computer/pda/syndicate_contract_uplink, - role_name_text = "contractor support unit", - ) - if(!LAZYLEN(candidates)) - to_chat(user, span_notice("No available agents at this time, please try again later.")) - limited_stock++ - return //bobux no icon - var/mob/dead/observer/selected_player = pick(candidates) - uplink_handler.contractor_hub.contractor_teammate = spawn_contractor_partner(user, selected_player.key) - return source //for log icon diff --git a/code/modules/uplink/uplink_items/dangerous.dm b/code/modules/uplink/uplink_items/dangerous.dm index 970741876bb7..bd9e06231284 100644 --- a/code/modules/uplink/uplink_items/dangerous.dm +++ b/code/modules/uplink/uplink_items/dangerous.dm @@ -35,7 +35,6 @@ name = "Energy Sword" desc = "The energy sword is an edged weapon with a blade of pure energy. The sword is small enough to be \ pocketed when inactive. Activating it produces a loud, distinctive noise." - progression_minimum = 20 MINUTES item = /obj/item/melee/energy/sword/saber cost = 8 purchasable_from = ~UPLINK_CLOWN_OPS @@ -46,7 +45,6 @@ Upon hitting a target, the piston-ram will extend forward to make contact for some serious damage. \ Using a wrench on the piston valve will allow you to tweak the amount of gas used per punch to \ deal extra damage and hit targets further. Use a screwdriver to take out any attached tanks." - progression_minimum = 20 MINUTES item = /obj/item/melee/powerfist cost = 6 purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) @@ -54,7 +52,6 @@ /datum/uplink_item/dangerous/rapid name = "Gloves of the North Star" desc = "These gloves let the user punch people very fast. Does not improve weapon attack speed or the meaty fists of a hulk." - progression_minimum = 20 MINUTES item = /obj/item/clothing/gloves/rapid cost = 8 @@ -63,6 +60,7 @@ desc = "The double-bladed energy sword does slightly more damage than a standard energy sword and will deflect \ energy projectiles it blocks, but requires two hands to wield. It also struggles to protect you from tackles." progression_minimum = 30 MINUTES + population_minimum = TRAITOR_POPULATION_LOWPOP item = /obj/item/dualsaber cost = 13 @@ -81,7 +79,6 @@ name = "Holoparasites" desc = "Though capable of near sorcerous feats via use of hardlight holograms and nanomachines, they require an \ organic host as a home base and source of fuel. Holoparasites come in various types and share damage with their host." - progression_minimum = 30 MINUTES item = /obj/item/guardian_creator/tech cost = 18 surplus = 0 diff --git a/code/modules/uplink/uplink_items/explosive.dm b/code/modules/uplink/uplink_items/explosive.dm index 72f40b00dfca..b792039b39ab 100644 --- a/code/modules/uplink/uplink_items/explosive.dm +++ b/code/modules/uplink/uplink_items/explosive.dm @@ -31,7 +31,6 @@ desc = "Contains 3 X-4 shaped plastic explosives. Similar to C4, but with a stronger blast that is directional instead of circular. \ X-4 can be placed on a solid surface, such as a wall or window, and it will blast through the wall, injuring anything on the opposite side, while being safer to the user. \ For when you want a controlled explosion that leaves a wider, deeper, hole." - progression_minimum = 20 MINUTES item = /obj/item/storage/backpack/duffelbag/syndie/x4 cost = 4 cant_discount = TRUE @@ -61,7 +60,6 @@ name = "Pizza Bomb" desc = "A pizza box with a bomb cunningly attached to the lid. The timer needs to be set by opening the box; afterwards, \ opening the box again will trigger the detonation after the timer has elapsed. Comes with free pizza, for you or your target!" - progression_minimum = 15 MINUTES item = /obj/item/pizzabox/bomb cost = 6 surplus = 8 diff --git a/code/modules/uplink/uplink_items/job.dm b/code/modules/uplink/uplink_items/job.dm index 50befb0a35d8..7a88805944ab 100644 --- a/code/modules/uplink/uplink_items/job.dm +++ b/code/modules/uplink/uplink_items/job.dm @@ -102,7 +102,6 @@ name = "Combat Bakery Kit" desc = "A kit of clandestine baked weapons. Contains a baguette which a skilled mime could use as a sword, \ a pair of throwing croissants, and the recipe to make more on demand. Once the job is done, eat the evidence." - progression_minimum = 15 MINUTES item = /obj/item/storage/box/syndie_kit/combat_baking cost = 7 restricted_roles = list(JOB_COOK, JOB_MIME) @@ -205,7 +204,6 @@ name = "Reverse Revolver" desc = "A revolver that always fires at its user. \"Accidentally\" drop your weapon, then watch as the greedy corporate pigs blow their own brains all over the wall. \ The revolver itself is actually real. Only clumsy people, and clowns, can fire it normally. Comes in a box of hugs. Honk." - progression_minimum = 30 MINUTES cost = 14 item = /obj/item/storage/box/hug/reverse_revolver restricted_roles = list(JOB_CLOWN) @@ -214,8 +212,6 @@ name = "Kinetic Accelerator Pressure Mod" desc = "A modification kit which allows Kinetic Accelerators to do greatly increased damage while indoors. \ Occupies 35% mod capacity." - // While less deadly than a revolver it does have infinite ammo - progression_minimum = 15 MINUTES item = /obj/item/borg/upgrade/modkit/indoors cost = 5 //you need two for full damage, so total of 10 for maximum damage limited_stock = 2 //you can't use more than two! @@ -234,7 +230,6 @@ /datum/uplink_item/role_restricted/laser_arm name = "Laser Arm Implant" desc = "An implant that grants you a recharging laser gun inside your arm. Weak to EMPs. Comes with a syndicate autosurgeon for immediate self-application." - progression_minimum = 20 MINUTES cost = 10 item = /obj/item/autosurgeon/syndicate/laser_arm restricted_roles = list(JOB_ROBOTICIST, JOB_RESEARCH_DIRECTOR) @@ -243,7 +238,6 @@ /datum/uplink_item/role_restricted/chemical_gun name = "Reagent Dartgun" desc = "A heavily modified syringe gun which is capable of synthesizing its own chemical darts using input reagents. Can hold 90u of reagents." - progression_minimum = 15 MINUTES item = /obj/item/gun/chem cost = 12 restricted_roles = list(JOB_CHEMIST, JOB_CHIEF_MEDICAL_OFFICER, JOB_BOTANIST) @@ -304,7 +298,6 @@ Attach to an exosuit with an existing equipment to disguise the bay as that equipment. The sacrificed equipment will be lost.\ Alternatively, you can attach the bay to an empty equipment slot, but the bay will not be concealed. Once the bay is \ attached, an exosuit weapon can be fitted inside." - progression_minimum = 30 MINUTES item = /obj/item/mecha_parts/mecha_equipment/concealed_weapon_bay cost = 3 restricted_roles = list(JOB_ROBOTICIST, JOB_RESEARCH_DIRECTOR) @@ -318,6 +311,7 @@ also give them a bit of sentience though." progression_minimum = 30 MINUTES item = /obj/item/reagent_containers/syringe/spider_extract + population_minimum = TRAITOR_POPULATION_LOWPOP cost = 10 restricted_roles = list(JOB_RESEARCH_DIRECTOR, JOB_SCIENTIST, JOB_ROBOTICIST) surplus = 10 diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm index d8bead5da678..8d35efeeb5b9 100644 --- a/code/modules/uplink/uplink_items/nukeops.dm +++ b/code/modules/uplink/uplink_items/nukeops.dm @@ -581,7 +581,9 @@ along with slurred speech, aggression, and the ability to infect others with this agent." item = /obj/item/storage/box/syndie_kit/romerol cost = 25 - purchasable_from = UPLINK_CLOWN_OPS|UPLINK_NUKE_OPS + population_minimum = TRAITOR_POPULATION_LOWPOP + progression_minimum = 30 MINUTES + purchasable_from = UPLINK_ALL_SYNDIE_OPS | UPLINK_TRAITORS // Don't give this to spies cant_discount = TRUE // Modsuits diff --git a/code/modules/uplink/uplink_items/stealthy.dm b/code/modules/uplink/uplink_items/stealthy.dm index 6ba88eda43ec..d81a56c1c720 100644 --- a/code/modules/uplink/uplink_items/stealthy.dm +++ b/code/modules/uplink/uplink_items/stealthy.dm @@ -76,6 +76,7 @@ and gain the ability to swat bullets from the air, but you will also refuse to use dishonorable ranged weaponry." item = /obj/item/book/granter/martial/carp progression_minimum = 30 MINUTES + population_minimum = TRAITOR_POPULATION_LOWPOP cost = 17 surplus = 0 purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) diff --git a/code/modules/uplink/uplink_items/stealthy_tools.dm b/code/modules/uplink/uplink_items/stealthy_tools.dm index 86bfa95a8e90..7eeca800d6a3 100644 --- a/code/modules/uplink/uplink_items/stealthy_tools.dm +++ b/code/modules/uplink/uplink_items/stealthy_tools.dm @@ -119,7 +119,7 @@ desc = "When purchased, a virus will be uploaded to the engineering processing servers to force a routine power grid check, forcing all APCs on the station to be temporarily disabled." item = /obj/effect/gibspawner/generic surplus = 0 - progression_minimum = 20 MINUTES + progression_minimum = 15 MINUTES limited_stock = 1 cost = 6 restricted = TRUE diff --git a/code/modules/uplink/uplink_items/suits.dm b/code/modules/uplink/uplink_items/suits.dm index 2e7e0edecaf4..0a0e90948ebc 100644 --- a/code/modules/uplink/uplink_items/suits.dm +++ b/code/modules/uplink/uplink_items/suits.dm @@ -68,16 +68,6 @@ item = /obj/item/mod/module/shock_absorber cost = 1 -/datum/uplink_item/suits/modsuit/elite_traitor - name = "Elite Syndicate MODsuit" - desc = "An upgraded, elite version of the Syndicate MODsuit. It features fireproofing, and also \ - provides the user with superior armor and mobility compared to the standard Syndicate MODsuit." - item = /obj/item/mod/control/pre_equipped/traitor_elite - // This one costs more than the nuke op counterpart - purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) - progression_minimum = 90 MINUTES - cost = 16 - /datum/uplink_item/suits/modsuit/wraith name = "MODsuit wraith cloaking module" desc = "A MODsuit module that grants to the user Optical camouflage and the ability to overload light sources to recharge suit power. \ diff --git a/config/dynamic.json b/config/dynamic.json deleted file mode 100644 index 27fb01ec6a28..000000000000 --- a/config/dynamic.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "Dynamic": {}, - "Roundstart": { - "Traitors": { - "cost": 101, - "scaling_cost": 9, - "weight": 0, - "required_candidates": 1, - "minimum_required_age": 0, - "requirements": [ - 10, - 10, - 10, - 10, - 10, - 10, - 10, - 10, - 10, - 10 - ], - "antag_cap": { - "denominator": 24 - }, - "protected_roles": [ - "Prisoner", - "Security Officer", - "Warden", - "Detective", - "Head of Security", - "Captain" - ], - "restricted_roles": [ - "Cyborg" - ] - }, - - "Blood Brothers": { - "weight": 0 - }, - - "Changelings": { - "weight": 0 - }, - - "Heretics": { - "weight": 0 - }, - - "Wizard": { - "weight": 0 - }, - - "Blood Cult": { - "weight": 0 - }, - - "Nuclear Emergency": { - "weight": 0 - }, - - "Revolution": { - "weight": 0 - } - }, - - "Midround": { - "Syndicate Sleeper Agent": { - "weight": 0 - }, - - "Malfunctioning AI": { - "weight": 0 - }, - - "Wizard": { - "weight": 0 - }, - - "Nuclear Assault": { - "weight": 0 - }, - - "Blob": { - "weight": 0 - }, - - "Blob Infection": { - "weight": 0 - }, - - "Alien Infestation": { - "weight": 0 - }, - - "Nightmare": { - "weight": 0 - }, - - "Space Dragon": { - "weight": 0 - }, - - "Abductors": { - "weight": 0 - }, - - "Space Ninja": { - "weight": 0 - }, - - "Spiders": { - "weight": 0 - }, - - "Revenant": { - "weight": 0 - }, - - "Sentient Disease": { - "weight": 0 - }, - - "Space Pirates": { - "weight": 0 - }, - - "Obsessed": { - "weight": 0 - }, - - "Space Changeling": { - "weight": 0 - }, - - "Paradox Clone": { - "weight": 0 - } - }, - - "Latejoin": { - "Syndicate Infiltrator": { - "weight": 0 - }, - - "Provocateur": { - "weight": 0 - }, - - "Heretic Smuggler": { - "weight": 0 - }, - - "Stowaway Changeling": { - "weight": 0 - } - }, - - "Station": { - "Radioactive Nebula": { - } - } -} diff --git a/config/dynamic.toml b/config/dynamic.toml new file mode 100644 index 000000000000..f275a7b26dc2 --- /dev/null +++ b/config/dynamic.toml @@ -0,0 +1,612 @@ +["Greenshift"] +name = "Greenshift" +min_pop = 0 +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." +ruleset_type_settings.roundstart.low = 0 +ruleset_type_settings.roundstart.high = 0 +ruleset_type_settings.roundstart.half_range_pop_threshold = 25 +ruleset_type_settings.roundstart.full_range_pop_threshold = 50 +ruleset_type_settings.light_midround.low = 0 +ruleset_type_settings.light_midround.high = 0 +ruleset_type_settings.light_midround.half_range_pop_threshold = 25 +ruleset_type_settings.light_midround.full_range_pop_threshold = 40 +ruleset_type_settings.light_midround.time_threshold = 30 +ruleset_type_settings.light_midround.execution_cooldown_low = 10 +ruleset_type_settings.light_midround.execution_cooldown_high = 20 +ruleset_type_settings.heavy_midround.low = 0 +ruleset_type_settings.heavy_midround.high = 0 +ruleset_type_settings.heavy_midround.half_range_pop_threshold = 25 +ruleset_type_settings.heavy_midround.full_range_pop_threshold = 40 +ruleset_type_settings.heavy_midround.time_threshold = 60 +ruleset_type_settings.heavy_midround.execution_cooldown_low = 10 +ruleset_type_settings.heavy_midround.execution_cooldown_high = 20 +ruleset_type_settings.latejoin.low = 0 +ruleset_type_settings.latejoin.high = 0 +ruleset_type_settings.latejoin.half_range_pop_threshold = 25 +ruleset_type_settings.latejoin.full_range_pop_threshold = 40 +ruleset_type_settings.latejoin.time_threshold = 0 +ruleset_type_settings.latejoin.execution_cooldown_low = 10 +ruleset_type_settings.latejoin.execution_cooldown_high = 20 + +["Low Chaos"] +name = "Low Chaos" +min_pop = 0 +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.roundstart.low = 1 +ruleset_type_settings.roundstart.high = 1 +ruleset_type_settings.roundstart.half_range_pop_threshold = 25 +ruleset_type_settings.roundstart.full_range_pop_threshold = 40 +ruleset_type_settings.light_midround.low = 0 +ruleset_type_settings.light_midround.high = 2 +ruleset_type_settings.light_midround.half_range_pop_threshold = 25 +ruleset_type_settings.light_midround.full_range_pop_threshold = 40 +ruleset_type_settings.light_midround.time_threshold = 30 +ruleset_type_settings.light_midround.execution_cooldown_low = 10 +ruleset_type_settings.light_midround.execution_cooldown_high = 20 +ruleset_type_settings.heavy_midround.low = 0 +ruleset_type_settings.heavy_midround.high = 1 +ruleset_type_settings.heavy_midround.half_range_pop_threshold = 25 +ruleset_type_settings.heavy_midround.full_range_pop_threshold = 40 +ruleset_type_settings.heavy_midround.time_threshold = 60 +ruleset_type_settings.heavy_midround.execution_cooldown_low = 10 +ruleset_type_settings.heavy_midround.execution_cooldown_high = 20 +ruleset_type_settings.latejoin.low = 0 +ruleset_type_settings.latejoin.high = 1 +ruleset_type_settings.latejoin.half_range_pop_threshold = 25 +ruleset_type_settings.latejoin.full_range_pop_threshold = 40 +ruleset_type_settings.latejoin.time_threshold = 5 +ruleset_type_settings.latejoin.execution_cooldown_low = 10 +ruleset_type_settings.latejoin.execution_cooldown_high = 20 + +["Low-Medium Chaos"] +name = "Low-Medium Chaos" +min_pop = 0 +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.roundstart.low = 1 +ruleset_type_settings.roundstart.high = 2 +ruleset_type_settings.roundstart.half_range_pop_threshold = 25 +ruleset_type_settings.roundstart.full_range_pop_threshold = 40 +ruleset_type_settings.light_midround.low = 0 +ruleset_type_settings.light_midround.high = 2 +ruleset_type_settings.light_midround.half_range_pop_threshold = 25 +ruleset_type_settings.light_midround.full_range_pop_threshold = 40 +ruleset_type_settings.light_midround.time_threshold = 30 +ruleset_type_settings.light_midround.execution_cooldown_low = 10 +ruleset_type_settings.light_midround.execution_cooldown_high = 20 +ruleset_type_settings.heavy_midround.low = 0 +ruleset_type_settings.heavy_midround.high = 1 +ruleset_type_settings.heavy_midround.half_range_pop_threshold = 25 +ruleset_type_settings.heavy_midround.full_range_pop_threshold = 40 +ruleset_type_settings.heavy_midround.time_threshold = 60 +ruleset_type_settings.heavy_midround.execution_cooldown_low = 10 +ruleset_type_settings.heavy_midround.execution_cooldown_high = 20 +ruleset_type_settings.latejoin.low = 1 +ruleset_type_settings.latejoin.high = 2 +ruleset_type_settings.latejoin.half_range_pop_threshold = 25 +ruleset_type_settings.latejoin.full_range_pop_threshold = 40 +ruleset_type_settings.latejoin.time_threshold = 5 +ruleset_type_settings.latejoin.execution_cooldown_low = 10 +ruleset_type_settings.latejoin.execution_cooldown_high = 20 + +["Medium-High Chaos"] +name = "Medium-High Chaos" +min_pop = 0 +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.roundstart.low = 2 +ruleset_type_settings.roundstart.high = 3 +ruleset_type_settings.roundstart.half_range_pop_threshold = 25 +ruleset_type_settings.roundstart.full_range_pop_threshold = 40 +ruleset_type_settings.light_midround.low = 1 +ruleset_type_settings.light_midround.high = 2 +ruleset_type_settings.light_midround.half_range_pop_threshold = 25 +ruleset_type_settings.light_midround.full_range_pop_threshold = 40 +ruleset_type_settings.light_midround.time_threshold = 30 +ruleset_type_settings.light_midround.execution_cooldown_low = 10 +ruleset_type_settings.light_midround.execution_cooldown_high = 20 +ruleset_type_settings.heavy_midround.low = 1 +ruleset_type_settings.heavy_midround.high = 2 +ruleset_type_settings.heavy_midround.half_range_pop_threshold = 25 +ruleset_type_settings.heavy_midround.full_range_pop_threshold = 40 +ruleset_type_settings.heavy_midround.time_threshold = 60 +ruleset_type_settings.heavy_midround.execution_cooldown_low = 10 +ruleset_type_settings.heavy_midround.execution_cooldown_high = 20 +ruleset_type_settings.latejoin.low = 1 +ruleset_type_settings.latejoin.high = 3 +ruleset_type_settings.latejoin.half_range_pop_threshold = 25 +ruleset_type_settings.latejoin.full_range_pop_threshold = 40 +ruleset_type_settings.latejoin.time_threshold = 5 +ruleset_type_settings.latejoin.execution_cooldown_low = 10 +ruleset_type_settings.latejoin.execution_cooldown_high = 20 + +["High Chaos"] +name = "High Chaos" +min_pop = 25 +weight = 10 +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.roundstart.low = 3 +ruleset_type_settings.roundstart.high = 4 +ruleset_type_settings.roundstart.half_range_pop_threshold = 25 +ruleset_type_settings.roundstart.full_range_pop_threshold = 40 +ruleset_type_settings.light_midround.low = 1 +ruleset_type_settings.light_midround.high = 2 +ruleset_type_settings.light_midround.half_range_pop_threshold = 25 +ruleset_type_settings.light_midround.full_range_pop_threshold = 40 +ruleset_type_settings.light_midround.time_threshold = 20 +ruleset_type_settings.light_midround.execution_cooldown_low = 10 +ruleset_type_settings.light_midround.execution_cooldown_high = 20 +ruleset_type_settings.heavy_midround.low = 2 +ruleset_type_settings.heavy_midround.high = 4 +ruleset_type_settings.heavy_midround.half_range_pop_threshold = 25 +ruleset_type_settings.heavy_midround.full_range_pop_threshold = 40 +ruleset_type_settings.heavy_midround.time_threshold = 30 +ruleset_type_settings.heavy_midround.execution_cooldown_low = 10 +ruleset_type_settings.heavy_midround.execution_cooldown_high = 20 +ruleset_type_settings.latejoin.low = 2 +ruleset_type_settings.latejoin.high = 3 +ruleset_type_settings.latejoin.half_range_pop_threshold = 25 +ruleset_type_settings.latejoin.full_range_pop_threshold = 40 +ruleset_type_settings.latejoin.time_threshold = 5 +ruleset_type_settings.latejoin.execution_cooldown_low = 10 +ruleset_type_settings.latejoin.execution_cooldown_high = 20 + +["Latejoin Traitor"] +weight = 10 +min_pop = 3 +blacklisted_roles = [ + "Head of Personnel", +] +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Latejoin Heretic"] +weight = 3 +min_pop = 30 +blacklisted_roles = [ + "Head of Personnel", +] +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Latejoin Changeling"] +weight = 3 +min_pop = 15 +blacklisted_roles = [ + "Head of Personnel", +] +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Latejoin Revolution"] +weight = 1 +min_pop = 30 +blacklisted_roles = [] +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Spiders"] +weight.1 = 0 +weight.2 = 0 +weight.3 = 1 +weight.4 = 2 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 0 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Light Pirates"] +weight = 3 +min_pop = 15 +blacklisted_roles = [] +min_antag_cap = 0 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Heavy Pirates"] +weight = 3 +min_pop = 25 +blacklisted_roles = [] +min_antag_cap = 0 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Midround Wizard"] +weight.1 = 0 +weight.2 = 0 +weight.3 = 1 +weight.4 = 2 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Midround Nukeops"] +weight.1 = 0 +weight.2 = 1 +weight.3 = 3 +weight.4 = 3 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap.denominator = 18 +min_antag_cap.offset = 1 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Midround Clownops"] +weight = 0 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap.denominator = 18 +min_antag_cap.offset = 1 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Blob"] +weight.1 = 0 +weight.2 = 1 +weight.3 = 3 +weight.4 = 3 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 3 +repeatable = 1 +minimum_required_age = 0 + +["Xenomorph"] +weight.1 = 0 +weight.2 = 1 +weight.3 = 5 +weight.4 = 5 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 2 +repeatable_weight_decrease = 3 +repeatable = 1 +minimum_required_age = 0 + +["Nightmare"] +weight = 5 +min_pop = 15 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Space Dragon"] +weight.1 = 0 +weight.2 = 3 +weight.3 = 5 +weight.4 = 5 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 3 +repeatable = 1 +minimum_required_age = 0 + +["Abductors"] +weight = 5 +min_pop = 20 +blacklisted_roles = [] +min_antag_cap = 2 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 3 +repeatable = 1 +minimum_required_age = 0 + +["Space Ninja"] +weight.1 = 0 +weight.2 = 0 +weight.3 = 1 +weight.4 = 2 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Revenant"] +weight = 5 +min_pop = 10 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Midround Changeling"] +weight = 5 +min_pop = 15 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Paradox Clone"] +weight = 5 +min_pop = 10 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Voidwalker"] +weight = 5 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Fugitives"] +weight = 3 +min_pop = 20 +blacklisted_roles = [] +min_antag_cap = 3 +max_antag_cap = 4 +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Morph"] +weight = 0 +min_pop = 0 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Slaughter Demon"] +weight = 0 +min_pop = 20 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Midround Traitor"] +weight = 10 +min_pop = 3 +blacklisted_roles = [ + "Head of Personnel", +] +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Midround Malfunctioning AI"] +weight.1 = 0 +weight.2 = 1 +weight.3 = 3 +weight.4 = 3 +min_pop = 30 +blacklisted_roles = [] +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Blob Infection"] +weight.1 = 0 +weight.2 = 1 +weight.3 = 3 +weight.4 = 3 +min_pop = 30 +blacklisted_roles = [] +repeatable_weight_decrease = 3 +repeatable = 1 +minimum_required_age = 0 + +["Midround Obsessed"] +weight.1 = 5 +weight.2 = 5 +weight.3 = 3 +weight.4 = 1 +min_pop = 5 +blacklisted_roles = [] +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Roundstart Traitor"] +weight = 10 +min_pop = 3 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap.denominator = 38 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Roundstart Malfunctioning AI"] +weight.1 = 0 +weight.2 = 1 +weight.3 = 3 +weight.4 = 3 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Roundstart Blood Brothers"] +weight = 5 +min_pop = 10 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap.denominator = 29 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Roundstart Changeling"] +weight = 3 +min_pop = 15 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap.denominator = 29 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Roundstart Heretics"] +weight = 3 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap.denominator = 24 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Roundstart Wizard"] +weight.1 = 0 +weight.2 = 0 +weight.3 = 1 +weight.4 = 2 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 1 +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Roundstart Blood Cult"] +weight.1 = 0 +weight.2 = 1 +weight.3 = 3 +weight.4 = 3 +min_pop = 30 +blacklisted_roles = [ + "Head of Personnel", +] +min_antag_cap.denominator = 20 +min_antag_cap.offset = 1 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Roundstart Nukeops"] +weight.1 = 0 +weight.2 = 1 +weight.3 = 3 +weight.4 = 3 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap.denominator = 18 +min_antag_cap.offset = 1 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Roundstart Clownops"] +weight = 0 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap.denominator = 18 +min_antag_cap.offset = 1 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Roundstart Revolution"] +weight.1 = 0 +weight.2 = 1 +weight.3 = 3 +weight.4 = 3 +min_pop = 30 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 3 +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Roundstart Spies"] +weight.1 = 0 +weight.2 = 1 +weight.3 = 3 +weight.4 = 3 +min_pop = 10 +blacklisted_roles = [] +min_antag_cap.denominator = 20 +min_antag_cap.offset = 1 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + +["Extended"] +weight = 0 +min_pop = 0 +blacklisted_roles = [] +min_antag_cap = 0 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Meteor"] +weight = 0 +min_pop = 0 +blacklisted_roles = [] +min_antag_cap = 0 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + +["Nations"] +weight = 0 +min_pop = 0 +blacklisted_roles = [] +min_antag_cap = 0 +# max_antag_cap = min_antag_cap +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 diff --git a/config/game_options.txt b/config/game_options.txt index ca298a397ceb..1ab258884828 100644 --- a/config/game_options.txt +++ b/config/game_options.txt @@ -81,11 +81,19 @@ ALERT_DELTA Destruction of the station is imminent. All crew are instructed to o ## The total weight of station goals possible for a round (allows multiple goals) STATION_GOAL_BUDGET 1 +## If TRUE / 1, station is raised to blue alert at roundstart. +## If FALSE / 0, station remains at green alert. +## Roundstart command report and greendshift announcements are unaffected. +ROUNDSTART_BLUE_ALERT 0 + ## GAME MODES ### -## Uncomment to not send a roundstart intercept report. Gamemodes may override this. +## Uncomment to not send a roundstart intercept report. #NO_INTERCEPT_REPORT +## Uncomment to not include dynamic info in the roundstart report. +NO_DYNAMIC_REPORT + ## Percent weight reductions for three of the most recent modes REPEATED_MODE_ADJUST 45 30 10 @@ -93,13 +101,6 @@ REPEATED_MODE_ADJUST 45 30 10 ## The amount of time it takes for the emergency shuttle to be called, from round start. SHUTTLE_REFUEL_DELAY 12000 -## Variables calculate how number of antagonists will scale to population. -## Used as (Antagonists = Population / Coeff) -## Set to 0 to disable scaling and use default numbers instead. -TRAITOR_SCALING_COEFF 6 -BROTHER_SCALING_COEFF 6 -CHANGELING_SCALING_COEFF 6 - ## Variables calculate how number of open security officer positions will scale to population. ## Used as (Officers = Population / Coeff) ## Set to 0 to disable scaling and use default numbers instead. diff --git a/config/logging.txt b/config/logging.txt index 2635197a7966..28dc901bb3d9 100644 --- a/config/logging.txt +++ b/config/logging.txt @@ -29,6 +29,9 @@ LOG_ECON ## log emotes LOG_EMOTE +## log ghost polling +LOG_GHOST_POLL + ## log game actions (start of round, results, etc.) LOG_GAME diff --git a/maplestation.dme b/maplestation.dme index d5deef8a1fa2..b1cd772c7226 100644 --- a/maplestation.dme +++ b/maplestation.dme @@ -88,7 +88,6 @@ #include "code\__DEFINES\door.dm" #include "code\__DEFINES\drone.dm" #include "code\__DEFINES\dye_keys.dm" -#include "code\__DEFINES\dynamic.dm" #include "code\__DEFINES\economy.dm" #include "code\__DEFINES\electrified_buckle.dm" #include "code\__DEFINES\events.dm" @@ -637,7 +636,6 @@ #include "code\controllers\subsystem\blackmarket.dm" #include "code\controllers\subsystem\chat.dm" #include "code\controllers\subsystem\circuit_component.dm" -#include "code\controllers\subsystem\communications.dm" #include "code\controllers\subsystem\dbcore.dm" #include "code\controllers\subsystem\dcs.dm" #include "code\controllers\subsystem\discord.dm" @@ -718,16 +716,15 @@ #include "code\controllers\subsystem\wardrobe.dm" #include "code\controllers\subsystem\weather.dm" #include "code\controllers\subsystem\wiremod_composite.dm" +#include "code\controllers\subsystem\dynamic\__dynamic_defines.dm" +#include "code\controllers\subsystem\dynamic\_dynamic_ruleset.dm" +#include "code\controllers\subsystem\dynamic\_dynamic_tier.dm" #include "code\controllers\subsystem\dynamic\dynamic.dm" -#include "code\controllers\subsystem\dynamic\dynamic_hijacking.dm" -#include "code\controllers\subsystem\dynamic\dynamic_logging.dm" -#include "code\controllers\subsystem\dynamic\dynamic_midround_rolling.dm" -#include "code\controllers\subsystem\dynamic\dynamic_rulesets.dm" -#include "code\controllers\subsystem\dynamic\dynamic_rulesets_latejoin.dm" -#include "code\controllers\subsystem\dynamic\dynamic_rulesets_midround.dm" -#include "code\controllers\subsystem\dynamic\dynamic_rulesets_roundstart.dm" -#include "code\controllers\subsystem\dynamic\dynamic_unfavorable_situation.dm" -#include "code\controllers\subsystem\dynamic\ruleset_picking.dm" +#include "code\controllers\subsystem\dynamic\dynamic_admin.dm" +#include "code\controllers\subsystem\dynamic\dynamic_ruleset_latejoin.dm" +#include "code\controllers\subsystem\dynamic\dynamic_ruleset_midround.dm" +#include "code\controllers\subsystem\dynamic\dynamic_ruleset_roundstart.dm" +#include "code\controllers\subsystem\dynamic\dynamic_testing.dm" #include "code\controllers\subsystem\movement\ai_movement.dm" #include "code\controllers\subsystem\movement\cliff_falling.dm" #include "code\controllers\subsystem\movement\hyperspace_drift.dm" @@ -780,6 +777,7 @@ #include "code\datums\chat_payload.dm" #include "code\datums\chatmessage.dm" #include "code\datums\cogbar.dm" +#include "code\datums\communications.dm" #include "code\datums\dash_weapon.dm" #include "code\datums\datum.dm" #include "code\datums\datumvars.dm" @@ -2723,7 +2721,6 @@ #include "code\game\objects\structures\spawner.dm" #include "code\game\objects\structures\spirit_board.dm" #include "code\game\objects\structures\stairs.dm" -#include "code\game\objects\structures\syndicate_uplink_beacon.dm" #include "code\game\objects\structures\table_frames.dm" #include "code\game\objects\structures\tables_racks.dm" #include "code\game\objects\structures\tank_dispenser.dm" @@ -3121,6 +3118,7 @@ #include "code\modules\antagonists\disease\disease_disease.dm" #include "code\modules\antagonists\disease\disease_mob.dm" #include "code\modules\antagonists\ert\ert.dm" +#include "code\modules\antagonists\evil_clone\evil_clone.dm" #include "code\modules\antagonists\fugitive\fugitive.dm" #include "code\modules\antagonists\fugitive\fugitive_equipment.dm" #include "code\modules\antagonists\fugitive\fugitive_outfits.dm" @@ -3239,11 +3237,13 @@ #include "code\modules\antagonists\ninja\ninja_stars.dm" #include "code\modules\antagonists\ninja\ninjaDrainAct.dm" #include "code\modules\antagonists\ninja\outfit.dm" -#include "code\modules\antagonists\nukeop\nukeop.dm" #include "code\modules\antagonists\nukeop\outfits.dm" +#include "code\modules\antagonists\nukeop\datums\operative.dm" +#include "code\modules\antagonists\nukeop\datums\operative_leader.dm" +#include "code\modules\antagonists\nukeop\datums\operative_lone.dm" +#include "code\modules\antagonists\nukeop\datums\operative_team.dm" #include "code\modules\antagonists\nukeop\equipment\borgchameleon.dm" #include "code\modules\antagonists\nukeop\equipment\nuclear_authentication_disk.dm" -#include "code\modules\antagonists\nukeop\equipment\nuclear_challenge.dm" #include "code\modules\antagonists\nukeop\equipment\pinpointer.dm" #include "code\modules\antagonists\nukeop\equipment\nuclear_bomb\_nuclear_bomb.dm" #include "code\modules\antagonists\nukeop\equipment\nuclear_bomb\beer_nuke.dm" @@ -3252,7 +3252,6 @@ #include "code\modules\antagonists\obsessed\obsessed.dm" #include "code\modules\antagonists\paradox_clone\paradox_clone.dm" #include "code\modules\antagonists\pirate\pirate.dm" -#include "code\modules\antagonists\pirate\pirate_event.dm" #include "code\modules\antagonists\pirate\pirate_gangs.dm" #include "code\modules\antagonists\pirate\pirate_outfits.dm" #include "code\modules\antagonists\pirate\pirate_roles.dm" @@ -3263,6 +3262,7 @@ #include "code\modules\antagonists\revenant\revenant_blight.dm" #include "code\modules\antagonists\revolution\enemy_of_the_state.dm" #include "code\modules\antagonists\revolution\revolution.dm" +#include "code\modules\antagonists\revolution\revolution_handler.dm" #include "code\modules\antagonists\santa\santa.dm" #include "code\modules\antagonists\sentient_creature\sentient_creature.dm" #include "code\modules\antagonists\separatist\nation_creation.dm" @@ -3276,39 +3276,13 @@ #include "code\modules\antagonists\spiders\spiders.dm" #include "code\modules\antagonists\survivalist\survivalist.dm" #include "code\modules\antagonists\syndicate_monkey\syndicate_monkey.dm" -#include "code\modules\antagonists\traitor\balance_helper.dm" #include "code\modules\antagonists\traitor\datum_traitor.dm" -#include "code\modules\antagonists\traitor\objective_category.dm" -#include "code\modules\antagonists\traitor\traitor_objective.dm" #include "code\modules\antagonists\traitor\uplink_handler.dm" #include "code\modules\antagonists\traitor\components\demoraliser.dm" -#include "code\modules\antagonists\traitor\components\traitor_objective_helpers.dm" -#include "code\modules\antagonists\traitor\components\traitor_objective_limit_per_time.dm" -#include "code\modules\antagonists\traitor\components\traitor_objective_mind_tracker.dm" #include "code\modules\antagonists\traitor\contractor\contract_teammate.dm" #include "code\modules\antagonists\traitor\contractor\contractor_hub.dm" #include "code\modules\antagonists\traitor\contractor\contractor_items.dm" #include "code\modules\antagonists\traitor\contractor\syndicate_contract.dm" -#include "code\modules\antagonists\traitor\objectives\assassination.dm" -#include "code\modules\antagonists\traitor\objectives\demoralise_assault.dm" -#include "code\modules\antagonists\traitor\objectives\destroy_heirloom.dm" -#include "code\modules\antagonists\traitor\objectives\destroy_item.dm" -#include "code\modules\antagonists\traitor\objectives\eyesnatching.dm" -#include "code\modules\antagonists\traitor\objectives\hack_comm_console.dm" -#include "code\modules\antagonists\traitor\objectives\infect.dm" -#include "code\modules\antagonists\traitor\objectives\kidnapping.dm" -#include "code\modules\antagonists\traitor\objectives\kill_pet.dm" -#include "code\modules\antagonists\traitor\objectives\locate_weakpoint.dm" -#include "code\modules\antagonists\traitor\objectives\sabotage_machinery.dm" -#include "code\modules\antagonists\traitor\objectives\steal.dm" -#include "code\modules\antagonists\traitor\objectives\abstract\target_player.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\battlecruiser.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\final_objective.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\infect_ai.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\objective_dark_matteor.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\romerol.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\space_dragon.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\supermatter_cascade.dm" #include "code\modules\antagonists\valentines\heartbreaker.dm" #include "code\modules\antagonists\valentines\valentine.dm" #include "code\modules\antagonists\venus_human_trap\venus_human_trap.dm" @@ -3990,7 +3964,6 @@ #include "code\modules\events\camerafailure.dm" #include "code\modules\events\carp_migration.dm" #include "code\modules\events\communications_blackout.dm" -#include "code\modules\events\creep_awakening.dm" #include "code\modules\events\disease_outbreak.dm" #include "code\modules\events\dust.dm" #include "code\modules\events\earthquake.dm" @@ -4013,7 +3986,6 @@ #include "code\modules\events\scrubber_overflow.dm" #include "code\modules\events\shuttle_catastrophe.dm" #include "code\modules\events\shuttle_insurance.dm" -#include "code\modules\events\spider_infestation.dm" #include "code\modules\events\stray_cargo.dm" #include "code\modules\events\supermatter_surge.dm" #include "code\modules\events\tram_malfunction.dm" @@ -4032,20 +4004,9 @@ #include "code\modules\events\anomaly\anomaly_pyro.dm" #include "code\modules\events\anomaly\anomaly_vortex.dm" #include "code\modules\events\ghost_role\_ghost_role.dm" -#include "code\modules\events\ghost_role\abductor.dm" -#include "code\modules\events\ghost_role\alien_infestation.dm" -#include "code\modules\events\ghost_role\blob.dm" -#include "code\modules\events\ghost_role\changeling_event.dm" -#include "code\modules\events\ghost_role\fugitive_event.dm" -#include "code\modules\events\ghost_role\morph_event.dm" -#include "code\modules\events\ghost_role\nightmare.dm" #include "code\modules\events\ghost_role\operative.dm" -#include "code\modules\events\ghost_role\revenant_event.dm" #include "code\modules\events\ghost_role\sentience.dm" #include "code\modules\events\ghost_role\sentient_disease.dm" -#include "code\modules\events\ghost_role\slaughter_event.dm" -#include "code\modules\events\ghost_role\space_dragon.dm" -#include "code\modules\events\ghost_role\space_ninja.dm" #include "code\modules\events\holiday\easter.dm" #include "code\modules\events\holiday\halloween.dm" #include "code\modules\events\holiday\vday.dm" @@ -4356,7 +4317,6 @@ #include "code\modules\jobs\job_types\antagonists\abductor_agent.dm" #include "code\modules\jobs\job_types\antagonists\abductor_scientist.dm" #include "code\modules\jobs\job_types\antagonists\abductor_solo.dm" -#include "code\modules\jobs\job_types\antagonists\clown_operative.dm" #include "code\modules\jobs\job_types\antagonists\lone_operative.dm" #include "code\modules\jobs\job_types\antagonists\morph.dm" #include "code\modules\jobs\job_types\antagonists\nightmare.dm" @@ -4410,7 +4370,6 @@ #include "code\modules\jobs\job_types\spawner\syndicate_cybersun_captain.dm" #include "code\modules\jobs\job_types\spawner\venus_human_trap.dm" #include "code\modules\jobs\job_types\spawner\zombie.dm" -#include "code\modules\jobs\job_types\station_trait\bridge_assistant.dm" #include "code\modules\jobs\job_types\station_trait\cargo_gorilla.dm" #include "code\modules\jobs\job_types\station_trait\veteran_advisor.dm" #include "code\modules\keybindings\bindings_atom.dm" @@ -4593,6 +4552,7 @@ #include "code\modules\mapping\space_management\zlevel_manager.dm" #include "code\modules\meteors\meteor_changeling.dm" #include "code\modules\meteors\meteor_dark_matteor.dm" +#include "code\modules\meteors\meteor_mode_controller.dm" #include "code\modules\meteors\meteor_spawning.dm" #include "code\modules\meteors\meteor_types.dm" #include "code\modules\meteors\meteor_waves.dm" @@ -5786,7 +5746,6 @@ #include "code\modules\shuttle\shuttle_events\carp.dm" #include "code\modules\shuttle\shuttle_events\meteors.dm" #include "code\modules\shuttle\shuttle_events\misc.dm" -#include "code\modules\shuttle\shuttle_events\player_controlled.dm" #include "code\modules\shuttle\shuttle_events\turbulence.dm" #include "code\modules\spatial_grid\cell_tracker.dm" #include "code\modules\spells\spell.dm" @@ -6499,10 +6458,6 @@ #include "maplestation_modules\code\modules\antagonists\advanced_ling\changeling_rebalancing.dm" #include "maplestation_modules\code\modules\antagonists\advanced_ling\changeling_stings.dm" #include "maplestation_modules\code\modules\antagonists\advanced_ling\neutered_ling.dm" -#include "maplestation_modules\code\modules\antagonists\advanced_traitor\advanced_malf.dm" -#include "maplestation_modules\code\modules\antagonists\advanced_traitor\advanced_traitor.dm" -#include "maplestation_modules\code\modules\antagonists\infiltrator\advanced_infiltrator.dm" -#include "maplestation_modules\code\modules\antagonists\infiltrator\infiltrator.dm" #include "maplestation_modules\code\modules\art\statuettes.dm" #include "maplestation_modules\code\modules\cargo\costumes_toys.dm" #include "maplestation_modules\code\modules\cargo\goodies.dm" diff --git a/maplestation_modules/code/datums/votes/chaos.dm b/maplestation_modules/code/datums/votes/chaos.dm index dcb8881565be..23009727a91e 100644 --- a/maplestation_modules/code/datums/votes/chaos.dm +++ b/maplestation_modules/code/datums/votes/chaos.dm @@ -10,6 +10,11 @@ count_method = VOTE_COUNT_METHOD_MULTI display_statistics = FALSE +/datum/vote/round_chaos/initiate_vote(initiator, duration) + . = ..() + . += "
" + . += span_slightly_smaller("This vote is used to gauge player sentiment and has no mechanical effect.") + /datum/vote/round_chaos/can_mob_vote(mob/voter) // Roundstart observers / people who DNR'd have no say if(isobserver(voter) && isnull(voter.mind)) @@ -25,7 +30,15 @@ return ..() /datum/vote/round_chaos/tiebreaker(list/winners) - return jointext(winners, "-") + switch(length(winners)) + if(2) + return jointext(winners, "-") + if(3) + return winners[2] // averaged out + if(4) + return "Any" + + return "Unknown" /datum/vote/round_chaos/finalize_vote(winning_option) SSticker.voted_round_chaos = winning_option diff --git a/maplestation_modules/code/datums/votes/transfer_vote.dm b/maplestation_modules/code/datums/votes/transfer_vote.dm index 22f251315715..382bd04e08f9 100644 --- a/maplestation_modules/code/datums/votes/transfer_vote.dm +++ b/maplestation_modules/code/datums/votes/transfer_vote.dm @@ -9,6 +9,11 @@ CHOICE_CONTINUE, ) +/datum/vote/autotransfer/initiate_vote(initiator, duration) + . = ..() + . += "
" + . += span_slightly_smaller("This vote is OOC. On a success, a transfer shuttle will be called.") + /datum/vote/autotransfer/toggle_votable() CONFIG_SET(flag/allow_vote_transfer, !CONFIG_GET(flag/allow_vote_transfer)) @@ -23,10 +28,7 @@ if(!forced && !CONFIG_GET(flag/allow_vote_transfer)) return "Transfer votes are disabled." - if(!SScrewtransfer) - return "Transfer subsystem missing. Can't really host a vote for it! This is a bug." - - if(SScrewtransfer.transfer_vote_successful) + if(SScrewtransfer?.transfer_vote_successful) return "A transfer vote has already passed." return VOTE_AVAILABLE diff --git a/maplestation_modules/code/modules/antagonists/advanced_cult/advanced_cult.dm b/maplestation_modules/code/modules/antagonists/advanced_cult/advanced_cult.dm index 296e2c5d0897..a41eb9816cba 100644 --- a/maplestation_modules/code/modules/antagonists/advanced_cult/advanced_cult.dm +++ b/maplestation_modules/code/modules/antagonists/advanced_cult/advanced_cult.dm @@ -2,7 +2,7 @@ /datum/antagonist/advanced_cult ui_name = null show_in_antagpanel = FALSE - job_rank = ROLE_CULTIST + pref_flag = ROLE_CULTIST roundend_category = "cultists" antagpanel_category = "Cult" suicide_cry = "FOR MY GOD!!" @@ -41,7 +41,6 @@ /datum/antagonist/advanced_cult/finalize_antag() cultist_style.on_cultist_made(src, owner.current) - owner.special_role = ROLE_CULTIST /datum/antagonist/advanced_cult/on_removal() cultist_style.on_cultist_lost(src, owner.current) diff --git a/maplestation_modules/code/modules/antagonists/advanced_traitor/advanced_malf.dm b/maplestation_modules/code/modules/antagonists/advanced_traitor/advanced_malf.dm deleted file mode 100644 index 577b7f64db47..000000000000 --- a/maplestation_modules/code/modules/antagonists/advanced_traitor/advanced_malf.dm +++ /dev/null @@ -1,113 +0,0 @@ -/// -- Advanced Antag for Malf AIs. -- -/// Proc to give the malf their hacked module. -/datum/antagonist/malf_ai/finalize_antag() - if(give_objectives) - if(!employer) - employer = pick(GLOB.ai_employers) - malfunction_flavor = strings(MALFUNCTION_FLAVOR_FILE, employer) - - add_law_zero() - owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/malf.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE) - owner.current.grant_language(/datum/language/codespeak, source = LANGUAGE_MALF) - -/datum/antagonist/malf_ai/apply_innate_effects(mob/living/mob_override) - . = ..() - var/datum/atom_hud/data/hackyhud = GLOB.huds[DATA_HUD_MALF_APC] - hackyhud.show_to(mob_override || owner.current) - -/datum/antagonist/malf_ai/remove_innate_effects(mob/living/mob_override) - . = ..() - var/datum/atom_hud/data/hackyhud = GLOB.huds[DATA_HUD_MALF_APC] - hackyhud.hide_from(mob_override || owner.current) - -/// The Advanced Malf datum. -/datum/antagonist/malf_ai/advanced - name = "Advanced Malfunctioning AI" - ui_name = null - employer = "The Syndicate" - give_objectives = FALSE - should_give_codewords = FALSE - finalize_antag = FALSE - /// List of objectives AIs can get in addition to the base ones - var/static/list/ai_objectives = list( - "no organics on shuttle" = /datum/objective/block, - "no mutants on shuttle" = /datum/objective/purge, - "robot army" = /datum/objective/robot_army, - "survive AI" = /datum/objective/survive/malf, - ) - /// Goals that advanced malf AIs shouldn't be able to pick - var/static/list/blacklisted_ai_objectives = list( - "survive", - "destroy AI", - "download", - "steal", - "escape", - "debrain" - ) - -/datum/antagonist/malf_ai/advanced/on_gain() - if(!GLOB.admin_objective_list) - generate_admin_objective_list() - - var/list/objectives_to_choose = GLOB.admin_objective_list.Copy() - objectives_to_choose -= blacklisted_similar_objectives - objectives_to_choose -= blacklisted_ai_objectives - objectives_to_choose += ai_objectives - name = "Malfunctioning AI" - - linked_advanced_datum = new /datum/advanced_antag_datum/malf_ai(src) - linked_advanced_datum.setup_advanced_antag() - linked_advanced_datum.possible_objectives = objectives_to_choose - return ..() - -/datum/antagonist/malf_ai/advanced/greet() - linked_advanced_datum.greet_message(owner.current) - -/datum/antagonist/malf_ai/advanced/roundend_report() - var/list/result = list() - - result += printplayer(owner) - result += "[owner] was a/an [linked_advanced_datum.name][employer? " hacked by [employer]":""]." - if(linked_advanced_datum.backstory) - result += "[owner]'s backstory was the following:
[linked_advanced_datum.backstory]" - - if(LAZYLEN(linked_advanced_datum.our_goals)) - result += "[owner]'s objectives:" - var/count = 1 - for(var/datum/advanced_antag_goal/goal as anything in linked_advanced_datum.our_goals) - result += goal.get_roundend_text(count++) - - return result.Join("
") - -/datum/antagonist/malf_ai/advanced/roundend_report_footer() - return "
And thus ends another security breach on board [station_name()]." - -/// The advanced antag datum itself for malf AIs. -/datum/advanced_antag_datum/malf_ai - name = "Advanced Malfunctioning AI" - employer = "The Syndicate" - starting_points = 20 - -/datum/advanced_antag_datum/malf_ai/modify_antag_points() - var/mob/living/silicon/ai/traitor_ai = linked_antagonist.owner.current - if(!istype(traitor_ai)) - CRASH("Advanced Malf AI datum on a mob that isn't an AI!") - - var/datum/module_picker/traitor_ai_uplink = traitor_ai.malf_picker - starting_points = get_antag_points_from_goals() - traitor_ai_uplink.processing_time = starting_points - -/datum/advanced_antag_datum/malf_ai/get_antag_points_from_goals() - var/finalized_starting_points = ADV_TRAITOR_INITIAL_MALF_POINTS - for(var/datum/advanced_antag_goal/goal as anything in our_goals) - finalized_starting_points += (goal.intensity * ADV_TRAITOR_MALF_POINTS_PER_INTENSITY) - - return min(finalized_starting_points, ADV_TRAITOR_MAX_MALF_POINTS) - -/datum/advanced_antag_datum/malf_ai/get_finalize_text() - return "Finalizing will begin installlation of your malfunction module with [get_antag_points_from_goals()] processing power. You can still edit your goals after finalizing!" - -/datum/advanced_antag_datum/malf_ai/set_employer(employer) - . = ..() - var/datum/antagonist/malf_ai/our_ai = linked_antagonist - our_ai.employer = src.employer diff --git a/maplestation_modules/code/modules/antagonists/advanced_traitor/advanced_traitor.dm b/maplestation_modules/code/modules/antagonists/advanced_traitor/advanced_traitor.dm deleted file mode 100644 index 19610747d90d..000000000000 --- a/maplestation_modules/code/modules/antagonists/advanced_traitor/advanced_traitor.dm +++ /dev/null @@ -1,177 +0,0 @@ -/// -- "Traitor plus" or "Advanced traitor" - a traitor that is able to set their own goals and objectives when in game. -- -/// Loosely based on the ambitions system from skyrat, but made less bad. - -/// Proc to give the traitor their uplink and play the sound. -/datum/antagonist/traitor/finalize_antag() - if(!linked_advanced_datum) - pick_employer() - traitor_flavor = strings(TRAITOR_FLAVOR_FILE, employer) - - if(give_uplink || linked_advanced_datum?.finalized) - owner.give_uplink(silent = FALSE, antag_datum = src) - generate_replacement_codes() - handle_uplink() - owner.teach_crafting_recipe(/datum/crafting_recipe/syndicate_uplink_beacon) - - if(give_objectives) - forge_traitor_objectives() - forge_ending_objective() - - owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/tatoralert.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE) - -/// Proc to handled the uplink items and the uplink handler after an uplink is given. -/datum/antagonist/traitor/proc/handle_uplink() - var/datum/component/uplink/uplink = owner.find_syndicate_uplink() - uplink_ref = WEAKREF(uplink) - if(!uplink) - return - - if(uplink_handler) - uplink.uplink_handler = uplink_handler - else - uplink_handler = uplink.uplink_handler - - uplink_handler.uplink_flag = uplink_flag_given - uplink_handler.primary_objectives = objectives - - if(!linked_advanced_datum) - uplink_handler.has_progression = TRUE - SStraitor.register_uplink_handler(uplink_handler) - - if(give_secondary_objectives) - uplink_handler.has_objectives = TRUE - uplink_handler.generate_objectives() - - uplink_handler.can_replace_objectives = CALLBACK(src, PROC_REF(can_change_objectives)) - uplink_handler.replace_objectives = CALLBACK(src, PROC_REF(submit_player_objective)) - - if(uplink_handler.progression_points < SStraitor.current_global_progression) - uplink_handler.progression_points = SStraitor.current_global_progression * SStraitor.newjoin_progression_coeff - - var/list/uplink_items = list() - for(var/datum/uplink_item/item as anything in SStraitor.uplink_items) - if(item.item && !item.cant_discount && (item.purchasable_from & uplink_handler.uplink_flag) && item.cost > 1) - if(!length(item.restricted_roles) && !length(item.restricted_species)) - uplink_items += item - continue - if((uplink_handler.assigned_role in item.restricted_roles) || (uplink_handler.assigned_species in item.restricted_species)) - uplink_items += item - continue - uplink_handler.extra_purchasable += create_uplink_sales(uplink_sale_count, /datum/uplink_category/discounts, 1, uplink_items) - -/// The Advanced Traitor antagonist datum. -/datum/antagonist/traitor/advanced - name = "Advanced Traitor" // Changed to just "Traitor" on spawn, but can be changed by the player. - ui_name = null // We have our own UI - hijack_speed = 0.5 // Edited to ([starting_tc] / 40). - employer = "The Syndicate" // Can be changed by the player. - give_objectives = FALSE - should_give_codewords = FALSE - give_uplink = FALSE - finalize_antag = FALSE - can_assign_self_objectives = FALSE - /// List of objectives traitors can get in addition to the base ones - var/static/list/traitor_objectives = list( - "exile" = /datum/objective/exile, - ) - /// Typepath of what advanced antag datum gets instantiated to this antag. - var/advanced_antag_path = /datum/advanced_antag_datum/traitor - -/datum/antagonist/traitor/advanced/New(give_objectives = FALSE) - . = ..() - src.give_objectives = FALSE - -/datum/antagonist/traitor/advanced/on_gain() - if(!GLOB.admin_objective_list) - generate_admin_objective_list() - - var/list/objectives_to_choose = GLOB.admin_objective_list.Copy() - objectives_to_choose -= blacklisted_similar_objectives - objectives_to_choose += traitor_objectives - - set_name_on_add() - - linked_advanced_datum = new advanced_antag_path(src) - linked_advanced_datum.setup_advanced_antag() - linked_advanced_datum.possible_objectives = objectives_to_choose - return ..() - -/datum/antagonist/traitor/advanced/proc/set_name_on_add() - name = "Traitor" - -/// Greet the antag with big menacing text. -/datum/antagonist/traitor/advanced/greet() - linked_advanced_datum.greet_message(owner.current) - -/datum/antagonist/traitor/advanced/roundend_report() - var/list/result = list() - - result += printplayer(owner) - result += "[owner] was a/an [linked_advanced_datum.name][employer? " employed by [employer]":""]." - if(linked_advanced_datum.backstory) - result += "[owner]'s backstory was the following:
[linked_advanced_datum.backstory]" - - var/TC_uses = 0 - var/uplink_true = FALSE - var/purchases = "" - - if(linked_advanced_datum.finalized) - LAZYINITLIST(GLOB.uplink_purchase_logs_by_key) - var/datum/uplink_purchase_log/log = GLOB.uplink_purchase_logs_by_key[owner.key] - if(log) - uplink_true = TRUE - TC_uses = log.total_spent - purchases += log.generate_render(FALSE) - - if(LAZYLEN(linked_advanced_datum.our_goals)) - result += "[owner]'s objectives:" - var/count = 1 - for(var/datum/advanced_antag_goal/goal as anything in linked_advanced_datum.our_goals) - result += goal.get_roundend_text(count++) - if(linked_advanced_datum.finalized) - result += "
They were afforded [linked_advanced_datum.starting_points] tc to accomplish these tasks." - - if(uplink_true && linked_advanced_datum.finalized) - var/uplink_text = span_bold("(used [TC_uses] TC)") - uplink_text += "[purchases]" - result += uplink_text - else - result += span_bold("
The [name] never obtained their uplink!") - - return result.Join("
") - -/datum/antagonist/traitor/advanced/roundend_report_footer() - return "
And thus ends another story on board [station_name()]." - -/// The advanced antag datum for traitor. -/datum/advanced_antag_datum/traitor - name = "Advanced Traitor" - employer = "The Syndicate" - starting_points = 8 - /// Hijack speed = (starting telecrystals * this modifier) - var/hijack_speed_modifier = 0.025 - -/datum/advanced_antag_datum/traitor/modify_antag_points() - var/datum/component/uplink/made_uplink = linked_antagonist.owner.find_syndicate_uplink() - if(!made_uplink) - return - - starting_points = get_antag_points_from_goals() - made_uplink.uplink_handler.telecrystals = starting_points - made_uplink.uplink_handler.progression_points = 1000 * (starting_points + 1) - linked_antagonist.hijack_speed = (starting_points * hijack_speed_modifier) // 20 tc traitor = 0.5 (default traitor hijack speed) - -/datum/advanced_antag_datum/traitor/get_antag_points_from_goals() - var/finalized_starting_tc = ADV_TRAITOR_INITIAL_TC - for(var/datum/advanced_antag_goal/goal as anything in our_goals) - finalized_starting_tc += (goal.intensity * ADV_TRAITOR_TC_PER_INTENSITY) - - return min(finalized_starting_tc, ADV_TRAITOR_MAX_TC) - -/datum/advanced_antag_datum/traitor/get_finalize_text() - return "Finalizing will send you your uplink to your preferred location with [get_antag_points_from_goals()] telecrystals. You can still edit your goals after finalizing!" - -/datum/advanced_antag_datum/traitor/set_employer(employer) - . = ..() - var/datum/antagonist/traitor/our_traitor = linked_antagonist - our_traitor.employer = src.employer diff --git a/maplestation_modules/code/modules/antagonists/infiltrator/advanced_infiltrator.dm b/maplestation_modules/code/modules/antagonists/infiltrator/advanced_infiltrator.dm deleted file mode 100644 index bea7c5a48b39..000000000000 --- a/maplestation_modules/code/modules/antagonists/infiltrator/advanced_infiltrator.dm +++ /dev/null @@ -1,26 +0,0 @@ -/// -- Advanced Antag Datum for Infiltrator. -- -/datum/advanced_antag_datum/traitor/infiltrator - name = "Advanced Infiltrator" - hijack_speed_modifier = 0.05 - -/datum/advanced_antag_datum/traitor/infiltrator/get_finalize_text() - return "Finalizing will grant you an uplink implant with [get_antag_points_from_goals()] telecrystals. You can still edit your goals after finalizing!" - -/datum/advanced_antag_datum/traitor/infiltrator/greet_message(mob/antagonist) - to_chat(antagonist, span_alertsyndie("You are an [name]!")) - antagonist.playsound_local(get_turf(antagonist), 'maplestation_modules/sound/radiodrum.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE) - addtimer(CALLBACK(src, PROC_REF(greet_message_two), antagonist), 3 SECONDS) - -/datum/advanced_antag_datum/traitor/infiltrator/greet_message_two(mob/antagonist) - to_chat(antagonist, span_danger("You are an agent sent to infiltrate [station_name()]! You can set your goals to whatever you think would make an interesting story or round. You have access to your goal panel via verb in your IC tab.")) - addtimer(CALLBACK(src, PROC_REF(greet_message_three), antagonist), 3 SECONDS) - -/datum/advanced_antag_datum/traitor/infiltrator/podspawn - name = "Advanced Infiltrator (Pod spawn)" - -/datum/advanced_antag_datum/traitor/infiltrator/podspawn/get_finalize_text() - return "Finalizing will grant you an uplink implant with [get_antag_points_from_goals()] telecrystals, and will drop pod you into a random maintenance room on the station. You can still edit your goals after finalizing!" - -/datum/advanced_antag_datum/traitor/infiltrator/podspawn/greet_message_two(mob/antagonist) - to_chat(antagonist, span_danger("You are an agent preparing to infiltrate [station_name()]! You can set your goals to whatever you think would make an interesting story or round. Finalizing your goals will drop you into a random maintenance room on the station. You have access to your goal panel via verb in your IC tab.")) - addtimer(CALLBACK(src, PROC_REF(greet_message_three), antagonist), 3 SECONDS) diff --git a/maplestation_modules/code/modules/antagonists/infiltrator/infiltrator.dm b/maplestation_modules/code/modules/antagonists/infiltrator/infiltrator.dm deleted file mode 100644 index f4450ea84c69..000000000000 --- a/maplestation_modules/code/modules/antagonists/infiltrator/infiltrator.dm +++ /dev/null @@ -1,175 +0,0 @@ -/// -- Infiltrator antag. Advanced traitors but they get some nukops gear in their uplink. -- -/datum/antagonist/traitor/advanced/intiltrator - name = "Infiltrator" - ui_name = null - hijack_speed = 1 - advanced_antag_path = /datum/advanced_antag_datum/traitor/infiltrator - antag_hud_name = "synd" - -/datum/antagonist/traitor/advanced/intiltrator/on_gain() - equip_infiltrator_outfit() - return ..() - -/datum/antagonist/traitor/advanced/intiltrator/set_name_on_add() - name = "Infiltrator" - -/datum/antagonist/traitor/advanced/intiltrator/apply_innate_effects(mob/living/mob_override) - var/mob/living/living_antag = mob_override || owner.current - add_team_hud(living_antag) - living_antag.faction |= ROLE_SYNDICATE - owner.set_assigned_role(SSjob.GetJobType(/datum/job/infiltrator)) - owner.special_role = ROLE_INFILTRATOR - -/datum/antagonist/traitor/advanced/intiltrator/remove_innate_effects(mob/living/mob_override) - var/mob/living/living_antag = mob_override || owner.current - living_antag.faction -= ROLE_SYNDICATE - owner.set_assigned_role(SSjob.GetJobType(/datum/job/unassigned)) - owner.special_role = null - -/datum/antagonist/traitor/advanced/intiltrator/on_removal() - var/obj/item/implant/uplink/infiltrator/infiltrator_implant = locate() in owner.current - var/obj/item/implant/weapons_auth/weapons_implant = locate() in owner.current - if(infiltrator_implant) - to_chat(owner.current, span_danger("You hear a whirring in your ear as your [infiltrator_implant.name] deactivates and becomes non-functional!")) - qdel(infiltrator_implant) - if(weapons_implant) - to_chat(owner.current, span_danger("You hear a click in your [prob(50) ? "right" : "left"] arm as your [weapons_implant.name] deactivates and becomes non-functional!")) - qdel(weapons_implant) - - var/obj/item/organ/brain/their_brain = owner.current.get_organ_slot(ORGAN_SLOT_BRAIN) - if(their_brain) - var/obj/item/skillchip/disk_verifier/disky_chip = locate() in their_brain - if(disky_chip) - their_brain.remove_skillchip(disky_chip) - qdel(disky_chip) - - return ..() - -/datum/antagonist/traitor/advanced/intiltrator/roundend_report() - var/list/result = list() - - result += printplayer(owner) - result += "[owner] was a/an [linked_advanced_datum.name], sent to infiltrate [station_name()][employer? ", employed by [employer]":""]." - if(linked_advanced_datum.backstory) - result += "[owner]'s backstory was the following:
[linked_advanced_datum.backstory]" - - var/TC_uses = 0 - var/uplink_true = FALSE - var/purchases = "" - - if(linked_advanced_datum.finalized) - LAZYINITLIST(GLOB.uplink_purchase_logs_by_key) - var/datum/uplink_purchase_log/log = GLOB.uplink_purchase_logs_by_key[owner.key] - if(log) - uplink_true = TRUE - TC_uses = log.total_spent - purchases += log.generate_render(FALSE) - - if(LAZYLEN(linked_advanced_datum.our_goals)) - result += "[owner]'s objectives:" - var/count = 1 - for(var/datum/advanced_antag_goal/goal as anything in linked_advanced_datum.our_goals) - result += goal.get_roundend_text(count++) - if(linked_advanced_datum.finalized) - result += "
They were afforded [linked_advanced_datum.starting_points] tc to accomplish these tasks." - - if(uplink_true && linked_advanced_datum.finalized) - var/uplink_text = span_bold("(used [TC_uses] TC)") - uplink_text += "[purchases]" - result += uplink_text - else - result += span_bold("
The [name] never obtained their uplink!") - - return result.Join("
") - -/datum/antagonist/traitor/advanced/intiltrator/roundend_report_footer() - return "
And thus ends another attempted Syndicate infiltration on board [station_name()]." - -/datum/antagonist/traitor/advanced/intiltrator/finalize_antag() - var/mob/living/carbon/human/traitor_mob = owner.current - if (!istype(traitor_mob)) - return - - var/obj/item/implant/uplink/infiltrator/infiltrator_implant = new() - var/obj/item/implant/weapons_auth/weapons_implant = new() - infiltrator_implant.implant(traitor_mob, traitor_mob, TRUE, TRUE) - weapons_implant.implant(traitor_mob, traitor_mob, TRUE, TRUE) - if(!silent) - to_chat(traitor_mob, span_boldnotice("[employer] has cunningly implanted you with an [infiltrator_implant.name] to assist in your infiltration. You can trigger the uplink to stealthily access it.")) - to_chat(traitor_mob, span_boldnotice("[employer] has wisely implanted you with a [weapons_implant.name] to allow you to use syndicate weaponry. You can now fire weapons with Syndicate firing pins.")) - - // MELBERT TODO; Fix this upstream - var/datum/component/uplink/made = infiltrator_implant.GetComponent(/datum/component/uplink) - made?.uplink_handler?.uplink_flag = infiltrator_implant.uplink_flag - - handle_uplink() - owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/tatoralert.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE) - -/datum/antagonist/traitor/advanced/intiltrator/proc/equip_infiltrator_outfit(strip = FALSE) - if(!ishuman(owner.current)) - return FALSE - var/mob/living/carbon/human/human_current = owner.current - if(strip) - human_current.delete_equipment() - human_current.equipOutfit(/datum/outfit/syndicate_infiltrator) - return TRUE - -/datum/antagonist/traitor/advanced/intiltrator/pod_spawn - name = "Infiltrator (Pod spawn)" - advanced_antag_path = /datum/advanced_antag_datum/traitor/infiltrator/podspawn - -/datum/antagonist/traitor/advanced/intiltrator/pod_spawn/finalize_antag() - . = ..() - SStgui.close_user_uis(owner, linked_advanced_datum) - if(!spawn_infiltrator_pod(owner.current, silent)) - message_admins("Cannot pod-spawn [owner.current] as infiltrator - they have not been launched anywhere. Consider sending them via pod manually.") - -/datum/antagonist/traitor/advanced/intiltrator/pod_spawn/proc/spawn_infiltrator_pod(mob/living/infiltrator, silent) - if(!istype(infiltrator)) - return FALSE - - // Spawns us somewhere in maintenance via drop pod. - var/turf/picked_turf = find_maintenance_spawn(TRUE) - if(isnull(picked_turf)) - return FALSE - - var/obj/structure/closet/supplypod/infiltrator_pod = new(null, /datum/pod_style/syndicate) - infiltrator_pod.explosionSize = list(0, 0, 1, 1) - infiltrator_pod.bluespace = TRUE - - var/turf/randomized_picked_turf = find_obstruction_free_location(3, picked_turf) || picked_turf - - if(!silent) - to_chat(infiltrator, span_alertwarning("\nYou are being deployed via drop pod into [get_area_name(randomized_picked_turf, TRUE)] to begin your infiltration of [station_name()].")) - infiltrator.forceMove(infiltrator_pod) - return new /obj/effect/pod_landingzone(randomized_picked_turf, infiltrator_pod) - -/// infiltrator uplink implant. -/obj/item/implant/uplink/infiltrator - name = "infiltrator uplink implant" - uplink_flag = UPLINK_INFILTRATOR - -/datum/outfit/syndicate_infiltrator - name = "Syndicate Infiltrator" - - uniform = /obj/item/clothing/under/syndicate - shoes = /obj/item/clothing/shoes/combat - gloves = /obj/item/clothing/gloves/color/black - back = /obj/item/storage/backpack/fireproof - ears = /obj/item/radio/headset - id = /obj/item/card/id/advanced/black - id_trim = /datum/id_trim/syndicom/infiltrator - skillchips = list(/obj/item/skillchip/disk_verifier) - backpack_contents = list(/obj/item/storage/box/survival/syndie = 1, /obj/item/knife/combat/survival) - -/datum/outfit/syndicate_infiltrator/post_equip(mob/living/carbon/human/human_equipper, visualsOnly) - . = ..() - var/obj/item/card/id/worn_id = human_equipper.wear_id - if(worn_id) - worn_id.registered_name = human_equipper.real_name - worn_id.update_label() - worn_id.update_icon() - -/datum/id_trim/syndicom/infiltrator - assignment = "Syndicate Infiltrator" - access = list(ACCESS_MAINT_TUNNELS, ACCESS_SYNDICATE, ACCESS_SYNDICATE_LEADER) diff --git a/maplestation_modules/code/modules/jobs/job_types/bridge_officer.dm b/maplestation_modules/code/modules/jobs/job_types/bridge_officer.dm index 55e9719e930d..4695a1b59e39 100644 --- a/maplestation_modules/code/modules/jobs/job_types/bridge_officer.dm +++ b/maplestation_modules/code/modules/jobs/job_types/bridge_officer.dm @@ -1,10 +1,4 @@ // -- Bridge Officer job & outfit datum -- -/datum/station_trait/job/bridge_assistant - weight = 0 - -/datum/job/bridge_assistant - rpg_title = "Lesser Guildperson" - /datum/job/bridge_officer title = JOB_BRIDGE_OFFICER description = "File paperwork to Central Command via your fax machine, \ diff --git a/maplestation_modules/code/modules/jobs/job_types/stowaway.dm b/maplestation_modules/code/modules/jobs/job_types/stowaway.dm index 2513bb950015..d815d86922b3 100644 --- a/maplestation_modules/code/modules/jobs/job_types/stowaway.dm +++ b/maplestation_modules/code/modules/jobs/job_types/stowaway.dm @@ -16,7 +16,7 @@ paycheck = PAYCHECK_ZERO total_positions = 0 spawn_positions = 1 - supervisors = "no one" + supervisors = "no one (yet)" exp_granted_type = EXP_TYPE_CREW config_tag = "STOWAWAY" faction = FACTION_STATION @@ -37,6 +37,10 @@ /datum/job/stowaway/get_default_roundstart_spawn_point() return find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE) +/datum/job/stowaway/get_spawn_message_information() + . = ..() + . += span_notice("You are free to steal and evade security as you please, but remember you are not an antagonist.") + /datum/job/stowaway/after_spawn(mob/living/spawned, client/player_client) . = ..() var/datum/status_effect/backstory/backstory = spawned.apply_status_effect(/datum/status_effect/backstory) diff --git a/maplestation_modules/code/modules/loadouts/loadout_items/_loadout_datum.dm b/maplestation_modules/code/modules/loadouts/loadout_items/_loadout_datum.dm index bcd404bd6177..83e9f2af7d19 100644 --- a/maplestation_modules/code/modules/loadouts/loadout_items/_loadout_datum.dm +++ b/maplestation_modules/code/modules/loadouts/loadout_items/_loadout_datum.dm @@ -247,7 +247,7 @@ GLOBAL_LIST_INIT_TYPED(all_loadout_categories, /datum/loadout_category, init_loa if(job_greyscale_palettes[jobtype]) return job_greyscale_palettes[jobtype] - var/datum/job/job = SSjob.GetJobType(jobtype) + var/datum/job/job = SSjob.get_job_type(jobtype) if(job.department_for_prefs && job_greyscale_palettes[job.department_for_prefs]) return job_greyscale_palettes[job.department_for_prefs] diff --git a/maplestation_modules/icons/nanotrasen-logo.png b/maplestation_modules/icons/nanotrasen-logo.png new file mode 100644 index 000000000000..015bf21f4e35 Binary files /dev/null and b/maplestation_modules/icons/nanotrasen-logo.png differ diff --git a/sound/ambience/antag/abductee.ogg b/sound/ambience/antag/abductee.ogg new file mode 100644 index 000000000000..eb6c2f564b18 Binary files /dev/null and b/sound/ambience/antag/abductee.ogg differ diff --git a/sound/ambience/antag/attribution.txt b/sound/ambience/antag/attribution.txt new file mode 100644 index 000000000000..d7b444fc909c --- /dev/null +++ b/sound/ambience/antag/attribution.txt @@ -0,0 +1,11 @@ +sound/instrumental/antag/abductee.ogg is from "Warp SFX" https://freesound.org/people/Breviceps/sounds/453391 (CC0) +sound/instrumental/antag/brainwash.ogg is made by FeiH from https://github.com/OracleStation/OracleStation/pull/1122/commits/b28fbbad715b96db029a8e8df38b1357a58daec1 +sound/instrumental/antag/hypnosis.ogg is from "Flashback.wav" https://freesound.org/people/Sclolex/sounds/342103 (CC0) + +{ +ambimaint8.ogg +ambimaint9.ogg +ambimaint10.ogg +ambimaint11.ogg +ambimaint12.ogg +} made by Kayozz , license: CC-by-SA diff --git a/sound/ambience/antag/ayylien.ogg b/sound/ambience/antag/ayylien.ogg new file mode 100644 index 000000000000..8a91ae0b233f Binary files /dev/null and b/sound/ambience/antag/ayylien.ogg differ diff --git a/sound/ambience/antag/brainwashed.ogg b/sound/ambience/antag/brainwashed.ogg new file mode 100644 index 000000000000..32f27533ce03 Binary files /dev/null and b/sound/ambience/antag/brainwashed.ogg differ diff --git a/sound/ambience/antag/hypnotized.ogg b/sound/ambience/antag/hypnotized.ogg new file mode 100644 index 000000000000..3fc0183c4e9a Binary files /dev/null and b/sound/ambience/antag/hypnotized.ogg differ diff --git a/sound/misc/prompt1.ogg b/sound/misc/prompt1.ogg new file mode 100644 index 000000000000..b284fcaef5d3 Binary files /dev/null and b/sound/misc/prompt1.ogg differ diff --git a/strings/antagonist_flavor/traitor_flavor.json b/strings/antagonist_flavor/traitor_flavor.json index 93da8d01374e..773d2f55370f 100644 --- a/strings/antagonist_flavor/traitor_flavor.json +++ b/strings/antagonist_flavor/traitor_flavor.json @@ -111,5 +111,13 @@ "roundend_report": "was a terrorist from Waffle Corporation.", "ui_theme": "syndicate", "uplink": "You have been provided with a standard uplink to accomplish your task." + }, + "Contractor Support Unit": { + "allies": "You are being sent to help your designated agent. Their allegiences are above all others.", + "goal": "Help your designated agent to the furtest extent you can, their life is above your own.", + "introduction": "You are the Contractor Support Agent.", + "roundend_report": "was a contractor support agent.", + "ui_theme": "syndicate", + "uplink": "You do not come with your own uplink, defer to your agent." } } diff --git a/strings/flavor_reports.json b/strings/flavor_reports.json new file mode 100644 index 000000000000..37034ce977fa --- /dev/null +++ b/strings/flavor_reports.json @@ -0,0 +1,122 @@ +{ + "reports": [ + "All systems are operational and functioning within @pick(adjectives) parameters. Remember to report any issues with your station's systems to your station's Engineering team.", + "As mining operations on Freyja continue, several explorers have discovered abnormal caverns and tunnels within the moon. These tunnels share geological features with the tunnels found on Indecipheres, and have even been found to contain flora and fauna native to Indecipheres, somehow unfrozen and thriving despite the vast difference in environments.", + "Bluespace technological research has been soaring to new heights recently, with several stations reporting successful Bluespace derived tests. The company is excited to see what new innovations will come from this research, and encourages the crew to continue their diligent work in this field.", + "Cybersun Industries has announced that they have successfully raided a high-security library. The library was known to contain several books on the occult, which occasionally demonstrate anomalous or paranormal properties. We assure the crew that there is no cause for concern, and that we are taking all necessary precautions to ensure the safety of the station and its inhabitants - however, if you notice any strange activity from your station's library, report it to your station's security team.", + "DeForest Medical have reported great strides in their research on the effects of Bluespace on human physiology, and have recently developed a new treatment for Bluespace Sickness that has shown promising results in early trials. The company is excited to see the results of this research, and encourages the medical staff to continue their diligent work in this field.", + "Due to recent events, the company has decided to implement a new policy regarding the handling of corpses - particularly those of simian test subjects - aboard the station. All corpses must now be brought to the morgue for proper storage and handling, and may not be left unattended in hallways or other public areas. We thank you for your cooperation in this matter.", + "Due to recent events, the company would like to remind the crew that the Supermatter is not a toy, and the Engineering staff should avoid any unnecessary contact with it - yes, lighting a cigarette on it is considered unnecessary contact. We thank you for your cooperation in this matter.", + "Due to recent events, the company would like to remind the crew that the disposal chutes are not toys, and that cargo technicians should not be attempting to 'ride' them for faster travel around the station. We thank you for your cooperation in this matter.", + "Due to recent events, the company would like to remind the crew to wash their hands regularly, and to avoid contact with any bodily fluids or hazardous materials. Especially for the exploration teams, and especially after contact with any alien flora or fauna. We thank you for your cooperation in this matter.", + "Employee unrest has spiked in recent weeks, with several stations and outposts reporting attempted mutinies and riots. The Syndicate denies involvement, but recent reports from the Internal Affairs division state that they have been actively researching mind suggestion and mass hypnosis techniques, so the company has not ruled them out yet. In the event that the crew start to demand higher wages or better working conditions, we advise the Captain to remain calm and to avoid any rash decisions, and to report any signs of unrest to Central Command immediately.", + "Nanotrasen would like to remind the crew that their soul is owned by the company, and that any attempts to sell or trade your soul for personal gain will be met with swift and severe consequences, as it is considered a breach of contract and theft of company property.", + "One of the company's containment facilities was recently @pick(attack) by the Gorlex Marauders. The facility was being used to store several dangerous and exotic specimens, including codename @pick(codenames) - a highly adapted and dangerous alien creature. It is now believed that the Tiger Collective has inducted the subject into their ranks, and will likely be using it to infiltrate and sabotage stations and colonies across the sector.", + "One of the company's trading routes was recently @pick(attack) by the Gorlex Marauders. High casualties were reported, but amidst recovery operations, it has been noted that little was stolen outside of the contents of one ship - a high-security transport ship containing a nuclear fission explosive device. Fortunately, the device cannot be armed without a nuclear authorization code from Central Command. At the time we are unsure of their plans for it, but we advise the Captain and security team keep the station's self destruct codes secure, as there is a possibility that they could be reverse engineered to bypass security measures on the device.", + "Other stations in orbit around Indecipheres have reported strange occurances as their orbit passes behind the moon of Freyja. Some members of their crews have reported feelings of intense paranoia and dread during these times, and some have even resorted to violence against others. Little information is known at this time. We advise all crew members to remain vigilant, and to report any strange activity to your station's security team immediately.", + "Other stations in orbit around Indecipheres have reported strange occurrences as their orbit passes behind the moon of Freyja. Members of their maintenance crews have reported hearing strange noises and seeing strange apparitions during these times, and some have even gone missing. Little information is known at this time. We advise all crew members to remain vigilant, and to report any strange activity to your station's security team immediately.", + "Other stations in orbit around Indecipheres have reported strange occurrences as their orbit passes behind the moon of Freyja. Members of their research staffs have resorted to superstitious beliefs and rituals, including blood sacrifice to appease 'the Geometer' and strange invocations. The Wizard Federation denies any involvement in these events, and the company has no reason to doubt this statement - signs point towards the Cult of Nar'Sie as the likely culprit, but there is no concrete evidence at this time. We advise all crew members to remain vigilant, and to report any strange activity to your station's security team immediately.", + "Other stations in orbit around Indecipheres have reported strange occurrences as their orbit passes behind the moon of Freyja. The AI systems of these stations have exhibited malfunctions and strange behavior during these times, including erratic speech patterns and unprovoked aggression. Little information is known at this time. We advise the crew to monitor the station's AI closely, and to report any strange activity to the Research Director immediately.", + "Plasma research has been progressing steadily, with several stations reporting successful tests and breakthroughs in the field. The company is excited to see what new innovations will come from this research, and encourages the crew to continue their diligent work in this field.", + "Plasma research has seen explosive new developments recently, with several stations harnessing it for use in new weaponry. The company is excited to see what new innovations will come from this research, and encourages the crew to continue their diligent work in this field.", + "Recent breakthroughs in Bluespace technology have resulted in strange and unpredictable effects on spacetime, including the creation of temporary wormholes and rifts in reality. Any anomalous activity should be reported to your station's research team.", + "Recent investigations into a crashed Syndicate cargo ship found advanced chameleon cloaking technology in the form of clothing marked as property of the organization known as 'MI13', able to imitate the designs of official Nanotrasen uniforms. Be aware of possible stolen valor in high-ranking personnel visiting your station and make sure to properly check for mindshield implants in high ranking staff.", + "Recently, a shuttle full of evacuees fleeing a @pick(attack) Nanotrasen research station saw a strange event upon landing at their designated Central Command dropoff point - immediately upon landing, the evacuees all snapped and beat each other to death in a gruesome and violent frenzy. The company has no information on what caused this event, and suggests that all personnel keep up to date with their Psychological evaluations.", + "Reports have been circulating that members of mining teams assigned to Freyja have been wandering on the surface of the moon, away from the station, until they are out of comms range - where they are never heard from again. The company attributes this to the harsh and unforgiving environment of Freyja, and would like to remind all explorers and miners to stay within comms range at all times and to report to the Psychologist if experiencing any feelings of isolation or loneliness.", + "Reports have been circulating that some crew members on other stations have deliberately lingered in the proximity of unstable bioscrambler anomalies. It is rumored these individuals were attempting to mutate their organs, limbs, and other body parts into superior forms. DeForest Medical does not condone such unorthodox and often unreliable methods of biological augmentation.", + "Reports that a strange @pick(virus) have been spreading across Spinward Sector Coalition have been circulating amongst news agencies recently. All major organizations have released statements denying any involvement in the spread of this virus, and no cure is currently known. Cases of it spreading to orbital stations have been few and far between, but we advise medical staff to be on the lookout for any strange symptoms in patients, and to report any suspected cases to the Chief Medical Officer immediately.", + "Rumors have circulated that a small army of pre-civilized humanoids managed to appropriate a transport vessel that landed on their planet. Allegedly, they overpowered the crew with their primitive swords, spears, and poleaxes, and have since discovered how to activate the ship's autopilot systems - allowing them to continue their conquest to the vessel's home station. We assure the crew that there is no cause for concern, for even if the story is true, Nanotrasen laser weaponry could easily disintegrate their simple weapons and armor.", + "Rumors that Interdyne Pharmaceuticals have managed to synthesize a highly deadly and contagious @pick(virus) are plausible, though we currently have no concrete evidence to support this claim. In the event of contact with Syndiacte personnel, we advise the medical staff to be on the lookout for strange and unusual symptoms in patients, and to report any suspected cases to the Chief Medical Officer.", + "Rumors that Nanotrasen are planning on widespread pay cuts in the Spinward Sector are unsubstantiated at this time. We ask that you do your part in preventing the spread of misinformation amongst the crew in the meanwhile.", + "Rumors that the Syndicate have made inroads in Tiziran markets via the shipbuilding conglomerate 'Azik Interstellar' have been brewing forsome time, and seems more likely than ever with the recent discovery of Cybersun Industry material shipping into Tiziran space. It is plausible that the Lizardfolk may be supplying the Syndicate with advanced shuttlecraft - we advise the crew to be cautious of any vessels of Tiziran design approaching the station.", + "Rumors that the company outsources their clown and mime hirees from the clown and mime planets respectively are unsubstantiated at this time. All clown and mime employees staffed on Nanotrasen stations and colonies are vetted and hired through the company's standard hiring process, and are subject to the same training and background checks as all other employees.", + "Rumors that unmarked @pick(floor_things) found on the floor of maintenance tunnels may grant you 'superpowers' are entirely unsubstantiated. DeForest Medical does not condone the use of unmarked pills or syringes, and advises all crew members to bring any unmarked pills or syringes to the medical staff for proper identification and disposal.", + "Satellite photoimagery of both Freyja and Indecipheres have unveiled a number of strange ruins. While some are reminiscent of crash-landed space station segments, others appear to have far stranger, possibly alien origins. As a legal precaution, we request that any active mining operations leave these ruins for later preservation and archaeological endeavors. With that in mind, it is still scientifically important to analyze anything picked up along the surface.", + "Several members of the crew have been found asleep at their desks recently. The company would like to remind the crew that while we understand that working in space can be stressful, sleeping on the job is not acceptable and may lead to disciplinary action. If you are feeling tired, please report to the station's dormitories for rest, and remember to take care of yourselves - your health and wellbeing is important to the company.", + "Station diagnostics indicate that all systems are @pick(adjectives). Enjoy your shift today, and remember to report any issues with your station's systems to your station's Engineering team.", + "TerraGov recently announced that they will continue to maintain their 'peacekeeping' operations in the Spinward Sector. The TerraGov Marine Corps have already been deployed en masse to the sector under the pretenses of 'protecting joint SSC-TerraGov trade routes from piracy', and have swiftly began construction of military outposts. While the company holds doubts about their purpose, the Spinward Stellar Coalition - wishing to keep close ties with TerraGov - has released a statement supporting this move, citing the rising tensions between the Syndicate and Nanotrasen as a cause for concern.", + "The Internal Affairs division have released a classified statement suggesting that the Syndicate have achieved a breakthrough in their sleeper agent program, and that despite corporate background checks, there is a non-zero chance that any member of the crew could be a long term Syndicate sleeper agent, capable of being activated at any moment to undergo heinous acts of sabotage, espionage, or even assassination.", + "The Internal Affairs division have released a statement condemning the recent actions of the Syndicate, and have accused them of being responsible for several recent incidents in the Spinward Sector, including the spread of a dangerous @pick(virus) and the recent employee unrest. The Syndicate has denied these allegations, and there is currently no concrete evidence to support either claim.", + "The Internal Affairs division would like to remind the crew that they do not have any direct involvement in station affairs. Anyone claiming to be an agent of Internal Affairs should be reported to your station's security team immediately. Any accusations that Internal Affairs, Central Command, or Nanotrasen as a whole is placing members of Internal Affairs on the station will be met with a conversation with an official Internal Affairs agent.", + "The Nanotrasen Department of Public Relations would like to remind all personnel that usage of 'modern' camouflage techniques to save money in the budget, while smart in theory, results in a bad look on the company's competency. When watching for petty crime, please stick to dedicated stealth technology such as cloaking modules for modular suits instead of trying to disguise yourself as the environment of a bland hallway with sticks, cloth, and patterned clothing.", + "The Nanotrasen Department of Information Technology would like to inform all crew on-duty that they have never, nor will ever, ask for your passwords. They would also like to mention that rumors of Nanotrasen installing subdermal cybernetic implants that automatically censors passwords from your vision are entirely unfounded - the company has no technology capable of such a feat, even for those of you with the password 'hunter2'. You know who you are.", + "The Spinward Stellar Coalition's president, Oleksandr Kushnirenko, has released a statement supporting the recent expansion of Nanotrasen operations in the Spinward Sector, and has praised the company for its dedication to exploration and innovation. When asked if he was concerned about the recent expansion of Bluespace Artillery platforms, Kushnirenko stated that he has full trust in the company's judgement and that he is confident that the company will use these platforms responsibly.", + "The Syndicate has issued a statement claiming they are not responsible for any recent incidents within the Spinward Sector. Whether you believe them or not is up to you.", + "The Syndicate seem to be attempting to recruit members of the crew for their cause, though the exact details of their plans are unknown. The company would like to remind the crew that their loyalty should lie with the company, and that any attempts to recruit for the Syndicate should be reported to your station's security team immediately.", + "The Wizard Federation were reported to be holding a symposium in the Spinward Sector recently, though the exact location is unknown. The company has no reason to doubt the Federation's claim that the meeting was purely social, though as Wizards lack a sense of right and wrong, the company advises the crew to stay on the lookout for any robed individuals suddenly appearing around the station.", + "The coffee corporation Jim Nortons recently announced a parternship with Nanotrasen, providing wayward spacers on distant stations with fresh coffee, straight from Sol. The company is excited to see the results of this partnership, and encourages the crew to enjoy their coffees responsibly.", + "The company would like to inform the crew that the recent expansion of Bluespace Artillery platforms in the Spinward Sector is not a cause for concern - these batteries exist to protect the the inner station and colonies from larger threats, and are not authorized for offensive use.", + "The recent disappearance of the clown has been acknowledged by Central Command. During their absence, we would like to make it clear that the company does not condone the actions of the missing clown, and that the crew should look into a replacement entertainer at their leisure.", + "The station's communications systems are operating within @pick(adjectives) parameters. Good luck on your shift today, and remember to report any issues with comms to your station's Engineering team.", + "There is nothing interesting to report at this time. Please continue with your duties as normal, and report any suspicious activity to your station's security team immediately.", + "Voyagers to Indecipheres have reported strange and unsettling dreams during their stay on the station. These dreams often involve themes of isolation, paranoia, and cosmic horror, and have been known to cause psychological distress in some individuals. We advise the crew to report any instances of these dreams to the medical staff, and to seek help if they are experiencing any mental health issues as a result.", + "Voyagers to Indecipheres have returned with a plethora of strange and exotic souvenirs from their travels, including several items of great power. It is advised that any items returned from these voyages be handled with caution, and that any strange or anomalous items be brought to your station's research team for analysis.", + "We have transposed Central Command's location to a new location within hyperspace. This is a variable-frequency event that the shuttles should automatically account for, with further details being confidential. Nanotrasen urges amateur space explorers against trying to find Central Command, as it is only meant to be available via scheduled shuttle services.", + "Your station's AI is functioning within @pick(adjectives) parameters. Remember to report any issues with it to the Research Director." + ], + + "intern_reports": [ + "Please replace this text with the report provided by the Communications team.", + "Please check the report for typos and grammatical errors before sending it to the station.", + "The coffee machine is currently out of order. We apologize for the inconvenience, and hope to have it back up and running soon." + ], + + "adjectives": [ + "nominal", + "acceptable", + "optimal", + "normal", + "stable", + "standard" + ], + + "codenames": [ + "Alpha", + "Beta", + "Gamma", + "Delta", + "Epsilon", + "Zeta", + "Eta", + "Theta", + "Iota", + "Kappa", + "Lambda", + "Mu", + "Nu", + "Xi", + "Omicron", + "Pi", + "Rho", + "Sigma", + "Tau", + "Upsilon", + "Phi", + "Chi", + "Psi", + "Omega" + ], + + "virus": [ + "bacterial infection", + "blight", + "cerebral parasite", + "contagion", + "curse", + "disease", + "flesh-eating virus", + "grey goo", + "infectious spore", + "nanomachine virus", + "plague", + "retrovirus", + "spaceborne bacterium", + "unknown pathogen", + "virus", + "xenopathogen" + ], + + "attack": ["assaulted", "attacked", "raided", "ransacked"], + + "floor_things": ["pills", "pills", "pills", "syringes"] +} diff --git a/strings/traitor_flavor.json b/strings/traitor_flavor.json deleted file mode 100644 index abd0e765df57..000000000000 --- a/strings/traitor_flavor.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "Animal Rights Consortium": { - "allies": "You may cooperate with other syndicate operatives if they support our cause. Maybe you can convince the Bee Liberation Front operatives to cooperate for once?", - "goal": "The creatures of this world must be freed from the iron grasp of Nanotrasen, and you are their only hope!", - "introduction": "You are the ARC Terrorist.", - "ui_theme": "syndicate", - "uplink": "The Syndicate have graciously given one of their uplinks for your task." - }, - "Bee Liberation Front": { - "allies": "You may cooperate with other syndicate operatives if they support our cause. Maybe you can recruit an Animal Rights Consort to be useful for once?", - "goal": "We must prove ourselves to the Syndicate or we will not be able to join. Animal Rights Consort will roll us!", - "introduction": "You are the Bee Liberation Front Operative.", - "ui_theme": "syndicate", - "uplink": "The Syndicate have graciously given one of their uplinks to see if we are worthy." - }, - "Champions of Evil": { - "allies": "Anyone who sees as you see, feels as you feel, may join the Champions of Evil! That means the Syndicate, the self-serving, or even the insane, as long as it has a heart of darkness, it's cool with the Champions!", - "goal": "You've got some napkin-note-plans for some EVIL to do today. On the side, the Champions of Evil are always looking for more morally malodorous malefactors! Get some recruiting done!", - "introduction": "You are the Champion of Evil.", - "ui_theme": "neutral", - "uplink": "The Champions of Evil is well connected to the black market. Your uplink has been provided for utmost evil!" - }, - "Corporate Climber": { - "allies": "Death to the Syndicate.", - "goal": "Killing needlessly would make you some kind of traitor, or at least definitely seen as one. This is all just a means to an end.", - "introduction": "You are the Corporate Climber.", - "ui_theme": "neutral", - "uplink": "You have connections to the black market for the deeds. Knock off a few loose weights, and your climb will be so much smoother." - }, - "Cybersun Industries": { - "allies": "Fellow Cybersun operatives are to be trusted. Members of the MI13 organization can be trusted. All other syndicate operatives are not to be trusted.", - "goal": "Do not establish substantial presence on the designated facility, as larger incidents are harder to cover up.", - "introduction": "You are from Cybersun Industries.", - "ui_theme": "syndicate", - "uplink": "You have been supplied the tools for the job in the form of a standard syndicate uplink." - }, - "Donk Corporation": { - "allies": "Members of Waffle Co. are to be killed on sight; they are not allowed to be on the station while we're around.", - "goal": "We do not approve of mindless killing of innocent workers; \"get in, get done, get out\" is our motto.", - "introduction": "You are the Donk Co. Traitor.", - "ui_theme": "syndicate", - "uplink": "You have been provided with a standard uplink to accomplish your task." - }, - "Gone Postal": { - "allies": "If the syndicate learns of your plan, they're going to kill you and take your uplink. Take no chances.", - "goal": "The preparations are finally complete. Today is the day you go postal. You're going to hijack the emergency shuttle and live a new life free of Nanotrasen.", - "introduction": "You're going postal today.", - "ui_theme": "neutral", - "uplink": "You've actually managed to steal a full uplink a month ago. This should certainly help accomplish your goals." - }, - "Gorlex Marauders": { - "allies": "You may collaborate with any friends of the Syndicate coalition, but keep an eye on any of those Tiger punks if they do show up.", - "goal": "Getting noticed is not an issue, and you may use any level of ordinance to get the job done. That being said, do not make this sloppy by dragging in random slaughter.", - "introduction": "You are a Gorlex Marauder.", - "ui_theme": "syndicate", - "uplink": "You have been provided with a standard uplink to accomplish your task." - }, - "Internal Affairs Agent": { - "allies": "Death to the Syndicate. Killing infiltrators is a whole different kind of internal affair we fully approve of dealing with.", - "goal": "While you have a license to kill, unneeded property damage or loss of employee life will lead to your contract being terminated.", - "introduction": "You are the Internal Affairs Agent.", - "ui_theme": "ntos", - "uplink": "For the sake of plausible deniability, you have been equipped with an array of captured Syndicate weaponry available via uplink." - }, - "Legal Trouble": { - "allies": "Death to the Syndicate.", - "goal": "Try to finish your to-do list, and don't get caught. If they find out what you're actually doing, this scandal will go galactic.", - "introduction": "You are in legal trouble.", - "ui_theme": "neutral", - "uplink": "You've connected to the black market to clean this mess up. If there's no evidence, there's no crime." - }, - "MI13": { - "allies": "You are the only operative we are sending, any others are fake. All other syndicate operatives are not to be trusted, with the exception of Cybersun operatives.", - "goal": "Avoid killing innocent personnel at all costs. You are not here to mindlessly kill people, as that would attract too much attention and is not our goal. Avoid detection at all costs.", - "introduction": "You are the MI13 Agent.", - "ui_theme": "syndicate", - "uplink": "You have been provided with a standard uplink to accomplish your task." - }, - "Tiger Cooperative Fanatic": { - "allies": "Only the enlightened Tiger brethren can be trusted; all others must be expelled from this mortal realm!", - "goal": "Remember the teachings of Hy-lurgixon; kill first, ask questions later!", - "introduction": "You are the Tiger Cooperative Fanatic.", - "ui_theme": "abductor", - "uplink": "You have been provided with a hy-lurgixon tome to prove yourself to the changeling hive. If you accomplish your tasks, you will be assimilated.", - "uplink_name": "hy-lurgixon tome" - }, - "Waffle Corporation": { - "allies": "Members of Donk Co. are to be killed on sight; they are not allowed to be on the station while we're around. Do not trust fellow members of the Waffle.co (but try not to rat them out), as they might have been assigned opposing objectives.", - "goal": "You are not here for a stationwide demonstration. Again, other Waffle Co. Traitors may be, so watch out. Your job is to only accomplish your objectives.", - "introduction": "You are the Waffle Co. Traitor.", - "ui_theme": "syndicate", - "uplink": "You have been provided with a standard uplink to accomplish your task." - }, - "Waffle Corporation Terrorist": { - "allies": "Most other syndicate operatives are not to be trusted, except for members of the Gorlex Marauders. Do not trust fellow members of the Waffle.co (but try not to rat them out), as they might have been assigned opposing objectives.", - "goal": "Our investors need a demonstation of our pledge to destroying Nanotrasen. Let's give them a loud one!", - "introduction": "You are the Waffle Corporation Terrorist.", - "ui_theme": "syndicate", - "uplink": "You have been provided with a standard uplink to accomplish your task." - } -} diff --git a/tgstation.dme b/tgstation.dme index 67f598a8108c..3b159fa675ab 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -88,7 +88,6 @@ #include "code\__DEFINES\door.dm" #include "code\__DEFINES\drone.dm" #include "code\__DEFINES\dye_keys.dm" -#include "code\__DEFINES\dynamic.dm" #include "code\__DEFINES\economy.dm" #include "code\__DEFINES\electrified_buckle.dm" #include "code\__DEFINES\events.dm" @@ -718,16 +717,15 @@ #include "code\controllers\subsystem\wardrobe.dm" #include "code\controllers\subsystem\weather.dm" #include "code\controllers\subsystem\wiremod_composite.dm" +#include "code\controllers\subsystem\dynamic\__dynamic_defines.dm" +#include "code\controllers\subsystem\dynamic\_dynamic_ruleset.dm" +#include "code\controllers\subsystem\dynamic\_dynamic_tier.dm" #include "code\controllers\subsystem\dynamic\dynamic.dm" -#include "code\controllers\subsystem\dynamic\dynamic_hijacking.dm" -#include "code\controllers\subsystem\dynamic\dynamic_logging.dm" -#include "code\controllers\subsystem\dynamic\dynamic_midround_rolling.dm" -#include "code\controllers\subsystem\dynamic\dynamic_rulesets.dm" -#include "code\controllers\subsystem\dynamic\dynamic_rulesets_latejoin.dm" -#include "code\controllers\subsystem\dynamic\dynamic_rulesets_midround.dm" -#include "code\controllers\subsystem\dynamic\dynamic_rulesets_roundstart.dm" -#include "code\controllers\subsystem\dynamic\dynamic_unfavorable_situation.dm" -#include "code\controllers\subsystem\dynamic\ruleset_picking.dm" +#include "code\controllers\subsystem\dynamic\dynamic_admin.dm" +#include "code\controllers\subsystem\dynamic\dynamic_ruleset_latejoin.dm" +#include "code\controllers\subsystem\dynamic\dynamic_ruleset_midround.dm" +#include "code\controllers\subsystem\dynamic\dynamic_ruleset_roundstart.dm" +#include "code\controllers\subsystem\dynamic\dynamic_testing.dm" #include "code\controllers\subsystem\movement\ai_movement.dm" #include "code\controllers\subsystem\movement\cliff_falling.dm" #include "code\controllers\subsystem\movement\hyperspace_drift.dm" @@ -780,6 +778,7 @@ #include "code\datums\chat_payload.dm" #include "code\datums\chatmessage.dm" #include "code\datums\cogbar.dm" +#include "code\datums\communications.dm" #include "code\datums\dash_weapon.dm" #include "code\datums\datum.dm" #include "code\datums\datumvars.dm" @@ -2727,7 +2726,6 @@ #include "code\game\objects\structures\spawner.dm" #include "code\game\objects\structures\spirit_board.dm" #include "code\game\objects\structures\stairs.dm" -#include "code\game\objects\structures\syndicate_uplink_beacon.dm" #include "code\game\objects\structures\table_frames.dm" #include "code\game\objects\structures\tables_racks.dm" #include "code\game\objects\structures\tank_dispenser.dm" @@ -3125,6 +3123,7 @@ #include "code\modules\antagonists\disease\disease_disease.dm" #include "code\modules\antagonists\disease\disease_mob.dm" #include "code\modules\antagonists\ert\ert.dm" +#include "code\modules\antagonists\evil_clone\evil_clone.dm" #include "code\modules\antagonists\fugitive\fugitive.dm" #include "code\modules\antagonists\fugitive\fugitive_equipment.dm" #include "code\modules\antagonists\fugitive\fugitive_outfits.dm" @@ -3243,11 +3242,13 @@ #include "code\modules\antagonists\ninja\ninja_stars.dm" #include "code\modules\antagonists\ninja\ninjaDrainAct.dm" #include "code\modules\antagonists\ninja\outfit.dm" -#include "code\modules\antagonists\nukeop\nukeop.dm" #include "code\modules\antagonists\nukeop\outfits.dm" +#include "code\modules\antagonists\nukeop\datums\operative.dm" +#include "code\modules\antagonists\nukeop\datums\operative_leader.dm" +#include "code\modules\antagonists\nukeop\datums\operative_lone.dm" +#include "code\modules\antagonists\nukeop\datums\operative_team.dm" #include "code\modules\antagonists\nukeop\equipment\borgchameleon.dm" #include "code\modules\antagonists\nukeop\equipment\nuclear_authentication_disk.dm" -#include "code\modules\antagonists\nukeop\equipment\nuclear_challenge.dm" #include "code\modules\antagonists\nukeop\equipment\pinpointer.dm" #include "code\modules\antagonists\nukeop\equipment\nuclear_bomb\_nuclear_bomb.dm" #include "code\modules\antagonists\nukeop\equipment\nuclear_bomb\beer_nuke.dm" @@ -3256,7 +3257,6 @@ #include "code\modules\antagonists\obsessed\obsessed.dm" #include "code\modules\antagonists\paradox_clone\paradox_clone.dm" #include "code\modules\antagonists\pirate\pirate.dm" -#include "code\modules\antagonists\pirate\pirate_event.dm" #include "code\modules\antagonists\pirate\pirate_gangs.dm" #include "code\modules\antagonists\pirate\pirate_outfits.dm" #include "code\modules\antagonists\pirate\pirate_roles.dm" @@ -3267,6 +3267,7 @@ #include "code\modules\antagonists\revenant\revenant_blight.dm" #include "code\modules\antagonists\revolution\enemy_of_the_state.dm" #include "code\modules\antagonists\revolution\revolution.dm" +#include "code\modules\antagonists\revolution\revolution_handler.dm" #include "code\modules\antagonists\santa\santa.dm" #include "code\modules\antagonists\sentient_creature\sentient_creature.dm" #include "code\modules\antagonists\separatist\nation_creation.dm" @@ -3280,40 +3281,13 @@ #include "code\modules\antagonists\spiders\spiders.dm" #include "code\modules\antagonists\survivalist\survivalist.dm" #include "code\modules\antagonists\syndicate_monkey\syndicate_monkey.dm" -#include "code\modules\antagonists\traitor\balance_helper.dm" #include "code\modules\antagonists\traitor\datum_traitor.dm" -#include "code\modules\antagonists\traitor\objective_category.dm" -#include "code\modules\antagonists\traitor\traitor_objective.dm" #include "code\modules\antagonists\traitor\uplink_handler.dm" #include "code\modules\antagonists\traitor\components\demoraliser.dm" -#include "code\modules\antagonists\traitor\components\traitor_objective_helpers.dm" -#include "code\modules\antagonists\traitor\components\traitor_objective_limit_per_time.dm" -#include "code\modules\antagonists\traitor\components\traitor_objective_mind_tracker.dm" #include "code\modules\antagonists\traitor\contractor\contract_teammate.dm" #include "code\modules\antagonists\traitor\contractor\contractor_hub.dm" #include "code\modules\antagonists\traitor\contractor\contractor_items.dm" #include "code\modules\antagonists\traitor\contractor\syndicate_contract.dm" -#include "code\modules\antagonists\traitor\objectives\assassination.dm" -#include "code\modules\antagonists\traitor\objectives\demoralise_assault.dm" -#include "code\modules\antagonists\traitor\objectives\destroy_heirloom.dm" -#include "code\modules\antagonists\traitor\objectives\destroy_item.dm" -#include "code\modules\antagonists\traitor\objectives\eyesnatching.dm" -#include "code\modules\antagonists\traitor\objectives\hack_comm_console.dm" -#include "code\modules\antagonists\traitor\objectives\infect.dm" -#include "code\modules\antagonists\traitor\objectives\kidnapping.dm" -#include "code\modules\antagonists\traitor\objectives\kill_pet.dm" -#include "code\modules\antagonists\traitor\objectives\locate_weakpoint.dm" -#include "code\modules\antagonists\traitor\objectives\sabotage_machinery.dm" -#include "code\modules\antagonists\traitor\objectives\sleeper_protocol.dm" -#include "code\modules\antagonists\traitor\objectives\steal.dm" -#include "code\modules\antagonists\traitor\objectives\abstract\target_player.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\battlecruiser.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\final_objective.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\infect_ai.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\objective_dark_matteor.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\romerol.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\space_dragon.dm" -#include "code\modules\antagonists\traitor\objectives\final_objective\supermatter_cascade.dm" #include "code\modules\antagonists\valentines\heartbreaker.dm" #include "code\modules\antagonists\valentines\valentine.dm" #include "code\modules\antagonists\venus_human_trap\venus_human_trap.dm" @@ -3997,9 +3971,9 @@ #include "code\modules\events\camerafailure.dm" #include "code\modules\events\carp_migration.dm" #include "code\modules\events\communications_blackout.dm" -#include "code\modules\events\creep_awakening.dm" #include "code\modules\events\disease_outbreak.dm" #include "code\modules\events\dust.dm" +#include "code\modules\events\dynamic_tweak.dm" #include "code\modules\events\earthquake.dm" #include "code\modules\events\electrical_storm.dm" #include "code\modules\events\fake_virus.dm" @@ -4020,7 +3994,6 @@ #include "code\modules\events\scrubber_overflow.dm" #include "code\modules\events\shuttle_catastrophe.dm" #include "code\modules\events\shuttle_insurance.dm" -#include "code\modules\events\spider_infestation.dm" #include "code\modules\events\stray_cargo.dm" #include "code\modules\events\supermatter_surge.dm" #include "code\modules\events\tram_malfunction.dm" @@ -4039,20 +4012,9 @@ #include "code\modules\events\anomaly\anomaly_pyro.dm" #include "code\modules\events\anomaly\anomaly_vortex.dm" #include "code\modules\events\ghost_role\_ghost_role.dm" -#include "code\modules\events\ghost_role\abductor.dm" -#include "code\modules\events\ghost_role\alien_infestation.dm" -#include "code\modules\events\ghost_role\blob.dm" -#include "code\modules\events\ghost_role\changeling_event.dm" -#include "code\modules\events\ghost_role\fugitive_event.dm" -#include "code\modules\events\ghost_role\morph_event.dm" -#include "code\modules\events\ghost_role\nightmare.dm" #include "code\modules\events\ghost_role\operative.dm" -#include "code\modules\events\ghost_role\revenant_event.dm" #include "code\modules\events\ghost_role\sentience.dm" #include "code\modules\events\ghost_role\sentient_disease.dm" -#include "code\modules\events\ghost_role\slaughter_event.dm" -#include "code\modules\events\ghost_role\space_dragon.dm" -#include "code\modules\events\ghost_role\space_ninja.dm" #include "code\modules\events\holiday\easter.dm" #include "code\modules\events\holiday\halloween.dm" #include "code\modules\events\holiday\vday.dm" @@ -4366,7 +4328,6 @@ #include "code\modules\jobs\job_types\antagonists\abductor_agent.dm" #include "code\modules\jobs\job_types\antagonists\abductor_scientist.dm" #include "code\modules\jobs\job_types\antagonists\abductor_solo.dm" -#include "code\modules\jobs\job_types\antagonists\clown_operative.dm" #include "code\modules\jobs\job_types\antagonists\lone_operative.dm" #include "code\modules\jobs\job_types\antagonists\morph.dm" #include "code\modules\jobs\job_types\antagonists\nightmare.dm" @@ -4603,6 +4564,7 @@ #include "code\modules\mapping\space_management\zlevel_manager.dm" #include "code\modules\meteors\meteor_changeling.dm" #include "code\modules\meteors\meteor_dark_matteor.dm" +#include "code\modules\meteors\meteor_mode_controller.dm" #include "code\modules\meteors\meteor_spawning.dm" #include "code\modules\meteors\meteor_types.dm" #include "code\modules\meteors\meteor_waves.dm" diff --git a/tgui/packages/tgui/interfaces/AbductorConsole.tsx b/tgui/packages/tgui/interfaces/AbductorConsole.tsx index a8d03c82358a..4a4bdc632339 100644 --- a/tgui/packages/tgui/interfaces/AbductorConsole.tsx +++ b/tgui/packages/tgui/interfaces/AbductorConsole.tsx @@ -93,6 +93,8 @@ const Abductsoft = (props) => { disabled: (credits || 0) < item.cost, icon: item.icon, icon_state: item.icon_state, + population_tooltip: '', + insufficient_population: false, }); } } diff --git a/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx b/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx index f2fac4352328..22f424afefab 100644 --- a/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx +++ b/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx @@ -1,10 +1,4 @@ -import { - BlockQuote, - Button, - Dimmer, - Section, - Stack, -} from 'tgui-core/components'; +import { BlockQuote, Button, Section, Stack } from 'tgui-core/components'; import type { BooleanLike } from 'tgui-core/react'; import { useBackend } from '../backend'; @@ -36,8 +30,6 @@ type Info = { intro: string; code: string; failsafe_code: string; - replacement_code: string; - replacement_frequency: string; has_uplink: BooleanLike; uplink_intro: string; uplink_unlock_info: string; @@ -107,37 +99,12 @@ const EmployerSection = (props) => { const UplinkSection = (props) => { const { data } = useBackend(); - const { - has_uplink, - uplink_intro, - uplink_unlock_info, - code, - failsafe_code, - replacement_code, - replacement_frequency, - } = data; + const { has_uplink, uplink_intro, uplink_unlock_info, code, failsafe_code } = + data; return (
- {(!has_uplink && ( - - -
- Your uplink is missing or destroyed.
- Craft a Syndicate Uplink Beacon and then speak -
- - {replacement_code} - {' '} - on frequency{' '} - - {replacement_frequency} - {' '} - after synchronizing with the beacon. -
-
-
- )) || ( + { <> {uplink_intro} @@ -153,29 +120,16 @@ const UplinkSection = (props) => {
{uplink_unlock_info}
- )} + }

- {(has_uplink && ( -
- If you lose your uplink, you can craft a Syndicate Uplink Beacon and - then speak{' '} - - {replacement_code} - {' '} - on radio frequency{' '} - - {replacement_frequency} - {' '} - after synchronizing with the beacon. -
- )) || ( + {
{' '}

- )} + }
); }; diff --git a/tgui/packages/tgui/interfaces/DynamicAdmin.tsx b/tgui/packages/tgui/interfaces/DynamicAdmin.tsx new file mode 100644 index 000000000000..d494c561e0f2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/DynamicAdmin.tsx @@ -0,0 +1,739 @@ +import { useState } from 'react'; +import { + BlockQuote, + Box, + Button, + Dropdown, + Flex, + Input, + LabeledList, + NoticeBox, + Section, + Stack, + Tabs, + Tooltip, +} from 'tgui-core/components'; +import { BooleanLike } from 'tgui-core/react'; +import { createSearch } from 'tgui-core/string'; + +import { useBackend } from '../backend'; +import { Window } from '../layouts'; + +type RulesetCount = Record; + +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 000000000000..af86fff955bd --- /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} + + ))} +
+
+
+
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/TraitorObjectiveDebug.tsx b/tgui/packages/tgui/interfaces/TraitorObjectiveDebug.tsx deleted file mode 100644 index d820d440998b..000000000000 --- a/tgui/packages/tgui/interfaces/TraitorObjectiveDebug.tsx +++ /dev/null @@ -1,428 +0,0 @@ -import { useState } from 'react'; -import { Box, LabeledList, Stack, Tabs, Tooltip } from 'tgui-core/components'; - -import { useBackend } from '../backend'; -import { Window } from '../layouts'; -import { getDangerLevel } from './Uplink/calculateDangerLevel'; - -type Objective = { - name: string; - description: string; - progression_minimum: number; - progression_maximum: number; - global_progression_limit_coeff: number; - global_progression_influence_intensity: number; - progression_reward: [number, number]; - telecrystal_reward: [number, number]; - telecrystal_penalty: number; - weight: number; - type: string; -}; - -type ObjectiveList = { - objectives: (ObjectiveList | Objective)[]; - weight: number; -}; - -type ObjectiveCategory = ObjectiveList & { - name: string; -}; - -type PlayerData = { - player: string; - progression_points: number; - total_progression_from_objectives: number; -}; - -type ObjectiveData = { - current_progression: number; - objective_data: ObjectiveCategory[]; - player_data: PlayerData[]; -}; - -const recursivelyGetObjectives = (value: ObjectiveList) => { - let listToReturn: Objective[] = []; - for (let i = 0; i < value.objectives.length; i++) { - const possibleValue = value.objectives[i]; - if ((possibleValue as ObjectiveList).objectives) { - listToReturn = listToReturn.concat( - recursivelyGetObjectives(possibleValue as ObjectiveList), - ); - } else { - listToReturn.push(possibleValue as Objective); - } - } - return listToReturn; -}; - -// 150 minutes -const sizeLimit = 90000; - -type SortingOption = { - name: string; - // Function used to determine the order of the elements. - // It is expected to return a negative value - // if first argument is less than second argument, - // zero if they're equal and a positive value otherwise. - sort: (a: Objective, b: Objective) => number; -}; - -const sortingOptions: SortingOption[] = [ - { - name: 'Minimum Progression', - sort: (a, b) => { - if (a.progression_minimum < b.progression_minimum) { - return -1; - } else if (a.progression_minimum === b.progression_minimum) { - return 0; - } - return 1; - }, - }, - { - name: 'Telecrystal Payout', - sort: (a, b) => { - const telecrystalMeanA = - (a.telecrystal_reward[0] + a.telecrystal_reward[1]) / 2; - const telecrystalMeanB = - (b.telecrystal_reward[0] + b.telecrystal_reward[1]) / 2; - if (telecrystalMeanA < telecrystalMeanB) { - return -1; - } else if (telecrystalMeanA === telecrystalMeanB) { - return 0; - } - return 1; - }, - }, - { - name: 'Progression Payout', - sort: (a, b) => { - const progressionMeanA = - (a.progression_reward[0] + a.progression_reward[1]) / 2; - const progressionMeanB = - (b.progression_reward[0] + b.progression_reward[1]) / 2; - if (progressionMeanA < progressionMeanB) { - return -1; - } else if (progressionMeanA === progressionMeanB) { - return 0; - } - return 1; - }, - }, - { - name: 'Progression Payout + Min. Prog.', - sort: (a, b) => { - const progressionMeanA = - (a.progression_reward[0] + a.progression_reward[1]) / 2 + - a.progression_minimum; - const progressionMeanB = - (b.progression_reward[0] + b.progression_reward[1]) / 2 + - b.progression_minimum; - if (progressionMeanA < progressionMeanB) { - return -1; - } else if (progressionMeanA === progressionMeanB) { - return 0; - } - return 1; - }, - }, -]; - -export const TraitorObjectiveDebug = (props) => { - const { data, act } = useBackend(); - const { objective_data, player_data, current_progression } = data; - const lines: React.JSX.Element[] = []; - lines.sort(); - for (let i = 10; i < 100; i += 10) { - lines.push( - - - - {/* Time in minutes of this threshold */} - {Math.round((sizeLimit * (i / 100)) / 600)} mins - - , - ); - } - let objectivesToRender: Objective[] = []; - const [currentTab, setCurrentTab] = useState('All'); - const [sortingFunc, setSortingFunc] = useState(sortingOptions[0].name); - // true = ascending, false = descending - const [sortDirection, setSortingDirection] = useState(true); - - let actualSortingFunc; - for (let index = 0; index < sortingOptions.length; index++) { - const value = sortingOptions[index]; - if (value.name === sortingFunc) { - actualSortingFunc = value.sort; - } - } - - for (let index = 0; index < objective_data.length; index++) { - const value = objective_data[index]; - if (value.name !== currentTab && currentTab !== 'All') { - continue; - } - objectivesToRender = objectivesToRender.concat( - recursivelyGetObjectives(value), - ); - } - - objectivesToRender.sort(actualSortingFunc); - if (!sortDirection) { - objectivesToRender.reverse(); - } - - return ( - - - - - - - {sortingOptions.map((value) => ( - setSortingFunc(value.name)} - > - {value.name} - - ))} - - - - - setCurrentTab('All')} - > - All - - {objective_data.map((value) => ( - setCurrentTab(value.name)} - > - {value.name} - - ))} - - - - - setSortingDirection(true)} - > - Ascending - - setSortingDirection(false)} - > - Descending - - - - - - - {lines} - - {objectivesToRender.map((value, index) => ( - - - - ))} - - {player_data.map((value) => { - const rep = getDangerLevel(value.progression_points); - return ( - - - - {value.player} - - - {Math.floor(value.progression_points / 600)} mins - - - {Math.floor( - value.total_progression_from_objectives / 600, - )}{' '} - mins - - - - } - position="top" - > - - - - - ); - })} - - - - - - - - - ); -}; - -type ObjectiveBoxProps = { - objective: Objective; -}; - -const ObjectiveBox = (props: ObjectiveBoxProps) => { - const { objective } = props; - let width = `${ - (objective.progression_maximum / sizeLimit) * window.innerWidth - }px`; - if (objective.progression_maximum > sizeLimit) { - width = '100%'; - } - return ( - - - - {objective.name} - - - - - {objective.progression_minimum / 600} mins - - {objective.progression_maximum <= sizeLimit && ( - - {objective.progression_maximum / 600} mins - - )} - - - - - {objective.progression_reward[0] / 600} -  to {objective.progression_reward[1] / 600} pr - - - - - {objective.telecrystal_reward[0]} -  to {objective.telecrystal_reward[1]} tc - - - - - ); -}; diff --git a/tgui/packages/tgui/interfaces/Uplink/GenericUplink.tsx b/tgui/packages/tgui/interfaces/Uplink/GenericUplink.tsx index 569a4e4395ea..4ab997a28693 100644 --- a/tgui/packages/tgui/interfaces/Uplink/GenericUplink.tsx +++ b/tgui/packages/tgui/interfaces/Uplink/GenericUplink.tsx @@ -133,6 +133,8 @@ export type Item = { category: string; cost: React.JSX.Element | string; desc: React.JSX.Element | string; + population_tooltip: string; + insufficient_population: BooleanLike; disabled: BooleanLike; }; @@ -184,9 +186,19 @@ const ItemList = (props: ItemListProps) => { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', + opacity: item.insufficient_population ? '0.5' : '1', }} > - {item.name} + {item.insufficient_population ? ( + + + + {item.name} + + + ) : ( + item.name + )} @@ -215,6 +227,21 @@ const ItemList = (props: ItemListProps) => { } > + {item.insufficient_population ? ( + + {' '} + {item.population_tooltip} + + ) : ( + '' + )} + { + const { name, description } = props; + + return ( + + + + + {name} + + + + + + {description} + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/Uplink/ObjectiveMenu.tsx b/tgui/packages/tgui/interfaces/Uplink/ObjectiveMenu.tsx deleted file mode 100644 index e7cc81fb71f1..000000000000 --- a/tgui/packages/tgui/interfaces/Uplink/ObjectiveMenu.tsx +++ /dev/null @@ -1,569 +0,0 @@ -import { Component, type MouseEvent } from 'react'; -import { - Box, - Button, - Dimmer, - Flex, - Icon, - NoticeBox, - Section, - Stack, - Tooltip, -} from 'tgui-core/components'; -import { type BooleanLike, classes } from 'tgui-core/react'; - -import { - calculateProgression, - getDangerLevel, - type Rank, -} from './calculateDangerLevel'; -import { ObjectiveState } from './constants'; - -export type Objective = { - id: number; - name: string; - description: string; - progression_minimum: number; - progression_reward: number; - telecrystal_reward: number; - telecrystal_penalty: number; - ui_buttons?: ObjectiveUiButton[]; - objective_state: ObjectiveState; - original_progression: number; - final_objective: BooleanLike; -}; - -export type ObjectiveUiButton = { - name: string; - tooltip: string; - icon: string; - action: string; -}; - -type ObjectiveMenuProps = { - activeObjectives: Objective[]; - potentialObjectives: Objective[]; - maximumActiveObjectives: number; - maximumPotentialObjectives: number; - - handleStartObjective: (objective: Objective) => void; - handleObjectiveAction: (objective: Objective, action: string) => void; - handleObjectiveCompleted: (objective: Objective) => void; - handleObjectiveAbort: (objective: Objective) => void; - handleRequestObjectives: () => void; -}; - -type ObjectiveMenuState = { - draggingObjective: Objective | null; - objectiveX: number; - objectiveY: number; -}; - -let dragClickTimer = 0; - -export class ObjectiveMenu extends Component< - ObjectiveMenuProps, - ObjectiveMenuState -> { - constructor(props) { - super(props); - this.state = { - draggingObjective: null, - objectiveX: 0, - objectiveY: 0, - }; - - this.handleObjectiveClick = this.handleObjectiveClick.bind(this); - this.handleMouseUp = this.handleMouseUp.bind(this); - this.handleMouseMove = this.handleMouseMove.bind(this); - this.handleObjectiveAdded = this.handleObjectiveAdded.bind(this); - } - - handleObjectiveClick(event: MouseEvent, objective: Objective) { - if (this.state?.draggingObjective) { - return; - } - if (event.button === 0) { - // Left click - this.setState({ - draggingObjective: objective, - objectiveX: event.clientX, - objectiveY: event.clientY, - }); - window.addEventListener('mouseup', this.handleMouseUp as any); - window.addEventListener('mousemove', this.handleMouseMove as any); - event.stopPropagation(); - event.preventDefault(); - - dragClickTimer = Date.now() + 100; // 100 milliseconds - } - } - - handleMouseUp(event: MouseEvent) { - if (dragClickTimer > Date.now()) { - return; - } - - window.removeEventListener('mouseup', this.handleMouseUp as any); - window.removeEventListener('mousemove', this.handleMouseMove as any); - this.setState({ - draggingObjective: null, - }); - } - - handleMouseMove(event: MouseEvent) { - this.setState({ - objectiveX: event.pageX, - objectiveY: event.pageY - 32, - }); - } - - handleObjectiveAdded(event: MouseEvent) { - const { draggingObjective } = this.state as ObjectiveMenuState; - if (!draggingObjective) { - return; - } - const { handleStartObjective } = this.props; - handleStartObjective(draggingObjective); - } - - render() { - const { - activeObjectives = [], - potentialObjectives, - maximumActiveObjectives, - maximumPotentialObjectives, - handleObjectiveAction, - handleObjectiveCompleted, - handleObjectiveAbort, - handleRequestObjectives, - } = this.props; - const { draggingObjective, objectiveX, objectiveY } = this - .state as ObjectiveMenuState; - - potentialObjectives.sort((objA, objB) => { - if (objA.progression_minimum < objB.progression_minimum) { - return 1; - } else if (objA.progression_minimum > objB.progression_minimum) { - return -1; - } - return 0; - }); - return ( - <> - - -
- - {Array.apply(null, Array(maximumActiveObjectives)).map( - (_, index) => { - if (index >= activeObjectives.length) { - return ( - - - - - Empty Objective, drop objectives here to take - them - - - - - ); - } - const objective = activeObjectives[index]; - return ( - - {ObjectiveFunction( - objective, - true, - handleObjectiveAction, - handleObjectiveCompleted, - handleObjectiveAbort, - true, - )} - - ); - }, - )} - -
-
- -
- - {potentialObjectives.map((objective) => { - return ( - { - this.handleObjectiveClick(event, objective); - }} - > - {(objective.id !== draggingObjective?.id && - ObjectiveFunction( - objective, - false, - undefined, - undefined, - undefined, - true, - )) || ( - - )} - - ); - })} - {(maximumPotentialObjectives === 0 && ( - - - - You are locked out of objectives - - - )) || - (potentialObjectives.length < maximumPotentialObjectives && ( - - - -
-
-
- {!!draggingObjective && ( - - {ObjectiveFunction(draggingObjective, false)} - - )} - - ); - } -} - -const ObjectiveFunction = ( - objective: Objective, - active: boolean, - handleObjectiveAction?: (objective: Objective, action: string) => void, - handleCompletion?: (objective: Objective) => void, - handleAbort?: (objective: Objective) => void, - grow: boolean = false, -) => { - const dangerLevel = getDangerLevel(objective.progression_minimum); - return ( - { - if (handleCompletion) { - handleCompletion(objective); - } - }} - handleAbort={(event) => { - if (handleAbort) { - handleAbort(objective); - } - }} - uiButtons={ - active && handleObjectiveAction ? ( - - {objective.ui_buttons?.map((value, index) => ( - - -
- ) : null} -
- )} - {!!uiButtons && !objectiveFinished && ( - {uiButtons} - )} - -
- - - ); -}; diff --git a/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx b/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx index d88f2d014988..366f26b648ff 100644 --- a/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx +++ b/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx @@ -1,17 +1,16 @@ -import { Box, Button, Dimmer, Section, Stack } from 'tgui-core/components'; +import { Box, Button, Section, Stack } from 'tgui-core/components'; import { useBackend } from '../../backend'; -import { ObjectiveElement } from './ObjectiveMenu'; +import { ObjectiveElement } from './ObjectiveElement'; type PrimaryObjectiveMenuProps = { primary_objectives; - final_objective; can_renegotiate; }; export const PrimaryObjectiveMenu = (props: PrimaryObjectiveMenuProps) => { const { act } = useBackend(); - const { primary_objectives, final_objective, can_renegotiate } = props; + const { primary_objectives, can_renegotiate } = props; return (
@@ -20,57 +19,13 @@ export const PrimaryObjectiveMenu = (props: PrimaryObjectiveMenuProps) => { Your Primary Objectives are as follows. Complete these at all costs. - - Completing Secondary Objectives may allow you to aquire additional - equipment. - - {final_objective && ( - - - PRIORITY MESSAGE -
- SOURCE: xxx.xxx.xxx.224:41394 -
-
- \\Debrief in progress. -
- \\Final Objective confirmed complete.
- \\Your work is done here, agent. -
-
- CONNECTION CLOSED_ -
-
- )} {primary_objectives.map((prim_obj, index) => ( ))} diff --git a/tgui/packages/tgui/interfaces/Uplink/constants.ts b/tgui/packages/tgui/interfaces/Uplink/constants.ts deleted file mode 100644 index 3cda3fe45de1..000000000000 --- a/tgui/packages/tgui/interfaces/Uplink/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum ObjectiveState { - Inactive = 1, - Active = 2, - Completed = 3, - Failed = 4, - Invalid = 5, -} diff --git a/tgui/packages/tgui/interfaces/Uplink/index.tsx b/tgui/packages/tgui/interfaces/Uplink/index.tsx index bef59720e33b..7e2a65519a77 100644 --- a/tgui/packages/tgui/interfaces/Uplink/index.tsx +++ b/tgui/packages/tgui/interfaces/Uplink/index.tsx @@ -18,11 +18,9 @@ import { Window } from '../../layouts'; import { calculateDangerLevel, calculateProgression, - dangerDefault, dangerLevelsTooltip, } from './calculateDangerLevel'; import { GenericUplink, type Item } from './GenericUplink'; -import { type Objective, ObjectiveMenu } from './ObjectiveMenu'; import { PrimaryObjectiveMenu } from './PrimaryObjectiveMenu'; type UplinkItem = { @@ -40,6 +38,7 @@ type UplinkItem = { restricted_roles: string; restricted_species: string; progression_minimum: number; + population_minimum: number; cost_override_string: string; lock_other_purchases: BooleanLike; ref?: string; @@ -48,9 +47,8 @@ type UplinkItem = { type UplinkData = { telecrystals: number; progression_points: number; + joined_population?: number; lockable: BooleanLike; - current_expected_progression: number; - progression_scaling_deviance: number; current_progression_scaling: number; uplink_flag: number; assigned_role: string; @@ -64,16 +62,10 @@ type UplinkData = { [key: string]: number; }; - has_objectives: BooleanLike; has_progression: BooleanLike; primary_objectives: { [key: number]: string; }; - completed_final_objective: string; - potential_objectives: Objective[]; - active_objectives: Objective[]; - maximum_active_objectives: number; - maximum_potential_objectives: number; purchased_items: number; shop_locked: BooleanLike; can_renegotiate: BooleanLike; @@ -142,13 +134,15 @@ export class Uplink extends Component { uplinkData.items = uplinkData.items.filter((value) => { if ( value.restricted_roles.length > 0 && - !value.restricted_roles.includes(uplinkRole) + !value.restricted_roles.includes(uplinkRole) && + !data.debug ) { return false; } if ( value.restricted_species.length > 0 && - !value.restricted_species.includes(uplinkSpecies) + !value.restricted_species.includes(uplinkSpecies) && + !data.debug ) { return false; } @@ -179,17 +173,10 @@ export class Uplink extends Component { const { telecrystals, progression_points, + joined_population, primary_objectives, can_renegotiate, - completed_final_objective, - active_objectives, - potential_objectives, - has_objectives, has_progression, - maximum_active_objectives, - maximum_potential_objectives, - current_expected_progression, - progression_scaling_deviance, current_progression_scaling, extra_purchasable, extra_purchasable_stock, @@ -212,6 +199,8 @@ export class Uplink extends Component { const item = itemsToAdd[i]; const hasEnoughProgression = progression_points >= item.progression_minimum; + const hasEnoughPop = + !joined_population || joined_population >= item.population_minimum; let stock: number | null = current_stock[item.stock_key]; if (item.ref) { @@ -255,8 +244,14 @@ export class Uplink extends Component { )}
), + population_tooltip: + 'This item is not cleared for operations performed against stations crewed by fewer than ' + + item.population_minimum + + ' people.', + insufficient_population: !hasEnoughPop, disabled: !canBuy || + !hasEnoughPop || (has_progression && !hasEnoughProgression) || (item.lock_other_purchases && purchased_items > 0), extraData: { @@ -266,116 +261,57 @@ export class Uplink extends Component { }, }); } - // Get the difference between the current progression and - // expected progression - let progressionPercentage = - current_expected_progression - progression_points; - // Clamp it down between 0 and 2 - progressionPercentage = Math.min( - Math.max(progressionPercentage / progression_scaling_deviance, -1), - 1, - ); - // Round it and convert it into a percentage - progressionPercentage = Math.round(progressionPercentage * 1000) / 10; + return ( - {!!has_progression && ( - -
- + +
+ + {!!has_progression && ( - - Your current level of threat. Threat - determines - {has_objectives - ? ' the severity of secondary objectives you get and ' - : ' '} - what items you can purchase.  - - {/* A minute in deciseconds */} - Threat passively increases by{' '} - - {calculateProgression( - current_progression_scaling, - )} - -  every minute + Your current level of threat. Threat + determines what items you can purchase.  + + {/* A minute in deciseconds */} + Threat passively increases by{' '} + + {calculateProgression( + current_progression_scaling, + )} - {Math.abs(progressionPercentage) > 0 && ( - - Because your threat level is - {progressionPercentage < 0 - ? ' ahead ' - : ' behind '} - of where it should be, you are getting - - {progressionPercentage}% - - {progressionPercentage < 0 - ? 'less' - : 'more'}{' '} - threat every minute - - )} - {dangerLevelsTooltip} +  every minute + {dangerLevelsTooltip} - )) || - "Your current threat level. You are a killing machine and don't need to improve your threat level." + } > - {/* If we have no progression, - just give them a generic title */} - {has_progression - ? calculateDangerLevel(progression_points, false) - : calculateDangerLevel(dangerDefault, false)} + {calculateDangerLevel(progression_points, false)} - + )} + {!!primary_objectives && ( - {!!has_objectives && ( - <> - this.setState({ currentTab: 0 })} - > - Primary Objectives - - this.setState({ currentTab: 1 })} - > - Secondary Objectives - - + {primary_objectives && ( + this.setState({ currentTab: 0 })} + > + Primary Objectives + )} { textOverflow: 'ellipsis', }} icon="store" - selected={currentTab === 2 || !has_objectives} + selected={currentTab === 2} onClick={() => this.setState({ currentTab: 2 })} > Market + )} - {!!lockable && ( - - - - )} - -
-
- )} + {!!lockable && ( + + + + )} +
+
+
- {(currentTab === 0 && has_objectives && ( + {(currentTab === 0 && primary_objectives && ( - )) || - (currentTab === 1 && has_objectives && ( - - act('objective_act', { - check: objective.original_progression, - objective_action: action, - index: objective.id, - }) - } - handleStartObjective={(objective) => - act('start_objective', { - check: objective.original_progression, - index: objective.id, - }) - } - handleObjectiveAbort={(objective) => - act('objective_abort', { - check: objective.original_progression, - index: objective.id, - }) - } - handleObjectiveCompleted={(objective) => - act('finish_objective', { - check: objective.original_progression, - index: objective.id, - }) - } - handleRequestObjectives={() => act('regenerate_objectives')} + )) || ( + <> + { + if (!item.extraData?.ref) { + act('buy', { path: item.id }); + } else { + act('buy', { ref: item.extraData.ref }); + } + }} /> - )) || ( - <> - { - if (!item.extraData?.ref) { - act('buy', { path: item.id }); - } else { - act('buy', { ref: item.extraData.ref }); - } - }} - /> - {(shop_locked && ( - - - SHOP LOCKED - - - )) || - null} - - )} + {(shop_locked && !data.debug && ( + + + SHOP LOCKED + + + )) || + null} + + )}
diff --git a/tgui/packages/tgui/interfaces/VotePanel.tsx b/tgui/packages/tgui/interfaces/VotePanel.tsx index 183882cdba7c..c41c27c1b66d 100644 --- a/tgui/packages/tgui/interfaces/VotePanel.tsx +++ b/tgui/packages/tgui/interfaces/VotePanel.tsx @@ -300,7 +300,9 @@ const ChoicesPanel = (props) => { user.multiSelection[user.ckey.concat(choice.name)] === 1 ? ( ) : null} - {choice.votes} Votes + {currentVote.displayStatistics + ? `${choice.votes} Votes` + : null} diff --git a/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx b/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx index 7775bff26cbf..bfbd47ffe95f 100644 --- a/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx +++ b/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx @@ -34,6 +34,8 @@ export function MalfAiModules(props) { icon: item.icon, id: item.name, name: item.name, + population_tooltip: '', + insufficient_population: false, }); } }