diff --git a/_maps/map_files/YogStation/YogStation.dmm b/_maps/map_files/YogStation/YogStation.dmm
index 70d8c64a3422..faa6d4f24067 100644
--- a/_maps/map_files/YogStation/YogStation.dmm
+++ b/_maps/map_files/YogStation/YogStation.dmm
@@ -64844,24 +64844,6 @@
},
/turf/open/floor/plating,
/area/science/robotics/lab)
-"wKO" = (
-/obj/structure/closet/secure_closet{
- name = "psychiatrist locker";
- req_access_txt = "5"
- },
-/obj/item/storage/pill_bottle/dice,
-/obj/item/storage/pill_bottle/psicodine,
-/obj/item/storage/pill_bottle/lsd{
- name = "very happy pills bottle"
- },
-/obj/item/storage/pill_bottle/happy{
- name = "happy pills bottle"
- },
-/obj/item/storage/pill_bottle/happiness,
-/obj/item/clothing/suit/straight_jacket,
-/obj/item/clothing/mask/muzzle,
-/turf/open/floor/wood,
-/area/medical/psych)
"wKP" = (
/obj/machinery/atmospherics/components/binary/pump{
dir = 8;
@@ -67425,6 +67407,25 @@
},
/turf/open/floor/carpet/purple,
/area/crew_quarters/heads/hor)
+"yaU" = (
+/obj/structure/closet/secure_closet{
+ name = "psychiatrist locker";
+ req_access_txt = "5"
+ },
+/obj/item/storage/pill_bottle/dice,
+/obj/item/storage/pill_bottle/psicodine,
+/obj/item/storage/pill_bottle/lsd{
+ name = "very happy pills bottle"
+ },
+/obj/item/storage/pill_bottle/happy{
+ name = "happy pills bottle"
+ },
+/obj/item/storage/pill_bottle/happiness,
+/obj/item/clothing/suit/straight_jacket,
+/obj/item/clothing/mask/muzzle,
+/obj/item/storage/box/psiimp,
+/turf/open/floor/wood,
+/area/medical/psych)
"ybf" = (
/obj/effect/decal/cleanable/dirt,
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer2{
@@ -92680,7 +92681,7 @@ hYb
ydS
uBk
sHf
-wKO
+yaU
aDR
ljR
cSb
@@ -133440,4 +133441,4 @@ aaa
aaa
aaa
aaa
-"}
\ No newline at end of file
+"}
diff --git a/code/__DEFINES/atom_hud.dm b/code/__DEFINES/atom_hud.dm
index 92941dfa419c..727999764c4f 100644
--- a/code/__DEFINES/atom_hud.dm
+++ b/code/__DEFINES/atom_hud.dm
@@ -44,6 +44,8 @@
#define DIAG_LAUNCHPAD_HUD "22"
//for antag huds. these are used at the /mob level
#define ANTAG_HUD "23"
+/// psi control implant
+#define IMPPSI_HUD "24"
//by default everything in the hud_list of an atom is an image
//a value in hud_list with one of these will change that behavior
diff --git a/code/__DEFINES/psi.dm b/code/__DEFINES/psi.dm
new file mode 100644
index 000000000000..82bdb908b62b
--- /dev/null
+++ b/code/__DEFINES/psi.dm
@@ -0,0 +1,33 @@
+#define PSI_COERCION "coercion"
+#define PSI_PSYCHOKINESIS "psychokinesis"
+#define PSI_REDACTION "redaction"
+#define PSI_ENERGISTICS "energistics"
+
+#define PSI_RANK_BLUNT 0
+#define PSI_RANK_LATENT 1
+#define PSI_RANK_OPERANT 2
+#define PSI_RANK_MASTER 3
+#define PSI_RANK_GRANDMASTER 4
+#define PSI_RANK_PARAMOUNT 5
+
+#define PSI_IMPLANT_AUTOMATIC "Security Level Derived"
+#define PSI_IMPLANT_SHOCK "Issue Neural Shock"
+#define PSI_IMPLANT_WARN "Issue Reprimand"
+#define PSI_IMPLANT_LOG "Log Incident"
+#define PSI_IMPLANT_DISABLED "Disabled"
+
+#define INVOKE_PSI_POWERS(holder, powers, target, return_on_invocation) \
+ if(holder?.psi?.can_use()) { \
+ for(var/datum/psionic_power/power as anything in powers) { \
+ var/obj/item/result = power.invoke(holder, target); \
+ if(result) { \
+ power.handle_post_power(holder, target); \
+ if(istype(result)) { \
+ holder.playsound_local(soundin = 'sound/effects/psi/power_evoke.ogg'); \
+ LAZYADD(holder.psi.manifested_items, result); \
+ holder.put_in_hands(result); \
+ } \
+ return return_on_invocation; \
+ } \
+ } \
+ }
diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm
index 07bb830ece9a..e7c5a1d02a19 100644
--- a/code/__DEFINES/role_preferences.dm
+++ b/code/__DEFINES/role_preferences.dm
@@ -22,6 +22,7 @@
#define ROLE_HERETIC "Heretic"
#define ROLE_BLOB "Blob"
#define ROLE_NINJA "Space Ninja"
+#define ROLE_PARAMOUNT "Paramount"
#define ROLE_MONKEY "Monkey"
#define ROLE_ABDUCTOR "Abductor"
#define ROLE_REVENANT "Revenant"
@@ -74,6 +75,7 @@ GLOBAL_LIST_INIT(special_roles, list(
ROLE_CULTIST = /datum/game_mode/cult,
ROLE_BLOB,
ROLE_NINJA,
+ ROLE_PARAMOUNT,
ROLE_OBSESSED,
ROLE_MONKEY = /datum/game_mode/monkey,
ROLE_REVENANT,
diff --git a/code/__DEFINES/{yogs_defines}/antagonists.dm b/code/__DEFINES/{yogs_defines}/antagonists.dm
index 7afc8ce41334..97414297ce8f 100644
--- a/code/__DEFINES/{yogs_defines}/antagonists.dm
+++ b/code/__DEFINES/{yogs_defines}/antagonists.dm
@@ -1,5 +1,6 @@
#define ANTAG_DATUM_VAMPIRE /datum/antagonist/vampire
#define ANTAG_DATUM_THRALL /datum/antagonist/thrall
+#define ANTAG_DATUM_SHADOWTHRALL /datum/antagonist/thrall/shadowling
#define ANTAG_DATUM_SLING /datum/antagonist/shadowling
#define ANTAG_DATUM_DARKSPAWN /datum/antagonist/darkspawn
#define ANTAG_DATUM_VEIL /datum/antagonist/veil
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index 0c1db1bd3dcc..265e07cf729d 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -214,6 +214,8 @@
var/obj/item/I = hud.mymob.get_active_held_item()
if(I)
I.Click(location, control, params)
+ else
+ hud.mymob.attack_empty_hand(hud.mymob.active_hand_index)
else
hud.mymob.swap_hand(held_index)
return 1
diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm
index 53ab9d125815..d54bee2ddf12 100644
--- a/code/_onclick/other_mobs.dm
+++ b/code/_onclick/other_mobs.dm
@@ -5,6 +5,8 @@
Otherwise pretty standard.
*/
/mob/living/carbon/human/UnarmedAttack(atom/A, proximity)
+ if(psi)
+ INVOKE_PSI_POWERS(src, psi.get_melee_powers(SSpsi.faculties_by_intent[a_intent]), A, FALSE)
if(HAS_TRAIT(A, TRAIT_NOINTERACT))
to_chat(A, span_notice("You can't touch things!"))
return
@@ -34,6 +36,14 @@
SEND_SIGNAL(src, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, A)
A.attack_hand(src)
+/mob/living/carbon/human/attack_empty_hand()
+ if(psi)
+ INVOKE_PSI_POWERS(src, psi.get_manifestations(), src, FALSE)
+
+/mob/living/carbon/human/RangedAttack(atom/A, params)
+ if(psi)
+ INVOKE_PSI_POWERS(src, psi.get_ranged_powers(SSpsi.faculties_by_intent[a_intent]), A, TRUE)
+
//Return TRUE to cancel other attack hand effects that respect it.
/atom/proc/attack_hand(mob/user)
. = FALSE
@@ -44,6 +54,9 @@
if(interaction_flags_atom & INTERACT_ATOM_ATTACK_HAND)
. = _try_interact(user)
+/mob/proc/attack_empty_hand(hand)
+ return
+
//Return a non FALSE value to cancel whatever called this from propagating, if it respects it.
/atom/proc/_try_interact(mob/user)
if(IsAdminGhost(user)) //admin abuse
@@ -111,8 +124,6 @@
/*
Animals & All Unspecified
*/
-/mob/living/UnarmedAttack(atom/A)
- A.attack_animal(src)
/atom/proc/attack_animal(mob/user)
return
diff --git a/code/controllers/subsystem/processing/psi.dm b/code/controllers/subsystem/processing/psi.dm
new file mode 100644
index 000000000000..eab50c4bca8c
--- /dev/null
+++ b/code/controllers/subsystem/processing/psi.dm
@@ -0,0 +1,38 @@
+GLOBAL_LIST_INIT(psychic_ranks_to_strings, list("Latent", "Operant", "Masterclass", "Grandmasterclass", "Paramount"))
+
+PROCESSING_SUBSYSTEM_DEF(psi)
+ name = "Psychics"
+ // priority = SS_PRIORITY_PSYCHICS
+ flags = SS_POST_FIRE_TIMING | SS_BACKGROUND
+
+ var/list/faculties_by_id = list()
+ var/list/faculties_by_name = list()
+ var/list/all_aura_images = list()
+ var/list/all_psi_complexes = list()
+ var/list/psi_dampeners = list()
+ var/list/psi_monitors = list()
+ var/list/armour_faculty_by_type = list()
+ var/list/faculties_by_intent = list()
+
+/datum/controller/subsystem/processing/psi/New()
+ NEW_SS_GLOBAL(SSpsi)
+
+/datum/controller/subsystem/processing/psi/proc/get_faculty(faculty)
+ return faculties_by_name[faculty] || faculties_by_id[faculty]
+
+/datum/controller/subsystem/processing/psi/Initialize()
+ . = ..()
+
+ var/list/faculties = subtypesof(/datum/psionic_faculty)
+ for(var/ftype in faculties)
+ var/datum/psionic_faculty/faculty = new ftype
+ faculties_by_id[faculty.id] = faculty
+ faculties_by_name[faculty.name] = faculty
+ faculties_by_intent[faculty.associated_intent] = faculty.id
+
+ var/list/powers = subtypesof(/datum/psionic_power)
+ for(var/ptype in powers)
+ var/datum/psionic_power/power = new ptype
+ if(power.faculty)
+ var/datum/psionic_faculty/faculty = get_faculty(power.faculty)
+ faculty?.powers |= power
diff --git a/code/datums/hud.dm b/code/datums/hud.dm
index 5a1221c915ce..4fbb77bbac21 100644
--- a/code/datums/hud.dm
+++ b/code/datums/hud.dm
@@ -17,6 +17,7 @@ GLOBAL_LIST_INIT(huds, list(
ANTAG_HUD_REV = new/datum/atom_hud/antag(),
ANTAG_HUD_OPS = new/datum/atom_hud/antag(),
ANTAG_HUD_WIZ = new/datum/atom_hud/antag(),
+ ANTAG_HUD_PARAMOUNT = new/datum/atom_hud/antag(),
ANTAG_HUD_SHADOW = new/datum/atom_hud/antag(),
ANTAG_HUD_TRAITOR = new/datum/atom_hud/antag/hidden(),
ANTAG_HUD_NINJA = new/datum/atom_hud/antag/hidden(),
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 3b82222e1f21..c7509a4cad0f 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -438,6 +438,13 @@
if(!check_rights(R_ADMIN))
return
+ if(current && isliving(current))
+ if(href_list["set_psi_faculty"] && href_list["set_psi_faculty_rank"])
+ current.set_psi_rank(href_list["set_psi_faculty"], text2num(href_list["set_psi_faculty_rank"]))
+ message_admins("[key_name_admin(usr)] set [key_name(current)]'s [href_list["set_psi_faculty"]] faculty to [text2num(href_list["set_psi_faculty_rank"])].")
+ log_admin("[key_name_admin(usr)] set [key_name(current)]'s [href_list["set_psi_faculty"]] faculty to [text2num(href_list["set_psi_faculty_rank"])].")
+ return TRUE
+
var/self_antagging = usr == current
if(href_list["add_antag"])
diff --git a/code/datums/traits/negative.dm b/code/datums/traits/negative.dm
index 4ee8b3217a30..1a3b6cee9db4 100644
--- a/code/datums/traits/negative.dm
+++ b/code/datums/traits/negative.dm
@@ -282,7 +282,7 @@
/datum/quirk/nyctophobia/on_process()
var/mob/living/carbon/human/H = quirk_holder
- if((H.dna.species.id in list("shadow", "nightmare", "darkspawn")) || (H.mind && (H.mind.has_antag_datum(ANTAG_DATUM_THRALL) || H.mind.has_antag_datum(ANTAG_DATUM_SLING) || H.mind.has_antag_datum(ANTAG_DATUM_DARKSPAWN) || H.mind.has_antag_datum(ANTAG_DATUM_VEIL)))) //yogs - thrall & sling check
+ if((H.dna.species.id in list("shadow", "nightmare", "darkspawn")) || (H.mind && (H.mind.has_antag_datum(ANTAG_DATUM_SHADOWTHRALL) || H.mind.has_antag_datum(ANTAG_DATUM_SLING) || H.mind.has_antag_datum(ANTAG_DATUM_DARKSPAWN) || H.mind.has_antag_datum(ANTAG_DATUM_VEIL)))) //yogs - thrall & sling check
return //we're tied with the dark, so we don't get scared of it; don't cleanse outright to avoid cheese
var/turf/T = get_turf(quirk_holder)
var/lums = T.get_lumcount()
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 8e46865dcb7a..5b49cd3d6481 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -987,3 +987,6 @@
animate(I, alpha = 175, pixel_x = to_x, pixel_y = to_y, time = 0.3 SECONDS, transform = M, easing = CUBIC_EASING)
sleep(0.1 SECONDS)
animate(I, alpha = 0, transform = matrix(), time = 0.1 SECONDS)
+
+/atom/movable/proc/do_simple_ranged_interaction(mob/user)
+ return FALSE
diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm
index 5e557bda1d72..2f9d6bbcbad7 100644
--- a/code/game/data_huds.dm
+++ b/code/game/data_huds.dm
@@ -48,7 +48,7 @@
hud_icons = list(ID_HUD)
/datum/atom_hud/data/human/security/advanced
- hud_icons = list(ID_HUD, IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD, WANTED_HUD, NANITE_HUD)
+ hud_icons = list(ID_HUD, IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD, WANTED_HUD, NANITE_HUD, IMPPSI_HUD)
do_silicon_check = TRUE
/datum/atom_hud/data/diagnostic
@@ -196,7 +196,7 @@
/mob/living/proc/sec_hud_set_implants()
var/image/holder
- for(var/i in list(IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD))
+ for(var/i in list(IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD, IMPPSI_HUD))
holder = hud_list[i]
holder.icon_state = null
for(var/obj/item/implant/I in implants)
@@ -210,6 +210,11 @@
var/icon/IC = icon(icon, icon_state, dir)
holder.pixel_y = IC.Height() - world.icon_size
holder.icon_state = "hud_imp_chem"
+ else if(istype(I, /obj/item/implant/psi_control))
+ holder = hud_list[IMPPSI_HUD]
+ var/icon/IC = icon(icon, icon_state, dir)
+ holder.pixel_y = IC.Height() - world.icon_size
+ holder.icon_state = "hud_imp_psi"
if(HAS_TRAIT(src, TRAIT_MINDSHIELD))
holder = hud_list[IMPLOYAL_HUD]
var/icon/IC = icon(icon, icon_state, dir)
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index 0688a16a63fa..55ffc2bb0bab 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -181,6 +181,14 @@
return
..()
+/obj/machinery/door/do_simple_ranged_interaction(mob/user)
+ if(!requiresID() || allowed(null))
+ if(density)
+ open()
+ else
+ close()
+ return TRUE
+
/obj/machinery/door/proc/try_to_activate_door(mob/user)
add_fingerprint(user)
if(operating || (obj_flags & EMAGGED))
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index c7e867c4f8ba..c4b4d60dfb11 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -402,6 +402,11 @@ GLOBAL_DATUM_INIT(welding_sparks, /mutable_appearance, mutable_appearance('icons
R.activate_module(src)
R.hud_used.update_robot_modules_display()
+/obj/item/do_simple_ranged_interaction(mob/user)
+ if(user)
+ attack_self(user)
+ return TRUE
+
/obj/item/proc/GetDeconstructableContents()
return GetAllContents() - src
diff --git a/code/game/objects/items/implants/implant_mindshield.dm b/code/game/objects/items/implants/implant_mindshield.dm
index 5dea8d41aada..f84b57851a87 100644
--- a/code/game/objects/items/implants/implant_mindshield.dm
+++ b/code/game/objects/items/implants/implant_mindshield.dm
@@ -80,6 +80,8 @@
target.mind.remove_antag_datum(/datum/antagonist/gang)
if(target.mind.has_antag_datum(/datum/antagonist/veil))
target.mind.remove_antag_datum(/datum/antagonist/veil)
+ if(target.mind.has_antag_datum(/datum/antagonist/thrall))
+ target.mind.remove_antag_datum(/datum/antagonist/thrall)
if(!silent)
if(target.mind in SSticker.mode.cult)
to_chat(target, span_warning("You feel something interfering with your mental conditioning, but you resist it!"))
diff --git a/code/game/objects/items/implants/implant_psi.dm b/code/game/objects/items/implants/implant_psi.dm
new file mode 100644
index 000000000000..38641fea5718
--- /dev/null
+++ b/code/game/objects/items/implants/implant_psi.dm
@@ -0,0 +1,174 @@
+/obj/item/implant/psi_control
+ name = "psi dampener implant"
+ desc = "A safety implant for registered psi-operants."
+ implant_color = "n"
+ activated = FALSE
+
+ var/overload = 0
+ var/max_overload = 100
+ var/cooldown_rate = 10
+ var/psi_mode = PSI_IMPLANT_AUTOMATIC
+ var/list/logs
+
+/obj/item/implant/psi_control/get_data()
+ var/dat = {"Implant Specifications:
+ Name: Nanotrasen Psionic Mitigation Implant
+ Life: Ten years.
+ Important Notes: Psionic personel injected with this device can have their psionic potental di.
+
+ Implant Details:
+ Function: Contains a small shard of nullglass that prevents those implanted from being able to use psionic powers.
+ Special Features: Will prevent and log the use of psionics.
+ Integrity: Implant will last so long as the device is inside the bloodstream."}
+ return dat
+
+/obj/item/implant/psi_control/Initialize()
+ . = ..()
+ SSpsi.psi_dampeners += src
+ SSpsi.processing += src
+
+/obj/item/implant/psi_control/Destroy()
+ SSpsi.psi_dampeners -= src
+ SSpsi.processing -= src
+ . = ..()
+
+/obj/item/implant/psi_control/process()
+ ..()
+ overload = max(overload - cooldown_rate, 0)
+
+/obj/item/implant/psi_control/disrupts_psionics()
+ if(!imp_in)
+ return FALSE
+ var/use_psi_mode = get_psi_mode()
+ return (use_psi_mode == PSI_IMPLANT_SHOCK || use_psi_mode == PSI_IMPLANT_WARN) ? src : FALSE
+
+/obj/item/implant/psi_control/removed()
+ var/mob/living/M = imp_in
+ if(disrupts_psionics() && istype(M) && M.psi)
+ to_chat(M, span_notice("You feel the chilly shackles around your psionic faculties fade away."))
+ . = ..()
+
+/obj/item/implant/psi_control/proc/update_functionality(silent)
+ var/mob/living/M = imp_in
+ if(silent || !M || !M.psi)
+ return
+ if(get_psi_mode() == PSI_IMPLANT_DISABLED)
+ to_chat(M, span_notice("You feel the chilly shackles around your psionic faculties fade away."))
+ else
+ to_chat(M, span_notice("Bands of hollow ice close themselves around your psionic faculties."))
+
+/obj/item/implant/psi_control/proc/meltdown()
+ overload = 100
+ if(imp_in)
+ report_failure()
+ psi_mode = PSI_IMPLANT_DISABLED
+ update_functionality()
+
+/obj/item/implant/psi_control/proc/get_psi_mode()
+ if(psi_mode == PSI_IMPLANT_AUTOMATIC)
+ switch(get_security_level())
+ if("green")
+ return PSI_IMPLANT_SHOCK
+ if("blue")
+ return PSI_IMPLANT_WARN
+ else
+ return PSI_IMPLANT_LOG
+
+ return psi_mode
+
+/obj/item/implant/psi_control/withstand_psi_stress(stress, atom/source)
+ if(source != imp_in)
+ return
+
+ var/use_psi_mode = get_psi_mode()
+
+ if(use_psi_mode == PSI_IMPLANT_DISABLED)
+ return stress
+
+ . = 0
+
+ if(stress)
+
+ // If we're disrupting psionic attempts at the moment, we might overload.
+ if(disrupts_psionics())
+ var/overload_amount = FLOOR(stress, 10)
+ if(overload_amount)
+ overload += overload_amount
+ if(overload >= 100)
+ if(imp_in)
+ to_chat(imp_in, span_danger("Your psi dampener overloads violently!"))
+ meltdown()
+ update_functionality()
+ return
+ if(imp_in)
+ if(overload >= 75 && overload < 100)
+ to_chat(imp_in, span_danger("Your psi dampener is searing hot!"))
+ else if(overload >= 50 && overload < 75)
+ to_chat(imp_in, span_warning("Your psi dampener is uncomfortably hot..."))
+ else if(overload >= 25 && overload < 50)
+ to_chat(imp_in, span_warning("You feel your psi dampener heating up..."))
+
+ // If all we're doing is logging the incident then just pass back stress without changing it.
+ if(source && source == imp_in)
+ report_violation(stress)
+ switch(use_psi_mode)
+ if(PSI_IMPLANT_LOG)
+ return stress
+ if(PSI_IMPLANT_SHOCK)
+ to_chat(imp_in, span_danger("Your psi dampener punishes you with a violent neural shock!"))
+ imp_in.electrocute_act(5, src)
+ if(isliving(imp_in))
+ var/mob/living/M = imp_in
+ if(M.psi) M.psi.stunned(5)
+ if(PSI_IMPLANT_WARN)
+ to_chat(imp_in, span_warning("Your psi dampener primly informs you it has reported this violation."))
+
+/obj/item/implant/psi_control/proc/report_failure()
+ LAZYADD(logs, "Critical system failure - [imp_in.name].")
+
+/obj/item/implant/psi_control/proc/report_violation(stress)
+ LAZYADD(logs, "Sigma [round(stress/10)] event - [imp_in.name].")
+
+/obj/item/implant/psi_control/psych
+ psi_mode = PSI_IMPLANT_LOG
+
+/obj/item/implanter/psi_control
+ name = "implanter (psi dampener)"
+ imp_type = /obj/item/implant/psi_control
+
+/obj/item/implantcase/psi_control
+ name = "implant case - 'Psi Dampener'"
+ desc = "A glass case containing a psi dampener implant."
+ imp_type = /obj/item/implant/psi_control
+
+/obj/item/implant/nullglass
+ name = "nullglass shard"
+ desc = "A shard of psionic inhibiting glass."
+ implant_color = "n"
+ var/stress_left = 100
+ var/lifespan = 1 MINUTES
+
+/obj/item/implant/nullglass/Initialize()
+ . = ..()
+ QDEL_IN(src, lifespan)
+
+/obj/item/implant/nullglass/disrupts_psionics()
+ if(imp_in)
+ return src
+
+/obj/item/implant/nullglass/withstand_psi_stress(stress, atom/source)
+ if(source != imp_in)
+ return stress
+
+ . = max(stress - stress_left, 0)
+ stress_left -= stress
+ if(imp_in)
+ if(stress_left > 0 && stress_left < 25)
+ to_chat(imp_in, span_danger("You feel a searing hot piece of glass in your body!"))
+ else if(stress_left >= 25 && stress_left < 50)
+ to_chat(imp_in, span_warning("You feel a piece of glass in your body getting uncomfortably hot..."))
+ else if(stress_left >= 50)
+ to_chat(imp_in, span_warning("You feel a piece of glass in your body heating up..."))
+ if(stress_left <= 0)
+ to_chat(imp_in, span_danger("You hear a piece of glass shatter in your body!"))
+ qdel(src)
diff --git a/code/game/objects/items/stacks/sheets/glass.dm b/code/game/objects/items/stacks/sheets/glass.dm
index 35e61cfab913..a791ca432c1e 100644
--- a/code/game/objects/items/stacks/sheets/glass.dm
+++ b/code/game/objects/items/stacks/sheets/glass.dm
@@ -249,6 +249,28 @@ GLOBAL_LIST_INIT(plastitaniumglass_recipes, list(
recipes = GLOB.plastitaniumglass_recipes
return ..()
+GLOBAL_LIST_INIT(nullglass_recipes, list ( \
+ new/datum/stack_recipe("nullglass tile", /obj/item/stack/tile/mineral/nullglass, time = 0), \
+))
+
+/obj/item/stack/sheet/nullglass
+ name = "nullglass"
+ desc = "A glass sheet made out of a strange black glass capable of nullifying magic."
+ singular_name = "nullglass sheet"
+ icon_state = "sheet-nullglass"
+ item_state = "sheet-nullglass"
+ materials = list(/datum/material/glass=MINERAL_MATERIAL_AMOUNT)
+ merge_type = /obj/item/stack/sheet/nullglass
+ grind_results = list(/datum/reagent/crystal = 1)
+ matter_amount = 4
+
+/obj/item/stack/sheet/nullglass/fifty
+ amount = 50
+
+/obj/item/stack/sheet/nullglass/Initialize(mapload, new_amount, merge = TRUE)
+ recipes = GLOB.nullglass_recipes
+ return ..()
+
/obj/item/shard
name = "shard"
desc = "A nasty looking shard of glass."
@@ -355,3 +377,12 @@ GLOBAL_LIST_INIT(plastitaniumglass_recipes, list(
icon_state = "plasmalarge"
materials = list(/datum/material/plasma=MINERAL_MATERIAL_AMOUNT * 0.5, /datum/material/glass=MINERAL_MATERIAL_AMOUNT)
icon_prefix = "plasma"
+
+/obj/item/shard/nullglass
+ name = "null shard"
+ desc = "A nasty looking shard of nullglass."
+ icon_state = "nulllarge"
+ icon_prefix = "null"
+
+/obj/item/shard/nullglass/disrupts_psionics()
+ return src
diff --git a/code/game/objects/items/storage/boxes.dm b/code/game/objects/items/storage/boxes.dm
index bd539c1c2be1..c11c1bc06162 100644
--- a/code/game/objects/items/storage/boxes.dm
+++ b/code/game/objects/items/storage/boxes.dm
@@ -403,6 +403,18 @@
/obj/item/implanter = 1)
generate_items_inside(items_inside,src)
+/obj/item/storage/box/psiimp
+ name = "boxed psi dampener implant kit"
+ desc = "Box full of implants to protect the mentaly gifted."
+ illustration = "implant"
+
+/obj/item/storage/box/psiimp/PopulateContents()
+ var/static/items_inside = list(
+ /obj/item/implantcase/psi_control = 4,
+ /obj/item/implanter = 1,
+ /obj/item/implantpad = 1)
+ generate_items_inside(items_inside,src)
+
/obj/item/storage/box/bodybags
name = "body bags"
desc = "The label indicates that it contains body bags."
@@ -917,6 +929,22 @@
for(var/i in 1 to 7)
new /obj/item/ammo_casing/shotgun/beanbag(src)
+/obj/item/storage/box/nullglass
+ name = "box of nullglass shells"
+ desc = "A box full of beanbag shells designed for shotguns. The box itself is designed for holding any kind of shotgun shell."
+ icon_state = "rubbershot_box"
+ illustration = null
+
+/obj/item/storage/box/nullglass/ComponentInitialize()
+ . = ..()
+ var/datum/component/storage/STR = GetComponent(/datum/component/storage)
+ STR.max_items = 7
+ STR.set_holdable(list(/obj/item/ammo_casing/shotgun))
+
+/obj/item/storage/box/nullglass/PopulateContents()
+ for(var/i in 1 to 7)
+ new /obj/item/ammo_casing/shotgun/nullglass(src)
+
/obj/item/storage/box/actionfigure
name = "box of action figures"
desc = "The latest set of collectable action figures."
diff --git a/code/game/objects/items/storage/firstaid.dm b/code/game/objects/items/storage/firstaid.dm
index ff8a5e9f9a35..0fcfbb87dde0 100644
--- a/code/game/objects/items/storage/firstaid.dm
+++ b/code/game/objects/items/storage/firstaid.dm
@@ -668,6 +668,14 @@
for(var/i in 1 to 5)
new /obj/item/reagent_containers/pill/aranesp(src)
+/obj/item/storage/pill_bottle/three_eye
+ name = "bottle of Three Eye pills"
+ desc = "Highly illegal drug. Stimulates rarely used portions of the brain."
+
+/obj/item/storage/pill_bottle/three_eye/PopulateContents()
+ for(var/i in 1 to 5)
+ new /obj/item/reagent_containers/pill/three_eye(src)
+
/obj/item/storage/pill_bottle/psicodine
name = "bottle of psicodine pills"
desc = "Contains pills used to treat mental distress and traumas."
diff --git a/code/game/objects/items/twohanded.dm b/code/game/objects/items/twohanded.dm
index 91984b0bf09d..2f91134ea2fe 100644
--- a/code/game/objects/items/twohanded.dm
+++ b/code/game/objects/items/twohanded.dm
@@ -972,3 +972,15 @@
/obj/item/twohanded/bamboospear/update_icon()
icon_state = "bamboo_spear[wielded]"
+
+/*
+ * Nullglass Spear
+ */
+/obj/item/twohanded/spear/nullglass
+ name = "nullglass spear"
+ icon_state = "spearnull0"
+ icon_prefix = "spearnull"
+ var/psi_stress = 0
+
+/obj/item/twohanded/spear/nullglass/disrupts_psionics()
+ return src
diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm
index 0148baf7ed5d..d44d7e5a8edd 100644
--- a/code/game/objects/items/weaponry.dm
+++ b/code/game/objects/items/weaponry.dm
@@ -238,6 +238,25 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301
block_chance = 30
armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 0, RAD = 0, FIRE = 100, ACID = 50)
+/obj/item/claymore/nullglass
+ name = "nullglass claymore"
+ icon_state = "claymore_nullglass"
+ item_state = "claymore_nullglass"
+ force = 20
+ throwforce = 5
+ block_chance = 15
+ var/shatter_chance = 30
+
+/obj/item/claymore/nullglass/disrupts_psionics()
+ return src
+
+/obj/item/claymore/nullglass/attack(mob/living/target, mob/living/user)
+ . = ..()
+ if(prob(shatter_chance))
+ var/obj/item/implant/nullglass/imp = new()
+ imp.implant(target)
+ playsound(loc, 'sound/effects/glass_step.ogg', 30, TRUE)
+
/obj/item/katana
name = "katana"
desc = "Woefully underpowered in D20."
diff --git a/code/game/objects/structures/bedsheet_bin.dm b/code/game/objects/structures/bedsheet_bin.dm
index b4210e5ea00d..6e1083cb1aa0 100644
--- a/code/game/objects/structures/bedsheet_bin.dm
+++ b/code/game/objects/structures/bedsheet_bin.dm
@@ -444,6 +444,29 @@ LINEN BINS
add_fingerprint(user)
+/obj/structure/bedsheetbin/do_simple_ranged_interaction(mob/user)
+ if(amount >= 1)
+ amount--
+
+ var/obj/item/bedsheet/B
+ if(sheets.len)
+ B = sheets[sheets.len]
+ sheets.Remove(B)
+
+ else
+ B = new /obj/item/bedsheet(loc)
+
+ B.forceMove(drop_location())
+ to_chat(user, span_notice("You telekinetically remove [B] from [src]."))
+ update_icon()
+
+ if(hidden)
+ hidden.forceMove(drop_location())
+ hidden = null
+
+
+ add_fingerprint(user)
+
/obj/item/bedsheet/adjusted
slot_flags = ITEM_SLOT_HEAD
flags_inv = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEGLOVES|HIDEJUMPSUIT|HIDENECK|HIDEFACIALHAIR|HIDESUITSTORAGE
diff --git a/code/game/objects/structures/extinguisher.dm b/code/game/objects/structures/extinguisher.dm
index 0d3f46a82036..13da01d6e833 100644
--- a/code/game/objects/structures/extinguisher.dm
+++ b/code/game/objects/structures/extinguisher.dm
@@ -103,6 +103,16 @@
else
toggle_cabinet(user)
+/obj/structure/extinguisher_cabinet/do_simple_ranged_interaction(mob/user)
+ if(stored_extinguisher)
+ stored_extinguisher.forceMove(loc)
+ stored_extinguisher = null
+ opened = 1
+ playsound(loc, 'sound/machines/click.ogg', 15, 1, -3)
+ update_icon()
+ else
+ toggle_cabinet(user)
+
/obj/structure/extinguisher_cabinet/attack_paw(mob/user)
return attack_hand(user)
diff --git a/code/game/turfs/simulated/walls.dm b/code/game/turfs/simulated/walls.dm
index 7000e3d4ef7b..584500854de2 100644
--- a/code/game/turfs/simulated/walls.dm
+++ b/code/game/turfs/simulated/walls.dm
@@ -227,7 +227,22 @@
return FALSE
/turf/closed/wall/proc/try_decon(obj/item/I, mob/user, turf/T)
- if(I.tool_behaviour == TOOL_WELDER)
+ if(istype(I, /obj/item/psychic_power/psiblade))
+ var/obj/item/psychic_power/psiblade/blade = I
+ if(!blade.can_break_wall)
+ return
+ to_chat(user, span_notice("You sink [blade] into [src] and begin trying to rip out the support frame..."))
+ playsound(src, 'sound/items/Welder.ogg', 100, 1)
+
+ if(!do_after(user, blade.wall_break_time, src))
+ return
+
+ to_chat(user, span_notice("You tear through [src]'s support system and plating!"))
+ dismantle_wall(TRUE)
+ user.visible_message(span_warning("[src] was torn open by [user]!"))
+ playsound(src, 'sound/items/Welder.ogg', 100, 1)
+
+ else if(I.tool_behaviour == TOOL_WELDER)
if(!I.tool_start_check(user, amount=0))
return FALSE
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index bcb43b841bc7..77aecb995984 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -133,6 +133,26 @@
body += "Redeem Antag Token | "
body += "See Antag Tokens"
+ body += "
"
+ body += "Psionics:
"
+ if(isliving(M))
+ var/mob/living/psyker = M
+ if(psyker.psi)
+ body += "Remove psionics.
"
+ body += "Trigger latencies.
"
+ body += ""
+ for(var/faculty in list(PSI_COERCION, PSI_PSYCHOKINESIS, PSI_REDACTION, PSI_ENERGISTICS))
+ var/datum/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty)
+ var/faculty_rank = psyker.psi ? psyker.psi.get_rank(faculty) : 0
+ body += "| [faculty_decl.name] | "
+ for(var/i = 1 to LAZYLEN(GLOB.psychic_ranks_to_strings))
+ var/psi_title = GLOB.psychic_ranks_to_strings[i]
+ if(i == faculty_rank)
+ psi_title = "[psi_title]"
+ body += "[psi_title] | "
+ body += "
"
+ body += "
"
+
if (M.client)
if(!isnewplayer(M))
body += "
"
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 781db60b883d..fe61199039a6 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -950,6 +950,20 @@
Game() // updates the main game menu
HandleFSecret()
+ else if(href_list["remove_psionics"])
+ var/datum/psi_complexus/psi = locate(href_list["remove_psionics"])
+ if(psi?.owner && !QDELETED(psi))
+ to_chat(psi.owner, span_notice("Your psionic powers vanish abruptly, leaving you cold and empty."))
+ log_admin("[key_name(usr)] removed all psionics from [key_name(psi.owner)].")
+ message_admins(span_adminnotice("[key_name_admin(usr)] removed all psionics from [key_name(psi.owner)]."))
+ QDEL_NULL(psi)
+
+ else if(href_list["trigger_psi_latencies"])
+ var/datum/psi_complexus/psi = locate(href_list["trigger_psi_latencies"])
+ log_admin("[key_name(usr)] triggered psi latencies for [key_name(psi.owner)].")
+ message_admins(span_adminnotice("[key_name_admin(usr)] triggered psi latencies for [key_name(psi.owner)]."))
+ psi.check_latency_trigger(100, "outside intervention", redactive = TRUE)
+
else if(href_list["monkeyone"])
if(!check_rights(R_SPAWN))
return
diff --git a/code/modules/antagonists/paramount/paramount.dm b/code/modules/antagonists/paramount/paramount.dm
new file mode 100644
index 000000000000..ad3c4a66454c
--- /dev/null
+++ b/code/modules/antagonists/paramount/paramount.dm
@@ -0,0 +1,99 @@
+/datum/antagonist/paramount
+ name = "Paramount"
+ roundend_category = "paramounts"
+ antagpanel_category = "Paramount"
+ job_rank = ROLE_PARAMOUNT
+ antag_moodlet = /datum/mood_event/focused
+
+/datum/antagonist/paramount/on_gain()
+ var/mob/living/carbon/human/H = owner?.current
+ if(!istype(H))
+ return
+
+ H.set_psi_rank(PSI_REDACTION, 3, defer_update = TRUE)
+ H.set_psi_rank(PSI_COERCION, 3, defer_update = TRUE)
+ H.set_psi_rank(PSI_PSYCHOKINESIS, 3, defer_update = TRUE)
+ H.set_psi_rank(PSI_ENERGISTICS, 3, defer_update = TRUE)
+ H.psi.update(TRUE)
+
+ H.equipOutfit(/datum/outfit/paramount)
+ addObjectives()
+ . = ..()
+/* Somehow trying to add this broke every single vent in the game so ???
+ hud_add()
+
+/datum/antagonist/paramount/on_removal()
+ . = ..()
+ hud_remove()
+
+/datum/antagonist/paramount/proc/hud_add()
+ var/datum/atom_hud/antag/hud = GLOB.huds[ANTAG_HUD_PARAMOUNT]
+ hud.join_hud(owner.current)
+ set_antag_hud(owner.current, "paramount")
+
+/datum/antagonist/paramount/proc/hud_remove()
+ var/datum/atom_hud/antag/hud = GLOB.huds[ANTAG_HUD_PARAMOUNT]
+ hud.leave_hud(owner.current)
+ set_antag_hud(owner.current, null)
+*/
+/datum/antagonist/paramount/proc/addObjectives()
+ switch(rand(1,100))
+ if(1 to 30)
+ var/datum/objective/assassinate/kill_objective = new
+ kill_objective.owner = owner
+ kill_objective.find_target()
+ objectives += kill_objective
+
+ if (!(locate(/datum/objective/escape) in objectives))
+ var/datum/objective/escape/escape_objective = new
+ escape_objective.owner = owner
+ objectives += escape_objective
+
+ if(31 to 60)
+ var/datum/objective/steal/steal_objective = new
+ steal_objective.owner = owner
+ steal_objective.find_target()
+ objectives += steal_objective
+
+ if (!(locate(/datum/objective/escape) in objectives))
+ var/datum/objective/escape/escape_objective = new
+ escape_objective.owner = owner
+ objectives += escape_objective
+
+ if(61 to 85)
+ var/datum/objective/assassinate/kill_objective = new
+ kill_objective.owner = owner
+ kill_objective.find_target()
+ objectives += kill_objective
+
+ var/datum/objective/steal/steal_objective = new
+ steal_objective.owner = owner
+ steal_objective.find_target()
+ objectives += steal_objective
+
+ if (!(locate(/datum/objective/survive) in objectives))
+ var/datum/objective/survive/survive_objective = new
+ survive_objective.owner = owner
+ objectives += survive_objective
+
+ else
+ if (!(locate(/datum/objective/hijack) in objectives))
+ var/datum/objective/hijack/hijack_objective = new
+ hijack_objective.owner = owner
+ objectives += hijack_objective
+
+/datum/antagonist/paramount/greet()
+ to_chat(owner, span_boldannounce("You are the Paramount!"))
+ to_chat(owner, "You were once one of the finest minds of your culture, now driven to madness by the whispers of the howling dark and blessed with psychic faculties that defy understanding.")
+ to_chat(owner, "Using your C-E rig and your twisted knowledge of psionics, advance your agenda in human space by doing the following tasks:")
+ owner.announce_objectives()
+ to_chat(owner,"Remember: do not forget to prepare your psi amp.")
+
+/datum/outfit/paramount
+ name = "Paramount"
+ uniform = /obj/item/clothing/under/color/lightpurple
+ suit = /obj/item/clothing/suit/wizrobe/fake
+ glasses = /obj/item/clothing/glasses/regular
+ head = /obj/item/clothing/head/helmet/space/psi_amp/paramount
+ shoes = /obj/item/clothing/shoes/sneakers/black
+ ears = /obj/item/radio/headset
diff --git a/code/modules/antagonists/thrall/thrall.dm b/code/modules/antagonists/thrall/thrall.dm
new file mode 100644
index 000000000000..61fe285da861
--- /dev/null
+++ b/code/modules/antagonists/thrall/thrall.dm
@@ -0,0 +1,21 @@
+/datum/antagonist/thrall
+ name = "Thrall"
+ roundend_category = "other"
+ antagpanel_category = "Paramount"
+ var/datum/mind/master
+
+/datum/antagonist/thrall/antag_panel_data()
+ return "Master : [master.name]"
+
+/datum/antagonist/thrall/on_gain()
+ if(!master)
+ return // Someone is playing with buttons they shouldn't be.
+ ..()
+ var/datum/objective/obey = new
+ obey.owner = owner
+ obey.explanation_text = "Obey your master, [master.name], in all things."
+ obey.completed = TRUE
+ objectives |= obey
+
+/datum/antagonist/thrall/greet()
+ to_chat(owner, "Your mind is no longer solely your own, your will has been subjugated by that of [master.name]. Obey them in all things.")
diff --git a/code/modules/client/client_colour.dm b/code/modules/client/client_colour.dm
index f1477fb4d2c2..146287c2db0d 100644
--- a/code/modules/client/client_colour.dm
+++ b/code/modules/client/client_colour.dm
@@ -111,3 +111,7 @@
/datum/client_colour/monochrome
colour = list(rgb(77,77,77), rgb(150,150,150), rgb(28,28,28), rgb(0,0,0))
priority = INFINITY //we can't see colors anyway!
+
+/datum/client_colour/thirdeye
+ colour = list(rgb(16,16,16), rgb(32,32,32), rgb(80,80,80))
+ priority = 300
diff --git a/code/modules/clothing/outfits/ert.dm b/code/modules/clothing/outfits/ert.dm
index d39cbc6f973a..2aeb189014fa 100644
--- a/code/modules/clothing/outfits/ert.dm
+++ b/code/modules/clothing/outfits/ert.dm
@@ -184,11 +184,13 @@
/datum/outfit/ert/commander/inquisitor
name = "Inquisition Commander"
suit = /obj/item/clothing/suit/space/hardsuit/ert/paranormal
- belt = /obj/item/nullrod/scythe/talking/chainsword
+ belt = /obj/item/claymore/nullglass
suit_store = /obj/item/gun/energy/e_gun
mask = /obj/item/clothing/mask/gas/sechailer
backpack_contents = list(
/obj/item/storage/box/engineer=1,
+ /obj/item/gun/ballistic/revolver/nullglass=1,
+ /obj/item/ammo_box/a357/nullglass=1,
/obj/item/assembly/flash/handheld=1,
/obj/item/grenade/flashbang=1,
/obj/item/reagent_containers/spray/pepper=1
diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm
index c7a391b36a9e..5dde345e7b79 100644
--- a/code/modules/jobs/job_types/_job.dm
+++ b/code/modules/jobs/job_types/_job.dm
@@ -135,6 +135,11 @@
H.dna.species.after_equip_job(src, H, visualsOnly)
+ if(H.psi && H.psi.has_rank_above(PSI_RANK_OPERANT))
+ var/obj/item/implant/psi_control/I = new(H)
+ if(!I.implant(H, null))
+ qdel(I) // For odd casses like the psych
+
if(!visualsOnly && announce)
announce(H)
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index dd22356ae1fa..de55e369d34a 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -305,12 +305,15 @@
if(restrained())
changeNext_move(CLICK_CD_BREAKOUT)
last_special = world.time + CLICK_CD_BREAKOUT
- var/buckle_cd = 600
+ var/buckle_cd = 1 MINUTES
+ if(psi?.can_use())
+ buckle_cd = max(0, buckle_cd - ((10 SECONDS) * psi.get_rank(PSI_PSYCHOKINESIS)))
+
if(handcuffed)
var/obj/item/restraints/O = src.get_item_by_slot(SLOT_HANDCUFFED)
buckle_cd = O.breakouttime
visible_message(span_warning("[src] attempts to unbuckle [p_them()]self!"), \
- span_notice("You attempt to unbuckle yourself... (This will take around [round(buckle_cd/10,1)] second\s, and you need to stay still.)"))
+ span_notice("You attempt to unbuckle yourself... (This will take around [DisplayTimeText(buckle_cd)] second\s, and you need to stay still.)"))
if(do_after(src, buckle_cd, src, FALSE))
if(!buckled)
return
@@ -353,12 +356,17 @@
cuff_resist(I)
-/mob/living/carbon/proc/cuff_resist(obj/item/I, breakouttime = 600, cuff_break = 0)
+/mob/living/carbon/proc/cuff_resist(obj/item/I, breakouttime = 60 SECONDS, cuff_break = 0)
if(I.item_flags & BEING_REMOVED)
to_chat(src, span_warning("You're already attempting to remove [I]!"))
return
I.item_flags |= BEING_REMOVED
breakouttime = I.breakouttime
+
+ if(psi?.can_use())
+ var/psi_mod = (1 - (psi.get_rank(PSI_PSYCHOKINESIS)*0.2))
+ breakouttime = max(5, breakouttime * psi_mod)
+
if(!cuff_break)
visible_message(span_warning("[src] attempts to remove [I]!"))
to_chat(src, span_notice("You attempt to remove [I]... (This will take around [DisplayTimeText(breakouttime)] and you need to stand still.)"))
@@ -368,9 +376,9 @@
to_chat(src, span_warning("You fail to remove [I]!"))
else if(cuff_break == FAST_CUFFBREAK)
- breakouttime = 50
+ breakouttime = breakouttime / 12
visible_message(span_warning("[src] is trying to break [I]!"))
- to_chat(src, span_notice("You attempt to break [I]... (This will take around 5 seconds and you need to stand still.)"))
+ to_chat(src, span_notice("You attempt to break [I]... (This will take around [DisplayTimeText(breakouttime)] and you need to stand still.)"))
if(do_after(src, breakouttime, src, FALSE))
clear_cuffs(I, cuff_break)
else
diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm
index 43ddff5b7d4d..49e58eb82669 100644
--- a/code/modules/mob/living/carbon/carbon_defense.dm
+++ b/code/modules/mob/living/carbon/carbon_defense.dm
@@ -604,3 +604,23 @@
user.visible_message(span_danger("[user] grasps at [user.p_their()] [grasped_part.name], trying to stop the bleeding."), span_notice("You grab hold of your [grasped_part.name] tightly."), vision_distance=COMBAT_MESSAGE_RANGE)
playsound(get_turf(src), 'sound/weapons/thudswoosh.ogg', 50, TRUE, -1)
return TRUE
+
+/// Exploads the head of the mob
+/mob/living/carbon/proc/explode_head(delete_brain)
+ var/obj/item/bodypart/head = get_bodypart(BODY_ZONE_HEAD)
+ var/obj/item/organ/brain/brain = getorganslot(ORGAN_SLOT_BRAIN)
+ if(!istype(head))
+ return FALSE
+ if(delete_brain && (brain in head.get_organs()))
+ qdel(brain)
+ head.drop_limb()
+ head.drop_organs(src, TRUE)
+ qdel(head)
+ spawn_gibs()
+
+/// Causes the mob to have a seizure
+/mob/living/carbon/proc/seizure(unconscious = 20 SECONDS, jitter = 1 SECONDS)
+ visible_message(span_danger("[src] starts having a seizure!"))
+ Unconscious(unconscious)
+ Jitter(jitter)
+ SEND_SIGNAL(src, COMSIG_ADD_MOOD_EVENT, "seizure", /datum/mood_event/epilepsy)
diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm
index 6c772157d81c..d76c70da523e 100644
--- a/code/modules/mob/living/carbon/human/examine.dm
+++ b/code/modules/mob/living/carbon/human/examine.dm
@@ -306,7 +306,7 @@
if(stun_absorption[i]["end_time"] > world.time && stun_absorption[i]["examine_message"])
msg += "[t_He] [t_is][stun_absorption[i]["examine_message"]]\n"
- if(!glasses && mind && mind.has_antag_datum(ANTAG_DATUM_THRALL))
+ if(!glasses && mind && mind.has_antag_datum(ANTAG_DATUM_SHADOWTHRALL))
if(getorganslot(ORGAN_SLOT_EYES))
msg += "[t_His] eyes seem unnaturally dark and soulless.\n" // I'VE BECOME SO NUMB, I CAN'T FEEL YOU THERE
else
@@ -561,7 +561,7 @@
msg += "[t_He] [p_do()]n't seem all there.\n"
- if(!glasses && mind && mind.has_antag_datum(ANTAG_DATUM_THRALL))
+ if(!glasses && mind && mind.has_antag_datum(ANTAG_DATUM_SHADOWTHRALL))
msg += "[t_His] eyes seem unnaturally dark and soulless.\n" // I'VE BECOME SO NUMB, I CAN'T FEEL YOU THERE
if (length(msg))
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index 5ee02f4c890b..803670d5e14f 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -59,6 +59,12 @@
if(spec_return)
return spec_return
+ if((!P.disrupts_psionics() && psi && psi.handle_block_chance(P) && psi.spend_power(round(P.damage/4), round(P.damage/20))))
+ P.firer = src
+ P.setAngle(rand(0, 360))
+ visible_message(span_danger("[src] deflects [P]!"))
+ return BULLET_ACT_FORCE_PIERCE
+
if(mind)
if(mind.martial_art && !incapacitated(FALSE, TRUE) && mind.martial_art.can_use(src) && (mind.martial_art.deflection_chance || ((mind.martial_art.id == "sleeping carp") && in_throw_mode))) //Some martial arts users can deflect projectiles!
if(prob(mind.martial_art.deflection_chance) || ((mind.martial_art.id == "sleeping carp") && in_throw_mode)) // special check if sleeping carp is our martial art and throwmode is on, deflect
@@ -124,7 +130,6 @@
/mob/living/carbon/human/proc/check_shields(atom/AM, var/damage, attack_text = "the attack", attack_type = MELEE_ATTACK, armour_penetration = 0)
var/block_chance_modifier = round(damage / -3)
-
for(var/obj/item/I in held_items)
if(!istype(I, /obj/item/clothing))
var/final_block_chance = I.block_chance - (clamp((armour_penetration-I.armour_penetration)/2,0,100)) + block_chance_modifier //So armour piercing blades can still be parried by other blades, for example
diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm
index 3051debb2b19..b35913182555 100644
--- a/code/modules/mob/living/carbon/human/human_defines.dm
+++ b/code/modules/mob/living/carbon/human/human_defines.dm
@@ -1,5 +1,5 @@
/mob/living/carbon/human
- hud_possible = list(HEALTH_HUD,STATUS_HUD,ID_HUD,WANTED_HUD,IMPLOYAL_HUD,IMPCHEM_HUD,IMPTRACK_HUD, NANITE_HUD, DIAG_NANITE_FULL_HUD,ANTAG_HUD,GLAND_HUD,SENTIENT_DISEASE_HUD)
+ hud_possible = list(HEALTH_HUD,STATUS_HUD,ID_HUD,WANTED_HUD,IMPLOYAL_HUD,IMPCHEM_HUD, IMPTRACK_HUD, IMPPSI_HUD, NANITE_HUD, DIAG_NANITE_FULL_HUD,ANTAG_HUD,GLAND_HUD,SENTIENT_DISEASE_HUD)
hud_type = /datum/hud/human
possible_a_intents = list(INTENT_HELP, INTENT_DISARM, INTENT_GRAB, INTENT_HARM)
pressure_resistance = 25
diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm
index 67b30379a50e..09bfaba8140a 100644
--- a/code/modules/mob/living/carbon/human/species.dm
+++ b/code/modules/mob/living/carbon/human/species.dm
@@ -159,6 +159,14 @@ GLOBAL_LIST_EMPTY(mentor_races)
//The component to add when swimming
var/swimming_component = /datum/component/swimming
+
+ // Psi Stuff
+ /// Prob chance that mobs of this species have latent psionics
+ var/latency_chance = 1
+ /// List of faculties that can be chosen for random psionics
+ var/possable_faculties = list(PSI_COERCION, PSI_PSYCHOKINESIS, PSI_REDACTION, PSI_ENERGISTICS)
+ /// What level starting faculties are at
+ var/starting_psi_level = PSI_RANK_LATENT
///////////
// PROCS //
@@ -416,6 +424,9 @@ GLOBAL_LIST_EMPTY(mentor_races)
C.add_movespeed_modifier(MOVESPEED_ID_SPECIES, TRUE, 100, override=TRUE, multiplicative_slowdown=speedmod, movetypes=(~FLYING))
+ if(!C.psi && prob(latency_chance))
+ C.set_psi_rank(pick(possable_faculties), starting_psi_level)
+
SEND_SIGNAL(C, COMSIG_SPECIES_GAIN, src, old_species)
diff --git a/code/modules/mob/living/carbon/human/species_types/ethereal.dm b/code/modules/mob/living/carbon/human/species_types/ethereal.dm
index 1795216f040a..2ccdd21de2dd 100644
--- a/code/modules/mob/living/carbon/human/species_types/ethereal.dm
+++ b/code/modules/mob/living/carbon/human/species_types/ethereal.dm
@@ -34,6 +34,7 @@
hair_color = "fixedmutcolor"
hair_alpha = 140
swimming_component = /datum/component/swimming/ethereal
+ possable_faculties = list(PSI_ENERGISTICS)
var/current_color
var/EMPeffect = FALSE
var/emageffect = FALSE
diff --git a/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm b/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm
index 739d6fa7e1de..c9b972829b60 100644
--- a/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm
+++ b/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm
@@ -31,6 +31,7 @@
mutanttail = /obj/item/organ/tail/polysmorph
mutantlungs = /obj/item/organ/lungs/xeno
changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_MAGIC | MIRROR_PRIDE | ERT_SPAWN | RACE_SWAP | SLIME_EXTRACT
+ latency_chance = 2
/datum/species/polysmorph/random_name(gender,unique,lastname)
if(unique)
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index b5d19fa4f8d1..8a984527d842 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -6,6 +6,10 @@
if(status_flags & GODMODE)
visible_message(span_danger("A strange force protects [src], [p_they()] can't be damaged!"), span_userdanger("A strange force protects you!"))
return armor
+ if(psi?.use_psi_armour && psi.last_armor_check == world.time)
+ show_message(span_warning("You block the blow with your mind!"))
+ psi.spend_power(10)
+ return 100
if(armor > 0 && armour_penetration)
armor = max(0, armor - armour_penetration)
if(penetrated_text)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 4ce8a9c755ee..2f3bb54da864 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -633,6 +633,8 @@
if(I)
I.attack_self(src)
update_inv_hands()
+ else
+ attack_empty_hand(active_hand_index)
/**
* Get the notes of this mob
diff --git a/code/modules/modular_computers/file_system/programs/psi_monitor.dm b/code/modules/modular_computers/file_system/programs/psi_monitor.dm
new file mode 100644
index 000000000000..0304365a2c32
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/psi_monitor.dm
@@ -0,0 +1,124 @@
+/*
+/datum/computer_file/program/psimonitor
+ filename = "psimonitor"
+ filedesc = "Psi Monitor"
+ category = PROGRAM_CATEGORY_CREW
+ program_icon_state = "comm_monitor"
+ extended_desc = "This program monitors and configures implanted psi monitors."
+ size = 6
+ requires_ntnet = TRUE
+ usage_flags = PROGRAM_CONSOLE | PROGRAM_LAPTOP | PROGRAM_TABLET | PROGRAM_PHONE | PROGRAM_TELESCREEN
+ transfer_access = ACCESS_MEDICAL
+ available_on_ntnet = TRUE
+ tgui_id = "NtosPsiMonitor"
+ var/obj/item/implant/psi_control/selected_implant
+ var/show_violations = FALSE
+ var/authorized
+
+/obj/machinery/psi_monitor/New()
+ SSpsi.psi_monitors += src
+ ..()
+/*
+/obj/machinery/psi_monitor/emag_act(var/remaining_charges, var/mob/user)
+ if(!emagged)
+ emagged = TRUE
+ remaining_charges--
+ req_one_access.Cut()
+ to_chat(user, "You short out the access protocols.")
+ return TRUE
+ return FALSE
+*/
+
+/datum/computer_file/program/psimonitor/ui_act(action, params)
+ if(..())
+ return
+ computer.play_interact_sound()
+ switch(action)
+ if("login")
+ var/obj/item/card/id/ID = usr.GetID()
+ if(!ID || (transfer_access in ID.GetAccess()))
+ to_chat(usr, span_warning("Access denied."))
+ else
+ authorized = "[ID.registered_name] ([ID.assignment])"
+ return TRUE
+ if("logout")
+ authorized = FALSE
+ return TRUE
+ if("change_mode")
+ selected_implant.psi_mode = input("Select a new implant mode.", "Psi Dampener") as null|anything in list(PSI_IMPLANT_AUTOMATIC, PSI_IMPLANT_SHOCK, PSI_IMPLANT_WARN, PSI_IMPLANT_LOG, PSI_IMPLANT_DISABLED)
+ return TRUE
+ /*
+ if("remove_violation")
+ var/remove_ind = text2num(href_list["remove_violation"])
+ if(remove_ind > 0 && remove_ind <= psi_violations.len)
+ psi_violations.Cut(remove_ind, remove_ind++)
+ return TRUE
+ */
+ if("change_mode")
+ selected_implant.psi_mode = input("Select a new implant mode.", "Psi Dampener") as null|anything in list(PSI_IMPLANT_AUTOMATIC, PSI_IMPLANT_SHOCK, PSI_IMPLANT_WARN, PSI_IMPLANT_LOG, PSI_IMPLANT_DISABLED)
+ return TRUE
+
+/datum/computer_file/program/psimonitor/ui_data(mob/user)
+ if(!SSnetworks.station_network)
+ return
+ var/list/data = get_header_data()
+
+ data["authorized"] = authorized
+ data["ntnetrelays"] = SSnetworks.station_network.relays.len
+ data["idsstatus"] = SSnetworks.station_network.intrusion_detection_enabled
+ data["idsalarm"] = SSnetworks.station_network.intrusion_detection_alarm
+
+ data["config_softwaredownload"] = SSnetworks.station_network.setting_softwaredownload
+ data["config_peertopeer"] = SSnetworks.station_network.setting_peertopeer
+ data["config_communication"] = SSnetworks.station_network.setting_communication
+ data["config_systemcontrol"] = SSnetworks.station_network.setting_systemcontrol
+
+ data["logs"] = list()
+ if(selected_implant.logs)
+ data["logs"] = selected_implant.logs
+
+ for(var/i in SSnetworks.station_network.logs)
+ data["ntnetlogs"] += list(list("entry" = i))
+ data["ntnetmaxlogs"] = SSnetworks.station_network.setting_maxlogcount
+/*
+/obj/machinery/psi_monitor/interact(var/mob/user)
+
+ var/list/dat = list()
+ dat += "Psi Dampener Monitor
"
+ if(authorized)
+ dat += "[authorized] Logout"
+ else
+ dat += "Login"
+
+ dat += "Active Psionic Dampeners
"
+ dat += ""
+ dat += "| Operant | System load | Mode |
"
+ for(var/thing in SSpsi.psi_dampeners)
+ var/obj/item/weapon/implant/psi_control/implant = thing
+ if(!implant.imp_in)
+ continue
+ dat += "| [implant.imp_in.name] | "
+ if(implant.malfunction)
+ dat += "ERROR | ERROR | "
+ else
+ dat += "[implant.overload]% | [authorized ? "[implant.psi_mode]" : "[implant.psi_mode]"] | "
+ dat += "
"
+ dat += "
"
+
+ if(show_violations)
+ dat += "Psionic Control Violations -
"
+ if(psi_violations.len)
+ for(var/i = 1 to psi_violations.len)
+ var/entry = psi_violations[i]
+ dat += " [entry] | [authorized ? "Remove" : ""] |
"
+ else
+ dat += "| None reported. |
"
+ dat += "
"
+ else
+ dat += "Psionic Control Violations +
"
+
+ var/datum/browser/popup = new(user, "psi_monitor_\ref[src]", "Psi-Monitor")
+ popup.set_content(jointext(dat,null))
+ popup.open()
+*/
+*/
diff --git a/code/modules/projectiles/ammunition/ballistic/revolver.dm b/code/modules/projectiles/ammunition/ballistic/revolver.dm
index 2e1b33c43950..338261116f7a 100644
--- a/code/modules/projectiles/ammunition/ballistic/revolver.dm
+++ b/code/modules/projectiles/ammunition/ballistic/revolver.dm
@@ -6,6 +6,11 @@
caliber = "357"
projectile_type = /obj/item/projectile/bullet/a357
+/obj/item/ammo_casing/a357/nullglass
+ name = ".357 NULL bullet casing"
+ desc = "A .357 NULL bullet casing."
+ projectile_type = /obj/item/projectile/bullet/a357/nullglass
+
// 7.62x38mmR (Nagant Revolver)
/obj/item/ammo_casing/n762
diff --git a/code/modules/projectiles/ammunition/ballistic/shotgun.dm b/code/modules/projectiles/ammunition/ballistic/shotgun.dm
index 06bb0275a487..5aba450e6743 100644
--- a/code/modules/projectiles/ammunition/ballistic/shotgun.dm
+++ b/code/modules/projectiles/ammunition/ballistic/shotgun.dm
@@ -144,6 +144,14 @@
pellets = 4
variance = 35
+/obj/item/ammo_casing/shotgun/nullglass
+ name = "nullglass buckshot shell"
+ desc = "A buckshot shell loaded with shells of nullglass that disrupt psionic."
+ icon_state = "mshell" // Temp
+ projectile_type = /obj/item/projectile/bullet/pellet/nullglass
+ pellets = 6
+ variance = 25
+
/obj/item/ammo_casing/shotgun/techshell
name = "unloaded technological shell"
desc = "A high-tech shotgun shell which can be loaded with materials to produce unique effects."
diff --git a/code/modules/projectiles/boxes_magazines/ammo_boxes.dm b/code/modules/projectiles/boxes_magazines/ammo_boxes.dm
index f90e12baa0d4..9b5f70981fbb 100644
--- a/code/modules/projectiles/boxes_magazines/ammo_boxes.dm
+++ b/code/modules/projectiles/boxes_magazines/ammo_boxes.dm
@@ -6,6 +6,13 @@
max_ammo = 7
multiple_sprites = AMMO_BOX_PER_BULLET
+/obj/item/ammo_box/a357/nullglass
+ name = "speed loader (.357 NULL)"
+ desc = "A seven-shot speed loader designed for .357 revolvers. \
+ These rounds trade damage for the ability to disrupt psionics."
+ icon_state = "357null"
+ ammo_type = /obj/item/ammo_casing/a357/nullglass
+
/obj/item/ammo_box/c38
name = "speed loader (.38)"
desc = "A six-shot speed loader designed for .38 revolvers."
diff --git a/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm b/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm
index a1d16826ccfe..38edfd067dde 100644
--- a/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm
+++ b/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm
@@ -58,4 +58,7 @@
for(var/i = 1, i <= max_ammo, i++)
if(!give_round(new load_type(src)))
break
- update_icon()
\ No newline at end of file
+ update_icon()
+
+/obj/item/ammo_box/magazine/internal/cylinder/nullglass
+ ammo_type = /obj/item/ammo_casing/a357/nullglass
diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm
index 2e3ea0e98137..eb64c9ee5a22 100644
--- a/code/modules/projectiles/guns/ballistic/revolver.dm
+++ b/code/modules/projectiles/guns/ballistic/revolver.dm
@@ -64,6 +64,9 @@
if (current_skin)
. += "It can be spun with alt+click"
+/obj/item/gun/ballistic/revolver/nullglass
+ mag_type = /obj/item/ammo_box/magazine/internal/cylinder/nullglass
+
/obj/item/gun/ballistic/revolver/detective
name = "\improper Colt Detective Special"
desc = "A classic, if not outdated, law enforcement firearm. Uses .38-special rounds."
diff --git a/code/modules/projectiles/projectile/bullets/revolver.dm b/code/modules/projectiles/projectile/bullets/revolver.dm
index ccfdafa87718..a05f2be02200 100644
--- a/code/modules/projectiles/projectile/bullets/revolver.dm
+++ b/code/modules/projectiles/projectile/bullets/revolver.dm
@@ -78,3 +78,17 @@
name = ".357 bullet"
damage = 60
wound_bonus = -70
+
+/obj/item/projectile/bullet/a357/nullglass
+ name = ".357 NULL bullet"
+ damage = 30
+
+/obj/item/projectile/bullet/a357/nullglass/disrupts_psionics()
+ return src
+
+/obj/item/projectile/bullet/a357/nullglass/on_hit(atom/target)
+ . = ..()
+ if(prob(50))
+ var/obj/item/implant/nullglass/imp = new()
+ imp.implant(target)
+ playsound(loc, 'sound/effects/glass_step.ogg', 30, TRUE)
diff --git a/code/modules/projectiles/projectile/bullets/shotgun.dm b/code/modules/projectiles/projectile/bullets/shotgun.dm
index 8b38bf93471a..0003ad623951 100644
--- a/code/modules/projectiles/projectile/bullets/shotgun.dm
+++ b/code/modules/projectiles/projectile/bullets/shotgun.dm
@@ -112,6 +112,22 @@
var/mob/living/M = target
M.adjust_bodytemperature(((100-blocked)/100)*(temperature - M.bodytemperature))
+/obj/item/projectile/bullet/pellet/nullglass
+ name = "nullglass pellet"
+ damage = 6
+ wound_bonus = 3
+ bare_wound_bonus = 3
+
+/obj/item/projectile/bullet/pellet/nullglass/disrupts_psionics()
+ return src
+
+/obj/item/projectile/bullet/pellet/nullglass/on_hit(atom/target)
+ . = ..()
+ if(prob(10))
+ var/obj/item/implant/nullglass/imp = new()
+ imp.implant(target)
+ playsound(loc, 'sound/effects/glass_step.ogg', 30, TRUE)
+
/obj/item/projectile/bullet/shotgun_uraniumslug
name = "depleted uranium slug"
icon_state = "ubullet"
diff --git a/code/modules/psionics/complexus/complexus.dm b/code/modules/psionics/complexus/complexus.dm
new file mode 100644
index 000000000000..f788ec39140e
--- /dev/null
+++ b/code/modules/psionics/complexus/complexus.dm
@@ -0,0 +1,121 @@
+/datum/psi_complexus
+ /// Whether or not we have been announced to our holder yet.
+ var/announced = FALSE
+ /// Whether or not we are suppressing our psi powers.
+ var/suppressed = TRUE
+ /// Whether or not we should automatically deflect/block incoming damage.
+ var/use_psi_armour = TRUE
+ /// Whether or not we should automatically heal damage damage.
+ var/use_autoredaction = TRUE
+ /// Whether or not zorch uses lethal projectiles.
+ var/zorch_harm = FALSE
+ /// What amount of heat the user wants to stop at.
+ var/limiter = 100
+ /// Whether or not we need to rebuild our cache of psi powers.
+ var/rebuild_power_cache = TRUE
+
+ /// Overall psi rating.
+ var/rating = 0
+ /// Multiplier for power use stamina costs.
+ var/cost_modifier = 1
+ /// Number of process ticks we are stunned for.
+ var/stun = 0
+ /// world.time minimum before next power use.
+ var/next_power_use = 0
+
+ // Stamina / Heat
+ /// Current psi pool.
+ var/stamina = 50
+ /// Max psi pool.
+ var/max_stamina = 50
+ /// Multiplier for the recharge rate of psi heat.
+ var/stamina_recharge_mult = 1
+ /// Current psi heat.
+ var/heat = 0
+ /// Max psi heat. 100 is safe, 300 has minor consequences, 500 is dangerous, max is death.
+ var/max_heat = 500
+ /// Multiplier for the decay rate of psi heat.
+ var/heat_decay_mult = 1
+
+ /// List of all currently latent faculties.
+ var/list/latencies
+ /// Assoc list of psi faculties to current rank.
+ var/list/ranks
+ /// Assoc list of psi faculties to base rank, in case reset is needed
+ var/list/base_ranks
+ /// List of atoms manifested/maintained by psychic power.
+ var/list/manifested_items
+ /// world.time minimum before a trigger can be attempted again.
+ var/next_latency_trigger = 0
+ /// world.time of last armour check.
+ var/last_armor_check
+ var/last_aura_size
+ var/last_aura_alpha
+ var/last_aura_color
+ var/aura_color = "#ff0022"
+
+ // Cached powers.
+ var/list/melee_powers // Powers used in melee range.
+ var/list/grab_powers // Powers use by using a grab.
+ var/list/ranged_powers // Powers used at range.
+ var/list/manifestation_powers // Powers that create an item.
+ var/list/powers_by_faculty // All powers within a given faculty.
+
+ var/obj/screen/psi/hub/ui // Reference to the master psi UI object.
+ var/mob/living/owner // Reference to our owner.
+ var/image/_aura_image // Client image
+
+/datum/psi_complexus/proc/get_aura_image()
+ if(_aura_image && !istype(_aura_image))
+ var/atom/A = _aura_image
+ destroy_aura_image(_aura_image)
+ _aura_image = null
+ CRASH("Non-image found in psi complexus: \ref[A] - \the [A] - [istype(A) ? A.type : "non-atom"]")
+ if(!_aura_image)
+ _aura_image = create_aura_image(owner)
+ return _aura_image
+
+/proc/create_aura_image(newloc)
+ var/image/aura_image = image(loc = newloc, icon = 'icons/effects/psi_aura_small.dmi', icon_state = "aura")
+ aura_image.blend_mode = BLEND_MULTIPLY
+ aura_image.appearance_flags = NO_CLIENT_COLOR | RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
+ aura_image.layer = TURF_LAYER + 0.5
+ aura_image.alpha = 0
+ aura_image.pixel_x = -64
+ aura_image.pixel_y = -64
+ aura_image.mouse_opacity = 0
+ aura_image.appearance_flags = 0
+ for(var/datum/psi_complexus/psychic in SSpsi.processing)
+ if(!psychic.suppressed)
+ psychic?.owner?.client?.images += aura_image
+ SSpsi.all_aura_images[aura_image] = TRUE
+ return aura_image
+
+/proc/destroy_aura_image(image/aura_image)
+ for(var/datum/psi_complexus/psychic in SSpsi.processing)
+ psychic?.owner?.client?.images -= aura_image
+ SSpsi.all_aura_images -= aura_image
+
+/datum/psi_complexus/New(mob/M)
+ owner = M
+ START_PROCESSING(SSpsi, src)
+
+/datum/psi_complexus/Destroy()
+ destroy_aura_image(_aura_image)
+ STOP_PROCESSING(SSpsi, src)
+ if(owner)
+ cancel()
+ if(owner.client)
+ owner.client.screen -= ui.components
+ owner.client.screen -= ui
+ for(var/thing in SSpsi.all_aura_images)
+ owner.client.images -= thing
+ QDEL_NULL(ui)
+ owner.psi = null
+ owner = null
+
+ if(manifested_items)
+ for(var/thing in manifested_items)
+ qdel(thing)
+ manifested_items.Cut()
+ . = ..()
diff --git a/code/modules/psionics/complexus/complexus_helpers.dm b/code/modules/psionics/complexus/complexus_helpers.dm
new file mode 100644
index 000000000000..8719fea250d3
--- /dev/null
+++ b/code/modules/psionics/complexus/complexus_helpers.dm
@@ -0,0 +1,185 @@
+/datum/psi_complexus/proc/cancel()
+ SEND_SOUND(owner, sound('sound/effects/psi/power_fail.ogg'))
+ if(LAZYLEN(manifested_items))
+ for(var/thing in manifested_items)
+ owner.dropItemToGround(thing)
+ qdel(thing)
+ manifested_items = null
+
+/datum/psi_complexus/proc/stunned(amount)
+ var/old_stun = stun
+ stun = max(stun, amount)
+ if(amount && !old_stun)
+ to_chat(owner, span_danger("Your concentration has been shattered! You cannot focus your psi power!"))
+ ui.update_icon()
+ cancel()
+
+/datum/psi_complexus/proc/get_armour(armourtype)
+ if(can_use_passive())
+ last_armor_check = world.time
+ return round(clamp(clamp(4 * rating, 0, 20) * get_rank(SSpsi.armour_faculty_by_type[armourtype]), 0, 100) * (stamina/max_stamina))
+ last_armor_check = 0
+ return 0
+
+/datum/psi_complexus/proc/handle_block_chance(obj/item/projectile/projectile)
+ var/effective_rank
+ var/chance = 0
+
+ if(istype(projectile, /obj/item/projectile/beam) || istype(projectile, /obj/item/projectile/energy))
+ effective_rank = get_rank(PSI_ENERGISTICS)
+ else
+ effective_rank = get_rank(PSI_PSYCHOKINESIS)
+
+ switch(effective_rank)
+ if(PSI_RANK_OPERANT)
+ chance = 1
+ if(PSI_RANK_MASTER)
+ chance = 10
+ if(PSI_RANK_GRANDMASTER)
+ chance = 50
+ if(PSI_RANK_PARAMOUNT)
+ chance = 90
+
+ return prob(chance)
+
+/datum/psi_complexus/proc/get_rank(faculty)
+ return LAZYACCESS(ranks, faculty)
+
+/datum/psi_complexus/proc/set_rank(faculty, rank, defer_update, temporary)
+ if(get_rank(faculty) != rank)
+ LAZYSET(ranks, faculty, rank)
+ if(!temporary)
+ LAZYSET(base_ranks, faculty, rank)
+ if(!defer_update)
+ update()
+
+/datum/psi_complexus/proc/set_cooldown(value)
+ next_power_use = world.time + value
+ ui.update_icon()
+
+/datum/psi_complexus/proc/can_use_passive()
+ return (owner.stat == CONSCIOUS && !suppressed && !stun)
+
+/datum/psi_complexus/proc/can_use(incapacitation_flags)
+ return (owner.stat == CONSCIOUS && !suppressed && !stun && world.time >= next_power_use)
+
+/datum/psi_complexus/proc/spend_power(stamina_cost = 0, heat_cost = 0)
+ . = FALSE
+ if(!can_use())
+ return FALSE
+
+ // Focus
+ stamina_cost = max(1, CEILING(stamina_cost * cost_modifier, 1))
+ if(stamina < stamina_cost)
+ return FALSE
+ if((heat + heat_cost) >= limiter)
+ return FALSE
+ adjust_stamina(-stamina_cost)
+ adjust_heat(heat_cost)
+ handle_heat_effects()
+
+ ui.update_icon()
+ return TRUE
+
+/datum/psi_complexus/proc/set_stamina(value = 0)
+ stamina = clamp(value, 0, max_stamina)
+
+/datum/psi_complexus/proc/adjust_stamina(value = 0)
+ set_stamina(stamina + value)
+
+/datum/psi_complexus/proc/set_heat(value = 0)
+ heat = clamp(value, 0, max_heat)
+
+/datum/psi_complexus/proc/adjust_heat(value = 0)
+ set_heat(heat + value)
+
+/datum/psi_complexus/proc/hide_auras()
+ if(owner.client)
+ for(var/image/I in SSpsi.all_aura_images)
+ owner.client.images -= I
+
+/datum/psi_complexus/proc/show_auras()
+ if(owner.client)
+ for(var/image/I in SSpsi.all_aura_images)
+ owner.client.images |= I
+
+/datum/psi_complexus/proc/handle_heat_effects(effective_heat)
+ if(!owner)
+ return FALSE
+ if(!effective_heat)
+ effective_heat = heat
+ if(effective_heat < 100)
+ return
+ // The Fun Effects (500 heat)
+ if(effective_heat >= max_heat)
+ switch(pick(1, 2))
+ //1, Your head asplode / you are gibbed
+ if(1)
+ if(iscarbon(owner))
+ var/mob/living/carbon/C = owner
+ C.explode_head()
+ else
+ owner.gib()
+ //2, Your psi powers are too strained, causing them to disapear forever
+ if(2)
+ qdel(src)
+
+ //Less fun effects
+ switch(rand(1, effective_heat - 100))
+ // Your nose bleeds a little.
+ if(1 to 20)
+ var/mob/living/carbon/human/H
+ if(istype(H) && (H.dna.species.species_traits & NOBLOOD))
+ return
+ to_chat(owner,span_warning("Your nose begins to bleed..."))
+ owner.add_splatter_floor(small_drip = TRUE)
+ // Your get a headache. Yes this is stolen from disease code, sue me
+ if(21 to effective_heat)
+ switch(effective_heat)
+ if(0 to 200)
+ to_chat(owner, span_warning("[pick("Your head hurts.", "Your head pounds.")]"))
+ adjust_stamina(rand(-5, -1))
+ if(201 to 400)
+ to_chat(owner, span_warning("[pick("Your head hurts a lot.", "Your head pounds incessantly.")]"))
+ adjust_stamina(rand(-10, -5))
+ owner.adjustStaminaLoss(25)
+ if(401 to 500)
+ to_chat(owner, span_userdanger("[pick("You feel a burning knife inside your brain!", "A wave of pain fills your head!")]"))
+ adjust_stamina(rand(-15, -10))
+ owner.Stun(3.5 SECONDS)
+
+/datum/psi_complexus/proc/backblast(value)
+
+ // Can't backblast if you're controlling your power.
+ if(!owner || suppressed)
+ return FALSE
+
+ SEND_SOUND(owner, sound('sound/effects/psi/power_feedback.ogg'))
+ to_chat(owner, span_danger("Wild energistic feedback blasts across your psyche!"))
+ stunned(value * 2)
+ set_cooldown(value * 100)
+
+ if(prob(value*10))
+ owner.emote("scream")
+ adjust_heat(value * 10)
+ // Your head asplode.
+ owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, value)
+ if(ishuman(owner))
+ var/mob/living/carbon/human/pop = owner
+ var/obj/item/organ/brain/sponge = pop.getorganslot(ORGAN_SLOT_BRAIN)
+ if(sponge && pop.getOrganLoss(ORGAN_SLOT_BRAIN) >= sponge.maxHealth)
+ pop.explode_head()
+
+/datum/psi_complexus/proc/has_rank_above(required_rank)
+ for(var/faculty in ranks)
+ if(required_rank <= get_rank(faculty))
+ return TRUE
+
+/datum/psi_complexus/proc/reset()
+ aura_color = initial(aura_color)
+ ranks = base_ranks ? base_ranks.Copy() : null
+ max_stamina = initial(max_stamina)
+ set_stamina(stamina)
+ set_heat(heat)
+ cancel()
+ update()
diff --git a/code/modules/psionics/complexus/complexus_latency.dm b/code/modules/psionics/complexus/complexus_latency.dm
new file mode 100644
index 000000000000..93494533b98a
--- /dev/null
+++ b/code/modules/psionics/complexus/complexus_latency.dm
@@ -0,0 +1,18 @@
+/datum/psi_complexus/proc/check_latency_trigger(trigger_strength = 0, source, redactive = FALSE)
+
+ if(!LAZYLEN(latencies) || world.time < next_latency_trigger)
+ return FALSE
+
+ if(!prob(trigger_strength))
+ next_latency_trigger = world.time + rand(100, 300)
+ return FALSE
+
+ var/faculty = pick(latencies)
+ var/new_rank = rand(2,5)
+ owner.set_psi_rank(faculty, new_rank)
+ var/datum/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty)
+ to_chat(owner, span_danger("You scream internally as your [faculty_decl.name] faculty is forced into operancy by [source]!"))
+ next_latency_trigger = world.time + rand(600, 1800) * new_rank
+ if(!redactive)
+ owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(trigger_strength * 2, trigger_strength * 4))
+ return TRUE
diff --git a/code/modules/psionics/complexus/complexus_power_cache.dm b/code/modules/psionics/complexus/complexus_power_cache.dm
new file mode 100644
index 000000000000..6f80dc7e773f
--- /dev/null
+++ b/code/modules/psionics/complexus/complexus_power_cache.dm
@@ -0,0 +1,40 @@
+/datum/psi_complexus/proc/rebuild_power_cache()
+ if(rebuild_power_cache)
+
+ melee_powers = list()
+ ranged_powers = list()
+ manifestation_powers = list()
+ powers_by_faculty = list()
+
+ for(var/faculty in ranks)
+ var/relevant_rank = get_rank(faculty)
+ var/datum/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty)
+ for(var/P in faculty_decl.powers)
+ var/datum/psionic_power/power = P
+ if(relevant_rank >= power.min_rank)
+ LAZYADD(powers_by_faculty[power.faculty], power)
+ if(power.use_ranged)
+ if(!ranged_powers[faculty])
+ ranged_powers[faculty] = list()
+ LAZYADD(ranged_powers[faculty], power)
+ if(power.use_melee)
+ LAZYADD(melee_powers[faculty], power)
+ if(power.use_manifest)
+ manifestation_powers += power
+ rebuild_power_cache = FALSE
+
+/datum/psi_complexus/proc/get_powers_by_faculty(faculty)
+ rebuild_power_cache()
+ return powers_by_faculty[faculty]
+
+/datum/psi_complexus/proc/get_melee_powers(faculty)
+ rebuild_power_cache()
+ return melee_powers[faculty]
+
+/datum/psi_complexus/proc/get_ranged_powers(faculty)
+ rebuild_power_cache()
+ return ranged_powers[faculty]
+
+/datum/psi_complexus/proc/get_manifestations()
+ rebuild_power_cache()
+ return manifestation_powers
diff --git a/code/modules/psionics/complexus/complexus_process.dm b/code/modules/psionics/complexus/complexus_process.dm
new file mode 100644
index 000000000000..6fb78b86d603
--- /dev/null
+++ b/code/modules/psionics/complexus/complexus_process.dm
@@ -0,0 +1,224 @@
+/datum/psi_complexus/proc/update(force)
+
+ set waitfor = FALSE
+
+ var/last_rating = rating
+ var/highest_faculty
+ var/highest_rank = 0
+ var/combined_rank = 0
+ for(var/faculty in ranks)
+ var/check_rank = get_rank(faculty)
+ if(check_rank == 1)
+ LAZYADD(latencies, faculty)
+ else
+ if(check_rank <= 0)
+ ranks -= faculty
+ LAZYREMOVE(latencies, faculty)
+ combined_rank += check_rank
+ if(!highest_faculty || highest_rank < check_rank)
+ highest_faculty = faculty
+ highest_rank = check_rank
+
+ UNSETEMPTY(latencies)
+ var/rank_count = max(1, LAZYLEN(ranks))
+ if(force || last_rating != CEILING(combined_rank/rank_count, 1))
+ if(highest_rank <= 1)
+ if(highest_rank == 0)
+ qdel(src)
+ return
+ rebuild_power_cache = TRUE
+ SEND_SOUND(owner, 'sound/effects/psi/power_unlock.ogg')
+ rating = CEILING(combined_rank/rank_count, 1)
+ cost_modifier = 1
+ if(rating > 1)
+ cost_modifier -= min(1, max(0.1, (rating-1) / 10))
+ if(!ui)
+ ui = new(owner)
+ if(owner.client)
+ owner.client.screen += ui.components
+ owner.client.screen += ui
+ if(!suppressed && owner.client)
+ for(var/image/I in SSpsi.all_aura_images)
+ owner.client.images |= I
+ var/image/aura_image = get_aura_image()
+ /*
+ if(rating >= PSI_RANK_PARAMOUNT) // spooky boosters
+ aura_color = "#aaffaa"
+ aura_image.blend_mode = BLEND_SUBTRACT
+ else
+ */
+ aura_image.blend_mode = BLEND_ADD
+ switch(highest_faculty)
+ if(PSI_COERCION)
+ aura_color = "#cc3333"
+ if(PSI_PSYCHOKINESIS)
+ aura_color = "#3333cc"
+ if(PSI_REDACTION)
+ aura_color = "#33cc33"
+ if(PSI_ENERGISTICS)
+ aura_color = "#cccc33"
+
+ if(!announced && owner?.client && !QDELETED(src))
+ announced = TRUE
+ to_chat(owner, "
")
+ to_chat(owner, span_notice("You are psionic, touched by powers beyond understanding."))
+ to_chat(owner, span_notice("Shift-left-click your Psi icon on the bottom right to view a summary of how to use them, or left click it to suppress or unsuppress your psionics. Beware: overusing your gifts can have deadly consequences."))
+ to_chat(owner, "
")
+
+/datum/psi_complexus/process()
+
+ var/update_hud
+ if(stun)
+ stun--
+ if(stun)
+ suppressed = TRUE
+ else
+ to_chat(owner, span_notice("You have recovered your mental composure."))
+ update_hud = TRUE
+ return
+
+ if(stamina < max_stamina)
+ adjust_stamina((owner.stat == CONSCIOUS ? rand(1,3) : rand(3,5)) * stamina_recharge_mult)
+
+ if(heat)
+ adjust_heat(((owner.stat == CONSCIOUS ? -1 : -3)) * heat_decay_mult)
+
+ if(owner.stat == CONSCIOUS && stamina && use_autoredaction && !suppressed && get_rank(PSI_REDACTION) >= PSI_RANK_OPERANT)
+ attempt_regeneration()
+
+ var/next_aura_size = max(0.1,((stamina/max_stamina)*min(3,rating))/5)
+ var/next_aura_alpha = round(((suppressed ? max(0,rating - 2) : rating)/5)*255)
+
+ if(next_aura_alpha != last_aura_alpha || next_aura_size != last_aura_size || aura_color != last_aura_color)
+ last_aura_size = next_aura_size
+ last_aura_alpha = next_aura_alpha
+ last_aura_color = aura_color
+ var/matrix/M = matrix()
+ if(next_aura_size != 1)
+ M.Scale(next_aura_size)
+ animate(get_aura_image(), alpha = next_aura_alpha, transform = M, color = aura_color, time = 3)
+
+ if(update_hud)
+ ui.update_icon()
+
+/datum/psi_complexus/proc/attempt_regeneration()
+
+ var/heal_general = FALSE
+ var/heal_poison = FALSE
+ var/heal_internal = FALSE
+// var/heal_bleeding = FALSE
+ var/heal_rate = 0
+ var/mend_prob = 0
+
+ switch(get_rank(PSI_REDACTION))
+ if(PSI_RANK_PARAMOUNT)
+ heal_general = TRUE
+ heal_poison = TRUE
+ heal_internal = TRUE
+ // heal_bleeding = TRUE
+ mend_prob = 50
+ heal_rate = 7
+ if(PSI_RANK_GRANDMASTER)
+ heal_poison = TRUE
+ heal_internal = TRUE
+ // heal_bleeding = TRUE
+ mend_prob = 20
+ heal_rate = 5
+ if(PSI_RANK_MASTER)
+ heal_internal = TRUE
+ // heal_bleeding = TRUE
+ mend_prob = 10
+ heal_rate = 3
+ if(PSI_RANK_OPERANT)
+ // heal_bleeding = TRUE
+ mend_prob = 5
+ heal_rate = 1
+ else
+ return
+
+ if(!heal_rate || stamina < heal_rate)
+ return // Don't backblast from trying to heal ourselves thanks.
+
+ if(ishuman(owner))
+
+ var/mob/living/carbon/human/H = owner
+
+ // Mend internal damage.
+ if(prob(mend_prob))
+/*
+ // Fix our heart if we're paramount.
+ if(heal_general && H.is_asystole() && spend_power(heal_rate))
+ H.resuscitate()
+*/
+ // Heal organ damage.
+ if(heal_internal)
+ for(var/obj/item/organ/I in H.internal_organs)
+
+ if(I.organ_flags & ORGAN_SYNTHETIC)
+ continue
+
+ if(I.damage > 0 && spend_power(heal_rate))
+ I.applyOrganDamage(-heal_rate)
+ if(prob(25))
+ to_chat(H, span_notice("Your innards itch as your autoredactive faculty mends your [I.name]."))
+ return
+ /* To fix
+ // Heal broken bones.
+ if(H.bad_external_organs.len)
+ for(var/obj/item/organ/external/E in H.bad_external_organs)
+
+ if(BP_IS_ROBOTIC(E))
+ continue
+
+ if(heal_internal && (E.status & ORGAN_BROKEN) && E.damage < (E.min_broken_damage * config.organ_health_multiplier)) // So we don't mend and autobreak.
+ if(spend_power(heal_rate))
+ if(E.mend_fracture())
+ to_chat(H, span_notice("Your autoredactive faculty coaxes together the shattered bones in your [E.name]."))
+ return
+
+ if(heal_bleeding)
+
+ if((E.status & ORGAN_ARTERY_CUT) && spend_power(heal_rate))
+ to_chat(H, span_notice("Your autoredactive faculty mends the torn artery in your [E.name], stemming the worst of the bleeding."))
+ E.status &= ~ORGAN_ARTERY_CUT
+ return
+
+ if(E.status & ORGAN_TENDON_CUT)
+ to_chat(H, span_notice("Your autoredactive faculty repairs the severed tendon in your [E.name]."))
+ E.status &= ~ORGAN_TENDON_CUT
+ return TRUE
+
+ for(var/datum/wound/W in E.wounds)
+
+ if(W.bleeding() && spend_power(heal_rate))
+ to_chat(H, span_notice("Your autoredactive faculty knits together severed veins, stemming the bleeding from \a [W.desc] on your [E.name]."))
+ W.bleed_timer = 0
+ W.clamped = TRUE
+ E.status &= ~ORGAN_BLEEDING
+ return
+ */
+
+
+ // Heal radiation, cloneloss and poisoning.
+ if(heal_poison)
+
+ if(owner.radiation && spend_power(heal_rate))
+ if(prob(25))
+ to_chat(owner, span_notice("Your autoredactive faculty repairs some of the radiation damage to your body."))
+ owner.radiation = max(0, owner.radiation - (heal_rate * 5))
+ return
+
+ if(owner.getCloneLoss() && spend_power(heal_rate))
+ if(prob(25))
+ to_chat(owner, span_notice("Your autoredactive faculty stitches together some of your mangled DNA."))
+ owner.adjustCloneLoss(-heal_rate)
+ return
+
+ // Heal everything left.
+ if(heal_general && prob(mend_prob) && (owner.getBruteLoss() || owner.getFireLoss() || owner.getOxyLoss()) && spend_power(heal_rate))
+ owner.adjustBruteLoss(-(heal_rate))
+ owner.adjustFireLoss(-(heal_rate))
+ owner.adjustOxyLoss(-(heal_rate))
+ new /obj/effect/temp_visual/heal(get_turf(owner), "#33cc33")
+ if(prob(25))
+ to_chat(owner, span_notice("Your skin crawls as your autoredactive faculty heals your body."))
diff --git a/code/modules/psionics/equipment/cerebro_enhancers.dm b/code/modules/psionics/equipment/cerebro_enhancers.dm
new file mode 100644
index 000000000000..a4281257649f
--- /dev/null
+++ b/code/modules/psionics/equipment/cerebro_enhancers.dm
@@ -0,0 +1,153 @@
+//Psi-boosting item (antag only)
+/obj/item/clothing/head/helmet/space/psi_amp
+ name = "cerebro-energetic enhancer"
+ desc = "A matte-black, eyeless cerebro-energetic enhancement helmet. It uses highly sophisticated, and illegal, techniques to drill into your brain and install psi-infected AIs into the fluid cavities between your lobes."
+ //actions_types = list(/datum/action/item_action/toggle_helmet_light)
+ icon_state = "cerebro"
+
+ var/operating = FALSE
+ var/list/boosted_faculties
+ var/boosted_rank = PSI_RANK_PARAMOUNT
+ var/unboosted_rank = PSI_RANK_MASTER
+ var/max_boosted_faculties = 3
+ var/boosted_psipower = 120
+ var/paramount_check = FALSE
+
+/obj/item/clothing/head/helmet/space/psi_amp/Initialize()
+ . = ..()
+ verbs += /obj/item/clothing/head/helmet/space/psi_amp/proc/integrate
+
+/obj/item/clothing/head/helmet/space/psi_amp/attack_self(mob/user)
+
+ if(operating)
+ return
+
+ var/mob/living/carbon/human/H = loc
+ if(istype(H) && H.head == src)
+ integrate()
+ return
+
+ if(paramount_check && !H?.mind?.has_antag_datum(/datum/antagonist/paramount))
+ to_chat(user, span_notice("You have no clue how to use this!"))
+
+ var/choice = input("Select a brainboard to install or remove.","Psionic Amplifier") as null|anything in SSpsi.faculties_by_name
+ if(!choice)
+ return
+
+ var/removed
+ var/slots_left = max_boosted_faculties - LAZYLEN(boosted_faculties)
+ var/datum/psionic_faculty/faculty = SSpsi.get_faculty(choice)
+ if(faculty.id in boosted_faculties)
+ LAZYREMOVE(boosted_faculties, faculty.id)
+ removed = TRUE
+ else
+ if(slots_left <= 0)
+ to_chat(user, span_warning("There are no slots left to install brainboards into."))
+ return
+ LAZYADD(boosted_faculties, faculty.id)
+ UNSETEMPTY(boosted_faculties)
+
+ slots_left = max_boosted_faculties - LAZYLEN(boosted_faculties)
+ to_chat(user, span_notice("You [removed ? "remove" : "install"] the [choice] brainboard [removed ? "from" : "in"] \the [src]. There [slots_left!=1 ? "are" : "is"] [slots_left] slot\s left."))
+
+/obj/item/clothing/head/helmet/space/psi_amp/AltClick(mob/user)
+ . = ..()
+ if(operating)
+ deintegrate()
+ else
+ integrate()
+
+/obj/item/clothing/head/helmet/space/psi_amp/proc/deintegrate()
+ if(operating)
+ return
+
+ var/mob/living/carbon/human/H = loc
+ if(!istype(H))
+ return
+ if(paramount_check && !H?.mind?.has_antag_datum(/datum/antagonist/paramount))
+ to_chat(H, span_notice("You have no clue how to use this!"))
+
+
+ to_chat(H, span_warning("You feel a strange tugging sensation as \the [src] begins removing the slave-minds from your brain..."))
+ playsound(H, 'sound/weapons/circsawhit.ogg', 50, 1, -1)
+ operating = TRUE
+
+ sleep(80)
+
+ if(H.psi)
+ H.psi.reset()
+
+ to_chat(H, span_notice("\The [src] chimes quietly as it finishes removing the slave-minds from your brain."))
+
+ REMOVE_TRAIT(src, TRAIT_NODROP, TRAIT_GENERIC)
+ operating = FALSE
+
+ set_light(0)
+
+/obj/item/clothing/head/helmet/space/psi_amp/Move()
+ var/lastloc = loc
+ . = ..()
+ if(.)
+ var/mob/living/carbon/human/H = lastloc
+ if(istype(H) && H.psi)
+ H.psi.reset()
+ H = loc
+ if(!istype(H) || H.head != src)
+ REMOVE_TRAIT(src, TRAIT_NODROP, TRAIT_GENERIC)
+
+/obj/item/clothing/head/helmet/space/psi_amp/proc/integrate()
+ if(operating)
+ return
+
+ var/mob/living/carbon/human/H = loc
+
+ if(!istype(H) || H.head != src)
+ to_chat(usr, span_warning("\The [src] must be worn on your head in order to be activated."))
+ return
+
+ if(paramount_check && !H?.mind?.has_antag_datum(/datum/antagonist/paramount))
+ to_chat(H, span_notice("You have no clue how to use this!"))
+ return
+
+ if(LAZYLEN(boosted_faculties) < max_boosted_faculties)
+ to_chat(usr, span_notice("You still have [max_boosted_faculties - LAZYLEN(boosted_faculties)] facult[LAZYLEN(boosted_faculties) == 1 ? "y" : "ies"] to select. Use \the [src] in-hand to select them."))
+ return
+
+ ADD_TRAIT(src, TRAIT_NODROP, TRAIT_GENERIC)
+ operating = TRUE
+ to_chat(H, span_warning("You feel a series of sharp pinpricks as \the [src] anaesthetises your scalp before drilling down into your brain."))
+ playsound(H, 'sound/weapons/circsawhit.ogg', 50, 1, -1)
+
+ sleep(80)
+
+ for(var/faculty in list(PSI_COERCION, PSI_PSYCHOKINESIS, PSI_REDACTION, PSI_ENERGISTICS))
+ if(faculty in boosted_faculties)
+ H.set_psi_rank(faculty, boosted_rank, take_larger = TRUE, temporary = TRUE)
+ else
+ H.set_psi_rank(faculty, unboosted_rank, take_larger = TRUE, temporary = TRUE)
+ if(H.psi)
+ H.psi.max_stamina = boosted_psipower
+ H.psi.set_stamina(H.psi.max_stamina)
+ H.psi.update(force = TRUE)
+
+ to_chat(H, span_notice("You experience a brief but powerful wave of deja vu as \the [src] finishes modifying your brain."))
+ operating = FALSE
+ H.update_action_buttons()
+
+ set_light(0.5, 0.1, 3, 2, l_color = "#880000")
+
+/obj/item/clothing/head/helmet/space/psi_amp/lesser
+ max_boosted_faculties = 1
+ boosted_rank = PSI_RANK_MASTER
+ unboosted_rank = PSI_RANK_OPERANT
+ boosted_psipower = 50
+
+/obj/item/clothing/head/helmet/space/psi_amp/lesser/crown
+ name = "psionic amplifier"
+ desc = "A crown-of-thorns cerebro-energetic enhancer that interfaces directly with the brain, isolating and strengthening psionic signals. It kind of looks like a tiara having sex with an industrial robot."
+ icon_state = "amp"
+ flags_inv = 0
+ body_parts_covered = 0
+
+/obj/item/clothing/head/helmet/space/psi_amp/paramount
+ paramount_check = TRUE
diff --git a/code/modules/psionics/equipment/psipower.dm b/code/modules/psionics/equipment/psipower.dm
new file mode 100644
index 000000000000..bce3b1eea34d
--- /dev/null
+++ b/code/modules/psionics/equipment/psipower.dm
@@ -0,0 +1,40 @@
+/obj/item/psychic_power
+ name = "psychic power"
+ icon = 'icons/obj/psychic_powers.dmi'
+ anchored = TRUE
+ var/maintain_cost = 3
+ var/mob/living/owner
+
+/obj/item/psychic_power/New(mob/living/L)
+ owner = L
+ if(!istype(owner))
+ qdel(src)
+ return
+ START_PROCESSING(SSprocessing, src)
+ ..()
+
+/obj/item/psychic_power/Destroy()
+ if(istype(owner) && owner.psi)
+ LAZYREMOVE(owner.psi.manifested_items, src)
+ UNSETEMPTY(owner.psi.manifested_items)
+ STOP_PROCESSING(SSprocessing, src)
+ . = ..()
+
+/obj/item/psychic_power/attack_self(mob/user)
+ user.playsound_local(soundin = 'sound/effects/psi/power_fail.ogg')
+ user.dropItemToGround(src)
+
+/obj/item/psychic_power/dropped()
+ ..()
+ qdel(src)
+
+/obj/item/psychic_power/process()
+ if(istype(owner))
+ owner.psi.spend_power(maintain_cost)
+ if(!owner || loc != owner || !(src in owner.held_items))
+ if(ishuman(loc))
+ var/mob/living/carbon/human/host = loc
+ host.remove_embedded_object(src)
+ host.dropItemToGround(src)
+ else
+ qdel(src)
diff --git a/code/modules/psionics/equipment/psipower_blade.dm b/code/modules/psionics/equipment/psipower_blade.dm
new file mode 100644
index 000000000000..9a3071dc15f4
--- /dev/null
+++ b/code/modules/psionics/equipment/psipower_blade.dm
@@ -0,0 +1,16 @@
+/obj/item/psychic_power/psiblade
+ name = "psychokinetic slash"
+ force = 10
+ sharpness = SHARP_EDGED
+ icon_state = "psiblade_short"
+ item_state = "psiblade"
+ lefthand_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi'
+ hitsound = 'sound/weapons/psisword.ogg'
+ var/can_break_wall = FALSE
+ var/wall_break_time = 6 SECONDS
+
+/obj/item/psychic_power/psiblade/dropped(var/mob/living/user)
+ ..()
+ playsound(loc, 'sound/effects/psi/power_fail.ogg', 30, 1)
+ QDEL_IN(src, 1)
diff --git a/code/modules/psionics/equipment/psipower_tinker.dm b/code/modules/psionics/equipment/psipower_tinker.dm
new file mode 100644
index 000000000000..477d6efb382c
--- /dev/null
+++ b/code/modules/psionics/equipment/psipower_tinker.dm
@@ -0,0 +1,24 @@
+/obj/item/psychic_power/tinker
+ name = "psychokinetic crowbar"
+ icon_state = "tinker"
+ force = 0
+ tool_behaviour = TOOL_CROWBAR
+ usesound = 'sound/weapons/etherealhit.ogg'
+ var/list/possible_tools
+
+/obj/item/psychic_power/tinker/attack_self()
+
+ if(!owner || loc != owner)
+ return
+
+ var/choice = input("Select a tool to emulate.","Power") as null|anything in possible_tools
+ if(!choice)
+ return
+
+ if(!owner || loc != owner)
+ return
+
+ tool_behaviour = choice
+ name = "psychokinetic [tool_behaviour]"
+ to_chat(owner, "You begin emulating \a [tool_behaviour].")
+ owner.playsound_local(soundin = 'sound/effects/psi/power_fabrication.ogg')
diff --git a/code/modules/psionics/equipment/psipower_tk.dm b/code/modules/psionics/equipment/psipower_tk.dm
new file mode 100644
index 000000000000..deb309afa468
--- /dev/null
+++ b/code/modules/psionics/equipment/psipower_tk.dm
@@ -0,0 +1,97 @@
+/obj/item/psychic_power/telekinesis
+ name = "telekinetic grip"
+ maintain_cost = 3
+ icon_state = "telekinesis"
+ var/atom/movable/focus
+
+/obj/item/psychic_power/telekinesis/Destroy()
+ focus = null
+ . = ..()
+
+/obj/item/psychic_power/telekinesis/process()
+ if(!focus || !isturf(focus.loc) || get_dist(get_turf(focus), get_turf(owner)) > owner.psi.get_rank(PSI_PSYCHOKINESIS))
+ owner.dropItemToGround(src)
+ return
+ . = ..()
+
+/obj/item/psychic_power/telekinesis/proc/set_focus(atom/movable/_focus)
+
+ if(!isturf(_focus.loc))
+ return FALSE
+
+ var/check_paramount
+ if(isliving(_focus))
+ var/mob/living/victim = _focus
+ check_paramount = (victim.mob_size >= MOB_SIZE_HUMAN)
+ else if(isitem(_focus))
+ var/obj/item/thing = _focus
+ check_paramount = (thing.w_class >= WEIGHT_CLASS_BULKY)
+ else
+ return FALSE
+
+ if(_focus.anchored || (check_paramount && owner.psi.get_rank(PSI_PSYCHOKINESIS) < PSI_RANK_PARAMOUNT))
+ focus = _focus
+ . = attack_self(owner)
+ if(!.)
+ to_chat(owner, span_warning("\The [_focus] is too hefty for you to get a mind-grip on."))
+ qdel(src)
+ return FALSE
+
+ focus = _focus
+ overlays.Cut()
+ var/image/I = image(icon = focus.icon, icon_state = focus.icon_state)
+ I.color = focus.color
+ I.overlays = focus.overlays
+ overlays += I
+ return TRUE
+
+/obj/item/psychic_power/telekinesis/attack_self(mob/user)
+ user.visible_message(span_notice("[user] makes a strange gesture."))
+ sparkle()
+ return focus.do_simple_ranged_interaction(user)
+
+/obj/item/psychic_power/telekinesis/afterattack(atom/target, mob/living/user, proximity)
+
+ if(!target || !user || (isobj(target) && !isturf(target.loc)) || !user.psi || !user.psi.can_use() || !user.psi.spend_power(5))
+ return
+
+ //user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN) FIX ME
+ user.psi.set_cooldown(5)
+
+ var/distance = get_dist(get_turf(user), get_turf(focus ? focus : target))
+ if(distance > user.psi.get_rank(PSI_PSYCHOKINESIS))
+ to_chat(user, span_warning("Your telekinetic power won't reach that far."))
+ return FALSE
+
+ if(target == focus)
+ attack_self(user)
+ else
+ user.visible_message(span_danger("[user] gestures sharply!"))
+ sparkle()
+ if(!isturf(target) && istype(focus,/obj/item) && target.Adjacent(focus))
+ var/obj/item/I = focus
+ var/resolved = target.attackby(I, user, user.zone_selected)
+ if(!resolved && target && I)
+ I.afterattack(target,user,1) // for splashing with beakers
+ else
+ if(!focus.anchored)
+ var/user_rank = owner.psi.get_rank(PSI_PSYCHOKINESIS)
+ focus.throw_at(target, user_rank*2, user_rank*10, owner)
+ sleep(1)
+ sparkle()
+ owner.dropItemToGround(src)
+
+/obj/item/psychic_power/telekinesis/proc/sparkle()
+ set waitfor = 0
+ if(focus)
+ var/obj/effect/overlay/O = new /obj/effect/overlay(get_turf(focus))
+ O.name = "sparkles"
+ O.anchored = 1
+ O.density = 0
+ O.layer = FLY_LAYER
+ //O.set_dir(pick(cardinal))
+ O.icon = 'icons/effects/effects.dmi'
+ O.icon_state = "nothing"
+ flick("empdisable",O)
+ sleep(5)
+ qdel(O)
diff --git a/code/modules/psionics/events/_psi.dm b/code/modules/psionics/events/_psi.dm
new file mode 100644
index 000000000000..9a40e22c61d8
--- /dev/null
+++ b/code/modules/psionics/events/_psi.dm
@@ -0,0 +1,20 @@
+/datum/round_event/psi
+ startWhen = 30
+ endWhen = 120
+
+/datum/round_event/psi/announce()
+ priority_announce( \
+ "A localized disruption within the neighboring psionic continua has been detected. All psi-operant crewmembers \
+ are advised to cease any sensitive activities and report to medical personnel in case of damage.", "Central Command Higher Dimensional Affairs")
+
+/datum/round_event/psi/end()
+ priority_announce( \
+ "The psi-disturbance has ended and baseline normality has been re-asserted. \
+ Anything you still can't cope with is therefore your own problem.", "Central Command Higher Dimensional Affairs")
+
+/datum/round_event/psi/tick()
+ for(var/thing in SSpsi.processing)
+ apply_psi_effect(thing)
+
+/datum/round_event/psi/proc/apply_psi_effect(var/datum/psi_complexus/psi)
+ return
diff --git a/code/modules/psionics/events/mini_spasm.dm b/code/modules/psionics/events/mini_spasm.dm
new file mode 100644
index 000000000000..47fdfc4b3b6d
--- /dev/null
+++ b/code/modules/psionics/events/mini_spasm.dm
@@ -0,0 +1,69 @@
+/datum/round_event_control/minispasm
+ name = "Minispasms"
+ typepath = /datum/round_event/minispasm
+ weight = 8
+ max_occurrences = 1
+ earliest_start = 30 MINUTES
+
+/datum/round_event/minispasm
+ startWhen = 60
+ endWhen = 90
+ var/static/list/psi_operancy_messages = list(
+ "There's something in your skull!",
+ "Something is eating your thoughts!",
+ "You can feel your brain being rewritten!",
+ "Something is crawling over your frontal lobe!",
+ "THE SIGNAL THE SIGNAL THE SIGNAL THE SIGNAL THE",
+ "Something is drilling through your skull!",
+ "Your head feels like it's going to implode!",
+ "Thousands of ants are tunneling in your head!"
+ )
+
+/datum/round_event/minispasm/announce()
+ priority_announce( \
+ "PRIORITY ALERT: SIGMA-[rand(50,80)] NON-STANDARD PSIONIC SIGNAL-WAVE TRANSMISSION DETECTED - 97% MATCH, NON-VARIANT \
+ SIGNAL SOURCE TRIANGULATED TO DISTANT SITE: All personnel are advised to avoid \
+ exposure to active audio transmission equipment including radio headsets and intercoms \
+ for the duration of the signal broadcast.",
+ "Central Command Higher Dimensional Affairs")
+
+/datum/round_event/minispasm/start()
+ var/list/victims = list()
+ for(var/obj/item/radio/radio in world)
+ if(radio.on)
+ for(var/mob/living/victim in range(radio.canhear_range, radio.loc))
+ if(!isnull(victims[victim]) || victim.stat != CONSCIOUS || HAS_TRAIT(victim, TRAIT_DEAF))
+ continue
+ victims[victim] = radio
+ for(var/thing in victims)
+ var/mob/living/victim = thing
+ var/obj/item/radio/source = victims[victim]
+ do_spasm(victim, source)
+
+/datum/round_event/minispasm/proc/do_spasm(mob/living/victim, obj/item/radio/source)
+ if(victim.psi)
+ playsound(source, 'sound/creatures/narsie_rises.ogg', 75) //LOUD AS FUCK BOY
+ to_chat(victim, span_danger("A hauntingly familiar sound hisses from \icon[source] \the [source], and your vision flickers!"))
+ victim.psi.backblast(rand(5,15))
+ victim.Paralyze(0.5 SECONDS)
+ victim.Jitter(10 SECONDS)
+ else
+ victim.visible_message(span_danger("[victim] starts having a seizure!"), span_userdanger("An indescribable, brain-tearing sound hisses from \icon[source] \the [source], and you collapse in a seizure!"))
+ victim.Unconscious(20 SECONDS)
+ victim.Jitter(1 SECONDS)
+ SEND_SIGNAL(victim, COMSIG_ADD_MOOD_EVENT, "minispasm", /datum/mood_event/epilepsy)
+ var/new_latencies = rand(2,4)
+ var/list/faculties = list(PSI_COERCION, PSI_REDACTION, PSI_ENERGISTICS, PSI_PSYCHOKINESIS)
+ for(var/i = 1 to new_latencies)
+ to_chat(victim, span_danger("[pick(psi_operancy_messages)]"))
+ victim.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(10,20))
+ victim.set_psi_rank(pick_n_take(faculties), 1)
+ sleep(30)
+ victim.psi.update()
+ sleep(4.5 SECONDS)
+ victim.psi.check_latency_trigger(100, "a psionic scream", redactive = TRUE)
+
+/datum/round_event/minispasm/end()
+ priority_announce( \
+ "PRIORITY ALERT: SIGNAL BROADCAST HAS CEASED. Personnel are cleared to resume use of non-hardened radio transmission equipment. Have a nice day.",
+ "Central Command Higher Dimensional Affairs")
diff --git a/code/modules/psionics/events/psi_balm.dm b/code/modules/psionics/events/psi_balm.dm
new file mode 100644
index 000000000000..5fe731f36a24
--- /dev/null
+++ b/code/modules/psionics/events/psi_balm.dm
@@ -0,0 +1,27 @@
+/datum/round_event_control/balm
+ name = "Psi Balm"
+ typepath = /datum/round_event/psi/balm
+ weight = 20
+ max_occurrences = 3
+ max_alert = SEC_LEVEL_DELTA
+
+/datum/round_event/psi/balm
+ var/static/list/balm_messages = list(
+ "A soothing balm washes over your psyche.",
+ "For a moment, you can hear a distant, familiar voice singing a quiet lullaby.",
+ "A sense of peace and comfort falls over you like a warm blanket."
+ )
+
+/datum/round_event/psi/balm/apply_psi_effect(var/datum/psi_complexus/psi)
+ var/soothed
+ if(psi.stun > 1)
+ psi.stun--
+ soothed = TRUE
+ else if(psi.stamina < psi.max_stamina)
+ psi.adjust_stamina(rand(1,3))
+ soothed = TRUE
+ else if(psi.owner.getOrganLoss(ORGAN_SLOT_BRAIN) > 0)
+ psi.owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, -1)
+ soothed = TRUE
+ if(soothed && prob(10))
+ to_chat(psi.owner, span_notice("[pick(balm_messages)]"))
diff --git a/code/modules/psionics/events/psi_wail.dm b/code/modules/psionics/events/psi_wail.dm
new file mode 100644
index 000000000000..7ea284cbe9f7
--- /dev/null
+++ b/code/modules/psionics/events/psi_wail.dm
@@ -0,0 +1,27 @@
+/datum/round_event_control/wail
+ name = "Psi Wail"
+ typepath = /datum/round_event/psi/wail
+ weight = 20
+ max_occurrences = 3
+ max_alert = SEC_LEVEL_DELTA
+
+/datum/round_event/psi/wail
+ var/static/list/whine_messages = list(
+ "A nerve-tearing psychic whine intrudes on your thoughts.",
+ "A horrible, distracting humming sound breaks your train of thought.",
+ "Your head aches as a psychic wail intrudes on your psyche."
+ )
+
+/datum/round_event/psi/wail/apply_psi_effect(var/datum/psi_complexus/psi)
+ var/annoyed
+ if(prob(1))
+ psi.stunned(1)
+ annoyed = TRUE
+ else if(prob(10))
+ psi.adjust_heat(rand(1,3))
+ annoyed = TRUE
+ else if(psi.stamina)
+ psi.adjust_stamina(-rand(1,3))
+ annoyed = TRUE
+ if(annoyed && prob(1))
+ to_chat(psi.owner, span_notice("[pick(whine_messages)]"))
diff --git a/code/modules/psionics/faculties/_faculty.dm b/code/modules/psionics/faculties/_faculty.dm
new file mode 100644
index 000000000000..28827b91f6a7
--- /dev/null
+++ b/code/modules/psionics/faculties/_faculty.dm
@@ -0,0 +1,11 @@
+/datum/psionic_faculty
+ var/id
+ var/name
+ var/associated_intent
+ var/list/armour_types = list()
+ var/list/powers = list()
+
+/datum/psionic_faculty/New()
+ ..()
+ for(var/atype in armour_types)
+ SSpsi.armour_faculty_by_type[atype] = id
diff --git a/code/modules/psionics/faculties/_power.dm b/code/modules/psionics/faculties/_power.dm
new file mode 100644
index 000000000000..b4b0fe15a262
--- /dev/null
+++ b/code/modules/psionics/faculties/_power.dm
@@ -0,0 +1,57 @@
+/datum/psionic_power
+ /// Name. If null, psipower won't be generated on roundstart.
+ var/name
+ /// Associated psi faculty.
+ var/faculty
+ /// Minimum psi rank to use this power.
+ var/min_rank
+ /// Base psi stamina cost for using this power.
+ var/cost
+ /// Base heat gained for using this power.
+ var/heat
+ /// Deciseconds cooldown after using this power.
+ var/cooldown
+ /// Whether or not using this power prints an admin attack log.
+ var/admin_log = TRUE
+ /// This power functions from a distance.
+ var/use_ranged
+ /// This power functions at melee range.
+ var/use_melee
+ /// This power manifests an item in the user's hands.
+ var/use_manifest
+ /// A short description of how to use this power, shown via assay.
+ var/use_description
+ /// A sound effect to play when the power is used.
+ var/use_sound = 'sound/effects/psi/power_used.ogg'
+
+/datum/psionic_power/proc/invoke(mob/living/user, atom/target)
+
+ if(!user.psi)
+ return FALSE
+
+ if(faculty && min_rank)
+ var/user_rank = user.psi.get_rank(faculty)
+ if(user_rank < min_rank)
+ return FALSE
+
+ if(cost && !user.psi.spend_power(cost, heat))
+ return FALSE
+
+ var/user_psi_leech = user.do_psionics_check(cost, user)
+ if(user_psi_leech)
+ to_chat(user, span_warning("Your power is leeched away by \the [user_psi_leech] as fast as you can focus it..."))
+ return FALSE
+
+ if(target.do_psionics_check(cost, user))
+ to_chat(user, span_warning("Your power skates across \the [target], but cannot get a grip..."))
+ return FALSE
+
+ return TRUE
+
+/datum/psionic_power/proc/handle_post_power(mob/living/user, atom/target)
+ if(cooldown)
+ user.psi.set_cooldown(cooldown)
+ if(admin_log && ismob(user) && ismob(target))
+ log_attack("[user] Used psipower ([name]) on [target]")
+ if(use_sound)
+ playsound(user.loc, use_sound, 75)
diff --git a/code/modules/psionics/faculties/coercion.dm b/code/modules/psionics/faculties/coercion.dm
new file mode 100644
index 000000000000..6912c5c6b47e
--- /dev/null
+++ b/code/modules/psionics/faculties/coercion.dm
@@ -0,0 +1,399 @@
+#define COGMANIP_HYPNOTIZE "Hypnotize"
+#define COGMANIP_ERASE_MEMORY "Erase Memory"
+#define COGMANIP_THRALL "Thrall"
+
+/datum/psionic_faculty/coercion
+ id = PSI_COERCION
+ name = "Coercion"
+ associated_intent = INTENT_DISARM
+
+/datum/psionic_power/coercion
+ faculty = PSI_COERCION
+
+/datum/psionic_power/coercion/invoke(var/mob/living/user, var/mob/living/target)
+ if (!istype(target))
+ to_chat(user, span_warning("You cannot mentally attack \the [target]."))
+ return FALSE
+
+ . = ..()
+
+/datum/psionic_power/coercion/blindstrike
+ name = "Blindstrike"
+ cost = 8
+ cooldown = 120
+ use_ranged = TRUE
+ use_melee = TRUE
+ min_rank = PSI_RANK_GRANDMASTER
+ use_description = "Target the eyes or mouth on disarm intent and click anywhere to use a radial attack that blinds, deafens and disorients everyone near you."
+
+/datum/psionic_power/coercion/blindstrike/invoke(var/mob/living/user, var/mob/living/target)
+ if(user.zone_selected == BODY_ZONE_PRECISE_MOUTH || user.zone_selected != BODY_ZONE_PRECISE_EYES || (istype(target) && target.pulledby == user))
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message(span_danger("\The [user] suddenly throws back their head, as though screaming silently!"))
+ to_chat(user, span_danger("You strike at all around you with a deafening psionic scream!"))
+ for(var/mob/living/M in orange(user, user.psi.get_rank(PSI_COERCION)))
+ if(M == user)
+ continue
+ M.emote("scream")
+ to_chat(M, span_danger("Your senses are blasted into oblivion by a psionic scream!"))
+ M.blind_eyes(1 SECONDS)
+ M.confused = rand(3,8)
+ return TRUE
+
+/datum/psionic_power/coercion/mindread
+ name = "Read Mind"
+ cost = 25
+ heat = 15
+ cooldown = 25 SECONDS //It should take a WHILE to be able to use this again.
+ use_melee = TRUE
+ min_rank = PSI_RANK_MASTER
+ use_description = "Target the head on disarm intent at melee range to attempt to read a victim's surface thoughts."
+
+/datum/psionic_power/coercion/mindread/invoke(var/mob/living/user, var/mob/living/target)
+ if(!istype(target) || target == user || user.zone_selected != BODY_ZONE_HEAD || target.pulledby == user)
+ return FALSE
+ . = ..()
+ if(!.)
+ return
+
+ if(target.stat == DEAD || (HAS_TRAIT(target, TRAIT_FAKEDEATH)) || !target.client)
+ to_chat(user, span_warning("\The [target] is in no state for a mind-read."))
+ return TRUE
+
+ user.visible_message(span_warning("\The [user] touches \the [target]'s temple..."))
+ var/question = input(user, "Say something?", "Read Mind", "Penny for your thoughts?") as null|text
+ if(!question || user.incapacitated() || !do_after(user, 20))
+ return TRUE
+
+ var/started_mindread = world.time
+ to_chat(user, span_notice("You dip your mentality into the surface layer of \the [target]'s mind, seeking an answer: [question]"))
+ to_chat(target, span_notice("Your mind is compelled to answer: [question]")) // I wonder how this will go down with the playerbase
+
+ var/answer = input(target, question, "Read Mind") as null|text
+ if(!answer || world.time > started_mindread + 25 SECONDS || user.stat != CONSCIOUS || target.stat == DEAD)
+ to_chat(user, span_notice("You receive nothing useful from \the [target]."))
+ else
+ to_chat(user, span_notice("You skim thoughts from the surface of \the [target]'s mind: [answer]"))
+ log_game("[key_name(user)] read mind of [key_name(target)] with question \"[question]\" and [answer?"got answer \"[answer]\".":"got no answer."]")
+ return TRUE
+
+/datum/psionic_power/coercion/agony
+ name = "Agony"
+ cost = 20
+ heat = 15
+ cooldown = 7 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_OPERANT
+ use_description = "Target the chest or groin on disarm intent to use a melee attack equivalent to a strike from a stun baton."
+
+/datum/psionic_power/coercion/agony/invoke(var/mob/living/user, var/mob/living/target)
+ if(!istype(target))
+ return FALSE
+ if(user.zone_selected != BODY_ZONE_CHEST && user.zone_selected != BODY_ZONE_PRECISE_GROIN)
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message("\The [target] has been struck by \the [user]!")
+ playsound(user.loc, 'sound/weapons/Egloves.ogg', 50, 1, -1)
+ target.apply_damage(10 * (user.psi.get_rank(PSI_COERCION) - 1), STAMINA, BODY_ZONE_CHEST)
+ return TRUE
+
+/datum/psionic_power/coercion/spasm
+ name = "Spasm"
+ cost = 15
+ cooldown = 100
+ use_melee = TRUE
+ use_ranged = TRUE
+ min_rank = PSI_RANK_MASTER
+ use_description = "Target the arms or hands on disarm intent to use a ranged attack that may rip the weapons away from the target."
+
+/datum/psionic_power/coercion/spasm/invoke(var/mob/living/user, var/mob/living/carbon/human/target)
+ if(!istype(target))
+ return FALSE
+
+ if(!(user.zone_selected in list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_PRECISE_L_HAND, BODY_ZONE_PRECISE_R_HAND)))
+ return FALSE
+
+ . = ..()
+
+ if(.)
+ to_chat(user, "You lash out, stabbing into \the [target] with a lance of psi-power.")
+ to_chat(target, "The muscles in your arms cramp horrendously!")
+ if(prob(75))
+ target.emote("scream")
+ if(prob(75) && target.held_items[1] && target.dropItemToGround(target.get_item_for_held_index(1)))
+ target.visible_message("\The [target] drops what they were holding as their left hand spasms!")
+ if(prob(75) && target.held_items[2] && target.dropItemToGround(target.get_item_for_held_index(2)))
+ target.visible_message("\The [target] drops what they were holding as their right hand spasms!")
+ return TRUE
+
+/datum/psionic_power/coercion/cognitivemanipulation
+ name = "Cognitive Manipulation"
+ cost = 28
+ cooldown = 20 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_GRANDMASTER
+ use_description = "Grab a victim, target the eyes, then attack them while on disarm intent, in order to manipulate their mind. The process takes some time, and failure is punished harshly."
+
+/datum/psionic_power/coercion/cognitivemanipulation/invoke(var/mob/living/user, var/mob/living/target)
+ if(!istype(target) || user.zone_selected != BODY_ZONE_PRECISE_EYES || target.pulledby != user)
+ return FALSE
+ . = ..()
+ if(.)
+ if(target.stat == DEAD || HAS_TRAIT(target, TRAIT_FAKEDEATH))
+ to_chat(user, span_warning("\The [target] is dead!"))
+ return TRUE
+ user.visible_message(span_danger("\The [user] seizes the head of \the [target] in both hands..."))
+
+ var/coercion_rank = user.psi.get_rank(PSI_COERCION)
+ var/target_coercion_rank = PSI_RANK_BLUNT
+ if(target.psi)
+ target_coercion_rank = target.psi.get_rank(PSI_COERCION)
+ var/relative_coercion_rank = target_coercion_rank ? coercion_rank - target_coercion_rank : coercion_rank
+
+ var/list/radial_list = list()
+ var/radial_icon = 'icons/mob/screen_psi.dmi'
+
+ if(coercion_rank >= PSI_RANK_GRANDMASTER)
+ var/datum/radial_menu_choice/choice = new
+ choice.image = icon(radial_icon, "hypnotise")
+ choice.info = "Make the target temporarily subject to a hypnosis-like effect, making them easily influenced by spoken words."
+ radial_list[COGMANIP_HYPNOTIZE] = choice
+
+ choice = new
+ choice.image = icon(radial_icon, "erase")
+ choice.info = "Rewrite the targets mind to remove a specific memory, which can cure them of related ailments."
+ radial_list[COGMANIP_ERASE_MEMORY] = choice
+
+ if(coercion_rank >= PSI_RANK_PARAMOUNT)
+ var/datum/radial_menu_choice/choice = new
+ choice.image = icon(radial_icon, "thrall")
+ choice.info = "Make the target a subservient thrall to your will."
+ radial_list[COGMANIP_THRALL] = choice
+ message_admins(COGMANIP_THRALL)
+
+ if(!radial_list.len)
+ return TRUE
+
+ var/choice = show_radial_menu(user, target, radial_list, require_near = TRUE, tooltips = TRUE)
+
+ if(!(choice in radial_list))
+ return TRUE
+
+ var/mob/living/carbon/C = target
+ // So much text
+ if(relative_coercion_rank < PSI_RANK_OPERANT || (relative_coercion_rank == PSI_RANK_OPERANT && (istype(C) && !C.hypnosis_vulnerable())))
+ to_chat(user, span_warning("[target] mind is too strong to hypnotize them!"))
+ if(target_coercion_rank >= PSI_RANK_OPERANT)
+ to_chat(target, span_warning("Your mind is invaded by the presence of \the [user], but you manage to [relative_coercion_rank == 1 ? "barely " :""]repel the attack!"))
+ else if(target_coercion_rank == PSI_RANK_LATENT)
+ to_chat(target, span_notice("Your somehow feel [user]'s presence in your head and something in your head holding strong."))
+ else
+ // Non-psionics have no clue what is going on, but they should still have some indication of whats is happening
+ to_chat(target, span_notice("Your feel a strange sensation in your head."))
+ return TRUE
+ if(target_coercion_rank >= PSI_RANK_OPERANT)
+ to_chat(target, span_warning("Your mind is invaded by the presence of \the [user], and your mental barriers [relative_coercion_rank > 1 ? "shatter like glass" : "fail"]!"))
+ to_chat(user, span_notice("You manage to [relative_coercion_rank > 1 ? "easily " : ""]push through [target]'s mental barriers and start working on the task at hand."))
+ else if(target_coercion_rank == PSI_RANK_LATENT)
+ to_chat(target, span_notice("Your somehow feel [user]'s presence in your head and something in your head failing."))
+ to_chat(user, span_notice("You manage to [relative_coercion_rank > 1 ? "easily " : ""]push through [target]'s amiture defenses and start working on the task at hand."))
+ else
+ to_chat(target, span_notice("Your feel a strange sensation in your head."))
+
+ switch(choice)
+ if(COGMANIP_HYPNOTIZE)
+ if(!do_after(user, 30 SECONDS, target, FALSE))
+ user.psi.backblast(rand(1, 5))
+ return TRUE
+ to_chat(user, span_danger("You surgicaly rearange \the [target]'s neurons, leaving [target.p_them()] easily influinced by the next thing [target.p_they()] hear. Choice you next words carefuly..."))
+ target.apply_status_effect(/datum/status_effect/trance, relative_coercion_rank * 10 SECONDS, relative_coercion_rank >= 2)
+
+ if(COGMANIP_ERASE_MEMORY)
+ if(!do_after(user, 30 SECONDS, target, FALSE))
+ user.psi.backblast(rand(1, 5))
+ return TRUE
+ var/lost_memory = pretty_filter(stripped_input(user, "What would you like [target] to forget?", "Cognative Manipulation"))
+ to_chat(user, span_danger("You surgicaly cut \the [target]'s hippocampus, removing every shread of memory surounding the phrase \"[lost_memory]\"."))
+ to_chat(target, "[lost_memory]")
+ to_chat(target, span_warning("You can't remember anything surounding that phrase!"))
+
+ if(COGMANIP_THRALL)
+ if(!target.mind || !target.key)
+ to_chat(user, span_warning("\The [target] is mindless!"))
+ return TRUE
+ to_chat(user, span_warning("You plunge your mentality into that of \the [target]..."))
+ if(!do_after(user, target.stat == CONSCIOUS ? 2 MINUTES : 1 MINUTES, target, FALSE))
+ user.psi.backblast(rand(10,25))
+ return TRUE
+ to_chat(user, span_danger("You sear through \the [target]'s neurons, reshaping as you see fit and leaving them subservient to your will!"))
+ to_chat(target, span_danger("Your defenses have eroded away and \the [user] has made you their mindslave."))
+ var/datum/antagonist/thrall/T = new()
+ T.master = user.mind
+ target.mind.add_antag_datum(T)
+ return TRUE
+
+/datum/psionic_power/coercion/assay
+ name = "Assay"
+ cost = 15
+ cooldown = 10 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_OPERANT
+ use_description = "Grab a patient, target the head, then use the grab on them while on disarm intent, in order to perform a deep coercive-redactive probe of their psionic potential."
+
+/datum/psionic_power/coercion/assay/invoke(var/mob/living/user, var/mob/living/target)
+ if(!istype(target) || user.zone_selected != BODY_ZONE_HEAD || target.pulledby != user || user == target)
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message(span_warning("\The [user] holds the head of \the [target] in both hands..."))
+ to_chat(user, span_notice("You insinuate your mentality into that of \the [target]..."))
+ to_chat(target, span_warning("Your persona is being probed by the psychic lens of \the [user]."))
+ if(!do_after(user, (target.stat == CONSCIOUS ? 50 : 25), target, FALSE))
+ user.psi.backblast(rand(5,10))
+ return TRUE
+ to_chat(user, span_notice("You retreat from \the [target], holding your new knowledge close."))
+ to_chat(target, span_danger("Your mental complexus is laid bare to judgement of \the [user]."))
+ target.show_psi_assay(user)
+ return TRUE
+
+/datum/psionic_power/coercion/focus
+ name = "Focus"
+ cost = 10
+ cooldown = 8 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_MASTER
+ use_description = "Grab a patient, target the mouth, then use the grab on them while on disarm intent, in order to cure ailments of the mind."
+
+/datum/psionic_power/coercion/focus/invoke(var/mob/living/user, var/mob/living/target)
+ if(user.zone_selected != BODY_ZONE_PRECISE_MOUTH || target.pulledby != user)
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message(span_warning("\The [user] holds the head of \the [target] in both hands..."))
+ to_chat(user, span_notice("You probe \the [target]'s mind for various ailments.."))
+ to_chat(target, span_warning("Your mind is being cleansed of ailments by \the [user]."))
+ if(!do_after(user, (target.stat == CONSCIOUS ? 5 SECONDS : 2.5 SECONDS), target, FALSE))
+ user.psi.backblast(rand(5,10))
+ return TRUE
+ to_chat(user, span_warning("You clear \the [target]'s mind of ailments."))
+ to_chat(target, span_warning("Your mind is cleared of ailments."))
+
+ var/coercion_rank = user.psi.get_rank(PSI_COERCION)
+ if(coercion_rank >= PSI_RANK_GRANDMASTER)
+ target.SetParalyzed(0)
+ if(coercion_rank >= PSI_RANK_PARAMOUNT)
+ target.SetParalyzed(0)
+ target.drowsyness = 0
+ if(istype(target, /mob/living/carbon))
+ var/mob/living/carbon/M = target
+ M.hallucination = max(M.hallucination, 10)
+ return TRUE
+
+/datum/psionic_power/coercion/commune
+ name = "Commune"
+ cost = 10
+ cooldown = 8 SECONDS
+ use_melee = TRUE
+ use_ranged = TRUE
+ min_rank = PSI_RANK_OPERANT
+ use_description = "Target the mouth and click on a creature on disarm intent to psionically send them a message."
+
+/datum/psionic_power/coercion/commune/invoke(var/mob/living/user, var/mob/living/target)
+ if(user.zone_selected != BODY_ZONE_PRECISE_MOUTH || user == target)
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message("[user] touches their fingers to their temple.")
+ var/text = pretty_filter(stripped_input(user, "What would you like to say?", "Speak to creature", null, null))
+
+ if(!text)
+ return
+
+ if(target.stat == DEAD)
+ to_chat(user,"Not even a psion of your level can speak to the dead.")
+ return
+
+ if (issilicon(target))
+ to_chat(user,"This can only be used on living organisms.")
+ return
+
+ log_say("[key_name(user)] communed to [key_name(target)]: [text]")
+
+ for (var/mob/M in GLOB.player_list)
+ if(M.stat == DEAD && M.client.prefs.toggles & CHAT_GHOSTEARS)
+ to_chat(M,"[user] psionically says to [target]: [text]")
+
+ var/mob/living/carbon/human/H = target
+ if(prob(25) && (target.mind && target.mind.assigned_role == "Chaplain"))
+ to_chat(H,"You sense [user]'s psyche enter your mind, whispering quietly: [text]")
+ else
+ to_chat(H,"You feel something crawl behind your eyes, hearing: [text]")
+ if(istype(H))
+ if(prob(10) && !(H.dna.species.species_traits & NOBLOOD))
+ to_chat(H,"Your nose begins to bleed...")
+ H.add_splatter_floor(small_drip = TRUE)
+ else if(prob(25))
+ to_chat(H,"Your head hurts...")
+ else if(prob(50))
+ to_chat(H,"Your mind buzzes...")
+
+/datum/psionic_power/coercion/psiping
+ name = "Psi-ping"
+ cost = 30
+ cooldown = 25 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_OPERANT
+ use_description = "Click on yourself with an empty hand on disarm intent to detect nearby psionic signatures."
+
+/datum/psionic_power/coercion/psiping/invoke(var/mob/living/user, var/mob/living/target)
+ if((target && user != target))
+ return FALSE
+ . = ..()
+ if(.)
+ to_chat(user, "You take a moment to tune into the local Nlom...")
+ if(!do_after(user, 3 SECONDS, user))
+ return
+ var/list/dirs = list()
+ for(var/mob/living/L in range(20))
+ var/turf/T = get_turf(L)
+ if(!T || L == user || L.stat == DEAD || issilicon(L))
+ continue
+ /*
+ var/image/ping_image = image(icon = 'icons/effects/effects.dmi', icon_state = "sonar_ping", loc = user)
+ ping_image.plane = LIGHTING_LAYER+1
+ ping_image.layer = LIGHTING_LAYER+1
+ ping_image.pixel_x = (T.x - user.x) * 32
+ ping_image.pixel_y = (T.y - user.y) * 32
+ user << ping_image
+ addtimer(CALLBACK(GLOBAL_PROC, /proc/qdel, ping_image), 8)
+ */
+ var/direction = num2text(get_dir(user, L))
+ var/dist
+ if(text2num(direction))
+ switch(get_dist(user, L))
+ if(0 to 10)
+ dist = "very close"
+ if(10 to 20)
+ dist = "close"
+ if(20 to 30)
+ dist = "a little ways away"
+ if(30 to 40)
+ dist = "farther away"
+ else
+ dist = "far away"
+ else
+ dist = "on top of you"
+ LAZYINITLIST(dirs[direction])
+ dirs[direction][dist] += 1
+ for(var/d in dirs)
+ var/list/feedback = list()
+ for(var/dst in dirs[d])
+ feedback += "[dirs[d][dst]] psionic signature\s [dst],"
+ if(feedback.len > 1)
+ feedback[feedback.len - 1] += " and"
+ to_chat(user, span_notice("You sense " + jointext(feedback, " ") + " towards the [dir2text(text2num(d))]."))
+ if(!length(dirs))
+ to_chat(user, span_notice("You detect no psionic signatures but your own."))
diff --git a/code/modules/psionics/faculties/energistics.dm b/code/modules/psionics/faculties/energistics.dm
new file mode 100644
index 000000000000..282fc52a67de
--- /dev/null
+++ b/code/modules/psionics/faculties/energistics.dm
@@ -0,0 +1,128 @@
+/datum/psionic_faculty/energistics
+ id = PSI_ENERGISTICS
+ name = "Energistics"
+ associated_intent = INTENT_HARM
+ armour_types = list("bomb", "laser", "energy")
+
+/datum/psionic_power/energistics
+ faculty = PSI_ENERGISTICS
+
+/datum/psionic_power/energistics/disrupt
+ name = "Disrupt"
+ cost = 20
+ heat = 20
+ cooldown = 10 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_MASTER
+ use_description = "Target the head, eyes or mouth while on harm intent to use a melee attack that causes a localized electromagnetic pulse."
+
+/datum/psionic_power/energistics/disrupt/invoke(var/mob/living/user, var/mob/living/target)
+ if(user.zone_selected != BODY_ZONE_HEAD && user.zone_selected != BODY_ZONE_PRECISE_EYES && user.zone_selected != BODY_ZONE_PRECISE_MOUTH)
+ return FALSE
+ if(isturf(target))
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message("\The [user] releases a gout of crackling static and arcing lightning over \the [target]!")
+ empulse(target, 0, 1)
+ return TRUE
+
+/datum/psionic_power/energistics/electrocute
+ name = "Electrocute"
+ cost = 10
+ heat = 30
+ cooldown = 7.5 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_GRANDMASTER
+ use_description = "Target the chest or groin while on harm intent to use a melee attack that electrocutes a victim."
+
+/datum/psionic_power/energistics/electrocute/invoke(var/mob/living/user, var/mob/living/target)
+ if(user.zone_selected != BODY_ZONE_CHEST && user.zone_selected != BODY_ZONE_PRECISE_GROIN)
+ return FALSE
+ if(isturf(target))
+ return FALSE
+ . = ..()
+ if(.)
+ if(istype(target))
+ user.visible_message(span_danger("\The [user] sends a jolt of electricity arcing into \the [target]!"))
+ target.electrocute_act(rand(15,45), user, 1, user.zone_selected)
+ return TRUE
+ else if(isatom(target))
+ var/obj/item/stock_parts/cell/charging_cell = target.get_cell()
+ if(istype(charging_cell))
+ user.visible_message(span_danger("\The [user] sends a jolt of electricity arcing into \the [target], charging it!"))
+ charging_cell.give(rand(15,45))
+ return TRUE
+ else
+ return FALSE
+
+/datum/psionic_power/energistics/zorch
+ name = "Zorch"
+ cost = 15
+ heat = 15
+ cooldown = 2 SECONDS
+ use_ranged = TRUE
+ min_rank = PSI_RANK_MASTER
+ use_description = "Use this ranged laser attack while on harm intent. Your mastery of Energistics will determine how powerful the laser is. Be wary of overuse, and try not to fry your own brain."
+
+/datum/psionic_power/energistics/zorch/invoke(var/mob/living/user, var/mob/living/target)
+ . = ..()
+ if(.)
+ if(HAS_TRAIT(user, TRAIT_PACIFISM) && user.psi.zorch_harm)
+ to_chat(user, span_notice("You manage to stop yourself before firing a harmful laser from your eyes, you don't want to risk harming anyone..."))
+
+ var/user_rank = user.psi.get_rank(faculty)
+ var/obj/item/projectile/pew
+ var/pew_sound
+
+ if(user.psi.zorch_harm)
+ pew = new /obj/item/projectile/beam/laser(get_turf(user))
+ else
+ pew = new /obj/item/projectile/beam/disabler(get_turf(user))
+
+ switch(user_rank)
+ if(PSI_RANK_PARAMOUNT)
+ pew.damage = 30
+ pew.name = "gigawatt mental laser"
+ pew_sound = 'sound/weapons/lasercannonfire.ogg'
+ if(PSI_RANK_GRANDMASTER)
+ pew.damage = 20
+ pew.name = "megawatt mental laser"
+ pew_sound = 'sound/weapons/Laser.ogg'
+ if(PSI_RANK_MASTER)
+ pew.damage = 10
+ pew.name = "mental laser"
+ pew_sound = 'sound/weapons/Taser.ogg'
+
+ if(istype(pew))
+ playsound(pew.loc, pew_sound, 25, 1)
+ pew.original = target
+ pew.starting = get_turf(user)
+ pew.firer = user
+ pew.fire(Get_Angle(user, target))
+ user.visible_message(span_danger("[user]'s eyes flare with light!"))
+ return TRUE
+
+/datum/psionic_power/energistics/spark
+ name = "Spark"
+ cost = 1
+ cooldown = 1 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_OPERANT
+ use_description = "Target a non-living target in melee range on harm intent to cause some sparks to appear. This can light fires."
+
+/datum/psionic_power/energistics/spark/invoke(var/mob/living/user, var/mob/living/target)
+ if(isnull(target) || istype(target))
+ return FALSE
+ . = ..()
+ if(.)
+ if(istype(target,/obj/item/clothing/mask/cigarette))
+ var/obj/item/clothing/mask/cigarette/S = target
+ S.light("[user] snaps \his fingers and \the [S.name] lights up.")
+ user.emote("snap")
+ playsound(S.loc, "sparks", 50, 1)
+ else
+ var/datum/effect_system/spark_spread/s = new /datum/effect_system/spark_spread
+ s.set_up(5, 1, src)
+ s.start()
+ return TRUE
diff --git a/code/modules/psionics/faculties/psychokinesis.dm b/code/modules/psionics/faculties/psychokinesis.dm
new file mode 100644
index 000000000000..23b977a38e5a
--- /dev/null
+++ b/code/modules/psionics/faculties/psychokinesis.dm
@@ -0,0 +1,114 @@
+/datum/psionic_faculty/psychokinesis
+ id = PSI_PSYCHOKINESIS
+ name = "Psychokinesis"
+ associated_intent = INTENT_GRAB
+ armour_types = list("melee", "bullet")
+
+/datum/psionic_power/psychokinesis
+ faculty = PSI_PSYCHOKINESIS
+ use_sound = null
+
+/datum/psionic_power/psychokinesis/psiblade
+ name = "Psiblade"
+ cost = 10
+ cooldown = 3 SECONDS
+ min_rank = PSI_RANK_OPERANT
+ use_description = "Click on or otherwise activate an empty hand while on harm intent to manifest a psychokinetic cutting blade. The power the blade will vary based on your mastery of the faculty."
+ use_sound = 'sound/effects/psi/power_fabrication.ogg'
+ use_manifest = TRUE
+ admin_log = FALSE
+
+/datum/psionic_power/psychokinesis/psiblade/invoke(var/mob/living/user, var/mob/living/target)
+ if((target && user != target) || user.a_intent != INTENT_HARM)
+ return FALSE
+ . = ..()
+ if(.)
+ var/obj/item/psychic_power/psiblade/blade = new /obj/item/psychic_power/psiblade(user, user)
+ switch(user.psi.get_rank(faculty))
+ if(PSI_RANK_PARAMOUNT)
+ blade.can_break_wall = TRUE
+ blade.wall_break_time = 3 SECONDS
+ blade.force = 40
+ if(PSI_RANK_GRANDMASTER)
+ blade.can_break_wall = TRUE
+ blade.force = 30
+ if(PSI_RANK_MASTER)
+ blade.force = 20
+ else
+ blade.force = 10
+ return blade
+
+/datum/psionic_power/psychokinesis/tinker
+ name = "Tinker"
+ cost = 5
+ cooldown = 10
+ min_rank = PSI_RANK_OPERANT
+ use_description = "Click on or otherwise activate an empty hand while on help intent to manifest a psychokinetic tool. Use it in-hand to switch between tool types."
+ use_sound = 'sound/effects/psi/power_fabrication.ogg'
+ use_manifest = TRUE
+ admin_log = FALSE
+
+/datum/psionic_power/psychokinesis/tinker/invoke(var/mob/living/user, var/mob/living/target)
+ if((target && user != target) || user.a_intent != INTENT_HELP)
+ return FALSE
+ . = ..()
+ if(.)
+ var/obj/item/psychic_power/tinker/tool = new(user)
+ switch(user.psi.get_rank(faculty))
+ if(PSI_RANK_PARAMOUNT)
+ tool.possible_tools = list(TOOL_SCREWDRIVER, TOOL_CROWBAR, TOOL_WIRECUTTER, TOOL_WRENCH, TOOL_WELDER, TOOL_MULTITOOL, TOOL_SCALPEL, TOOL_SAW, TOOL_RETRACTOR, TOOL_HEMOSTAT, TOOL_DRILL, TOOL_CAUTERY, TOOL_BONESET, TOOL_MINING, TOOL_SHOVEL, TOOL_HATCHET)
+ tool.toolspeed = 0.25
+ if(PSI_RANK_GRANDMASTER)
+ tool.possible_tools = list(TOOL_SCREWDRIVER, TOOL_CROWBAR, TOOL_WIRECUTTER, TOOL_WRENCH, TOOL_SCALPEL, TOOL_SAW, TOOL_RETRACTOR, TOOL_HEMOSTAT, TOOL_DRILL, TOOL_CAUTERY, TOOL_MINING, TOOL_SHOVEL, TOOL_HATCHET)
+ tool.toolspeed = 0.5
+ if(PSI_RANK_MASTER)
+ tool.possible_tools = list(TOOL_SCREWDRIVER, TOOL_CROWBAR, TOOL_WIRECUTTER, TOOL_WRENCH, TOOL_SCALPEL, TOOL_RETRACTOR, TOOL_HEMOSTAT, TOOL_MINING, TOOL_SHOVEL, TOOL_HATCHET)
+ tool.toolspeed = 1
+ if(PSI_RANK_OPERANT)
+ tool.possible_tools = list(TOOL_SCREWDRIVER, TOOL_CROWBAR, TOOL_WRENCH, TOOL_MINING)
+ tool.toolspeed = 1.5
+ return tool
+
+/datum/psionic_power/psychokinesis/telekinesis
+ name = "Telekinesis"
+ cost = 5
+ cooldown = 1 SECONDS
+ use_ranged = TRUE
+ use_manifest = FALSE
+ min_rank = PSI_RANK_GRANDMASTER
+ use_description = "Click on a distant target while on grab intent to manifest a psychokinetic grip. Use it manipulate objects at a distance."
+ admin_log = FALSE
+ use_sound = 'sound/effects/psi/power_used.ogg'
+ var/global/list/valid_machine_types = list(
+ /obj/machinery/door
+ )
+
+/datum/psionic_power/psychokinesis/telekinesis/invoke(var/mob/living/user, var/mob/living/target)
+ if(user.a_intent != INTENT_GRAB)
+ return FALSE
+ . = ..()
+ if(.)
+
+ var/distance = get_dist(user, target)
+ if(distance > user.psi.get_rank(PSI_PSYCHOKINESIS) * 2)
+ to_chat(user, span_warning("Your telekinetic power won't reach that far."))
+ return FALSE
+
+ if(istype(target, /mob) || istype(target, /obj))
+ var/obj/item/psychic_power/telekinesis/tk = new(user)
+ if(tk.set_focus(target))
+ tk.sparkle()
+ user.visible_message("\The [user] reaches out.")
+ return tk
+ else if(istype(target, /obj/structure))
+ user.visible_message("\The [user] makes a strange gesture.")
+ var/obj/O = target
+ O.attack_hand(user)
+ return TRUE
+ else if(istype(target, /obj/machinery))
+ for(var/mtype in valid_machine_types)
+ if(istype(target, mtype))
+ var/obj/machinery/machine = target
+ machine.attack_hand(user)
+ return TRUE
+ return FALSE
diff --git a/code/modules/psionics/faculties/redaction.dm b/code/modules/psionics/faculties/redaction.dm
new file mode 100644
index 000000000000..f4547e656568
--- /dev/null
+++ b/code/modules/psionics/faculties/redaction.dm
@@ -0,0 +1,166 @@
+/datum/psionic_faculty/redaction
+ id = PSI_REDACTION
+ name = "Redaction"
+ associated_intent = INTENT_HELP
+ armour_types = list(BIO, RAD)
+
+/datum/psionic_power/redaction
+ faculty = PSI_REDACTION
+ admin_log = FALSE
+
+/datum/psionic_power/redaction/proc/check_dead(var/mob/living/target)
+ if(!istype(target))
+ return FALSE
+ if(target.stat == DEAD || HAS_TRAIT(target, TRAIT_FAKEDEATH))
+ return TRUE
+ return FALSE
+
+/datum/psionic_power/redaction/invoke(var/mob/living/user, var/mob/living/target)
+ if(check_dead(target))
+ return FALSE
+ . = ..()
+
+/datum/psionic_power/redaction/skinsight
+ name = "Skinsight"
+ cost = 3
+ heat = 1
+ cooldown = 3 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_OPERANT
+ use_description = "Grab a patient, target the chest, then switch to help intent and use the grab on them to perform a health scan."
+
+/datum/psionic_power/redaction/skinsight/invoke(var/mob/living/user, var/mob/living/target)
+ if(!istype(target) || user.zone_selected != BODY_ZONE_CHEST || target.pulledby == user)
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message(span_notice("\The [user] rests a hand on \the [target]."))
+ healthscan(user, target, user.psi.get_rank(PSI_REDACTION) >= PSI_RANK_GRANDMASTER)
+ return TRUE
+
+/datum/psionic_power/redaction/mend
+ name = "Mend"
+ cost = 7
+ heat = 10
+ cooldown = 5 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_OPERANT
+ use_description = "Target a patient while on help intent at melee range to mend a variety of maladies, such as bleeding or broken bones. Higher ranks in this faculty allow you to mend a wider range of problems."
+
+/datum/psionic_power/redaction/mend/invoke(var/mob/living/user, var/mob/living/carbon/human/target)
+ if(!istype(user) || !istype(target) || target.pulledby != user || user.grab_state >= GRAB_AGGRESSIVE)
+ return FALSE
+ . = ..()
+ if(.)
+ var/obj/item/bodypart/E = target.get_bodypart(user.zone_selected)
+
+ if(!E)
+ to_chat(user, span_warning("They are missing that limb."))
+ return TRUE
+
+ if(E.status == BODYPART_ROBOTIC)
+ to_chat(user, span_warning("That limb is prosthetic."))
+ return TRUE
+
+ user.visible_message(span_notice("\The [user] rests a hand on \the [target]'s [E.name]..."))
+ to_chat(target, span_notice("A healing warmth suffuses you."))
+
+ var/redaction_rank = user.psi.get_rank(PSI_REDACTION)
+ var/pk_rank = user.psi.get_rank(PSI_PSYCHOKINESIS)
+
+ if(pk_rank >= PSI_RANK_LATENT && redaction_rank >= PSI_RANK_MASTER)
+ var/removal_size = clamp(5-pk_rank, 0, 5)
+ var/valid_objects = list()
+ for(var/obj/item/thing in E.embedded_objects)
+ if(thing.w_class >= removal_size)
+ valid_objects += thing
+ if(LAZYLEN(valid_objects))
+ var/removing = pick(valid_objects)
+ target.remove_embedded_object(removing)
+ to_chat(user, span_notice("You extend a tendril of psychokinetic-redactive power and carefully tease \the [removing] free of \the [E]."))
+ return TRUE
+
+ if(redaction_rank >= PSI_RANK_GRANDMASTER)
+ for(var/obj/item/organ/O in target.internal_organs)
+ if(O.damage > 0)
+ to_chat(user, span_notice("You encourage the damaged tissue of \the [O] to repair itself."))
+ O.applyOrganDamage(-rand(redaction_rank, redaction_rank * 2))
+ return TRUE
+ if(E.get_damage(TRUE))
+ E.heal_damage((redaction_rank * 10), (redaction_rank * 10))
+ to_chat(user, span_notice("You patch up some of the damage to [target]'s [E]."))
+ new /obj/effect/temp_visual/heal(get_turf(target), "#33cc33")
+ return TRUE
+
+ to_chat(user, span_notice("You can find nothing within \the [target]'s [E.name] to mend."))
+ return FALSE
+
+/datum/psionic_power/redaction/cleanse
+ name = "Cleanse"
+ cost = 9
+ heat = 15
+ cooldown = 6 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_GRANDMASTER
+ use_description = "Target a patient while on help intent at melee range to cleanse radiation and genetic damage from a patient."
+
+/datum/psionic_power/redaction/cleanse/invoke(var/mob/living/user, var/mob/living/carbon/human/target)
+ if(!istype(user) || !istype(target) || target.pulledby != user || user.zone_selected != BODY_ZONE_PRECISE_MOUTH)
+ return FALSE
+ . = ..()
+ if(.)
+ // No messages, as Mend procs them even if it fails to heal anything, and Cleanse is always checked after Mend.
+ var/removing = rand(20,25)
+ if(target.radiation)
+ to_chat(user, span_notice("You repair some of the radiation-damaged tissue within \the [target]..."))
+ if(target.radiation > removing)
+ target.radiation -= removing
+ else
+ target.radiation = 0
+ return TRUE
+ if(target.getCloneLoss())
+ to_chat(user, span_notice("You stitch together some of the mangled DNA within \the [target]..."))
+ if(target.getCloneLoss() >= removing)
+ target.adjustCloneLoss(-removing)
+ else
+ target.adjustCloneLoss(-(target.getCloneLoss()))
+ return TRUE
+ to_chat(user, span_notice("You can find no genetic damage or radiation to heal within \the [target]."))
+ return TRUE
+
+/datum/psionic_power/revive
+ name = "Revive"
+ cost = 25
+ heat = 100
+ cooldown = 8 SECONDS
+ use_melee = TRUE
+ min_rank = PSI_RANK_PARAMOUNT
+ faculty = PSI_REDACTION
+ use_description = "Obtain a grab on a dead target, target the head, then select help intent and use the grab against them to attempt to bring them back to life. The process is lengthy and failure is punished harshly."
+ admin_log = FALSE
+
+/datum/psionic_power/revive/invoke(var/mob/living/user, var/mob/living/target)
+ if(!isliving(target) || !istype(target) || user.zone_selected != BODY_ZONE_PRECISE_EYES || target.pulledby != user || user.grab_state < GRAB_AGGRESSIVE)
+ return FALSE
+ . = ..()
+ if(.)
+ if(target.stat != DEAD && !HAS_TRAIT(target, TRAIT_FAKEDEATH))
+ to_chat(user, span_warning("This person is already alive!"))
+ return TRUE
+
+ if(((world.time - target.timeofdeath) > DEFIB_TIME_LIMIT))
+ to_chat(user, span_warning("\The [target] has been dead for too long to revive."))
+ return TRUE
+
+ user.visible_message(span_notice("\The [user] splays out their hands over \the [target]'s body..."))
+ target.notify_ghost_cloning("Your heart is being revived!")
+ target.grab_ghost()
+ if(!do_after(user, 10 SECONDS, target, FALSE))
+ user.psi.backblast(rand(10,25))
+ return TRUE
+
+ to_chat(target, span_notice("Life floods back into your body!"))
+ target.visible_message(span_notice("\The [target] shudders violently!"))
+ target.adjustOxyLoss(-rand(15,20))
+ target.revive()
+ return TRUE
diff --git a/code/modules/psionics/interface/ui.dm b/code/modules/psionics/interface/ui.dm
new file mode 100644
index 000000000000..e891a7b31e39
--- /dev/null
+++ b/code/modules/psionics/interface/ui.dm
@@ -0,0 +1,20 @@
+/obj/screen/psi
+ icon = 'icons/mob/screen_psi.dmi'
+ var/mob/living/owner
+ var/hidden = TRUE
+
+/obj/screen/psi/New(var/mob/living/_owner)
+ loc = null
+ owner = _owner
+ update_icon()
+
+/obj/screen/psi/Destroy()
+ if(owner && owner.client)
+ owner.client.screen -= src
+ . = ..()
+
+/obj/screen/psi/update_icon()
+ if(hidden)
+ invisibility = 101
+ else
+ invisibility = 0
diff --git a/code/modules/psionics/interface/ui_hub.dm b/code/modules/psionics/interface/ui_hub.dm
new file mode 100644
index 000000000000..d23ee62e72a8
--- /dev/null
+++ b/code/modules/psionics/interface/ui_hub.dm
@@ -0,0 +1,98 @@
+/obj/screen/psi/hub
+ name = "Psi"
+ icon_state = "psi_suppressed"
+ screen_loc = "EAST-1:28,CENTER-4:7"
+ hidden = FALSE
+ maptext_x = 6
+ maptext_y = -8
+ var/image/on_cooldown
+ var/mutable_appearance/heat_bar
+ var/mutable_appearance/heat_bar_filling
+ var/list/components
+
+/obj/screen/psi/hub/New(var/mob/living/_owner)
+ on_cooldown = image(icon, "cooldown")
+ heat_bar = mutable_appearance(icon, "heat_bar")
+ heat_bar.pixel_y += 28
+ heat_bar_filling = mutable_appearance(icon, "")
+ heat_bar_filling.pixel_y += 28
+ components = list(
+ new /obj/screen/psi/armour(_owner),
+ new /obj/screen/psi/autoredaction(_owner),
+ new /obj/screen/psi/zorch_harm(_owner),
+ new /obj/screen/psi/limiter(_owner),
+ new /obj/screen/psi/toggle_psi_menu(_owner, src)
+ )
+ ..()
+ START_PROCESSING(SSprocessing, src)
+
+/obj/screen/psi/hub/update_icon()
+ if(!owner.psi)
+ return
+ cut_overlays()
+ icon_state = owner.psi.suppressed ? "psi_suppressed" : "psi_active"
+ if(world.time < owner.psi.next_power_use)
+ add_overlay(on_cooldown)
+ heat_bar_filling.icon_state = "heat_[round(owner.psi.heat / 5, 5)]"
+ switch(owner.psi.heat)
+ if(400 to 500)
+ heat_bar_filling.color = "#FF0033"
+ if(300 to 400)
+ heat_bar_filling.color = "#FF9933"
+ if(100 to 300)
+ heat_bar_filling.color = "#00FF33"
+ if(0 to 100)
+ heat_bar_filling.color = "#6699FF"
+ add_overlay(heat_bar)
+ add_overlay(heat_bar_filling)
+ var/offset = 1
+ for(var/thing in components)
+ var/obj/screen/psi/component = thing
+ component.update_icon()
+ if(!component.invisibility)
+ component.screen_loc = "EAST-[++offset]:28,CENTER-4:7"
+
+/obj/screen/psi/hub/Destroy()
+ STOP_PROCESSING(SSprocessing, src)
+ owner = null
+ for(var/thing in components)
+ qdel(thing)
+ components.Cut()
+ . = ..()
+
+/obj/screen/psi/hub/process()
+ if(!istype(owner))
+ qdel(src)
+ return
+ if(!owner.psi)
+ return
+ maptext = "[round((owner.psi.stamina/owner.psi.max_stamina)*100)]%"
+ update_icon()
+
+/obj/screen/psi/hub/MouseEntered(location, control, params)
+ . = ..()
+ openToolTip(usr, src, params, title = "[owner.mind.name]'s Psi Complexus", content = "Stamina: [(owner.psi.stamina/owner.psi.max_stamina)*100]%\nHeat: [owner.psi.heat]\nStuned: [owner.psi.stun ? "True" : "False"]\n")
+
+/obj/screen/psi/hub/MouseExited(location, control, params)
+ . = ..()
+ closeToolTip(usr)
+
+/obj/screen/psi/hub/Click(var/location, var/control, var/params)
+ var/list/click_params = params2list(params)
+ if(click_params["shift"])
+ owner.show_psi_assay(owner)
+ return
+
+ if(owner.psi.suppressed && owner.psi.stun)
+ to_chat(owner, "You are dazed and reeling, and cannot muster enough focus to do that!")
+ return
+
+ owner.psi.suppressed = !owner.psi.suppressed
+ to_chat(owner, "You are [owner.psi.suppressed ? "now suppressing" : "no longer suppressing"] your psi-power.")
+ if(owner.psi.suppressed)
+ owner.psi.cancel()
+ owner.psi.hide_auras()
+ else
+ owner.playsound_local(soundin = 'sound/effects/psi/power_unlock.ogg')
+ owner.psi.show_auras()
+ update_icon()
diff --git a/code/modules/psionics/interface/ui_toggles.dm b/code/modules/psionics/interface/ui_toggles.dm
new file mode 100644
index 000000000000..da9ae39a873b
--- /dev/null
+++ b/code/modules/psionics/interface/ui_toggles.dm
@@ -0,0 +1,114 @@
+// Begin psi armour toggle.
+/obj/screen/psi/armour
+ name = "Psi-Armour"
+ icon_state = "psiarmour_off"
+
+/obj/screen/psi/armour/update_icon()
+ ..()
+ if(invisibility == 0)
+ icon_state = owner.psi.use_psi_armour ? "psiarmour_on" : "psiarmour_off"
+
+/obj/screen/psi/armour/Click()
+ if(!owner.psi)
+ return
+ owner.psi.use_psi_armour = !owner.psi.use_psi_armour
+ to_chat(owner, span_notice("You will [owner.psi.use_psi_armour ? "no longer" : "now"] use your psionics to deflect or block incoming attacks."))
+ update_icon()
+
+// End psi armour toggle.
+
+// Begin autoredaction toggle.
+/obj/screen/psi/autoredaction
+ name = "Autoredaction"
+ icon_state = "healing_off"
+
+/obj/screen/psi/autoredaction/update_icon()
+ ..()
+ if(invisibility == 0)
+ icon_state = owner.psi.use_autoredaction ? "healing_on" : "healing_off"
+
+/obj/screen/psi/autoredaction/Click()
+ if(!owner.psi)
+ return
+ owner.psi.use_autoredaction = !owner.psi.use_autoredaction
+ to_chat(owner, span_notice("You will [owner.psi.use_autoredaction ? "now" : "no longer"] use your psionics to regenerate."))
+ update_icon()
+
+// End autoredaction toggle.
+
+// Begin zorch harm toggle.
+/obj/screen/psi/zorch_harm
+ name = "Zorch Mode"
+ icon_state = "zorch_disable"
+
+/obj/screen/psi/zorch_harm/update_icon()
+ ..()
+ if(invisibility == 0)
+ icon_state = owner.psi.zorch_harm ? "zorch_harm" : "zorch_disable"
+
+/obj/screen/psi/zorch_harm/Click()
+ if(!owner.psi)
+ return
+ owner.psi.zorch_harm = !owner.psi.zorch_harm
+ to_chat(owner, span_notice("You will now fire [owner.psi.zorch_harm ? "lethal" : "non-lethal"] lasers with your psionics."))
+ update_icon()
+
+// End zorch harm toggle.
+
+// Begin limiter toggle.
+/obj/screen/psi/limiter
+ name = "Psi-Limiter"
+ icon_state = "limiter_100"
+
+/obj/screen/psi/limiter/update_icon()
+ ..()
+ if(invisibility == 0)
+ switch(owner.psi.limiter)
+ if(100)
+ icon_state = "limiter_100"
+ if(300)
+ icon_state = "limiter_300"
+ if(INFINITY)
+ icon_state = "limiter_500"
+
+/obj/screen/psi/limiter/Click()
+ if(!owner.psi)
+ return
+ switch(owner.psi.limiter)
+ if(100)
+ owner.psi.limiter = 300
+ if(300)
+ owner.psi.limiter = INFINITY
+ if(INFINITY)
+ owner.psi.limiter = 100
+ if(owner.psi.limiter == INFINITY)
+ to_chat(owner, span_warning("You release your self imposed shackles!"))
+ else
+ to_chat(owner, span_notice("Your mental limiters will stop you at [owner.psi.limiter] heat."))
+ update_icon()
+
+// End limiter toggle.
+
+// Menu toggle.
+/obj/screen/psi/toggle_psi_menu
+ name = "Show/Hide Psi UI"
+ icon_state = "arrow_left"
+ var/obj/screen/psi/hub/controller
+
+/obj/screen/psi/toggle_psi_menu/New(var/mob/living/_owner, var/obj/screen/psi/hub/_controller)
+ controller = _controller
+ ..(_owner)
+
+/obj/screen/psi/toggle_psi_menu/Click()
+ var/set_hidden = !hidden
+ for(var/thing in controller.components)
+ var/obj/screen/psi/psi = thing
+ psi.hidden = set_hidden
+ controller.update_icon()
+
+/obj/screen/psi/toggle_psi_menu/update_icon()
+ if(hidden)
+ icon_state = "arrow_left"
+ else
+ icon_state = "arrow_right"
+// End menu toggle.
diff --git a/code/modules/psionics/mob/mob.dm b/code/modules/psionics/mob/mob.dm
new file mode 100644
index 000000000000..c2f576780f78
--- /dev/null
+++ b/code/modules/psionics/mob/mob.dm
@@ -0,0 +1,20 @@
+/mob/living
+ var/datum/psi_complexus/psi
+
+/mob/living/Login()
+ . = ..()
+ if(psi)
+ psi.update(TRUE)
+ if(!psi.suppressed)
+ psi.show_auras()
+
+/mob/living/Destroy()
+ QDEL_NULL(psi)
+ . = ..()
+
+/mob/living/proc/set_psi_rank(faculty, rank, take_larger, defer_update, temporary)
+ if(!psi)
+ psi = new(src)
+ var/current_rank = psi.get_rank(faculty)
+ if(current_rank != rank && (!take_larger || current_rank < rank))
+ psi.set_rank(faculty, rank, defer_update, temporary)
diff --git a/code/modules/psionics/mob/mob_assay.dm b/code/modules/psionics/mob/mob_assay.dm
new file mode 100644
index 000000000000..f1631c4a1b7b
--- /dev/null
+++ b/code/modules/psionics/mob/mob_assay.dm
@@ -0,0 +1,79 @@
+/mob/living/proc/show_psi_assay(var/mob/viewer)
+
+ if(!viewer) viewer = usr
+
+ var/use_He_is = "You are"
+ var/use_He_has = "You have"
+ if(istype(machine) || viewer != src)
+ use_He_is = "[p_they(TRUE)] [p_are()]"
+ use_He_has = "[p_they(TRUE)] [p_have()]"
+
+ var/list/dat = list()
+
+ dat += "Summary
"
+ dat += "
"
+
+ if(psi)
+
+ // Hi Warhammer 40k rating system, how are you?
+ // I hope you get along with the Galactic Milieu metapsychics.
+ var/use_rating
+ var/effective_rating = psi.rating
+ if(effective_rating > 1 && psi.suppressed)
+ effective_rating = max(0, psi.rating-2)
+ var/rating_descriptor
+ /* FIX THIS
+ if(viewer != usr && thralls?.mind?.has_antag_datum() && ishuman(viewer))
+ var/mob/living/H = viewer
+ if(H.psi && H.psi.get_rank(PSI_REDACTION) >= PSI_RANK_GRANDMASTER)
+ dat += "Their mind has been cored like an apple, and enslaved by another operant psychic."
+ */
+ if(!use_rating)
+ switch(effective_rating)
+ if(1)
+ use_rating = "[effective_rating]-Epsilon"
+ rating_descriptor = "This indicates the presence of minor latent psi potential with little or no operant capabilities."
+ if(2)
+ use_rating = "[effective_rating]-Delta"
+ rating_descriptor = "This indicates the presence of minor psi capabilities of the Operant rank or higher."
+ if(3)
+ use_rating = "[effective_rating]-Gamma"
+ rating_descriptor = "This indicates the presence of psi capabilities of the Master rank or higher."
+ if(4)
+ use_rating = "[effective_rating]-Beta"
+ rating_descriptor = "This indicates the presence of significant psi capabilities of the Grandmaster rank or higher."
+ if(5)
+ use_rating = "[effective_rating]-Alpha"
+ rating_descriptor = "This indicates the presence of major psi capabilities of the Paramount Grandmaster rank or higher."
+ else
+ use_rating = "[effective_rating]-Lambda"
+ rating_descriptor = "This indicates the presence of trace latent psi capabilities."
+
+ dat += "[use_He_has] an overall psi rating of [use_rating].
[rating_descriptor]
"
+
+ dat += "[use_He_is] currently [psi.suppressed ? "suppressing" : "not suppressing"] your psychic operancy.
"
+ dat += "[use_He_has] [psi.stamina]/[psi.max_stamina] psi stamina remaining.
"
+ dat += "
"
+
+ for(var/faculty_id in psi.ranks)
+ var/datum/psionic_faculty/faculty = SSpsi.get_faculty(faculty_id)
+ if(psi.ranks[faculty.id] > 0)
+ dat += "[use_He_is] assayed at the rank of [GLOB.psychic_ranks_to_strings[psi.ranks[faculty.id]]] for the [faculty.name] faculty.
"
+ else
+ dat += "[use_He_has] no notable power within the [faculty.name] faculty.
"
+ dat += "
"
+
+ if(viewer == usr)
+ dat += "Psi-power Usage |
"
+ for(var/faculty_id in psi.ranks)
+ var/list/check_powers = psi.get_powers_by_faculty(faculty_id)
+ if(LAZYLEN(check_powers))
+ var/datum/psionic_faculty/faculty = SSpsi.get_faculty(faculty_id)
+ dat += "| [use_He_has] access to the following psi-powers within the [faculty.name] faculty: |
"
+ for(var/datum/psionic_power/power in check_powers)
+ dat += "| [power.name] | [power.use_description] |
"
+ dat += "
"
+
+ var/datum/browser/popup = new(viewer, "psi_assay_\ref[src]", "Psi-Assay")
+ popup.set_content(jointext(dat,null))
+ popup.open()
diff --git a/code/modules/psionics/null/_null.dm b/code/modules/psionics/null/_null.dm
new file mode 100644
index 000000000000..3b4b09e07253
--- /dev/null
+++ b/code/modules/psionics/null/_null.dm
@@ -0,0 +1,28 @@
+/atom/proc/disrupts_psionics()
+ for(var/atom/movable/AM in contents)
+ if(!istype(AM) || AM == src)
+ continue
+ var/disrupted_by = AM.disrupts_psionics()
+ if(disrupted_by)
+ return disrupted_by
+ return FALSE
+
+/atom/proc/do_psionics_check(var/stress, var/atom/source)
+ var/turf/T = get_turf(src)
+ if(istype(T) && T != src)
+ var/V = T.do_psionics_check(stress, source)
+ if(V)
+ return V
+ stress = withstand_psi_stress(stress, source)
+ var/V = disrupts_psionics()
+ return V
+
+/atom/proc/withstand_psi_stress(var/stress, var/atom/source)
+ . = max(stress, 0)
+ if(.)
+ for(var/thing in contents)
+ var/atom/movable/AM = thing
+ if(istype(AM) && AM != src && AM.disrupts_psionics())
+ . = AM.withstand_psi_stress(., source)
+ if(. <= 0)
+ break
diff --git a/code/modules/psionics/null/chemistry.dm b/code/modules/psionics/null/chemistry.dm
new file mode 100644
index 000000000000..e26dbe3a7013
--- /dev/null
+++ b/code/modules/psionics/null/chemistry.dm
@@ -0,0 +1,37 @@
+/datum/reagent/crystal
+ name = "crystallizing agent"
+ taste_description = "sharpness"
+ reagent_state = LIQUID
+ color = "#13bc5e"
+/*
+/datum/reagent/crystal/affect_blood(var/mob/living/carbon/M, var/alien, var/removed)
+ var/result_mat = (M.psi || (M.mind && GLOB.wizards.is_antagonist(M.mind))) ? MATERIAL_NULLGLASS : MATERIAL_CRYSTAL
+ if(ishuman(M))
+ var/mob/living/carbon/human/H = M
+ if(prob(5))
+ var/obj/item/organ/external/E = pick(H.organs)
+ if(!E || E.is_stump() || BP_IS_ROBOTIC(E))
+ return
+ if(BP_IS_CRYSTAL(E))
+ E.heal_damage(rand(3,5), rand(3,5))
+ if(BP_IS_BRITTLE(E) && prob(5))
+ E.status &= ~ORGAN_BRITTLE
+ else if(E.organ_tag != BP_CHEST && E.organ_tag != BP_GROIN)
+ to_chat(H, SPAN_DANGER("Your [E.name] is being lacerated from within!"))
+ if(H.can_feel_pain())
+ H.emote("scream")
+ if(prob(25))
+ for(var/i = 1 to rand(3,5))
+ new /obj/item/weapon/material/shard(get_turf(E), result_mat)
+ E.droplimb(0, DROPLIMB_BLUNT)
+ else
+ E.take_external_damage(rand(20,30), 0)
+ E.status |= ORGAN_CRYSTAL
+ E.status |= ORGAN_BRITTLE
+ return
+
+ to_chat(M, SPAN_DANGER("Your flesh is being lacerated from within!"))
+ M.adjustBruteLoss(rand(3,6))
+ if(prob(10))
+ new /obj/item/weapon/material/shard(get_turf(M), result_mat)
+*/
diff --git a/code/modules/psionics/null/flooring.dm b/code/modules/psionics/null/flooring.dm
new file mode 100644
index 000000000000..b7751417da57
--- /dev/null
+++ b/code/modules/psionics/null/flooring.dm
@@ -0,0 +1,17 @@
+/turf/open/floor
+ var/psi_null
+
+/turf/open/floor/disrupts_psionics()
+ return (psi_null ? src : FALSE)
+
+/turf/open/floor/nullglass
+ name = "nullglass plating"
+ desc = "You can hear the tiles whispering..."
+ icon_state = "light_off"
+ psi_null = TRUE
+ floor_tile = /obj/item/stack/tile/mineral/nullglass
+
+/obj/item/stack/tile/mineral/nullglass
+ name = "nullglass floor tile"
+ icon_state = "tile_e"
+ turf_type = /turf/open/floor/nullglass
diff --git a/code/modules/reagents/chemistry/reagents/drug_reagents.dm b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
index 1390faa4684a..a35aa17f082f 100644
--- a/code/modules/reagents/chemistry/reagents/drug_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
@@ -572,3 +572,80 @@
if(prob(15))
M.adjustToxLoss(2, 0)
..()
+
+/datum/reagent/drug/three_eye
+ name = "Three Eye"
+ taste_description = "liquid starlight"
+ description = "Three Eye is one of the most notorious narcotics to ever come out of the independant habitats, allowing those who take it to see through walls."
+ reagent_state = LIQUID
+ color = "#ccccff"
+ metabolization_rate = REAGENTS_METABOLISM
+ overdose_threshold = 25
+
+ // M A X I M U M C H E E S E
+ var/global/list/dose_messages = list(
+ "Your name is called. It is your time.",
+ "You are dissolving. Your hands are wax...",
+ "It all runs together. It all mixes.",
+ "It is done. It is over. You are done. You are over.",
+ "You won't forget. Don't forget. Don't forget.",
+ "Light seeps across the edges of your vision...",
+ "Something slides and twitches within your sinus cavity...",
+ "Your bowels roil. It waits within.",
+ "Your gut churns. You are heavy with potential.",
+ "Your heart flutters. It is winged and caged in your chest.",
+ "There is a precious thing, behind your eyes.",
+ "Everything is ending. Everything is beginning.",
+ "Nothing ends. Nothing begins.",
+ "Wake up. Please wake up.",
+ "Stop it! You're hurting them!",
+ "It's too soon for this. Please go back.",
+ "We miss you. Where are you?",
+ "Come back from there. Please."
+ )
+
+ var/global/list/overdose_messages = list(
+ "THE SIGNAL THE SIGNAL THE SIGNAL THE SIGNAL",
+ "IT CRIES IT CRIES IT WAITS IT CRIES",
+ "NOT YOURS NOT YOURS NOT YOURS NOT YOURS",
+ "THAT IS NOT FOR YOU",
+ "IT RUNS IT RUNS IT RUNS IT RUNS",
+ "THE BLOOD THE BLOOD THE BLOOD THE BLOOD",
+ "THE LIGHT THE DARK A STAR IN CHAINS"
+ )
+
+/datum/reagent/drug/three_eye/on_mob_metabolize(mob/living/L)
+ ..()
+ ADD_TRAIT(L, TRAIT_XRAY_VISION, type)
+ L.add_client_colour(/datum/client_colour/thirdeye)
+ L.update_sight()
+
+/datum/reagent/drug/three_eye/on_mob_end_metabolize(mob/living/L)
+ REMOVE_TRAIT(L, TRAIT_XRAY_VISION, type)
+ L.remove_client_colour(/datum/client_colour/thirdeye)
+ L.update_sight()
+ ..()
+
+/datum/reagent/drug/three_eye/on_mob_life(mob/living/carbon/M)
+ M.hallucination += 50
+ M.Jitter(3)
+ M.Dizzy(3)
+ if(prob(0.1))
+ M.visible_message(span_danger("[M] starts having a seizure!"), span_userdanger("You have a seizure!"))
+ M.Unconscious(100)
+ M.Jitter(350)
+ M.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(8, 12))
+ if(prob(5))
+ to_chat(M, span_warning("[pick(dose_messages)]"))
+
+/datum/reagent/drug/three_eye/overdose_process(mob/living/M)
+ ..()
+ M.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(1, 5))
+ if(prob(10))
+ M.visible_message(span_danger("[M] starts having a seizure!"), span_userdanger("You have a seizure!"))
+ M.Unconscious(100)
+ M.Jitter(350)
+ if(prob(10))
+ to_chat(M, span_danger("[pick(overdose_messages)]"))
+ if(M.psi)
+ M.psi.check_latency_trigger(30, "a Three Eye overdose")
diff --git a/code/modules/reagents/reagent_containers/pill.dm b/code/modules/reagents/reagent_containers/pill.dm
index 06f9225306a9..aadec1c06f79 100644
--- a/code/modules/reagents/reagent_containers/pill.dm
+++ b/code/modules/reagents/reagent_containers/pill.dm
@@ -239,6 +239,12 @@
icon_state = "pill_happy"
list_reagents = list(/datum/reagent/drug/happiness = 10)
+/obj/item/reagent_containers/pill/three_eye
+ name = "strange pill"
+ desc = "The surface of this unlabelled pill crawls against your skin."
+ icon_state = "pill12"
+ list_reagents = list(/datum/reagent/drug/three_eye = 10)
+
/obj/item/reagent_containers/pill/floorpill
name = "floorpill"
desc = "A strange pill found in the depths of maintenance."
diff --git a/icons/effects/psi_aura_small.dmi b/icons/effects/psi_aura_small.dmi
new file mode 100644
index 000000000000..f5e222b61f5f
Binary files /dev/null and b/icons/effects/psi_aura_small.dmi differ
diff --git a/icons/mob/clothing/head/head.dmi b/icons/mob/clothing/head/head.dmi
index b11532a147c3..3a16bbb7e43a 100644
Binary files a/icons/mob/clothing/head/head.dmi and b/icons/mob/clothing/head/head.dmi differ
diff --git a/icons/mob/inhands/misc/sheets_lefthand.dmi b/icons/mob/inhands/misc/sheets_lefthand.dmi
index 96310e6a934e..4dc3b21bf832 100644
Binary files a/icons/mob/inhands/misc/sheets_lefthand.dmi and b/icons/mob/inhands/misc/sheets_lefthand.dmi differ
diff --git a/icons/mob/inhands/misc/sheets_righthand.dmi b/icons/mob/inhands/misc/sheets_righthand.dmi
index d0823c6a2611..0d05e0544e3a 100644
Binary files a/icons/mob/inhands/misc/sheets_righthand.dmi and b/icons/mob/inhands/misc/sheets_righthand.dmi differ
diff --git a/icons/mob/inhands/weapons/swords_lefthand.dmi b/icons/mob/inhands/weapons/swords_lefthand.dmi
index 091353292e20..2d2d081edf1f 100644
Binary files a/icons/mob/inhands/weapons/swords_lefthand.dmi and b/icons/mob/inhands/weapons/swords_lefthand.dmi differ
diff --git a/icons/mob/inhands/weapons/swords_righthand.dmi b/icons/mob/inhands/weapons/swords_righthand.dmi
index 9c0d85941426..bfe77ced9abf 100644
Binary files a/icons/mob/inhands/weapons/swords_righthand.dmi and b/icons/mob/inhands/weapons/swords_righthand.dmi differ
diff --git a/icons/mob/screen_psi.dmi b/icons/mob/screen_psi.dmi
new file mode 100644
index 000000000000..44b3941ead8b
Binary files /dev/null and b/icons/mob/screen_psi.dmi differ
diff --git a/icons/obj/ammo.dmi b/icons/obj/ammo.dmi
index 0a1f20a36c4f..a276f10aef60 100644
Binary files a/icons/obj/ammo.dmi and b/icons/obj/ammo.dmi differ
diff --git a/icons/obj/clothing/hats.dmi b/icons/obj/clothing/hats.dmi
index 2be963bc260a..7b6256618521 100644
Binary files a/icons/obj/clothing/hats.dmi and b/icons/obj/clothing/hats.dmi differ
diff --git a/icons/obj/implants.dmi b/icons/obj/implants.dmi
index 9f9d9f06515a..e827508e7915 100644
Binary files a/icons/obj/implants.dmi and b/icons/obj/implants.dmi differ
diff --git a/icons/obj/psychic_powers.dmi b/icons/obj/psychic_powers.dmi
new file mode 100644
index 000000000000..e527fe580588
Binary files /dev/null and b/icons/obj/psychic_powers.dmi differ
diff --git a/icons/obj/shards.dmi b/icons/obj/shards.dmi
index 94d1602fa007..c4b6b618c45a 100644
Binary files a/icons/obj/shards.dmi and b/icons/obj/shards.dmi differ
diff --git a/icons/obj/tiles.dmi b/icons/obj/tiles.dmi
index b69c131a49c9..b5bfcc67fe88 100644
Binary files a/icons/obj/tiles.dmi and b/icons/obj/tiles.dmi differ
diff --git a/icons/obj/weapons/spears.dmi b/icons/obj/weapons/spears.dmi
index 24fe42c47e62..5d7031bd8bc2 100644
Binary files a/icons/obj/weapons/spears.dmi and b/icons/obj/weapons/spears.dmi differ
diff --git a/icons/obj/weapons/swords.dmi b/icons/obj/weapons/swords.dmi
index 460e561a27dc..4ff96f5a9b6a 100644
Binary files a/icons/obj/weapons/swords.dmi and b/icons/obj/weapons/swords.dmi differ
diff --git a/sound/effects/psi/power_evoke.ogg b/sound/effects/psi/power_evoke.ogg
new file mode 100644
index 000000000000..37d9c5a54013
Binary files /dev/null and b/sound/effects/psi/power_evoke.ogg differ
diff --git a/sound/effects/psi/power_fabrication.ogg b/sound/effects/psi/power_fabrication.ogg
new file mode 100644
index 000000000000..8720d196a80f
Binary files /dev/null and b/sound/effects/psi/power_fabrication.ogg differ
diff --git a/sound/effects/psi/power_fail.ogg b/sound/effects/psi/power_fail.ogg
new file mode 100644
index 000000000000..75364171dd3f
Binary files /dev/null and b/sound/effects/psi/power_fail.ogg differ
diff --git a/sound/effects/psi/power_feedback.ogg b/sound/effects/psi/power_feedback.ogg
new file mode 100644
index 000000000000..139dfba0bd46
Binary files /dev/null and b/sound/effects/psi/power_feedback.ogg differ
diff --git a/sound/effects/psi/power_unlock.ogg b/sound/effects/psi/power_unlock.ogg
new file mode 100644
index 000000000000..3ba24f81b38d
Binary files /dev/null and b/sound/effects/psi/power_unlock.ogg differ
diff --git a/sound/effects/psi/power_used.ogg b/sound/effects/psi/power_used.ogg
new file mode 100644
index 000000000000..aa0978f14bca
Binary files /dev/null and b/sound/effects/psi/power_used.ogg differ
diff --git a/sound/weapons/psisword.ogg b/sound/weapons/psisword.ogg
new file mode 100644
index 000000000000..a89b39723034
Binary files /dev/null and b/sound/weapons/psisword.ogg differ
diff --git a/yogstation.dme b/yogstation.dme
index 9ade5c068bfe..50967948b228 100644
--- a/yogstation.dme
+++ b/yogstation.dme
@@ -89,6 +89,7 @@
#include "code\__DEFINES\preferences.dm"
#include "code\__DEFINES\procpath.dm"
#include "code\__DEFINES\profile.dm"
+#include "code\__DEFINES\psi.dm"
#include "code\__DEFINES\qdel.dm"
#include "code\__DEFINES\radiation.dm"
#include "code\__DEFINES\radio.dm"
@@ -335,6 +336,7 @@
#include "code\controllers\subsystem\processing\obj.dm"
#include "code\controllers\subsystem\processing\processing.dm"
#include "code\controllers\subsystem\processing\projectiles.dm"
+#include "code\controllers\subsystem\processing\psi.dm"
#include "code\controllers\subsystem\processing\quirks.dm"
#include "code\controllers\subsystem\processing\slowprocess.dm"
#include "code\controllers\subsystem\processing\station.dm"
@@ -1096,6 +1098,7 @@
#include "code\game\objects\items\implants\implant_mindshield.dm"
#include "code\game\objects\items\implants\implant_mindshieldtot.dm"
#include "code\game\objects\items\implants\implant_misc.dm"
+#include "code\game\objects\items\implants\implant_psi.dm"
#include "code\game\objects\items\implants\implant_spell.dm"
#include "code\game\objects\items\implants\implant_stealth.dm"
#include "code\game\objects\items\implants\implant_storage.dm"
@@ -1603,6 +1606,7 @@
#include "code\modules\antagonists\nukeop\equipment\nuclearbomb.dm"
#include "code\modules\antagonists\nukeop\equipment\pinpointer.dm"
#include "code\modules\antagonists\official\official.dm"
+#include "code\modules\antagonists\paramount\paramount.dm"
#include "code\modules\antagonists\pirate\pirate.dm"
#include "code\modules\antagonists\revenant\revenant.dm"
#include "code\modules\antagonists\revenant\revenant_abilities.dm"
@@ -1616,6 +1620,7 @@
#include "code\modules\antagonists\slaughter\slaughterevent.dm"
#include "code\modules\antagonists\space_dragon\space_dragon.dm"
#include "code\modules\antagonists\swarmer\swarmer.dm"
+#include "code\modules\antagonists\thrall\thrall.dm"
#include "code\modules\antagonists\traitor\datum_traitor.dm"
#include "code\modules\antagonists\traitor\syndicate_contract.dm"
#include "code\modules\antagonists\traitor\brother\traitor_bro.dm"
@@ -2660,6 +2665,7 @@
#include "code\modules\modular_computers\file_system\programs\ntnrc_client.dm"
#include "code\modules\modular_computers\file_system\programs\portrait_printer.dm"
#include "code\modules\modular_computers\file_system\programs\powermonitor.dm"
+#include "code\modules\modular_computers\file_system\programs\psi_monitor.dm"
#include "code\modules\modular_computers\file_system\programs\radar.dm"
#include "code\modules\modular_computers\file_system\programs\robocontrol.dm"
#include "code\modules\modular_computers\file_system\programs\robotact.dm"
@@ -2910,6 +2916,34 @@
#include "code\modules\projectiles\projectile\special\rocket.dm"
#include "code\modules\projectiles\projectile\special\temperature.dm"
#include "code\modules\projectiles\projectile\special\wormhole.dm"
+#include "code\modules\psionics\complexus\complexus.dm"
+#include "code\modules\psionics\complexus\complexus_helpers.dm"
+#include "code\modules\psionics\complexus\complexus_latency.dm"
+#include "code\modules\psionics\complexus\complexus_power_cache.dm"
+#include "code\modules\psionics\complexus\complexus_process.dm"
+#include "code\modules\psionics\equipment\cerebro_enhancers.dm"
+#include "code\modules\psionics\equipment\psipower.dm"
+#include "code\modules\psionics\equipment\psipower_blade.dm"
+#include "code\modules\psionics\equipment\psipower_tinker.dm"
+#include "code\modules\psionics\equipment\psipower_tk.dm"
+#include "code\modules\psionics\events\_psi.dm"
+#include "code\modules\psionics\events\mini_spasm.dm"
+#include "code\modules\psionics\events\psi_balm.dm"
+#include "code\modules\psionics\events\psi_wail.dm"
+#include "code\modules\psionics\faculties\_faculty.dm"
+#include "code\modules\psionics\faculties\_power.dm"
+#include "code\modules\psionics\faculties\coercion.dm"
+#include "code\modules\psionics\faculties\energistics.dm"
+#include "code\modules\psionics\faculties\psychokinesis.dm"
+#include "code\modules\psionics\faculties\redaction.dm"
+#include "code\modules\psionics\interface\ui.dm"
+#include "code\modules\psionics\interface\ui_hub.dm"
+#include "code\modules\psionics\interface\ui_toggles.dm"
+#include "code\modules\psionics\mob\mob.dm"
+#include "code\modules\psionics\mob\mob_assay.dm"
+#include "code\modules\psionics\null\_null.dm"
+#include "code\modules\psionics\null\chemistry.dm"
+#include "code\modules\psionics\null\flooring.dm"
#include "code\modules\reagents\chem_splash.dm"
#include "code\modules\reagents\reagent_containers.dm"
#include "code\modules\reagents\reagent_dispenser.dm"
diff --git a/yogstation/code/game/gamemodes/shadowling/shadowling.dm b/yogstation/code/game/gamemodes/shadowling/shadowling.dm
index 271118675904..7aa466aaccd8 100644
--- a/yogstation/code/game/gamemodes/shadowling/shadowling.dm
+++ b/yogstation/code/game/gamemodes/shadowling/shadowling.dm
@@ -251,7 +251,7 @@ Made by Xhuis
/mob/living/proc/add_thrall()
if(!istype(mind))
return FALSE
- return mind.add_antag_datum(ANTAG_DATUM_THRALL)
+ return mind.add_antag_datum(ANTAG_DATUM_SHADOWTHRALL)
/mob/living/proc/add_sling()
if(!istype(mind))
@@ -261,7 +261,7 @@ Made by Xhuis
/mob/living/proc/remove_thrall()
if(!istype(mind))
return FALSE
- return mind.remove_antag_datum(ANTAG_DATUM_THRALL)
+ return mind.remove_antag_datum(ANTAG_DATUM_SHADOWTHRALL)
/mob/living/proc/remove_sling()
if(!istype(mind))
diff --git a/yogstation/code/modules/antagonists/shadowling/thrall.dm b/yogstation/code/modules/antagonists/shadowling/thrall.dm
index af3321d21d99..02880bedc770 100644
--- a/yogstation/code/modules/antagonists/shadowling/thrall.dm
+++ b/yogstation/code/modules/antagonists/shadowling/thrall.dm
@@ -1,13 +1,13 @@
GLOBAL_LIST_INIT(thrall_spell_types, typecacheof(list(/obj/effect/proc_holder/spell/self/lesser_shadowling_hivemind, /obj/effect/proc_holder/spell/targeted/lesser_glare, /obj/effect/proc_holder/spell/self/lesser_shadow_walk, /obj/effect/proc_holder/spell/self/thrall_night_vision)))
-/datum/antagonist/thrall
+/datum/antagonist/thrall/shadowling
name = "Shadowling Thrall"
job_rank = ROLE_SHADOWLING
roundend_category = "thralls"
antagpanel_category = "Shadowlings"
antag_moodlet = /datum/mood_event/thrall
-/datum/antagonist/thrall/can_be_owned(datum/mind/new_owner)
+/datum/antagonist/thrall/shadowling/can_be_owned(datum/mind/new_owner)
. = ..()
if(.)
var/list/no_team_antag = list(
@@ -21,7 +21,7 @@ GLOBAL_LIST_INIT(thrall_spell_types, typecacheof(list(/obj/effect/proc_holder/sp
if(NTA.type in no_team_antag)
return FALSE
-/datum/antagonist/thrall/on_gain()
+/datum/antagonist/thrall/shadowling/on_gain()
. = ..()
SSticker.mode.update_shadow_icons_added(owner)
SSticker.mode.thralls += owner
@@ -33,7 +33,7 @@ GLOBAL_LIST_INIT(thrall_spell_types, typecacheof(list(/obj/effect/proc_holder/sp
owner.AddSpell(new /obj/effect/proc_holder/spell/self/lesser_shadow_walk(null))
owner.AddSpell(new /obj/effect/proc_holder/spell/self/thrall_night_vision(null))
-/datum/antagonist/thrall/on_removal()
+/datum/antagonist/thrall/shadowling/on_removal()
SSticker.mode.update_shadow_icons_removed(owner)
SSticker.mode.thralls -= owner
message_admins("[key_name_admin(owner.current)] was dethralled!")
@@ -53,7 +53,7 @@ GLOBAL_LIST_INIT(thrall_spell_types, typecacheof(list(/obj/effect/proc_holder/sp
M.update_sight()
return ..()
-/datum/antagonist/thrall/greet()
+/datum/antagonist/thrall/shadowling/greet()
to_chat(owner, span_shadowling("You see the truth. Reality has been torn away and you realize what a fool you've been."))
if(ispreternis(owner))
to_chat(owner, span_shadowling("The shadowlings- your creators, have returned to become gods. Serve them above all else and ensure they complete their goals."))
@@ -65,5 +65,5 @@ GLOBAL_LIST_INIT(thrall_spell_types, typecacheof(list(/obj/effect/proc_holder/sp
to_chat(owner, span_shadowling("You may communicate with your allies by using the Lesser Commune ability."))
SEND_SOUND(owner.current, sound('yogstation/sound/ambience/antag/thrall.ogg'))
-/datum/antagonist/thrall/roundend_report()
+/datum/antagonist/thrall/shadowling/roundend_report()
return "[printplayer(owner)]"
diff --git a/yogstation/code/modules/jobs/job_types/psychiatrist.dm b/yogstation/code/modules/jobs/job_types/psychiatrist.dm
index 92528a7840a9..57f00371a273 100644
--- a/yogstation/code/modules/jobs/job_types/psychiatrist.dm
+++ b/yogstation/code/modules/jobs/job_types/psychiatrist.dm
@@ -27,6 +27,12 @@
/datum/job/psych/proc/GaxStationChanges() // I'M SORRY
return TRUE
+/datum/job/psych/after_spawn(mob/living/H, mob/M, latejoin = FALSE)
+ . = ..()
+ H.set_psi_rank(PSI_REDACTION, PSI_RANK_OPERANT)
+ if(H.psi)
+ to_chat(M, "You are psionically awakened, part of a tiny minority, and you are the first and only exposure most of the crew will have to the mentally gifted.")
+
/datum/outfit/job/psych
name = "Psych"
jobtype = /datum/job/psych
@@ -36,3 +42,5 @@
l_hand = /obj/item/storage/briefcase
glasses = /obj/item/clothing/glasses/regular
ears = /obj/item/radio/headset/headset_med
+
+ implants = list(/obj/item/implant/psi_control/psych)
diff --git a/yogstation/code/modules/mob/living/carbon/human/species_types/plantpeople.dm b/yogstation/code/modules/mob/living/carbon/human/species_types/plantpeople.dm
index 7c3e8247a871..d8a52ce1d784 100644
--- a/yogstation/code/modules/mob/living/carbon/human/species_types/plantpeople.dm
+++ b/yogstation/code/modules/mob/living/carbon/human/species_types/plantpeople.dm
@@ -52,7 +52,7 @@
C.faction -= "vines"
/datum/species/pod/spec_life(mob/living/carbon/human/H)
- if(H.stat == DEAD || H.stat == UNCONSCIOUS || (H.mind && H.mind.has_antag_datum(ANTAG_DATUM_THRALL)))
+ if(H.stat == DEAD || H.stat == UNCONSCIOUS || (H.mind && H.mind.has_antag_datum(ANTAG_DATUM_SHADOWTHRALL)))
return
if(IS_BLOODSUCKER(H) && !HAS_TRAIT(H, TRAIT_MASQUERADE))
return
diff --git a/yogstation/icons/mob/hud.dmi b/yogstation/icons/mob/hud.dmi
index e3a3a76ce0a2..5ad254eaa04d 100644
Binary files a/yogstation/icons/mob/hud.dmi and b/yogstation/icons/mob/hud.dmi differ
diff --git a/yogstation/icons/obj/stack_objects.dmi b/yogstation/icons/obj/stack_objects.dmi
index 6bcabac71439..b35a1e616435 100644
Binary files a/yogstation/icons/obj/stack_objects.dmi and b/yogstation/icons/obj/stack_objects.dmi differ