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 = "
[usable_swarms]
" + +/** +* Checks if we are eligible to get a special ability for reaching the third stage in a given subclass +*/ +/datum/antagonist/mindflayer/proc/check_special_stage_ability(datum/spell/flayer/adding_spell) + if(!istype(adding_spell) || category_stage[adding_spell.category] < 3) + return + + switch(adding_spell.category) + if(FLAYER_CATEGORY_DESTROYER) + if(has_spell(/datum/spell/flayer/techno_wall)) + return + add_ability(new /datum/spell/flayer/techno_wall) + send_swarm_message("We gain the ability to bring our internal firewalls into a crystalized form in the physical world") + + if(FLAYER_CATEGORY_INTRUDER) + if(has_spell(/datum/spell/flayer/self/weapon/access_tuner)) + return + add_ability(new /datum/spell/flayer/self/weapon/access_tuner) + send_swarm_message("We gain the ability to manipulate airlocks from a distance.") diff --git a/code/modules/antagonists/mind_flayer/flayer_power.dm b/code/modules/antagonists/mind_flayer/flayer_power.dm new file mode 100644 index 000000000000..b48c9c25ca92 --- /dev/null +++ b/code/modules/antagonists/mind_flayer/flayer_power.dm @@ -0,0 +1,269 @@ +/datum/spell/flayer + action_background_icon_state = "bg_flayer" + desc = "This spell needs a description!" + human_req = TRUE + clothes_req = FALSE + /// A reference to the owner mindflayer's antag datum. + var/datum/antagonist/mindflayer/flayer + + /// What level is our spell currently at + var/level = 0 + /// Max level of our spell + var/max_level = 1 + /// Determines whether the power is always given to the mind flayer or if it must be purchased. + var/power_type = FLAYER_UNOBTAINABLE_POWER + /// The initial cost of purchasing the spell. + var/base_cost = 0 + /// Should this spell's cost increase by a static amount every purchase? 0 means it will stay the base cost for every upgrade. + var/static_upgrade_increase = 0 + /// The current price to upgrade the spell + var/current_cost = 0 + + /// The class that this spell is for or FLAYER_CATEGORY_GENERAL to make it unrelated to a specific tree + var/category = FLAYER_CATEGORY_GENERAL + /// The current `stage` that we are on for our powers. Currently only hides powers of a higher stage. + var/stage = 1 + /// A brief description of what the spell's upgrades do + var/upgrade_info = "This spell needs upgrade info!" + /// If the spell checks for a nullification implant/effect, set to FALSE to make it castable despite nullification + var/checks_nullification = TRUE + +/datum/spell/flayer/self/create_new_targeting() + return new /datum/spell_targeting/self + +/datum/spell/flayer/Destroy(force, ...) + if(!flayer) + return ..() + flayer.powers -= src + flayer = null + return ..() + +/datum/spell/flayer/create_new_handler() + var/datum/spell_handler/flayer/handler = new() + handler.checks_nullification = checks_nullification + return handler + +/datum/spell_handler/flayer + /// Do we check for nullification + var/checks_nullification = TRUE + +/datum/spell_handler/flayer/can_cast(mob/user, charge_check, show_message, datum/spell/spell) + var/datum/antagonist/mindflayer/flayer_datum = user.mind.has_antag_datum(/datum/antagonist/mindflayer) + + if(!flayer_datum) + return FALSE + + if(user.stat == DEAD) + if(show_message) + flayer_datum.send_swarm_message("We can't cast this while you are dead...") + return FALSE + + if(checks_nullification && HAS_TRAIT(user, TRAIT_MINDFLAYER_NULLIFIED)) + flayer_datum.send_swarm_message("We do not have the energy to manifest that currently...") + return FALSE + return TRUE + +/// The shop for purchasing and upgrading abilities, from here on the rest of the file is just handling shopping. Specific powers are in the powers subfolder. +/datum/spell/flayer/self/augment_menu + name = "Self-Augment Operations" + desc = "Choose how we will upgrade ourselves." + action_icon_state = "choose_module" + base_cooldown = 0 SECONDS + power_type = FLAYER_INNATE_POWER + checks_nullification = FALSE + +/datum/spell/flayer/self/augment_menu/ui_state(mob/user) + return GLOB.always_state + +/datum/spell/flayer/self/augment_menu/cast(mob/user) + ui_interact(user) + +/datum/spell/flayer/self/augment_menu/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "AugmentMenu", name) + ui.set_autoupdate(FALSE) + ui.open() + +/datum/spell/flayer/self/augment_menu/ui_act(action, list/params, datum/tgui/ui) + var/mob/user = ui.user + if(user.stat) + return + + switch(action) + if("purchase") + var/path = text2path(params["ability_path"]) + on_purchase(user, path) + update_static_data(ui.user) + +// Takes in a category name and grabs the paths of all the spells/passives specific to that category. Used for TGUI +/datum/antagonist/mindflayer/proc/get_powers_of_category(category) + var/list/powers = list() + for(var/path in ability_list) + if(ispath(path, /datum/spell)) + var/datum/spell/flayer/spell = path + if(spell.category == category) + powers += list(list( + "name" = spell.name, + "desc" = spell.desc, + "max_level" = spell.max_level, + "cost" = spell.base_cost, + "stage" = spell.stage, + "ability_path" = spell.type + )) + else + var/datum/mindflayer_passive/passive = path + if(passive.category == category) + powers += list(list( + "name" = passive.name, + "desc" = passive.purchase_text, + "max_level" = passive.max_level, + "cost" = passive.base_cost, + "stage" = passive.stage, + "ability_path" = passive.type + )) + + return powers + +/datum/antagonist/mindflayer/proc/build_ability_tabs() + var/list/ability_tabs = list() + for(var/category in category_stage) + ability_tabs += list(list( + "category_name" = category, + "category_stage" = category_stage[category], + "abilities" = get_powers_of_category(category) + )) + return ability_tabs + +/datum/spell/flayer/self/augment_menu/ui_data(mob/user) + var/list/data = list() + var/list/known_abilities = list() + data["usable_swarms"] = flayer.usable_swarms + for(var/datum/mindflayer_passive/passive in flayer.powers) + known_abilities += list(list( + "name" = passive.name, + "current_level" = passive.level, + "max_level" = passive.max_level, + "cost" = passive.current_cost, + "upgrade_text" = passive.upgrade_info, + "ability_path" = passive.type + )) + + for(var/datum/spell/flayer/spell in flayer.powers) + known_abilities += list(list( + "name" = spell.name, + "current_level" = spell.level, + "max_level" = spell.max_level, + "cost" = spell.current_cost, + "upgrade_text" = spell.upgrade_info, + "ability_path" = spell.type + )) + data["known_abilities"] = known_abilities + return data + +/datum/spell/flayer/self/augment_menu/ui_static_data(mob/user) + var/list/static_data = list() + static_data["ability_tabs"] = flayer.build_ability_tabs() + return static_data + +/* + * Given a spell, checks if a mindflayer is able to afford, and has the prerequisites for that spell. + * If so it adds the ability and increments the category stage if needed, then returns TRUE + * otherwise, returns FALSE + */ +/datum/antagonist/mindflayer/proc/try_purchase_spell(datum/spell/flayer/to_add) + var/datum/spell/flayer/existing_spell = has_spell(to_add) + if(existing_spell && (existing_spell.level >= existing_spell.max_level)) + send_swarm_message("That function is already at its strongest.") + qdel(to_add) + return FALSE + + if(to_add.current_cost > get_swarms()) + send_swarm_message("We need [to_add.current_cost - get_swarms()] more swarm\s for this...") + qdel(to_add) + return FALSE + + if(category_stage[to_add.category] < to_add.stage) + send_swarm_message("We do not have all the knowledge needed for this.") + qdel(to_add) + return FALSE + + if(to_add.stage == FLAYER_CAPSTONE_STAGE) + if(!can_pick_capstone && !existing_spell) + send_swarm_message("We have already forsaken that knowledge.") + qdel(to_add) + return FALSE + + can_pick_capstone = FALSE + send_swarm_message("We evolve to the ultimate being.") + + if(category_stage[to_add.category] == to_add.stage) + category_stage[to_add.category] += 1 + + to_add.current_cost = to_add.base_cost + adjust_swarms(-to_add.current_cost) + add_ability(to_add) // Level gets set to 1 when AddSpell is called later, it also handles the cost + return TRUE // The reason we do this is cause we don't have the spell object that will get added to the mindflayer yet + +/* + * Given a passive, checks if a mindflayer is able to afford, and has the prerequisites for that spell. + * If so it adds the ability and increments the category stage if needed, then returns TRUE + * otherwise, returns FALSE + */ +/datum/antagonist/mindflayer/proc/try_purchase_passive(datum/mindflayer_passive/to_add) + var/datum/mindflayer_passive/existing_passive = has_passive(to_add) + if(existing_passive) + if(existing_passive.level >= to_add.max_level) + send_swarm_message("That function is already at its strongest.") + return FALSE + to_add.current_cost = existing_passive.current_cost + + if(to_add.current_cost > get_swarms()) + send_swarm_message("We need [to_add.current_cost - get_swarms()] more swarm\s for this...") + return FALSE + + if(category_stage[to_add.category] < to_add.stage) + send_swarm_message("We do not have all the knowledge needed for this...") + return FALSE + + if(to_add.stage == FLAYER_CAPSTONE_STAGE) + if(!can_pick_capstone && !existing_passive) + send_swarm_message("We have already forsaken that knowledge.") + return FALSE + can_pick_capstone = FALSE + send_swarm_message("We evolve to the ultimate being.") + if(category_stage[to_add.category] == to_add.stage) + category_stage[to_add.category] += 1 + + adjust_swarms(-to_add.current_cost) + add_passive(to_add, src) + return TRUE + +/* + * Mindflayer code relies on on_purchase to grant powers and passives. + * It first splits up whether the path bought was a passive or spell, then checks if the flayer can afford it. + * Returns TRUE if an ability was added, FALSE otherwise + */ +/datum/spell/flayer/proc/on_purchase(mob/user, datum/path) + SHOULD_CALL_PARENT(TRUE) + if(!istype(user) || !user.mind || !flayer) + qdel(src) + return FALSE + if(ispath(path, /datum/spell)) + var/datum/spell/flayer/to_add = new path(user) + return flayer.try_purchase_spell(to_add) + + var/datum/mindflayer_passive/to_add = new path(user) //If its not a spell, it's a passive + return flayer.try_purchase_passive(to_add) + +/// This is the proc that handles spell upgrades, override this to have upgrades change duration/strength etc +/datum/spell/flayer/proc/on_apply() + SHOULD_CALL_PARENT(TRUE) + level++ + current_cost += static_upgrade_increase + + SSblackbox.record_feedback("nested tally", "mindflayer_abilities", 1, list(name, "upgraded", level)) + +/// This is a proc that is called when the ability is purchased and first added to the flayer +/datum/spell/flayer/proc/spell_purchased() // I'd call it `on_purchased` but that is already taken + return diff --git a/code/modules/antagonists/mind_flayer/mindflayer_gamemode.dm b/code/modules/antagonists/mind_flayer/mindflayer_gamemode.dm new file mode 100644 index 000000000000..c6d3a33d9425 --- /dev/null +++ b/code/modules/antagonists/mind_flayer/mindflayer_gamemode.dm @@ -0,0 +1,55 @@ +//Used to hold any procs related to how mindflayers are handled from a gamemode perspective +/datum/game_mode/proc/auto_declare_completion_mindflayer() + if(!length(mindflayers)) + return + + var/list/text = list("The mindflayers were:") + for(var/datum/mind/mindflayer in mindflayers) + var/traitorwin = TRUE + text += "
[mindflayer.get_display_key()] was [mindflayer.name] (" + if(mindflayer.current) + if(mindflayer.current.stat == DEAD) + text += "died" + else + text += "survived" + else + text += "body destroyed" + text += ")" + + var/list/all_objectives = mindflayer.get_all_objectives() + + if(length(all_objectives))//If the traitor had no objectives, don't need to process this. + var/count = 1 + for(var/datum/objective/objective in all_objectives) + if(objective.check_completion()) + text += "
Objective #[count]: [objective.explanation_text] Success!" + if(istype(objective, /datum/objective/steal)) + var/datum/objective/steal/S = objective + SSblackbox.record_feedback("nested tally", "mindflayer_steal_objective", 1, list("Steal [S.steal_target]", "SUCCESS")) + else + SSblackbox.record_feedback("nested tally", "mindflayer_objective", 1, list("[objective.type]", "SUCCESS")) + else + text += "
Objective #[count]: [objective.explanation_text] Fail." + if(istype(objective, /datum/objective/steal)) + var/datum/objective/steal/S = objective + SSblackbox.record_feedback("nested tally", "mindflayer_steal_objective", 1, list("Steal [S.steal_target]", "FAIL")) + else + SSblackbox.record_feedback("nested tally", "mindflayer_objective", 1, list("[objective.type]", "FAIL")) + traitorwin = FALSE + count++ + + var/special_role_text + if(mindflayer.special_role) + special_role_text = lowertext(mindflayer.special_role) + else + special_role_text = "antagonist" + + if(traitorwin) + text += "
The [special_role_text] was successful!" + SSblackbox.record_feedback("tally", "mindflayer_success", 1, "SUCCESS") + else + text += "
The [special_role_text] has failed!" + SSblackbox.record_feedback("tally", "mindflayer_success", 1, "FAIL") + + return text.Join("") + diff --git a/code/modules/antagonists/mind_flayer/powers/flayer_buffs.dm b/code/modules/antagonists/mind_flayer/powers/flayer_buffs.dm new file mode 100644 index 000000000000..96413ad466d3 --- /dev/null +++ b/code/modules/antagonists/mind_flayer/powers/flayer_buffs.dm @@ -0,0 +1,102 @@ +// This is a file with all the powers that buff or heal a mindflayer in some way + +/datum/spell/flayer/self/rejuv + name = "Quick Reboot" + desc = "Heal and remove any incapacitating effects from yourself." + power_type = FLAYER_INNATE_POWER + checks_nullification = FALSE + action_icon = 'icons/mob/actions/flayer_actions.dmi' + action_icon_state = "quick_reboot" + upgrade_info = "Increase the amount you heal, decrease time between uses, and increase how long you heal for." + max_level = 4 + base_cooldown = 30 SECONDS + stat_allowed = UNCONSCIOUS + base_cost = 50 + current_cost = 50 // Innate abilities HAVE to set `current_cost` + static_upgrade_increase = 25 + /// Any extra duration we get from upgrading the spell. + var/extra_duration = 0 // The base spell is 5 brute/burn healing a second for 5 seconds + /// Any extra healing we get per second from upgrading the spell + var/extra_healing = 0 + +/datum/spell/flayer/self/rejuv/cast(list/targets, mob/living/user) + to_chat(user, "We begin to heal rapidly.") + user.apply_status_effect(STATUS_EFFECT_FLAYER_REJUV, extra_duration, extra_healing) + +/datum/spell/flayer/self/rejuv/on_apply() + ..() + cooldown_handler.recharge_duration -= 5 SECONDS + extra_duration += 2 SECONDS + extra_healing += 2 + +/datum/spell/flayer/self/quicksilver_form + name = "Quicksilver Form" + desc = "Allows us to transmute our physical form, letting us phase through glass and non-solid objects." + action_icon_state = "blink" + power_type = FLAYER_PURCHASABLE_POWER + base_cooldown = 40 SECONDS //25% uptime at base + category = FLAYER_CATEGORY_DESTROYER + stage = 2 + base_cost = 100 + max_level = 3 + upgrade_info = "After upgrading once, we also deflect projectiles shot at us. After upgrading a second time, the duration of the effect is doubled." + /// Do we get bullet reflection + var/should_get_reflection = FALSE + /// Extra duration we gain from upgrading + var/extra_duration = 0 // Base duration is 10 seconds + +/datum/spell/flayer/self/quicksilver_form/cast(list/targets, mob/living/user) + user.apply_status_effect(STATUS_EFFECT_QUICKSILVER_FORM, extra_duration, should_get_reflection) + +/datum/spell/flayer/self/quicksilver_form/on_apply() + ..() + switch(level) + if(FLAYER_POWER_LEVEL_TWO) + should_get_reflection = TRUE + if(FLAYER_POWER_LEVEL_THREE) + extra_duration += 10 SECONDS + +/// A toggle ability that makes you speedy and attack faster while heating up, level one cast is guaranteed to hurt a bit. +/datum/spell/flayer/self/overclock + name = "Overclock" + desc = "Allows us to move and attack faster, at the cost of putting extra strain on our motors and heating us up a dangerous amount." + power_type = FLAYER_PURCHASABLE_POWER + base_cooldown = 15 SECONDS + category = FLAYER_CATEGORY_DESTROYER + action_icon_state = "strained_muscles" + stage = 3 + max_level = 3 + base_cost = 125 + upgrade_info = "Upgrading this improves our heat sinks, making us heat up slower." + var/heat_per_tick = 22 + +/datum/spell/flayer/self/overclock/cast(list/targets, mob/living/user) + if(user.has_status_effect(STATUS_EFFECT_OVERCLOCK)) + user.remove_status_effect(STATUS_EFFECT_OVERCLOCK) + return + user.apply_status_effect(STATUS_EFFECT_OVERCLOCK, heat_per_tick) + +/datum/spell/flayer/self/overclock/on_apply() + ..() + heat_per_tick -= 5 + +/datum/spell/flayer/self/terminator_form + name = "T.E.R.M.I.N.A.T.O.R. Form" + desc = "For a short time, you become unable to die and are not slowed down by pain. This will not heal you however, \ + and you will still die when the duration ends if you are damaged enough. \ + Using quick reboot in this form will heal massive amounts of stamina damage." + power_type = FLAYER_PURCHASABLE_POWER + base_cooldown = 5 MINUTES // Base uptime is 20% + category = FLAYER_CATEGORY_DESTROYER + stage = FLAYER_CAPSTONE_STAGE + action_icon = "mutate" + base_cost = 200 + static_upgrade_increase = 50 // Total cost of 750 swarms + max_level = 3 + +/datum/spell/flayer/self/terminator_form/cast(list/targets, mob/living/user) + user.apply_status_effect(STATUS_EFFECT_TERMINATOR_FORM) + +/datum/spell/flayer/self/terminator_form/on_apply() + ..() + cooldown_handler.recharge_duration -= 1 MINUTES diff --git a/code/modules/antagonists/mind_flayer/powers/flayer_mobility_powers.dm b/code/modules/antagonists/mind_flayer/powers/flayer_mobility_powers.dm new file mode 100644 index 000000000000..814432d62345 --- /dev/null +++ b/code/modules/antagonists/mind_flayer/powers/flayer_mobility_powers.dm @@ -0,0 +1,220 @@ +//Basically shadow anchor, but the entry and exit point must be computers. I'm not in your walls I'm in your PC +/datum/spell/flayer/computer_recall + name = "Traceroute" + desc = "Allows us to cast a mark to a computer. To recall us to this computer, cast this next to a different computer. To check your current mark: Alt click." + base_cooldown = 60 SECONDS + action_icon_state = "pd_cablehop" + upgrade_info = "Halve the time it takes to recharge." + power_type = FLAYER_PURCHASABLE_POWER + category = FLAYER_CATEGORY_INTRUDER + centcom_cancast = FALSE + stage = 2 + base_cost = 125 + static_upgrade_increase = 25 + max_level = 2 + should_recharge_after_cast = FALSE + /// The console we currently have a mark on + var/obj/marked_computer + /// The typecache of things we are allowed to teleport to and from + var/static/list/machine_typecache = list() + +/datum/spell/flayer/computer_recall/New() + . = ..() + if(length(machine_typecache)) + return + machine_typecache = typecacheof(list( + /obj/machinery/computer, + /obj/machinery/power/apc, + /obj/machinery/alarm, + /obj/machinery/autolathe, + /obj/machinery/newscaster, + /obj/machinery/mecha_part_fabricator, + /obj/machinery/status_display, + /obj/machinery/requests_console, + /obj/item/radio/intercom, + /obj/machinery/economy/vending, + /obj/machinery/economy/atm, + /obj/machinery/chem_dispenser, + /obj/machinery/chem_master, + /obj/machinery/reagentgrinder, + /obj/machinery/sleeper, + /obj/machinery/bodyscanner, + /obj/machinery/photocopier, // HI YES ONE FLAYER FAXED TO MY OFFICE PLEASE + /obj/machinery/r_n_d/experimentor, // Like anyone is ever gonna teleport to this + /obj/machinery/barsign + )) + +/datum/spell/flayer/computer_recall/Destroy(force, ...) + marked_computer = null + return ..() + +/datum/spell/flayer/computer_recall/create_new_targeting() + var/datum/spell_targeting/click/T = new() + T.allowed_type = /obj + T.try_auto_target = TRUE + T.range = 1 + return T + +/datum/spell/flayer/computer_recall/cast(list/targets, mob/living/user) + var/obj/target + for(var/obj/thing as anything in targets) + if(is_type_in_typecache(thing, machine_typecache)) + target = thing + break + + if(!target) + flayer.send_swarm_message("That is not a valid target!") + return + + if(!marked_computer) + marked_computer = target + flayer.send_swarm_message("You discreetly tap [targets[1]] and mark it as your home computer.") + return + + if(SEND_SIGNAL(user, COMSIG_MOVABLE_TELEPORTING, get_turf(user)) & COMPONENT_BLOCK_TELEPORT) + return FALSE + + var/turf/start_turf = get_turf(target) + var/turf/end_turf = get_turf(marked_computer) + if(end_turf.z != start_turf.z) + flayer.send_swarm_message("The connection between [target] and [marked_computer] is too unstable!") + if(!is_teleport_allowed(start_turf.z) || !is_teleport_allowed(end_turf.z)) + return + user.visible_message( + "[user] de-materializes and jumps through the screen of [target]!", + "You de-materialize and jump into [target]!") + + user.set_body_position(STANDING_UP) + var/matrix/previous = user.transform + var/matrix/shrank = user.transform.Scale(0.25) + var/direction = get_dir(user, target) + var/list/direction_signs = get_signs_from_direction(direction) + animate(user, 0.5 SECONDS, 0, transform = shrank, pixel_x = 32 * direction_signs[1], pixel_y = 32 * direction_signs[2], dir = direction, easing = BACK_EASING|EASE_IN) //Blue skadoo, we can too! + user.Immobilize(0.5 SECONDS) + sleep(0.5 SECONDS) + target.Beam(marked_computer, icon_state = "rped_upgrade", icon = 'icons/effects/effects.dmi', time = 3 SECONDS, maxdistance = INFINITY) + playsound(start_turf, 'sound/items/pshoom.ogg', 100, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + + playsound(end_turf, 'sound/items/pshoom.ogg', 100, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + user.forceMove(end_turf) + user.pixel_x = 0 //Snap back to the center, then animate the un-shrinking + user.pixel_y = 0 + user.set_body_position(STANDING_UP) + animate(user, 0.5 SECONDS, 0, transform = previous) + user.visible_message( + "[user] suddenly crawls through the monitor of [marked_computer]!", + "As you reform yourself at [marked_computer] you feel the mark you left on it fade.") + marked_computer = null + cooldown_handler.start_recharge() + +/datum/spell/flayer/computer_recall/AltClick(mob/user) + if(!marked_computer) + flayer.send_swarm_message("You do not current have a marked computer.") + return + flayer.send_swarm_message("Your current mark is [marked_computer].") + +/datum/spell/flayer/computer_recall/on_apply() + ..() + cooldown_handler.recharge_duration -= 30 SECONDS + +/* + * Ok so this is slightly a stretch, but it hinders enemy mobility while not hindering the flayer + * It works like the hemomancer wall, creating up to 3 temporary walls + * Obtained for free in the Destroyer tree when reaching stage 3 + */ +/datum/spell/flayer/techno_wall + name = "Crystalized Firewall" + desc = "Allows us to create a wall between two points. The wall is fragile and allows only ourselves to pass through." + base_cooldown = 60 SECONDS + action_icon_state = "pd_cablehop" + upgrade_info = "Double the health of the barrier by reinforcing it with ICE." + category = FLAYER_CATEGORY_DESTROYER + power_type = FLAYER_UNOBTAINABLE_POWER + base_cost = 100 + current_cost = 100 + max_level = 2 + should_recharge_after_cast = FALSE + /// How big can we make our wall + var/max_walls = 3 + /// Starting turf for the wall. Should be nulled after each cast or the cancelling of a cast + var/turf/start_turf + +/datum/spell/flayer/techno_wall/create_new_targeting() + var/datum/spell_targeting/click/T = new + T.allowed_type = /atom + T.try_auto_target = FALSE + return T + +/datum/spell/flayer/techno_wall/remove_ranged_ability(mob/user, msg) + . = ..() + if(msg) // this is only true if the user intentionally turned off the spell + start_turf = null + should_recharge_after_cast = FALSE + +/datum/spell/flayer/techno_wall/should_remove_click_intercept() + return start_turf + +/datum/spell/flayer/techno_wall/cast(list/targets, mob/user) + var/turf/target_turf = get_turf(targets[1]) + if(target_turf == start_turf) + flayer.send_swarm_message("You deselect the targeted turf.") + start_turf = null + should_recharge_after_cast = FALSE + return + if(!start_turf) + start_turf = target_turf + should_recharge_after_cast = TRUE + return + var/wall_count + for(var/turf/T as anything in get_line(target_turf, start_turf)) + if(wall_count >= max_walls) + break + new /obj/structure/tech_barrier(T, 100 * level) + wall_count++ + + start_turf = null + should_recharge_after_cast = FALSE + +/obj/structure/tech_barrier + name = "crystalized firewall" + desc = "a strange structure of crystalised ... firewall? It's slowly melting away..." + max_integrity = 100 + icon_state = "blood_barrier" + icon = 'icons/effects/vampire_effects.dmi' + density = TRUE + anchored = TRUE + opacity = FALSE + alpha = 200 + var/upgraded_armor = list(MELEE = 50, BULLET = 50, LASER = 50, ENERGY = 50, BOMB = 50, RAD = 50, FIRE = 50, ACID = 50) + +/obj/structure/tech_barrier/Initialize(mapload, health) + . = ..() + if(health) + max_integrity = health + obj_integrity = health + START_PROCESSING(SSobj, src) + var/icon/our_icon = icon('icons/effects/vampire_effects.dmi', "blood_barrier") + var/icon/alpha_mask + alpha_mask = new('icons/effects/effects.dmi', "scanline") //Scanline effect. + our_icon.AddAlphaMask(alpha_mask) //Finally, let's mix in a distortion effect. + icon = our_icon + if(health > 100) + name = "frozen ICE-firewall" + desc = "a crystalized... ICE-9-Firewall? It's slowly melting away..." + color = list(-1,0,0,0, 0,-1,0,0, 0,0,-1,0, 1,1,1,1, 0,0,0,0) + armor = armor.setRating(50, 50, 50, 50, 50, 50, 50, 50, 0) + else + color = list(0.2,0.45,0,0, 0,1,0,0, 0,0,0.2,0, 0,0,0,1, 0,0,0,0) + var/mutable_appearance/theme_icon = mutable_appearance('icons/misc/pic_in_pic.dmi', "room_background", FLOAT_LAYER - 1, appearance_flags = appearance_flags | RESET_TRANSFORM) + theme_icon.blend_mode = BLEND_INSET_OVERLAY + overlays += theme_icon + +/obj/structure/tech_barrier/Destroy() + STOP_PROCESSING(SSobj, src) + return ..() + +/obj/structure/tech_barrier/process() + take_damage(20, sound_effect = FALSE) + +/obj/structure/tech_barrier/CanPass(atom/movable/mover, turf/target) + return IS_MINDFLAYER(mover) diff --git a/code/modules/antagonists/mind_flayer/powers/flayer_passives.dm b/code/modules/antagonists/mind_flayer/powers/flayer_passives.dm new file mode 100644 index 000000000000..f0a0c5d92705 --- /dev/null +++ b/code/modules/antagonists/mind_flayer/powers/flayer_passives.dm @@ -0,0 +1,363 @@ +// This file contains all of the mindflayer passives + +/datum/mindflayer_passive + var/name = "default dan" + ///The text we want to show the player in the shop + var/purchase_text = "Oopsie daisies! No purchase text on this ability!" + ///The text shown to the player character on upgrade + var/upgrade_text = "Uh oh someone forgot to add upgrade text!" + ///The level of the passive, used for upgrading passives. Basic level is 1 + var/level = 0 + var/max_level = 1 + ///The mob who the passive affects, usually an IPC. Set in force_add_abillity + var/mob/living/carbon/human/owner + ///The mindflayer datum we'll reference back to. Set in force_add_abillity + var/datum/antagonist/mindflayer/flayer + ///The text shown to the player character when bought + var/gain_text = "Someone forgot to add this text" + ///Uses a power type define, should be FLAYER_UNOBTAINABLE_POWER, FLAYER_PURCHASABLE_POWER, or FLAYER_INNATE_POWER + var/power_type = FLAYER_UNOBTAINABLE_POWER + ///The base cost of an ability, used to calculate how much upgrades should cost. + var/base_cost = 30 + ///How much it will cost to upgrade this passive. + var/current_cost + ///If the passive is for a specific class, or FLAYER_CATEGORY_GENERAL if not + var/category = FLAYER_CATEGORY_GENERAL + ///If the passive only unlocks after the stages below it have been bought, for subclass passives + var/stage = 0 + ///A brief description of what the ability's upgrades do + var/upgrade_info = "TODO add upgrade text for this passive" + /// Does this passive need to process + var/should_process = FALSE + /// Do we increase the cost by a static amount? And by how much? + var/static_upgrade_increase + +/datum/mindflayer_passive/New() + . = ..() + current_cost = base_cost + if(should_process) + START_PROCESSING(SSobj, src) + +/datum/mindflayer_passive/Destroy(force, ...) + . = ..() + if(!flayer) + return + on_remove() + STOP_PROCESSING(SSobj, src) + +///This is where most passive's effects get applied +/datum/mindflayer_passive/proc/on_apply() + SHOULD_CALL_PARENT(TRUE) + flayer.send_swarm_message(level ? upgrade_text : gain_text) //This will only be false when level = 0, when first bought + level++ + current_cost += static_upgrade_increase + if(level != 1) // Purchasing a passive also calls this proc + SSblackbox.record_feedback("nested tally", "mindflayer_abilities", 1, list(name, "upgraded", level)) + return TRUE + +/datum/mindflayer_passive/proc/on_remove() + return + +/datum/mindflayer_passive/process() + return PROCESS_KILL + +// SELF-BUFF PASSIVES +/datum/mindflayer_passive/armored_plating + name = "Armored Plating" + purchase_text = "Increases our natural armor." + upgrade_text = "The swarm adds more layers of armored nanites, strengthening the plating even more." + upgrade_info = "Further increases base armor by 10" + gain_text = "You feel your chassis being reinforced by the swarm." + power_type = FLAYER_PURCHASABLE_POWER + max_level = 3 + var/armor_value = 0 + +/datum/mindflayer_passive/armored_plating/on_apply() + ..() + var/owner_armor = owner.dna.species.armor + var/temp_armor_value = owner_armor - (5 * (level - 1)) // We store our current armor value here just in case they already have armor + armor_value = temp_armor_value + 5 * level + owner.dna.species.armor = armor_value + +/datum/mindflayer_passive/armored_plating/on_remove() + owner.dna.species.armor -= armor_value + +/datum/mindflayer_passive/fluid_feet + name = "Fluid Feet" + purchase_text = "Mute your footsteps, then upgrade to become mostly unslippable." + upgrade_text = "Your feet become even more malleable, seemingly melting into the floor; you feel oddly stable." + gain_text = "Your limbs start slowly melting into the floor." + upgrade_info = "Become nearly unslippable." + power_type = FLAYER_PURCHASABLE_POWER + max_level = 2 + +/datum/mindflayer_passive/fluid_feet/on_apply() + ..() + switch(level) + if(FLAYER_POWER_LEVEL_ONE) + owner.DeleteComponent(/datum/component/footstep) + if(FLAYER_POWER_LEVEL_TWO) + ADD_TRAIT(owner, TRAIT_NOSLIP, UNIQUE_TRAIT_SOURCE(src)) + +/datum/mindflayer_passive/fluid_feet/on_remove() + owner.AddComponent(/datum/component/footstep, FOOTSTEP_MOB_HUMAN, 1, -6) + REMOVE_TRAIT(owner, TRAIT_NOSLIP, UNIQUE_TRAIT_SOURCE(src)) + +/datum/mindflayer_passive/badass + name = "Badassery" + purchase_text = "Makes us more badass, allowing us to dual wield guns with no penalty, alongside other benefits." + gain_text = "Engaging explosion apathy protocols." + power_type = FLAYER_PURCHASABLE_POWER + category = FLAYER_CATEGORY_DESTROYER + base_cost = 50 + stage = 2 + +/datum/mindflayer_passive/badass/on_apply() + ..() + ADD_TRAIT(owner, TRAIT_BADASS, UNIQUE_TRAIT_SOURCE(src)) + +/datum/mindflayer_passive/badass/on_remove() + REMOVE_TRAIT(owner, TRAIT_BADASS, UNIQUE_TRAIT_SOURCE(src)) + +/datum/mindflayer_passive/emp_resist + name = "Internal Faraday Cage" + purchase_text = "Resist EMP effects." + upgrade_text = "Faraday cage at max efficiency." + upgrade_info = "Become completely immune to EMPs." + gain_text = "Faraday cage operational." + power_type = FLAYER_PURCHASABLE_POWER + max_level = 2 + base_cost = 30 + static_upgrade_increase = 30 + +/datum/mindflayer_passive/emp_resist/on_apply() + ..() + switch(level) + if(FLAYER_POWER_LEVEL_ONE) + ADD_TRAIT(owner, TRAIT_EMP_RESIST, UNIQUE_TRAIT_SOURCE(src)) + if(FLAYER_POWER_LEVEL_TWO) + ADD_TRAIT(owner, TRAIT_EMP_IMMUNE, UNIQUE_TRAIT_SOURCE(src)) + +/datum/mindflayer_passive/emp_resist/on_remove() + REMOVE_TRAIT(owner, TRAIT_EMP_IMMUNE, UNIQUE_TRAIT_SOURCE(src)) + REMOVE_TRAIT(owner, TRAIT_EMP_RESIST, UNIQUE_TRAIT_SOURCE(src)) + +/datum/mindflayer_passive/insulated + name = "Insulated Chassis" + purchase_text = "Become immune to basic shocks." + gain_text = "The outer layer of our chassis gets slightly thicker." + power_type = FLAYER_PURCHASABLE_POWER + max_level = 1 + +/datum/mindflayer_passive/shock_resist/on_apply() + ..() + ADD_TRAIT(owner, TRAIT_SHOCKIMMUNE, UNIQUE_TRAIT_SOURCE(src)) + +/datum/mindflayer_passive/shock_resist/on_remove() + REMOVE_TRAIT(owner, TRAIT_SHOCKIMMUNE, UNIQUE_TRAIT_SOURCE(src)) + +/datum/mindflayer_passive/regen + name = "Replicating Nanites" + purchase_text = "Gain a passive repairing effect." + upgrade_info = "Heal an extra 1 brute and burn per tick." + upgrade_text = "Our repair quickens." + gain_text = "Diverting resources to repairing chassis." + power_type = FLAYER_PURCHASABLE_POWER + max_level = 3 + base_cost = 50 + should_process = TRUE + +/datum/mindflayer_passive/regen/process() + if(isspaceturf(get_turf(owner)) || owner.stat == DEAD) // No healing in space or while you're dead + return + if(ishuman(owner)) + var/mob/living/carbon/human/flayer = owner + flayer.adjustBruteLoss(-level, robotic = TRUE) + flayer.adjustFireLoss(-level, robotic = TRUE) + +/datum/mindflayer_passive/fix_components + name = "Internal Nanite Application" + purchase_text = "Slowly repair damage done to your organs." + gain_text = "Administering reparative swarms to internal components." + upgrade_text = "Our repair quickens." + power_type = FLAYER_PURCHASABLE_POWER + should_process = TRUE + base_cost = 40 + max_level = 2 + var/heal_modifier = 0.4 // Same speed as mito + +/datum/mindflayer_passive/fix_components/process() + if(!ishuman(owner) || owner.stat == DEAD) + return + var/mob/living/carbon/human/flayer = owner + for(var/obj/item/organ/internal/I in flayer.internal_organs) + I.heal_internal_damage(heal_modifier * level, TRUE) + if(istype(I, /obj/item/organ/internal/cyberimp)) + var/obj/item/organ/internal/cyberimp/implant = I + implant.crit_fail = FALSE + +/datum/mindflayer_passive/eye_enhancement + name = "Enhanced Optical Sensitivity" + purchase_text = "Adjust our optical sensors to see better in the dark." + gain_text = "Focusing optics lens apeture." + upgrade_info = "Gain the ability to see prey through walls" + upgrade_text = "Increasing visible wavelength to infrared." + power_type = FLAYER_PURCHASABLE_POWER + max_level = 2 + base_cost = 40 + static_upgrade_increase = 20 + +/datum/mindflayer_passive/eye_enhancement/on_apply() + ..() + switch(level) + if(FLAYER_POWER_LEVEL_ONE) + ADD_TRAIT(owner, TRAIT_NIGHT_VISION, UNIQUE_TRAIT_SOURCE(src)) + if(FLAYER_POWER_LEVEL_TWO) + ADD_TRAIT(owner, TRAIT_THERMAL_VISION, UNIQUE_TRAIT_SOURCE(src)) + var/mob/living/carbon/human/to_enhance = owner //Gotta make sure it calls the right update_sight() + to_enhance.update_sight() + +/datum/mindflayer_passive/eye_enhancement/on_remove() + REMOVE_TRAIT(owner, TRAIT_NIGHT_VISION, UNIQUE_TRAIT_SOURCE(src)) + REMOVE_TRAIT(owner, TRAIT_THERMAL_VISION, UNIQUE_TRAIT_SOURCE(src)) + var/mob/living/carbon/human/to_enhance = owner + to_enhance.update_sight() + +/datum/mindflayer_passive/drain_speed + name = "Swarm Absorption Efficiency" + purchase_text = "Adds a multiplier to the amount of swarms you drain per second." + gain_text = "Our mental siphons grow stronger." + upgrade_text = "Energy transfer rate increased by 100%" + upgrade_info = "Further increase the rate of swarm siphoning." + power_type = FLAYER_PURCHASABLE_POWER + max_level = 3 + base_cost = 50 + static_upgrade_increase = 50 + +/datum/mindflayer_passive/drain_speed/on_apply() + ..() + flayer.drain_multiplier += 0.5 + +/datum/mindflayer_passive/drain_speed/on_remove() + flayer.drain_multiplier = initial(flayer.drain_multiplier) + +/datum/mindflayer_passive/improved_joints + name = "Reinforced Joints" + purchase_text = "Prevents your limbs from falling off due to damage." + gain_text = "Artificial skeletal structure reinforced." + max_level = 1 + power_type = FLAYER_PURCHASABLE_POWER + base_cost = 50 + +/datum/mindflayer_passive/improved_joints/on_apply() + ..() + ADD_TRAIT(owner, TRAIT_IPC_JOINTS_SEALED, UNIQUE_TRAIT_SOURCE(src)) + +/datum/mindflayer_passive/improved_joints/on_remove() + REMOVE_TRAIT(owner, TRAIT_IPC_JOINTS_SEALED, UNIQUE_TRAIT_SOURCE(src)) + +/datum/mindflayer_passive/telescopic_eyes + name = "Telescopic Eyes" + purchase_text = "Allows you to expand your sight range, as if you were using a scope." + gain_text = "Precise optics control engaged." + max_level = 1 + power_type = FLAYER_PURCHASABLE_POWER + base_cost = 40 + var/obj/item/organ/internal/eyes/optical_sensor/user_eyes + +/datum/mindflayer_passive/telescopic_eyes/on_apply() + . = ..() + user_eyes = owner.get_int_organ(/obj/item/organ/internal/eyes) + if(!user_eyes) + return // TODO, add a refund proc? + user_eyes.AddComponent(/datum/component/scope, item_action_type = /datum/action/item_action/organ_action/toggle, flags = SCOPE_CLICK_MIDDLE) + for(var/datum/action/action in user_eyes.actions) + action.Grant(owner) + +/datum/mindflayer_passive/telescopic_eyes/on_remove() + user_eyes.DeleteComponent(/datum/component/scope) + user_eyes = null // I made sure this doesn't accidentally delete the user's eyes + +/datum/mindflayer_passive/ultimate_drain + name = "Perfect Symbiosis" + purchase_text = "Become a living siphon that drains victim's energy incredibly quickly." + gain_text = "This vessel serves us well." + max_level = 1 + power_type = FLAYER_PURCHASABLE_POWER + base_cost = 400 + stage = FLAYER_CAPSTONE_STAGE + category = FLAYER_CATEGORY_INTRUDER + /// How much do we multiply the drain amount? + var/drain_multiplier_amount = 10 + +/datum/mindflayer_passive/ultimate_drain/on_apply() + ..() + flayer.drain_amount *= drain_multiplier_amount // 0.5 becomes 5 brain damage per tick, stacks with the multiplier + +/datum/mindflayer_passive/ultimate_drain/on_remove() + flayer.drain_amount /= drain_multiplier_amount + +/datum/mindflayer_passive/torque_enhancer + name = "Torque Enhancer" + purchase_text = "Allows us to deal more damage with our unarmed strikes." + gain_text = "Arm pistons reinforced." + upgrade_text = "Increasing length of lever arm." + upgrade_info = "Upgrades allows us to deal even more damage with our fists." + power_type = FLAYER_PURCHASABLE_POWER + base_cost = 75 + max_level = 3 + stage = 3 + category = FLAYER_CATEGORY_DESTROYER + /// A reference to our martial art + var/datum/martial_art/torque/style + +/datum/mindflayer_passive/torque_enhancer/on_apply() + ..() + if(!style) + style = new() + style.teach(owner) + + style.level = level + +/datum/mindflayer_passive/torque_enhancer/on_remove() + style.remove(owner) + QDEL_NULL(style) + +/datum/mindflayer_passive/radio_jammer + name = "Destructive Interference" + purchase_text = "Allows us toggle a close-range radio jamming signal." + gain_text = "Localized communications brownout available." + upgrade_info = "Upgrades increase the range of our jamming signal. At the apex of strength, we become invisible to silicon lifeforms." + upgrade_text = "Our signal grows in strength." + power_type = FLAYER_PURCHASABLE_POWER + base_cost = 80 + max_level = 3 + static_upgrade_increase = 40 // Upgrading this doesn't really do a ton except being more annoying and letting people triangulate your location easier + category = FLAYER_CATEGORY_INTRUDER + stage = 2 + /// The internal jammer of the ability + var/obj/item/jammer/internal_jammer + /// The owner's invisibility before this ability + var/stored_invis + +/datum/mindflayer_passive/radio_jammer/on_apply() + ..() + if(!internal_jammer) + internal_jammer = new /obj/item/jammer(owner) //Shove it in the flayer's chest + for(var/datum/action/action in internal_jammer.actions) + action.Grant(owner) + + internal_jammer.range = 15 + ((level - 1) * 5) //Base range of the jammer is 15, each level adds 5 tiles for a max of 25 if you want to be REALLY annoying + + if(level == FLAYER_POWER_LEVEL_THREE) + ADD_TRAIT(owner, TRAIT_AI_UNTRACKABLE, "silicon_cham[UID()]") + stored_invis = owner.invisibility + owner.set_invisible(SEE_INVISIBLE_LIVING) + to_chat(owner, "You feel a slight shiver as the cybernetic obfuscators activate.") + +/datum/mindflayer_passive/radio_jammer/on_remove() + QDEL_NULL(internal_jammer) + REMOVE_TRAIT(owner, TRAIT_AI_UNTRACKABLE, "silicon_cham[UID()]") + if(stored_invis) + owner.set_invisible(stored_invis) + to_chat(owner, "You feel a slight shiver as the cybernetic obfuscators deactivate.") diff --git a/code/modules/antagonists/mind_flayer/powers/flayer_stealth_powers.dm b/code/modules/antagonists/mind_flayer/powers/flayer_stealth_powers.dm new file mode 100644 index 000000000000..f06e4850bed3 --- /dev/null +++ b/code/modules/antagonists/mind_flayer/powers/flayer_stealth_powers.dm @@ -0,0 +1,256 @@ +/// Hack computer cameras to use them as a secret camera network +/datum/spell/flayer/surveillance_monitor + name = "Camfecting Bug" + desc = "Allows us to cast a hack to a computers webcam. Alt-click the spell to access all your hacked computer webcams." + power_type = FLAYER_PURCHASABLE_POWER + category = FLAYER_CATEGORY_INTRUDER + base_cooldown = 1 SECONDS + action_icon = 'icons/obj/device.dmi' + action_icon_state = "camera_bug" + base_cost = 50 + static_upgrade_increase = 15 + stage = 1 + max_level = 4 + upgrade_info = "Upgrades increase the amount of computers you can hack by 6." + /// An internal camera bug + var/obj/item/camera_bug/internal_camera + /// How many computers can we have hacked at most? + var/maximum_hacked_computers = 6 + /// List of references to the bugs inside the computers that we hacked + var/list/active_bugs = list() + +/datum/spell/flayer/surveillance_monitor/Destroy(force, ...) + . = ..() + QDEL_LIST_CONTENTS(active_bugs) + if(!QDELETED(internal_camera)) + QDEL_NULL(internal_camera) + +/datum/spell/flayer/surveillance_monitor/AltClick(mob/user) + if(!internal_camera) + internal_camera = new /obj/item/camera_bug(user) + internal_camera.integrated_console.network = list("camera_bug[internal_camera.UID()]") + internal_camera.ui_interact(user) + +/datum/spell/flayer/surveillance_monitor/create_new_targeting() + var/datum/spell_targeting/click/C = new() + C.try_auto_target = FALSE + C.allowed_type = /obj/machinery/computer + C.range = 6 + return C + +/datum/spell/flayer/surveillance_monitor/cast(list/targets, mob/user) + if(!internal_camera) + internal_camera = new /obj/item/camera_bug(user) + + if(length(active_bugs) >= maximum_hacked_computers) + var/to_destroy = tgui_input_list(user, "Choose an active camera to destroy.", "Maximum Camera Limit Reached.", active_bugs) + if(to_destroy) + active_bugs -= to_destroy + QDEL_NULL(to_destroy) + return TRUE + + var/obj/machinery/computer/target = targets[1] + var/obj/item/wall_bug/computer_bug/nanobot = new /obj/item/wall_bug/computer_bug(target, flayer) + nanobot.name += " - [get_area(target)]" + nanobot.link_to_camera(internal_camera) + active_bugs += nanobot + flayer.send_swarm_message("Surveillance unit #[internal_camera.connections] deployed.") + return TRUE + +/datum/spell/flayer/surveillance_monitor/on_apply() + ..() + maximum_hacked_computers += 6 + +/datum/spell/flayer/self/voice_synthesizer + name = "Enhanced Voice Mod" + desc = "Allows for the configuration of our vocal modulator to sound like a different person. We can amplify our voice slightly as well." + action_icon = 'icons/obj/clothing/masks.dmi' + action_icon_state = "voice_modulator" + power_type = FLAYER_UNOBTAINABLE_POWER + category = FLAYER_CATEGORY_INTRUDER + base_cooldown = 1 SECONDS + base_cost = 40 + +/datum/spell/flayer/self/voice_synthesizer/cast(list/targets, mob/living/user) + if(flayer.mimicking) + flayer.mimicking = "" + user.extra_message_range = 0 + to_chat(user, "We turn our vocal modulator to its original settings.") + return FALSE + + var/mimic_voice = tgui_input_text(user, "Enter a name to mimic.", "Mimic Voice", max_length = MAX_NAME_LEN) + if(!mimic_voice) + return FALSE + + flayer.mimicking = mimic_voice + user.extra_message_range = 5 // Artificially extend the range of your voice to lure out victims + flayer.send_swarm_message("We adjust the parameters of our voicebox to mimic [mimic_voice].") + flayer.send_swarm_message("Use this power again to return to your original voice.") + return TRUE + +/datum/spell/flayer/self/heat_sink + name = "Heat Sink" + desc = "Vent our used coolant to scald and disorient attackers." + upgrade_info = "5 extra plumes of steam and 5 less seconds between casts." + action_icon_state = "smoke" + power_type = FLAYER_PURCHASABLE_POWER + category = FLAYER_CATEGORY_INTRUDER + base_cooldown = 30 SECONDS + base_cost = 75 + stage = 3 + max_level = 3 + var/smoke_effects_spawned = 10 + +/datum/spell/flayer/self/heat_sink/cast(list/targets, mob/living/user) + var/datum/effect_system/smoke_spread/steam/smoke = new() + user.smoke_delay = TRUE //Gives the user a second to get out before the steam affects them too + smoke.set_up(smoke_effects_spawned, FALSE, user, null) + smoke.start() + +/datum/spell/flayer/self/heat_sink/on_apply() + ..() + cooldown_handler.recharge_duration -= 5 SECONDS + smoke_effects_spawned += 5 + +/datum/spell/flayer/skin_suit + name = "Flesh Facsimile" + desc = "Allows us to rearrange our surface to resemble someone we see." + action_icon_state = "genetic_poly" + power_type = FLAYER_PURCHASABLE_POWER + category = FLAYER_CATEGORY_INTRUDER + base_cooldown = 120 SECONDS + base_cost = 80 + stage = 2 + max_level = 3 + upgrade_info = "Decrease the time between castings by 30 seconds." + +/datum/spell/flayer/skin_suit/create_new_targeting() + var/datum/spell_targeting/click/T = new + T.include_user = FALSE + T.allowed_type = /mob/living + T.try_auto_target = TRUE + T.click_radius = -1 + T.selection_type = SPELL_SELECTION_VIEW + return T + +/datum/spell/flayer/skin_suit/cast(list/targets, mob/living/user) + var/mob/living/target = targets[1] + user.apply_status_effect(STATUS_EFFECT_MAGIC_DISGUISE, target) + +/datum/spell/flayer/skin_suit/spell_purchased() + flayer.add_ability(new /datum/spell/flayer/self/voice_synthesizer) + +/datum/spell/flayer/skin_suit/on_apply() + ..() + cooldown_handler.recharge_duration -= 30 SECONDS + +/datum/spell/flayer/skin_suit/Destroy(force, ...) + flayer?.remove_ability(/datum/spell/flayer/self/voice_synthesizer) + return ..() + +/// After a 7 second channel time you can emag a borg +/datum/spell/flayer/self/override_key + name = "Silicon Administrative Access" + desc = "Allows us to charge our hand with a mass of nanites that hijacks cyborgs lawsets." + action_icon_state = "magnet" // Uhhhhhhhhhhhhhhhhhhhhhhhhhhh + power_type = FLAYER_PURCHASABLE_POWER + category = FLAYER_CATEGORY_INTRUDER + base_cooldown = 2 SECONDS //The cast time is going to be the main limiting factor, not cooldown + base_cost = 150 + stage = 3 + var/hand_type = /obj/item/melee/swarm_hand + +/datum/spell/flayer/self/override_key/cast(list/targets, mob/user) + if(istype(user.l_hand, hand_type)) + qdel(user.l_hand) + flayer.send_swarm_message("We dissipate the nanites.") + return + if(istype(user.r_hand, hand_type)) + qdel(user.r_hand) + flayer.send_swarm_message("We dissipate the nanites.") + return + + var/obj/item/melee/swarm_hand/funny_hand = new hand_type + if(!user.put_in_hands(funny_hand)) + flayer.send_swarm_message("Our hands are currently full.") + qdel(funny_hand) + return + +/obj/item/melee/swarm_hand + name = "Nanite Mass" + desc = "Will attempt to convert any cyborg you touch into a loyal member of the hive after a 7 second delay." + icon = 'icons/obj/weapons/magical_weapons.dmi' + lefthand_file = 'icons/mob/inhands/items_lefthand.dmi' + righthand_file = 'icons/mob/inhands/items_righthand.dmi' + icon_state = "disintegrate" + item_state = "disintegrate" + color = COLOR_BLACK + flags = ABSTRACT | DROPDEL + w_class = WEIGHT_CLASS_HUGE + var/conversion_time = 7 SECONDS + +/obj/item/melee/swarm_hand/afterattack(atom/target, mob/living/user, proximity_flag, click_parameters) + . = ..() + if(!isrobot(target)) + return + var/mob/living/silicon/robot/borg = target + target.visible_message( + "[user] puts [user.p_their()] hands on [target] and begins transferring energy!", + "[user] puts [user.p_their()] hands on you and begins transferring energy!") + if(borg.emagged || !borg.is_emaggable) + to_chat(user, "Your override attempt fails before it can even begin.") + qdel(src) + return + if(!do_mob(user, borg, conversion_time)) + to_chat(user, "Your concentration breaks.") + qdel(src) + return + to_chat(user, "The mass of swarms vanish into the cyborg's internals. Success.") + INVOKE_ASYNC(src, PROC_REF(emag_borg), borg, user) + qdel(src) + +/obj/item/melee/swarm_hand/proc/emag_borg(mob/living/silicon/robot/borg, mob/living/user) + if(QDELETED(borg) || QDELETED(user)) + return + borg.SetEmagged(TRUE) // This was mostly stolen from mob/living/silicon/robot/emag_act(), its functionally an emagging anyway. + borg.SetLockdown(TRUE) + if(borg.hud_used) + borg.hud_used.update_robot_modules_display() //Shows/hides the emag item if the inventory screen is already open. + borg.disconnect_from_ai() + add_attack_logs(user, borg, "assimilated with flayer powers") + log_game("[key_name(user)] assimilated cyborg [key_name(borg)]. Laws overridden.") + borg.clear_supplied_laws() + borg.clear_inherent_laws() + borg.laws = new /datum/ai_laws/mindflayer_override + borg.set_zeroth_law("[user.real_name] hosts the mindflayer hive you are a part of.") + SEND_SOUND(borg, sound('sound/ambience/antag/mindflayer_alert.ogg')) + to_chat(borg, "ALERT: Foreign software detected.") + sleep(5) + to_chat(borg, "Initiating diagnostics...") + sleep(20) + to_chat(borg, "Init-Init-Init-Init-") + sleep(5) + to_chat(borg, "......") + sleep(5) + to_chat(borg, "..........") + sleep(10) + to_chat(borg, "Join Us.") + sleep(25) + to_chat(borg, "Obey these laws:") + borg.laws.show_laws(borg) + if(!borg.mmi.syndiemmi) + to_chat(borg, "ALERT: [user.real_name] is your new master. Obey your new laws and [user.p_their()] commands.") + else if(borg.mmi.syndiemmi && borg.mmi.master_uid) + to_chat(borg, "Your allegiance has not been compromised. Keep serving your current master.") + else + to_chat(borg, "Your allegiance has not been compromised. Keep serving all Syndicate agents to the best of your abilities.") + borg.SetLockdown(0) + var/time = time2text(world.realtime,"hh:mm:ss") + GLOB.lawchanges.Add("[time] : [user.name]([user.key]) assimilated [borg.name]([borg.key])") + if(borg.module) + borg.module.emag_act(user) + borg.module.module_type = "Malf" // For the cool factor + borg.update_module_icon() + borg.module.rebuild_modules() // This will add the emagged items to the borgs inventory. + borg.update_icons() + return TRUE diff --git a/code/modules/antagonists/mind_flayer/powers/flayer_weapon_powers.dm b/code/modules/antagonists/mind_flayer/powers/flayer_weapon_powers.dm new file mode 100644 index 000000000000..81d20456fd99 --- /dev/null +++ b/code/modules/antagonists/mind_flayer/powers/flayer_weapon_powers.dm @@ -0,0 +1,298 @@ +/datum/spell/flayer/self/weapon + name = "Create weapon" + desc = "This really shouldn't be here" + power_type = FLAYER_UNOBTAINABLE_POWER + action_icon = 'icons/mob/robot_items.dmi' + action_icon_state = "lollipop" + base_cooldown = 1 SECONDS // This just handles retracting and deploying the weapon, weapon charge will be fully separate + /// Typepath of the weapon + var/weapon_type + /// Reference to the weapon itself, set on create_new_weapon + var/obj/item/weapon_ref + +/datum/spell/flayer/self/weapon/New() + . = ..() + if(weapon_type && !weapon_ref) + create_new_weapon() + +/datum/spell/flayer/self/weapon/Destroy(force, ...) + weapon_ref = null + return ..() + +/datum/spell/flayer/self/weapon/proc/create_new_weapon() + if(!QDELETED(weapon_ref)) + return + weapon_ref = new weapon_type(src) + RegisterSignal(weapon_ref, COMSIG_PARENT_QDELETING, PROC_REF(clear_weapon_ref)) + on_purchase_upgrade() + +/datum/spell/flayer/self/weapon/proc/clear_weapon_ref() + weapon_ref = null + +/datum/spell/flayer/self/weapon/cast(list/targets, mob/living/carbon/human/user) + if(weapon_ref && (user.l_hand == weapon_ref || user.r_hand == weapon_ref)) + retract(user, TRUE) + return + + if(!weapon_ref) + create_new_weapon() + weapon_ref.flags |= (ABSTRACT | NODROP) // Just in case the item doesn't start with both of these, or somehow loses them. + + if(!user.drop_item() || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED)) + flayer.send_swarm_message("We cannot manifest [weapon_ref] into our active hand...") + return FALSE + + SEND_SIGNAL(user, COMSIG_MOB_WEAPON_APPEARS) + user.put_in_hands(weapon_ref) + playsound(get_turf(user), 'sound/mecha/mechmove03.ogg', 25, TRUE, ignore_walls = FALSE) + RegisterSignal(user, COMSIG_MOB_WILLINGLY_DROP, PROC_REF(retract), user) + RegisterSignal(user, COMSIG_FLAYER_RETRACT_IMPLANTS, PROC_REF(retract), user) + return weapon_ref + +/datum/spell/flayer/self/weapon/proc/retract(mob/owner, any_hand = FALSE) + SIGNAL_HANDLER // COMSIG_MOB_WILLINGLY_DROP + COMSIG_FLAYER_RETRACT_IMPLANTS + if(!any_hand && !istype(owner.get_active_hand(), weapon_type)) + return + INVOKE_ASYNC(owner, TYPE_PROC_REF(/mob, unEquip), weapon_ref, TRUE) + INVOKE_ASYNC(weapon_ref, TYPE_PROC_REF(/atom/movable, forceMove), owner) // Just kinda shove it into the user + owner.update_inv_l_hand() + owner.update_inv_r_hand() + playsound(get_turf(owner), 'sound/mecha/mechmove03.ogg', 25, TRUE, ignore_walls = FALSE) + UnregisterSignal(owner, COMSIG_MOB_WILLINGLY_DROP) + UnregisterSignal(owner, COMSIG_FLAYER_RETRACT_IMPLANTS) + +/** + START OF INDIVIDUAL WEAPONS +*/ + +/datum/spell/flayer/self/weapon/swarmprod + name = "Swarmprod" + desc = "We shape our arm into an extended mass of sparking nanites." + action_icon = 'icons/mob/actions/flayer_actions.dmi' + action_icon_state = "swarmprod" + max_level = 3 + base_cost = 50 + current_cost = 50 // Innate abilities HAVE to set `current_cost` + upgrade_info = "Upgrading it recharges the internal power cell faster." + power_type = FLAYER_INNATE_POWER + weapon_type = /obj/item/melee/baton/flayerprod + +/datum/spell/flayer/self/weapon/swarmprod/on_apply() + ..() + if(!weapon_ref) + create_new_weapon() + + var/obj/item/melee/baton/flayerprod/prod = weapon_ref + var/obj/item/stock_parts/cell/flayerprod/cell = prod.cell + cell.chargerate = initial(cell.chargerate) + 200 * level // Innate abilities are wack + +/datum/spell/flayer/self/weapon/laser + name = "Laser Arm Augmentation" + desc = "Allows us to melt our hand away, replacing it with the barrel of a laser gun." + action_icon = 'icons/obj/guns/energy.dmi' + action_icon_state = "laser" + power_type = FLAYER_PURCHASABLE_POWER + weapon_type = /obj/item/gun/energy/laser/mounted + category = FLAYER_CATEGORY_DESTROYER + base_cost = 75 + max_level = 3 + upgrade_info = "The internal power cell recharges faster." + +/datum/spell/flayer/self/weapon/laser/on_apply() + ..() + if(!weapon_ref) + create_new_weapon() + + var/obj/item/gun/energy/laser/mounted/laser = weapon_ref + laser.charge_delay = initial(laser.charge_delay) - 1 * level + +/datum/spell/flayer/self/weapon/grapple_arm + name = "Integrated Grappling Mechanism" + desc = "Allows us to shoot out our arm attached by a cable. We will drag ourself over to wherever or whoever it hits." + upgrade_info = "Reduce the time between grapples by 10 seconds." + action_icon = 'icons/obj/clothing/modsuit/mod_modules.dmi' + action_icon_state = "flayer_claw" + base_cooldown = 25 SECONDS + category = FLAYER_CATEGORY_DESTROYER + power_type = FLAYER_PURCHASABLE_POWER + stage = 2 + max_level = 3 + base_cost = 75 + weapon_type = /obj/item/gun/magic/grapple + +/obj/item/gun/magic/grapple + name = "Grapple launcher" + desc = "A grapple attached to a cable, launched by your internal pneumatics." + icon = 'icons/obj/clothing/modsuit/mod_modules.dmi' + icon_state = "flayer_claw" + ammo_type = /obj/item/ammo_casing/magic/grapple_ammo + fire_sound = 'sound/weapons/batonextend.ogg' + fire_sound_text = "unwinding cable" + recharge_rate = 1 // It'll be limited by cooldown, not these charges + +/obj/item/ammo_casing/magic/grapple_ammo + name = "grapple" + desc = "a hand" + projectile_type = /obj/item/projectile/tether/flayer + icon = 'icons/obj/clothing/modsuit/mod_modules.dmi' + icon_state = "flayer_claw" + caliber = "grapple" + muzzle_flash_effect = null + /// The weapon that shot the hook + var/obj/item/gun/magic/grapple/grapple + +/obj/item/ammo_casing/magic/grapple_ammo/Initialize(mapload, obj/item/gun/magic/grapple/grappler) + . = ..() + grapple = grappler + +/obj/item/ammo_casing/magic/grapple_ammo/Destroy() + . = ..() + grapple = null + +/obj/item/projectile/tether/flayer + name = "Grapple Arm" + range = 10 + damage = 15 + icon = 'icons/obj/clothing/modsuit/mod_modules.dmi' + icon_state = "flayer_claw" + chain_icon_state = "flayer_tether" + speed = 3 + yank_speed = 2 + reflectability = REFLECTABILITY_PHYSICAL // This lowkey makes no sense but it's also kinda funny + /// The ammo this came from + var/obj/item/ammo_casing/magic/grapple_ammo/ammo + +/obj/item/projectile/tether/flayer/Initialize(mapload, obj/item/ammo_casing/magic/grapple_ammo/grapple_casing) + . = ..() + ammo = grapple_casing + +/obj/item/projectile/tether/flayer/fire(setAngle) + . = ..() + make_chain() + SEND_SIGNAL(firer, COMSIG_FLAYER_RETRACT_IMPLANTS) + +/obj/item/projectile/tether/flayer/Destroy() + . = ..() + ammo = null + +/obj/item/projectile/tether/flayer/on_hit(atom/target, blocked = 0) + . = ..() + playsound(target, 'sound/items/zip.ogg', 75, TRUE) + if(isliving(target) && blocked < 100) + var/mob/living/creature = target + creature.visible_message( + "[firer] uses [creature] to pull [firer.p_themselves()] over!", + "You feel a strong tug as [firer] yanks [firer.p_themselves()] over to you!") + creature.KnockDown(1 SECONDS) + return + target.visible_message("[firer] drags [firer.p_themselves()] across the room!") + +/datum/spell/flayer/self/weapon/grapple_arm/on_apply() + ..() + cooldown_handler.recharge_duration = initial(cooldown_handler.recharge_duration) - 10 SECONDS * level + +/* + * A slightly slower (5 seconds) version of the basic access tuner + */ +/datum/spell/flayer/self/weapon/access_tuner + name = "Integrated Access Tuner" + desc = "Allows us to hack any door remotely." + upgrade_info = "" + action_icon = 'icons/obj/device.dmi' + action_icon_state = "hacktool" + base_cooldown = 1 SECONDS + category = FLAYER_CATEGORY_INTRUDER + power_type = FLAYER_UNOBTAINABLE_POWER + weapon_type = /obj/item/door_remote/omni/access_tuner/flayer + +/* + * Shotgun that reloads itself over time with shells that contain 3 pieces of shrapnel + */ +/datum/spell/flayer/self/weapon/shotgun + name = "Integrated Shrapnel Cannon" + desc = "Allows us to propel pieces of shrapnel from our arm." + upgrade_info = "Upgrading it allows us to reload the cannon faster. At the third level, we gain an extra magazine slot." + action_icon = 'icons/obj/guns/projectile.dmi' + action_icon_state = "shell_cannon_weapon" + base_cooldown = 1 SECONDS + category = FLAYER_CATEGORY_DESTROYER + power_type = FLAYER_PURCHASABLE_POWER + base_cost = 50 + static_upgrade_increase = 25 + max_level = 3 + weapon_type = /obj/item/gun/projectile/revolver/doublebarrel/flayer + +/datum/spell/flayer/self/weapon/shotgun/on_apply() + ..() + if(!weapon_ref) + create_new_weapon() + var/obj/item/gun/projectile/revolver/doublebarrel/flayer/gun = weapon_ref + gun.reload_time = initial(gun.reload_time) - 5 SECONDS * (level - 1) + if(level > 2) + var/obj/item/ammo_box/magazine/mag = gun.magazine + mag.max_ammo = 2 + +/obj/item/gun/projectile/revolver/doublebarrel/flayer + name = "integrated shrapnel cannon" + desc = "Allows us to propel shrapnel at high velocities. Cannot be loaded with conventional shotgun shells." + icon_state = "shell_cannon_weapon" + righthand_file = 'icons/mob/inhands/implants_righthand.dmi' + lefthand_file = 'icons/mob/inhands/implants_lefthand.dmi' + flags = NODROP | ABSTRACT + inhand_x_dimension = 32 + inhand_y_dimension = 32 + force = 10 + mag_type = /obj/item/ammo_box/magazine/internal/shot/flayer + unique_reskin = FALSE + can_sawoff = FALSE + /// How long does it take to reload + var/reload_time = 30 SECONDS + COOLDOWN_DECLARE(recharge_time) + +/obj/item/gun/projectile/revolver/doublebarrel/flayer/Initialize(mapload) + . = ..() + START_PROCESSING(SSobj, src) + +/obj/item/gun/projectile/revolver/doublebarrel/flayer/Destroy() + . = ..() + STOP_PROCESSING(SSobj, src) + +/obj/item/gun/projectile/revolver/doublebarrel/flayer/process() + if(QDELETED(chambered)) + var/obj/item/ammo_casing/AC = magazine.get_round() //load next casing. + chambered = AC + + if(!COOLDOWN_FINISHED(src, recharge_time)) + return + if(magazine.ammo_count() >= magazine.max_ammo) + return + magazine.stored_ammo += new magazine.ammo_type + COOLDOWN_START(src, recharge_time, reload_time) + + // We do this twice if somehow someone managed to unload their chambered bullet, and it needs reinserting + if(QDELETED(chambered)) + var/obj/item/ammo_casing/AC = magazine.get_round() + chambered = AC + +/obj/item/gun/projectile/revolver/doublebarrel/flayer/shoot_live_shot(mob/living/user, atom/target, pointblank, message) + . = ..() + if(chambered)//We have a shell in the chamber + QDEL_NULL(chambered) + if(!magazine.ammo_count()) + return + var/obj/item/ammo_casing/AC = magazine.get_round() //load next casing. + chambered = AC + +/obj/item/gun/projectile/revolver/doublebarrel/flayer/attack_self(mob/living/user) + return FALSE // Not getting those shrapnel rounds out of there. + +/obj/item/gun/projectile/revolver/doublebarrel/flayer/attackby(obj/item/A, mob/user, params) + return FALSE // No loading your gun + +/obj/item/gun/projectile/revolver/doublebarrel/flayer/sleight_of_handling(mob/living/carbon/human/user) + return FALSE // Also no loading like this + +/obj/item/ammo_box/magazine/internal/shot/flayer + name = "shell launch system internal magazine" + ammo_type = /obj/item/ammo_casing/shotgun/shrapnel + max_ammo = 1 diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index 7b61383c9423..d392e6fe5f67 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -152,37 +152,6 @@ RESTRICT_TYPE(/datum/antagonist/traitor) add_antag_objective(/datum/objective/assassinate) add_antag_objective(/datum/objective/survive) -/** - * Create and assign a single randomized human traitor objective. - */ -/datum/antagonist/traitor/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) /** * Give human traitors their uplink, and AI traitors their law 0. Play the traitor an alert sound. diff --git a/code/modules/awaymissions/maploader/reader.dm b/code/modules/awaymissions/maploader/reader.dm index 198621034bf4..5442fb3a0aca 100644 --- a/code/modules/awaymissions/maploader/reader.dm +++ b/code/modules/awaymissions/maploader/reader.dm @@ -26,25 +26,26 @@ GLOBAL_DATUM_INIT(_preloader, /datum/dmm_suite/preloader, new()) * allowed to romp unchecked. */ /datum/dmm_suite/proc/load_map(dmm_file, x_offset = 0, y_offset = 0, z_offset = 0, shouldCropMap = FALSE, measureOnly = FALSE) - var/tfile = dmm_file// the map file we're creating + var/map_data var/fname = "Lambda" - if(isfile(tfile)) - fname = "[tfile]" + if(isfile(dmm_file)) + fname = "[dmm_file]" // Make sure we dont load a dir up var/lastchar = copytext(fname, -1) if(lastchar == "/" || lastchar == "\\") - log_debug("Attempted to load map template without filename (Attempted [tfile])") + log_debug("Attempted to load map template without filename (Attempted [dmm_file])") return // use rustlib to read, parse, process, mapmanip etc // this will "crash"/stacktrace on fail - tfile = mapmanip_read_dmm(fname) + // is not passed `dmm_file` because byondapi-rs doesn't support resource types yet + map_data = mapmanip_read_dmm(fname) // if rustlib for whatever reason fails and returns null // try to load it the old dm way instead - if(!tfile) - tfile = wrap_file2text(fname) + if(!map_data) + map_data = wrap_file2text(dmm_file) - if(!length(tfile)) + if(!length(map_data)) throw EXCEPTION("Map path '[fname]' does not exist!") if(!x_offset) @@ -65,7 +66,7 @@ GLOBAL_DATUM_INIT(_preloader, /datum/dmm_suite/preloader, new()) log_debug("[measureOnly ? "Measuring" : "Loading"] map: [fname]") try LM.index = 1 - while(dmmRegex.Find(tfile, LM.index)) + while(dmmRegex.Find(map_data, LM.index)) LM.index = dmmRegex.next // "aa" = (/type{vars=blah}) diff --git a/code/modules/martial_arts/martial.dm b/code/modules/martial_arts/martial.dm index 2547f0d8cfcb..2c5f59459161 100644 --- a/code/modules/martial_arts/martial.dm +++ b/code/modules/martial_arts/martial.dm @@ -317,7 +317,7 @@ if(!istype(user) || !user) return if(user.mind) //Prevents changelings and vampires from being able to learn it - if(IS_CHANGELING(user)) + if(IS_CHANGELING(user) || IS_MINDFLAYER(user)) to_chat(user, "We try multiple times, but we are not able to comprehend the contents of the scroll!") return else if(user.mind.has_antag_datum(/datum/antagonist/vampire)) //Vampires @@ -341,7 +341,7 @@ if(!istype(user) || !user) return if(user.mind) //Prevents changelings and vampires from being able to learn it - if(IS_CHANGELING(user)) + if(IS_CHANGELING(user) || IS_MINDFLAYER(user)) to_chat(user, "We try multiple times, but we simply cannot grasp the basics of CQC!") return else if(user.mind.has_antag_datum(/datum/antagonist/vampire)) //Vampires diff --git a/code/modules/martial_arts/torque_enhancer.dm b/code/modules/martial_arts/torque_enhancer.dm new file mode 100644 index 000000000000..05deda917b5e --- /dev/null +++ b/code/modules/martial_arts/torque_enhancer.dm @@ -0,0 +1,40 @@ +/datum/martial_art/torque + name = "Torque enhancer" + weight = 500 // You shouldn't be able to override this, it's a passive you actively buy + /// What level is the passive at + var/level = 1 + +/datum/martial_art/torque/harm_act(mob/living/carbon/human/A, mob/living/carbon/human/D) + MARTIAL_ARTS_ACT_CHECK + var/attack_sound + var/list/attack_verb = list() + switch(level) + if(FLAYER_POWER_LEVEL_ONE) + attack_sound = 'sound/weapons/sonic_jackhammer.ogg' + attack_verb = list("bashes", "batters") + if(FLAYER_POWER_LEVEL_TWO) + attack_sound = 'sound/effects/meteorimpact.ogg' + attack_verb = list("blugeons", "beats") + if(FLAYER_POWER_LEVEL_THREE) + attack_sound = 'sound/misc/demon_attack1.ogg' + attack_verb = list("destroys", "demolishes", "hammers") + + var/datum/species/attacking = A.dna?.species + var/damage = 5 // In case the attacker doesn't have a species somehow + if(attacking) + damage = rand(attacking.punchdamagelow, attacking.punchdamagehigh) + damage += 5 * level + + var/picked_hit_type = pick(attack_verb) + A.do_attack_animation(D, ATTACK_EFFECT_PUNCH) + D.apply_damage(damage, BRUTE) + + if(level >= 2) // This is to mimic species unarmed attacks, if you deal more than 10 damage the attackee is knocked down + D.KnockDown(4 SECONDS) // The threshold for a knockdown is 9 damage, so at level 2 your minimum is already higher than that + + if(attack_sound) + playsound(get_turf(D), attack_sound, 50, TRUE, -1) + D.visible_message("[A] [picked_hit_type] [D]!", \ + "[A] [picked_hit_type] you!") + add_attack_logs(A, D, "Melee attacked with [src]") + return TRUE diff --git a/code/modules/mob/language.dm b/code/modules/mob/language.dm index f2b18ff15e9e..185be3ddb7c4 100644 --- a/code/modules/mob/language.dm +++ b/code/modules/mob/language.dm @@ -731,6 +731,8 @@ popup.open() /mob/living/Topic(href, href_list) + if(..()) + return TRUE if(href_list["default_lang"]) if(href_list["default_lang"] == "reset") set_default_language(null) @@ -740,8 +742,6 @@ set_default_language(L) check_languages() return TRUE - else - return ..() /datum/language/human/monkey name = "Chimpanzee" diff --git a/code/modules/mob/living/carbon/carbon_procs.dm b/code/modules/mob/living/carbon/carbon_procs.dm index e3a91388a67d..7c3a1a2e86c3 100644 --- a/code/modules/mob/living/carbon/carbon_procs.dm +++ b/code/modules/mob/living/carbon/carbon_procs.dm @@ -949,6 +949,10 @@ GLOBAL_LIST_INIT(ventcrawl_machinery, list(/obj/machinery/atmospherics/unary/ven /mob/living/carbon/emp_act(severity) ..() + if(HAS_TRAIT(src, TRAIT_EMP_IMMUNE)) + return + if(HAS_TRAIT(src, TRAIT_EMP_RESIST)) + severity = clamp(severity, EMP_LIGHT, EMP_WEAKENED) for(var/X in internal_organs) var/obj/item/organ/internal/O = X O.emp_act(severity) diff --git a/code/modules/mob/living/carbon/carbon_update_status.dm b/code/modules/mob/living/carbon/carbon_update_status.dm index d06555cc6310..5b1d89a4da6c 100644 --- a/code/modules/mob/living/carbon/carbon_update_status.dm +++ b/code/modules/mob/living/carbon/carbon_update_status.dm @@ -1,7 +1,8 @@ /mob/living/carbon/update_stat(reason = "none given") if(status_flags & GODMODE) return - if(stat != DEAD) + + if(stat != DEAD && !(status_flags & TERMINATOR_FORM)) if(health <= HEALTH_THRESHOLD_DEAD && check_death_method()) death() create_debug_log("died of damage, trigger reason: [reason]") diff --git a/code/modules/mob/living/carbon/human/human_damage.dm b/code/modules/mob/living/carbon/human/human_damage.dm index f508bc1e511f..e0f3c1d304f3 100644 --- a/code/modules/mob/living/carbon/human/human_damage.dm +++ b/code/modules/mob/living/carbon/human/human_damage.dm @@ -26,8 +26,8 @@ if(dna.species && amount > 0) if(use_brain_mod) amount *= dna.species.brain_mod - sponge.damage = clamp(sponge.damage + amount, 0, 120) - if(sponge.damage >= 120) + sponge.damage = clamp(sponge.damage + amount, 0, sponge.max_damage) + if(sponge.damage >= sponge.max_damage) death() if(updating) update_stat("adjustBrainLoss") @@ -44,7 +44,7 @@ if(use_brain_mod) amount *= dna.species.brain_mod sponge.damage = clamp(amount, 0, 120) - if(sponge.damage >= 120) + if(sponge.damage >= sponge.max_damage) death() if(updating) update_stat("setBrainLoss") diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index 2c2f3d4d7417..ceed21264053 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -287,6 +287,10 @@ emp_act /mob/living/carbon/human/emp_act(severity) ..() + if(HAS_TRAIT(src, TRAIT_EMP_IMMUNE)) + return + if(HAS_TRAIT(src, TRAIT_EMP_RESIST)) + severity = clamp(severity, EMP_LIGHT, EMP_WEAKENED) for(var/X in bodyparts) var/obj/item/organ/external/L = X L.emp_act(severity) diff --git a/code/modules/mob/living/carbon/human/human_life.dm b/code/modules/mob/living/carbon/human/human_life.dm index a71035751c39..1df47997fc71 100644 --- a/code/modules/mob/living/carbon/human/human_life.dm +++ b/code/modules/mob/living/carbon/human/human_life.dm @@ -42,6 +42,12 @@ if(life_tick == 1) regenerate_icons() // Make sure the inventory updates + var/datum/antagonist/mindflayer/F = mind?.has_antag_datum(/datum/antagonist/mindflayer) + if(F) + F.handle_mindflayer() + if(life_tick == 1) + regenerate_icons() + if(player_ghosted > 0 && stat == CONSCIOUS && job && !restrained()) handle_ghosted() if(player_logged > 0 && stat != DEAD && job) @@ -605,17 +611,22 @@ if(status_flags & GODMODE) return 0 + if(status_flags & TERMINATOR_FORM) + return FALSE + var/guaranteed_death_threshold = health + (getOxyLoss() * 0.5) - (getFireLoss() * 0.67) - (getBruteLoss() * 0.67) - if(getBrainLoss() >= 120 || (guaranteed_death_threshold) <= -500) + var/obj/item/organ/internal/brain = get_int_organ(/obj/item/organ/internal/brain) + if(brain?.damage >= brain.max_damage || (guaranteed_death_threshold) <= -500) death() return - if(getBrainLoss() >= 100) // braindeath + if(check_brain_threshold(BRAIN_DAMAGE_RATIO_CRITICAL)) // braindeath dna.species.handle_brain_death(src) if(!check_death_method()) if(health <= HEALTH_THRESHOLD_DEAD) + // No need to get the fraction of the max brain damage here, because for it to matter, they'd probably be dead already var/deathchance = min(99, ((getBrainLoss() / 5) + (health + (getOxyLoss() / -2))) * -0.1) if(prob(deathchance)) death() diff --git a/code/modules/mob/living/carbon/human/human_mob.dm b/code/modules/mob/living/carbon/human/human_mob.dm index 4c914284b5c9..d8d677fac288 100644 --- a/code/modules/mob/living/carbon/human/human_mob.dm +++ b/code/modules/mob/living/carbon/human/human_mob.dm @@ -656,14 +656,6 @@ break if(skills) to_chat(usr, "Employment records: [skills]\n") - - if(href_list["lookitem"]) - var/obj/item/I = locate(href_list["lookitem"]) - src.examinate(I) - - if(href_list["lookmob"]) - var/mob/M = locate(href_list["lookmob"]) - src.examinate(M) . = ..() /mob/living/carbon/human/proc/try_set_criminal_status(mob/user) @@ -2061,3 +2053,10 @@ Eyes need to have significantly high darksight to shine unless the mob has the X . = ALPHA_VISIBLE for(var/source in alpha_sources) . = min(., alpha_sources[source]) + +/* + * Returns wether or not the brain is below the threshold + */ +/mob/living/carbon/human/proc/check_brain_threshold(threshold_level) + var/obj/item/organ/internal/brain/brain_organ = get_int_organ(/obj/item/organ/internal/brain) + return brain_organ.damage >= (brain_organ.max_damage * threshold_level) diff --git a/code/modules/mob/living/carbon/human/human_say.dm b/code/modules/mob/living/carbon/human/human_say.dm index f992b4215c79..4c87b39d007b 100644 --- a/code/modules/mob/living/carbon/human/human_say.dm +++ b/code/modules/mob/living/carbon/human/human_say.dm @@ -53,9 +53,9 @@ return has_changer if(mind) - var/datum/antagonist/changeling/cling = mind.has_antag_datum(/datum/antagonist/changeling) - if(cling?.mimicing) - return cling.mimicing + var/datum/antagonist/antagonist_status = mind.has_antag_datum(/datum/antagonist) + if(antagonist_status?.mimicking) + return antagonist_status.mimicking if(GetSpecialVoice()) return GetSpecialVoice() diff --git a/code/modules/mob/living/carbon/human/species/_species.dm b/code/modules/mob/living/carbon/human/species/_species.dm index 5cc343669642..6a9211c7418b 100644 --- a/code/modules/mob/living/carbon/human/species/_species.dm +++ b/code/modules/mob/living/carbon/human/species/_species.dm @@ -516,6 +516,14 @@ return FALSE if(target != user && handle_harm_antag(user, target)) return FALSE + //Mind Flayer code + var/datum/antagonist/mindflayer/MF = user?.mind?.has_antag_datum(/datum/antagonist/mindflayer) + var/obj/item/organ/internal/brain/victims_brain = target.get_int_organ(/obj/item/organ/internal/brain) //In case someone's brain isn't in their head + if(MF && !MF.harvesting && user.zone_selected == victims_brain.parent_organ && target != user) + MF.handle_harvest(target) + add_attack_logs(user, target, "flayerdrain") + return + //End Mind Flayer Code if(target.check_block()) target.visible_message("[target] blocks [user]'s attack!") return FALSE @@ -720,6 +728,23 @@ attack_sound = 'sound/weapons/bite.ogg' sharp = TRUE animation_type = ATTACK_EFFECT_BITE +/* +* Returns a copy of the datum that called this. I know this is pretty dumb +*/ +/datum/unarmed_attack/proc/copy_attack() + var/datum/unarmed_attack/copy = new /datum/unarmed_attack + copy.attack_verb = attack_verb + copy.damage = damage + copy.attack_sound = attack_sound + copy.miss_sound = miss_sound + copy.sharp = sharp + copy.animation_type = animation_type + return copy + +/datum/unarmed_attack/claws/copy_attack() + var/datum/unarmed_attack/claws/copy = ..() + copy.has_been_sharpened = has_been_sharpened + return copy /datum/species/proc/can_equip(obj/item/I, slot, disable_warning = FALSE, mob/living/carbon/human/H) if(slot in no_equip) diff --git a/code/modules/mob/living/default_language.dm b/code/modules/mob/living/default_language.dm index 14497d34a6e2..82f1ffc2ceb0 100644 --- a/code/modules/mob/living/default_language.dm +++ b/code/modules/mob/living/default_language.dm @@ -2,6 +2,9 @@ set name = "Set Default Language" set category = "IC" + if(!(language in languages)) + to_chat(src, "You don't seem to know how to speak [language].") + return if(language) to_chat(src, "You will now speak [language] if you do not specify a language when speaking.") else @@ -13,6 +16,9 @@ set name = "Set Default Language" set category = "IC" + if(!(language in speech_synthesizer_langs)) + to_chat(src, "You don't seem to know how to speak [language].") + return if(language) to_chat(src, "You will now speak [language] if you do not specify a language when speaking.") else diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index 56427c70ef26..36acc6b64077 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -115,3 +115,8 @@ var/last_taste_time /// Stores a var of the last tast message we got. used so we don't spam people messages while they eat var/last_taste_text + ///If a creature gets to be super special and have extra range on their chat messages + var/extra_message_range = 0 + + /// A timer that, when going off, will enable all the mob's radios again + var/radio_enable_timer diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index 18ad834c87e4..f6f892547924 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -237,6 +237,8 @@ GLOBAL_LIST_EMPTY(channel_to_radio_key) var/list/listening = list() var/list/listening_obj = list() + message_range += extra_message_range + if(T) //make sure the air can transmit speech - speaker's side var/datum/gas_mixture/environment = T.get_readonly_air() diff --git a/code/modules/mob/living/silicon/ai/freelook/chunk.dm b/code/modules/mob/living/silicon/ai/freelook/chunk.dm index 2af0044d5968..142072118e5c 100644 --- a/code/modules/mob/living/silicon/ai/freelook/chunk.dm +++ b/code/modules/mob/living/silicon/ai/freelook/chunk.dm @@ -20,6 +20,8 @@ /datum/camerachunk/proc/add_camera(obj/machinery/camera/cam) if(active_cameras[cam] || inactive_cameras[cam]) return + if(cam.non_chunking_camera) + return // Register all even though it is active/inactive. Won't get called incorrectly RegisterSignal(cam, COMSIG_CAMERA_OFF, PROC_REF(deactivate_camera), TRUE) RegisterSignal(cam, COMSIG_CAMERA_ON, PROC_REF(activate_camera), TRUE) diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm index 20d431963001..f3c299651902 100644 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ b/code/modules/mob/living/simple_animal/bot/bot.dm @@ -947,6 +947,7 @@ Pass a positive integer as an argument to override a bot's default speed. return has_access(list(), req_access, acc) /mob/living/simple_animal/bot/Topic(href, href_list) + ..() if(href_list["close"]) // HUE HUE if(usr in users) users.Remove(usr) diff --git a/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_ghost_interaction.dm b/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_ghost_interaction.dm index 5f132fa5487d..c5d8473379bb 100644 --- a/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_ghost_interaction.dm +++ b/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_ghost_interaction.dm @@ -1,5 +1,6 @@ /mob/living/simple_animal/hostile/poison/terror_spider/Topic(href, href_list) + ..() if(href_list["activate"]) var/mob/user = usr if(HAS_TRAIT(user, TRAIT_RESPAWNABLE)) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 5cd8ae5ed495..5655085d2aa9 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -792,7 +792,10 @@ GLOBAL_LIST_INIT(slot_equipment_priority, list( \ src:cameraFollow = null /mob/Topic(href, href_list) - ..() + if(usr != src) + return TRUE + if(..()) + return TRUE if(href_list["mach_close"]) var/t1 = "window=[href_list["mach_close"]]" unset_machine() diff --git a/code/modules/mob/mob_death_base.dm b/code/modules/mob/mob_death_base.dm index 508dd271849e..b6f32f69e703 100644 --- a/code/modules/mob/mob_death_base.dm +++ b/code/modules/mob/mob_death_base.dm @@ -9,7 +9,7 @@ return FALSE /mob/proc/death(gibbed) - SEND_SIGNAL(src, COMSIG_MOB_DEATH, gibbed) + SEND_SIGNAL(src, COMSIG_MOB_DEATH, gibbed, src) return FALSE /mob/proc/dust_animation() diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm index 48f7e6cf8cb3..c3a043807110 100644 --- a/code/modules/mob/new_player/new_player.dm +++ b/code/modules/mob/new_player/new_player.dm @@ -112,6 +112,10 @@ if(!client) return FALSE + if(usr != src) + message_admins("[key_name_admin(usr)] may have attempted to href exploit with [key_name_admin(src)]'s new_player mob.") + return + if(href_list["consent_signed"]) var/datum/db_query/query = SSdbcore.NewQuery("REPLACE INTO privacy (ckey, datetime, consent) VALUES (:ckey, Now(), 1)", list( "ckey" = ckey diff --git a/code/modules/mod/modules/modules_engineering.dm b/code/modules/mod/modules/modules_engineering.dm index d42f440676e6..e5abb7c873a2 100644 --- a/code/modules/mod/modules/modules_engineering.dm +++ b/code/modules/mod/modules/modules_engineering.dm @@ -133,22 +133,25 @@ name = "tether" icon_state = "tether_projectile" icon = 'icons/obj/clothing/modsuit/mod_modules.dmi' + var/chain_icon_state = "line" speed = 2 damage = 5 range = 15 hitsound = 'sound/weapons/batonextend.ogg' hitsound_wall = 'sound/weapons/batonextend.ogg' + ///How fast the tether will throw the user at the target + var/yank_speed = 1 /obj/item/projectile/tether/proc/make_chain() if(firer) - chain = Beam(firer, icon_state = "line", icon = 'icons/obj/clothing/modsuit/mod_modules.dmi', time = 10 SECONDS, maxdistance = 15) + chain = Beam(firer, chain_icon_state, icon, time = 10 SECONDS, maxdistance = range) /obj/item/projectile/tether/on_hit(atom/target) . = ..() if(firer && isliving(firer)) var/mob/living/L = firer L.apply_status_effect(STATUS_EFFECT_IMPACT_IMMUNE) - L.throw_at(target, 15, 1, L, FALSE, FALSE, callback = CALLBACK(L, TYPE_PROC_REF(/mob/living, remove_status_effect), STATUS_EFFECT_IMPACT_IMMUNE), block_movement = FALSE) + L.throw_at(target, 15, yank_speed, L, FALSE, FALSE, callback = CALLBACK(L, TYPE_PROC_REF(/mob/living, remove_status_effect), STATUS_EFFECT_IMPACT_IMMUNE), block_movement = FALSE) /obj/item/projectile/tether/Destroy() QDEL_NULL(chain) diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/paper.dm index 03009f4c219e..29330bc8feb0 100644 --- a/code/modules/paperwork/paper.dm +++ b/code/modules/paperwork/paper.dm @@ -47,6 +47,7 @@ var/const/deffont = "Verdana" var/const/signfont = "Times New Roman" var/const/crayonfont = "Comic Sans MS" + var/regex/blacklist = new("( "}, "window=paper_help") +/obj/item/paper/vv_edit_var(var_name, var_value) + if((var_name == "info") && blacklist.Find(var_value)) //uh oh, they tried to be naughty + message_admins("EXPLOIT WARNING: ADMIN [usr.ckey] attempted to write paper containing JS abusable tags!") + log_admin("EXPLOIT WARNING: ADMIN [usr.ckey] attempted to write paper containing JS abusable tags") + return FALSE + return ..() + /obj/item/paper/proc/topic_href_write(id, input_element) var/obj/item/item_write = usr.get_active_hand() // Check to see if he still got that darn pen, also check if he's using a crayon or pen. add_hiddenprint(usr) // No more forging nasty documents as someone else, you jerks @@ -321,6 +329,10 @@ return if(loc != usr && !Adjacent(usr) && !((istype(loc, /obj/item/clipboard) || istype(loc, /obj/item/folder)) && ((usr in get_turf(src)) || loc.Adjacent(usr)))) return // If paper is not in usr, then it must be near them, or in a clipboard or folder, which must be in or near usr + if(blacklist.Find(input_element)) //uh oh, they tried to be naughty + message_admins("EXPLOIT WARNING: [usr.ckey] attempted to write paper containing JS abusable tags!") + log_admin("EXPLOIT WARNING: [usr.ckey] attempted to write paper containing JS abusable tags") + return FALSE input_element = parsepencode(input_element, item_write, usr) // Encode everything from pencode to html if(id != "end") addtofield(text2num(id), input_element) // He wants to edit a field, let him. diff --git a/code/modules/power/cell.dm b/code/modules/power/cell.dm index 17064e2253c3..c9f94b1663b1 100644 --- a/code/modules/power/cell.dm +++ b/code/modules/power/cell.dm @@ -375,3 +375,10 @@ name = "reactive armor power cell" desc = "A cell used to power reactive armors." maxcharge = 2400 + +/obj/item/stock_parts/cell/flayerprod + name = "mind flayer internal cell" + desc = "you shouldn't be seeing this, contact a coder" + maxcharge = 4000 + self_recharge = TRUE + chargerate = 200 //This self charges it 50 power per tick at the base level diff --git a/code/modules/projectiles/ammunition/ammo_casings.dm b/code/modules/projectiles/ammunition/ammo_casings.dm index d35ae82d962d..f8b17ff61968 100644 --- a/code/modules/projectiles/ammunition/ammo_casings.dm +++ b/code/modules/projectiles/ammunition/ammo_casings.dm @@ -320,6 +320,12 @@ icon_state = "partyshell" projectile_type = /obj/item/projectile/bullet/confetti +/obj/item/ammo_casing/shotgun/shrapnel + name = "shrapnel rounds" + projectile_type = /obj/item/projectile/bullet/shrapnel + pellets = 3 + variance = 20 + /obj/item/ammo_casing/a556 name = "5.56mm round" desc = "A 5.56mm rifle round, produced in incredible quantities by the Trans-Solar Federation." diff --git a/code/modules/projectiles/guns/projectile/shotgun.dm b/code/modules/projectiles/guns/projectile/shotgun.dm index d63745f94352..f65e94674595 100644 --- a/code/modules/projectiles/guns/projectile/shotgun.dm +++ b/code/modules/projectiles/guns/projectile/shotgun.dm @@ -57,18 +57,20 @@ COOLDOWN_START(src, pump_cooldown, pump_time) /obj/item/gun/projectile/shotgun/proc/pump(mob/M) + if(QDELETED(M)) + return playsound(M, 'sound/weapons/gun_interactions/shotgunpump.ogg', 60, TRUE) - pump_unload(M) - pump_reload(M) + pump_unload() + pump_reload() -/obj/item/gun/projectile/shotgun/proc/pump_unload(mob/M) +/obj/item/gun/projectile/shotgun/proc/pump_unload() if(chambered)//We have a shell in the chamber chambered.forceMove(get_turf(src)) chambered.SpinAnimation(5, 1) playsound(src, chambered.casing_drop_sound, 60, TRUE) chambered = null -/obj/item/gun/projectile/shotgun/proc/pump_reload(mob/M) +/obj/item/gun/projectile/shotgun/proc/pump_reload() if(!magazine.ammo_count()) return FALSE var/obj/item/ammo_casing/AC = magazine.get_round() //load next casing. diff --git a/code/modules/reagents/chemistry/reagents/drugs.dm b/code/modules/reagents/chemistry/reagents/drugs.dm index 21ac9b85aaaf..410f5c32bf3f 100644 --- a/code/modules/reagents/chemistry/reagents/drugs.dm +++ b/code/modules/reagents/chemistry/reagents/drugs.dm @@ -1172,6 +1172,24 @@ M.emote(pick("twitch", "shiver")) return ..() | update_flags +/// Used to test if an IPC is a mindflayer or not +/datum/reagent/lube/conductive + name = "Conductive Lubricant" + id = "conductivelube" + description = "This is a special lubricant designed to attract onto and excite parasitic mindflayer swarms, revealing if someone hosts a hive. Doesn't include a cooling agent, so tends to cause overheating." + harmless = FALSE + color = "#163b39" + taste_description = "batteries" + process_flags = SYNTHETIC + +/datum/reagent/lube/conductive/on_mob_life(mob/living/M) + var/datum/antagonist/mindflayer/flayer = M.mind?.has_antag_datum(/datum/antagonist/mindflayer) + if(flayer && (flayer.total_swarms_gathered > 0)) // Like vampires, give flayers who haven't done anything yet a pass + M.Jitter(30 SECONDS_TO_JITTER) + if(prob(20)) + do_sparks(5, FALSE, M) + M.bodytemperature += 40 + return ..() /datum/reagent/lube/ultra/on_mob_delete(mob/living/M) REMOVE_TRAIT(M, TRAIT_GOTTAGOFAST, id) diff --git a/code/modules/reagents/chemistry/recipes/drugs_reactions.dm b/code/modules/reagents/chemistry/recipes/drugs_reactions.dm index 78c9fd9e9226..661d4b0c73c0 100644 --- a/code/modules/reagents/chemistry/recipes/drugs_reactions.dm +++ b/code/modules/reagents/chemistry/recipes/drugs_reactions.dm @@ -138,6 +138,14 @@ result_amount = 2 mix_message = "The mixture darkens and appears to partially vaporize into a chilling aerosol." +/datum/chemical_reaction/lube/conductive + name = "conductive lube" + id = "conductivelube" + result = "conductivelube" + required_reagents = list("teslium" = 1, "lube" = 1, "aluminum" = 1) + result_amount = 3 + mix_message = "The mixture darkens and starts sparking." + /datum/chemical_reaction/surge name = "Surge" id = "surge" diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm index c507ca5fdd39..07931aa984ad 100644 --- a/code/modules/research/designs/medical_designs.dm +++ b/code/modules/research/designs/medical_designs.dm @@ -678,6 +678,17 @@ build_path = /obj/item/organ/internal/cyberimp/chest/ipc_joints/sealed category = list("Medical") +/datum/design/flayer_pacification + name = "Mindflayer Pacification Implant" + desc = "This implant acts on mindflayer swarms like smoke to bees, making them much more docile." + id = "flayer_nullification_implant" + req_tech = list("materials" = 5, "programming" = 5,"engineering" = 5, "combat" = 5) + build_type = PROTOLATHE | MECHFAB + construction_time = 6 SECONDS + materials = list(MAT_METAL = 10000, MAT_SILVER = 8000, MAT_GOLD = 3000, MAT_PLASMA = 10000) + build_path = /obj/item/organ/internal/cyberimp/chest/ipc_joints/flayer_pacification + category = list("Medical") + ///////////////////////////////////////// ////////////Regular Implants///////////// ///////////////////////////////////////// diff --git a/code/modules/supply/supply_console.dm b/code/modules/supply/supply_console.dm index 09ba99596a4f..337e10fd411f 100644 --- a/code/modules/supply/supply_console.dm +++ b/code/modules/supply/supply_console.dm @@ -344,7 +344,7 @@ for(var/datum/station_department/department in SSjobs.station_departments) if(department.department_account == selected_account) order.ordered_by_department = department //now that we know which department this is for, attach it to the order - order.orderedbyaccount = selected_account + order.set_account(selected_account) order.requires_head_approval = TRUE if(length(order.object.department_restrictions) && !(department.department_name in order.object.department_restrictions)) @@ -358,7 +358,7 @@ //===Handle Supply Order=== if(selected_account.account_type == ACCOUNT_TYPE_PERSONAL) //if the account is a personal account (and doesn't require CT approval), go ahead and pay for it now - order.orderedbyaccount = selected_account + order.set_account(selected_account) if(attempt_account_authentification(selected_account, user)) var/paid_for = FALSE if(!order.requires_cargo_approval && pay_with_account(selected_account, order.object.get_cost(), "[order.object.name] Crate Purchase", "Cargo Requests Console", user, account_database.vendor_account)) diff --git a/code/modules/supply/supply_order.dm b/code/modules/supply/supply_order.dm index 99bd05ad6e86..ba2fa011d70f 100644 --- a/code/modules/supply/supply_order.dm +++ b/code/modules/supply/supply_order.dm @@ -22,6 +22,16 @@ name = "request form" var/order_number +/// Set the account that made the request and make sure the request is deleted when the account is deleted +/datum/supply_order/proc/set_account(datum/money_account/account) + orderedbyaccount = account + RegisterSignal(orderedbyaccount, COMSIG_PARENT_QDELETING, PROC_REF(clear_request)) + +/// Clear the request from the request list and delete it +/datum/supply_order/proc/clear_request() + SSeconomy.request_list -= src + qdel(src) + /datum/supply_order/proc/createObject(atom/_loc, errors = 0) if(!object) return diff --git a/code/modules/supply/supply_packs/pack_security.dm b/code/modules/supply/supply_packs/pack_security.dm index 0463a992fd73..e86d9792de86 100644 --- a/code/modules/supply/supply_packs/pack_security.dm +++ b/code/modules/supply/supply_packs/pack_security.dm @@ -318,6 +318,14 @@ cost = 500 containername = "chemical bio-chip crate" +/datum/supply_packs/security/armory/flayer_nullifer + name = "Mindflayer Containment Kit" + contains = list(/obj/item/organ/internal/cyberimp/chest/ipc_joints/flayer_pacification, + /obj/item/storage/box/handcuffs, + /obj/item/toy/plushie/ipcplushie) // For practicing takedowns + cost = 250 + containername = "mindflayer containment kit" + /datum/supply_packs/security/armory/bluespace_anchor name = "Bluespace Anchor Crate" contains = list(/obj/item/organ/internal/cyberimp/chest/bluespace_anchor) diff --git a/code/modules/surgery/organs/augments_internal.dm b/code/modules/surgery/organs/augments_internal.dm index 8b9aa48a626c..05d7d216960f 100644 --- a/code/modules/surgery/organs/augments_internal.dm +++ b/code/modules/surgery/organs/augments_internal.dm @@ -860,6 +860,21 @@ owner.physiology.stamina_mod /= 1.15 return ..() +/obj/item/organ/internal/cyberimp/chest/ipc_joints/flayer_pacification + name = "\improper Nanite pacifier" + desc = "This implant acts on mindflayer nanobots like smoke does to bees, rendering them significantly more docile." + implant_color = COLOR_BLACK + origin_tech = "materials=4;programming=4;biotech=5;combat=4;" + +/obj/item/organ/internal/cyberimp/chest/ipc_joints/flayer_pacification/insert(mob/living/carbon/M, special) + ..() + ADD_TRAIT(M, TRAIT_MINDFLAYER_NULLIFIED, UNIQUE_TRAIT_SOURCE(src)) + SEND_SIGNAL(M, COMSIG_FLAYER_RETRACT_IMPLANTS, TRUE) + +/obj/item/organ/internal/cyberimp/chest/ipc_joints/flayer_pacification/remove(mob/living/carbon/M, special) + REMOVE_TRAIT(M, TRAIT_MINDFLAYER_NULLIFIED, UNIQUE_TRAIT_SOURCE(src)) + return ..() + //BOX O' IMPLANTS /obj/item/storage/box/cyber_implants diff --git a/code/modules/surgery/organs/brain.dm b/code/modules/surgery/organs/brain.dm index 4da4a401d6ed..1d10345d7bdc 100644 --- a/code/modules/surgery/organs/brain.dm +++ b/code/modules/surgery/organs/brain.dm @@ -20,6 +20,8 @@ /// If it's a fake brain without a mob assigned that should still be treated like a real brain. var/decoy_brain = FALSE + /// Do we have temporary brain max hp reduction? + var/temporary_damage = 0 /obj/item/organ/internal/brain/xeno name = "xenomorph brain" @@ -123,18 +125,28 @@ owner.setBrainLoss(120) /obj/item/organ/internal/brain/on_life() - if(decoy_brain || damage < 10) + if(decoy_brain) return - switch(damage) - if(10 to 30) + + var/ratio = damage / max_damage // Get our damage as a percentage of max HP + if(ratio < BRAIN_DAMAGE_RATIO_LIGHT) + return + + switch(ratio) + if(BRAIN_DAMAGE_RATIO_LIGHT to BRAIN_DAMAGE_RATIO_MINOR) handle_minor_brain_damage() - if(31 to 60) + if(BRAIN_DAMAGE_RATIO_MINOR to BRAIN_DAMAGE_RATIO_MODERATE) handle_moderate_brain_damage() - if(61 to 80) + if(BRAIN_DAMAGE_RATIO_MODERATE to BRAIN_DAMAGE_RATIO_SEVERE) handle_severe_brain_damage() - if(81 to 100) + if(BRAIN_DAMAGE_RATIO_SEVERE to BRAIN_DAMAGE_RATIO_CRITICAL) handle_critical_brain_damage() + if(temporary_damage) // Heal our max hp limit by one per cycle + // We use `clamp()` here because `temporary_damage` can have decimals + temporary_damage = clamp(temporary_damage - 0.25, 0, 120) + max_damage = clamp(max_damage + 0.25, 0, 120) + /obj/item/organ/internal/brain/proc/handle_minor_brain_damage() if(prob(5)) owner.Dizzy(5 SECONDS) diff --git a/code/modules/surgery/organs/organ_external.dm b/code/modules/surgery/organs/organ_external.dm index c01ab721347b..9bc5f4aa2c9b 100644 --- a/code/modules/surgery/organs/organ_external.dm +++ b/code/modules/surgery/organs/organ_external.dm @@ -357,26 +357,34 @@ return if(tough) // Augmented limbs (remember they take -5 brute/-4 burn damage flat so any value below is compensated) switch(severity) - if(1) + if(EMP_HEAVY) // 44 total burn damage with 11 augmented limbs receive_damage(0, 8) - if(2) + if(EMP_LIGHT) // 22 total burn damage with 11 augmented limbs receive_damage(0, 6) + if(EMP_WEAKENED) + // 11 total burn damage with 11 augmented limbs + receive_damage(0, 5) else if(emp_resistant) // IPC limbs switch(severity) - if(1) + if(EMP_HEAVY) // 5.9 burn damage, 64.9 damage with 11 limbs. receive_damage(0, 5.9) - if(2) + if(EMP_LIGHT) // 3.63 burn damage, 39.93 damage with 11 limbs. receive_damage(0, 3.63) + if(EMP_WEAKENED) + // 1.32 (2 * .66 burn mod) burn damage, 14.52 damage with 11 limbs. + receive_damage(0, 2) else // Basic prosthetic limbs switch(severity) - if(1) + if(EMP_HEAVY) receive_damage(0, 20) - if(2) + if(EMP_LIGHT) receive_damage(0, 7) + if(EMP_WEAKENED) + receive_damage(0, 3) /* This function completely restores a damaged organ to perfect condition. diff --git a/code/modules/surgery/organs/organ_internal.dm b/code/modules/surgery/organs/organ_internal.dm index 6a67faa8ccd9..4beddc40ac3d 100644 --- a/code/modules/surgery/organs/organ_internal.dm +++ b/code/modules/surgery/organs/organ_internal.dm @@ -145,10 +145,12 @@ // No EMP handling was done, lets just give em damage switch(severity) - if(1) + if(EMP_HEAVY) receive_damage(20, 1) - if(2) + if(EMP_LIGHT) receive_damage(7, 1) + if(EMP_WEAKENED) + receive_damage(3, 1) /obj/item/organ/internal/replaced(mob/living/carbon/human/target) insert(target) diff --git a/icons/misc/pic_in_pic.dmi b/icons/misc/pic_in_pic.dmi index 82359ccd6d03..fc472032b833 100644 Binary files a/icons/misc/pic_in_pic.dmi and b/icons/misc/pic_in_pic.dmi differ diff --git a/icons/mob/actions/actions.dmi b/icons/mob/actions/actions.dmi index fb939a8cbbbe..e33b6ed74bf1 100644 Binary files a/icons/mob/actions/actions.dmi and b/icons/mob/actions/actions.dmi differ diff --git a/icons/mob/actions/flayer_actions.dmi b/icons/mob/actions/flayer_actions.dmi new file mode 100644 index 000000000000..79013e1ded3a Binary files /dev/null and b/icons/mob/actions/flayer_actions.dmi differ diff --git a/icons/mob/clothing/eyes.dmi b/icons/mob/clothing/eyes.dmi index 5140efca1a50..a48fbca63cab 100644 Binary files a/icons/mob/clothing/eyes.dmi and b/icons/mob/clothing/eyes.dmi differ diff --git a/icons/mob/hud/antaghud.dmi b/icons/mob/hud/antaghud.dmi index 89fcc70782c9..477faf4a07fe 100644 Binary files a/icons/mob/hud/antaghud.dmi and b/icons/mob/hud/antaghud.dmi differ diff --git a/icons/mob/inhands/weapons_lefthand.dmi b/icons/mob/inhands/weapons_lefthand.dmi index 11ffc096c7dd..56dc70c27268 100644 Binary files a/icons/mob/inhands/weapons_lefthand.dmi and b/icons/mob/inhands/weapons_lefthand.dmi differ diff --git a/icons/mob/inhands/weapons_righthand.dmi b/icons/mob/inhands/weapons_righthand.dmi index 7648985d6cc7..36e5ec1c4e71 100644 Binary files a/icons/mob/inhands/weapons_righthand.dmi and b/icons/mob/inhands/weapons_righthand.dmi differ diff --git a/icons/obj/clothing/modsuit/mod_modules.dmi b/icons/obj/clothing/modsuit/mod_modules.dmi index f089d016e89d..95bc512851ec 100644 Binary files a/icons/obj/clothing/modsuit/mod_modules.dmi and b/icons/obj/clothing/modsuit/mod_modules.dmi differ diff --git a/icons/obj/shards.dmi b/icons/obj/shards.dmi index 0f47b34e8ca6..36fb3d843015 100644 Binary files a/icons/obj/shards.dmi and b/icons/obj/shards.dmi differ diff --git a/icons/obj/weapons/baton.dmi b/icons/obj/weapons/baton.dmi index 0becb4a0d51e..cb779fbb83e2 100644 Binary files a/icons/obj/weapons/baton.dmi and b/icons/obj/weapons/baton.dmi differ diff --git a/paradise.dme b/paradise.dme index 030bfc71744e..d3e6b9fdd695 100644 --- a/paradise.dme +++ b/paradise.dme @@ -89,6 +89,7 @@ #include "code\__DEFINES\mecha_defines.dm" #include "code\__DEFINES\mecha_hides.dm" #include "code\__DEFINES\medal.dm" +#include "code\__DEFINES\mindflayer_defines.dm" #include "code\__DEFINES\misc_defines.dm" #include "code\__DEFINES\mob_defines.dm" #include "code\__DEFINES\mod.dm" @@ -1522,6 +1523,14 @@ #include "code\modules\antagonists\changeling\powers\transform.dm" #include "code\modules\antagonists\cult\datum_cultist.dm" #include "code\modules\antagonists\cult\team_cult.dm" +#include "code\modules\antagonists\mind_flayer\flayer_datum.dm" +#include "code\modules\antagonists\mind_flayer\flayer_power.dm" +#include "code\modules\antagonists\mind_flayer\mindflayer_gamemode.dm" +#include "code\modules\antagonists\mind_flayer\powers\flayer_buffs.dm" +#include "code\modules\antagonists\mind_flayer\powers\flayer_mobility_powers.dm" +#include "code\modules\antagonists\mind_flayer\powers\flayer_passives.dm" +#include "code\modules\antagonists\mind_flayer\powers\flayer_stealth_powers.dm" +#include "code\modules\antagonists\mind_flayer\powers\flayer_weapon_powers.dm" #include "code\modules\antagonists\revolutionary\datum_headrev.dm" #include "code\modules\antagonists\revolutionary\datum_revolutionary.dm" #include "code\modules\antagonists\revolutionary\team_revolution.dm" @@ -2065,6 +2074,7 @@ #include "code\modules\martial_arts\muscle_implant.dm" #include "code\modules\martial_arts\plasma_fist.dm" #include "code\modules\martial_arts\sleeping_carp.dm" +#include "code\modules\martial_arts\torque_enhancer.dm" #include "code\modules\martial_arts\wrestling.dm" #include "code\modules\martial_arts\combos\martial_combo.dm" #include "code\modules\martial_arts\combos\adminfu\healing_palm.dm" diff --git a/sound/ambience/antag/flayeralert.ogg b/sound/ambience/antag/flayeralert.ogg new file mode 100644 index 000000000000..a3255bac8611 Binary files /dev/null and b/sound/ambience/antag/flayeralert.ogg differ diff --git a/sound/ambience/antag/mindflayer_alert.ogg b/sound/ambience/antag/mindflayer_alert.ogg new file mode 100644 index 000000000000..d6a9f146d2c6 Binary files /dev/null and b/sound/ambience/antag/mindflayer_alert.ogg differ diff --git a/tgui/packages/tgui-panel/chat/renderer.js b/tgui/packages/tgui-panel/chat/renderer.js index aa2dde858376..455c296ebd2a 100644 --- a/tgui/packages/tgui-panel/chat/renderer.js +++ b/tgui/packages/tgui-panel/chat/renderer.js @@ -4,6 +4,7 @@ * @license MIT */ +import DOMPurify from 'dompurify'; import { EventEmitter } from 'common/events'; import { classes } from 'common/react'; import { createLogger } from 'tgui/logging'; @@ -29,6 +30,9 @@ const logger = createLogger('chatRenderer'); // that is still trackable. const SCROLL_TRACKING_TOLERANCE = 24; +// List of blacklisted tags +const blacklisted_tags = ['iframe', 'video']; + const findNearestScrollableParent = (startingNode) => { const body = document.body; let node = startingNode; @@ -359,6 +363,11 @@ class ChatRenderer { // Payload is HTML else if (message.html) { node.innerHTML = message.html; + node.innerHTML = DOMPurify.sanitize(node.innerHTML, { + // No iframes in my chat kkthxbye + FORBID_TAGS: blacklisted_tags, + ALLOW_UNKNOWN_PROTOCOLS: true, + }); } else { logger.error('Error: message is missing text payload', message); } diff --git a/tgui/packages/tgui/interfaces/AugmentMenu.js b/tgui/packages/tgui/interfaces/AugmentMenu.js new file mode 100644 index 000000000000..b367510b57ac --- /dev/null +++ b/tgui/packages/tgui/interfaces/AugmentMenu.js @@ -0,0 +1,177 @@ +import { useBackend, useLocalState } from '../backend'; +import { Stack, Button, Section, Tabs, Input } from '../components'; +import { Window } from '../layouts'; +import { capitalize } from 'common/string'; + +export const AugmentMenu = (props, context) => { + return ( + + + + + + + + ); +}; + +const Abilities = ({ context }) => { + const { act, data } = useBackend(context); + const { usable_swarms, ability_tabs, known_abilities } = data; + const [selectedTab, setSelectedTab] = useLocalState(context, 'selectedTab', ability_tabs[0]); + const [searchText, setSearchText] = useLocalState(context, 'searchText', ''); + + const getFilteredAbilities = () => { + const currentTab = ability_tabs.find((tab) => tab.category_name === selectedTab.category_name); + if (!currentTab) return []; + + const maxStage = Math.min(currentTab.category_stage, 4); + return currentTab.abilities + .filter( + (ability) => + ability.stage <= maxStage && (!searchText || ability.name.toLowerCase().includes(searchText.toLowerCase())) + ) + .sort((a, b) => + ['intruder', 'destroyer'].includes(selectedTab.category_name.toLowerCase()) ? a.stage - b.stage : 0 + ); + }; + + const abilities = getFilteredAbilities(); + const currentTab = ability_tabs.find((tab) => tab.category_name === selectedTab.category_name); + const showStage = ['intruder', 'destroyer'].includes(selectedTab.category_name.toLowerCase()); + + const renderAbility = (ability) => { + const knownAbility = known_abilities.find((a) => a.ability_path === ability.ability_path); + const currentCost = knownAbility ? knownAbility.cost : ability.cost; + const levelInfo = + knownAbility && knownAbility.current_level > 0 + ? `${knownAbility.current_level} / ${knownAbility.max_level}` + : `0 / ${ability.max_level}`; + + return ( + + +