"
+
+ var/datum/browser/popup = new(user, "psi_monitor_\ref[src]", "Psi-Monitor")
+ popup.set_content(jointext(dat,null))
+ popup.open()
+
+
+/obj/machinery/psi_monitor/proc/report_failure(obj/item/implant/psi_control/implant)
+ psi_violations += span_redtext("Critical system failure - [implant.imp_in.name].")
+
+/obj/machinery/psi_monitor/proc/report_violation(obj/item/implant/psi_control/implant, stress)
+ psi_violations += "Sigma [round(stress/10)] event - [implant.imp_in.name]."
diff --git a/code/modules/projectiles/ammunition/ballistic/revolver.dm b/code/modules/projectiles/ammunition/ballistic/revolver.dm
index aa98196f2cadc..d3ce06a1db12b 100644
--- a/code/modules/projectiles/ammunition/ballistic/revolver.dm
+++ b/code/modules/projectiles/ammunition/ballistic/revolver.dm
@@ -42,6 +42,13 @@
caliber = CALIBER_44MAG
projectile_type = /obj/projectile/bullet/m44
+// revolver?
+
+/obj/item/ammo_casing/a357/nullglass
+ name = ".357 NULL bullet casing"
+ desc = "A .357 NULL bullet casing."
+ projectile_type = /obj/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 de7538c5a49ec..d032fbc7a2dd6 100644
--- a/code/modules/projectiles/ammunition/ballistic/shotgun.dm
+++ b/code/modules/projectiles/ammunition/ballistic/shotgun.dm
@@ -143,6 +143,14 @@
pellets = 4
variance = 30
+/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/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 948f6e3c2c34b..d404af2b66111 100644
--- a/code/modules/projectiles/boxes_magazines/ammo_boxes.dm
+++ b/code/modules/projectiles/boxes_magazines/ammo_boxes.dm
@@ -55,6 +55,13 @@
max_ammo = 6
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
+
// .38 special loaders
/obj/item/ammo_box/c38
diff --git a/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm b/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm
index aa8e6514d69fa..4d687d61a9f1e 100644
--- a/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm
+++ b/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm
@@ -59,3 +59,6 @@
if(!give_round(new load_type(src)))
break
update_appearance(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.dm b/code/modules/projectiles/guns/ballistic.dm
index 9c5044a1a94b4..d6b295f2ffe24 100644
--- a/code/modules/projectiles/guns/ballistic.dm
+++ b/code/modules/projectiles/guns/ballistic.dm
@@ -588,7 +588,7 @@ GLOBAL_LIST_INIT(gun_saw_types, typecacheof(list(
///Handles all the logic of sawing off guns,
/obj/item/gun/ballistic/proc/sawoff(mob/user, obj/item/saw)
- if(!saw.is_sharp() || !is_type_in_typecache(saw, GLOB.gun_saw_types)) //needs to be sharp. Otherwise turned off eswords can cut this.
+ if((!saw.is_sharp() || !is_type_in_typecache(saw, GLOB.gun_saw_types)) && (saw.tool_behaviour != TOOL_SAW)) //needs to be sharp. Otherwise turned off eswords can cut this.
return
if(sawn_off)
to_chat(user, span_warning("\The [src] is already shortened!"))
diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm
index 9c7e08f0d957a..7b980f06c7e07 100644
--- a/code/modules/projectiles/guns/ballistic/revolver.dm
+++ b/code/modules/projectiles/guns/ballistic/revolver.dm
@@ -71,6 +71,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/ultrasecure
pin = /obj/item/firing_pin/fucked
diff --git a/code/modules/projectiles/projectile/bullets/shotgun.dm b/code/modules/projectiles/projectile/bullets/shotgun.dm
index ef855bbf36915..1a3cd49ebd3d8 100644
--- a/code/modules/projectiles/projectile/bullets/shotgun.dm
+++ b/code/modules/projectiles/projectile/bullets/shotgun.dm
@@ -223,3 +223,20 @@
damage_type = STAMINA // Doesn't do "real" damage
sharpness = SHARP_NONE
armour_penetration = -40 // Energy armor is usually very low so uhh
+
+
+/obj/projectile/bullet/pellet/nullglass
+ name = "nullglass pellet"
+ damage = 6
+ wound_bonus = 3
+ bare_wound_bonus = 3
+
+/obj/projectile/bullet/pellet/nullglass/disrupts_psionics()
+ return src
+
+/obj/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)
diff --git a/code/modules/psionics/complexus/complexus.dm b/code/modules/psionics/complexus/complexus.dm
new file mode 100644
index 0000000000000..06454e893a901
--- /dev/null
+++ b/code/modules/psionics/complexus/complexus.dm
@@ -0,0 +1,181 @@
+/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
+ var/last_aura_size
+ var/last_aura_alpha
+ var/last_aura_color
+ var/aura_color = "#ff0022"
+
+ // Cached powers.
+ var/list/learned_powers // All powers known
+ var/list/powers_by_faculty
+ var/datum/psionic_power/selected_power // Power currently selected
+
+ var/obj/screen/psi/hub/ui // Reference to the master psi UI object.
+ var/mob/living/owner // Reference to our owner.
+ var/datum/mind/thinker
+ var/image/_aura_image // Client image
+ var/obj/effect/overlay/aura/image_holder //holder so we don't have to apply the image directly to the mob, making it's clickbox massive
+
+/datum/psi_complexus/New(mob/M)
+ owner = M
+ image_holder = new(src)
+ owner.vis_contents += image_holder
+ START_PROCESSING(SSpsi, src)
+ RegisterSignal(owner, COMSIG_PSI_SELECTION, PROC_REF(select_power))
+ RegisterSignal(owner, COMSIG_PSI_INVOKE, PROC_REF(invoke_power))
+ thinker = owner.mind
+ if(thinker && istype(thinker))
+ RegisterSignal(thinker, COMSIG_MIND_TRANSFERRED, PROC_REF(mind_swap))
+
+/datum/psi_complexus/proc/mind_swap(datum/mind/brain, mob/living/oldbody)
+ if(!brain || !istype(brain) || !brain.current)
+ return
+ UnregisterSignal(owner, COMSIG_PSI_SELECTION)
+ UnregisterSignal(owner, COMSIG_PSI_INVOKE)
+ owner.vis_contents -= image_holder
+ owner.psi = null
+ owner = brain.current
+ owner.vis_contents += image_holder
+ RegisterSignal(owner, COMSIG_PSI_SELECTION, PROC_REF(select_power))
+ RegisterSignal(owner, COMSIG_PSI_INVOKE, PROC_REF(invoke_power))
+ owner.psi = src
+ update(TRUE, TRUE)
+
+/datum/psi_complexus/Destroy()
+ destroy_aura_image(_aura_image)
+ STOP_PROCESSING(SSpsi, src)
+ if(thinker && istype(thinker))
+ UnregisterSignal(thinker, COMSIG_MIND_TRANSFERRED)
+ if(owner)
+ UnregisterSignal(owner, COMSIG_PSI_SELECTION)
+ UnregisterSignal(owner, COMSIG_PSI_INVOKE)
+ 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
+ QDEL_NULL(image_holder)
+
+ if(manifested_items)
+ for(var/thing in manifested_items)
+ qdel(thing)
+ manifested_items.Cut()
+ . = ..()
+
+/datum/psi_complexus/proc/select_power(mob/user)
+ if(suppressed)
+ return
+ rebuild_power_cache()
+ if(!LAZYLEN(learned_powers))
+ return
+ var/list/choice_list = LAZYCOPY(learned_powers)
+ for(var/datum/psionic_power/I as anything in choice_list)
+ choice_list[I] = image(I.icon, null, I.icon_state)
+ var/selection = show_radial_menu(user, user, choice_list, null, 40, tooltips = TRUE, autopick_single_option = FALSE)
+ selected_power = selection
+ if(selection) //wipe the selected power unless something was actually chosen
+ selected_power.on_select(user)
+ user.balloon_alert(user, "Selected [selected_power.name]")
+
+/datum/psi_complexus/proc/invoke_power(mob/user, atom/target, proximity, parameters)
+ if(suppressed)
+ return
+ if(!selected_power)
+ return
+ . = selected_power.invoke(user, target, proximity, parameters)
+ if(.)
+ selected_power.handle_post_power(user, target)
+
+/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(image_holder)
+ return _aura_image
+
+/obj/effect/overlay/aura
+ name = ""
+ // icon = 'icons/effects/psi_aura_small.dmi'
+ // icon_state = "aura"
+ // pixel_x = -64
+ // pixel_y = -64
+ plane = GAME_PLANE
+ appearance_flags = NO_CLIENT_COLOR | RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ blend_mode = BLEND_MULTIPLY
+ layer = TURF_LAYER + 0.5
+ alpha = 0
+
+/proc/create_aura_image(newloc)
+ var/image/aura_image = image('icons/effects/psi_aura_small.dmi', newloc, "aura")
+ aura_image.blend_mode = BLEND_MULTIPLY
+ aura_image.appearance_flags = NO_CLIENT_COLOR | RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
+ aura_image.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ aura_image.layer = TURF_LAYER + 0.5
+ aura_image.alpha = 0
+ aura_image.pixel_x = -64
+ aura_image.pixel_y = -64
+ 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
diff --git a/code/modules/psionics/complexus/complexus_helpers.dm b/code/modules/psionics/complexus/complexus_helpers.dm
new file mode 100644
index 0000000000000..f741fb34826b2
--- /dev/null
+++ b/code/modules/psionics/complexus/complexus_helpers.dm
@@ -0,0 +1,191 @@
+/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(!use_psi_armour || !can_use_passive())
+ return 0
+
+ /**
+ * rating multiplied by the rank of that specific faculty, multiplied by stamina percentage
+ */
+ return round(clamp(clamp(4 * rating, 0, 20) * get_rank(SSpsi.armour_faculty_by_type[armourtype]), 0, 100) * (stamina/max_stamina))
+
+/datum/psi_complexus/proc/handle_block_chance(obj/projectile/projectile)
+ if(!use_psi_armour || !can_use_passive())
+ return FALSE
+
+ var/effective_rank
+ var/chance = 0
+
+ if(istype(projectile, /obj/projectile/beam) || istype(projectile, /obj/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)
+ to_chat(owner, "Your limiter prevents you from performing that.")
+ 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 500)
+ 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 0000000000000..988491c12ac77
--- /dev/null
+++ b/code/modules/psionics/complexus/complexus_latency.dm
@@ -0,0 +1,36 @@
+/datum/psi_complexus/proc/check_latency_trigger(trigger_strength = 0, source, brain_damage = 0, force = FALSE)
+
+ if(!LAZYLEN(latencies))
+ return FALSE
+
+ if(brain_damage) //don't force it when it's not had time to rest
+ owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(brain_damage/2, brain_damage))
+
+ if(world.time < next_latency_trigger && !force)
+ if(brain_damage)
+ to_chat(owner, span_danger("Your head throbs as [source] messes with your brain!"))
+ return
+
+ next_latency_trigger = world.time + rand(10 SECONDS, 30 SECONDS)
+
+ if(prob(trigger_strength))
+ var/faculty = pick(latencies)
+
+ var/new_rank = PSI_RANK_OPERANT
+ switch(rand(0, 10000)) //i intially tried using a weighted list with pickweight, but i kept getting out of bounds errors for some reason
+ if(0) //weighted so you can still roll grandmaster, but at an incredibly rare chance
+ new_rank = PSI_RANK_GRANDMASTER
+ if(1 to 100)
+ new_rank = PSI_RANK_MASTER
+ else
+ new_rank = PSI_RANK_OPERANT
+
+ 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(60 SECONDS, 180 SECONDS) * new_rank)
+ else if(brain_damage)
+ to_chat(owner, span_danger("Your head throbs as [source] messes with your brain!"))
+ return FALSE
+
+ 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 0000000000000..b3476b45eb74d
--- /dev/null
+++ b/code/modules/psionics/complexus/complexus_power_cache.dm
@@ -0,0 +1,24 @@
+/datum/psi_complexus/proc/rebuild_power_cache()
+ if(rebuild_power_cache)
+
+ learned_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)
+ if(!faculty_decl) //if it's not initialized yet, come back later
+ continue
+ for(var/P in faculty_decl.powers)
+ var/datum/psionic_power/power = P
+ if(!power.min_rank) //if a minimum rank wasn't set, it's probably either bad coding or a parent used for typepathing, so don't include it
+ continue
+
+ if(relevant_rank >= power.min_rank)
+ LAZYADD(powers_by_faculty[power.faculty], power)
+ LAZYADD(learned_powers, power)
+ rebuild_power_cache = FALSE
+
+/datum/psi_complexus/proc/get_powers_by_faculty(faculty)
+ rebuild_power_cache()
+ return powers_by_faculty[faculty]
diff --git a/code/modules/psionics/complexus/complexus_process.dm b/code/modules/psionics/complexus/complexus_process.dm
new file mode 100644
index 0000000000000..0dd169e39e541
--- /dev/null
+++ b/code/modules/psionics/complexus/complexus_process.dm
@@ -0,0 +1,222 @@
+/datum/psi_complexus/proc/update(force, silent)
+
+ set waitfor = FALSE
+
+ if(HAS_TRAIT(owner, TRAIT_PSIONICALLY_IMMUNE) || HAS_TRAIT(owner, TRAIT_PSIONICALLY_DEAFENED)) //no psionics for you
+ qdel(src)
+ return
+
+ 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 == PSI_RANK_LATENT && !LAZYFIND(latencies, faculty)) //if they're latent and it's not already on the list, add it to latencies
+ LAZYADD(latencies, faculty)
+ else if(check_rank != PSI_RANK_LATENT)
+ if(check_rank <= PSI_RANK_BLUNT) //if they're not capable at all, remove it entirely
+ ranks -= faculty
+ LAZYREMOVE(latencies, faculty) //remove it from the list because they aren't latent anymore
+ 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))
+ rebuild_power_cache = TRUE
+ rebuild_power_cache()
+ if(force || last_rating != CEILING(combined_rank/rank_count, 1))
+ if(highest_rank <= 1)
+ if(highest_rank == 0)
+ qdel(src)
+ return
+ if(!silent)
+ 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()
+ 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()
+ if(HAS_TRAIT(owner, TRAIT_PSIONICALLY_IMMUNE) || HAS_TRAIT(owner, TRAIT_PSIONICALLY_DEAFENED)) //no psionics for you
+ qdel(src)
+ return
+
+ 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(2, rating) ) /2)
+ 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()
+
+/**
+ * Priority order is
+ * -Stopped heart (actively dying)
+ * -Regular damage (probably dying)
+ * -Wounds (might bleed out)
+ * -Tox/Clone/Rad (You're probably not hurt a different way when you need to heal this)
+ * -Organ damage (just keep us busy and topped off)
+ */
+/datum/psi_complexus/proc/attempt_regeneration()
+
+ var/heal_paramount = FALSE
+ var/heal_poison = FALSE
+ var/heal_internal = FALSE
+ var/heal_rate = 0
+ var/mend_prob = 0
+
+ switch(get_rank(PSI_REDACTION))
+ if(PSI_RANK_PARAMOUNT)
+ heal_paramount = TRUE
+ heal_poison = TRUE
+ heal_internal = TRUE
+ mend_prob = 50
+ heal_rate = 10
+ if(PSI_RANK_GRANDMASTER)
+ heal_poison = TRUE
+ heal_internal = TRUE
+ mend_prob = 20
+ heal_rate = 7
+ if(PSI_RANK_MASTER)
+ heal_internal = TRUE
+ mend_prob = 10
+ heal_rate = 4
+ if(PSI_RANK_OPERANT)
+ 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(prob(mend_prob))
+ // Fix our heart if we're paramount.
+ if(heal_paramount && ishuman(owner))
+ var/mob/living/carbon/human/H = owner
+ var/obj/item/organ/heart/should_beat = H.getorganslot(ORGAN_SLOT_HEART)
+ if(should_beat && !should_beat.beating && spend_power(heal_rate))
+ should_beat.Restart()
+
+ // Heal actual damage
+ if((owner.getBruteLoss() || owner.getFireLoss() || owner.getOxyLoss()) && spend_power(heal_rate))
+ owner.heal_ordered_damage(heal_rate, list(BRUTE, BURN, OXY), BODYPART_ANY) //it gets to heal robotic parts because otherwise it'd suck you dry trying to fix unfixable limbs
+ 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."))
+
+ // Repair wounds
+ if(ishuman(owner))
+ var/mob/living/carbon/human/H = owner
+ for(var/datum/wound/W in H.all_wounds)
+ if(!spend_power(heal_rate))
+ return
+
+ if(W.blood_flow)
+ W.blood_flow -= (heal_rate * 0.5)
+ if(prob(25))
+ to_chat(owner, span_notice("Your autoredactive faculty stems the flow of blood from your [W.limb]."))
+ return
+
+ if(istype(W, /datum/wound/burn))
+ var/datum/wound/burn/degree = W
+ degree.sanitization += (heal_rate * 0.5)
+ degree.flesh_healing += (heal_rate * 0.5)
+ if(prob(25))
+ to_chat(owner, span_notice("Your autoredactive faculty cleans and mends the burn on your [W.limb]."))
+ return
+
+ if(istype(W, /datum/wound/blunt))
+ qdel(W)
+ playsound(H, 'sound/surgery/bone3.ogg', 25)
+ to_chat(owner, span_notice("Your autoredactive faculty snaps the bones in your [W.limb] back into place."))
+ return
+
+ // Heal radiation, cloneloss and poisoning.
+ if(heal_poison)
+ if(owner.getToxLoss() && spend_power(heal_rate))
+ if(prob(25))
+ to_chat(owner, span_notice("Your autoredactive faculty purges foreign toxins in your body."))
+ owner.adjustToxLoss(heal_rate, TRUE, TRUE)
+
+ 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/2))
+ return
+
+ 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
+
+ // Heal organ damage.
+ if(heal_internal && ishuman(owner))
+ var/mob/living/carbon/human/H = owner
+ 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
diff --git a/code/modules/psionics/equipment/cerebro_enhancers.dm b/code/modules/psionics/equipment/cerebro_enhancers.dm
new file mode 100644
index 0000000000000..e63c258424451
--- /dev/null
+++ b/code/modules/psionics/equipment/cerebro_enhancers.dm
@@ -0,0 +1,146 @@
+//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/verb/integrate_action()
+ set name = "Integrate"
+ set category = null
+ set src in usr
+ 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
+
+ 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
+
+ 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(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_cover = NONE
+ 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 0000000000000..23fe640d78783
--- /dev/null
+++ b/code/modules/psionics/equipment/psipower.dm
@@ -0,0 +1,39 @@
+/obj/item/psychic_power
+ name = "psychic power"
+ icon = 'icons/obj/psychic_powers.dmi'
+ anchored = TRUE
+ var/maintain_cost = 3
+ var/mob/living/owner
+ item_flags = DROPDEL
+
+/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/throw_at(atom/target, range, speed, mob/thrower, spin, diagonals_first, datum/callback/callback, force, quickstart)
+ SEND_SOUND(thrower, sound('sound/effects/psi/power_fail.ogg', volume = 50))
+ qdel(src)
+
+/obj/item/psychic_power/attack_self(mob/user)
+ SEND_SOUND(user, sound('sound/effects/psi/power_fail.ogg', volume = 50))
+ user.dropItemToGround(src)
+
+/obj/item/psychic_power/process()
+ if(!owner || loc != owner || !(src in owner.held_items))
+ if(ishuman(loc))
+ var/mob/living/carbon/human/host = loc
+ host.remove_embedded_object(src)
+ qdel(src)
+ else if(istype(owner) && !owner?.psi?.spend_power(maintain_cost))
+ qdel(src)
diff --git a/code/modules/psionics/equipment/psipower_baton.dm b/code/modules/psionics/equipment/psipower_baton.dm
new file mode 100644
index 0000000000000..94a4c4ecfd7e5
--- /dev/null
+++ b/code/modules/psionics/equipment/psipower_baton.dm
@@ -0,0 +1,50 @@
+/obj/item/melee/classic_baton/psibaton
+ name = "psychokinetic bash"
+ desc = "A psiokenetic truncheon for beating psycho scum."
+ force = 0
+ stamina_damage = 10
+ icon = 'icons/obj/psychic_powers.dmi'
+ icon_state = "psibaton"
+ item_state = "psibaton"
+ lefthand_file = 'icons/mob/inhands/weapons/melee_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/weapons/melee_righthand.dmi'
+ hitsound = 'sound/effects/psi/psisword.ogg'
+ drop_sound = 'sound/effects/psi/power_fail.ogg'
+ item_flags = DROPDEL
+ var/maintain_cost = 3
+ var/mob/living/owner
+
+/obj/item/melee/classic_baton/psibaton/New(mob/living/L)
+ owner = L
+ if(!istype(owner))
+ qdel(src)
+ return
+ START_PROCESSING(SSprocessing, src)
+ ..()
+
+/obj/item/melee/classic_baton/psibaton/Destroy()
+ if(istype(owner) && owner.psi)
+ LAZYREMOVE(owner.psi.manifested_items, src)
+ UNSETEMPTY(owner.psi.manifested_items)
+ STOP_PROCESSING(SSprocessing, src)
+ . = ..()
+
+/obj/item/melee/classic_baton/psibaton/throw_at(atom/target, range, speed, mob/thrower, spin, diagonals_first, datum/callback/callback, force, quickstart)
+ SEND_SOUND(thrower, sound('sound/effects/psi/power_fail.ogg', volume = 50))
+ qdel(src)
+
+/obj/item/melee/classic_baton/psibaton/attack_self(mob/user)
+ SEND_SOUND(user, sound('sound/effects/psi/power_fail.ogg', volume = 50))
+ user.dropItemToGround(src)
+
+/obj/item/melee/classic_baton/psibaton/process()
+ if(istype(owner))
+ if(!owner?.psi?.spend_power(maintain_cost))
+ qdel(src)
+ 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 0000000000000..294ddaaf15759
--- /dev/null
+++ b/code/modules/psionics/equipment/psipower_blade.dm
@@ -0,0 +1,12 @@
+/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/effects/psi/psisword.ogg'
+ var/can_break_wall = FALSE
+ var/wall_break_time = 6 SECONDS
+ drop_sound = 'sound/effects/psi/power_fail.ogg'
diff --git a/code/modules/psionics/equipment/psipower_tinker.dm b/code/modules/psionics/equipment/psipower_tinker.dm
new file mode 100644
index 0000000000000..93462ea86e9a8
--- /dev/null
+++ b/code/modules/psionics/equipment/psipower_tinker.dm
@@ -0,0 +1,30 @@
+/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/list/choice_list = LAZYCOPY(possible_tools)
+ for(var/I as anything in choice_list)
+ choice_list[I] = image(icon, null, I)
+ var/choice = show_radial_menu(owner, owner, choice_list, null, 40, tooltips = TRUE)
+
+ if(!choice)
+ return
+
+ if(!owner || loc != owner)
+ return
+
+ tool_behaviour = choice
+ name = "psychokinetic [tool_behaviour]"
+ icon_state = "[tool_behaviour]"
+ update_icon()
+ to_chat(owner, span_notice("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 0000000000000..63db8d50ea711
--- /dev/null
+++ b/code/modules/psionics/equipment/psipower_tk.dm
@@ -0,0 +1,111 @@
+/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) || !valid_distance(owner, focus))
+ qdel(src)
+ else if(!owner || loc != owner || !(src in owner.held_items))
+ if(ishuman(loc))
+ var/mob/living/carbon/human/host = loc
+ host.remove_embedded_object(src)
+ qdel(src)
+
+/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/proc/valid_distance(mob/living/user, atom/target)
+ var/distance = get_dist(get_turf(user), get_turf(focus ? focus : target))
+ return (distance <= (owner.psi.get_rank(PSI_PSYCHOKINESIS) * 2))
+
+/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)))
+ return
+
+ if(!valid_distance(user, target))
+ to_chat(user, span_warning("Your telekinetic power won't reach that far."))
+ qdel(src)
+ return
+
+ if(!user.psi || !user.psi.can_use())
+ return
+
+ if(!user.psi.spend_power(5))
+ qdel(src)
+ return
+
+ user.psi.set_cooldown(5)
+
+ 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, owner, callback = CALLBACK(src, PROC_REF(end_throw)))
+
+/obj/item/psychic_power/telekinesis/proc/end_throw()
+ sparkle()
+ if(!focus || !isturf(focus.loc) || !valid_distance(owner, focus))
+ qdel(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 0000000000000..99a9d7932c527
--- /dev/null
+++ b/code/modules/psionics/events/_psi.dm
@@ -0,0 +1,22 @@
+// /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)
+// if(!istype(thing, /datum/psi_complexus))
+// continue
+// apply_psi_effect(thing)
+
+// /datum/round_event/psi/proc/apply_psi_effect(datum/psi_complexus/psi)
+// return
diff --git a/code/modules/psionics/events/mini_spasms.dm b/code/modules/psionics/events/mini_spasms.dm
new file mode 100644
index 0000000000000..6c8bfe736d3b1
--- /dev/null
+++ b/code/modules/psionics/events/mini_spasms.dm
@@ -0,0 +1,73 @@
+// /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]
+// INVOKE_ASYNC(src, PROC_REF(do_spasm), victim, source)
+
+// /datum/round_event/minispasm/proc/do_spasm(mob/living/victim, obj/item/radio/source)
+// if(HAS_TRAIT(src, TRAIT_PSIONICALLY_DEAFENED) || HAS_TRAIT(src, TRAIT_PSIONICALLY_IMMUNE))
+// return
+// if(!victim.mind)
+// return
+// 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.adjust_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.adjust_jitter(1 SECONDS)
+// SEND_SIGNAL(victim, COMSIG_ADD_MOOD_EVENT, "minispasm", /datum/mood_event/epilepsy)
+// var/new_latencies = rand(1,2)
+// 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), PSI_RANK_LATENT)
+// sleep(30)
+// victim.psi.update()
+// sleep(4.5 SECONDS)
+// victim.psi.check_latency_trigger(100, "a psionic scream")
+
+// /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 0000000000000..2ff3b3febd345
--- /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(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 0000000000000..7b64fc43cdc59
--- /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(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 0000000000000..28827b91f6a70
--- /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 0000000000000..ab5ba1ac3d928
--- /dev/null
+++ b/code/modules/psionics/faculties/_power.dm
@@ -0,0 +1,63 @@
+/datum/psionic_power
+ /// Name. If null, psipower won't be generated on roundstart.
+ var/name
+ /// Associated psi faculty.
+ var/faculty
+ /// File to pull the ability icon from.
+ var/icon = 'icons/obj/psychic_powers.dmi'
+ /// Sprite of the ability itself.
+ var/icon_state = "base_power"
+ /// 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
+ /// 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, proximity, parameters)
+
+ 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(isitem(target))//don't invoke if we're clicking in our inventory
+ var/obj/item/thing = target
+ if(thing in user.get_all_contents())
+ 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)
+
+/datum/psionic_power/proc/on_select(mob/living/user)
+ return TRUE
diff --git a/code/modules/psionics/faculties/coercion.dm b/code/modules/psionics/faculties/coercion.dm
new file mode 100644
index 0000000000000..3c63fff33c33a
--- /dev/null
+++ b/code/modules/psionics/faculties/coercion.dm
@@ -0,0 +1,321 @@
+#define COGMANIP_HYPNOTIZE "Hypnotize"
+#define COGMANIP_ERASE_MEMORY "Erase Memory"
+#define COGMANIP_THRALL "Thrall"
+
+/datum/psionic_faculty/coercion
+ id = PSI_COERCION
+ name = "Coercion"
+
+/datum/psionic_power/coercion
+ faculty = PSI_COERCION
+
+/datum/psionic_power/coercion/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if (!istype(target))
+ to_chat(user, span_warning("You cannot mentally attack \the [target]."))
+ return FALSE
+ if(HAS_TRAIT(target, TRAIT_PSIONICALLY_IMMUNE))
+ to_chat(user, span_warning("[target]'s unnatural anatomy refuses the psionic tampering"))
+ return FALSE
+ . = ..()
+
+/datum/psionic_power/coercion/commune
+ name = "Commune"
+ cost = 10
+ cooldown = 5 SECONDS
+ min_rank = PSI_RANK_OPERANT
+ icon_state = "coe_commune"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click on a creature on to psionically send them a message."
+
+/datum/psionic_power/coercion/commune/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(!istype(target) || user == target)
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message(span_notice("[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, span_cult("Not even a psion of your level can speak to the dead."))
+ return TRUE
+
+ if (issilicon(target))
+ to_chat(user, span_warning("This can only be used on living organisms."))
+ return TRUE
+
+ log_say("[key_name(user)] communed to [key_name(target)]: [text]")
+ to_chat(user, span_warning("You succeed in sending your commune to [target]! Now to see if they listen..."))
+
+ for (var/mob/M in GLOB.dead_mob_list)
+ to_chat(M,span_notice("[user] psionically says to [target]: [text]"))
+
+ var/mob/living/carbon/human/H = target
+ to_chat(H, span_notice("You feel something crawl behind your eyes, hearing: [text]"))
+ if(istype(H))
+ if(prob(10) && !(H.dna.species.species_traits & NOBLOOD))
+ to_chat(H, span_warning("Your nose begins to bleed..."))
+ H.add_splatter_floor(small_drip = TRUE)
+ else if(prob(25))
+ to_chat(H, span_warning("Your head hurts..."))
+ else if(prob(50))
+ to_chat(H, span_warning("Your mind buzzes..."))
+
+/datum/psionic_power/coercion/assay
+ name = "Assay"
+ cost = 15
+ cooldown = 5 SECONDS
+ min_rank = PSI_RANK_OPERANT
+ icon_state = "coe_assay"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click on a target in order to perform a deep coercive-redactive probe of their psionic potential."
+
+/datum/psionic_power/coercion/assay/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(!istype(target) || user == target)
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message(span_warning("\The [user] taps into the the head of \the [target]..."))
+ 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]."))
+ var/speed = (4 - (user.psi.get_rank(PSI_COERCION) - 1)) SECONDS
+ if(!do_after(user, speed, 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/psiping
+// name = "Psi-ping"
+// cost = 50
+// heat = 20
+// cooldown = 20 SECONDS
+// min_rank = PSI_RANK_OPERANT
+// icon_state = "coe_psiping"
+// use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click on yourself with an empty hand to detect nearby psionic signatures."
+// var/searching = FALSE
+
+// /datum/psionic_power/coercion/psiping/invoke(mob/living/user, mob/living/target, proximity, parameters)
+// if(user != target || searching)
+// return FALSE
+// . = ..()
+// if(.)
+// to_chat(user, span_notice("You take a moment to tune into the local Nlom..."))
+// searching = TRUE
+// if(!do_after(user, 3 SECONDS, user))
+// searching = FALSE
+// return FALSE
+// searching = FALSE
+// var/list/dirs = list()
+// for(var/mob/living/L in GLOB.mob_living_list)
+// var/turf/T = get_turf(L)
+// if(!T || L == user || L.stat == DEAD || issilicon(L) || !(L.psi || isdarkspawn(L)) || (L.z != user.z))
+// 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(angle2dir(Get_Angle(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
+// if(length(dirs))
+// var/list/feedback = list()
+// feedback += "You sense..."
+// for(var/d in dirs)
+// feedback += "[capitalize(dir2text(text2num(d)))]:"
+// for(var/dst in dirs[d])
+// feedback += "[dirs[d][dst]] psionic signature\s [dst]."
+
+// to_chat(user, span_notice(feedback.Join(" ")))
+// else
+// to_chat(user, span_notice("You detect no psionic signatures but your own."))
+// return TRUE
+
+/datum/psionic_power/coercion/agony
+ name = "Agony"
+ cost = 20
+ heat = 20
+ cooldown = 2 SECONDS
+ min_rank = PSI_RANK_OPERANT
+ icon_state = "coe_agony"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, attack someone while in combat mode to deal minor stamina damage. Higher psi levels augment the damage done."
+
+/datum/psionic_power/coercion/agony/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(!istype(target) || !proximity || user == target || !user.combat_mode)
+ return FALSE
+ . = ..()
+ if(.)
+ user.do_attack_animation(target, ATTACK_EFFECT_PUNCH)
+ user.visible_message("\The [target] has been struck by \the [user]!")
+ playsound(user.loc, 'sound/weapons/Egloves.ogg', 50, 1, -1)
+ target.apply_damage(20 * (user.psi.get_rank(PSI_COERCION) - 1), STAMINA, BODY_ZONE_CHEST)
+ return TRUE
+
+/datum/psionic_power/coercion/spasm
+ name = "Spasm"
+ cost = 15
+ heat = 10
+ cooldown = 10 SECONDS
+ min_rank = PSI_RANK_MASTER
+ icon_state = "coe_spasm"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target a creature to use a ranged attack that may rip the weapons away from the target."
+
+/datum/psionic_power/coercion/spasm/invoke(mob/living/user, mob/living/carbon/human/target, proximity, parameters)
+ if(!istype(target) || user == target || !user.combat_mode)
+ return FALSE
+ . = ..()
+
+ if(.)
+ to_chat(user, span_danger("You lash out, stabbing into \the [target] with a lance of psi-power."))
+ to_chat(target, span_danger("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(span_danger("\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(span_danger("\The [target] drops what they were holding as their right hand spasms!"))
+ return TRUE
+
+/datum/psionic_power/coercion/focus
+ name = "Focus"
+ cost = 10
+ cooldown = 8 SECONDS
+ min_rank = PSI_RANK_MASTER
+ icon_state = "coe_focus"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click on someone in order to cure ailments of the mind."
+
+/datum/psionic_power/coercion/focus/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(!istype(target) || !proximity || 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 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 ? 4 SECONDS : 2 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/resilience = TRAUMA_RESILIENCE_BASIC
+ var/coercion_rank = user.psi.get_rank(PSI_COERCION)
+ if(coercion_rank >= PSI_RANK_GRANDMASTER)
+ target.SetParalyzed(0)
+ resilience = TRAUMA_RESILIENCE_SURGERY
+ if(coercion_rank >= PSI_RANK_PARAMOUNT)
+ target.SetAllImmobility(0)
+ resilience = TRAUMA_RESILIENCE_LOBOTOMY
+
+ target.SetDaze(0)
+ if(istype(target, /mob/living/carbon))
+ var/mob/living/carbon/M = target
+ M.cure_trauma_type(resilience = resilience)
+ M.adjust_hallucinations(10 SECONDS)
+ 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.
+ min_rank = PSI_RANK_MASTER
+ icon_state = "coe_mindread"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click on someone in melee range to attempt to read a surface level thought."
+
+/datum/psionic_power/coercion/mindread/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(!istype(target) || target == user || !proximity)
+ 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 FALSE
+
+ 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_hypnophrase("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/blindstrike
+ name = "Blindstrike"
+ cost = 8
+ heat = 15
+ cooldown = 10 SECONDS
+ min_rank = PSI_RANK_GRANDMASTER
+ icon_state = "coe_blindstrike"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click anywhere to use a radial attack that blinds, deafens and disorients everyone near you."
+
+/datum/psionic_power/coercion/blindstrike/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ . = ..()
+ if(.)
+ user.visible_message(span_danger("\The [user] suddenly throws back their head, as though screaming silently!"), 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
+ if(HAS_TRAIT(M, TRAIT_PSIONICALLY_IMMUNE))
+ 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.adjust_confusion(10 SECONDS)
+ return TRUE
+
+/datum/psionic_power/coercion/dis_arm
+ name = "Dis-Arm"
+ cost = 40
+ heat = 50
+ cooldown = 100 SECONDS
+ min_rank = PSI_RANK_PARAMOUNT
+ icon_state = "coe_disarm"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click your target with combat mode to Psionically rip their arms off."
+
+/datum/psionic_power/coercion/dis_arm/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(!user.combat_mode || user == target)
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message(span_danger("\The [user] grows two psionic arms, ripping [target]'s arms off!"))
+ to_chat(user, span_danger("You channel your full mental might into ripping and tearing!"))
+ var/mob/living/carbon/CM = target
+ for(var/obj/item/bodypart/bodypart in CM.bodyparts)
+ if(!(bodypart.body_part & (HEAD|CHEST|LEGS)))
+ if(bodypart.dismemberable)
+ bodypart.dismember()
+ return TRUE
diff --git a/code/modules/psionics/faculties/energistics.dm b/code/modules/psionics/faculties/energistics.dm
new file mode 100644
index 0000000000000..038bdd2398cdf
--- /dev/null
+++ b/code/modules/psionics/faculties/energistics.dm
@@ -0,0 +1,126 @@
+/datum/psionic_faculty/energistics
+ id = PSI_ENERGISTICS
+ name = "Energistics"
+ armour_types = list(BOMB, LASER, ENERGY, FIRE)
+
+/datum/psionic_power/energistics
+ faculty = PSI_ENERGISTICS
+
+/datum/psionic_power/energistics/electrocute
+ name = "Electrocute"
+ cost = 10
+ heat = 30
+ cooldown = 7.5 SECONDS
+ min_rank = PSI_RANK_OPERANT
+ icon_state = "ene_ele"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, in an empty, then Enter combat mode to use a melee attack that electrocutes a victim, or charge an energy cell."
+
+/datum/psionic_power/energistics/electrocute/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(!user.combat_mode || !istype(target) || !proximity)
+ return FALSE
+ . = ..()
+ if(.)
+ var/psyrank = user.psi.get_rank(PSI_ENERGISTICS)
+ if(istype(target))
+ user.visible_message(span_danger("\The [user] sends a jolt of electricity arcing into \the [target]!"))
+ target.electrocute_act(rand(psyrank * 5, psyrank * 10), user, 1, user.zone_selected, stun = (psyrank >= PSI_RANK_PARAMOUNT))
+ 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(psyrank * 5, psyrank * 10))
+ return TRUE
+ else
+ return FALSE
+
+/datum/psionic_power/energistics/spark
+ name = "Spark"
+ cost = 1
+ cooldown = 1 SECONDS
+ min_rank = PSI_RANK_OPERANT
+ icon_state = "ene_spark"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target a non-living thing in melee range with combat mode on to cause some sparks to appear. This can light fires."
+
+/datum/psionic_power/energistics/spark/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(!user.combat_mode || isnull(target) || istype(target) || !proximity)
+ 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(user.psi.get_rank(PSI_ENERGISTICS), 1, target)
+ s.start()
+ return TRUE
+
+/datum/psionic_power/energistics/zorch
+ name = "Zorch"
+ cost = 15
+ heat = 15
+ cooldown = 2 SECONDS
+ min_rank = PSI_RANK_MASTER
+ icon_state = "ene_zorch"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then use this ranged laser attack with combat mode on. 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(mob/living/user, mob/living/target, proximity, parameters)
+ if(!user.combat_mode)
+ return FALSE
+ . = ..()
+ 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/projectile/pew
+ var/pew_sound
+
+ if(user.psi.zorch_harm)
+ pew = new /obj/projectile/beam/laser(get_turf(user))
+ else
+ pew = new /obj/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/disrupt
+ name = "Disrupt"
+ cost = 20
+ heat = 20
+ cooldown = 10 SECONDS
+ min_rank = PSI_RANK_GRANDMASTER
+ icon_state = "ene_disrupt"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then attack a target while in combat mode to cause a localized electromagnetic pulse."
+
+/datum/psionic_power/energistics/disrupt/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(!user.combat_mode || !istype(target) || !proximity)
+ return FALSE
+ . = ..()
+ if(.)
+ user.visible_message("\The [user] releases a gout of crackling static and arcing lightning over \the [target]!")
+ empulse(target, 5, 1)
+ return TRUE
diff --git a/code/modules/psionics/faculties/psychokenisis.dm b/code/modules/psionics/faculties/psychokenisis.dm
new file mode 100644
index 0000000000000..e427672ad5662
--- /dev/null
+++ b/code/modules/psionics/faculties/psychokenisis.dm
@@ -0,0 +1,135 @@
+/datum/psionic_faculty/psychokinesis
+ id = PSI_PSYCHOKINESIS
+ name = "Psychokinesis"
+ armour_types = list(MELEE, BULLET, WOUND)
+
+/datum/psionic_power/psychokinesis
+ faculty = PSI_PSYCHOKINESIS
+ use_sound = null
+
+/datum/psionic_power/psychokinesis/psiblade
+ name = "Psiblade/Psibaton"
+ cost = 10
+ cooldown = 3 SECONDS
+ min_rank = PSI_RANK_OPERANT
+ icon_state = "psy_blade"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, summon a psiblade, or psibaton if the user is a pacifist. The power the blade/baton will vary based on your mastery of the faculty."
+ use_sound = 'sound/effects/psi/power_fabrication.ogg'
+ admin_log = FALSE
+
+/datum/psionic_power/psychokinesis/psiblade/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ return FALSE
+
+/datum/psionic_power/psychokinesis/psiblade/on_select(mob/living/user)
+ . = ..()
+ if(.)
+ playsound(user.loc, use_sound, 75)
+ if(HAS_TRAIT(user, TRAIT_PACIFISM))
+ var/obj/item/melee/classic_baton/psibaton/baton = new /obj/item/melee/classic_baton/psibaton(user, user)
+ user.put_in_hands(baton)
+ switch(user.psi.get_rank(faculty))
+ if(PSI_RANK_PARAMOUNT)
+ baton.stamina_damage = 50
+ if(PSI_RANK_GRANDMASTER)
+ baton.stamina_damage = 40
+ if(PSI_RANK_MASTER)
+ baton.stamina_damage = 25
+ else
+ baton.stamina_damage = 15
+ return baton
+ else
+ var/obj/item/psychic_power/psiblade/blade = new /obj/item/psychic_power/psiblade(user, user)
+ user.put_in_hands(blade)
+ switch(user.psi.get_rank(faculty))
+ if(PSI_RANK_PARAMOUNT)
+ blade.can_break_wall = TRUE
+ blade.wall_break_time = 3 SECONDS
+ blade.force = 40
+ blade.armour_penetration = 30
+ blade.AddComponent(/datum/component/cleave_attack, arc_size=180, requires_wielded=TRUE)
+ if(PSI_RANK_GRANDMASTER)
+ blade.can_break_wall = TRUE
+ blade.force = 24
+ blade.armour_penetration = 30
+ blade.AddComponent(/datum/component/cleave_attack, arc_size=180, requires_wielded=TRUE)
+ if(PSI_RANK_MASTER)
+ blade.force = 18
+ else
+ blade.force = 12
+ return blade
+
+/datum/psionic_power/psychokinesis/tinker
+ name = "Tinker"
+ cost = 5
+ cooldown = 10
+ min_rank = PSI_RANK_OPERANT
+ icon_state = "psy_tinker"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, Use it in-hand to switch between tool types, different tools are available at different psi levels."
+ use_sound = 'sound/effects/psi/power_fabrication.ogg'
+ admin_log = FALSE
+
+/datum/psionic_power/psychokinesis/tinker/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ return FALSE
+
+/datum/psionic_power/psychokinesis/tinker/on_select(mob/living/user)
+ . = ..()
+ if(.)
+ playsound(user.loc, use_sound, 75)
+ var/obj/item/psychic_power/tinker/tool = new(user)
+ user.put_in_hands(tool)
+ switch(user.psi.get_rank(faculty))
+ if(PSI_RANK_PARAMOUNT)
+ tool.possible_tools = list(TOOL_SCREWDRIVER, TOOL_CROWBAR, TOOL_WRENCH, TOOL_WIRECUTTER, TOOL_WELDER, TOOL_MULTITOOL, TOOL_SCALPEL, TOOL_HEMOSTAT, TOOL_RETRACTOR, TOOL_CAUTERY, TOOL_SAW, TOOL_DRILL, 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_WRENCH, TOOL_WIRECUTTER, TOOL_SCALPEL, TOOL_HEMOSTAT, TOOL_RETRACTOR, TOOL_CAUTERY, TOOL_SAW, TOOL_DRILL, TOOL_MINING, TOOL_SHOVEL, TOOL_HATCHET)
+ tool.toolspeed = 0.5
+ if(PSI_RANK_MASTER)
+ tool.possible_tools = list(TOOL_SCREWDRIVER, TOOL_CROWBAR, TOOL_WRENCH, TOOL_WIRECUTTER, TOOL_SCALPEL, TOOL_HEMOSTAT, TOOL_CAUTERY, 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_SHOVEL)
+ tool.toolspeed = 1.5
+ return tool
+
+/datum/psionic_power/psychokinesis/telekinesis
+ name = "Telekinesis"
+ cost = 5
+ cooldown = 1 SECONDS
+ min_rank = PSI_RANK_MASTER
+ icon_state = "psy_tele"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, to manifest a psychokinetic grip. Use it manipulate objects at a distance."
+ admin_log = FALSE
+ use_sound = 'sound/effects/psi/power_used.ogg'
+ var/list/valid_types = list( //a list of all
+ /obj/machinery/door,
+ /obj/structure/window,
+ /obj/structure/closet,
+ /obj/structure/chair
+ )
+
+/datum/psionic_power/psychokinesis/telekinesis/New()
+ . = ..()
+ valid_types = typecacheof(valid_types)
+
+/datum/psionic_power/psychokinesis/telekinesis/invoke(mob/living/user, atom/target, proximity, parameters)
+ 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, /obj/machinery) || istype(target, /obj/structure)) && !is_type_in_typecache(target, valid_types))
+ return FALSE
+ . = ..()
+ if(.)
+ if(istype(target, /obj/structure) || istype(target, /obj/machinery))
+ user.visible_message(span_notice("\The [user] makes a strange gesture."))
+ user.UnarmedAttack(target, TRUE)
+ return TRUE
+ else if(istype(target, /mob) || istype(target, /obj))
+ var/obj/item/psychic_power/telekinesis/tk = new(user)
+ user.put_in_hands(tk)
+ if(tk.set_focus(target))
+ tk.sparkle()
+ user.visible_message(span_notice("\The [user] reaches out."))
+ 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 0000000000000..bcea5c91dc16c
--- /dev/null
+++ b/code/modules/psionics/faculties/redaction.dm
@@ -0,0 +1,186 @@
+/datum/psionic_faculty/redaction
+ id = PSI_REDACTION
+ name = "Redaction"
+ armour_types = list(BIO, RAD, ACID)
+
+/datum/psionic_power/redaction
+ faculty = PSI_REDACTION
+ admin_log = FALSE
+
+/datum/psionic_power/redaction/proc/check_dead(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(mob/living/user, mob/living/target, proximity, parameters)
+ if(HAS_TRAIT(target, TRAIT_PSIONICALLY_IMMUNE))
+ to_chat(user, span_warning("[target]'s unnatural anatomy refuses the psionic tampering"))
+ return FALSE
+ if(check_dead(target))
+ return FALSE
+ . = ..()
+
+/datum/psionic_power/redaction/skinsight
+ name = "Skinsight"
+ cost = 3
+ heat = 1
+ cooldown = 3 SECONDS
+ min_rank = PSI_RANK_OPERANT
+ icon_state = "redac_skinsight"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target the mob you wish to scan with combat mode off. Higher psi levels provide more information."
+
+/datum/psionic_power/redaction/skinsight/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(user.combat_mode || !istype(target) || !proximity)
+ 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
+ min_rank = PSI_RANK_OPERANT
+ icon_state = "redac_mend"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target the mob you wish to heal with combat mode off. Higher psi levels provide further healing."
+
+/datum/psionic_power/redaction/mend/invoke(mob/living/user, mob/living/carbon/human/target, proximity, parameters)
+ if(user.combat_mode || !istype(target) || !proximity || !..())
+ return FALSE
+
+ user.visible_message(span_notice("\The [user] rests a hand on \the [target]..."))
+ to_chat(target, span_notice("A healing warmth suffuses you."))
+ new /obj/effect/temp_visual/heal(get_turf(target), "#33cc33")
+
+ var/pk_rank = user.psi.get_rank(PSI_PSYCHOKINESIS)
+ var/redaction_rank = user.psi.get_rank(PSI_REDACTION)
+
+
+ if(pk_rank >= PSI_RANK_LATENT && redaction_rank >= PSI_RANK_MASTER) //realistically, not likely to happen, and not even that powerful
+ var/removal_size = clamp(5-pk_rank, 0, 5)
+ var/list/embedded_list = list()
+ var/obj/item/bodypart/body_part
+ for(var/obj/item/bodypart/part in target.bodyparts)
+ for(var/obj/item/embedded in part.embedded_objects)
+ if(embedded.w_class >= removal_size)
+ embedded_list += embedded
+ if(LAZYLEN(embedded_list))
+ var/removed_item = pick(embedded_list)
+ body_part = target.get_embedded_part(removed_item)
+ target.remove_embedded_object(removed_item, get_turf(target))
+ to_chat(user, span_notice("You extend a tendril of psychokinetic-redactive power and carefully tease \the [removed_item] free of [target]'s [body_part]."))
+ return TRUE
+
+ if(target.heal_ordered_damage(redaction_rank * 10, list(BRUTE, BURN)) > 0) //this returns a number greater than 0 if it does any healing, we've already spent the psi, so we can afford to do this
+ to_chat(user, span_notice("You patch up some of the damage to [target]."))
+ return TRUE
+
+ if(redaction_rank >= PSI_RANK_GRANDMASTER)
+ // Repair wounds
+ if(ishuman(target))
+ var/mob/living/carbon/human/H = target
+ for(var/datum/wound/W in H.all_wounds)
+ if(W.blood_flow)
+ W.blood_flow -= (redaction_rank * 0.5)
+ if(prob(25))
+ to_chat(user, span_notice("You stem the flow of blood streaming from [target]'s [W.limb]."))
+ return TRUE
+
+ if(istype(W, /datum/wound/burn))
+ var/datum/wound/burn/degree = W
+ degree.sanitization += (redaction_rank * 0.5)
+ degree.flesh_healing += (redaction_rank * 0.5)
+ if(prob(25))
+ to_chat(user, span_notice("You clean and mend the burns on [target]'s [W.limb]."))
+ return TRUE
+
+ if(istype(W, /datum/wound/blunt))
+ qdel(W)
+ playsound(H, 'sound/surgery/bone3.ogg', 25)
+ to_chat(user, span_notice("You snap the bones in [target]'s [W.limb] back into place."))
+ return TRUE
+
+ // Repair internal organs
+ for(var/obj/item/organ/O in target.internal_organs)
+ if(O.damage > 0)
+ var/heal = redaction_rank * 10
+ to_chat(user, span_notice("You encourage the damaged tissues of \the [O] to repair itself."))
+ O.applyOrganDamage(-rand(heal, heal * 2))
+ return TRUE
+
+ to_chat(user, span_notice("You can find nothing within \the [target] to mend."))
+ return FALSE
+
+/datum/psionic_power/revive
+ name = "Revive"
+ cost = 25
+ heat = 100
+ cooldown = 8 SECONDS
+ min_rank = PSI_RANK_OPERANT
+ faculty = PSI_REDACTION
+ icon_state = "redac_revive"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target the mob you wish to revive with combat mode off. Has several limiting factors. Higher psi levels upgrade the revive."
+ admin_log = FALSE
+
+/datum/psionic_power/revive/invoke(mob/living/user, mob/living/target, proximity, parameters)
+ if(user.combat_mode || !istype(target) || !proximity || isipc(target))
+ return FALSE
+ . = ..()
+ if(.)
+ if(target.stat != DEAD && !HAS_TRAIT(target, TRAIT_FAKEDEATH))
+ to_chat(user, span_warning("This person is already alive!"))
+ return TRUE
+
+ var/is_paramount = user.psi.get_rank(PSI_REDACTION) >= PSI_RANK_PARAMOUNT
+
+ if(!is_paramount && (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(is_paramount)
+ return TRUE
+
+/datum/psionic_power/redaction/cleanse
+ name = "Cleanse"
+ cost = 9
+ heat = 15
+ cooldown = 6 SECONDS
+ min_rank = PSI_RANK_MASTER
+ icon_state = "redac_cleanse"
+ use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target the mob you wish cleanse with combat mode off. Cleanses radiation, clone damage, and toxins. Higher psi levels provide further cleansing."
+
+/datum/psionic_power/redaction/cleanse/invoke(mob/living/user, mob/living/carbon/human/target, proximity, parameters)
+ if(user.combat_mode || !istype(target) || !proximity)
+ return FALSE
+ . = ..()
+ if(.)
+ var/removing = (user.psi.get_rank(PSI_REDACTION) - 1) * 10
+ if(target.radiation)
+ to_chat(user, span_notice("You repair some of the radiation-damaged tissue within \the [target]..."))
+ target.radiation = max(target.radiation - removing, 0)
+ return TRUE
+ if(target.getCloneLoss())
+ to_chat(user, span_notice("You stitch together some of the mangled DNA within \the [target]..."))
+ target.adjustCloneLoss(-removing)
+ return TRUE
+ if(target.getToxLoss())
+ to_chat(user, span_notice("You expunge some of the toxins within \the [target]..."))
+ target.adjustToxLoss(-removing, TRUE, TRUE)
+ return TRUE
+ to_chat(user, span_notice("You can find no impurities to cleanse from \the [target]."))
+ return TRUE
diff --git a/code/modules/psionics/interfaces/ui.dm b/code/modules/psionics/interfaces/ui.dm
new file mode 100644
index 0000000000000..1285f4c9aa13d
--- /dev/null
+++ b/code/modules/psionics/interfaces/ui.dm
@@ -0,0 +1,24 @@
+/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()
+ . = ..()
+ handle_visibility()
+
+/obj/screen/psi/proc/handle_visibility()
+ if(hidden)
+ invisibility = 101
+ else
+ invisibility = 0
diff --git a/code/modules/psionics/interfaces/ui_hub.dm b/code/modules/psionics/interfaces/ui_hub.dm
new file mode 100644
index 0000000000000..a2b79874cd96a
--- /dev/null
+++ b/code/modules/psionics/interfaces/ui_hub.dm
@@ -0,0 +1,102 @@
+/obj/screen/psi/hub
+ name = "Psi"
+ icon_state = "psi_suppressed"
+ screen_loc = "EAST-1:28,CENTER-4"
+ plane = HUD_PLANE
+ 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(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/limiter(_owner),
+ new /obj/screen/psi/armour(_owner),
+ new /obj/screen/psi/autoredaction(_owner),
+ new /obj/screen/psi/zorch_harm(_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"
+
+/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 = "Shift Click To Open The Psi Complexus\nStamina: [(owner.psi.stamina/owner.psi.max_stamina)*100]%\nHeat: [owner.psi.heat]\nStunned: [owner.psi.stun ? "True" : "False"]\n")
+
+/obj/screen/psi/hub/MouseExited(location, control, params)
+ . = ..()
+ closeToolTip(usr)
+
+/obj/screen/psi/hub/Click(location, control, params)
+ var/list/click_params = params2list(params)
+ if(click_params["shift"])
+ owner.psi.ui_interact(owner)
+ return
+ if(owner.stat != CONSCIOUS)
+ 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
+ SEND_SOUND(owner, sound('sound/effects/psi/power_unlock.ogg'))
+ owner.psi.show_auras()
+ update_icon()
diff --git a/code/modules/psionics/interfaces/ui_toggle.dm b/code/modules/psionics/interfaces/ui_toggle.dm
new file mode 100644
index 0000000000000..420f9c925129a
--- /dev/null
+++ b/code/modules/psionics/interfaces/ui_toggle.dm
@@ -0,0 +1,126 @@
+// Begin psi armour toggle.
+/obj/screen/psi/armour
+ plane = HUD_PLANE
+ name = "Psi-Armour"
+ icon_state = "psiarmour_off"
+
+/obj/screen/psi/armour/update_icon()
+ ..()
+ //everything but coercion gets psi armour
+ if(!(owner.psi.get_rank(PSI_ENERGISTICS) >= PSI_RANK_OPERANT || owner.psi.get_rank(PSI_PSYCHOKINESIS) >= PSI_RANK_OPERANT || owner.psi.get_rank(PSI_REDACTION) >= PSI_RANK_OPERANT))
+ invisibility = 101
+ if(invisibility == 0)
+ icon_state = owner.psi.use_psi_armour ? "psiarmour_on" : "psiarmour_off"
+
+/obj/screen/psi/armour/Click()
+ if(!owner.psi || owner.stat != CONSCIOUS)
+ return
+ owner.psi.use_psi_armour = !owner.psi.use_psi_armour
+ to_chat(owner, span_notice("You will [owner.psi.use_psi_armour ? "now" : "no longer"] use your psionics to deflect or block incoming attacks."))
+ update_icon()
+
+// End psi armour toggle.
+
+// Begin autoredaction toggle.
+/obj/screen/psi/autoredaction
+ plane = HUD_PLANE
+ name = "Autoredaction"
+ icon_state = "healing_off"
+
+/obj/screen/psi/autoredaction/update_icon()
+ ..()
+ if(owner.psi.get_rank(PSI_REDACTION) < PSI_RANK_OPERANT) //only redaction gets autoredaction
+ invisibility = 101
+ if(invisibility == 0)
+ icon_state = owner.psi.use_autoredaction ? "healing_on" : "healing_off"
+
+/obj/screen/psi/autoredaction/Click()
+ if(!owner.psi || owner.stat != CONSCIOUS)
+ 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"
+ plane = HUD_PLANE
+
+/obj/screen/psi/zorch_harm/update_icon()
+ ..()
+ if(owner.psi.get_rank(PSI_ENERGISTICS) < PSI_RANK_MASTER) //only energistics get zorch
+ invisibility = 101
+ if(invisibility == 0)
+ icon_state = owner.psi.zorch_harm ? "zorch_harm" : "zorch_disable"
+
+/obj/screen/psi/zorch_harm/Click()
+ if(!owner.psi || owner.stat != CONSCIOUS)
+ 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
+ plane = HUD_PLANE
+ 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
+ plane = HUD_PLANE
+
+/obj/screen/psi/toggle_psi_menu/New(mob/living/_owner, 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/handle_visibility()
+ 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 0000000000000..8968a982c360b
--- /dev/null
+++ b/code/modules/psionics/mob/mob.dm
@@ -0,0 +1,46 @@
+/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(isipc(src))
+ return
+ 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)
+
+/mob/living/carbon/human
+ /// Whether or not it's tried to apply the species based latency
+ var/tried_species = FALSE
+
+/mob/living/carbon/human/Login() //happens here because psi sorta relies on the thing having a mind
+ . = ..()
+ if(HAS_TRAIT(src, TRAIT_PSIONICALLY_DEAFENED) || HAS_TRAIT(src, TRAIT_PSIONICALLY_IMMUNE))
+ return
+ if(!psi && !tried_species)
+ tried_species = TRUE
+ var/datum/species/dude = dna.species
+
+ var/list/latencies = dude.possible_faculties
+ latencies = latencies.Copy()
+ if(!length(latencies))
+ return
+
+ if(prob(dude.latency_chance))
+ set_psi_rank(pick_n_take(latencies), dude.starting_psi_level)
+
+ if(prob(dude.latency_chance * 0.1)) //really low chance of getting two if you're tuned
+ set_psi_rank(pick(latencies), dude.starting_psi_level)
+
diff --git a/code/modules/psionics/mob/mob_assay.dm b/code/modules/psionics/mob/mob_assay.dm
new file mode 100644
index 0000000000000..11987e3469e67
--- /dev/null
+++ b/code/modules/psionics/mob/mob_assay.dm
@@ -0,0 +1,153 @@
+/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"
+ var/use_Your = "your"
+ if(istype(machine) || viewer != src)
+ use_He_is = "[p_they(TRUE)] [p_are()]"
+ use_He_has = "[p_they(TRUE)] [p_have()]"
+ use_Your = "[p_their()]"
+
+ 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 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]"
+
+ if(psi.announced)
+ dat += "[use_He_is] currently [psi.suppressed ? "suppressing" : "not suppressing"] [use_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()
+
+/datum/psi_complexus/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "PsionicComplexus", "Psi Complexus")
+ ui.open()
+
+/datum/psi_complexus/ui_data(mob/user)
+ var/list/data = list()
+ var/use_rating
+ var/effective_rating = rating
+ if(effective_rating > 1 && suppressed)
+ effective_rating = max(0, rating-2)
+ var/rating_descriptor
+ 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 rank or higher."
+ else
+ use_rating = "[effective_rating]-Lambda"
+ rating_descriptor = "This indicates the presence of trace latent psi capabilities."
+
+ data["use_rating"] = use_rating
+ data["rating_descriptor"] = rating_descriptor
+ data["faculties"] = list()
+ for(var/faculty_id in ranks)
+ var/list/check_powers = get_powers_by_faculty(faculty_id)
+ if(LAZYLEN(check_powers))
+ var/list/details = list()
+ var/datum/psionic_faculty/faculty = SSpsi.get_faculty(faculty_id)
+ details["name"] += faculty.name
+ details["rank"] += ranks[faculty_id]
+ for(var/datum/psionic_power/power in check_powers)
+ var/list/power_data = list()
+ power_data["name"] = power.name
+ power_data["description"] = power.use_description
+ details["powers"] += list(power_data)
+ data["faculties"] += list(details)
+ return data
+
+/datum/psi_complexus/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/spritesheet/sheetmaterials)
+ )
+
+/datum/psi_complexus/ui_act(action, params)
+ . = ..()
+ if(.)
+ return
+
+/datum/psi_complexus/ui_state()
+ return GLOB.always_state
+
+/datum/asset/spritesheet/psi_icons
+ name = "psi_icons"
+
+/datum/asset/spritesheet/psi_icons/create_spritesheets()
+ InsertAll("", 'icons/obj/psychic_powers.dmi')
diff --git a/code/modules/psionics/null/_null.dm b/code/modules/psionics/null/_null.dm
new file mode 100644
index 0000000000000..3b4b09e07253c
--- /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 0000000000000..68db9e953f315
--- /dev/null
+++ b/code/modules/psionics/null/chemistry.dm
@@ -0,0 +1,35 @@
+/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 0000000000000..b7751417da57a
--- /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/drink_reagents.dm b/code/modules/reagents/chemistry/reagents/drink_reagents.dm
index 8403e5b44ca88..94191e95d6723 100644
--- a/code/modules/reagents/chemistry/reagents/drink_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drink_reagents.dm
@@ -1134,3 +1134,56 @@
glass_icon_state = "cucumber_lemonade"
glass_name = "cucumber lemonade"
glass_desc = "Lemonade, with added cucumber."
+
+/**
+ * basic wakefulness chem
+ * addiction gets less bad over time rather than the inverse
+ */
+/datum/reagent/drug/caffeine
+ name = "Caffeine"
+ description = "Slightly increases wakefulness. If overdosed it will cause jitters and heart problems."
+ reagent_state = SOLID //powder in pure form
+ color = "#ffffff" //very white
+ metabolization_rate = REAGENTS_METABOLISM
+ overdose_threshold = 20 //please don't consume pure caffeine
+ addiction_threshold = 30 //not easy to get addicted to unless you have way too much
+ trippy = FALSE
+ var/list/overdose_text = list("Your head pounds.", "You feel lethargic.", "You feel drowsy.", "You feel weak.", "You just want to sleep.")
+
+/datum/reagent/drug/caffeine/on_mob_life(mob/living/carbon/M)
+ . = ..()
+ if(prob(1))
+ var/caffeine_message = pick("You feel alert.")
+ to_chat(M, span_notice("[caffeine_message]"))
+ M.adjust_drowsiness(-6 SECONDS * REM)
+ M.AdjustSleeping(-4 SECONDS, FALSE)
+ M.adjust_dizzy(-4 SECONDS * REM)
+
+/datum/reagent/drug/caffeine/overdose_process(mob/living/M)
+ . = ..()
+ M.set_jitter_if_lower(20 SECONDS)
+ M.adjustOrganLoss(ORGAN_SLOT_HEART, 1.25*REM)
+
+/datum/reagent/drug/caffeine/proc/apply_drowsy(mob/living/M)
+ M.adjust_drowsiness_up_to(3 SECONDS * REM, 10 SECONDS)
+ if(prob(50))
+ to_chat(M, span_warning(pick(overdose_text)))
+
+/**
+ * doesn't call the parent addiction acts because it doesn't function the same way
+ */
+/datum/reagent/drug/caffeine/addiction_act_stage1(mob/living/M)
+ if(prob(75) && iscarbon(M))
+ apply_drowsy(M)
+
+/datum/reagent/drug/caffeine/addiction_act_stage2(mob/living/M)
+ if(prob(60) && iscarbon(M))
+ apply_drowsy(M)
+
+/datum/reagent/drug/caffeine/addiction_act_stage3(mob/living/M)
+ if(prob(45) && iscarbon(M))
+ apply_drowsy(M)
+
+/datum/reagent/drug/caffeine/addiction_act_stage4(mob/living/M)
+ if(prob(30) && iscarbon(M))
+ apply_drowsy(M)
diff --git a/code/modules/reagents/chemistry/reagents/drug_reagents.dm b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
index 06f1d08eebe07..68a33157633fc 100644
--- a/code/modules/reagents/chemistry/reagents/drug_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
@@ -795,40 +795,6 @@
..()
. = 1
-/**
- * basic wakefulness chem
- * addiction gets less bad over time rather than the inverse
- */
-/datum/reagent/drug/caffeine
- name = "Caffeine"
- description = "Slightly increases wakefulness. If overdosed it will cause jitters and heart problems."
- reagent_state = SOLID //powder in pure form
- color = "#ffffff" //very white
- metabolization_rate = REAGENTS_METABOLISM
- overdose_threshold = 20 //please don't consume pure caffeine
- addiction_threshold = 20 //the addiction isn't that dangerous
- trippy = FALSE
- var/list/overdose_text = list("Your head pounds.", "You feel lethargic.", "You feel drowsy.", "You feel weak.", "You just want to sleep.")
-
-/datum/reagent/drug/caffeine/on_mob_life(mob/living/carbon/M)
- . = ..()
- if(prob(1))
- var/caffeine_message = pick("You feel alert.")
- to_chat(M, span_notice("[caffeine_message]"))
- M.adjust_drowsiness(-6 SECONDS * REM)
- M.AdjustSleeping(-4 SECONDS, FALSE)
- M.adjust_dizzy(-4 SECONDS * REM)
-
-/datum/reagent/drug/caffeine/overdose_process(mob/living/M)
- . = ..()
- M.set_jitter_if_lower(20 SECONDS)
- M.adjustOrganLoss(ORGAN_SLOT_HEART, 1.25*REM)
-
-/datum/reagent/drug/caffeine/proc/apply_drowsy(mob/living/M)
- M.adjust_drowsiness_up_to(3 SECONDS * REM, 10 SECONDS)
- if(prob(50))
- to_chat(M, span_warning(pick(overdose_text)))
-
/**
* doesn't call the parent addiction acts because it doesn't function the same way
*/
@@ -847,3 +813,90 @@
/datum/reagent/drug/caffeine/addiction_act_stage4(mob/living/M)
if(prob(45) && iscarbon(M))
apply_drowsy(M)
+
+
+/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"
+ )
+
+ COOLDOWN_DECLARE(next_trigger)
+ var/cooldown_duration = 15 SECONDS
+
+/datum/reagent/drug/three_eye/on_mob_metabolize(mob/living/L)
+ . = ..()
+ ADD_TRAIT(L, TRAIT_THERMAL_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_THERMAL_VISION, type)
+ L.remove_client_colour(/datum/client_colour/thirdeye)
+ L.update_sight()
+ return ..()
+
+/datum/reagent/drug/three_eye/on_mob_life(mob/living/carbon/M)
+ . = ..()
+ M.adjust_hallucinations_up_to(10 SECONDS, 50 SECONDS)
+ M.adjust_jitter_up_to(3 SECONDS, 10 SECONDS)
+ M.adjust_dizzy_up_to(3 SECONDS, 10 SECONDS)
+ if(prob(0.1))
+ seizure(M)
+ 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))
+ seizure(M)
+ if(prob(10))
+ to_chat(M, span_danger("[pick(overdose_messages)]"))
+ return
+ if(!COOLDOWN_FINISHED(src, next_trigger))
+ return
+ COOLDOWN_START(src, next_trigger, cooldown_duration)
+ if(M.psi)
+ M.psi.check_latency_trigger(30, "a Three Eye overdose", 30)
+
+/datum/reagent/drug/three_eye/proc/seizure(mob/living/M)
+ M.visible_message(span_danger("[M] starts having a seizure!"), span_userdanger("You have a seizure!"))
+ M.Unconscious(10 SECONDS)
+ M.adjust_jitter(10 SECONDS)
diff --git a/code/modules/reagents/chemistry/recipes/drugs.dm b/code/modules/reagents/chemistry/recipes/drugs.dm
index be345727ab00c..aa3e8e8eb0000 100644
--- a/code/modules/reagents/chemistry/recipes/drugs.dm
+++ b/code/modules/reagents/chemistry/recipes/drugs.dm
@@ -12,7 +12,6 @@
mix_message = "The mixture violently reacts, leaving behind a few crystalline shards."
required_temp = 390
-
/datum/chemical_reaction/krokodil
name = "Krokodil"
id = /datum/reagent/drug/krokodil
@@ -80,3 +79,10 @@
id = /datum/reagent/drug/blue_eye
results = list(/datum/reagent/drug/blue_eye = 5)
required_reagents = list(/datum/reagent/medicine/diphenhydramine = 1, /datum/reagent/bluespace = 2, /datum/reagent/iodine = 1, /datum/reagent/gas/hydrogen = 1, /datum/reagent/consumable/sugar = 1)
+
+/datum/chemical_reaction/three_eye
+ name = "Three-Eye"
+ id = /datum/reagent/drug/three_eye
+ results = list(/datum/reagent/drug/three_eye = 3)
+ required_reagents = list(/datum/reagent/drug/blue_eye = 3, /datum/reagent/toxin/mindbreaker = 3, /datum/reagent/medicine/neurine = 3)
+ required_temp = 333
diff --git a/code/modules/reagents/reagent_containers/pill.dm b/code/modules/reagents/reagent_containers/pill.dm
index 3e4f12fa63f11..69f86de89126c 100644
--- a/code/modules/reagents/reagent_containers/pill.dm
+++ b/code/modules/reagents/reagent_containers/pill.dm
@@ -256,6 +256,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/code/modules/research/designs/autolathe_designs.dm b/code/modules/research/designs/autolathe_designs.dm
index 2fc7d0a542aa6..8fea0f94deeab 100644
--- a/code/modules/research/designs/autolathe_designs.dm
+++ b/code/modules/research/designs/autolathe_designs.dm
@@ -965,6 +965,20 @@
build_path = /obj/item/ammo_casing/a357
category = list("hacked", "Security")
+/obj/projectile/bullet/a357/nullglass
+ name = ".357 NULL bullet"
+ damage = 30
+
+/obj/projectile/bullet/a357/nullglass/disrupts_psionics()
+ return src
+
+/obj/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)
+
/datum/design/a357/ironfeather
name = ".357 Ironfeather Bullet"
id = "a357_ironfeather"
diff --git a/code/modules/surgery/amputation.dm b/code/modules/surgery/amputation.dm
index 618f73d01d3e9..69b79c853449d 100644
--- a/code/modules/surgery/amputation.dm
+++ b/code/modules/surgery/amputation.dm
@@ -26,7 +26,7 @@
/datum/surgery_step/sever_limb
name = "sever limb"
- implements = list(TOOL_SCALPEL = 100, TOOL_SAW = 100, /obj/item/melee/arm_blade = 80, /obj/item/melee/chainsaw = 80, /obj/item/mounted_chainsaw = 80, /obj/item/fireaxe = 50, /obj/item/hatchet = 40, /obj/item/kitchen/knife/butcher = 25)
+ implements = list(TOOL_SCALPEL = 100, TOOL_SAW = 100, /obj/item/melee/arm_blade = 80, /obj/item/melee/chainsaw = 80, /obj/item/mounted_chainsaw = 80, /obj/item/fireaxe = 50, TOOL_HATCHET = 40, /obj/item/kitchen/knife/butcher = 25)
time = 6.4 SECONDS
preop_sound = 'sound/surgery/scalpel1.ogg'
success_sound = 'sound/surgery/organ2.ogg'
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 9691c7f672fe6..f29992c4ef13b 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -518,24 +518,27 @@
var/armor_ablation = 0
var/injury_mod = 0
- if(owner && ishuman(owner))
- var/mob/living/carbon/human/H = owner
-
- if(H?.physiology?.armor?.wound)//if there is any innate wound armor (poly or genetics)
- armor_ablation += H.physiology.armor.getRating(WOUND)
-
- var/list/clothing = H.clothingonpart(body_part)
- for(var/c in clothing)
- var/obj/item/clothing/C = c
- // unlike normal armor checks, we tabluate these piece-by-piece manually so we can also pass on appropriate damage the clothing's limbs if necessary
- armor_ablation += C.armor.getRating(WOUND)
- if(wounding_type == WOUND_SLASH)
- C.take_damage_zone(body_zone, damage, BRUTE)
- else if(wounding_type == WOUND_BURN && damage >= 10) // lazy way to block freezing from shredding clothes without adding another var onto apply_damage()
- C.take_damage_zone(body_zone, damage, BURN)
-
- if(!armor_ablation)
- injury_mod += bare_wound_bonus
+ if(owner)
+ if(owner.psi)
+ armor_ablation += owner.psi.get_armour(WOUND)
+ if(ishuman(owner))
+ var/mob/living/carbon/human/H = owner
+
+ if(H?.physiology?.armor?.wound)//if there is any innate wound armor (poly or genetics)
+ armor_ablation += H.physiology.armor.getRating(WOUND)
+
+ var/list/clothing = H.clothingonpart(body_part)
+ for(var/c in clothing)
+ var/obj/item/clothing/C = c
+ // unlike normal armor checks, we tabluate these piece-by-piece manually so we can also pass on appropriate damage the clothing's limbs if necessary
+ armor_ablation += C.armor.getRating(WOUND)
+ if(wounding_type == WOUND_SLASH)
+ C.take_damage_zone(body_zone, damage, BRUTE)
+ else if(wounding_type == WOUND_BURN && damage >= 10) // lazy way to block freezing from shredding clothes without adding another var onto apply_damage()
+ C.take_damage_zone(body_zone, damage, BURN)
+
+ if(!armor_ablation)
+ injury_mod += bare_wound_bonus
injury_mod -= armor_ablation
injury_mod += wound_bonus
diff --git a/code/modules/surgery/lipoplasty.dm b/code/modules/surgery/lipoplasty.dm
index bb0b5ebdfbc61..68f6f0eb31f2e 100644
--- a/code/modules/surgery/lipoplasty.dm
+++ b/code/modules/surgery/lipoplasty.dm
@@ -28,7 +28,7 @@
//cut fat
/datum/surgery_step/cut_fat
name = "cut excess fat"
- implements = list(TOOL_SCALPEL = 100, /obj/item/hatchet = 35, /obj/item/kitchen/knife/butcher = 25)
+ implements = list(TOOL_SCALPEL = 100, TOOL_HATCHET = 35, /obj/item/kitchen/knife/butcher = 25)
time = 6.4 SECONDS
preop_sound = 'sound/surgery/scalpel1.ogg'
success_sound = 'sound/surgery/scalpel2.ogg'
diff --git a/code/modules/surgery/mechanic_steps.dm b/code/modules/surgery/mechanic_steps.dm
index 0d76573a3c86c..c3bb323b584f9 100644
--- a/code/modules/surgery/mechanic_steps.dm
+++ b/code/modules/surgery/mechanic_steps.dm
@@ -18,7 +18,7 @@
/datum/surgery_step/mechanic_open/tool_check(mob/user, obj/item/tool)
if(istype(tool))
- if(!tool.is_sharp())
+ if(implement_type == /obj/item && !tool.is_sharp())
return FALSE
if(tool.usesound)
preop_sound = tool.usesound
@@ -45,7 +45,7 @@
/datum/surgery_step/mechanic_close/tool_check(mob/user, obj/item/tool)
if(istype(tool))
- if(!tool.is_sharp())
+ if(implement_type == /obj/item && !tool.is_sharp())
return FALSE
if(tool.usesound)
preop_sound = tool.usesound
diff --git a/code/modules/surgery/organic_steps.dm b/code/modules/surgery/organic_steps.dm
index 084b48e0de16f..d542ee9c63ad8 100644
--- a/code/modules/surgery/organic_steps.dm
+++ b/code/modules/surgery/organic_steps.dm
@@ -120,7 +120,7 @@
name = "saw bone"
implements = list(TOOL_SAW = 100, /obj/item/melee/transforming/energy/sword/cyborg/saw = 100,
/obj/item/melee/arm_blade = 75, /obj/item/mounted_chainsaw = 65, /obj/item/melee/chainsaw = 50,
- /obj/item/fireaxe = 50, /obj/item/hatchet = 35, /obj/item/kitchen/knife/butcher = 25)
+ /obj/item/fireaxe = 50, TOOL_HATCHET = 35, /obj/item/kitchen/knife/butcher = 25)
time = 5.4 SECONDS
preop_sound = list(
/obj/item/circular_saw = 'sound/surgery/saw.ogg',
diff --git a/code/modules/vending/security.dm b/code/modules/vending/security.dm
index 9b561e265a5f2..9ba62370bf1dc 100644
--- a/code/modules/vending/security.dm
+++ b/code/modules/vending/security.dm
@@ -10,6 +10,7 @@
products = list(/obj/item/clothing/head/helmet/plated = 6,
/obj/item/clothing/suit/armor/plated = 6,
/obj/item/restraints/handcuffs = 8,
+ /obj/item/implanter/psi_control = 3,
/obj/item/clothing/neck/anti_magic_collar = 3,
/obj/item/restraints/handcuffs/cable/zipties = 10,
/obj/item/grenade/flashbang = 4,
diff --git a/icons/effects/psi_aura_small.dmi b/icons/effects/psi_aura_small.dmi
new file mode 100644
index 0000000000000..f5e222b61f5ff
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 6710eb3d925f8..8b4eda3147496 100644
Binary files a/icons/mob/clothing/head/head.dmi and b/icons/mob/clothing/head/head.dmi differ
diff --git a/icons/mob/hud.dmi b/icons/mob/hud.dmi
index 3731e47bd1c8e..7fd2cb6a5628d 100644
Binary files a/icons/mob/hud.dmi and b/icons/mob/hud.dmi differ
diff --git a/icons/mob/inhands/misc/sheets_lefthand.dmi b/icons/mob/inhands/misc/sheets_lefthand.dmi
index 96310e6a934e7..2d86b0b0657ad 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 d0823c6a26110..d08de7f774439 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/melee_lefthand.dmi b/icons/mob/inhands/weapons/melee_lefthand.dmi
index dd0e7cfc86b0f..2a30de4649127 100644
Binary files a/icons/mob/inhands/weapons/melee_lefthand.dmi and b/icons/mob/inhands/weapons/melee_lefthand.dmi differ
diff --git a/icons/mob/inhands/weapons/melee_righthand.dmi b/icons/mob/inhands/weapons/melee_righthand.dmi
index 6b66977d77a07..6ff5b86d65902 100644
Binary files a/icons/mob/inhands/weapons/melee_righthand.dmi and b/icons/mob/inhands/weapons/melee_righthand.dmi differ
diff --git a/icons/mob/inhands/weapons/swords_lefthand.dmi b/icons/mob/inhands/weapons/swords_lefthand.dmi
index d37c6a3960e6d..bad6742d8ab0c 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 1164a7cb7db2b..0f36ce1a39c07 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 0000000000000..44b3941ead8b7
Binary files /dev/null and b/icons/mob/screen_psi.dmi differ
diff --git a/icons/obj/clothing/hats/hats.dmi b/icons/obj/clothing/hats/hats.dmi
index 81a4088cf212b..7c1e102e8d6a3 100644
Binary files a/icons/obj/clothing/hats/hats.dmi and b/icons/obj/clothing/hats/hats.dmi differ
diff --git a/icons/obj/implants.dmi b/icons/obj/implants.dmi
index bcb69a7c44481..fbfacf07e029f 100644
Binary files a/icons/obj/implants.dmi and b/icons/obj/implants.dmi differ
diff --git a/icons/obj/machines/psimeter.dmi b/icons/obj/machines/psimeter.dmi
new file mode 100644
index 0000000000000..d39e351f2cfa0
Binary files /dev/null and b/icons/obj/machines/psimeter.dmi differ
diff --git a/icons/obj/psychic_powers.dmi b/icons/obj/psychic_powers.dmi
new file mode 100644
index 0000000000000..22876436397db
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 94d1602fa007f..c4b6b618c45a6 100644
Binary files a/icons/obj/shards.dmi and b/icons/obj/shards.dmi differ
diff --git a/icons/obj/telescience.dmi b/icons/obj/telescience.dmi
index 8241d42b2fac8..7e5ee24d4dc0d 100644
Binary files a/icons/obj/telescience.dmi and b/icons/obj/telescience.dmi differ
diff --git a/icons/obj/weapons/baton.dmi b/icons/obj/weapons/baton.dmi
index 348db83f41ba0..269abe1ac284d 100644
Binary files a/icons/obj/weapons/baton.dmi and b/icons/obj/weapons/baton.dmi differ
diff --git a/icons/obj/weapons/longsword.dmi b/icons/obj/weapons/longsword.dmi
index a8f7496ac4504..7bba8e2b055c4 100644
Binary files a/icons/obj/weapons/longsword.dmi and b/icons/obj/weapons/longsword.dmi differ
diff --git a/icons/obj/weapons/spears.dmi b/icons/obj/weapons/spears.dmi
index 321ef21ec7b8d..9c75ac7e08ee6 100644
Binary files a/icons/obj/weapons/spears.dmi and b/icons/obj/weapons/spears.dmi differ
diff --git a/sound/effects/psi/power_evoke.ogg b/sound/effects/psi/power_evoke.ogg
new file mode 100644
index 0000000000000..37d9c5a54013c
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 0000000000000..8720d196a80f3
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 0000000000000..75364171dd3fe
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 0000000000000..139dfba0bd46f
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 0000000000000..3ba24f81b38d3
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 0000000000000..aa0978f14bca6
Binary files /dev/null and b/sound/effects/psi/power_used.ogg differ
diff --git a/sound/effects/psi/psisword.ogg b/sound/effects/psi/psisword.ogg
new file mode 100644
index 0000000000000..a89b39723034a
Binary files /dev/null and b/sound/effects/psi/psisword.ogg differ
diff --git a/tgui/packages/tgui/interfaces/PsionicAwakener.js b/tgui/packages/tgui/interfaces/PsionicAwakener.js
new file mode 100644
index 0000000000000..f188a62d34b7a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PsionicAwakener.js
@@ -0,0 +1,129 @@
+import { useBackend } from '../backend';
+import { Box, Button, LabeledList, ProgressBar, Section, AnimatedNumber } from '../components';
+import { Window } from '../layouts';
+
+export const PsionicAwakener = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const {
+ open,
+ occupant = {},
+ occupied,
+ ready,
+ timeleft,
+ result,
+ active_treatment,
+ treatment_cost,
+ nullspace,
+ nullspace_max,
+ } = data;
+
+ const treatments = data.treatments || [];
+
+ return (
+
+
+
+ {occupant.stat}
+
+ )}>
+ {!!occupied && (
+
+
+
+
+ {!!result && (
+
+ {result}
+
+ )}
+
+ )}
+
+ act('door')} />
+ )}>
+
+
+ {
+ nullspace ? nullspace : 0}/{nullspace_max}
+
+
+
+ {active_treatment}
+
+
+ {treatment_cost ? treatment_cost : "nothing"}
+
+
+
+ {!ready && (
+
+
+ ({timeleft}) seconds
+
+
+ )}
+ {treatment_cost > nullspace && (
+
+ INSUFFICIENT NULLSPACE
+
+ )}
+
+
+
+ {treatments.map(treatment => (
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PsionicComplexus.tsx b/tgui/packages/tgui/interfaces/PsionicComplexus.tsx
new file mode 100644
index 0000000000000..3b4850e57fb5f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PsionicComplexus.tsx
@@ -0,0 +1,75 @@
+import { useBackend } from '../backend';
+import { Section, Stack, Table, Collapsible } from '../components';
+import { TableCell, TableRow } from '../components/Table';
+import { Window } from '../layouts';
+
+type Data = {
+ antag_name: string;
+ loud: boolean;
+ faculties: Psi_Faculty[];
+ use_rating: string;
+ rating_descriptor: string;
+};
+
+type User = {
+ psi_stamina: number;
+ supressing: boolean;
+ known_powers: Psi_Power;
+ psi_faculties: Psi_Faculty[];
+
+};
+
+type Psi_Power = {
+ name: string;
+ description: string;
+
+};
+
+type Psi_Faculty = {
+ name: string;
+ rank: number;
+ powers: Psi_Power[];
+
+}
+
+export const PsionicComplexus = (props, context) => {
+ const { data } = useBackend(context);
+ const { faculties = [], use_rating, rating_descriptor } = data;
+ return (
+
+
+
+
+
+
+ {
+ "Psi-Rating: " + use_rating
+ }
+
+
+
+
+
+
+ {faculties.map(faculty => (
+
+ {faculty.powers.map(power => (
+
+
+ {power.description}
+
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+ );
+};
+
diff --git a/yogstation.dme b/yogstation.dme
index 14aa3c5a0e37c..51c525845ffe1 100644
--- a/yogstation.dme
+++ b/yogstation.dme
@@ -123,6 +123,7 @@
#include "code\__DEFINES\procpath.dm"
#include "code\__DEFINES\profile.dm"
#include "code\__DEFINES\projectiles.dm"
+#include "code\__DEFINES\psi.dm"
#include "code\__DEFINES\qdel.dm"
#include "code\__DEFINES\radiation.dm"
#include "code\__DEFINES\radio.dm"
@@ -499,6 +500,7 @@
#include "code\controllers\subsystem\processing\plumbing.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"
@@ -1453,6 +1455,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"
@@ -3245,6 +3248,7 @@
#include "code\modules\modular_computers\file_system\programs\ntpda_msg.dm"
#include "code\modules\modular_computers\file_system\programs\paperworkprinter.dm"
#include "code\modules\modular_computers\file_system\programs\portrait_printer.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\robotact.dm"
#include "code\modules\modular_computers\file_system\programs\themeify.dm"
@@ -3548,6 +3552,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_baton.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_spasms.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\psychokenisis.dm"
+#include "code\modules\psionics\faculties\redaction.dm"
+#include "code\modules\psionics\interfaces\ui.dm"
+#include "code\modules\psionics\interfaces\ui_hub.dm"
+#include "code\modules\psionics\interfaces\ui_toggle.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\flooring.dm"
#include "code\modules\reagents\chem_splash.dm"
#include "code\modules\reagents\reagent_containers.dm"
#include "code\modules\reagents\reagent_dispenser.dm"
@@ -4109,6 +4141,7 @@
#include "yogstation\code\game\gamemodes\vampire\vampire_objectives.dm"
#include "yogstation\code\game\gamemodes\vampire\vampire_other.dm"
#include "yogstation\code\game\gamemodes\vampire\vampire_powers.dm"
+#include "yogstation\code\game\machinery\psionic_awakener.dm"
#include "yogstation\code\game\machinery\suit_storage_unit.dm"
#include "yogstation\code\game\machinery\computer\arcade.dm"
#include "yogstation\code\game\machinery\computer\atmos_sim.dm"
@@ -4180,6 +4213,7 @@
#include "yogstation\code\game\objects\items\robot\robot_parts.dm"
#include "yogstation\code\game\objects\items\stacks\dilithiumcrystal.dm"
#include "yogstation\code\game\objects\items\stacks\sheets\leather.dm"
+#include "yogstation\code\game\objects\items\stacks\sheets\nullspace_crystals.dm"
#include "yogstation\code\game\objects\items\stacks\tiles\tile_types.dm"
#include "yogstation\code\game\objects\items\storage\backpack.dm"
#include "yogstation\code\game\objects\items\storage\bags.dm"
diff --git a/yogstation/code/game/machinery/psionic_awakener.dm b/yogstation/code/game/machinery/psionic_awakener.dm
new file mode 100644
index 0000000000000..fe992896c622e
--- /dev/null
+++ b/yogstation/code/game/machinery/psionic_awakener.dm
@@ -0,0 +1,315 @@
+#define AWAKENER_TRIGGER "Awaken Latencies"
+#define AWAKENER_COERCION "Reinforce Coercion"
+#define AWAKENER_REDACTION "Reinforce Redaction"
+#define AWAKENER_ENERGISTICS "Reinforce Energistics"
+#define AWAKENER_PSYCHOKINESIS "Reinforce Psychokinesis"
+
+/obj/machinery/psionic_awakener
+ name = "psionic awakener"
+ desc = "An enclosed machine used trigger psionic latencies."
+ icon = 'icons/obj/machines/sleeper.dmi'
+ icon_state = "oldpod"
+ base_icon_state = "oldpod"
+ density = FALSE
+ state_open = TRUE
+ circuit = /obj/item/circuitboard/machine/psionic_awakener
+ clicksound = 'sound/machines/pda_button1.ogg'
+
+ var/enter_message = "As the lid slams shut you become acutely aware of how dark and quiet it is inside."
+ var/open_sound = 'sound/machines/podopen.ogg'
+ var/close_sound = 'sound/machines/podclose.ogg'
+
+ /// how much brain damage it does if it tries to unlock potential
+ var/brain_damage = 60 //effectively 50 because the default components reduce it by 10
+ /// % chance of psionic power triggering being successful
+ var/trigger_power = 40 //effectively 50 because the default components increase it by 10
+
+ /// maximum amount of nullspace dust the machine can hold
+ var/nullspace_max = 200
+ /// current amount of nullspace dust the machine has
+ var/nullspace_dust = 0
+
+ /// list of all treatments the awakener is capable of
+ var/list/treatments = list(
+ AWAKENER_TRIGGER = 0,
+ AWAKENER_COERCION = PSI_COERCION,
+ AWAKENER_REDACTION = PSI_REDACTION,
+ AWAKENER_ENERGISTICS = PSI_ENERGISTICS,
+ AWAKENER_PSYCHOKINESIS = PSI_PSYCHOKINESIS
+ )
+
+ /// currently selected outcome from pressing the button
+ var/active_treatment = "none"
+
+ /// text print of the most recent activation result
+ var/recent_result
+
+ COOLDOWN_DECLARE(next_trigger)
+ var/cooldown_duration = 10 SECONDS
+
+/obj/machinery/psionic_awakener/Initialize(mapload)
+ . = ..()
+ occupant_typecache = GLOB.typecache_living
+ update_appearance(UPDATE_ICON)
+
+/obj/machinery/psionic_awakener/update_icon_state()
+ icon_state = "[base_icon_state][state_open ? "-open" : null]"
+ return ..()
+
+/obj/machinery/psionic_awakener/RefreshParts()
+ var/E
+ for(var/obj/item/stock_parts/manipulator/B in component_parts)
+ E += B.rating
+ var/F
+ for(var/obj/item/stock_parts/matter_bin/B in component_parts)
+ F += B.rating
+
+ brain_damage = initial(brain_damage) - (10 * E)
+ trigger_power = initial(trigger_power) + (10 * F)
+
+/obj/machinery/psionic_awakener/container_resist(mob/living/user)
+ visible_message(span_notice("[occupant] emerges from [src]!"), span_notice("You climb out of [src]!"))
+ open_machine()
+
+/obj/machinery/psionic_awakener/Exited(atom/movable/user)
+ if (!state_open && user == occupant)
+ container_resist(user)
+
+/obj/machinery/psionic_awakener/relaymove(mob/user)
+ if (!state_open)
+ container_resist(user)
+
+/obj/machinery/psionic_awakener/open_machine()
+ recent_result = null
+ if(!state_open && !panel_open)
+ flick("[base_icon_state]-anim", src)
+ if(open_sound)
+ playsound(src, open_sound, 40)
+ ..()
+
+/obj/machinery/psionic_awakener/close_machine(mob/user)
+ if((isnull(user) || istype(user)) && state_open && !panel_open)
+ flick("[base_icon_state]-anim", src)
+ ..(user)
+ var/mob/living/mob_occupant = occupant
+ if(mob_occupant && mob_occupant.stat != DEAD)
+ to_chat(occupant, "[enter_message]")
+ if(close_sound)
+ playsound(src, close_sound, 40)
+
+/obj/machinery/psionic_awakener/emp_act(severity)
+ . = ..()
+ if (. & EMP_PROTECT_SELF)
+ return
+ if(is_operational() && occupant)
+ open_machine()
+
+/obj/machinery/psionic_awakener/emag_act(mob/user, obj/item/card/emag/emag_card)
+ if(obj_flags & EMAGGED)
+ return FALSE
+ to_chat(user, span_danger("You disable the safeties of [src]..."))
+ obj_flags |= EMAGGED
+ return TRUE
+
+/obj/machinery/psionic_awakener/MouseDrop_T(mob/target, mob/user)
+ if(user.stat || !Adjacent(user) || !user.Adjacent(target) || !iscarbon(target) || !user.IsAdvancedToolUser())
+ return
+ if(isliving(user))
+ var/mob/living/L = user
+ if(!(L.mobility_flags & MOBILITY_STAND))
+ return
+ close_machine(target)
+
+/obj/machinery/psionic_awakener/screwdriver_act(mob/living/user, obj/item/I)
+ . = TRUE
+ if(..())
+ return
+ if(occupant)
+ to_chat(user, span_warning("[src] is currently occupied!"))
+ return
+ if(state_open)
+ to_chat(user, span_warning("[src] must be closed to [panel_open ? "close" : "open"] its maintenance hatch!"))
+ return
+ if(default_deconstruction_screwdriver(user, "[base_icon_state]-o", base_icon_state, I))
+ return
+ return FALSE
+
+/obj/machinery/psionic_awakener/wrench_act(mob/living/user, obj/item/I)
+ . = ..()
+ if(default_change_direction_wrench(user, I))
+ return TRUE
+
+/obj/machinery/psionic_awakener/crowbar_act(mob/living/user, obj/item/I)
+ . = ..()
+ if(default_pry_open(I))
+ return TRUE
+ if(default_deconstruction_crowbar(I))
+ return TRUE
+
+/obj/machinery/psionic_awakener/default_pry_open(obj/item/I) //wew
+ . = !(state_open || panel_open || (flags_1 & NODECONSTRUCT_1)) && I.tool_behaviour == TOOL_CROWBAR
+ if(.)
+ I.play_tool_sound(src, 50)
+ visible_message(span_notice("[usr] pries open [src]."), span_notice("You pry open [src]."))
+ open_machine()
+
+/obj/machinery/psionic_awakener/ui_state(mob/user)
+ return GLOB.notcontained_state
+
+/obj/machinery/psionic_awakener/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "PsionicAwakener", name)
+ ui.open()
+
+/obj/machinery/psionic_awakener/AltClick(mob/user)
+ if(!user.canUseTopic(src, !issilicon(user)))
+ return
+ if(state_open)
+ close_machine()
+ else
+ open_machine()
+
+/obj/machinery/psionic_awakener/examine(mob/user)
+ . = ..()
+ . += span_notice("Alt-click [src] to [state_open ? "close" : "open"] it.")
+
+/obj/machinery/psionic_awakener/ui_data(mob/user)
+ var/list/data = list()
+ data["occupied"] = occupant ? 1 : 0
+ data["open"] = state_open
+ data["ready"] = COOLDOWN_FINISHED(src, next_trigger)
+ data["timeleft"] = (COOLDOWN_TIMELEFT(src, next_trigger))/10
+ data["result"] = recent_result
+ data["nullspace"] = nullspace_dust
+ data["nullspace_max"] = nullspace_max
+ data["active_treatment"] = active_treatment
+
+ data["treatments"] = list()
+ for(var/T in treatments)
+ data["treatments"] += T
+
+ data["occupant"] = list()
+ var/mob/living/mob_occupant = occupant
+ if(mob_occupant)
+ data["occupant"]["name"] = mob_occupant.name
+ switch(mob_occupant.stat)
+ if(CONSCIOUS)
+ data["occupant"]["stat"] = "Conscious"
+ data["occupant"]["statstate"] = "good"
+ if(SOFT_CRIT)
+ data["occupant"]["stat"] = "Conscious"
+ data["occupant"]["statstate"] = "average"
+ if(UNCONSCIOUS)
+ data["occupant"]["stat"] = "Unconscious"
+ data["occupant"]["statstate"] = "average"
+ if(DEAD)
+ data["occupant"]["stat"] = "Dead"
+ data["occupant"]["statstate"] = "bad"
+ data["occupant"]["brainLoss"] = mob_occupant.getOrganLoss(ORGAN_SLOT_BRAIN)
+
+ data["treatment_cost"] = get_cost(mob_occupant)
+ return data
+
+/obj/machinery/psionic_awakener/proc/get_cost(mob/living/mob_occupant)
+ var/faculty = treatments[active_treatment]
+ var/cost = 0
+ if(faculty)
+ cost = 50 //costs 50 base to unlock a specific faculty
+ if(mob_occupant.psi)
+ var/faculty_rank = mob_occupant.psi.get_rank(faculty)
+ cost += max(faculty_rank-1, 0) * 35 //cost 35 more dust per rank beyond that
+ if(faculty_rank >= PSI_RANK_GRANDMASTER) //could maybe tweak this to be exponential scaling rather than linear
+ cost += 50 //extra 50 to go to paramount (since paramount is fuckin strong)
+ if(faculty_rank >= PSI_RANK_PARAMOUNT)
+ cost = 9999999
+ return cost
+
+/obj/machinery/psionic_awakener/ui_act(action, params)
+ if(..())
+ return
+ var/mob/living/mob_occupant = occupant
+ switch(action)
+ if("door")
+ if(state_open)
+ close_machine()
+ else
+ open_machine()
+ . = TRUE
+ if("set")
+ var/treatment = params["treatment"]
+ if(!is_operational() || isnull(treatment))
+ return
+ active_treatment = treatment
+ . = TRUE
+ if("activate")
+ if(!is_operational() || !mob_occupant || !mob_occupant.mind)
+ return
+
+ switch(active_treatment)
+ if("none")
+ return
+ if(AWAKENER_TRIGGER)
+ trigger_psionics(mob_occupant)
+ else
+ empower_psionics(mob_occupant)
+ . = TRUE
+
+/obj/machinery/psionic_awakener/proc/trigger_psionics(mob/living/mob_occupant)
+ if(!mob_occupant || mob_occupant.stat == DEAD || !COOLDOWN_FINISHED(src, next_trigger))
+ return
+ COOLDOWN_START(src, next_trigger, cooldown_duration)
+
+ if(!mob_occupant.psi || !LAZYLEN(mob_occupant.psi.latencies))
+ visible_message(span_notice("[src] whirrs quietly as it fails to detect any untapped psionic potential."))
+ playsound(src, 'sound/effects/psi/power_fail.ogg', 50, TRUE, 2)
+ recent_result = "Incapable"
+ return
+
+ var/actual_power = trigger_power
+ if(obj_flags & EMAGGED)
+ actual_power = 100
+
+ if(obj_flags & EMAGGED)
+ playsound(src, 'sound/effects/gravhit.ogg', 30, TRUE, 5)
+ visible_message(span_notice("[src] makes some unusual noises."))
+ mob_occupant.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(brain_damage, brain_damage * 2))
+
+ if(mob_occupant?.psi?.check_latency_trigger(actual_power, name, brain_damage))
+ visible_message(span_notice("[src] whirrs loudly as it successfully triggers latent psionic abilities in [mob_occupant]."))
+ playsound(src, 'sound/effects/psi/power_evoke.ogg', 50, TRUE, 2)
+ playsound(src, 'sound/effects/psi/power_fabrication.ogg', 50, TRUE, 2)
+ log_admin("[name] triggered psi latencies for [key_name(mob_occupant)].")
+ message_admins(span_adminnotice("[ADMIN_FLW(name)] triggered psi latencies for [key_name(mob_occupant)]."))
+ recent_result = "Successful"
+ else
+ visible_message(span_notice("[src] whirrs quietly as it fails to unlock any psionic potential."))
+ playsound(src, 'sound/effects/psi/power_fail.ogg', 50, TRUE, 2)
+ recent_result = "Failure"
+
+/obj/machinery/psionic_awakener/proc/empower_psionics(mob/living/mob_occupant)
+ if(!mob_occupant || mob_occupant.stat == DEAD || !COOLDOWN_FINISHED(src, next_trigger))
+ return
+
+ var/faculty = treatments[active_treatment]
+ var/cost = get_cost(mob_occupant)
+
+ if(nullspace_dust < cost)
+ return
+
+ COOLDOWN_START(src, next_trigger, cooldown_duration)
+
+ nullspace_dust -= cost
+
+
+ var/new_rank = max(mob_occupant?.psi?.get_rank(faculty) + 1, PSI_RANK_OPERANT)
+ mob_occupant.set_psi_rank(faculty, new_rank)
+ mob_occupant.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(brain_damage, brain_damage * 2))
+ to_chat(mob_occupant, span_danger("Your head throbs as [src] messes with your brain!"))
+
+ visible_message(span_notice("[src] whirrs loudly as it successfully [new_rank == PSI_RANK_OPERANT ? "awakens" : "reinforces"] [mob_occupant]'s [faculty] faculty."))
+ playsound(src, 'sound/effects/psi/power_evoke.ogg', 50, TRUE, 2)
+ playsound(src, 'sound/effects/psi/power_fabrication.ogg', 50, TRUE, 2)
+ log_admin("[name] upgraded psi [faculty] for [key_name(mob_occupant)].")
+ message_admins(span_adminnotice("[ADMIN_FLW(name)] upgraded psi [faculty] for [key_name(mob_occupant)]."))
+ recent_result = "Successful"
diff --git a/yogstation/code/game/objects/items/stacks/sheets/nullspace_crystals.dm b/yogstation/code/game/objects/items/stacks/sheets/nullspace_crystals.dm
new file mode 100644
index 0000000000000..1d14ce9b7b44f
--- /dev/null
+++ b/yogstation/code/game/objects/items/stacks/sheets/nullspace_crystals.dm
@@ -0,0 +1,48 @@
+/obj/item/nullspace_crystal
+ name = "null skull"
+ desc = "a skull of an ancient psionic user, grants a small amount of nulldust when ground up."
+ icon = 'icons/obj/telescience.dmi'
+ icon_state = "null_skull"
+ w_class = WEIGHT_CLASS_TINY
+ ///how much nullspace dust does each skull give when used on the psionic awakener
+ var/dust = 5
+
+/obj/item/nullspace_crystal/attack_atom(atom/attacked_atom, mob/living/user, params)
+ if(istype(attacked_atom, /obj/machinery/psionic_awakener))
+ var/obj/machinery/psionic_awakener/cart = attacked_atom
+ cart.nullspace_dust += dust
+ to_chat(user, span_notice("You force the [name] into the psionic awakener's grinding port, crushing it to microscopic pieces."))
+ qdel(src)
+ return
+ . = ..()
+
+/obj/item/nullspace_crystal/brilliant
+ name = "fresh null skull"
+ desc = "a fresh skull of a weak psionic user, grants a fair amount of nulldust when ground up."
+ icon_state = "fresh_null_skull"
+ w_class = WEIGHT_CLASS_TINY
+ dust = 10
+
+/obj/item/nullspace_crystal/prismatic
+ name = "aged null skull"
+ desc = "an older skull of an adept psionic user, grants a lot of nulldust when ground up."
+ icon_state = "aged_null_skull"
+ w_class = WEIGHT_CLASS_SMALL
+ dust = 15
+
+/obj/item/nullspace_crystal/true
+ name = "living null skull"
+ desc = "a pitch black skull of a powerful psionic user, looking into it's eye sockets make your cerebellum burn. Grants a huge boon of nulldust when ground up."
+ icon_state = "psionic_null_skull"
+ w_class = WEIGHT_CLASS_NORMAL
+ dust = 50
+
+/obj/effect/spawner/lootdrop/nullspace_crystal_spawner
+ name = "nullskull spawner"
+ lootdoubles = FALSE
+
+ loot = list(
+ /obj/item/nullspace_crystal = 75,
+ /obj/item/nullspace_crystal/brilliant = 20,
+ /obj/item/nullspace_crystal/prismatic = 4,
+ /obj/item/nullspace_crystal/true = 1)
diff --git a/yogstation/code/modules/antagonists/darkspawn/darkspawn_progenitor.dm b/yogstation/code/modules/antagonists/darkspawn/darkspawn_progenitor.dm
index 7806848b9ad7d..bf207e1b15aad 100644
--- a/yogstation/code/modules/antagonists/darkspawn/darkspawn_progenitor.dm
+++ b/yogstation/code/modules/antagonists/darkspawn/darkspawn_progenitor.dm
@@ -91,6 +91,7 @@
//add passive traits, elements, and components
ADD_TRAIT(src, TRAIT_HOLY, INNATE_TRAIT) //sorry no magic
ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT) //so people can actually look at the sprite without the weird bobbing up and down
+ ADD_TRAIT(src, TRAIT_PSIONICALLY_IMMUNE, INNATE_TRAIT) //so no psionic fuckery can happen
AddElement(/datum/element/death_explosion, 20, 20, 20) //with INFINITY health, they're not really able to die, but IF THEY DO
AddComponent(/datum/component/light_eater)
diff --git a/yogstation/code/modules/jobs/job_types/psychiatrist.dm b/yogstation/code/modules/jobs/job_types/psychiatrist.dm
index 1bdb441a54067..c13ed0c8b2d3b 100644
--- a/yogstation/code/modules/jobs/job_types/psychiatrist.dm
+++ b/yogstation/code/modules/jobs/job_types/psychiatrist.dm
@@ -40,6 +40,13 @@
/datum/job/psych/proc/GaxStationChanges() // I'M SORRY
return TRUE
+/datum/job/psych/after_spawn(mob/living/H, mob/M, latejoin = FALSE)
+ . = ..()
+ if(!isipc(H))
+ 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
@@ -49,3 +56,4 @@
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 7371d816dc5e7..29bd1e062fe57 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
@@ -31,6 +31,10 @@
wings_icon = "Plant"
wings_detail = "Plantdetails"
inert_mutation = SAPBLOOD
+
+ latency_chance = 50
+ possible_faculties = list(PSI_REDACTION)
+ starting_psi_level = PSI_RANK_LATENT
var/no_light_heal = FALSE
var/light_heal_multiplier = 1
diff --git a/yogstation/code/modules/mob/living/carbon/human/species_types/preternis/preternis.dm b/yogstation/code/modules/mob/living/carbon/human/species_types/preternis/preternis.dm
index 6c806c3c11e32..b6d4b2fdd1ea3 100644
--- a/yogstation/code/modules/mob/living/carbon/human/species_types/preternis/preternis.dm
+++ b/yogstation/code/modules/mob/living/carbon/human/species_types/preternis/preternis.dm
@@ -27,6 +27,11 @@
punchstunthreshold = 7 //technically better stunning
siemens_coeff = 1.75 //Circuits REALLY don't like extra electricity flying around
+ //psionics
+ latency_chance = 100
+ possible_faculties = list(PSI_COERCION)
+ starting_psi_level = PSI_RANK_LATENT
+
//organs
mutanteyes = /obj/item/organ/eyes/robotic/preternis
mutantlungs = /obj/item/organ/lungs/preternis
diff --git a/yogstation/code/modules/mob/living/carbon/human/species_types/vox.dm b/yogstation/code/modules/mob/living/carbon/human/species_types/vox.dm
index b6cb8a8ffd49b..40132ae87d95a 100644
--- a/yogstation/code/modules/mob/living/carbon/human/species_types/vox.dm
+++ b/yogstation/code/modules/mob/living/carbon/human/species_types/vox.dm
@@ -42,6 +42,10 @@
smells_like = "musty quills"
liked_food = MEAT | FRIED
species_language_holder = /datum/language_holder/vox
+
+ latency_chance = 35
+ possible_faculties = list(PSI_COERCION, PSI_REDACTION)
+ starting_psi_level = PSI_RANK_LATENT
/datum/species/vox/get_species_description()
return "The Vox are remnants of an ancient race, that originate from arkships. \
diff --git a/yogstation/icons/obj/stack_objects.dmi b/yogstation/icons/obj/stack_objects.dmi
index 3825fad165bc1..dabdfbf5a97bc 100644
Binary files a/yogstation/icons/obj/stack_objects.dmi and b/yogstation/icons/obj/stack_objects.dmi differ