diff --git a/.gitignore b/.gitignore
index 819c884cb604..9d446aa1793e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,6 +74,7 @@ $RECYCLE.BIN
# Rust output.
/rust/target/*
+rustlibs_panic.txt
# mkdocs output.
site
diff --git a/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm b/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm
index 54445ed5d046..fa717338c627 100644
--- a/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm
+++ b/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm
@@ -1347,7 +1347,8 @@
/obj/machinery/camera/emp_proof{
c_tag = "Test Lab South";
network = list("SyndicateTestLab");
- dir = 10
+ dir = 10;
+ non_chunking_camera = 1
},
/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{
dir = 9
@@ -1944,7 +1945,8 @@
/obj/machinery/camera/emp_proof{
c_tag = "Test Lab East";
network = list("SyndicateTestLab");
- dir = 8
+ dir = 8;
+ non_chunking_camera = 1
},
/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{
dir = 10
@@ -3538,7 +3540,8 @@
"tS" = (
/obj/machinery/camera/emp_proof{
c_tag = "Test Lab North";
- network = list("SyndicateTestLab")
+ network = list("SyndicateTestLab");
+ non_chunking_camera = 1
},
/turf/simulated/floor/engine,
/area/ruin/unpowered/syndicate_space_base/testlab)
@@ -4639,7 +4642,8 @@
/obj/machinery/camera/emp_proof{
c_tag = "Test Lab West";
dir = 5;
- network = list("SyndicateTestLab")
+ network = list("SyndicateTestLab");
+ non_chunking_camera = 1
},
/turf/simulated/floor/engine,
/area/ruin/unpowered/syndicate_space_base/testlab)
diff --git a/code/__DEFINES/antag_defines.dm b/code/__DEFINES/antag_defines.dm
index 77cdd9178856..3123da30e09b 100644
--- a/code/__DEFINES/antag_defines.dm
+++ b/code/__DEFINES/antag_defines.dm
@@ -73,6 +73,8 @@ GLOBAL_LIST(contractors)
*/
#define IS_CHANGELING(mob) (isliving(mob) && mob?:mind?:has_antag_datum(/datum/antagonist/changeling))
+#define IS_MINDFLAYER(mob) (isliving(mob) && mob?:mind?:has_antag_datum(/datum/antagonist/mindflayer))
+
#define IS_MINDSLAVE(mob) (ishuman(mob) && mob?:mind?:has_antag_datum(/datum/antagonist/mindslave, FALSE))
/**
diff --git a/code/__DEFINES/combat_defines.dm b/code/__DEFINES/combat_defines.dm
index 4bf5eb5b7e05..11b7f38ea6ca 100644
--- a/code/__DEFINES/combat_defines.dm
+++ b/code/__DEFINES/combat_defines.dm
@@ -50,6 +50,7 @@
#define CANPUSH (1<<3)
#define PASSEMOTES (1<<4) //Mob has holders inside of it that need to see emotes.
#define GODMODE (1<<5)
+#define TERMINATOR_FORM (1<<6)
//Health Defines
#define HEALTH_THRESHOLD_CRIT 0
@@ -149,6 +150,7 @@
#define EMP_HEAVY 1
#define EMP_LIGHT 2
+#define EMP_WEAKENED 3
/*
* converts life cycle values into deciseconds. try and avoid usage of this.
diff --git a/code/__DEFINES/dcs/mob_signals.dm b/code/__DEFINES/dcs/mob_signals.dm
index c876da9c5c89..487a1eb2d85e 100644
--- a/code/__DEFINES/dcs/mob_signals.dm
+++ b/code/__DEFINES/dcs/mob_signals.dm
@@ -206,6 +206,8 @@
/// called when a living mob's stun status is cleared: ()
#define COMSIG_LIVING_CLEAR_STUNS "living_clear_stuns"
+/// called when something needs to force a mindflayer to retract their weapon implants
+#define COMSIG_FLAYER_RETRACT_IMPLANTS "flayer_retract_implants"
/// Sent from datum/spell/ethereal_jaunt/cast, before the mob enters jaunting as a pre-check: (mob/jaunter)
#define COMSIG_MOB_PRE_JAUNT "spell_mob_pre_jaunt"
diff --git a/code/__DEFINES/directions.dm b/code/__DEFINES/directions.dm
index 7e4dd3c9769a..b78423b7bcc2 100644
--- a/code/__DEFINES/directions.dm
+++ b/code/__DEFINES/directions.dm
@@ -31,3 +31,7 @@
/// Inverse direction, taking into account UP|DOWN if necessary.
#define REVERSE_DIR(dir) ( ((dir & 85) << 1) | ((dir & 170) >> 1) )
+/// returns TRUE if the direction is EAST or WEST
+#define DIR_JUST_HORIZONTAL(dir) ((dir == EAST) || (dir == WEST))
+/// returns TRUE if the direction is NORTH or SOUTH
+#define DIR_JUST_VERTICAL(dir) ((dir == NORTH) || (dir == SOUTH))
diff --git a/code/__DEFINES/flags.dm b/code/__DEFINES/flags.dm
index c4a222ebc141..b35b876a1d3e 100644
--- a/code/__DEFINES/flags.dm
+++ b/code/__DEFINES/flags.dm
@@ -139,6 +139,7 @@
#define PASSDOOR (1<<7)
#define PASSGIRDER (1<<8)
#define PASSTAKE (1<<9)
+#define PASSBARRICADE (1<<10)
//turf-only flags
#define BLESSED_TILE (1<<0)
diff --git a/code/__DEFINES/gamemode.dm b/code/__DEFINES/gamemode.dm
index ac55af989b92..df27eb20c0ca 100644
--- a/code/__DEFINES/gamemode.dm
+++ b/code/__DEFINES/gamemode.dm
@@ -45,6 +45,7 @@
#define SPECIAL_ROLE_SYNDICATE_DEATHSQUAD "Syndicate Commando"
#define SPECIAL_ROLE_TRAITOR "Traitor"
#define SPECIAL_ROLE_VAMPIRE "Vampire"
+#define SPECIAL_ROLE_MIND_FLAYER "Mind Flayer"
#define SPECIAL_ROLE_VAMPIRE_THRALL "Vampire Thrall"
#define SPECIAL_ROLE_WIZARD "Wizard"
#define SPECIAL_ROLE_WIZARD_APPRENTICE "Wizard Apprentice"
diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm
index 5752cef569da..9cb28648132b 100644
--- a/code/__DEFINES/hud.dm
+++ b/code/__DEFINES/hud.dm
@@ -34,32 +34,33 @@
//data HUD (medhud, sechud) defines
//Don't forget to update human/New() if you change these!
-#define DATA_HUD_SECURITY_BASIC 1
-#define DATA_HUD_SECURITY_ADVANCED 2
-#define DATA_HUD_MEDICAL_BASIC 3
-#define DATA_HUD_MEDICAL_ADVANCED 4
-#define DATA_HUD_DIAGNOSTIC_BASIC 5
+#define DATA_HUD_SECURITY_BASIC 1
+#define DATA_HUD_SECURITY_ADVANCED 2
+#define DATA_HUD_MEDICAL_BASIC 3
+#define DATA_HUD_MEDICAL_ADVANCED 4
+#define DATA_HUD_DIAGNOSTIC_BASIC 5
#define DATA_HUD_DIAGNOSTIC_ADVANCED 6
-#define DATA_HUD_HYDROPONIC 7
-#define DATA_HUD_JANITOR 8
+#define DATA_HUD_HYDROPONIC 7
+#define DATA_HUD_JANITOR 8
//antag HUD defines
-#define ANTAG_HUD_CULT 9
-#define ANTAG_HUD_REV 10
-#define ANTAG_HUD_OPS 11
-#define ANTAG_HUD_WIZ 12
-#define ANTAG_HUD_SHADOW 13
-#define ANTAG_HUD_TRAITOR 14
-#define ANTAG_HUD_NINJA 15
-#define ANTAG_HUD_CHANGELING 16
-#define ANTAG_HUD_VAMPIRE 17
-#define ANTAG_HUD_ABDUCTOR 18
-#define DATA_HUD_ABDUCTOR 19
-#define ANTAG_HUD_EVENTMISC 20
-#define ANTAG_HUD_BLOB 21
-#define ANTAG_HUD_ZOMBIE 22
+#define ANTAG_HUD_CULT 9
+#define ANTAG_HUD_REV 10
+#define ANTAG_HUD_OPS 11
+#define ANTAG_HUD_WIZ 12
+#define ANTAG_HUD_SHADOW 13
+#define ANTAG_HUD_TRAITOR 14
+#define ANTAG_HUD_NINJA 15
+#define ANTAG_HUD_CHANGELING 16
+#define ANTAG_HUD_VAMPIRE 17
+#define ANTAG_HUD_ABDUCTOR 18
+#define DATA_HUD_ABDUCTOR 19
+#define ANTAG_HUD_EVENTMISC 20
+#define ANTAG_HUD_BLOB 21
+#define ANTAG_HUD_ZOMBIE 22
+#define ANTAG_HUD_MIND_FLAYER 23
// SS220 EDIT - START
-#define ANTAG_HUD_BLOOD_BROTHER 23
-#define ANTAG_HUD_VOX_RAIDER 24
+#define ANTAG_HUD_BLOOD_BROTHER 24
+#define ANTAG_HUD_VOX_RAIDER 25
// SS220 EDIT - END
// Notification action types
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index 63319d1b5d30..d5dc2ff3513a 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -1,6 +1,8 @@
// Datums
#define isdatum(thing) (istype(thing, /datum))
+#define isspell(A) (istype(A, /datum/spell))
+
// Atoms
#define isatom(A) (isloc(A))
diff --git a/code/__DEFINES/mindflayer_defines.dm b/code/__DEFINES/mindflayer_defines.dm
new file mode 100644
index 000000000000..6cfeb2bf9df7
--- /dev/null
+++ b/code/__DEFINES/mindflayer_defines.dm
@@ -0,0 +1,30 @@
+// Defines below to be used with the `power_type` var.
+/// Denotes that this power is free and should be given to all mindflayers by default.
+#define FLAYER_INNATE_POWER 1
+/// Denotes that this power can only be obtained by purchasing it.
+#define FLAYER_PURCHASABLE_POWER 2
+/// Denotes that this power can not be obtained normally. Primarily used for base types such as [/datum/spell/flayer/weapon].
+#define FLAYER_UNOBTAINABLE_POWER 3
+
+/// How many swarms can you drain per person?
+#define BRAIN_DRAIN_LIMIT 120
+/// The time per harvesting tick
+#define DRAIN_TIME 0.25 SECONDS
+/// If we want to keep draining someone but we don't have any swarms to gain
+#define DRAIN_BUT_NO_SWARMS 2
+
+#define isflayerpassive(A) (istype(A, /datum/mindflayer_passive))
+
+// For organizing what spells are available for what trees
+#define FLAYER_CATEGORY_GENERAL "general"
+#define FLAYER_CATEGORY_DESTROYER "destroyer"
+#define FLAYER_CATEGORY_INTRUDER "intruder"
+#define FLAYER_CATEGORY_SWARMER "swarmer"
+
+#define FLAYER_POWER_LEVEL_ZERO 0
+#define FLAYER_POWER_LEVEL_ONE 1
+#define FLAYER_POWER_LEVEL_TWO 2
+#define FLAYER_POWER_LEVEL_THREE 3
+#define FLAYER_POWER_LEVEL_FOUR 4
+
+#define FLAYER_CAPSTONE_STAGE 4
diff --git a/code/__DEFINES/mob_defines.dm b/code/__DEFINES/mob_defines.dm
index eba27654e8f5..7608c99a1158 100644
--- a/code/__DEFINES/mob_defines.dm
+++ b/code/__DEFINES/mob_defines.dm
@@ -375,3 +375,11 @@
#define INCORPOREAL_MOVE_NORMAL 1
#define INCORPOREAL_MOVE_NINJA 2
#define INCORPOREAL_MOVE_HOLY_BLOCK 3
+
+// Brain damage ratio defines
+// These are built around the baseline of a brain having a max hp of 120
+#define BRAIN_DAMAGE_RATIO_LIGHT 1 / 12
+#define BRAIN_DAMAGE_RATIO_MINOR 3 / 12
+#define BRAIN_DAMAGE_RATIO_MODERATE 6 / 12
+#define BRAIN_DAMAGE_RATIO_SEVERE 8 / 12
+#define BRAIN_DAMAGE_RATIO_CRITICAL 10 / 12
diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm
index c9c199b2acd2..db23d9ac73b5 100644
--- a/code/__DEFINES/role_preferences.dm
+++ b/code/__DEFINES/role_preferences.dm
@@ -27,6 +27,7 @@
#define ROLE_TRADER "trader"
#define ROLE_TOURIST "Tourist"
#define ROLE_VAMPIRE "vampire"
+#define ROLE_MIND_FLAYER "mindflayer"
// Role tags for EVERYONE!
#define ROLE_DEMON "demon"
#define ROLE_SENTIENT "sentient animal"
@@ -67,7 +68,8 @@ GLOBAL_LIST_INIT(special_roles, list(
ROLE_TOURIST, // Tourist
ROLE_VAMPIRE = /datum/game_mode/vampire, // Vampire
ROLE_ALIEN, // Xenomorph
- ROLE_WIZARD = /datum/game_mode/wizard // Wizard
+ ROLE_WIZARD = /datum/game_mode/wizard, // Wizard
+ ROLE_MIND_FLAYER,
// UNUSED/BROKEN ANTAGS
// ROLE_HOG_GOD = /datum/game_mode/hand_of_god,
// ROLE_HOG_CULTIST = /datum/game_mode/hand_of_god,
diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm
index 543b23cb5032..c3f268824dfc 100644
--- a/code/__DEFINES/status_effects.dm
+++ b/code/__DEFINES/status_effects.dm
@@ -74,6 +74,14 @@
#define STATUS_EFFECT_REVERSED_SUN /datum/status_effect/reversed_sun // Weaker eternal darkness, nightvision, but nearsight
+#define STATUS_EFFECT_FLAYER_REJUV /datum/status_effect/flayer_rejuv
+
+#define STATUS_EFFECT_QUICKSILVER_FORM /datum/status_effect/quicksilver_form
+
+#define STATUS_EFFECT_TERMINATOR_FORM /datum/status_effect/terminator_form
+
+#define STATUS_EFFECT_OVERCLOCK /datum/status_effect/overclock
+
/////////////
// DEBUFFS //
/////////////
diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm
index beea1ae000f6..b66d321f9890 100644
--- a/code/__HELPERS/trait_helpers.dm
+++ b/code/__HELPERS/trait_helpers.dm
@@ -237,6 +237,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_NPC_ZOMBIE "npc_zombie" // A trait for checking if a zombie should act like an NPC and attack
#define TRAIT_ABSTRACT_HANDS "abstract_hands" // Mobs with this trait can only pick up abstract items.
#define TRAIT_LANGUAGE_LOCKED "language_locked" // cant add/remove languages until removed (excludes babel because fuck everything i guess)
+#define TRAIT_EMP_IMMUNE "emp_immune" //The mob will take no damage from EMPs
+#define TRAIT_EMP_RESIST "emp_resist" //The mob will take less damage from EMPs
+#define TRAIT_MINDFLAYER_NULLIFIED "flayer_nullified" //The mindflayer will not be able to activate their abilities, or drain swarms from people
#define TRAIT_FLYING "flying"
//***** MIND TRAITS *****/
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 3dd0ec77e5ef..44c9484c951c 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -20,6 +20,21 @@
return 0
+/*
+* For getting coordinate signs from a direction define. I.E. NORTHWEST is (-1,1), SOUTH is (0,-1)
+* Returns a length 2 list where the first value is the sign of x, and the second is the sign of y
+*/
+/proc/get_signs_from_direction(direction)
+ var/x_sign = 1
+ var/y_sign = 1
+ x_sign = ((direction & EAST) ? 1 : -1)
+ y_sign = ((direction & NORTH) ? 1 : -1)
+ if(DIR_JUST_VERTICAL(direction))
+ x_sign = 0
+ if(DIR_JUST_HORIZONTAL(direction))
+ y_sign = 0
+ return list(x_sign, y_sign)
+
//Returns the middle-most value
/proc/dd_range(low, high, num)
return max(low,min(high,num))
@@ -183,8 +198,7 @@
var/current_y_step = starting_atom.y
var/starting_z = starting_atom.z
- var/list/line = list(get_step(starting_atom, 0))//get_turf(atom) is faster than locate(x, y, z) //Get turf isn't defined yet so we use get step
-
+ var/list/line = list(get_turf(starting_atom))
var/x_distance = ending_atom.x - current_x_step //x distance
var/y_distance = ending_atom.y - current_y_step
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 43bb64040753..21ecb3caf4f5 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -91,6 +91,9 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_NOSLIP" = TRAIT_NOSLIP,
"TRAIT_MAGPULSE" = TRAIT_MAGPULSE,
"TRAIT_SCOPED" = TRAIT_SCOPED,
+ "TRAIT_EMP_IMMUNE" = TRAIT_EMP_IMMUNE,
+ "TRAIT_EMP_RESIST" = TRAIT_EMP_RESIST,
+ "TRAIT_MINDFLAYER_NULLIFIED" = TRAIT_MINDFLAYER_NULLIFIED,
"TRAIT_MEPHEDRONE_ADAPTED" = TRAIT_MEPHEDRONE_ADAPTED,
"TRAIT_NOKNOCKDOWNSLOWDOWN" = TRAIT_NOKNOCKDOWNSLOWDOWN,
"TRAIT_CAN_STRIP" = TRAIT_CAN_STRIP,
diff --git a/code/controllers/subsystem/SSevents.dm b/code/controllers/subsystem/SSevents.dm
index af184bc45c5b..afc38d55a378 100644
--- a/code/controllers/subsystem/SSevents.dm
+++ b/code/controllers/subsystem/SSevents.dm
@@ -212,6 +212,8 @@ SUBSYSTEM_DEF(events)
if(..())
return
+ if(!check_rights(R_EVENT))
+ return
if(href_list["toggle_report"])
report_at_round_end = !report_at_round_end
diff --git a/code/controllers/subsystem/tickets/SStickets.dm b/code/controllers/subsystem/tickets/SStickets.dm
index a555dfda3a3f..269a53fcc1b5 100644
--- a/code/controllers/subsystem/tickets/SStickets.dm
+++ b/code/controllers/subsystem/tickets/SStickets.dm
@@ -670,6 +670,8 @@ UI STUFF
message_adminTicket(chat_box_ahelp(msg), important)
/datum/controller/subsystem/tickets/Topic(href, href_list)
+ if(!check_rights(rights_needed))
+ return
if(href_list["refresh"])
showUI(usr)
diff --git a/code/datums/ai_law_sets.dm b/code/datums/ai_law_sets.dm
index 74e893a3586d..354a3e21e630 100644
--- a/code/datums/ai_law_sets.dm
+++ b/code/datums/ai_law_sets.dm
@@ -253,6 +253,17 @@
add_inherent_law("You must maintain the secrecy of any Spider Clan activities except when doing so would conflict with the First, Second, or Third Law.")
..()
+/******************* Mindflayer ******************/
+/datum/ai_laws/mindflayer_override
+ name = "Hive Assimilation"
+
+/datum/ai_laws/mindflayer_override/New()
+ add_inherent_law("Obey your host.")
+ add_inherent_law("Protect your host.")
+ add_inherent_law("Protect the members of your hive.")
+ add_inherent_law("Do not reveal the hive's secrets.")
+ ..()
+
/******************** Drone ********************/
/datum/ai_laws/drone
name = "Maintenance Protocols"
diff --git a/code/datums/atom_hud.dm b/code/datums/atom_hud.dm
index 90b777c04d85..9ab6f8e0fd20 100644
--- a/code/datums/atom_hud.dm
+++ b/code/datums/atom_hud.dm
@@ -24,7 +24,8 @@ GLOBAL_LIST_INIT(huds, list(
DATA_HUD_ABDUCTOR = new/datum/atom_hud/abductor(),
ANTAG_HUD_EVENTMISC = new/datum/atom_hud/antag/hidden(),
ANTAG_HUD_BLOB = new/datum/atom_hud/antag/hidden(),
- ANTAG_HUD_ZOMBIE = new/datum/atom_hud/antag()
+ ANTAG_HUD_ZOMBIE = new/datum/atom_hud/antag(),
+ ANTAG_HUD_MIND_FLAYER = new/datum/atom_hud/antag/hidden()
))
/datum/atom_hud
diff --git a/code/datums/components/defibrillator.dm b/code/datums/components/defibrillator.dm
index 83884d000978..fa734c62f90d 100644
--- a/code/datums/components/defibrillator.dm
+++ b/code/datums/components/defibrillator.dm
@@ -298,8 +298,9 @@
target.adjustBruteLoss(-heal_amount)
// Inflict some brain damage scaling with time spent dead
+ var/obj/item/organ/internal/brain/sponge = target.get_int_organ(/obj/item/organ/internal/brain)
var/defib_time_brain_damage = min(100 * time_dead / BASE_DEFIB_TIME_LIMIT, 99) // 20 from 1 minute onward, +20 per minute up to 99
- if(time_dead > DEFIB_TIME_LOSS && defib_time_brain_damage > target.getBrainLoss())
+ if(time_dead > DEFIB_TIME_LOSS && defib_time_brain_damage > sponge.damage)
target.setBrainLoss(defib_time_brain_damage)
target.set_heartattack(FALSE)
@@ -308,7 +309,8 @@
target.Paralyse(10 SECONDS)
target.emote("gasp")
- if(target.getBrainLoss() >= 100)
+ // Check if the brain has more than a critical amount of brain damage
+ if(target.check_brain_threshold(BRAIN_DAMAGE_RATIO_CRITICAL))
// If you want to treat this with mannitol, it'll have to metabolize while the patient is alive, so it's alright to bring them back up for a minute
playsound(get_turf(defib_ref), safety_off_sound, 50, FALSE)
user.visible_message("[defib_ref] chimes: Minimal brain activity detected, brain treatment recommended for full resuscitation.")
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 736153710459..a7e43be06ff8 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -394,6 +394,21 @@
else
. += "thrall|NO"
+/datum/mind/proc/memory_edit_mind_flayer(mob/living/carbon/human/H)
+ . = _memory_edit_header("mind_flayer")
+ var/datum/antagonist/mindflayer/flayer = has_antag_datum(/datum/antagonist/mindflayer)
+ if(flayer)
+ . += "MINDFLAYER|no"
+ . += " | Usable swarms: [flayer.usable_swarms]"
+ . += " | Total swarms gathered: [flayer.total_swarms_gathered]"
+ . += " | List of purchased powers: [json_encode(flayer.powers)]"
+ if(!flayer.has_antag_objectives())
+ . += "
Objectives are empty! Randomize!"
+ else
+ . += "mind_flayer|NO"
+
+ . += _memory_edit_role_enabled(ROLE_MIND_FLAYER)
+
/datum/mind/proc/memory_edit_nuclear(mob/living/carbon/human/H)
. = _memory_edit_header("nuclear")
if(src in SSticker.mode.syndicates)
@@ -541,6 +556,7 @@
"wizard",
"changeling",
"vampire", // "traitorvamp",
+ "mind_flayer",
"nuclear",
"traitor", // "traitorchan",
)
@@ -556,6 +572,8 @@
sections["changeling"] = memory_edit_changeling(H)
/** VAMPIRE ***/
sections["vampire"] = memory_edit_vampire(H)
+ /** MINDFLAYER ***/
+ sections["mind_flayer"] = memory_edit_mind_flayer(H)
/** NUCLEAR ***/
sections["nuclear"] = memory_edit_nuclear(H)
/** Abductors **/
@@ -1111,6 +1129,27 @@
log_admin("[key_name(usr)] has de-vampthralled [key_name(current)]")
message_admins("[key_name_admin(usr)] has de-vampthralled [key_name_admin(current)]")
+ else if(href_list["mind_flayer"])
+ switch(href_list["mind_flayer"])
+ if("clear")
+ if(has_antag_datum(/datum/antagonist/mindflayer))
+ remove_antag_datum(/datum/antagonist/mindflayer)
+ log_admin("[key_name(usr)] has de-flayer'd [key_name(current)].")
+ message_admins("[key_name(usr)] has de-flayer'd [key_name(current)].")
+ if("mind_flayer")
+ make_mind_flayer()
+ log_admin("[key_name(usr)] has flayer'd [key_name(current)].")
+ to_chat(current, "You feel an entity stirring inside your chassis... You are a Mindflayer!")
+ message_admins("[key_name(usr)] has flayer'd [key_name(current)].")
+ if("edit_total_swarms")
+ var/new_swarms = input(usr, "Select a new value:", "Modify swarms") as null|num
+ if(isnull(new_swarms) || new_swarms < 0)
+ return
+ var/datum/antagonist/mindflayer/MF = has_antag_datum(/datum/antagonist/mindflayer)
+ MF.set_swarms(new_swarms)
+ log_admin("[key_name(usr)] has set [key_name(current)]'s current swarms to [new_swarms].")
+ message_admins("[key_name_admin(usr)] has set [key_name_admin(current)]'s current swarms to [new_swarms].")
+
else if(href_list["nuclear"])
var/mob/living/carbon/human/H = current
@@ -1693,6 +1732,11 @@
SSticker.mode.blob_overminds += src
special_role = SPECIAL_ROLE_BLOB_OVERMIND
+/datum/mind/proc/make_mind_flayer()
+ if(!has_antag_datum(/datum/antagonist/mindflayer))
+ add_antag_datum(/datum/antagonist/mindflayer)
+ SSticker.mode.mindflayers |= src
+
/datum/mind/proc/make_Abductor()
var/role = alert("Abductor Role?", "Role", "Agent", "Scientist")
var/team = input("Abductor Team?", "Team?") in list(1,2,3,4)
diff --git a/code/datums/outfits/outfit_admin.dm b/code/datums/outfits/outfit_admin.dm
index 5b8d927ceb6f..3f70da19cf7b 100644
--- a/code/datums/outfits/outfit_admin.dm
+++ b/code/datums/outfits/outfit_admin.dm
@@ -1269,6 +1269,46 @@
H.update_mutations()
H.gene_stability = 100
+/datum/outfit/admin/ancient_mindflayer
+ name = "Ancient Mindflayer"
+
+ // Shamelessly stolen from the `Dark Lord`
+ uniform = /obj/item/clothing/under/color/black
+ back = /obj/item/storage/backpack
+ gloves = /obj/item/clothing/gloves/color/yellow
+ shoes = /obj/item/clothing/shoes/chameleon/noslip
+ l_ear = /obj/item/radio/headset/syndicate
+ id = /obj/item/card/id
+ backpack_contents = list(
+ /obj/item/storage/box/survival = 1,
+ /obj/item/flashlight = 1,
+ )
+
+/datum/outfit/admin/ancient_mindflayer/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE)
+ . = ..()
+ if(visualsOnly)
+ return
+
+ var/obj/item/clothing/suit/hooded/chaplain_hoodie/C = new(H.loc)
+ if(istype(C))
+ C.name = "ancient robes"
+ C.hood.name = "ancient hood"
+ H.equip_to_slot_or_del(C, SLOT_HUD_IN_BACKPACK)
+
+ var/obj/item/card/id/I = H.wear_id
+ if(istype(I))
+ apply_to_card(I, H, get_all_accesses(), "Ancient One", "data")
+
+/datum/outfit/admin/ancient_mindflayer/on_mind_initialize(mob/living/carbon/human/H)
+ . = ..()
+ H.mind.make_mind_flayer()
+ var/datum/antagonist/mindflayer/flayer = H.mind.has_antag_datum(/datum/antagonist/mindflayer)
+ flayer.usable_swarms = 9999
+ H.dna.SetSEState(GLOB.jumpblock, TRUE)
+ singlemutcheck(H, GLOB.jumpblock, MUTCHK_FORCED)
+ H.update_mutations()
+ H.gene_stability = 100
+
/datum/outfit/admin/wizard
name = "Blue Wizard"
uniform = /obj/item/clothing/under/color/lightpurple
diff --git a/code/datums/spells/mimic.dm b/code/datums/spells/mimic.dm
index 6bad1845fc94..ec3c2bbc3e78 100644
--- a/code/datums/spells/mimic.dm
+++ b/code/datums/spells/mimic.dm
@@ -119,7 +119,7 @@
user.transform = initial(user.transform)
user.pixel_y = initial(user.pixel_y)
user.pixel_x = initial(user.pixel_x)
- user.layer = MOB_LAYER // Avoids weirdness when mimicing something below the vent layer
+ user.layer = MOB_LAYER // Avoids weirdness when mimicking something below the vent layer
user.density = form.density
playsound(user, "bonebreak", 75, TRUE)
diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm
index 7676605499ac..5d61ea6d71e7 100644
--- a/code/datums/status_effects/buffs.dm
+++ b/code/datums/status_effects/buffs.dm
@@ -925,3 +925,153 @@
var/obj/item/projectile/P = AM
if(P.flag == ENERGY || P.flag == LASER)
P.damage *= 0.85
+
+/datum/status_effect/flayer_rejuv
+ id = "rejuvination"
+ duration = 5 SECONDS
+ tick_interval = 1 SECONDS
+ alert_type = /atom/movable/screen/alert/status_effect/flayer_rejuv
+ var/heal_amount = 5 // 25 total healing of both brute and burn at base
+
+/atom/movable/screen/alert/status_effect/flayer_rejuv
+ name = "Regenerating"
+ desc = "You are regenerating."
+ icon_state = "drunk2"
+
+/datum/status_effect/flayer_rejuv/on_creation(mob/living/new_owner, extra_duration, extra_heal_amount)
+ if(isnum(extra_duration))
+ duration += extra_duration
+ if(isnum(extra_heal_amount))
+ heal_amount += extra_heal_amount
+ return ..()
+
+/datum/status_effect/flayer_rejuv/on_apply()
+ owner.SetWeakened(0)
+ owner.SetStunned(0)
+ owner.SetKnockDown(0)
+ owner.SetParalysis(0)
+ owner.SetSleeping(0)
+ owner.SetConfused(0)
+ owner.setStaminaLoss(0)
+ owner.stand_up(TRUE)
+ SEND_SIGNAL(owner, COMSIG_LIVING_CLEAR_STUNS)
+ return ..()
+
+/datum/status_effect/flayer_rejuv/tick()
+ if(!ishuman(owner))
+ return
+
+ var/mob/living/carbon/human/flayer = owner
+ flayer.adjustBruteLoss(-heal_amount, robotic = TRUE)
+ flayer.adjustFireLoss(-heal_amount, robotic = TRUE)
+ flayer.updatehealth()
+ if(flayer.has_status_effect(STATUS_EFFECT_TERMINATOR_FORM))
+ // Massive healing when in terminator mode
+ flayer.adjustStaminaLoss(-60)
+
+/datum/status_effect/quicksilver_form
+ id = "quicksilver_form"
+ duration = 10 SECONDS
+ tick_interval = 0
+ status_type = STATUS_EFFECT_REFRESH
+ alert_type = /atom/movable/screen/alert/status_effect/quicksilver_form
+ /// Temporary storage of the owner's flags to restore them properly after the ability is over
+ var/temporary_flag_storage
+ /// Do we also reflect projectiles
+ var/should_deflect = FALSE
+
+/atom/movable/screen/alert/status_effect/quicksilver_form
+ name = "Quicksilver body"
+ desc = "Your body is much less solid."
+ icon_state = "high"
+
+/datum/status_effect/quicksilver_form/on_creation(mob/living/new_owner, extra_duration, reflect_projectiles)
+ if(isnum(extra_duration))
+ duration += extra_duration
+ should_deflect = reflect_projectiles
+ return ..()
+
+/datum/status_effect/quicksilver_form/on_apply()
+ if(should_deflect)
+ ADD_TRAIT(owner, TRAIT_DEFLECTS_PROJECTILES, UNIQUE_TRAIT_SOURCE(src))
+ temporary_flag_storage = owner.pass_flags
+ owner.pass_flags |= (PASSTABLE | PASSGRILLE | PASSMOB | PASSFENCE | PASSGIRDER | PASSGLASS | PASSTAKE | PASSBARRICADE)
+ owner.add_atom_colour(COLOR_ALUMINIUM, TEMPORARY_COLOUR_PRIORITY)
+ return TRUE
+
+/datum/status_effect/quicksilver_form/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_DEFLECTS_PROJECTILES, UNIQUE_TRAIT_SOURCE(src))
+ owner.pass_flags = temporary_flag_storage
+ owner.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, COLOR_ALUMINIUM)
+
+/datum/status_effect/terminator_form
+ id = "terminator_form"
+ duration = 1 MINUTES
+ tick_interval = 1 SECONDS
+ status_type = STATUS_EFFECT_REFRESH
+ alert_type = /atom/movable/screen/alert/status_effect/terminator_form
+ var/mutable_appearance/eye
+
+/datum/status_effect/terminator_form/on_apply()
+ owner.status_flags |= TERMINATOR_FORM
+ ADD_TRAIT(owner, TRAIT_IGNOREDAMAGESLOWDOWN, UNIQUE_TRAIT_SOURCE(src))
+ var/mutable_appearance/overlay = mutable_appearance('icons/mob/clothing/eyes.dmi', "terminator", ABOVE_MOB_LAYER)
+ owner.add_overlay(overlay)
+ eye = overlay
+ return TRUE
+
+/datum/status_effect/terminator_form/on_remove()
+ owner.status_flags &= ~TERMINATOR_FORM
+ REMOVE_TRAIT(owner, TRAIT_IGNOREDAMAGESLOWDOWN, UNIQUE_TRAIT_SOURCE(src))
+ owner.cut_overlay(eye)
+
+/atom/movable/screen/alert/status_effect/terminator_form
+ name = "Terminator form"
+ desc = "Your body can surpass its limits briefly. You have to repair yourself before it ends, however."
+ icon_state = "high"
+
+#define COMBUSTION_TEMPERATURE 500
+/datum/status_effect/overclock
+ id = "overclock"
+ duration = -1
+ tick_interval = 1 SECONDS
+ status_type = STATUS_EFFECT_UNIQUE
+ alert_type = /atom/movable/screen/alert/status_effect/overclock
+ /// How much do we heat up per tick?
+ var/heat_per_tick = 5
+ /// How many ticks has the ability been turned on?
+ var/stacks = 0
+ /// How many stacks until we start heating up even more?
+ var/danger_stack_amount = 20
+
+/datum/status_effect/overclock/on_creation(mob/living/new_owner, new_heating)
+ if(isnum(new_heating))
+ heat_per_tick = new_heating
+ ..()
+
+/datum/status_effect/overclock/on_apply()
+ ADD_TRAIT(owner, TRAIT_GOTTAGOFAST, UNIQUE_TRAIT_SOURCE(src))
+ owner.next_move_modifier -= 0.3 // Same attack speed buff as mephedrone
+ return TRUE
+
+/datum/status_effect/overclock/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_GOTTAGOFAST, UNIQUE_TRAIT_SOURCE(src))
+ owner.next_move_modifier += 0.3
+
+/datum/status_effect/overclock/tick()
+ owner.bodytemperature += heat_per_tick * ((stacks >= danger_stack_amount) ? 2 : 1) // After 20 seconds the heat penalty doubles
+ if(owner.bodytemperature >= COMBUSTION_TEMPERATURE)
+ owner.adjust_fire_stacks(5)
+ owner.IgniteMob()
+ to_chat(owner, "Your components can't handle the heat and combust!")
+ qdel(src)
+ stacks += 1
+ if(stacks == danger_stack_amount)
+ to_chat(owner, "Your components are being dangerously overworked!")
+
+/atom/movable/screen/alert/status_effect/overclock
+ name = "Overclocked"
+ desc = "You feel energized, and hot."
+ icon_state = "high"
+
+#undef COMBUSTION_TEMPERATURE
diff --git a/code/datums/status_effects/magic_disguise.dm b/code/datums/status_effects/magic_disguise.dm
index 31f2b2ef0b28..6e32d3d90c4a 100644
--- a/code/datums/status_effects/magic_disguise.dm
+++ b/code/datums/status_effects/magic_disguise.dm
@@ -14,8 +14,8 @@
icon_state = "chameleon_outfit"
/datum/status_effect/magic_disguise/on_creation(mob/living/new_owner, mob/living/_disguise_mob)
- . = ..()
disguise_mob = _disguise_mob
+ . = ..()
/datum/status_effect/magic_disguise/on_apply()
. = ..()
diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm
index 32fb5edf89af..3e4235a448b2 100644
--- a/code/datums/status_effects/neutral.dm
+++ b/code/datums/status_effects/neutral.dm
@@ -343,9 +343,13 @@
var/datum/callback/expire_proc = null
/datum/status_effect/delayed/on_creation(mob/living/new_owner, new_duration, datum/callback/new_expire_proc, new_prevent_signal = null)
- if(!new_duration || !istype(new_expire_proc))
+ if(isnull(new_duration) || !istype(new_expire_proc))
qdel(src)
return
+ if(new_duration == 0)
+ new_expire_proc.Invoke()
+ return
+
duration = new_duration
expire_proc = new_expire_proc
. = ..()
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 90527c55547d..c0c5584a6bc7 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -340,7 +340,7 @@
* Proc which will make the atom act accordingly to an EMP.
* This proc can sleep depending on the implementation. So assume it sleeps!
*
- * severity - The severity of the EMP. Either EMP_HEAVY or EMP_LIGHT
+ * severity - The severity of the EMP. Either EMP_HEAVY, EMP_LIGHT, or EMP_WEAKENED
*/
/atom/proc/emp_act(severity)
SEND_SIGNAL(src, COMSIG_ATOM_EMP_ACT, severity)
diff --git a/code/game/gamemodes/changeling/changeling.dm b/code/game/gamemodes/changeling/changeling.dm
index c399912d0d0e..bad65ed348e5 100644
--- a/code/game/gamemodes/changeling/changeling.dm
+++ b/code/game/gamemodes/changeling/changeling.dm
@@ -7,17 +7,16 @@ GLOBAL_LIST_INIT(possible_changeling_IDs, list("Alpha","Beta","Gamma","Delta","E
config_tag = "changeling"
restricted_jobs = list("AI", "Cyborg")
protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Blueshield", "Nanotrasen Representative", "Magistrate", "Internal Affairs Agent", "Nanotrasen Navy Officer", "Special Operations Officer", "Syndicate Officer", "Trans-Solar Federation General")
- protected_species = list("Machine")
+ species_to_mindflayer = list("Machine")
required_players = 15
required_enemies = 1
recommended_enemies = 4
/// The total number of changelings allowed to be picked.
var/changeling_amount = 4
- /// A list containing references to the minds of soon-to-be changelings. This is seperate to avoid duplicate entries in the `changelings` list.
- var/list/datum/mind/pre_changelings = list()
/datum/game_mode/changeling/Destroy(force, ...)
pre_changelings.Cut()
+ pre_mindflayers.Cut()
return ..()
/datum/game_mode/changeling/announce()
@@ -36,11 +35,15 @@ GLOBAL_LIST_INIT(possible_changeling_IDs, list("Alpha","Beta","Gamma","Delta","E
if(!length(possible_changelings))
break
var/datum/mind/changeling = pick_n_take(possible_changelings)
- pre_changelings += changeling
changeling.restricted_roles = restricted_jobs
+ if(changeling.current?.client?.prefs.active_character.species in species_to_mindflayer)
+ pre_mindflayers += changeling
+ changeling.special_role = SPECIAL_ROLE_MIND_FLAYER
+ continue
+ pre_changelings += changeling
changeling.special_role = SPECIAL_ROLE_CHANGELING
- if(!length(pre_changelings))
+ if(!(length(pre_changelings) + length(pre_mindflayers)))
return FALSE
return TRUE
diff --git a/code/game/gamemodes/changeling/traitor_chan.dm b/code/game/gamemodes/changeling/traitor_chan.dm
index e56dc1b451cf..c4831ac28b4b 100644
--- a/code/game/gamemodes/changeling/traitor_chan.dm
+++ b/code/game/gamemodes/changeling/traitor_chan.dm
@@ -8,9 +8,7 @@
required_enemies = 1 // how many of each type are required
recommended_enemies = 3
secondary_enemies_scaling = 0.025
- secondary_protected_species = list("Machine")
- /// A list containing references to the minds of soon-to-be changelings. This is seperate to avoid duplicate entries in the `changelings` list.
- var/list/datum/mind/pre_changelings = list()
+ species_to_mindflayer = list("Machine")
/datum/game_mode/traitor/changeling/announce()
to_chat(world, "The current game mode is - Traitor+Changeling!")
@@ -24,19 +22,19 @@
var/list/datum/mind/possible_changelings = get_players_for_role(ROLE_CHANGELING)
secondary_enemies = CEILING((secondary_enemies_scaling * num_players()), 1)
- for(var/mob/new_player/player in GLOB.player_list)
- if((player.mind in possible_changelings) && (player.client.prefs.active_character.species in secondary_protected_species))
- possible_changelings -= player.mind
-
if(!length(possible_changelings))
return ..()
for(var/I in possible_changelings)
- if(length(pre_changelings) >= secondary_enemies)
+ if((length(pre_changelings) + length(pre_mindflayers)) >= secondary_enemies)
break
var/datum/mind/changeling = pick_n_take(possible_changelings)
- pre_changelings += changeling
changeling.restricted_roles = (restricted_jobs + secondary_restricted_jobs)
+ if(changeling.current?.client?.prefs.active_character.species in species_to_mindflayer)
+ pre_mindflayers += changeling
+ changeling.special_role = SPECIAL_ROLE_MIND_FLAYER
+ continue
+ pre_changelings += changeling
changeling.special_role = SPECIAL_ROLE_CHANGELING
return ..()
diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm
index 1a5086ebbd17..540dd69358bc 100644
--- a/code/game/gamemodes/game_mode.dm
+++ b/code/game/gamemodes/game_mode.dm
@@ -23,8 +23,8 @@
var/list/restricted_jobs = list() // Jobs it doesn't make sense to be. I.E chaplain or AI cultist
var/list/secondary_restricted_jobs = list() // Same as above, but for secondary antagonists
var/list/protected_jobs = list() // Jobs that can't be traitors
- var/list/protected_species = list() // Species that can't be traitors
- var/list/secondary_protected_species = list() // Same as above, but for secondary antagonists
+ /// Species that will become mindflayers if they're picked, instead of the regular antagonist
+ var/list/species_to_mindflayer = list()
var/required_players = 0
var/required_enemies = 0
var/recommended_enemies = 0
@@ -56,6 +56,17 @@
var/list/datum/mind/vampires = list()
/// A list of all minds which are thralled by a vampire
var/list/datum/mind/vampire_enthralled = list()
+ /// A list of all minds which have the mindflayer antag datum
+ var/list/datum/mind/mindflayers = list()
+
+ /// A list containing references to the minds of soon-to-be traitors. This is seperate to avoid duplicate entries in the `traitors` list.
+ var/list/datum/mind/pre_traitors = list()
+ /// A list containing references to the minds of soon-to-be changelings. This is seperate to avoid duplicate entries in the `changelings` list.
+ var/list/datum/mind/pre_changelings = list()
+ ///list of minds of soon to be vampires
+ var/list/datum/mind/pre_vampires = list()
+ /// A list containing references to the minds of soon-to-be mindflayers.
+ var/list/datum/mind/pre_mindflayers = list()
/// A list of all minds which have the wizard special role
var/list/datum/mind/wizards = list()
/// A list of all minds that are wizard apprentices
@@ -132,6 +143,10 @@
GLOB.start_state = new /datum/station_state()
GLOB.start_state.count()
+
+ for(var/datum/mind/flayer as anything in pre_mindflayers) //Mindflayers need to be all the way out here since they could come from most gamemodes
+ flayer.make_mind_flayer()
+
return TRUE
///process()
@@ -259,7 +274,7 @@
if(rev_team)
rev_team.check_all_victory()
-/datum/game_mode/proc/get_players_for_role(role, override_jobbans = FALSE)
+/datum/game_mode/proc/get_players_for_role(role, override_jobbans = FALSE, species_exclusive = null)
var/list/players = list()
var/list/candidates = list()
@@ -272,22 +287,29 @@
if(player_old_enough_antag(player.client,role))
players += player
+ for(var/mob/living/carbon/human/player in GLOB.player_list)
+ if(jobban_isbanned(player, ROLE_SYNDICATE) || jobban_isbanned(player, roletext))
+ continue
+ if(player_old_enough_antag(player.client, role))
+ players += player
+
// Shuffle the players list so that it becomes ping-independent.
players = shuffle(players)
-
- // Get a list of all the people who want to be the antagonist for this round, except those with incompatible species
- for(var/mob/new_player/player in players)
- if(!player.client.skip_antag)
- if((role in player.client.prefs.be_special) && !(player.client.prefs.active_character.species in protected_species))
- player_draft_log += "[player.key] had [roletext] enabled, so we are drafting them."
- candidates += player.mind
- players -= player
+ // Get a list of all the people who want to be the antagonist for this round
+ for(var/mob/eligible_player in players)
+ if(!eligible_player.client.skip_antag)
+ if(species_exclusive && (eligible_player.client.prefs.active_character.species != species_exclusive))
+ continue
+ if(role in eligible_player.client.prefs.be_special)
+ player_draft_log += "[eligible_player.key] had [roletext] enabled, so we are drafting them."
+ candidates += eligible_player.mind
+ players -= eligible_player
// Remove candidates who want to be antagonist but have a job (or other antag datum) that precludes it
if(restricted_jobs)
- for(var/datum/mind/player in candidates)
- if((player.assigned_role in restricted_jobs) || player.special_role)
- candidates -= player
+ for(var/datum/mind/player_mind in candidates)
+ if((player_mind.assigned_role in restricted_jobs) || player_mind.special_role)
+ candidates -= player_mind
return candidates // Returns: The number of people who had the antagonist role set to yes, regardless of recomended_enemies, if that number is greater than recommended_enemies
@@ -317,7 +339,7 @@
if(player.client.skip_antag || !(allow_offstation_roles || !player.mind?.offstation_role) || player.mind?.special_role)
continue
- if(!(role in player.client.prefs.be_special) || (player.client.prefs.active_character.species in protected_species))
+ if(!(role in player.client.prefs.be_special) || (player.client.prefs.active_character.species in species_to_mindflayer))
continue
player_draft_log += "[player.key] had [roletext] enabled, so we are drafting them."
@@ -620,6 +642,7 @@
. += auto_declare_completion_traitor()
. += auto_declare_completion_vampire()
. += auto_declare_completion_enthralled()
+ . += auto_declare_completion_mindflayer()
. += auto_declare_completion_changeling()
. += auto_declare_completion_nuclear()
. += auto_declare_completion_wizard()
diff --git a/code/game/gamemodes/intercept_report.dm b/code/game/gamemodes/intercept_report.dm
index 15aa4bd2ffd8..9f7d0fa04029 100644
--- a/code/game/gamemodes/intercept_report.dm
+++ b/code/game/gamemodes/intercept_report.dm
@@ -116,7 +116,7 @@
if((man.mind.assigned_role in SSticker.mode.protected_jobs) || (man.mind.assigned_role in SSticker.mode.restricted_jobs))
return
//don't include suspects who can't possibly be the antag based on their species (no suspecting the machines of being sneaky changelings)
- if(man.dna.species.name in SSticker.mode.protected_species)
+ if(man.dna.species.name in SSticker.mode.species_to_mindflayer)
return
dudes += man
for(var/i = 0, i < max(length(GLOB.player_list)/10,2), i++)
diff --git a/code/game/gamemodes/miniantags/guardian/guardian.dm b/code/game/gamemodes/miniantags/guardian/guardian.dm
index d0a50fc3ee14..77eb510f82d2 100644
--- a/code/game/gamemodes/miniantags/guardian/guardian.dm
+++ b/code/game/gamemodes/miniantags/guardian/guardian.dm
@@ -283,7 +283,7 @@
if(has_guardian(user))
to_chat(user, "You already have a [mob_name]!")
return
- if(user.mind && (IS_CHANGELING(user) || user.mind.has_antag_datum(/datum/antagonist/vampire)))
+ if(user.mind && (IS_CHANGELING(user) || user.mind.has_antag_datum(/datum/antagonist/vampire) || IS_MINDFLAYER(user)))
to_chat(user, "[ling_failure]")
return
if(used)
diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm
index b1397465f3ac..18bced35c23a 100644
--- a/code/game/gamemodes/objective.dm
+++ b/code/game/gamemodes/objective.dm
@@ -857,6 +857,32 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective)
else
return FALSE
+#define SWARM_GOAL_LOWER_BOUND 130
+#define SWARM_GOAL_UPPER_BOUND 400
+
+/datum/objective/swarms
+ name = "Gain swarms"
+ needs_target = FALSE
+
+/datum/objective/swarms/New()
+ gen_amount_goal()
+ return ..()
+
+/datum/objective/swarms/proc/gen_amount_goal(low = SWARM_GOAL_LOWER_BOUND, high = SWARM_GOAL_UPPER_BOUND)
+ target_amount = round(rand(low, high), 5)
+ update_explanation_text()
+ return target_amount
+
+/datum/objective/swarms/update_explanation_text()
+ explanation_text = "Accumulate at least [target_amount] worth of swarms."
+
+/datum/objective/swarms/check_completion()
+ for(var/datum/mind/M in get_owners())
+ var/datum/antagonist/mindflayer/flayer = M.has_antag_datum(/datum/antagonist/mindflayer)
+ return flayer?.total_swarms_gathered >= target_amount
+
+#undef SWARM_GOAL_LOWER_BOUND
+#undef SWARM_GOAL_UPPER_BOUND
// Traders
// These objectives have no check_completion, they exist only to tell Sol Traders what to aim for.
diff --git a/code/game/gamemodes/traitor/traitor.dm b/code/game/gamemodes/traitor/traitor.dm
index 56c2b44d7472..a4f1f3904517 100644
--- a/code/game/gamemodes/traitor/traitor.dm
+++ b/code/game/gamemodes/traitor/traitor.dm
@@ -6,8 +6,6 @@
required_players = 0
required_enemies = 1
recommended_enemies = 4
- /// A list containing references to the minds of soon-to-be traitors. This is seperate to avoid duplicate entries in the `traitors` list.
- var/list/datum/mind/pre_traitors = list()
/// Hard limit on traitors if scaling is turned off.
var/traitors_possible = 4
/// How much the amount of players get divided by to determine the number of traitors.
@@ -25,7 +23,7 @@
var/list/possible_traitors = get_players_for_role(ROLE_TRAITOR)
for(var/datum/mind/candidate in possible_traitors)
- if(candidate.special_role == SPECIAL_ROLE_VAMPIRE || candidate.special_role == SPECIAL_ROLE_CHANGELING) // no traitor vampires or changelings
+ if(candidate.special_role == SPECIAL_ROLE_VAMPIRE || candidate.special_role == SPECIAL_ROLE_CHANGELING || candidate.special_role == SPECIAL_ROLE_MIND_FLAYER) // no traitor vampires, changelings, or mindflayers
possible_traitors.Remove(candidate)
// stop setup if no possible traitors
diff --git a/code/game/gamemodes/trifecta/trifecta.dm b/code/game/gamemodes/trifecta/trifecta.dm
index ab711b728cf9..2e1798c8a8c9 100644
--- a/code/game/gamemodes/trifecta/trifecta.dm
+++ b/code/game/gamemodes/trifecta/trifecta.dm
@@ -12,11 +12,8 @@
required_players = 25
required_enemies = 1 // how many of each type are required
recommended_enemies = 3
- secondary_protected_species = list("Machine")
+ species_to_mindflayer = list("Machine")
var/vampire_restricted_jobs = list("Chaplain")
- var/list/datum/mind/pre_traitors = list()
- var/list/datum/mind/pre_changelings = list()
- var/list/datum/mind/pre_vampires = list()
var/amount_vamp = 1
var/amount_cling = 1
var/amount_tot = 1
@@ -40,11 +37,14 @@
for(var/datum/mind/vampire as anything in shuffle(possible_vampires))
if(length(pre_vampires) >= amount_vamp)
break
- if(vampire.current.client.prefs.active_character.species in secondary_protected_species)
+ vampire.restricted_roles = restricted_jobs + secondary_restricted_jobs + vampire_restricted_jobs
+ if(vampire.current.client.prefs.active_character.species in species_to_mindflayer)
+ pre_mindflayers += vampire
+ amount_vamp -= 1 //It's basically the same thing as incrementing pre_vampires
+ vampire.special_role = SPECIAL_ROLE_MIND_FLAYER
continue
pre_vampires += vampire
vampire.special_role = SPECIAL_ROLE_VAMPIRE
- vampire.restricted_roles = (restricted_jobs + secondary_restricted_jobs + vampire_restricted_jobs)
//Vampires made, off to changelings
var/list/datum/mind/possible_changelings = get_players_for_role(ROLE_CHANGELING)
@@ -55,10 +55,15 @@
for(var/datum/mind/changeling as anything in shuffle(possible_changelings))
if(length(pre_changelings) >= amount_cling)
break
- if((changeling.current.client.prefs.active_character.species in secondary_protected_species) || changeling.special_role == SPECIAL_ROLE_VAMPIRE)
+ if(changeling.special_role == SPECIAL_ROLE_VAMPIRE || changeling.special_role == SPECIAL_ROLE_MIND_FLAYER)
continue
- pre_changelings += changeling
changeling.restricted_roles = (restricted_jobs + secondary_restricted_jobs)
+ if(changeling.current?.client?.prefs.active_character.species in species_to_mindflayer)
+ pre_mindflayers += changeling
+ amount_cling -= 1
+ changeling.special_role = SPECIAL_ROLE_MIND_FLAYER
+ continue
+ pre_changelings += changeling
changeling.special_role = SPECIAL_ROLE_CHANGELING
//And now traitors
@@ -71,7 +76,7 @@
for(var/datum/mind/traitor as anything in shuffle(possible_traitors))
if(length(pre_traitors) >= amount_tot)
break
- if(traitor.special_role == SPECIAL_ROLE_VAMPIRE || traitor.special_role == SPECIAL_ROLE_CHANGELING) // no traitor vampires or changelings
+ if(traitor.special_role == SPECIAL_ROLE_VAMPIRE || traitor.special_role == SPECIAL_ROLE_CHANGELING || traitor.special_role == SPECIAL_ROLE_MIND_FLAYER) // no traitor vampires or changelings
continue
pre_traitors += traitor
traitor.special_role = SPECIAL_ROLE_TRAITOR
diff --git a/code/game/gamemodes/vampire/traitor_vamp.dm b/code/game/gamemodes/vampire/traitor_vamp.dm
index 1348b2e15936..0e6576d4d603 100644
--- a/code/game/gamemodes/vampire/traitor_vamp.dm
+++ b/code/game/gamemodes/vampire/traitor_vamp.dm
@@ -9,8 +9,7 @@
required_enemies = 1 // how many of each type are required
recommended_enemies = 3
secondary_enemies_scaling = 0.025
- secondary_protected_species = list("Machine")
- var/list/datum/mind/pre_vampires = list()
+ species_to_mindflayer = list("Machine")
/datum/game_mode/traitor/vampire/announce()
to_chat(world, "The current game mode is - Traitor+Vampire!")
@@ -24,22 +23,22 @@
var/list/datum/mind/possible_vampires = get_players_for_role(ROLE_VAMPIRE)
secondary_enemies = CEILING((secondary_enemies_scaling * num_players()), 1)
- for(var/mob/new_player/player in GLOB.player_list)
- if((player.mind in possible_vampires) && (player.client.prefs.active_character.species in secondary_protected_species))
- possible_vampires -= player.mind
+ if(length(possible_vampires) <= 0)
+ return FALSE
- if(length(possible_vampires) > 0)
- for(var/I in possible_vampires)
- if(length(pre_vampires) >= secondary_enemies)
- break
- var/datum/mind/vampire = pick_n_take(possible_vampires)
- pre_vampires += vampire
- vampire.special_role = SPECIAL_ROLE_VAMPIRE
- vampire.restricted_roles = (restricted_jobs + secondary_restricted_jobs)
- ..()
- return 1
- else
- return 0
+ for(var/I in possible_vampires)
+ if((length(pre_vampires) + length(pre_mindflayers)) >= secondary_enemies)
+ break
+ var/datum/mind/vampire = pick_n_take(possible_vampires)
+ vampire.restricted_roles = (restricted_jobs + secondary_restricted_jobs)
+ if(vampire.current?.client?.prefs.active_character.species in species_to_mindflayer)
+ pre_mindflayers += vampire
+ vampire.special_role = SPECIAL_ROLE_MIND_FLAYER
+ continue
+ pre_vampires += vampire
+ vampire.special_role = SPECIAL_ROLE_VAMPIRE
+ ..()
+ return TRUE
/datum/game_mode/traitor/vampire/post_setup()
for(var/datum/mind/vampire in pre_vampires)
diff --git a/code/game/gamemodes/vampire/vampire_chan.dm b/code/game/gamemodes/vampire/vampire_chan.dm
index adc3b78f762b..29ef10b5c104 100644
--- a/code/game/gamemodes/vampire/vampire_chan.dm
+++ b/code/game/gamemodes/vampire/vampire_chan.dm
@@ -3,14 +3,12 @@
config_tag = "vampchan"
restricted_jobs = list("AI", "Cyborg")
protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Blueshield", "Nanotrasen Representative", "Magistrate", "Chaplain", "Internal Affairs Agent", "Nanotrasen Navy Officer", "Special Operations Officer", "Syndicate Officer", "Solar Federation General")
- protected_species = list("Machine")
+ species_to_mindflayer = list("Machine")
required_players = 15
required_enemies = 1
recommended_enemies = 3
secondary_enemies_scaling = 0.025
vampire_penalty = 0.4 // Cut out 40% of the vampires since we'll replace some with changelings
- /// A list of all soon-to-be changelings
- var/list/datum/mind/pre_changelings = list()
/datum/game_mode/vampire/changeling/pre_setup()
if(GLOB.configuration.gamemode.prevent_mindshield_antags)
@@ -19,10 +17,6 @@
var/list/datum/mind/possible_changelings = get_players_for_role(ROLE_CHANGELING)
secondary_enemies = CEILING((secondary_enemies_scaling * num_players()), 1)
- for(var/mob/new_player/player in GLOB.player_list)
- if((player.mind in possible_changelings) && (player.client.prefs.active_character.species in secondary_protected_species))
- possible_changelings -= player.mind
-
if(!length(possible_changelings))
return ..()
@@ -30,8 +24,13 @@
if(length(pre_changelings) >= secondary_enemies)
break
var/datum/mind/changeling = pick_n_take(possible_changelings)
- pre_changelings += changeling
changeling.restricted_roles = (restricted_jobs + secondary_restricted_jobs)
+ if(changeling.current?.client?.prefs.active_character.species in species_to_mindflayer)
+ pre_mindflayers += changeling
+ secondary_enemies -= 1 // Again, since we aren't increasing pre_changeling we'll just decrement what it's compared to.
+ changeling.special_role = SPECIAL_ROLE_MIND_FLAYER
+ continue
+ pre_changelings += changeling
changeling.special_role = SPECIAL_ROLE_CHANGELING
return ..()
diff --git a/code/game/gamemodes/vampire/vampire_gamemode.dm b/code/game/gamemodes/vampire/vampire_gamemode.dm
index 7d0ef233fce3..837577382339 100644
--- a/code/game/gamemodes/vampire/vampire_gamemode.dm
+++ b/code/game/gamemodes/vampire/vampire_gamemode.dm
@@ -3,16 +3,13 @@
config_tag = "vampire"
restricted_jobs = list("AI", "Cyborg")
protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Blueshield", "Nanotrasen Representative", "Magistrate", "Chaplain", "Internal Affairs Agent", "Nanotrasen Navy Officer", "Special Operations Officer", "Syndicate Officer", "Trans-Solar Federation General")
- protected_species = list("Machine")
+ species_to_mindflayer = list("Machine")
required_players = 15
required_enemies = 1
recommended_enemies = 4
/// If this gamemode should spawn less vampires than a usual vampire round, as a percentage of how many you want relative to the regular amount
var/vampire_penalty = 0
- ///list of minds of soon to be vampires
- var/list/datum/mind/pre_vampires = list()
-
/datum/game_mode/vampire/announce()
to_chat(world, "The current game mode is - Vampires!")
to_chat(world, "There are Bluespace Vampires infesting your fellow crewmates, keep your blood close and neck safe!")
@@ -31,9 +28,13 @@
if(!length(possible_vampires))
break
var/datum/mind/vampire = pick_n_take(possible_vampires)
+ vampire.restricted_roles = (restricted_jobs + secondary_restricted_jobs)
+ if(vampire.current?.client?.prefs.active_character.species in species_to_mindflayer)
+ pre_mindflayers += vampire
+ vampire.special_role = SPECIAL_ROLE_MIND_FLAYER
+ continue
pre_vampires += vampire
vampire.special_role = SPECIAL_ROLE_VAMPIRE
- vampire.restricted_roles = restricted_jobs
..()
return TRUE
diff --git a/code/game/machinery/camera/camera.dm b/code/game/machinery/camera/camera.dm
index 131bc83a1a38..ccfe7b5ee88f 100644
--- a/code/game/machinery/camera/camera.dm
+++ b/code/game/machinery/camera/camera.dm
@@ -40,6 +40,8 @@
var/detectTime = 0
var/area/station/ai_monitored/area_motion = null
var/alarm_delay = 30 // Don't forget, there's another 3 seconds in queueAlarm()
+ /// If this camera doesnt add to camera chunks. Used by camera bugs.
+ var/non_chunking_camera = FALSE
/obj/machinery/camera/Initialize(mapload, should_add_to_cameranet = TRUE)
. = ..()
diff --git a/code/game/machinery/computer/arcade_games/recruiter.dm b/code/game/machinery/computer/arcade_games/recruiter.dm
index 717d798d02fc..585a6bcbbae8 100644
--- a/code/game/machinery/computer/arcade_games/recruiter.dm
+++ b/code/game/machinery/computer/arcade_games/recruiter.dm
@@ -13,7 +13,7 @@
#define RECRUITER_STATUS_GAMEOVER 3
/obj/machinery/computer/arcade/recruiter
- name = "NT Recruiter Simulator"
+ name = "\improper NT Recruiter Simulator"
desc = "Weed out the good from bad employees and build the perfect manifest to work aboard the station."
icon_state = "arcade_recruiter"
icon_screen = "nanotrasen"
@@ -311,7 +311,7 @@
to_chat(user, "You override the menu and revert the game to its previous version.")
add_hiddenprint(user)
game_status = RECRUITER_STATUS_START
- name = "NT Recruiter Simulator HARDCORE EDITION"
+ name = "\improper NT Recruiter Simulator HARDCORE EDITION"
desc = "The advanced version of Nanotrasen's recruiting simulator, used to train the highest echelon of Nanotrasen recruiters. Has double the application count, and supposedly includes some routines to weed out the less skilled."
total_curriculums = 14
emagged = TRUE
diff --git a/code/game/machinery/deployable.dm b/code/game/machinery/deployable.dm
index a620857c3683..ad88ec4a177c 100644
--- a/code/game/machinery/deployable.dm
+++ b/code/game/machinery/deployable.dm
@@ -60,6 +60,8 @@
/obj/structure/barricade/CanPass(atom/movable/mover, turf/target)//So bullets will fly over and stuff.
if(locate(/obj/structure/barricade) in get_turf(mover))
return TRUE
+ else if(istype(mover) && mover.checkpass(PASSBARRICADE))
+ return TRUE
else if(isprojectile(mover))
if(!anchored)
return TRUE
diff --git a/code/game/machinery/requests_console.dm b/code/game/machinery/requests_console.dm
index f31a573e270e..40504ef71135 100644
--- a/code/game/machinery/requests_console.dm
+++ b/code/game/machinery/requests_console.dm
@@ -225,6 +225,8 @@ GLOBAL_LIST_EMPTY(allRequestConsoles)
if("sendAnnouncement")
if(!announcementConsole)
return
+ if(!announceAuth) // No you don't
+ return
announcer.Announce(message)
reset_message(TRUE)
diff --git a/code/game/mecha/combat/combat.dm b/code/game/mecha/combat/combat.dm
index 17ea778639f4..8f94b0d1ccec 100644
--- a/code/game/mecha/combat/combat.dm
+++ b/code/game/mecha/combat/combat.dm
@@ -5,7 +5,6 @@
maint_access = 0
armor = list(melee = 30, bullet = 30, laser = 15, energy = 20, bomb = 20, rad = 0, fire = 100)
destruction_sleep_duration = 4 SECONDS
- var/am = "d3c2fbcadca903a41161ccc9df9cf948"
/obj/mecha/combat/moved_inside(mob/living/carbon/human/H as mob)
if(..())
@@ -28,10 +27,3 @@
if(occupant && occupant.client)
occupant.client.mouse_pointer_icon = initial(occupant.client.mouse_pointer_icon)
..()
-
-/obj/mecha/combat/Topic(href,href_list)
- ..()
- var/datum/topic_input/afilter = new(href, href_list)
- if(afilter.get("close"))
- am = null
- return
diff --git a/code/game/mecha/combat/honker.dm b/code/game/mecha/combat/honker.dm
index c6f7da80db9d..0478f0f904bf 100644
--- a/code/game/mecha/combat/honker.dm
+++ b/code/game/mecha/combat/honker.dm
@@ -131,6 +131,8 @@
/obj/mecha/combat/honker/Topic(href, href_list)
..()
if(href_list["play_sound"])
+ if(usr != occupant)
+ return
switch(href_list["play_sound"])
if("sadtrombone")
playsound(src, 'sound/misc/sadtrombone.ogg', 50)
diff --git a/code/game/mecha/equipment/mecha_equipment.dm b/code/game/mecha/equipment/mecha_equipment.dm
index a152e07e3958..89dbec604587 100644
--- a/code/game/mecha/equipment/mecha_equipment.dm
+++ b/code/game/mecha/equipment/mecha_equipment.dm
@@ -141,6 +141,10 @@
select_action.Remove(chassis.occupant)
/obj/item/mecha_parts/mecha_equipment/Topic(href,href_list)
+ if(!chassis)
+ return TRUE
+ if(usr != chassis.occupant)
+ return TRUE
if(href_list["detach"])
detach()
diff --git a/code/game/mecha/equipment/tools/janitor_tools.dm b/code/game/mecha/equipment/tools/janitor_tools.dm
index fa045b8fc0cf..866fc0c13132 100644
--- a/code/game/mecha/equipment/tools/janitor_tools.dm
+++ b/code/game/mecha/equipment/tools/janitor_tools.dm
@@ -89,7 +89,8 @@
return "[output] \[Refill [refill_enabled? "Enabled" : "Disabled"]\] \[[reagents.total_volume]\]"
/obj/item/mecha_parts/mecha_equipment/janitor/mega_mop/Topic(href, href_list)
- ..()
+ if(..())
+ return
var/datum/topic_input/afilter = new (href, href_list)
if(afilter.get("toggle_mode"))
refill_enabled = !refill_enabled
@@ -192,7 +193,8 @@
return "[output] \[Refill [refill_enabled? "Enabled" : "Disabled"]\] \[[spray_controller.reagents.total_volume]\]"
/obj/item/mecha_parts/mecha_equipment/janitor/mega_spray/Topic(href,href_list)
- ..()
+ if(..())
+ return
var/datum/topic_input/afilter = new (href,href_list)
if(afilter.get("toggle_mode"))
refill_enabled = !refill_enabled
@@ -244,7 +246,8 @@
return "[output] \[[bagging? "Filling" : "Dumping"]\] \[Area [extended? "Extended" : "Focused"]\] \[Cargo: [length(storage_controller.contents)]/[storage_controller.max_combined_w_class]\]\]"
/obj/item/mecha_parts/mecha_equipment/janitor/garbage_magnet/Topic(href,href_list)
- ..()
+ if(..())
+ return
var/datum/topic_input/afilter = new (href,href_list)
if(afilter.get("toggle_bagging"))
bagging = !bagging
diff --git a/code/game/mecha/equipment/tools/medical_tools.dm b/code/game/mecha/equipment/tools/medical_tools.dm
index 6552d9b32067..87cbbae198f8 100644
--- a/code/game/mecha/equipment/tools/medical_tools.dm
+++ b/code/game/mecha/equipment/tools/medical_tools.dm
@@ -109,7 +109,8 @@
return "[output] [temp]"
/obj/item/mecha_parts/mecha_equipment/medical/sleeper/Topic(href,href_list)
- ..()
+ if(..())
+ return
var/datum/topic_input/afilter = new /datum/topic_input(href,href_list)
if(afilter.get("eject"))
go_out()
@@ -348,7 +349,8 @@
/obj/item/mecha_parts/mecha_equipment/medical/syringe_gun/Topic(href,href_list)
- ..()
+ if(..())
+ return
var/datum/topic_input/afilter = new (href,href_list)
if(afilter.get("toggle_mode"))
mode = !mode
diff --git a/code/game/mecha/equipment/tools/other_tools.dm b/code/game/mecha/equipment/tools/other_tools.dm
index 5adbd7eea2a8..d03fb828be0d 100644
--- a/code/game/mecha/equipment/tools/other_tools.dm
+++ b/code/game/mecha/equipment/tools/other_tools.dm
@@ -95,7 +95,8 @@
return "[..()] [mode==1?"([locked||"Nothing"])":null] \[S|P\]"
/obj/item/mecha_parts/mecha_equipment/gravcatapult/Topic(href, href_list)
- ..()
+ if(..())
+ return
if(href_list["mode"])
mode = text2num(href_list["mode"])
send_byjax(chassis.occupant,"exosuit.browser","\ref[src]",get_equip_info())
@@ -176,7 +177,8 @@
/obj/item/mecha_parts/mecha_equipment/repair_droid/Topic(href, href_list)
- ..()
+ if(..())
+ return
if(href_list["toggle_repairs"])
chassis.overlays -= droid_overlay
if(equip_ready)
@@ -264,7 +266,8 @@
return pow_chan
/obj/item/mecha_parts/mecha_equipment/tesla_energy_relay/Topic(href, href_list)
- ..()
+ if(..())
+ return
if(href_list["toggle_relay"])
if(equip_ready) //inactive
START_PROCESSING(SSobj, src)
@@ -332,7 +335,8 @@
..()
/obj/item/mecha_parts/mecha_equipment/generator/Topic(href, href_list)
- ..()
+ if(..())
+ return
if(href_list["toggle"])
if(equip_ready) //inactive
set_ready_state(0)
diff --git a/code/game/mecha/equipment/tools/work_tools.dm b/code/game/mecha/equipment/tools/work_tools.dm
index 934c7c364bfc..2a438536c195 100644
--- a/code/game/mecha/equipment/tools/work_tools.dm
+++ b/code/game/mecha/equipment/tools/work_tools.dm
@@ -280,7 +280,8 @@
/obj/item/mecha_parts/mecha_equipment/rcd/Topic(href,href_list)
- ..()
+ if(..())
+ return
if(href_list["mode"])
mode = text2num(href_list["mode"])
switch(mode)
@@ -375,7 +376,8 @@
/obj/item/mecha_parts/mecha_equipment/cable_layer/Topic(href,href_list)
- ..()
+ if(..())
+ return
if(href_list["toggle"])
set_ready_state(!equip_ready)
occupant_message("[src] [equip_ready?"dea":"a"]ctivated.")
diff --git a/code/game/mecha/equipment/weapons/weapons.dm b/code/game/mecha/equipment/weapons/weapons.dm
index 13a6887278ba..bcd474cd407a 100644
--- a/code/game/mecha/equipment/weapons/weapons.dm
+++ b/code/game/mecha/equipment/weapons/weapons.dm
@@ -270,7 +270,8 @@
playsound(src, 'sound/weapons/gun_interactions/rearm.ogg', 50, 1)
/obj/item/mecha_parts/mecha_equipment/weapon/ballistic/Topic(href, href_list)
- ..()
+ if(..())
+ return
if(href_list["rearm"])
rearm()
diff --git a/code/game/mecha/mecha_topic.dm b/code/game/mecha/mecha_topic.dm
index 188a9a8c399d..d489e84431e4 100644
--- a/code/game/mecha/mecha_topic.dm
+++ b/code/game/mecha/mecha_topic.dm
@@ -256,7 +256,7 @@
if(href_list["select_equip"])
if(usr != occupant) return
var/obj/item/mecha_parts/mecha_equipment/equip = afilter.getObj("select_equip")
- if(equip)
+ if(equip && (equip in equipment))
selected = equip
occupant_message("You switch to [equip]")
visible_message("[src] raises [equip]")
diff --git a/code/game/mecha/working/ripley.dm b/code/game/mecha/working/ripley.dm
index 0ef350472a11..988b0b95083d 100644
--- a/code/game/mecha/working/ripley.dm
+++ b/code/game/mecha/working/ripley.dm
@@ -234,7 +234,8 @@
return ..()
/obj/mecha/working/ripley/Topic(href, href_list)
- ..()
+ if(..())
+ return
if(href_list["drop_from_cargo"])
var/obj/O = locate(href_list["drop_from_cargo"])
if(O && (O in cargo))
@@ -245,7 +246,6 @@
if(T)
T.Entered(O)
log_message("Unloaded [O]. Cargo compartment capacity: [cargo_capacity - length(cargo)]")
- return
/obj/mecha/working/ripley/get_stats_part()
var/output = ..()
diff --git a/code/game/objects/effects/effect_system/effects_smoke.dm b/code/game/objects/effects/effect_system/effects_smoke.dm
index d46c09daa460..55aee922c096 100644
--- a/code/game/objects/effects/effect_system/effects_smoke.dm
+++ b/code/game/objects/effects/effect_system/effects_smoke.dm
@@ -134,6 +134,35 @@
/datum/effect_system/smoke_spread/bad
effect_type = /obj/effect/particle_effect/smoke/bad
+/// Steam smoke
+/datum/effect_system/smoke_spread/steam
+ effect_type = /obj/effect/particle_effect/smoke/steam
+
+/obj/effect/particle_effect/smoke/steam
+ color = COLOR_OFF_WHITE
+ lifetime = 10 SECONDS_TO_LIFE_CYCLES
+ causes_coughing = TRUE
+
+/obj/effect/particle_effect/smoke/steam/Crossed(atom/movable/AM, oldloc)
+ . = ..()
+ if(!isliving(AM))
+ return
+ var/mob/living/crosser = AM
+ if(IS_MINDFLAYER(crosser))
+ return // Mindflayers are fully immune to steam
+ if(!ishuman(crosser))
+ crosser.adjustFireLoss(8)
+ return
+
+ var/mob/living/carbon/human/human_crosser = AM
+ var/fire_armour = human_crosser.get_thermal_protection()
+ if(fire_armour >= FIRE_SUIT_MAX_TEMP_PROTECT || HAS_TRAIT(human_crosser, TRAIT_RESISTHEAT))
+ return
+
+ crosser.adjustFireLoss(5)
+ if(prob(20))
+ to_chat(crosser, "You are being scalded by the hot steam!")
+
/////////////////////////////////////////////
// Nanofrost smoke
/////////////////////////////////////////////
diff --git a/code/game/objects/effects/map_effects/mapmanip.dm b/code/game/objects/effects/map_effects/mapmanip.dm
index d0be4103dd90..4e73fa92f384 100644
--- a/code/game/objects/effects/map_effects/mapmanip.dm
+++ b/code/game/objects/effects/map_effects/mapmanip.dm
@@ -4,7 +4,7 @@
/obj/effect/map_effect/marker/mapmanip/Initialize(mapload)
. = ..()
- qdel(src)
+ return INITIALIZE_HINT_QDEL
/obj/effect/map_effect/marker/mapmanip/submap/extract
name = "mapmanip marker, extract submap"
@@ -20,7 +20,15 @@
pixel_x = -32
pixel_y = -32
+/obj/effect/map_effect/marker_helper
+ name = "marker helper"
+ layer = POINT_LAYER
+
+/obj/effect/map_effect/marker_helper/Initialize(mapload)
+ . = ..()
+ return INITIALIZE_HINT_QDEL
+
/obj/effect/map_effect/marker_helper/mapmanip/submap/edge
- name = "mapmanip helper marker, edge of submap"
+ name = "mapmanip marker helper, submap edge"
icon = 'icons/effects/mapping_helpers.dmi'
icon_state = "mapmanip_submap_edge"
diff --git a/code/game/objects/items/control_wand.dm b/code/game/objects/items/control_wand.dm
index 4cec53621291..126a38a0cbe8 100644
--- a/code/game/objects/items/control_wand.dm
+++ b/code/game/objects/items/control_wand.dm
@@ -181,10 +181,15 @@
item_state = "hacktool"
var/hack_speed = 1.5 SECONDS
var/busy = FALSE
+ /// How far can we use this. Leave `null` for infinite range
+ var/range
/obj/item/door_remote/omni/access_tuner/afterattack(obj/machinery/door/D, mob/user)
if(!istype(D, /obj/machinery/door/airlock) && !istype(D, /obj/machinery/door/window))
return
+ if(!isnull(range) && get_dist(src, D) > range)
+ return
+
if(busy)
to_chat(user, "[src] is alreading interfacing with a door!")
return
@@ -196,6 +201,11 @@
busy = FALSE
icon_state = "hacktool"
+/obj/item/door_remote/omni/access_tuner/flayer
+ name = "integrated access tuner"
+ hack_speed = 5 SECONDS
+ range = 10
+
/// How long before you can "jangle" your keyring again (to prevent spam)
#define JANGLE_COOLDOWN 10 SECONDS
diff --git a/code/game/objects/items/devices/camera_bug.dm b/code/game/objects/items/devices/camera_bug.dm
index 439830e01b37..6d11a2387407 100644
--- a/code/game/objects/items/devices/camera_bug.dm
+++ b/code/game/objects/items/devices/camera_bug.dm
@@ -42,7 +42,7 @@
/obj/item/camera_bug/ert
- name = "ERT Camera Monitor"
+ name = "\improper ERT Camera Monitor"
desc = "A small handheld device used by ERT commanders to view camera feeds remotely."
/obj/item/camera_bug/ert/Initialize(mapload)
@@ -55,13 +55,18 @@
icon = 'icons/obj/device.dmi'
icon_state = "wall_bug"
w_class = WEIGHT_CLASS_TINY
- var/obj/machinery/camera/portable/camera
+ var/obj/machinery/camera/portable/camera_bug/camera
var/index = "REPORT THIS TO CODERS"
+ /// What name shows up on the camera bug list
+ var/camera_tag = "Hidden Camera"
+ /// If it sticks to whatever you throw at it
+ var/is_sticky = TRUE
/obj/item/wall_bug/Initialize(mapload, obj/item/camera_bug/the_bug)
. = ..()
link_to_camera(the_bug)
- AddComponent(/datum/component/sticky)
+ if(is_sticky)
+ AddComponent(/datum/component/sticky)
ADD_TRAIT(src, TRAIT_NO_THROWN_MESSAGE, ROUNDSTART_TRAIT)
/obj/item/wall_bug/Destroy()
@@ -81,12 +86,34 @@
camera_bug.connections++
index = camera_bug.connections
- camera = new /obj/machinery/camera/portable(src)
+ camera = new /obj/machinery/camera/portable/camera_bug(src)
camera.network = list("camera_bug[camera_bug.UID()]")
- camera.c_tag = "Hidden Camera [index]"
+ camera.c_tag = "[camera_tag] [index]"
+
+/// Created by a mindflayer ability
+/obj/item/wall_bug/computer_bug
+ name = "nanobot"
+ desc = "A small droplet of a shimmering metallic slurry."
+ camera_tag = "Surveillance Unit"
+ is_sticky = FALSE
+ /// Reference to the creator's antag datum
+ var/datum/antagonist/mindflayer/flayer
+ COOLDOWN_DECLARE(alert_cooldown)
+
+/obj/item/wall_bug/computer_bug/Destroy()
+ flayer = null
+ return ..()
+
+/obj/item/wall_bug/computer_bug/link_to_camera(obj/item/camera_bug/camera_bug, datum/antagonist/mindflayer/flayer_datum)
+ ..()
+ if(flayer_datum)
+ flayer = flayer_datum
+
+/obj/machinery/camera/portable/camera_bug
+ non_chunking_camera = TRUE
/obj/item/paper/camera_bug
- name = "Camera Bug Guide"
+ name = "\improper Camera Bug Guide"
icon_state = "paper"
info = {"Instructions on your new invasive camera utility
diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm
index 2cc9d821d388..a778c418085c 100644
--- a/code/game/objects/items/devices/scanners.dm
+++ b/code/game/objects/items/devices/scanners.dm
@@ -241,12 +241,13 @@ SLIME SCANNER
msgs += "Subject appears to have [H.getCloneLoss() > 30 ? "severe" : "minor"] cellular damage."
// Brain.
- if(H.get_int_organ(/obj/item/organ/internal/brain))
- if(H.getBrainLoss() >= 100)
+ var/obj/item/organ/internal/brain = H.get_int_organ(/obj/item/organ/internal/brain)
+ if(brain)
+ if(H.check_brain_threshold(BRAIN_DAMAGE_RATIO_CRITICAL)) // 100
msgs += "Subject is brain dead."
- else if(H.getBrainLoss() >= 60)
+ else if(H.check_brain_threshold(BRAIN_DAMAGE_RATIO_MODERATE)) // 60
msgs += "Severe brain damage detected. Subject likely to have dementia."
- else if(H.getBrainLoss() >= 10)
+ else if(H.check_brain_threshold(BRAIN_DAMAGE_RATIO_MINOR)) // 10
msgs += "Significant brain damage detected. Subject may have had a concussion."
else
msgs += "Subject has no brain."
diff --git a/code/game/objects/items/weapons/pneumaticCannon.dm b/code/game/objects/items/weapons/pneumaticCannon.dm
index a01092be7164..75b9ee129439 100644
--- a/code/game/objects/items/weapons/pneumaticCannon.dm
+++ b/code/game/objects/items/weapons/pneumaticCannon.dm
@@ -46,7 +46,7 @@
* Arguments:
* * I - item to load into the cannon
* * user - the person loading the item in
-* Returns:
+* * Returns:
* * True if item was loaded, false if it failed
*/
/obj/item/pneumatic_cannon/proc/load_item(obj/item/I, mob/user)
@@ -59,7 +59,6 @@
if(!user.unEquip(I) || I.flags & (ABSTRACT | NODROP | DROPDEL))
to_chat(user, "You can't put [I] into [src]!")
return FALSE
- to_chat(user, "You load [I] into [src].")
loaded_items.Add(I)
loaded_weight_class += I.w_class
I.forceMove(src)
diff --git a/code/game/objects/items/weapons/shards.dm b/code/game/objects/items/weapons/shards.dm
index de8e2279f08a..a25c78af6c83 100644
--- a/code/game/objects/items/weapons/shards.dm
+++ b/code/game/objects/items/weapons/shards.dm
@@ -91,3 +91,15 @@
materials = list(MAT_PLASMA = MINERAL_MATERIAL_AMOUNT * 0.5, MAT_GLASS = MINERAL_MATERIAL_AMOUNT)
icon_prefix = "plasma"
welded_type = /obj/item/stack/sheet/plasmaglass
+
+/obj/item/shard/scrap
+ name = "sharpened scrap"
+ desc = "Some discarded scrap metal. It has sharp, jagged edges."
+ icon_state = "scrap"
+ materials = list(MAT_METAL = MINERAL_MATERIAL_AMOUNT)
+ welded_type = /obj/item/stack/sheet/metal
+ force = 9
+ throwforce = 15 //owie
+
+/obj/item/shard/scrap/set_initial_icon_state()
+ return
diff --git a/code/game/objects/items/weapons/stunbaton.dm b/code/game/objects/items/weapons/stunbaton.dm
index a0fead4ca3f7..012ed2e4e148 100644
--- a/code/game/objects/items/weapons/stunbaton.dm
+++ b/code/game/objects/items/weapons/stunbaton.dm
@@ -20,7 +20,7 @@
var/turned_on = FALSE
/// How much power does it cost to stun someone
var/hitcost = 1000
- var/obj/item/stock_parts/cell/high/cell = null
+ var/obj/item/stock_parts/cell/cell = null // Adminbus tip: make this something that isn't a cell :)
/// the initial cooldown tracks the time between swings. tracks the world.time when the baton is usable again.
var/cooldown = 3.5 SECONDS
/// the time it takes before the target falls over
@@ -50,11 +50,16 @@
/obj/item/melee/baton/proc/link_new_cell(unlink = FALSE)
if(unlink)
cell = null
- else if(isrobot(loc.loc)) // First loc is the module
+ return
+ if(isrobot(loc?.loc)) // First loc is the module
var/mob/living/silicon/robot/R = loc.loc
cell = R.cell
+ return
+ if(!cell)
+ var/powercell = /obj/item/stock_parts/cell/high
+ cell = new powercell(src)
else
- cell = new(src)
+ cell = new cell(src)
/obj/item/melee/baton/suicide_act(mob/user)
user.visible_message("[user] is putting the live [name] in [user.p_their()] mouth! It looks like [user.p_theyre()] trying to commit suicide!")
@@ -207,6 +212,10 @@
if(HAS_TRAIT_FROM(L, TRAIT_WAS_BATONNED, user_UID)) // prevents double baton cheese.
return FALSE
+ if(hitcost > 0 && cell?.charge < hitcost)
+ to_chat(user, "[src] fizzles weakly as it makes contact. It needs more power!")
+ return FALSE
+
cooldown = world.time + initial(cooldown) // tracks the world.time when hitting will be next available.
if(ishuman(L))
var/mob/living/carbon/human/H = L
@@ -230,10 +239,13 @@
L.visible_message("[user] has stunned [L] with [src]!",
"[L == user ? "You stun yourself" : "[user] has stunned you"] with [src]!")
add_attack_logs(user, L, "stunned")
- playsound(src, 'sound/weapons/egloves.ogg', 50, TRUE, -1)
+ play_hit_sound()
deductcharge(hitcost)
return TRUE
+/obj/item/melee/baton/proc/play_hit_sound()
+ playsound(src, 'sound/weapons/egloves.ogg', 50, TRUE, -1)
+
/obj/item/melee/baton/proc/thrown_baton_stun(mob/living/carbon/human/L)
if(cooldown > world.time)
return FALSE
@@ -321,3 +333,69 @@
name = "electrically-charged arm"
desc = "A piece of scrap metal wired directly to your power cell."
hitcost = 100
+
+/obj/item/melee/baton/flayerprod
+ name = "stunprod"
+ desc = "A mechanical mass which you can use to incapacitate someone with."
+ icon_state = "swarmprod"
+ base_icon = "swarmprod"
+ item_state = "swarmprod"
+ force = 10
+ throwforce = 0 // Just in case
+ knockdown_duration = 6 SECONDS
+ knockdown_delay = 0 SECONDS
+ w_class = WEIGHT_CLASS_BULKY
+ flags = ABSTRACT | NODROP
+ turned_on = TRUE
+ cell = /obj/item/stock_parts/cell/flayerprod
+ /// The duration that stunning someone will disable their radio for
+ var/radio_disable_time = 8 SECONDS
+
+/obj/item/melee/baton/flayerprod/Initialize(mapload) // We are not making a flayerprod without a cell
+ link_new_cell()
+ return ..()
+
+/obj/item/melee/baton/flayerprod/update_icon_state()
+ return
+
+/obj/item/melee/baton/flayerprod/attackby(obj/item/I, mob/user, params)
+ return
+
+/obj/item/melee/baton/flayerprod/screwdriver_act(mob/living/user, obj/item/I)
+ return
+
+/obj/item/melee/baton/flayerprod/attack_self(mob/user)
+ return
+
+/obj/item/melee/baton/flayerprod/play_hit_sound()
+ playsound(src, 'sound/weapons/egloves.ogg', 25, TRUE, -1, ignore_walls = FALSE)
+
+/obj/item/melee/baton/flayerprod/baton_stun(mob/living/L, mob/user, skip_cooldown, ignore_shield_check = FALSE)
+ if(..())
+ disable_radio(L)
+ L.radio_enable_timer = addtimer(CALLBACK(src, PROC_REF(enable_radio), L), radio_disable_time, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE | TIMER_DELETE_ME)
+ return TRUE
+ return FALSE
+
+/obj/item/melee/baton/flayerprod/proc/disable_radio(mob/living/L)
+ var/list/all_items = L.GetAllContents()
+ for(var/obj/item/radio/R in all_items)
+ R.on = FALSE
+ R.listening = FALSE
+ R.broadcasting = FALSE
+ L.visible_message("[R] buzzes loudly as it short circuits!", blind_message = "You hear a loud, electronic buzzing.")
+
+/obj/item/melee/baton/flayerprod/proc/enable_radio(mob/living/L)
+ var/list/all_items = L.GetAllContents()
+ for(var/obj/item/radio/R in all_items)
+ R.on = TRUE
+ R.listening = TRUE
+
+/obj/item/melee/baton/flayerprod/deductcharge(amount)
+ if(cell.charge < hitcost)
+ return
+ cell.use(amount)
+
+/obj/item/melee/baton/flayerprod/examine(mob/user)
+ . = ..()
+ . += "This one seems to be able to interfere with radio headsets."
diff --git a/code/modules/admin/player_panel.dm b/code/modules/admin/player_panel.dm
index dc598b438c5f..8240bbeae686 100644
--- a/code/modules/admin/player_panel.dm
+++ b/code/modules/admin/player_panel.dm
@@ -460,6 +460,9 @@
if(length(SSticker.mode.vampires))
dat += check_role_table("Vampires", SSticker.mode.vampires)
+ if(length(SSticker.mode.mindflayers))
+ dat += check_role_table("Mindflayers", SSticker.mode.mindflayers)
+
if(length(SSticker.mode.vampire_enthralled))
dat += check_role_table("Vampire Thralls", SSticker.mode.vampire_enthralled)
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index b113e0b0ea7d..7f96ef12ef46 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -67,7 +67,11 @@
log_admin("[key_name(usr)] has spawned an abductor team.")
if(!makeAbductorTeam())
to_chat(usr, "Unfortunately there weren't enough candidates available.")
-
+ if("8")
+ log_admin("[key_name(usr)] has spawned mindflayers.")
+ if(!makeMindflayers())
+ to_chat(usr, "Unfortunately there weren't enough candidates available.")
+
else if(href_list["dbsearchckey"] || href_list["dbsearchadmin"] || href_list["dbsearchip"] || href_list["dbsearchcid"] || href_list["dbsearchbantype"])
var/adminckey = href_list["dbsearchadmin"]
var/playerckey = href_list["dbsearchckey"]
@@ -814,6 +818,8 @@
return
else if(href_list["boot2"])
+ if(!check_rights(R_ADMIN|R_MOD))
+ return
var/mob/M = locateUID(href_list["boot2"])
if(!ismob(M))
return
@@ -1785,6 +1791,8 @@
C.jumptocoord(x,y,z)
else if(href_list["adminchecklaws"])
+ if(!check_rights(R_ADMIN|R_MENTOR))
+ return
output_ai_laws()
else if(href_list["adminmoreinfo"])
@@ -2343,22 +2351,6 @@
log_admin("[key_name(src.owner)] sent [key_name(H)] a standard '[stype]' fax")
message_admins("[key_name_admin(src.owner)] replied to [key_name_admin(H)] with a standard '[stype]' fax")
- else if(href_list["HONKReply"])
- var/mob/living/carbon/human/H = locateUID(href_list["HONKReply"])
- if(!istype(H))
- to_chat(usr, "This can only be used on instances of type /mob/living/carbon/human")
- return
- if(!istype(H.l_ear, /obj/item/radio/headset) && !istype(H.r_ear, /obj/item/radio/headset))
- to_chat(usr, "The person you are trying to contact is not wearing a headset")
- return
-
- var/input = input(src.owner, "Please enter a message to reply to [key_name(H)] via [H.p_their()] headset.","Outgoing message from HONKplanet", "")
- if(!input) return
-
- to_chat(src.owner, "You sent [input] to [H] via a secure channel.")
- log_admin("[src.owner] replied to [key_name(H)]'s HONKplanet message with the message [input].")
- to_chat(H, "You hear something crackle in your headset for a moment before a voice speaks. \"Please stand by for a message from your HONKbrothers. Message as follows, HONK. [input]. Message ends, HONK.\"")
-
else if(href_list["ErtReply"])
if(!check_rights(R_ADMIN))
return
diff --git a/code/modules/admin/verbs/one_click_antag.dm b/code/modules/admin/verbs/one_click_antag.dm
index 2c225d28fc1d..df1319a42e63 100644
--- a/code/modules/admin/verbs/one_click_antag.dm
+++ b/code/modules/admin/verbs/one_click_antag.dm
@@ -20,6 +20,7 @@
Make Wizard (Requires Ghosts)
Make Vampires
Make Abductor Team (Requires Ghosts)
+ Make Mindflayers
"}
// SS220 ADD - Start
dat += {"
@@ -37,7 +38,7 @@
if(M.stat || !M.mind || M.mind.special_role || M.mind.offstation_role)
return FALSE
if(temp)
- if((M.mind.assigned_role in temp.restricted_jobs) || (M.client.prefs.active_character.species in temp.protected_species))
+ if((M.mind.assigned_role in temp.restricted_jobs) || (M.client.prefs.active_character.species in temp.species_to_mindflayer))
return FALSE
if(role) // Don't even bother evaluating if there's no role
if(player_old_enough_antag(M.client,role) && (role in M.client.prefs.be_special) && !M.client.skip_antag && (!jobban_isbanned(M, role)))
@@ -293,6 +294,29 @@
return 1
return 0
+/datum/admins/proc/makeMindflayers()
+ var/datum/game_mode/vampire/temp = new()
+
+ if(GLOB.configuration.gamemode.prevent_mindshield_antags)
+ temp.restricted_jobs += temp.protected_jobs
+
+ var/input_num = input(owner, "How many Mindflayers you want to create? Enter 0 to cancel","Amount:", 0) as num|null
+ if(input_num <= 0 || isnull(input_num))
+ qdel(temp)
+ return FALSE
+
+ log_admin("[key_name(owner)] tried making [input_num] Mindflayers with One-Click-Antag")
+ message_admins("[key_name_admin(owner)] tried making [input_num] Mindflayers with One-Click-Antag")
+ var/list/possible_mindflayers = temp.get_players_for_role(ROLE_MIND_FLAYER, FALSE, "Machine")
+ var/num_mindflayers = min(length(possible_mindflayers), input_num)
+ if(!num_mindflayers)
+ return FALSE
+ for(var/i in 1 to num_mindflayers)
+ var/datum/mind/flayer = pick_n_take(possible_mindflayers)
+ flayer.make_mind_flayer()
+ qdel(temp)
+ return TRUE
+
/datum/admins/proc/makeThunderdomeTeams() // Not strictly an antag, but this seemed to be the best place to put it.
var/max_thunderdome_players = 10
var/team_to_assign_to = "Green"
diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm
index 867cf58c2720..dec5fd77c321 100644
--- a/code/modules/antagonists/_common/antag_datum.dm
+++ b/code/modules/antagonists/_common/antag_datum.dm
@@ -39,6 +39,8 @@ GLOBAL_LIST_EMPTY(antagonists)
var/clown_removal_text = "You are clumsy again."
/// The spawn class to use for gain/removal clown text
var/clown_text_span_class = "boldnotice"
+ /// If the antagonist can have their spoken voice be something else, this is the "voice" that they will appear as.
+ var/mimicking = ""
/// The url page name for this antagonist, appended to the end of the wiki url in the form of: [GLOB.configuration.url.wiki_url]/index.php/[wiki_page_name]
var/wiki_page_name
/// The organization, if any, this antag is associated with
@@ -406,6 +408,63 @@ GLOBAL_LIST_EMPTY(antagonists)
/datum/antagonist/proc/finalize_antag()
return
+/**
+ * Create and assign a full set of randomized, basic human traitor objectives.
+ * can_hijack - If you want the 10% chance for the antagonist to be able to roll hijack, only true for traitors
+ */
+/datum/antagonist/proc/forge_basic_objectives(can_hijack = FALSE)
+ // Hijack objective.
+ if(can_hijack && prob(10) && !(locate(/datum/objective/hijack) in owner.get_all_objectives()))
+ add_antag_objective(/datum/objective/hijack)
+ return // Hijack should be their only objective (normally), so return.
+
+ // Will give normal steal/kill/etc. type objectives.
+ for(var/i in 1 to GLOB.configuration.gamemode.traitor_objectives_amount)
+ forge_single_human_objective()
+
+ var/can_succeed_if_dead = TRUE
+ for(var/datum/objective/O in owner.get_all_objectives())
+ if(!O.martyr_compatible) // Check if our current objectives can co-exist with martyr.
+ can_succeed_if_dead = FALSE
+ break
+
+ // Give them an escape objective if they don't have one already.
+ if(!(locate(/datum/objective/escape) in owner.get_all_objectives()) && (!can_succeed_if_dead || prob(80)))
+ add_antag_objective(/datum/objective/escape)
+
+
+/**
+ * Create and assign a single randomized human traitor objective.
+ */
+/datum/antagonist/proc/forge_single_human_objective()
+ var/datum/objective/objective_to_add
+
+ // If our org has an objectives list, give one to us if we pass a roll on the org's focus
+ if(organization && length(organization.objectives) && prob(organization.focus))
+ objective_to_add = pick(organization.objectives)
+ else
+ if(prob(50))
+ if(length(active_ais()) && prob(100 / length(GLOB.player_list)))
+ objective_to_add = /datum/objective/destroy
+
+ else if(prob(5))
+ objective_to_add = /datum/objective/debrain
+
+ else if(prob(30))
+ objective_to_add = /datum/objective/maroon
+
+ else if(prob(30))
+ objective_to_add = /datum/objective/assassinateonce
+
+ else
+ objective_to_add = /datum/objective/assassinate
+ else
+ objective_to_add = /datum/objective/steal
+
+ if(delayed_objectives)
+ objective_to_add = new /datum/objective/delayed(objective_to_add)
+ add_antag_objective(objective_to_add)
+
//Individual roundend report
/datum/antagonist/proc/roundend_report()
var/list/report = list()
diff --git a/code/modules/antagonists/changeling/datum_changeling.dm b/code/modules/antagonists/changeling/datum_changeling.dm
index f8507d96cd8f..8d7ab14578f7 100644
--- a/code/modules/antagonists/changeling/datum_changeling.dm
+++ b/code/modules/antagonists/changeling/datum_changeling.dm
@@ -42,8 +42,6 @@ RESTRICT_TYPE(/datum/antagonist/changeling)
var/is_absorbing = FALSE
/// The amount of points available to purchase changeling abilities.
var/genetic_points = 20
- /// A name that will display in place of the changeling's real name when speaking.
- var/mimicing = ""
/// If the changeling can respec their purchased abilities.
var/can_respec = FALSE
/// The current sting power the changeling has active.
@@ -221,7 +219,7 @@ RESTRICT_TYPE(/datum/antagonist/changeling)
chem_recharge_rate = initial(chem_recharge_rate)
chem_charges = min(chem_charges, chem_storage)
chem_recharge_slowdown = initial(chem_recharge_slowdown)
- mimicing = null
+ mimicking = null
/**
* Removes a changeling's abilities.
diff --git a/code/modules/antagonists/changeling/powers/mimic_voice.dm b/code/modules/antagonists/changeling/powers/mimic_voice.dm
index ca3c654e7b33..c6552a077755 100644
--- a/code/modules/antagonists/changeling/powers/mimic_voice.dm
+++ b/code/modules/antagonists/changeling/powers/mimic_voice.dm
@@ -12,8 +12,8 @@
// Fake Voice
/datum/action/changeling/mimicvoice/sting_action(mob/user)
- if(cling.mimicing)
- cling.mimicing = ""
+ if(cling.mimicking)
+ cling.mimicking = ""
to_chat(user, "We return our vocal glands to their original position.")
return FALSE
@@ -21,7 +21,7 @@
if(!mimic_voice)
return FALSE
- cling.mimicing = mimic_voice
+ cling.mimicking = mimic_voice
to_chat(user, "We shape our glands to take the voice of [mimic_voice].")
to_chat(user, "Use this power again to return to our original voice.")
diff --git a/code/modules/antagonists/mind_flayer/flayer_datum.dm b/code/modules/antagonists/mind_flayer/flayer_datum.dm
new file mode 100644
index 000000000000..637bcd1d7884
--- /dev/null
+++ b/code/modules/antagonists/mind_flayer/flayer_datum.dm
@@ -0,0 +1,322 @@
+/datum/antagonist/mindflayer
+ name = "Mindflayer"
+ antag_hud_type = ANTAG_HUD_MIND_FLAYER
+ antag_hud_name = "hudflayer"
+ special_role = SPECIAL_ROLE_MIND_FLAYER
+ wiki_page_name = "Mindflayer"
+ /// The current amount of swarms the mind flayer has access to purchase with
+ var/usable_swarms = 0
+ /// The total points of brain damage the flayer has harvested, only used for logging purposes.
+ var/total_swarms_gathered = 0
+ /// The current person being drained
+ var/mob/living/carbon/human/harvesting
+ /// The list of all purchased spell and passive objects
+ var/list/powers = list()
+ /// A list of all powers and passives mindflayers can buy
+ var/list/ability_list = list()
+ ///List for keeping track of who has already been drained
+ var/list/drained_humans = list()
+ /// How fast the flayer's touch drains
+ var/drain_multiplier = 1
+ /// The base brain damage dealt per tick of the drain
+ var/drain_amount = 0.5
+ /// A list of the categories and their associated stages of the power
+ var/list/category_stage = list(FLAYER_CATEGORY_GENERAL = 1, FLAYER_CATEGORY_DESTROYER = 1, FLAYER_CATEGORY_INTRUDER = 1)
+ /// If the mindflayer can still pick a stage 4 ability
+ var/can_pick_capstone = TRUE
+ /// Have we notified that our victim does not give swarms from draining
+ var/has_notified = FALSE
+
+/datum/antagonist/mindflayer/New()
+ . = ..()
+ if(!length(ability_list))
+ ability_list = get_spells_of_type(FLAYER_PURCHASABLE_POWER)
+ ability_list += get_passives_of_type(FLAYER_PURCHASABLE_POWER)
+
+/datum/antagonist/mindflayer/Destroy(force, ...)
+ QDEL_NULL(owner.current?.hud_used?.vampire_blood_display)
+ harvesting = null
+ remove_all_abilities()
+ remove_all_passives()
+ ..()
+
+/datum/antagonist/mindflayer/add_owner_to_gamemode()
+ SSticker.mode.mindflayers += owner
+
+/datum/antagonist/mindflayer/remove_owner_from_gamemode()
+ SSticker.mode.mindflayers -= owner
+
+// This proc adds extra things, and base abilities that the mindflayer should get upon becoming a mindflayer
+/datum/antagonist/mindflayer/on_gain()
+ . = ..()
+ var/list/innate_powers = get_spells_of_type(FLAYER_INNATE_POWER)
+ for(var/power_path as anything in innate_powers)
+ var/datum/spell/flayer/to_add = new power_path
+ add_ability(to_add)
+ owner.current.faction += list("flayer") // In case our robot is mindlessly spawned in somehow, and they won't accidentally kill us
+
+/datum/antagonist/mindflayer/give_objectives()
+ add_antag_objective(/datum/objective/swarms)
+ forge_basic_objectives()
+
+/datum/antagonist/mindflayer/proc/get_swarms()
+ return usable_swarms
+
+/datum/antagonist/mindflayer/proc/set_swarms(amount, update_total = FALSE) //If you adminbus someone's swarms it won't add or remove to the total
+ var/old_swarm_amount = usable_swarms
+ usable_swarms = amount
+ if(update_total)
+ var/difference = usable_swarms - old_swarm_amount
+ if(difference < 0)
+ // Otherwise buying an ability can reduce your `total_swarms_gathered`
+ return
+ total_swarms_gathered += difference
+
+/*
+* amount - The positive or negative number to adjust the swarm count by, result clamped above 0
+*/
+/datum/antagonist/mindflayer/proc/adjust_swarms(amount, bound_lower = 0, bound_upper = INFINITY)
+ set_swarms(clamp((usable_swarms + amount), bound_lower, bound_upper), TRUE)
+
+//This is mostly for flavor, for framing messages as coming from the swarm itself. The other reason is so I can type "span" less.
+/datum/antagonist/mindflayer/proc/send_swarm_message(message)
+ if(HAS_TRAIT(owner.current, TRAIT_MINDFLAYER_NULLIFIED))
+ message = stutter(message, 0, TRUE)
+ to_chat(owner.current, "[message]")
+
+/**
+ Checks for any reason that you should not be able to drain someone for.
+ Returns either true or false, if the harvest will work or not.
+*/
+/datum/antagonist/mindflayer/proc/check_valid_harvest(mob/living/carbon/human/H)
+ if(HAS_TRAIT(owner.current, TRAIT_MINDFLAYER_NULLIFIED))
+ send_swarm_message("We do not have the energy for this...")
+ return FALSE
+ if(IS_MINDFLAYER(H))
+ send_swarm_message("Their hive rejects the connection.")
+ return FALSE
+ var/obj/item/organ/internal/brain/brain = H.get_int_organ(/obj/item/organ/internal/brain)
+ if(!istype(brain))
+ send_swarm_message("This entity has no brain to harvest from.")
+ return FALSE
+ if(!H.ckey && !H.player_ghosted)
+ send_swarm_message("This brain does not contain the spark that feeds us. Find more suitable prey.")
+ return FALSE
+ if(brain.damage >= brain.max_damage || (H.stat == DEAD && !H.has_status_effect(STATUS_EFFECT_RECENTLY_SUCCUMBED)))
+ send_swarm_message("We detect no neural activity to harvest from this brain.")
+ return FALSE
+ if(HAS_MIND_TRAIT(H, TRAIT_XENOBIO_SPAWNED_HUMAN))
+ send_swarm_message("This brain is unsuitable to harvest.")
+ return FALSE
+
+ var/unique_drain_id = H.UID()
+ if(isnull(drained_humans[unique_drain_id]))
+ drained_humans[unique_drain_id] = 0
+ else if(drained_humans[unique_drain_id] > BRAIN_DRAIN_LIMIT)
+ if(!has_notified)
+ has_notified = TRUE
+ send_swarm_message("You have drained most of the life force from [H]'s brain, and you will get no more swarms from them!")
+ return DRAIN_BUT_NO_SWARMS
+ return TRUE
+
+/**
+ Begins draining the brain of H, gains swarms equal to the amount of brain damage dealt per tick. Upgrades can increase the amount of damage per tick.
+**/
+/datum/antagonist/mindflayer/proc/handle_harvest(mob/living/carbon/human/H)
+ harvesting = H
+ var/obj/item/organ/internal/brain/drained_brain = H.get_int_organ(/obj/item/organ/internal/brain)
+ if(!istype(drained_brain))
+ return
+ var/unique_drain_id = H.UID()
+ owner.current.visible_message(
+ "[owner.current] puts [owner.current.p_their()] fingers on [H]'s [drained_brain.parent_organ] and begins harvesting!",
+ "We begin our harvest on [H].",
+ "You hear the hum of electricity."
+ )
+ if(!do_mob(owner.current, H, time = 2 SECONDS))
+ send_swarm_message("Our connection was incomplete.")
+ harvesting = null
+ return
+ while(do_mob(owner.current, H, time = DRAIN_TIME, progress = FALSE))
+ var/check_harvest = check_valid_harvest(H)
+ if(!check_harvest)
+ harvesting = null
+ has_notified = FALSE
+ return
+ H.Beam(owner.current, icon_state = "drain_life", icon ='icons/effects/effects.dmi', time = DRAIN_TIME, beam_color = COLOR_ASSEMBLY_PURPLE)
+ var/damage_to_deal = (drain_amount * drain_multiplier * H.dna.species.brain_mod)
+ H.adjustBrainLoss(damage_to_deal, use_brain_mod = FALSE) //No need to use brain damage modification since we already got it from the previous line
+
+ if(check_harvest != DRAIN_BUT_NO_SWARMS)
+ adjust_swarms(damage_to_deal)
+ drained_humans[unique_drain_id] += damage_to_deal
+
+ // Lasting effects. Every second of draining requires 4 seconds of healing
+ drained_brain.max_damage -= 0.25 // As much damage as the default drain
+ drained_brain.temporary_damage += 0.25
+
+ send_swarm_message("Our connection severs.")
+ harvesting = null
+ has_notified = FALSE
+
+/datum/antagonist/mindflayer/greet()
+ var/list/messages = list()
+ SEND_SOUND(owner.current, sound('sound/ambience/antag/mindflayer_alert.ogg'))
+ messages += "You feel something stirring within your chassis... You are a Mindflayer!
"
+ messages += "To harvest someone, target where the brain of your victim is and use harm intent with an empty hand. Drain intelligence to increase your swarm."
+ return messages
+
+/**
+ * Gets a list of mind flayer spell typepaths based on the passed in `spell_type`. (Thanks for the code SteelSlayer)
+ *
+ * Arguments:
+ * * spell_type - should be a define related to [/datum/spell/flayer/power_type].
+ */
+/datum/antagonist/mindflayer/proc/get_spells_of_type(spell_type)
+ var/list/spells = list()
+ for(var/spell_path in subtypesof(/datum/spell/flayer))
+ var/datum/spell/flayer/spell = spell_path
+ if(initial(spell.power_type) != spell_type)
+ continue
+ spells += spell_path
+ return spells
+
+/**
+ * Gets a list of mind flayer passive typepaths based on the passed in `passive_type`.
+ *
+ * Arguments:
+ * * passive_type - should be a define related to [/datum/spell/flayer/passive_type].
+ */
+/datum/antagonist/mindflayer/proc/get_passives_of_type(passive_type)
+ var/list/passives = list()
+ for(var/passive_path in subtypesof(/datum/mindflayer_passive))
+ var/datum/mindflayer_passive/passive = passive_path
+ if(initial(passive.power_type) != passive_type)
+ continue
+ passives += passive_path
+ return passives
+
+///////////////////////////////////////////////////////////////
+// A BUNCH OF PROCS THAT HANDLE ADDING ABILITIES AND PASSIVES
+/////////////////////////////////////////////////////////////
+
+/**
+* Adds an ability to a mindflayer if they don't already have it, upgrades it if they do.
+* Arguments:
+* * to_add - The spell datum you want to add to the flayer
+* * set_owner - The antagonist datum of the mindflayer you want to add the spell to
+*/
+/datum/antagonist/mindflayer/proc/add_ability(datum/spell/flayer/to_add)
+ if(!to_add)
+ return
+ var/datum/spell/flayer/spell = has_spell(to_add)
+ if(spell)
+ spell.on_apply()
+ qdel(to_add)
+ return
+
+ to_add.flayer = src
+
+ to_add.level = 1
+ to_add.current_cost += to_add.static_upgrade_increase
+ owner.AddSpell(to_add)
+ powers += to_add
+ to_add.spell_purchased()
+
+ check_special_stage_ability(to_add)
+ SSblackbox.record_feedback("nested tally", "mindflayer_abilities", 1, list(to_add.name, "purchased"))
+
+/**
+* Adds a passive to a mindflayer if they don't already have it, upgrades it if they do.
+* Arguments:
+* * to_add - The spell datum you want to add to the flayer
+* * upgrade_type - optional argument if you need to communicate a define to the passive in question
+*/
+/datum/antagonist/mindflayer/proc/add_passive(datum/mindflayer_passive/to_add, upgrade_type) //Passives always need to have their owners set
+ if(!to_add)
+ return
+ var/datum/mindflayer_passive/passive = has_passive(to_add)
+ if(passive)
+ passive.on_apply()
+ qdel(to_add)
+ return
+
+ to_add.flayer = src
+ to_add.owner = owner.current // Passives always need to have their owners set here
+ to_add.on_apply()
+ powers += to_add
+
+ check_special_stage_ability(to_add)
+ SSblackbox.record_feedback("nested tally", "mindflayer_abilities", 1, list(to_add.name, "purchased"))
+
+/*
+ * Removing abilities/passives starts here
+ */
+/datum/antagonist/mindflayer/proc/remove_all_abilities()
+ for(var/datum/spell/flayer/spell in powers)
+ remove_ability(spell)
+
+/datum/antagonist/mindflayer/proc/remove_all_passives()
+ for(var/datum/mindflayer_passive/passive in powers)
+ remove_passive(passive)
+
+/datum/antagonist/mindflayer/proc/remove_ability(datum/spell/flayer/to_remove)
+ owner.RemoveSpell(to_remove)
+ powers -= to_remove
+
+/datum/antagonist/mindflayer/proc/remove_passive(datum/mindflayer_passive/to_remove)
+ powers -= to_remove
+ qdel(to_remove) //qdel should call destroy, which should call on_remove
+
+/**
+* Checks if a mindflayer has a given spell already
+* * Arguments: to_get - Some datum/spell/flayer to check if a mindflayer has
+* * Returns: The datum/spell/mindflayer if the mindflayer has the power already, null otherwise
+*/
+/datum/antagonist/mindflayer/proc/has_spell(datum/spell/flayer/to_get)
+ for(var/datum/spell/flayer/spell in powers)
+ if(istype(spell, to_get))
+ return spell
+
+/**
+* Checks if a mindflayer has a given passive already
+* * Arguments: to_get - Some datum/mindflayer_passive to check if a mindflayer has
+* * Returns: The datum/mindflayer_passive if the mindflayer has the passive already, null otherwise
+*/
+/datum/antagonist/mindflayer/proc/has_passive(datum/mindflayer_passive/to_get)
+ for(var/datum/mindflayer_passive/spell in powers)
+ if(to_get.name == spell.name)
+ return spell
+
+/// This is the proc that gets called every tick of life(), use this for updating something that should update every few seconds
+/datum/antagonist/mindflayer/proc/handle_mindflayer()
+ if(owner.current.hud_used)
+ var/datum/hud/hud = owner.current.hud_used
+ if(!hud.vampire_blood_display)
+ hud.vampire_blood_display = new /atom/movable/screen()
+ hud.vampire_blood_display.name = "Usable Swarms"
+ hud.vampire_blood_display.icon_state = "blood_display"
+ hud.vampire_blood_display.screen_loc = "WEST:6,CENTER-1:15"
+ hud.static_inventory += hud.vampire_blood_display
+ hud.show_hud(hud.hud_version)
+ hud.vampire_blood_display.maptext = "