diff --git a/SQL/migrate/V081__psionics.sql b/SQL/migrate/V081__psionics.sql
new file mode 100644
index 00000000000..a2f318f7ffe
--- /dev/null
+++ b/SQL/migrate/V081__psionics.sql
@@ -0,0 +1,5 @@
+--
+-- Adds psionic prefs
+--
+
+ALTER TABLE `ss13_characters` ADD COLUMN `psionics` LONGTEXT DEFAULT null AFTER `origin`;
diff --git a/aurorastation.dme b/aurorastation.dme
index a0dab8d0565..6b799fdac63 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -1585,6 +1585,7 @@
#include "code\modules\client\preference_setup\general\04_equipment.dm"
#include "code\modules\client\preference_setup\general\05_background.dm"
#include "code\modules\client\preference_setup\general\06_flavor.dm"
+#include "code\modules\client\preference_setup\general\07_psionics.dm"
#include "code\modules\client\preference_setup\global\01_ui.dm"
#include "code\modules\client\preference_setup\global\02_settings.dm"
#include "code\modules\client\preference_setup\global\03_language.dm"
@@ -2890,32 +2891,66 @@
#include "code\modules\projectiles\targeting\targeting_mob.dm"
#include "code\modules\projectiles\targeting\targeting_overlay.dm"
#include "code\modules\projectiles\targeting\targeting_triggers.dm"
+#include "code\modules\psionics\abilities\_ability.dm"
+#include "code\modules\psionics\abilities\air_bullet.dm"
+#include "code\modules\psionics\abilities\assay.dm"
+#include "code\modules\psionics\abilities\barrier.dm"
+#include "code\modules\psionics\abilities\bubble.dm"
+#include "code\modules\psionics\abilities\charge.dm"
+#include "code\modules\psionics\abilities\command.dm"
+#include "code\modules\psionics\abilities\commune.dm"
+#include "code\modules\psionics\abilities\electrocute.dm"
+#include "code\modules\psionics\abilities\emotional_suggestion.dm"
+#include "code\modules\psionics\abilities\focus.dm"
+#include "code\modules\psionics\abilities\grip.dm"
+#include "code\modules\psionics\abilities\hollow_purple.dm"
+#include "code\modules\psionics\abilities\ion.dm"
+#include "code\modules\psionics\abilities\jump.dm"
+#include "code\modules\psionics\abilities\lightning.dm"
+#include "code\modules\psionics\abilities\mend.dm"
+#include "code\modules\psionics\abilities\mind_muddle.dm"
+#include "code\modules\psionics\abilities\mirror_shade.dm"
+#include "code\modules\psionics\abilities\nlom_eyes.dm"
+#include "code\modules\psionics\abilities\psi_awakening.dm"
+#include "code\modules\psionics\abilities\psi_drain.dm"
+#include "code\modules\psionics\abilities\psi_punch.dm"
+#include "code\modules\psionics\abilities\psi_recovery.dm"
+#include "code\modules\psionics\abilities\psi_search.dm"
+#include "code\modules\psionics\abilities\psi_stamina.dm"
+#include "code\modules\psionics\abilities\psi_sunder.dm"
+#include "code\modules\psionics\abilities\psi_suppression.dm"
+#include "code\modules\psionics\abilities\psi_sword.dm"
+#include "code\modules\psionics\abilities\psi_tool.dm"
+#include "code\modules\psionics\abilities\pull.dm"
+#include "code\modules\psionics\abilities\read_mind.dm"
+#include "code\modules\psionics\abilities\rejuvenate.dm"
+#include "code\modules\psionics\abilities\rend.dm"
+#include "code\modules\psionics\abilities\shockwave.dm"
+#include "code\modules\psionics\abilities\singularity.dm"
+#include "code\modules\psionics\abilities\skinsight.dm"
+#include "code\modules\psionics\abilities\spasm.dm"
+#include "code\modules\psionics\abilities\stasis.dm"
+#include "code\modules\psionics\abilities\throw.dm"
+#include "code\modules\psionics\abilities\time_stop.dm"
+#include "code\modules\psionics\abilities\warp.dm"
+#include "code\modules\psionics\abilities\zona_absorption.dm"
#include "code\modules\psionics\complexus\complexus.dm"
#include "code\modules\psionics\complexus\complexus_helpers.dm"
#include "code\modules\psionics\complexus\complexus_latency.dm"
-#include "code\modules\psionics\complexus\complexus_power_cache.dm"
#include "code\modules\psionics\complexus\complexus_process.dm"
#include "code\modules\psionics\equipment\cerebro_enhancers.dm"
#include "code\modules\psionics\equipment\psipower.dm"
#include "code\modules\psionics\equipment\psipower_blade.dm"
#include "code\modules\psionics\equipment\psipower_tinker.dm"
-#include "code\modules\psionics\equipment\psipower_tk.dm"
#include "code\modules\psionics\events\_psi.dm"
#include "code\modules\psionics\events\mini_spasm.dm"
#include "code\modules\psionics\events\psi_balm.dm"
#include "code\modules\psionics\events\psi_wail.dm"
-#include "code\modules\psionics\faculties\_faculty.dm"
-#include "code\modules\psionics\faculties\_power.dm"
-#include "code\modules\psionics\faculties\coercion.dm"
-#include "code\modules\psionics\faculties\energistics.dm"
-#include "code\modules\psionics\faculties\psychokinesis.dm"
-#include "code\modules\psionics\faculties\redaction.dm"
#include "code\modules\psionics\interface\ui.dm"
#include "code\modules\psionics\interface\ui_hub.dm"
+#include "code\modules\psionics\mob\armour.dm"
#include "code\modules\psionics\mob\mob.dm"
-#include "code\modules\psionics\mob\mob_assay.dm"
#include "code\modules\psionics\mob\mob_helpers.dm"
-#include "code\modules\psionics\mob\mob_interactions.dm"
#include "code\modules\radiation\radiation.dm"
#include "code\modules\random_map\_random_map_setup.dm"
#include "code\modules\random_map\random_map.dm"
diff --git a/code/__defines/_macros.dm b/code/__defines/_macros.dm
index 8fb3983648c..df8c471e300 100644
--- a/code/__defines/_macros.dm
+++ b/code/__defines/_macros.dm
@@ -30,11 +30,11 @@
#define FONT_SIZE_HUGE 4
#define FONT_SIZE_GIANT 5
-#define FONT_SMALL(X) ("" + X + "")
-#define FONT_NORMAL(X) ("" + X + "")
-#define FONT_LARGE(X) ("" + X + "")
-#define FONT_HUGE(X) ("" + X + "")
-#define FONT_GIANT(X) ("" + X + "")
+#define FONT_SMALL(X) ("" + X + "")
+#define FONT_NORMAL(X) ("" + X + "")
+#define FONT_LARGE(X) ("" + X + "")
+#define FONT_HUGE(X) ("" + X + "")
+#define FONT_GIANT(X) ("" + X + "")
#define MATRIX_DANGER(X) (FONT_LARGE(SPAN_DANGER(X)))
#define MATRIX_NOTICE(X) (FONT_LARGE(SPAN_NOTICE(X)))
diff --git a/code/__defines/damage_organs.dm b/code/__defines/damage_organs.dm
index a8fd161b5ca..703fea6c878 100644
--- a/code/__defines/damage_organs.dm
+++ b/code/__defines/damage_organs.dm
@@ -20,6 +20,7 @@
#define DAMAGE_FLAG_EXPLODE 16
#define DAMAGE_FLAG_DISPERSED 32 // Makes apply_damage calls without specified zone distribute damage rather than randomly choose organ (for humans)
#define DAMAGE_FLAG_BIO 64
+#define DAMAGE_FLAG_PSIONIC 128
#define STUN "stun"
#define WEAKEN "weaken"
diff --git a/code/__defines/psi.dm b/code/__defines/psi.dm
index 70b14bf049a..7d3ffc7728c 100644
--- a/code/__defines/psi.dm
+++ b/code/__defines/psi.dm
@@ -1,11 +1,17 @@
-#define PSI_COERCION "coercion"
-#define PSI_PSYCHOKINESIS "psychokinesis"
-#define PSI_REDACTION "redaction"
-#define PSI_ENERGISTICS "energistics"
+#define PSI_RANK_SENSITIVE 1 /// Psionically sensitive. Skrell.
+#define PSI_RANK_HARMONIOUS 2 /// Psionically harmonious. Event Skrell, antagonists.
+#define PSI_RANK_APEX 3 /// Psionic apex. Event people.
+#define PSI_RANK_LIMITLESS 4 /// Admin-only. Yes, it's a Gojou Satoru reference.
-#define PSI_RANK_BLUNT 0
-#define PSI_RANK_LATENT 1
-#define PSI_RANK_OPERANT 2
-#define PSI_RANK_MASTER 3
-#define PSI_RANK_GRANDMASTER 4
-#define PSI_RANK_PARAMOUNT 5
\ No newline at end of file
+#define PSI_FLAG_CANON 1 /// An ability that can be selected by a ship character.
+#define PSI_FLAG_EVENT 2 /// An ability for event characters, not selectable by ship characters, so technically canon.
+#define PSI_FLAG_ANTAG 4 /// An ability for antag characters, not selectable by ship characters, not really canon.
+#define PSI_FLAG_FOUNDATIONAL 8 /// An ability automatically given to Psionically Sensitive users.
+#define PSI_FLAG_APEX 16 /// An ability automatically given to Psionic Apex users.
+#define PSI_FLAG_LIMITLESS 32 /// An ability automatically given to Limitless users.
+#define PSI_FLAG_SPECIAL 64 /// Unselectable in the point shop - must be handed out manually.
+
+/// Psi shop points given by a certain rank.
+#define PSI_POINTS_HARMONIOUS 10
+#define PSI_POINTS_APEX 14
+#define PSI_POINTS_LIMITLESS 40
diff --git a/code/__defines/traits.dm b/code/__defines/traits.dm
index 9062782c3b5..7b07c65dc7f 100644
--- a/code/__defines/traits.dm
+++ b/code/__defines/traits.dm
@@ -139,6 +139,24 @@
/// forces the mob to speak gibberish, like highly damaged borgs
#define TRAIT_SPEAKING_GIBBERISH "speaking_gibberish"
+/// Mob does not need to breathe.
+#define TRAIT_NO_BREATHE "no_breathe"
+
+/// Mob has pressure immunity.
+#define TRAIT_PRESSURE_IMMUNITY "pressure_immunity"
+
+/// Trait sourced from changeling abilities.
+#define TRAIT_SOURCE_CHANGELING "changeling"
+
+/// Mob is psionically deaf.
+#define TRAIT_PSIONICALLY_DEAF "psionically_deaf"
+
+/// Zona bovinae absorbed, used by Loner to track a mob that has had its ZB consumed. Doesn't make them psi-deaf.
+#define TRAIT_ZONA_BOVINAE_ABSORBED "bovinae_absorbed"
+
+/// Hidden from Psi-Search.
+#define TRAIT_PSIONIC_SUPPRESSION "psionic_suppression"
+
/// lets mobs that traditionally don't hallucinate, hallucinate
#define TRAIT_BYPASS_HALLUCINATION_RESTRICTION "bypassing_hallucination_restriction"
@@ -153,3 +171,6 @@
#define TRAIT_OVERLOADER_OD_INITIAL "overloader_od_initial"
#define TRAIT_OVERLOADER_OD_MEDIUM "overloader_od_medium"
#define TRAIT_OVERLOADER_OD_EFFECT "overloader_od_effect"
+
+/// Traits given by psionics.
+#define TRAIT_SOURCE_PSIONICS "psionics"
diff --git a/code/_onclick/hud/ability_screen_objects.dm b/code/_onclick/hud/ability_screen_objects.dm
index c9cbef6601d..256fc7ad1b6 100644
--- a/code/_onclick/hud/ability_screen_objects.dm
+++ b/code/_onclick/hud/ability_screen_objects.dm
@@ -126,6 +126,10 @@
for(var/obj/screen/ability/A in ability_objects)
remove_ability(A)
+/obj/screen/movable/ability_master/proc/remove_all_psionic_abilities()
+ for(var/obj/screen/ability/obj_based/psionic/A in ability_objects)
+ remove_ability(A)
+
/obj/screen/movable/ability_master/proc/get_ability_by_name(name_to_search)
for(var/obj/screen/ability/A in ability_objects)
if(A.name == name_to_search)
@@ -147,7 +151,7 @@
/mob/living/LateLogin()
. = ..()
if(ability_master)
- ability_master.toggle_open(1)
+ ability_master.toggle_open(2)
client.screen -= ability_master
/mob/living/Initialize()
@@ -183,10 +187,15 @@
overlays += ability_icon_state
-/obj/screen/ability/Click()
+/obj/screen/ability/Click(var/location, var/control, var/params)
if(!usr)
return
+ var/list/click_params = params2list(params)
+ if(click_params["shift"])
+ examine(usr)
+ return
+
activate()
/obj/screen/ability/MouseDrop(var/atom/A)
@@ -283,13 +292,44 @@
//////////////////////////////
/obj/screen/ability/obj_based
- var/obj/object = null
+ var/obj/object
/obj/screen/ability/obj_based/activate()
if(object)
object.Click()
-// Technomancer
+/// Psionics.
+/obj/screen/ability/obj_based/psionic
+ icon_state = "nano_spell_base"
+ background_base_state = "nano"
+ var/singleton/psionic_power/connected_power
+
+/obj/screen/ability/obj_based/psionic/Destroy()
+ connected_power = null
+ return ..()
+
+/obj/screen/movable/ability_master/proc/add_psionic_ability(var/obj/object_given, var/ability_icon_given, var/singleton/psionic_power/P)
+ if(!object_given)
+ message_admins("ERROR: add_psionic_ability() was not given an object in its arguments.")
+ if(!P)
+ message_admins("Psionic ability added without connected psionic power singleton!")
+ if(get_ability_by_instance(object_given))
+ return // Duplicate
+ var/obj/screen/ability/obj_based/psionic/A = new /obj/screen/ability/obj_based/psionic()
+ A.ability_master = src
+ A.object = object_given
+ A.ability_icon_state = ability_icon_given
+ A.name = object_given.name
+ A.connected_power = P
+ ability_objects.Add(A)
+ if(my_mob.client)
+ toggle_open(2) //forces the icons to refresh on screen
+
+/obj/screen/ability/obj_based/psionic/examine(mob/user)
+ to_chat(user, SPAN_NOTICE("This ability is [connected_power.name]."))
+ to_chat(user, SPAN_NOTICE("[connected_power.desc]"))
+
+/// Technomancer.
/obj/screen/ability/obj_based/technomancer
icon_state = "wiz_spell_base"
background_base_state = "wiz"
diff --git a/code/controllers/subsystems/processing/psi.dm b/code/controllers/subsystems/processing/psi.dm
index c6a2b7bee15..2a764990c20 100644
--- a/code/controllers/subsystems/processing/psi.dm
+++ b/code/controllers/subsystems/processing/psi.dm
@@ -1,40 +1,100 @@
-var/global/list/psychic_ranks_to_strings = list("Latent", "Operant", "Masterclass", "Grandmasterclass", "Paramount")
+var/global/list/psychic_ranks_to_strings = list("Psionically Sensitive", "Psionically Harmonious", "Psionic Apex", "Limitless")
+
/var/datum/controller/subsystem/processing/psi/SSpsi
/datum/controller/subsystem/processing/psi
- name = "Psychics"
+ name = "Psionics"
priority = SS_PRIORITY_PSYCHICS
- flags = SS_POST_FIRE_TIMING | SS_BACKGROUND
+ flags = SS_BACKGROUND
- var/list/faculties_by_id = list()
- var/list/faculties_by_name = list()
- var/list/all_aura_images = list()
- var/list/all_psi_complexes = list()
- var/list/psi_dampeners = list()
- var/list/psi_monitors = list()
- var/list/armor_faculty_by_type = list()
- var/list/faculties_by_intent = list()
+ var/checking_nlom = FALSE
+ var/last_nlom_awareness_check = 0
+ var/completing_nlom = FALSE
+ var/complete_nlom_time = 0
+ var/list/nlom_votes = list()
+ var/list/nlom_voters = list()
+ var/list/all_aura_images = list()
+ var/list/all_psi_complexes = list()
/datum/controller/subsystem/processing/psi/New()
NEW_SS_GLOBAL(SSpsi)
-/datum/controller/subsystem/processing/psi/proc/get_faculty(var/faculty)
- return faculties_by_name[faculty] || faculties_by_id[faculty]
+/datum/controller/subsystem/processing/psi/fire(resumed)
+ ..()
+ if((world.time >= (last_nlom_awareness_check + 30 MINUTES)) && !checking_nlom && !completing_nlom)
+ checking_nlom = TRUE
+ nlom_votes = list(
+ "Calm" = 0,
+ "Happy" = 0,
+ "Sad" = 0,
+ "Fearful" = 0,
+ "Angry" = 0,
+ "Stressed" = 0,
+ "Confused" = 0
+ )
+ for(var/datum/psi_complexus/PC in all_psi_complexes)
+ if(PC.psionic_rank >= PSI_RANK_SENSITIVE)
+ to_chat(PC.owner, SPAN_NOTICE(FONT_HUGE("The Nlom field prompts you for your emotional state.")))
+ to_chat(PC.owner,
+ "Calm | Happy | \
+ Sad | Fearful | \
+ Angry | Stressed | \
+ Confused= complete_nlom_time)
+ var/highest_emotion = "Happy"
+ for(var/emotion in nlom_votes)
+ if(nlom_votes[highest_emotion] < nlom_votes[emotion])
+ highest_emotion = emotion
+ for(var/datum/psi_complexus/PC in all_psi_complexes)
+ if(PC.psionic_rank >= PSI_RANK_SENSITIVE)
+ var/static/nlom_to_span = list(
+ "Calm" = "good",
+ "Happy" = "good",
+ "Sad" = "notice",
+ "Fearful" = "alien",
+ "Angry" = "danger",
+ "Stressed" = "warning",
+ "Confused" = "cult"
+ )
+ var/static/nlom_to_chat_text = list(
+ "Calm" = "calmness",
+ "Happy" = "happiness",
+ "Sad" = "sadness",
+ "Fearful" = "fear",
+ "Angry" = "anger",
+ "Stressed" = "stress",
+ "Confused" = "confusion"
+ )
+ if(nlom_votes[highest_emotion] > 0)
+ to_chat(PC.owner, SPAN_NOTICE(FONT_HUGE("A sense of [nlom_to_chat_text[highest_emotion]] washes over you.")))
+ else
+ to_chat(PC.owner, SPAN_WARNING(FONT_HUGE("The Nlom is silent and still. You feel uneasy.")))
+ checking_nlom = FALSE
+ completing_nlom = FALSE
+ last_nlom_awareness_check = world.time
+ nlom_votes.Cut()
+ nlom_voters.Cut()
-/datum/controller/subsystem/processing/psi/Initialize()
+/datum/controller/subsystem/processing/psi/Topic(href, href_list)
. = ..()
+ if(.)
+ return
- var/list/faculties = subtypesof(/datum/psionic_faculty)
- for(var/ftype in faculties)
- var/datum/psionic_faculty/faculty = new ftype
- faculties_by_id[faculty.id] = faculty
- faculties_by_name[faculty.name] = faculty
- faculties_by_intent[faculty.associated_intent] = faculty.id
-
- var/list/powers = subtypesof(/datum/psionic_power)
- for(var/ptype in powers)
- var/datum/psionic_power/power = new ptype
- if(power.faculty)
- var/datum/psionic_faculty/faculty = get_faculty(power.faculty)
- if(faculty)
- faculty.powers |= power
\ No newline at end of file
+ if(href_list["emotion"])
+ var/emotion = href_list["emotion"]
+ var/datum/psi_complexus/PC = locate(href_list["voter"])
+ if(PC in nlom_voters)
+ to_chat(PC.owner, SPAN_WARNING("You have already sent your emotional state to the Nlom."))
+ return
+ if(emotion in nlom_votes)
+ var/base_vote = 1
+ var/psi_rank = PC.get_rank()
+ if(psi_rank == PSI_RANK_HARMONIOUS)
+ base_vote = 2
+ else if(psi_rank == PSI_RANK_APEX)
+ base_vote = 4
+ nlom_votes[emotion] += base_vote
+ to_chat(PC.owner, SPAN_NOTICE("You have transmitted your emotional state to the Nlom."))
+ nlom_voters |= PC
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 90a6f10da3e..97c506c4d0d 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -112,7 +112,7 @@
new_character.make_vampire()
if(active)
new_character.key = key //now transfer the key to link the client to our new body
-
+
/datum/mind/proc/store_memory(new_text)
. = length(memory + new_text)
@@ -179,11 +179,6 @@
/datum/mind/Topic(href, href_list)
if(!check_rights(R_ADMIN)) return
- if(current && isliving(current))
- if(href_list["set_psi_faculty"] && href_list["set_psi_faculty_rank"])
- current.set_psi_rank(href_list["set_psi_faculty"], text2num(href_list["set_psi_faculty_rank"]))
- return TRUE
-
if(href_list["add_antagonist"])
var/datum/antagonist/antag = all_antag_types[href_list["add_antagonist"]]
if(antag)
@@ -441,6 +436,16 @@
for(var/datum/objective/objective in objectives)
to_chat(current, "Objective #[obj_count]: [objective.explanation_text]")
obj_count++
+
+ else if(href_list["set_psi_rank"])
+ current.set_psi_rank(text2num(href_list["set_psi_rank"]))
+ return
+ else if(href_list["set_psi_rank_limitless"])
+ var/sure = input(usr, "Limitless is INTENTIONALLY STUPIDLY OVERPOWERED! YOU SHOULD NOT BE USING THIS WITHOUT KNOWING EXACTLY WHAT YOU'RE DOING!", "Don't Get A Staff Complaint") as anything in list("I know what I'm doing!", "I fear no man. But that thing... it scares me!")
+ if(sure == "I know what I'm doing!")
+ current.set_psi_rank(PSI_RANK_LIMITLESS)
+ return
+
edit_memory()
/datum/mind/proc/find_syndicate_uplink()
diff --git a/code/datums/outfits/outfit_antag.dm b/code/datums/outfits/outfit_antag.dm
index 079f63708fa..db9819455c8 100644
--- a/code/datums/outfits/outfit_antag.dm
+++ b/code/datums/outfits/outfit_antag.dm
@@ -172,7 +172,6 @@
/datum/outfit/admin/syndicate/mercenary/loner
name = "Loner"
- head = /obj/item/clothing/head/helmet/space/psi_amp/lesser
l_ear = /obj/item/device/radio/headset/syndicate
r_pocket = /obj/item/device/special_uplink/burglar
diff --git a/code/datums/uplink/highly visible and dangerous weapons.dm b/code/datums/uplink/highly visible and dangerous weapons.dm
index d54c2de97da..86a82e4fa89 100644
--- a/code/datums/uplink/highly visible and dangerous weapons.dm
+++ b/code/datums/uplink/highly visible and dangerous weapons.dm
@@ -251,3 +251,8 @@
name = ".50 Pistol"
telecrystal_cost = 5
path = /obj/item/gun/projectile/deagle/adhomai
+
+/datum/uplink_item/item/visible_weapons/psionic_jumpstarter
+ name = "Psionic Jumpstarter"
+ telecrystal_cost = 19
+ path = /obj/item/psionic_jumpstarter
diff --git a/code/game/antagonist/outsider/loner.dm b/code/game/antagonist/outsider/loner.dm
index f45f5f7ed61..5b30620ec47 100644
--- a/code/game/antagonist/outsider/loner.dm
+++ b/code/game/antagonist/outsider/loner.dm
@@ -7,8 +7,8 @@ var/datum/antagonist/loner/loners
bantype = "loner"
antag_indicator = "loner"
landmark_id = "lonerspawn"
- welcome_text = "You are a Loner, someone underequipped to deal with the station. You will probably not survive for the whole round, so don't sweat it if you die!
\
- You are equipped with a lesser cerebro-enhancer, which allows you to unlock your psionic potential. Use it in-hand to choose your boosted faculty, then install it on your head."
+ welcome_text = "You are a Loner, someone underequipped to deal with the station. You will probably not survive for the whole round, so don't sweat it if you die!
\
+ You have a special psionic power that allows you to absorb a psionic energy from a being's Zona Bovinae, granting you an extra point to be used in the Point Shop."
flags = ANTAG_OVERRIDE_JOB | ANTAG_CLEAR_EQUIPMENT | ANTAG_CHOOSE_NAME | ANTAG_VOTABLE | ANTAG_SET_APPEARANCE
antaghud_indicator = "hudloner"
required_age = 7
@@ -39,8 +39,11 @@ var/datum/antagonist/loner/loners
player.preEquipOutfit(/datum/outfit/admin/syndicate/mercenary/loner, FALSE)
player.equipOutfit(/datum/outfit/admin/syndicate/mercenary/loner, FALSE)
+ player.set_psi_rank(PSI_RANK_HARMONIOUS)
+ var/singleton/psionic_power/P = GET_SINGLETON(/singleton/psionic_power/zona_absorption)
+ P.apply(player)
player.force_update_limbs()
player.update_eyes()
player.regenerate_icons()
- return TRUE
\ No newline at end of file
+ return TRUE
diff --git a/code/game/gamemodes/changeling/helpers/_framework.dm b/code/game/gamemodes/changeling/helpers/_framework.dm
index ce7b30abccc..d0c8160bfb5 100644
--- a/code/game/gamemodes/changeling/helpers/_framework.dm
+++ b/code/game/gamemodes/changeling/helpers/_framework.dm
@@ -10,9 +10,7 @@ var/global/list/possible_changeling_IDs = list("Alpha","Beta","Gamma","Delta","E
var/chem_recharge_rate = 0.5
var/chem_storage = 50
var/sting_range = 1
- var/space_adapted = FALSE
var/using_thermals = FALSE
- var/no_breathing = FALSE
var/changelingID = "Changeling"
var/geneticdamage = 0
var/isabsorbing = 0
diff --git a/code/game/gamemodes/changeling/implements/powers/resources.dm b/code/game/gamemodes/changeling/implements/powers/resources.dm
index 850922335b5..c928a3da13c 100644
--- a/code/game/gamemodes/changeling/implements/powers/resources.dm
+++ b/code/game/gamemodes/changeling/implements/powers/resources.dm
@@ -12,8 +12,7 @@
//removes the need to breathe, removes effects of very low pressure
/mob/proc/changeling_spaceadaption()
- var/datum/changeling/changeling = mind.antag_datums[MODE_CHANGELING]
- changeling.space_adapted = TRUE
+ ADD_TRAIT(src, TRAIT_PRESSURE_IMMUNITY, TRAIT_SOURCE_CHANGELING)
return TRUE
/mob/proc/changeling_armor()
@@ -22,8 +21,7 @@
//removes the need to breathe
/mob/proc/changeling_nobreathing()
- var/datum/changeling/changeling = mind.antag_datums[MODE_CHANGELING]
- changeling.no_breathing = TRUE
+ ADD_TRAIT(src, TRAIT_NO_BREATHE, TRAIT_SOURCE_CHANGELING)
return TRUE
// HIVE MIND UPLOAD/DOWNLOAD DNA
@@ -97,4 +95,4 @@ var/list/datum/absorbed_dna/hivemind_bank = list()
absorbDNA(chosen_dna)
to_chat(src, "We absorb the DNA of [S] from the air.")
feedback_add_details("changeling_powers", "HD")
- return TRUE
\ No newline at end of file
+ return TRUE
diff --git a/code/game/gamemodes/technomancer/catalog.dm b/code/game/gamemodes/technomancer/catalog.dm
index 7027cf96e4e..cb1a329e116 100644
--- a/code/game/gamemodes/technomancer/catalog.dm
+++ b/code/game/gamemodes/technomancer/catalog.dm
@@ -356,7 +356,7 @@ var/list/all_technomancer_assistance = typesof(/datum/technomancer/assistance) -
return
var/obj/item/technomancer_core/core = H.get_technomancer_core()
if(core)
- for(var/obj/spellbutton/spell in core.spells)
+ for(var/obj/spellbutton/technomancer/spell in core.spells)
for(var/datum/technomancer/spell/spell_datum in spell_instances)
if(spell_datum.obj_path == spell.spellpath)
budget += spell_datum.cost
@@ -381,7 +381,7 @@ var/list/all_technomancer_assistance = typesof(/datum/technomancer/assistance) -
// We sadly need to do special stuff here or else people who refund cores with spells will lose points permanently.
if(istype(AM, /obj/item/technomancer_core))
var/obj/item/technomancer_core/core = AM
- for(var/obj/spellbutton/spell in core.spells)
+ for(var/obj/spellbutton/technomancer/spell in core.spells)
for(var/datum/technomancer/spell/spell_datum in spell_instances)
if(spell_datum.obj_path == spell.spellpath)
budget += spell_datum.cost
diff --git a/code/game/gamemodes/technomancer/core_obj.dm b/code/game/gamemodes/technomancer/core_obj.dm
index 39f307d1c1f..dc3b553ac43 100644
--- a/code/game/gamemodes/technomancer/core_obj.dm
+++ b/code/game/gamemodes/technomancer/core_obj.dm
@@ -178,17 +178,15 @@
/obj/spellbutton
name = "generic spellbutton"
var/spellpath
- var/obj/item/technomancer_core/core
var/ability_icon_state
-/obj/spellbutton/New(loc, var/path, var/new_name, var/new_icon_state)
+/obj/spellbutton/Initialize(mapload, var/path, var/new_name, var/new_icon_state)
+ . = ..()
if(!path || !ispath(path))
- message_admins("ERROR: /obj/spellbutton/New() was not given a proper path!")
- qdel(src)
+ message_admins("ERROR: /obj/spellbutton/Initialize() was not given a proper path!")
+ return INITIALIZE_HINT_QDEL
src.name = new_name
src.spellpath = path
- src.loc = loc
- src.core = loc
src.ability_icon_state = new_icon_state
/obj/spellbutton/Click()
@@ -199,6 +197,14 @@
/obj/spellbutton/DblClick()
return Click()
+/obj/spellbutton/technomancer
+ var/obj/item/technomancer_core/core
+
+/obj/spellbutton/technomancer/Initialize(mapload, var/path, var/new_name, var/new_icon_state)
+ . = ..()
+ src.loc = loc
+ src.core = loc
+
/mob/living/carbon/human/get_status_tab_items()
. = ..()
@@ -216,7 +222,7 @@
message_admins("ERROR: /obj/item/technomancer_core/add_spell() was not given a proper path! \
The path supplied was [path].")
return
- var/obj/spellbutton/spell = new(src, path, new_name, ability_icon_state)
+ var/obj/spellbutton/technomancer/spell = new(src, path, new_name, ability_icon_state)
spells.Add(spell)
if(wearer)
wearer.ability_master.add_technomancer_ability(spell, ability_icon_state)
diff --git a/code/game/gamemodes/technomancer/spell_objs.dm b/code/game/gamemodes/technomancer/spell_objs.dm
index 93bd9058620..8958ed6d376 100644
--- a/code/game/gamemodes/technomancer/spell_objs.dm
+++ b/code/game/gamemodes/technomancer/spell_objs.dm
@@ -20,6 +20,7 @@
#define ASPECT_UNSTABLE "unstable" //Heavily RNG-based, causes instability to the victim.
#define ASPECT_CHROMATIC "chromatic" //Used to combine with other spells.
#define ASPECT_UNHOLY "unholy" //Involves the dead, blood, and most things against divine beings.
+#define ASPECT_PSIONIC "psionics" //Psionic power. Bypasses core checks.
/obj/item/spell
name = "glowing particles"
@@ -32,13 +33,15 @@
)
throwforce = 0
force = 0
- var/mob/living/carbon/human/owner = null
- var/obj/item/technomancer_core/core = null
+ flags = NOBLUDGEON
+ var/mob/living/carbon/human/owner
+ var/obj/item/technomancer_core/core
var/cast_methods = null // Controls how the spell is casted.
var/aspect = null // Used for combining spells.
var/toggled = 0 // Mainly used for overlays.
var/cooldown = 0 // If set, will add a cooldown overlay and adjust click delay. Must be a multiple of 5 for overlays.
var/cast_sound = null // Sound file played when this is used.
+ var/psi_cost = 0 // Psi complexus cost to use this spell.
/obj/item/spell/examine(mob/user, distance) // Nothing on examine.
return
@@ -46,38 +49,62 @@
// Proc: on_use_cast()
// Parameters: 1 (user - the technomancer casting the spell)
// Description: Override this for clicking the spell in your hands.
-/obj/item/spell/proc/on_use_cast(mob/user)
- return
+/obj/item/spell/proc/on_use_cast(mob/user, var/bypass_psi_check)
+ SHOULD_CALL_PARENT(TRUE)
+ if(aspect == ASPECT_PSIONIC && !bypass_psi_check)
+ if(!owner.psi.spend_power(psi_cost))
+ return FALSE
+ return TRUE
// Proc: on_throw_cast()
// Parameters: 1 (hit_atom - the atom hit by the spell object)
// Description: Override this for throwing effects.
-/obj/item/spell/proc/on_throw_cast(atom/hit_atom)
- return
+/obj/item/spell/proc/on_throw_cast(atom/hit_atom, var/bypass_psi_check)
+ SHOULD_CALL_PARENT(TRUE)
+ if(aspect == ASPECT_PSIONIC && !bypass_psi_check)
+ if(!owner.psi.spend_power(psi_cost))
+ return FALSE
+ return TRUE
// Proc: on_ranged_cast()
// Parameters: 2 (hit_atom - the atom clicked on by the user, user - the technomancer that clicked hit_atom)
// Description: Override this for ranged effects.
-/obj/item/spell/proc/on_ranged_cast(atom/hit_atom, mob/user)
- return
+/obj/item/spell/proc/on_ranged_cast(atom/hit_atom, mob/user, var/bypass_psi_check)
+ SHOULD_CALL_PARENT(TRUE)
+ if(aspect == ASPECT_PSIONIC && !bypass_psi_check)
+ if(!owner.psi.spend_power(psi_cost))
+ return FALSE
+ return TRUE
// Proc: on_melee_cast()
// Parameters: 3 (hit_atom - the atom clicked on by the user, user - the technomancer that clicked hit_atom, def_zone - unknown)
// Description: Override this for effects that occur at melee range.
/obj/item/spell/proc/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
- return
+ SHOULD_CALL_PARENT(TRUE)
+ if(aspect == ASPECT_PSIONIC)
+ if(!owner.psi.spend_power(psi_cost))
+ return FALSE
+ return TRUE
// Proc: on_combine_cast()
// Parameters: 2 (I - the item trying to merge with the spell, user - the technomancer who initiated the merge)
// Description: Override this for combining spells, like Aspect spells.
/obj/item/spell/proc/on_combine_cast(obj/item/I, mob/user)
- return
+ SHOULD_CALL_PARENT(TRUE)
+ if(aspect == ASPECT_PSIONIC)
+ if(!owner.psi.spend_power(psi_cost))
+ return FALSE
+ return TRUE
// Proc: on_innate_cast()
// Parameters: 1 (user - the entity who is casting innately (without using hands).)
// Description: Override this for casting without using hands (and as a result not using spell objects).
/obj/item/spell/proc/on_innate_cast(mob/user)
- return
+ SHOULD_CALL_PARENT(TRUE)
+ if(aspect == ASPECT_PSIONIC)
+ if(!owner.psi.spend_power(psi_cost))
+ return FALSE
+ return TRUE
// Proc: on_scepter_use_cast()
// Parameters: 1 (user - the holder of the Scepter that clicked.)
@@ -99,7 +126,9 @@
// if they are able to pay, it is deducted automatically.
/obj/item/spell/proc/pay_energy(var/amount)
if(!core)
- return 0
+ if(aspect == ASPECT_PSIONIC)
+ return owner.psi.spend_power(amount)
+ return amount
return core.pay_energy(amount)
// Proc: give_energy()
@@ -107,7 +136,7 @@
// Description: Redirects the call to the core's give_energy().
/obj/item/spell/proc/give_energy(var/amount)
if(!core)
- return 0
+ return amount
return core.give_energy(amount)
// Proc: adjust_instability()
@@ -141,11 +170,17 @@
if(isliving(loc))
owner = loc
if(owner)
- core = owner.get_technomancer_core()
- if(!core)
- to_chat(owner, "You need a Core to do that.")
- qdel(src)
- return
+ if(aspect != ASPECT_PSIONIC)
+ core = owner.get_technomancer_core()
+ if(!core)
+ to_chat(owner, "You need a Core to do that.")
+ return INITIALIZE_HINT_QDEL
+ else
+ if(owner.psi.get_rank() >= PSI_RANK_APEX)
+ if(force)
+ force *= 1.1
+ armor_penetration *= 1.1
+
update_icon()
// Proc: Destroy()
@@ -180,32 +215,37 @@
// if it still can't find one. It will also check if the core is being worn properly, and finally checks if the owner is a technomancer.
/obj/item/spell/proc/run_checks()
if(!owner)
- return 0
- if(!core)
- core = locate(/obj/item/technomancer_core) in owner
+ return FALSE
+ if(aspect != ASPECT_PSIONIC)
if(!core)
+ core = locate(/obj/item/technomancer_core) in owner
+ if(!core)
+ to_chat(owner, "You need to be wearing a core on your back or your wrists!")
+ return FALSE
+ if(core.loc != owner || (owner.back != core && owner.wrists != core)) //Make sure the core's being worn.
to_chat(owner, "You need to be wearing a core on your back or your wrists!")
- return 0
- if(core.loc != owner || (owner.back != core && owner.wrists != core)) //Make sure the core's being worn.
- to_chat(owner, "You need to be wearing a core on your back or your wrists!")
- return 0
- if(!core.simple_operation && !technomancers.is_technomancer(owner.mind)) //Now make sure the person using this is the actual antag.
- to_chat(owner, "You can't seem to figure out how to make the machine work properly.")
- return 0
- return 1
+ return FALSE
+ if(!core.simple_operation && !technomancers.is_technomancer(owner.mind)) //Now make sure the person using this is the actual antag.
+ to_chat(owner, SPAN_DANGER("You can't seem to figure out how to make the machine work properly."))
+ return FALSE
+ else
+ if(!owner.psi)
+ return FALSE
+ return TRUE
// Proc: check_for_scepter()
// Parameters: 0
// Description: Terrible code to check if a scepter is in the offhand, returns 1 if yes.
/obj/item/spell/proc/check_for_scepter()
- if(!src || !owner) return 0
+ if(!src || !owner)
+ return FALSE
if(owner.r_hand == src)
if(istype(owner.l_hand, /obj/item/scepter))
- return 1
+ return TRUE
else
if(istype(owner.r_hand, /obj/item/scepter))
- return 1
- return 0
+ return TRUE
+ return FALSE
// Proc: get_other_hand()
// Parameters: 1 (I - item being compared to determine what the offhand is)
@@ -259,7 +299,11 @@
if(cast_methods & CAST_RANGED) //Try to use a ranged method if a melee one doesn't exist.
on_ranged_cast(target, user)
if(cooldown)
- var/effective_cooldown = round(cooldown * core.cooldown_modifier, 5)
+ var/effective_cooldown
+ if(aspect == ASPECT_PSIONIC)
+ effective_cooldown = cooldown
+ else
+ effective_cooldown = round(cooldown * core.cooldown_modifier, 5)
user.setClickCooldown(effective_cooldown)
flick("cooldown_[effective_cooldown]",src)
@@ -330,3 +374,8 @@
spawn(20)
if(src)
qdel(src)
+
+/obj/item/spell/damage_flags()
+ . = ..()
+ if(aspect == ASPECT_PSIONIC)
+ . |= DAMAGE_FLAG_PSIONIC
diff --git a/code/game/gamemodes/technomancer/spells/abjuration.dm b/code/game/gamemodes/technomancer/spells/abjuration.dm
index d5f6f315dd6..7e967d4e082 100644
--- a/code/game/gamemodes/technomancer/spells/abjuration.dm
+++ b/code/game/gamemodes/technomancer/spells/abjuration.dm
@@ -15,6 +15,7 @@
aspect = ASPECT_TELE
/obj/item/spell/abjuration/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
if(istype(hit_atom, /mob/living) && pay_energy(500) && within_range(hit_atom))
var/mob/living/L = hit_atom
var/mob/living/simple_animal/SM = null
@@ -38,4 +39,4 @@
// In case NarNar comes back someday.
if(istype(hit_atom, /obj/singularity/narsie))
to_chat(user, "One does not simply abjurate Nar'sie away.")
- adjust_instability(200)
\ No newline at end of file
+ adjust_instability(200)
diff --git a/code/game/gamemodes/technomancer/spells/apportation.dm b/code/game/gamemodes/technomancer/spells/apportation.dm
index e8d7a767e22..7f9ba497b04 100644
--- a/code/game/gamemodes/technomancer/spells/apportation.dm
+++ b/code/game/gamemodes/technomancer/spells/apportation.dm
@@ -16,6 +16,7 @@
aspect = ASPECT_TELE
/obj/item/spell/apportation/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
if(istype(hit_atom, /atom/movable))
var/atom/movable/AM = hit_atom
diff --git a/code/game/gamemodes/technomancer/spells/audible_deception.dm b/code/game/gamemodes/technomancer/spells/audible_deception.dm
index f4245cb329d..af74f41971c 100644
--- a/code/game/gamemodes/technomancer/spells/audible_deception.dm
+++ b/code/game/gamemodes/technomancer/spells/audible_deception.dm
@@ -65,6 +65,7 @@
var/selected_sound = null
/obj/item/spell/audible_deception/on_use_cast(mob/user)
+ . = ..()
var/list/sound_options = available_sounds
if(check_for_scepter())
sound_options["!!AIR HORN!!"] = 'sound/items/AirHorn.ogg'
@@ -73,6 +74,7 @@
selected_sound = sound_options[new_sound]
/obj/item/spell/audible_deception/on_ranged_cast(atom/hit_atom, mob/living/user)
+ . = ..()
var/turf/T = get_turf(hit_atom)
if(selected_sound && pay_energy(200))
playsound(src, selected_sound, 80, 1, -1)
diff --git a/code/game/gamemodes/technomancer/spells/aura/biomed_aura.dm b/code/game/gamemodes/technomancer/spells/aura/biomed_aura.dm
index 29127fc99b1..3d515319b99 100644
--- a/code/game/gamemodes/technomancer/spells/aura/biomed_aura.dm
+++ b/code/game/gamemodes/technomancer/spells/aura/biomed_aura.dm
@@ -36,5 +36,6 @@
adjust_instability(2)
/obj/item/spell/aura/biomed/on_use_cast(mob/living/user)
+ . = ..()
heal_allies_only = !heal_allies_only
to_chat(user, "Your aura will now heal [heal_allies_only ? "your allies" : "everyone"] near you.")
diff --git a/code/game/gamemodes/technomancer/spells/chroma.dm b/code/game/gamemodes/technomancer/spells/chroma.dm
index 44f43121dda..a677d8516ba 100644
--- a/code/game/gamemodes/technomancer/spells/chroma.dm
+++ b/code/game/gamemodes/technomancer/spells/chroma.dm
@@ -30,6 +30,7 @@
set_light(6, 5, l_color = new_color)
/obj/item/spell/chroma/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
var/turf/T = get_turf(hit_atom)
if(T)
new /obj/effect/temporary_effect/chroma(T, color_to_use)
@@ -37,6 +38,7 @@
qdel(src)
/obj/item/spell/chroma/on_use_cast(mob/user)
+ . = ..()
var/new_color = input(user, "Choose the color you want your light to be.", "Color selection") as null|color
if(new_color)
color_to_use = new_color
diff --git a/code/game/gamemodes/technomancer/spells/condensation.dm b/code/game/gamemodes/technomancer/spells/condensation.dm
index 0f430f4c0c7..f1d5e9b2445 100644
--- a/code/game/gamemodes/technomancer/spells/condensation.dm
+++ b/code/game/gamemodes/technomancer/spells/condensation.dm
@@ -17,6 +17,7 @@
cooldown = 2 SECONDS
/obj/item/spell/condensation/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
if(pay_energy(200))
if(istype(hit_atom, /turf/simulated) && within_range(hit_atom))
var/turf/simulated/T = hit_atom
@@ -41,4 +42,4 @@
log_and_message_admins("has wetted the floor with [src] at [T.x],[T.y],[T.z].")
else if(hit_atom.reagents && !ismob(hit_atom)) //TODO: Something for the scepter
hit_atom.reagents.add_reagent(/singleton/reagent/water, 60, safety = FALSE)
- adjust_instability(5)
\ No newline at end of file
+ adjust_instability(5)
diff --git a/code/game/gamemodes/technomancer/spells/dispel.dm b/code/game/gamemodes/technomancer/spells/dispel.dm
index 17895c6d4bb..d34d3890a21 100644
--- a/code/game/gamemodes/technomancer/spells/dispel.dm
+++ b/code/game/gamemodes/technomancer/spells/dispel.dm
@@ -16,5 +16,6 @@
aspect = ASPECT_BIOMED
/obj/item/spell/dispel/on_ranged_cast(atom/hit_atom, mob/living/user)
+ . = ..()
user.adjust_instability(10)
- qdel(src)
\ No newline at end of file
+ qdel(src)
diff --git a/code/game/gamemodes/technomancer/spells/energy_siphon.dm b/code/game/gamemodes/technomancer/spells/energy_siphon.dm
index 70613fcc86b..423fa489271 100644
--- a/code/game/gamemodes/technomancer/spells/energy_siphon.dm
+++ b/code/game/gamemodes/technomancer/spells/energy_siphon.dm
@@ -49,6 +49,7 @@
/obj/item/spell/energy_siphon/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
if(istype(hit_atom, /atom/movable) && within_range(hit_atom, 4))
var/atom/movable/AM = hit_atom
populate_siphon_list(AM)
diff --git a/code/game/gamemodes/technomancer/spells/flame_tongue.dm b/code/game/gamemodes/technomancer/spells/flame_tongue.dm
index ec73519aac3..8275abeeec1 100644
--- a/code/game/gamemodes/technomancer/spells/flame_tongue.dm
+++ b/code/game/gamemodes/technomancer/spells/flame_tongue.dm
@@ -34,9 +34,10 @@
//Needed to make the spell welder have infinite fuel. Don't worry, it uses energy instead.
/obj/item/weldingtool/spell/use(var/amount = 1, var/mob/M = null, var/colourChange = TRUE)
- return 1
+ return TRUE
/obj/item/spell/flame_tongue/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
if(isliving(hit_atom) && user.a_intent != I_HELP)
var/mob/living/L = hit_atom
if(pay_energy(1000))
diff --git a/code/game/gamemodes/technomancer/spells/gambit.dm b/code/game/gamemodes/technomancer/spells/gambit.dm
index 16cf330e06c..ea3a71eef10 100644
--- a/code/game/gamemodes/technomancer/spells/gambit.dm
+++ b/code/game/gamemodes/technomancer/spells/gambit.dm
@@ -26,6 +26,7 @@
aspect = ASPECT_UNSTABLE
/obj/item/spell/gambit/on_use_cast(mob/living/carbon/human/user)
+ . = ..()
if(pay_energy(200))
adjust_instability(3)
if(check_for_scepter())
@@ -113,4 +114,4 @@
if(!potential_spells.len)
potential_spells = all_technomancer_gambit_spells.Copy()
- return pick(potential_spells)
\ No newline at end of file
+ return pick(potential_spells)
diff --git a/code/game/gamemodes/technomancer/spells/illusion.dm b/code/game/gamemodes/technomancer/spells/illusion.dm
index 8572b0bd300..e326b11ef07 100644
--- a/code/game/gamemodes/technomancer/spells/illusion.dm
+++ b/code/game/gamemodes/technomancer/spells/illusion.dm
@@ -17,6 +17,7 @@
var/mob/living/simple_animal/illusion/illusion = null
/obj/item/spell/illusion/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
if(istype(hit_atom, /atom/movable))
var/atom/movable/AM = hit_atom
if(pay_energy(100))
@@ -38,6 +39,7 @@
return 1
/obj/item/spell/illusion/on_use_cast(mob/user)
+ . = ..()
if(illusion)
var/choice = alert(user, "Would you like to have \the [illusion] speak, or do an emote?", "Illusion", "Speak","Emote","Cancel")
switch(choice)
diff --git a/code/game/gamemodes/technomancer/spells/insert/insert.dm b/code/game/gamemodes/technomancer/spells/insert/insert.dm
index 36eb4aa2c4b..ccfad5eb5a4 100644
--- a/code/game/gamemodes/technomancer/spells/insert/insert.dm
+++ b/code/game/gamemodes/technomancer/spells/insert/insert.dm
@@ -49,11 +49,13 @@
qdel(src)
/obj/item/spell/insert/on_melee_cast(atom/hit_atom, mob/user)
+ . = ..()
if(istype(hit_atom, /mob/living))
var/mob/living/L = hit_atom
insert(L,user)
/obj/item/spell/insert/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
if(istype(hit_atom, /mob/living))
var/mob/living/L = hit_atom
- insert(L,user)
\ No newline at end of file
+ insert(L,user)
diff --git a/code/game/gamemodes/technomancer/spells/instability_tap.dm b/code/game/gamemodes/technomancer/spells/instability_tap.dm
index 67aac5557fb..e68fdb8e6d1 100644
--- a/code/game/gamemodes/technomancer/spells/instability_tap.dm
+++ b/code/game/gamemodes/technomancer/spells/instability_tap.dm
@@ -19,6 +19,7 @@
set_light(3, 2, l_color = "#FA58F4")
/obj/item/spell/instability_tap/on_use_cast(mob/user)
+ . = ..()
var/amount = calculate_spell_power(5000)
if(check_for_scepter())
core.give_energy(amount * 1.5)
@@ -27,4 +28,4 @@
core.give_energy(amount)
adjust_instability(50)
playsound(src, 'sound/effects/supermatter.ogg', 75, 1)
- qdel(src)
\ No newline at end of file
+ qdel(src)
diff --git a/code/game/gamemodes/technomancer/spells/mark_recall.dm b/code/game/gamemodes/technomancer/spells/mark_recall.dm
index 1c78660582d..86a4ceb6f7b 100644
--- a/code/game/gamemodes/technomancer/spells/mark_recall.dm
+++ b/code/game/gamemodes/technomancer/spells/mark_recall.dm
@@ -27,6 +27,7 @@
aspect = ASPECT_TELE
/obj/item/spell/mark/on_use_cast(mob/living/user)
+ . = ..()
if(!allowed_to_teleport()) // Otherwise you could teleport back to the admin Z-level.
to_chat(user, "You can't teleport here!")
return 0
@@ -63,6 +64,7 @@
aspect = ASPECT_TELE
/obj/item/spell/recall/on_use_cast(mob/living/user)
+ . = ..()
if(pay_energy(3000))
if(!mark_spell_ref)
to_chat(user, "There's no Mark!")
diff --git a/code/game/gamemodes/technomancer/spells/mend_organs.dm b/code/game/gamemodes/technomancer/spells/mend_organs.dm
index 1b440605ba3..b6c079b4cad 100644
--- a/code/game/gamemodes/technomancer/spells/mend_organs.dm
+++ b/code/game/gamemodes/technomancer/spells/mend_organs.dm
@@ -17,6 +17,7 @@
light_color = "#FF5C5C"
/obj/item/spell/mend_organs/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
if(isliving(hit_atom))
var/mob/living/L = hit_atom
var/heal_power = calculate_spell_power(40)
diff --git a/code/game/gamemodes/technomancer/spells/modifier/modifier.dm b/code/game/gamemodes/technomancer/spells/modifier/modifier.dm
index b88cefc1ab9..5f7c621f63c 100644
--- a/code/game/gamemodes/technomancer/spells/modifier/modifier.dm
+++ b/code/game/gamemodes/technomancer/spells/modifier/modifier.dm
@@ -14,11 +14,13 @@
set_light(spell_light_range, spell_light_intensity, l_color = light_color)
/obj/item/spell/modifier/on_melee_cast(atom/hit_atom, mob/user)
+ . = ..()
if(isliving(hit_atom))
return on_add_modifier(hit_atom)
return FALSE
/obj/item/spell/modifier/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
if(isliving(hit_atom))
return on_add_modifier(hit_atom)
return FALSE
@@ -45,4 +47,4 @@
/datum/modifier/technomancer/stop()
..()
- to_chat(target, SPAN_WARNING(on_expired_text))
\ No newline at end of file
+ to_chat(target, SPAN_WARNING(on_expired_text))
diff --git a/code/game/gamemodes/technomancer/spells/oxygenate.dm b/code/game/gamemodes/technomancer/spells/oxygenate.dm
index eafd2cd7bdb..f17955af0d1 100644
--- a/code/game/gamemodes/technomancer/spells/oxygenate.dm
+++ b/code/game/gamemodes/technomancer/spells/oxygenate.dm
@@ -16,6 +16,7 @@
cooldown = 30
/obj/item/spell/oxygenate/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
if(!within_range(hit_atom))
return
if(ishuman(hit_atom))
@@ -30,4 +31,4 @@
T.assume_gas("oxygen", 200)
T.assume_gas("nitrogen", 800)
playsound(src, 'sound/effects/spray.ogg', 50, 1, -3)
- adjust_instability(10)
\ No newline at end of file
+ adjust_instability(10)
diff --git a/code/game/gamemodes/technomancer/spells/passwall.dm b/code/game/gamemodes/technomancer/spells/passwall.dm
index 4fcb5a206b8..ae42c71184f 100644
--- a/code/game/gamemodes/technomancer/spells/passwall.dm
+++ b/code/game/gamemodes/technomancer/spells/passwall.dm
@@ -17,6 +17,7 @@
var/busy = 0
/obj/item/spell/passwall/on_melee_cast(atom/hit_atom, mob/user)
+ . = ..()
if(busy) //Prevent someone from trying to get two uses of the spell from one instance.
return 0
if(!allowed_to_teleport())
diff --git a/code/game/gamemodes/technomancer/spells/phase_shift.dm b/code/game/gamemodes/technomancer/spells/phase_shift.dm
index 2afac756a7a..c05c02c4908 100644
--- a/code/game/gamemodes/technomancer/spells/phase_shift.dm
+++ b/code/game/gamemodes/technomancer/spells/phase_shift.dm
@@ -46,7 +46,7 @@
L.forceMove(get_turf(src))
L.visible_message(SPAN_WARNING("\The [src] ejects [L]!"))
-/obj/effect/phase_shift/relaymove(mob/user as mob)
+/obj/effect/phase_shift/relaymove(mob/user)
if(user.stat)
return
@@ -55,6 +55,7 @@
qdel(src)
/obj/item/spell/phase_shift/on_use_cast(mob/user)
+ . = ..()
if(isturf(user.loc)) //Check if we're not already in a rift.
if(pay_energy(2000))
var/obj/effect/phase_shift/PS = new(get_turf(user))
diff --git a/code/game/gamemodes/technomancer/spells/projectile/projectile.dm b/code/game/gamemodes/technomancer/spells/projectile/projectile.dm
index f953b1c538a..35758fe7a92 100644
--- a/code/game/gamemodes/technomancer/spells/projectile/projectile.dm
+++ b/code/game/gamemodes/technomancer/spells/projectile/projectile.dm
@@ -10,9 +10,11 @@
var/fire_sound = null
/obj/item/spell/projectile/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
on_ranged_cast(hit_atom, user, hit_atom)
/obj/item/spell/projectile/on_ranged_cast(atom/hit_atom, mob/living/user, atom/pb_target)
+ . = ..(hit_atom, user, TRUE)
if(set_up(hit_atom, user))
var/obj/item/projectile/new_projectile = make_projectile(spell_projectile, user)
new_projectile.old_style_target(hit_atom)
@@ -20,18 +22,28 @@
log_and_message_admins("has casted [src] at \the [hit_atom].")
if(fire_sound)
playsound(src, fire_sound, 75, 1)
- adjust_instability(instability_per_shot)
- return 1
- return 0
+ if(aspect != ASPECT_PSIONIC)
+ adjust_instability(instability_per_shot)
+ return TRUE
+ return FALSE
/obj/item/spell/projectile/proc/make_projectile(obj/item/projectile/projectile_type, mob/living/user)
var/obj/item/projectile/P = new projectile_type(get_turf(user))
- P.damage = calculate_spell_power(P.damage)
+ P.firer = user
+ if(aspect != ASPECT_PSIONIC)
+ P.damage = calculate_spell_power(P.damage)
+ else
+ if(owner.psi.get_rank() >= PSI_RANK_APEX)
+ if(P.damage)
+ P.damage *= 1.1
+ P.armor_penetration *= 1.1
+ P.damage_flags |= DAMAGE_FLAG_PSIONIC
return P
/obj/item/spell/projectile/proc/set_up(atom/hit_atom, mob/living/user)
if(spell_projectile)
- if(pay_energy(energy_cost_per_shot))
+ /// Psionic energy payment is handled at the beginning of on_ranged_cast.
+ if(pay_energy(aspect == ASPECT_PSIONIC ? psi_cost : energy_cost_per_shot))
if(pre_shot_delay)
var/image/target_image = image(icon = 'icons/obj/spells.dmi', loc = get_turf(hit_atom), icon_state = "target")
user << target_image
@@ -42,4 +54,4 @@
return TRUE
return FALSE // We got dropped before the firing occurred.
return TRUE // No delay, no need to check.
- return FALSE
\ No newline at end of file
+ return FALSE
diff --git a/code/game/gamemodes/technomancer/spells/raise_wall.dm b/code/game/gamemodes/technomancer/spells/raise_wall.dm
index 0e18ff062e0..c4a9f2b474f 100644
--- a/code/game/gamemodes/technomancer/spells/raise_wall.dm
+++ b/code/game/gamemodes/technomancer/spells/raise_wall.dm
@@ -16,6 +16,7 @@
aspect = ASPECT_FORCE
/obj/item/spell/raise_wall/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
var/turf/target_turf = isturf(hit_atom) ? hit_atom : get_turf(hit_atom)
if(target_turf.density)
return
diff --git a/code/game/gamemodes/technomancer/spells/resurrect.dm b/code/game/gamemodes/technomancer/spells/resurrect.dm
index 209b78145fc..1cb2c9ecb76 100644
--- a/code/game/gamemodes/technomancer/spells/resurrect.dm
+++ b/code/game/gamemodes/technomancer/spells/resurrect.dm
@@ -16,6 +16,7 @@
aspect = ASPECT_BIOMED
/obj/item/spell/resurrect/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
if(isliving(hit_atom))
var/mob/living/L = hit_atom
if(L == user)
diff --git a/code/game/gamemodes/technomancer/spells/shared_burden.dm b/code/game/gamemodes/technomancer/spells/shared_burden.dm
index 5fb94651610..f73ae5df0a7 100644
--- a/code/game/gamemodes/technomancer/spells/shared_burden.dm
+++ b/code/game/gamemodes/technomancer/spells/shared_burden.dm
@@ -15,6 +15,7 @@
aspect = ASPECT_UNSTABLE
/obj/item/spell/shared_burden/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
if(ishuman(hit_atom) && within_range(hit_atom))
var/mob/living/carbon/human/H = hit_atom
if(H == user)
@@ -27,4 +28,4 @@
var/instability_to_drain = min(H.instability, 25)
to_chat(user, "You draw instability away from \the [H] and towards you.")
adjust_instability(instability_to_drain)
- H.adjust_instability(-calculate_spell_power(instability_to_drain))
\ No newline at end of file
+ H.adjust_instability(-calculate_spell_power(instability_to_drain))
diff --git a/code/game/gamemodes/technomancer/spells/spawner/pulsar.dm b/code/game/gamemodes/technomancer/spells/spawner/pulsar.dm
index cb0837fa510..b2c428e420e 100644
--- a/code/game/gamemodes/technomancer/spells/spawner/pulsar.dm
+++ b/code/game/gamemodes/technomancer/spells/spawner/pulsar.dm
@@ -24,6 +24,7 @@
..()
/obj/item/spell/spawner/pulsar/on_throw_cast(atom/hit_atom, mob/user)
+ . = ..()
empulse(hit_atom, 1, 1, 1, 1, log=1)
// Does something every so often. Deletes itself when pulses_remaining hits zero.
@@ -40,7 +41,7 @@
/obj/effect/temporary_effect/pulse/proc/pulse_loop()
set waitfor = FALSE
-
+
while(pulses_remaining)
sleep(pulse_delay)
on_pulse()
diff --git a/code/game/gamemodes/technomancer/spells/spawner/spawner.dm b/code/game/gamemodes/technomancer/spells/spawner/spawner.dm
index 47c2a264ade..8eaf0fb2851 100644
--- a/code/game/gamemodes/technomancer/spells/spawner/spawner.dm
+++ b/code/game/gamemodes/technomancer/spells/spawner/spawner.dm
@@ -7,9 +7,10 @@
var/obj/effect/spawner_type = null
/obj/item/spell/spawner/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
var/turf/T = get_turf(hit_atom)
if(T)
new spawner_type(T)
to_chat(user, "You shift \the [src] onto \the [T].")
log_and_message_admins("has casted [src] at [T.x],[T.y],[T.z].")
- qdel(src)
\ No newline at end of file
+ qdel(src)
diff --git a/code/game/gamemodes/technomancer/spells/summon/summon.dm b/code/game/gamemodes/technomancer/spells/summon/summon.dm
index bd86a735a57..6caba308b56 100644
--- a/code/game/gamemodes/technomancer/spells/summon/summon.dm
+++ b/code/game/gamemodes/technomancer/spells/summon/summon.dm
@@ -12,6 +12,7 @@
var/instability_cost = 0
/obj/item/spell/summon/on_ranged_cast(atom/hit_atom, mob/living/user)
+ . = ..()
var/turf/T = get_turf(hit_atom)
if(summoned_mob_type && core.summoned_mobs.len < core.max_summons && within_range(hit_atom) && pay_energy(energy_cost))
var/obj/effect/E = new(T)
@@ -30,6 +31,7 @@
user.adjust_instability(instability_cost)
/obj/item/spell/summon/on_use_cast(mob/living/user)
+ . = ..()
if(summon_options.len)
var/choice = input(user, "Choose a creature to kidnap from somewhere!", "Summon") as null|anything in summon_options
if(choice)
@@ -37,4 +39,4 @@
// Called when a new mob is summoned, override for special behaviour.
/obj/item/spell/summon/proc/on_summon(var/mob/living/summoned)
- return
\ No newline at end of file
+ return
diff --git a/code/game/gamemodes/technomancer/spells/targeting_matrix.dm b/code/game/gamemodes/technomancer/spells/targeting_matrix.dm
index 4ead252ceb1..db39d54e577 100644
--- a/code/game/gamemodes/technomancer/spells/targeting_matrix.dm
+++ b/code/game/gamemodes/technomancer/spells/targeting_matrix.dm
@@ -15,6 +15,7 @@
aspect = ASPECT_FORCE //idk?
/obj/item/spell/targeting_matrix/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
var/turf/T = get_turf(hit_atom)
if(T)
var/mob/living/chosen_target = targeting_assist(T,5) //The person who's about to get attacked.
diff --git a/code/game/gamemodes/technomancer/spells/track.dm b/code/game/gamemodes/technomancer/spells/track.dm
index ac284da18bd..d84f1944431 100644
--- a/code/game/gamemodes/technomancer/spells/track.dm
+++ b/code/game/gamemodes/technomancer/spells/track.dm
@@ -27,6 +27,7 @@ var/list/technomancer_belongings = list()
return ..()
/obj/item/spell/track/on_use_cast(mob/user)
+ . = ..()
if(tracking)
tracking = 0
to_chat(user, "You stop tracking for \the [tracked]'s whereabouts.")
@@ -75,4 +76,4 @@ var/list/technomancer_belongings = list()
icon_state = "track_far"
spawn(5)
- .()
\ No newline at end of file
+ .()
diff --git a/code/game/gamemodes/technomancer/spells/warp_strike.dm b/code/game/gamemodes/technomancer/spells/warp_strike.dm
index a717a4b0a42..88436f4a627 100644
--- a/code/game/gamemodes/technomancer/spells/warp_strike.dm
+++ b/code/game/gamemodes/technomancer/spells/warp_strike.dm
@@ -14,6 +14,7 @@
aspect = ASPECT_TELE
/obj/item/spell/warp_strike/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
var/turf/T = get_turf(hit_atom)
if(T)
if(!within_range(T))
diff --git a/code/game/objects/effects/effect_system.dm b/code/game/objects/effects/effect_system.dm
index c337fe24176..cd17484cea7 100644
--- a/code/game/objects/effects/effect_system.dm
+++ b/code/game/objects/effects/effect_system.dm
@@ -6,7 +6,7 @@ would spawn and follow the beaker, even if it is carried or thrown.
*/
-/obj/effect/effect
+/obj/effect
name = "effect"
icon = 'icons/effects/effects.dmi'
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
diff --git a/code/game/objects/items/weapons/cigs_lighters.dm b/code/game/objects/items/weapons/cigs_lighters.dm
index 1b47579cd18..32f703e74cb 100644
--- a/code/game/objects/items/weapons/cigs_lighters.dm
+++ b/code/game/objects/items/weapons/cigs_lighters.dm
@@ -105,7 +105,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM
/obj/item/flame/match/proc/light()
lit = TRUE
- damtype = "burn"
+ damtype = "fire"
icon_state = "match_lit"
item_state = "match_lit"
if(ismob(loc))
diff --git a/code/game/objects/structures/full_window_frame.dm b/code/game/objects/structures/full_window_frame.dm
index 38446ceb9f8..c460b87fe5f 100644
--- a/code/game/objects/structures/full_window_frame.dm
+++ b/code/game/objects/structures/full_window_frame.dm
@@ -163,6 +163,12 @@
else
to_chat(user, SPAN_WARNING("You need at least [glass_needed] sheets of [MATERIAL_GLASS_REINFORCED_PHORON] to finished the window."))
+/obj/structure/window_frame/hitby(atom/movable/AM, speed)
+ . = ..()
+ var/obj/structure/window/W = locate() in get_turf(src)
+ if(istype(W))
+ W.hitby(AM)
+
/obj/structure/window_frame/unanchored // Used during in-game construction.
should_check_mapload = FALSE // No glass.
anchored = FALSE
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index abe27409bb8..9a13d40176a 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -106,18 +106,16 @@ var/global/enabled_spooking = 0
var/mob/living/psyker = M
if(psyker.psi)
body += "Remove psionics.
"
- body += "Trigger latencies.
"
body += "
"
- for(var/faculty in list(PSI_COERCION, PSI_PSYCHOKINESIS, PSI_REDACTION, PSI_ENERGISTICS))
- var/datum/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty)
- var/faculty_rank = psyker.psi ? psyker.psi.get_rank(faculty) : 0
- body += "| [faculty_decl.name] | "
- for(var/i = 1 to LAZYLEN(psychic_ranks_to_strings))
- var/psi_title = psychic_ranks_to_strings[i]
- if(i == faculty_rank)
- psi_title = "[psi_title]"
- body += "[psi_title] | "
- body += "
"
+ for(var/psi_rank in list(PSI_RANK_SENSITIVE, PSI_RANK_HARMONIOUS, PSI_RANK_APEX, PSI_RANK_LIMITLESS))
+ var/owner_rank = psyker.psi ? psyker.psi.get_rank() : 0
+ var/psi_title = psychic_ranks_to_strings[psi_rank]
+ if(psi_rank == owner_rank)
+ psi_title = "[psi_title]"
+ if(psi_rank != PSI_RANK_LIMITLESS)
+ body += "[psi_title]
"
+ else
+ body += "[psi_title]
"
body += "
"
if (M.client)
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 8516f945ae0..b80f43e5a0a 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -432,8 +432,8 @@
else if(href_list["trigger_psi_latencies"])
var/datum/psi_complexus/psi = locate(href_list["trigger_psi_latencies"])
- log_and_message_admins("triggered psi latencies for [key_name(psi.owner)].")
- psi.check_latency_trigger(100, "outside intervention", redactive = TRUE)
+ log_and_message_admins("triggered psionics for [key_name(psi.owner)].")
+ psi.check_psionic_trigger(100, "outside intervention", redactive = TRUE)
else if(href_list["monkeyone"])
if(!check_rights(R_SPAWN)) return
diff --git a/code/modules/admin/verbs/buildmode.dm b/code/modules/admin/verbs/buildmode.dm
index 78fe6034eb9..a0015ebc3e0 100644
--- a/code/modules/admin/verbs/buildmode.dm
+++ b/code/modules/admin/verbs/buildmode.dm
@@ -42,10 +42,11 @@
/obj/effect/bmode//Cleaning up the tree a bit
density = 1
anchored = 1
- layer = SCREEN_LAYER
+ layer = SCREEN_LAYER + 1
dir = NORTH
icon = 'icons/misc/buildmode.dmi'
- var/obj/effect/bmode/buildholder/master = null
+ mouse_opacity = MOUSE_OPACITY_OPAQUE
+ var/obj/effect/bmode/buildholder/master
/obj/effect/bmode/Destroy()
if(master && master.cl)
diff --git a/code/modules/client/preference_setup/general/07_psionics.dm b/code/modules/client/preference_setup/general/07_psionics.dm
new file mode 100644
index 00000000000..161a8f0cc61
--- /dev/null
+++ b/code/modules/client/preference_setup/general/07_psionics.dm
@@ -0,0 +1,135 @@
+/datum/category_item/player_setup_item/general/psionics
+ name = "Psionics"
+ sort_order = 7
+
+/datum/category_item/player_setup_item/general/psionics/load_character(var/savefile/S)
+ S["psionics"] >> pref.psionics
+
+/datum/category_item/player_setup_item/general/psionics/save_character(var/savefile/S)
+ var/list/psionics = pref.psionics
+ for(var/psi in psionics)
+ var/singleton/psionic_power/P = GET_SINGLETON(text2path(psi))
+ if(!istype(P))
+ continue
+ psionics |= P.type
+ S["psionics"] << json_encode(psionics)
+
+/datum/category_item/player_setup_item/general/psionics/gather_load_query()
+ return list(
+ "ss13_characters" = list(
+ "vars" = list(
+ "psionics" = "psionics",
+ ),
+ "args" = list("id")
+ )
+ )
+
+/datum/category_item/player_setup_item/general/psionics/gather_load_parameters()
+ return list("id" = pref.current_character)
+
+/datum/category_item/player_setup_item/general/psionics/gather_save_query()
+ return list(
+ "ss13_characters" = list(
+ "psionics",
+ "id" = 1
+ )
+ )
+
+/datum/category_item/player_setup_item/general/psionics/gather_save_parameters()
+ var/list/sanitized_psionics = list()
+ for(var/S in pref.psionics)
+ var/singleton/psionic_power/P = GET_SINGLETON(text2path(S))
+ if(!istype(P))
+ continue
+ sanitized_psionics |= "[P.type]"
+ return list(
+ "psionics" = json_encode(sanitized_psionics),
+ "char_id" = pref.current_character,
+ "ckey" = PREF_CLIENT_CKEY
+ )
+
+/datum/category_item/player_setup_item/general/psionics/load_special(savefile/S)
+ var/before = pref.psionics
+ try
+ pref.psionics = json_decode(pref.psionics)
+ catch (var/exception/e)
+ log_debug("PSIONICS: Caught [e]. Initial value: [before]")
+ pref.psionics = list()
+
+/datum/category_item/player_setup_item/general/psionics/sanitize_character(var/sql_load = 0)
+ var/datum/species/mob_species = all_species[pref.species]
+ if(length(pref.psionics) && !mob_species.has_psionics)
+ to_chat(pref.client, SPAN_WARNING("This species does not have psionics! Resetting..."))
+ pref.psionics = list()
+ return
+ var/list/bought_psionic_powers = list()
+ for(var/S in pref.psionics)
+ var/singleton/psionic_power/P = GET_SINGLETON(text2path(S))
+ if(istype(P))
+ if(!(P.ability_flags & PSI_FLAG_CANON))
+ pref.psionics -= S
+ to_chat(pref.client, SPAN_WARNING("[P.name] is an invalid psionic!"))
+ continue
+ bought_psionic_powers |= P
+ else
+ to_chat(pref.client, SPAN_WARNING("[S] is an invalid psionic!"))
+ pref.psionics -= S
+ continue
+ if(length(bought_psionic_powers) > mob_species.character_creation_psi_points)
+ to_chat(pref.client, SPAN_WARNING("You have more psionics than possible! Resetting..."))
+ pref.psionics = list()
+
+/datum/category_item/player_setup_item/general/psionics/content(var/mob/user)
+ var/datum/species/mob_species = all_species[pref.species]
+ if(!(mob_species.has_psionics))
+ return
+ var/list/bought_psionic_powers = list()
+ for(var/S in pref.psionics)
+ var/singleton/psionic_power/P = GET_SINGLETON(text2path(S))
+ if(istype(P))
+ bought_psionic_powers |= P
+
+ var/list/dat = list(
+ "Psionics:
"
+ )
+ for(var/singleton/psionic_power/P in bought_psionic_powers)
+ dat += "- [P.name] -
"
+ dat += "Add Psionic Power
"
+ . = dat.Join()
+
+/datum/category_item/player_setup_item/general/psionics/OnTopic(var/href,var/list/href_list, var/mob/user)
+ if(href_list["remove_psi_power"])
+ var/power_to_remove = href_list["remove_psi_power"]
+ if(power_to_remove && (power_to_remove in pref.psionics))
+ pref.psionics -= power_to_remove
+ return TOPIC_REFRESH
+
+ else if(href_list["add_psi_power"])
+ var/datum/species/mob_species = all_species[pref.species]
+ var/total_psi_points = mob_species.character_creation_psi_points
+ var/list/available_psionics = list()
+ var/list/psionic_map = list()
+ var/list/bought_psionic_powers = list()
+
+ for(var/S in pref.psionics)
+ var/singleton/psionic_power/P = GET_SINGLETON(text2path(S))
+ if(istype(P))
+ bought_psionic_powers |= P
+ total_psi_points = max(0, total_psi_points - P.point_cost)
+
+ for(var/singleton/psionic_power/P in GET_SINGLETON_SUBTYPE_LIST(/singleton/psionic_power))
+ if((P.ability_flags & PSI_FLAG_CANON) && (P.point_cost <= total_psi_points) && !(P in bought_psionic_powers))
+ available_psionics |= "[P.name]"
+ psionic_map[P.name] = P.type
+
+ if(!length(available_psionics))
+ to_chat(user, SPAN_WARNING("You ran out of points!"))
+ return
+
+ var/new_power = input(user, "Choose a psionic power to add.", "Psionics") as null|anything in available_psionics
+ if(new_power)
+ var/singleton/psionic_power/P = GET_SINGLETON(psionic_map[new_power])
+ if(istype(P))
+ pref.psionics += "[P.type]"
+ return TOPIC_REFRESH
+
diff --git a/code/modules/client/preference_setup/loadout/loadout_augments.dm b/code/modules/client/preference_setup/loadout/loadout_augments.dm
index 42c5c368fe5..0a0e8754d66 100644
--- a/code/modules/client/preference_setup/loadout/loadout_augments.dm
+++ b/code/modules/client/preference_setup/loadout/loadout_augments.dm
@@ -144,8 +144,7 @@
display_name = "psionic receiver"
description = "An augment installed into the head that functions as a surrogate for a missing zona bovinae, also functioning as a filter for the psionically-challenged."
path = /obj/item/organ/internal/augment/psi
- whitelisted = list(SPECIES_HUMAN, SPECIES_HUMAN_OFFWORLD, SPECIES_TAJARA, SPECIES_TAJARA_ZHAN, SPECIES_TAJARA_MSAI, SPECIES_VAURCA_WORKER, SPECIES_VAURCA_WARRIOR, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER, SPECIES_IPC, SPECIES_IPC_G1, SPECIES_IPC_G2, SPECIES_IPC_XION, SPECIES_IPC_ZENGHU, SPECIES_IPC_BISHOP, SPECIES_IPC_SHELL)
-
+ whitelisted = list(SPECIES_HUMAN, SPECIES_HUMAN_OFFWORLD, SPECIES_TAJARA, SPECIES_TAJARA_ZHAN, SPECIES_TAJARA_MSAI, SPECIES_UNATHI, SPECIES_VAURCA_WORKER, SPECIES_VAURCA_WARRIOR, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER, SPECIES_IPC, SPECIES_IPC_G1, SPECIES_IPC_G2, SPECIES_IPC_XION, SPECIES_IPC_ZENGHU, SPECIES_IPC_BISHOP, SPECIES_IPC_SHELL)
/datum/gear/augment/memory_inhibitor
display_name = "memory inhibitor"
description = "A Zeng Hu implant that allows one to have control over their memories, allowing you to set a timer and remove any memories developed within it."
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index dcfcaa18d2b..78b875f81c8 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -99,6 +99,8 @@ var/list/preferences_datums = list()
var/culture
var/origin
+ var/list/psionics = list()
+
var/list/char_render_holders //Should only be a key-value list of north/south/east/west = obj/screen.
var/static/list/preview_screen_locs = list(
"1" = "character_preview_map:1,5:-12",
@@ -471,6 +473,12 @@ var/list/preferences_datums = list()
character.headset_choice = headset_choice
+ if(length(psionics) && character.client)
+ for(var/power in psionics)
+ var/singleton/psionic_power/P = GET_SINGLETON(text2path(power))
+ if(istype(P) && (P.ability_flags & PSI_FLAG_CANON))
+ P.apply(character)
+
if(icon_updates)
character.force_update_limbs()
character.update_mutations(0)
@@ -638,6 +646,7 @@ var/list/preferences_datums = list()
ccia_actions = list()
disabilities = list()
+ psionics = list()
economic_status = ECONOMICALLY_AVERAGE
diff --git a/code/modules/mob/living/carbon/breathe.dm b/code/modules/mob/living/carbon/breathe.dm
index e47e9ca2a9d..13362a331f4 100644
--- a/code/modules/mob/living/carbon/breathe.dm
+++ b/code/modules/mob/living/carbon/breathe.dm
@@ -23,12 +23,9 @@
/mob/living/carbon/proc/breathe(var/volume_needed = BREATH_VOLUME)
if(species && (species.flags & NO_BREATHE))
return
- var/datum/changeling/changeling = get_antag_datum(MODE_CHANGELING)
- if(changeling?.space_adapted)
+ if(HAS_TRAIT(src, TRAIT_PRESSURE_IMMUNITY))
return
- if(changeling?.no_breathing)
- return
-
+
volume_needed *= (species?.breath_vol_mul || 1)
var/datum/gas_mixture/breath = null
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index b7034b5bf72..96f8d5d100b 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -1485,6 +1485,9 @@
species.set_default_tail(src)
+ if(species.psi_deaf || HAS_FLAG(species.flags, IS_MECHANICAL) || HAS_FLAG(species.flags, NO_SCAN))
+ ADD_TRAIT(src, TRAIT_PSIONICALLY_DEAF, INNATE_TRAIT)
+
if(client)
client.init_verbs()
diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm
index 6460ce4fbb8..e2e6b129fc9 100644
--- a/code/modules/mob/living/carbon/human/human_helpers.dm
+++ b/code/modules/mob/living/carbon/human/human_helpers.dm
@@ -185,7 +185,7 @@
return O
-/mob/living/carbon/human/proc/awaken_psi_basic(var/source, var/allow_latency = TRUE)
+/mob/living/carbon/human/proc/awaken_psi_basic(var/source)
var/static/list/psi_operancy_messages = list(
"There's something in your skull!",
"Something is eating your thoughts!",
@@ -197,13 +197,10 @@
)
to_chat(src, SPAN_DANGER("An indescribable, brain-tearing sound hisses from [source], and you collapse in a seizure!"))
seizure()
- var/new_latencies = rand(2,4)
- var/list/faculties = list(PSI_COERCION, PSI_REDACTION, PSI_ENERGISTICS, PSI_PSYCHOKINESIS)
- for(var/i = 1 to new_latencies)
- custom_pain(SPAN_DANGER("[pick(psi_operancy_messages)]"), 25)
- set_psi_rank(pick_n_take(faculties), allow_latency ? PSI_RANK_LATENT : PSI_RANK_OPERANT) // if set to latent, it spikes anywhere from OPERANT to PARAMOUNT
- sleep(30)
- addtimer(CALLBACK(psi, TYPE_PROC_REF(/datum/psi_complexus, check_latency_trigger), 100, source, TRUE), 4.5 SECONDS)
+ custom_pain(SPAN_DANGER(FONT_LARGE("[pick(psi_operancy_messages)]")), 25)
+ set_psi_rank(PSI_RANK_HARMONIOUS)
+ sleep(30)
+ addtimer(CALLBACK(psi, TYPE_PROC_REF(/datum/psi_complexus, check_psionic_trigger), 100, source, TRUE), 4.5 SECONDS)
/mob/living/carbon/human/get_resist_power()
return species.resist_mod
@@ -349,8 +346,7 @@
/mob/living/carbon/human/proc/pressure_resistant()
if(HAS_FLAG(mutations, COLD_RESISTANCE))
return TRUE
- var/datum/changeling/changeling = get_antag_datum(MODE_CHANGELING)
- if(changeling?.space_adapted)
+ if(HAS_TRAIT(src, TRAIT_PRESSURE_IMMUNITY))
return TRUE
return FALSE
@@ -418,7 +414,7 @@
/mob/living/carbon/human/proc/get_tail_accessory()
var/obj/item/organ/external/groin/G = organs_by_name[BP_GROIN]
- if(!G)
+ if(!istype(G))
return
if(!G.tail_storage)
return
diff --git a/code/modules/mob/living/carbon/human/human_powers.dm b/code/modules/mob/living/carbon/human/human_powers.dm
index 7f2f292ed66..b8578bec788 100644
--- a/code/modules/mob/living/carbon/human/human_powers.dm
+++ b/code/modules/mob/living/carbon/human/human_powers.dm
@@ -239,14 +239,16 @@
// Simple mobs cannot use Skrellepathy
-/mob/proc/can_commune()
+/mob/proc/has_psionics()
return FALSE
-/mob/living/carbon/human/can_commune()
+/mob/living/carbon/human/has_psionics()
+ if(HAS_TRAIT(src, TRAIT_PSIONICALLY_DEAF))
+ return FALSE
if(psi)
return TRUE
else
- return species ? species.can_commune() : FALSE
+ return species ? species.has_psionics() : FALSE
/mob/living/carbon/human/proc/commune()
set category = "Abilities"
@@ -322,14 +324,14 @@
to_chat(M,"[src] telepathically says to [target]: [text]")
var/mob/living/carbon/human/H = target
- if (target.can_commune())
+ if (target.has_psionics())
to_chat(H,"You instinctively sense [src] sending their thoughts into your mind, hearing: [text]")
else if(prob(25) && (target.mind && target.mind.assigned_role=="Chaplain"))
to_chat(H,"You sense [src]'s thoughts enter your mind, whispering quietly: [text]")
else
to_chat(H,"You feel pressure behind your eyes as alien thoughts enter your mind: [text]")
if(istype(H))
- if (target.can_commune())
+ if (target.has_psionics())
return
if(prob(10) && !(H.species.flags & NO_BLOOD))
to_chat(H,"Your nose begins to bleed...")
diff --git a/code/modules/mob/living/carbon/human/species/species.dm b/code/modules/mob/living/carbon/human/species/species.dm
index e5337a801f2..300bce9f707 100644
--- a/code/modules/mob/living/carbon/human/species/species.dm
+++ b/code/modules/mob/living/carbon/human/species/species.dm
@@ -295,6 +295,13 @@
var/use_alt_hair_layer = FALSE
+ /// Species psionics. FALSE for no psionics. Otherwise, set to the PSI_RANK define you want.
+ var/has_psionics = FALSE
+ /// Number of psi points in character creation.
+ var/character_creation_psi_points = 0
+ /// Is this species psionically deaf?
+ var/psi_deaf = FALSE
+
/datum/species/proc/get_eyes(var/mob/living/carbon/human/H)
return
@@ -470,6 +477,8 @@
if(!H.client || !H.client.prefs || !H.client.prefs.gender)
H.gender = pick(default_genders)
H.pronouns = H.gender
+ if(has_psionics)
+ H.set_psi_rank(has_psionics)
/datum/species/proc/handle_death(var/mob/living/carbon/human/H, var/gibbed = 0) //Handles any species-specific death events (such as dionaea nymph spawns).
return
@@ -739,11 +748,8 @@
/datum/species/proc/get_digestion_product()
return /singleton/reagent/nutriment
-/datum/species/proc/can_commune()
- return FALSE
-
-/datum/species/proc/has_psi_potential()
- return TRUE
+/datum/species/proc/has_psionics()
+ return has_psionics
/datum/species/proc/handle_despawn()
return
diff --git a/code/modules/mob/living/carbon/human/species/station/diona/diona.dm b/code/modules/mob/living/carbon/human/species/station/diona/diona.dm
index 742abdd0d1a..7060dab22ce 100644
--- a/code/modules/mob/living/carbon/human/species/station/diona/diona.dm
+++ b/code/modules/mob/living/carbon/human/species/station/diona/diona.dm
@@ -129,6 +129,7 @@
)
alterable_internal_organs = list()
+ psi_deaf = TRUE
/datum/species/diona/can_understand(var/mob/other)
var/mob/living/carbon/alien/diona/D = other
@@ -184,9 +185,6 @@
if(SB)
SB.handle_item_insertion(new /obj/item/device/flashlight/survival(get_turf(H)), TRUE)
-/datum/species/diona/has_psi_potential()
- return FALSE
-
/datum/species/diona/is_naturally_insulated()
return TRUE
diff --git a/code/modules/mob/living/carbon/human/species/station/golem.dm b/code/modules/mob/living/carbon/human/species/station/golem.dm
index 02461d5f097..3e71859230f 100644
--- a/code/modules/mob/living/carbon/human/species/station/golem.dm
+++ b/code/modules/mob/living/carbon/human/species/station/golem.dm
@@ -98,6 +98,8 @@ var/global/list/golem_types = list(SPECIES_GOLEM_COAL,
hud_type = /datum/hud_data/construct
+ psi_deaf = TRUE
+
var/turn_into_materials = TRUE //the golem will turn into materials upon its death
var/golem_designation = "Coal" //used in the creation of the name
@@ -124,9 +126,6 @@ var/global/list/golem_types = list(SPECIES_GOLEM_COAL,
return TRUE
return FALSE
-/datum/species/golem/has_psi_potential()
- return FALSE
-
/datum/species/golem/iron
name = SPECIES_GOLEM_IRON
name_plural = "iron golems"
diff --git a/code/modules/mob/living/carbon/human/species/station/ipc/ipc.dm b/code/modules/mob/living/carbon/human/species/station/ipc/ipc.dm
index 4ba63ef5beb..405f16c0bed 100644
--- a/code/modules/mob/living/carbon/human/species/station/ipc/ipc.dm
+++ b/code/modules/mob/living/carbon/human/species/station/ipc/ipc.dm
@@ -143,6 +143,7 @@
var/move_charge_factor = 1
use_alt_hair_layer = TRUE
+ psi_deaf = TRUE
/datum/species/machine/handle_post_spawn(var/mob/living/carbon/human/H)
. = ..()
@@ -373,9 +374,6 @@
. = ..()
check_tag(H, H.client)
-/datum/species/machine/has_psi_potential()
- return FALSE
-
/datum/species/machine/handle_death_check(var/mob/living/carbon/human/H)
if(H.get_total_health() <= config.health_threshold_dead)
return TRUE
diff --git a/code/modules/mob/living/carbon/human/species/station/skrell/skrell.dm b/code/modules/mob/living/carbon/human/species/station/skrell/skrell.dm
index 6c0ef461c5e..5a7222d60e9 100644
--- a/code/modules/mob/living/carbon/human/species/station/skrell/skrell.dm
+++ b/code/modules/mob/living/carbon/human/species/station/skrell/skrell.dm
@@ -107,6 +107,9 @@
alterable_internal_organs = list(BP_HEART, BP_EYES, BP_LUNGS, BP_LIVER, BP_KIDNEYS, BP_STOMACH)
+ has_psionics = PSI_RANK_SENSITIVE
+ character_creation_psi_points = 4
+
/datum/species/skrell/handle_trail(var/mob/living/carbon/human/H, var/turf/T)
var/list/trail_info = ..()
if(!length(trail_info) && !H.shoes)
@@ -117,10 +120,6 @@
return trail_info
-/datum/species/skrell/handle_post_spawn(mob/living/carbon/human/H)
- ..()
- H.set_psi_rank(PSI_COERCION, PSI_RANK_OPERANT)
-
/datum/species/skrell/handle_strip(var/mob/user, var/mob/living/carbon/human/H, var/action)
switch(action)
if("headtail")
@@ -142,6 +141,3 @@
/datum/species/skrell/can_breathe_water()
return TRUE
-
-/datum/species/skrell/can_commune()
- return TRUE
diff --git a/code/modules/mob/living/carbon/human/species/station/vaurca/vaurca.dm b/code/modules/mob/living/carbon/human/species/station/vaurca/vaurca.dm
index 48bfae9bc7b..4c3a815a5a1 100644
--- a/code/modules/mob/living/carbon/human/species/station/vaurca/vaurca.dm
+++ b/code/modules/mob/living/carbon/human/species/station/vaurca/vaurca.dm
@@ -141,6 +141,7 @@
alterable_internal_organs = list(BP_HEART, BP_EYES, BP_LUNGS, BP_STOMACH, BP_APPENDIX)
+ psi_deaf = TRUE
/datum/species/bug/before_equip(var/mob/living/carbon/human/H)
. = ..()
@@ -158,9 +159,6 @@
H.gender = NEUTER
return ..()
-/datum/species/bug/has_psi_potential()
- return FALSE
-
/datum/species/bug/is_naturally_insulated()
return TRUE
@@ -168,3 +166,4 @@
if(I.w_class <= ITEMSIZE_SMALL)
return TRUE
return FALSE
+
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 535cf791c7e..bcb76210d1d 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -18,6 +18,10 @@
var/natural_armor = GetComponent(/datum/component/armor)
if(natural_armor)
. += natural_armor
+ if(psi)
+ var/armor = psi.GetComponent(/datum/component/armor/psionic)
+ if(armor)
+ . += armor
/mob/living/bullet_act(var/obj/item/projectile/P, var/def_zone, var/used_weapon = null)
@@ -247,7 +251,7 @@
// End BS12 momentum-transfer code.
-/mob/living/attack_generic(var/mob/user, var/damage, var/attack_message, var/armor_penetration, var/attack_flags)
+/mob/living/attack_generic(var/mob/user, var/damage, var/attack_message, var/armor_penetration, var/attack_flags, var/damage_type = DAMAGE_BRUTE)
if(!damage)
return
@@ -257,7 +261,7 @@
src.visible_message("[user] has [attack_message] [src]!")
user.do_attack_animation(src)
- apply_damage(damage, DAMAGE_BRUTE, user.zone_sel?.selecting, armor_pen = armor_penetration, damage_flags = attack_flags)
+ apply_damage(damage, damage_type, user.zone_sel?.selecting, armor_pen = armor_penetration, damage_flags = attack_flags)
updatehealth()
return TRUE
diff --git a/code/modules/mob/living/simple_animal/borer/borer_powers.dm b/code/modules/mob/living/simple_animal/borer/borer_powers.dm
index 128b46d1018..d435234cc0a 100644
--- a/code/modules/mob/living/simple_animal/borer/borer_powers.dm
+++ b/code/modules/mob/living/simple_animal/borer/borer_powers.dm
@@ -453,8 +453,8 @@
if(chemicals < 150)
to_chat(src, SPAN_WARNING("You don't have enough chemicals!"))
return
- if(!host.species.has_psi_potential())
- to_chat(src, SPAN_WARNING("\The [host] lacks a zona bovinidae to psionically enlighten! How disturbing..."))
+ if(host.species.psi_deaf)
+ to_chat(src, SPAN_WARNING("\The [host] lacks a zona bovinae to psionically enlighten! How disturbing..."))
if(host.psi)
to_chat(src, SPAN_WARNING("Your host is already psionically active!"))
return
@@ -481,13 +481,14 @@
return
to_chat(src, SPAN_NOTICE("You succeed in interfacing with the host's zona bovinae, this will be a painful process for them."))
- host.awaken_psi_basic("something in your head", FALSE)
+ host.awaken_psi_basic("something in your head")
+ host.psi.psi_points = 3 /// You don't get a lot at the start.
host.add_language(LANGUAGE_TCB) // if we don't have TCB, give them TCB | this allows monkey borers to RP
-/mob/living/simple_animal/borer/verb/advance_faculty()
+/mob/living/simple_animal/borer/verb/advance_psionics()
set category = "Abilities"
- set name = "Advance Psionic Faculty (75)"
- set desc = "Advance one of your host's psionic faculties by one step."
+ set name = "Advance Psionics (75)"
+ set desc = "Advance your host's psionic capability by one step."
if(!host)
to_chat(src, SPAN_NOTICE("You are not inside a host body."))
@@ -507,18 +508,8 @@
to_chat(src, SPAN_WARNING("\The [host]'s mind is shielded against your powers."))
return
- var/list/faculties = list(capitalize(PSI_COERCION), capitalize(PSI_REDACTION), capitalize(PSI_ENERGISTICS), capitalize(PSI_PSYCHOKINESIS))
- var/selected_faculty = input(src, "Choose a faculty to upgrade.") as null|anything in faculties
- if(!selected_faculty)
- return
- selected_faculty = lowertext(selected_faculty)
- var/max_rank = islesserform(host) ? PSI_RANK_OPERANT : PSI_RANK_MASTER
- if(host.psi.get_rank(selected_faculty) >= max_rank)
- to_chat(src, SPAN_NOTICE("This faculty has already been pushed to the max potential you can achieve!"))
- return
-
- var/faculty_time = 10 SECONDS
- if(!start_ability(host, faculty_time))
+ var/upgrade_time = 10 SECONDS
+ if(!start_ability(host, upgrade_time))
to_chat(src, SPAN_WARNING("You're busy doing something else, complete that task first."))
return
@@ -526,19 +517,16 @@
to_chat(src, SPAN_NOTICE("You probe your tendrils deep within your host's zona bovinae, seeking to upgrade their abilities."))
to_chat(host, SPAN_WARNING("You feel a burning, tingling sensation at the back of your head..."))
- addtimer(CALLBACK(src, PROC_REF(faculty_upgrade), selected_faculty), faculty_time)
+ addtimer(CALLBACK(src, PROC_REF(upgrade_rank)), upgrade_time)
-/mob/living/simple_animal/borer/proc/faculty_upgrade(var/selected_faculty)
+/mob/living/simple_animal/borer/proc/upgrade_rank()
if(!host)
return
- var/host_psi_rank = host.psi.get_rank(selected_faculty)
- var/next_rank = host_psi_rank > PSI_RANK_BLUNT ? host_psi_rank + 1 : PSI_RANK_OPERANT
- host.psi.set_rank(selected_faculty, next_rank)
- host.psi.update(TRUE)
- to_chat(src, SPAN_NOTICE("You successfully manage to upgrade your host to [psychic_ranks_to_strings[host.psi.ranks[selected_faculty]]] [selected_faculty]."))
+ host.psi.psi_points++
+ to_chat(src, SPAN_NOTICE("You successfully manage to expand your living host's control over their Zona Bovinae. They know have an extra psionic point to spend."))
to_chat(host, SPAN_GOOD("A breeze of fresh air washes over your mind, you feel powerful!"))
- to_chat(host, SPAN_NOTICE("You have been psionically enlightened. You are now a [psychic_ranks_to_strings[host.psi.ranks[selected_faculty]]] in the [selected_faculty] faculty."))
+ to_chat(host, SPAN_NOTICE("Your control over the Nlom has expanded. You now have an extra psionic point to spend in the Point Shop."))
/mob/living/simple_animal/borer/verb/host_health_scan()
set category = "Abilities"
diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm
index 5bd4571af3b..0f348947355 100644
--- a/code/modules/mob/living/simple_animal/hostile/hostile.dm
+++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm
@@ -214,7 +214,7 @@
var/atom/target
if(isliving(target_mob))
var/mob/living/L = target_mob
- on_attack_mob(L, L.attack_generic(src, rand(melee_damage_lower, melee_damage_upper), attacktext, armor_penetration, attack_flags))
+ on_attack_mob(L, L.attack_generic(src, rand(melee_damage_lower, melee_damage_upper), attacktext, armor_penetration, attack_flags, damage_type))
target = L
else if(istype(target_mob, /obj/machinery/bot))
var/obj/machinery/bot/B = target_mob
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 34ef0df031f..98eea889cdf 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -14,6 +14,9 @@
mob_swap_flags = MONKEY|SLIME|SIMPLE_ANIMAL
mob_push_flags = MONKEY|SLIME|SIMPLE_ANIMAL
+ /// The type of damage this mob deals.
+ var/damage_type = DAMAGE_BRUTE
+
var/show_stat_health = 1 //does the percentage health show in the stat panel for the mob
var/icon_living = ""
@@ -538,7 +541,7 @@
simple_harm_attack(user)
/mob/living/simple_animal/proc/simple_harm_attack(var/mob/living/user)
- apply_damage(harm_intent_damage, DAMAGE_BRUTE, used_weapon = "Attack by [user.name]")
+ apply_damage(harm_intent_damage, damage_type, used_weapon = "Attack by [user.name]")
user.visible_message(SPAN_WARNING("\The [user] [response_harm] \the [src]!"), SPAN_WARNING("You [response_harm] \the [src]!"))
user.do_attack_animation(src, FIST_ATTACK_ANIMATION)
poke(TRUE)
diff --git a/code/modules/mob/mob_grab.dm b/code/modules/mob/mob_grab.dm
index f0d62fd2f22..13f828fd2de 100644
--- a/code/modules/mob/mob_grab.dm
+++ b/code/modules/mob/mob_grab.dm
@@ -348,11 +348,6 @@
if(!affecting)
return
- if(ishuman(user) && affecting == M)
- var/mob/living/carbon/human/H = user
- if(H.check_psi_grab(src))
- return
-
if(world.time < (last_action + 20))
return
diff --git a/code/modules/organs/organ_external.dm b/code/modules/organs/organ_external.dm
index 0fc4102eac7..8c338a4d067 100644
--- a/code/modules/organs/organ_external.dm
+++ b/code/modules/organs/organ_external.dm
@@ -284,8 +284,26 @@
var/laser = (damage_flags & DAMAGE_FLAG_LASER)
var/sharp = (damage_flags & DAMAGE_FLAG_SHARP)
var/edge = (damage_flags & DAMAGE_FLAG_EDGE)
+ var/psionic = HAS_FLAG(damage_flags, DAMAGE_FLAG_PSIONIC)
var/blunt = !!(brute && !sharp && !edge)
+ /// Psionics and psionically deaf species take varying amounts of damage from psionic abilities.
+ if(psionic)
+ if(!owner.has_psionics())
+ brute *= 0.9
+ burn *= 0.9
+ else if(owner.psi)
+ var/psi_rank = owner.psi.get_rank()
+ if(psi_rank == PSI_RANK_SENSITIVE)
+ brute *= 1.05
+ burn *= 1.05
+ else if(psi_rank == PSI_RANK_HARMONIOUS)
+ brute *= 1.1
+ burn *= 1.1
+ else if(psi_rank == PSI_RANK_APEX)
+ brute *= 1.2
+ burn *= 1.2
+
if(status & ORGAN_BROKEN && prob(40) && brute)
if(owner && (owner.can_feel_pain()) && owner.stat == CONSCIOUS)
owner.emote(pick("scream", "groan")) //getting hit on broken hand hurts
diff --git a/code/modules/organs/subtypes/augment.dm b/code/modules/organs/subtypes/augment.dm
index 686bf169609..a1db016ee69 100644
--- a/code/modules/organs/subtypes/augment.dm
+++ b/code/modules/organs/subtypes/augment.dm
@@ -529,7 +529,6 @@
organ_tag = BP_AUG_COCHLEAR
parent_organ = BP_HEAD
-// Snakebitten!
/obj/item/organ/internal/augment/psi
name = "psionic receiver"
desc = "An augment installed into the head that functions as a surrogate for a missing zona bovinae, also functioning as a filter for the psionically-challenged."
diff --git a/code/modules/projectiles/pins.dm b/code/modules/projectiles/pins.dm
index 1201ae24b2e..0124d3af927 100644
--- a/code/modules/projectiles/pins.dm
+++ b/code/modules/projectiles/pins.dm
@@ -112,7 +112,7 @@ Pins Below.
fail_message = "PSIONICS CHECK FAILED."
/obj/item/device/firing_pin/psionic/pin_auth(mob/living/user)
- if(user.can_commune())
+ if(user.has_psionics())
return 1
else
return 0
diff --git a/code/modules/projectiles/projectile/beams.dm b/code/modules/projectiles/projectile/beams.dm
index fe219b3dbd1..cd9e02e9f93 100644
--- a/code/modules/projectiles/projectile/beams.dm
+++ b/code/modules/projectiles/projectile/beams.dm
@@ -469,15 +469,6 @@
if(isliving(target))
tesla_zap(target, 5, 5000)
-/obj/item/projectile/beam/tesla/master
- damage = 15
-
-/obj/item/projectile/beam/tesla/grandmaster
- damage = 20
-
-/obj/item/projectile/beam/tesla/paramount
- damage = 25
-
/obj/item/projectile/beam/freezer
name = "freezing ray"
icon_state = "bluelaser"
diff --git a/code/modules/psionics/abilities/_ability.dm b/code/modules/psionics/abilities/_ability.dm
new file mode 100644
index 00000000000..f04e1ca6e57
--- /dev/null
+++ b/code/modules/psionics/abilities/_ability.dm
@@ -0,0 +1,29 @@
+/singleton/psionic_power
+ /// Ability name.
+ var/name
+ /// Description of what the ability does.
+ var/desc
+ /// Spell object to spawn. Must be a path.
+ var/spell_path
+ /// Spell icon state.
+ var/icon_state
+ /// Ability flags define who can pick an ability - ship characters, adminspawn characters, antags.
+ var/ability_flags
+ /// Minimum required rank to use an ability.
+ var/minimum_rank = PSI_RANK_SENSITIVE
+ /// Point shop cost.
+ var/point_cost = 1
+
+/// Called when a power is given to a mob.
+/singleton/psionic_power/proc/apply(var/mob/living/carbon/human/H)
+ if(H.ability_master)
+ var/obj/spellbutton/spell = new(H, spell_path, name, icon_state)
+ H.ability_master.add_psionic_ability(spell, icon_state, src)
+ H.psi.psionic_powers |= type
+ return TRUE
+ else
+ log_debug("Psionic power [src.name] given to mob [H] without ability master!")
+ return FALSE
+
+/// A psionic power is made up of two elements: the psionic power singleton (which defines the ability metadata such as its name, description, and cost)
+/// and the physical spell object, which defines its behaviour when used. Keep in mind that ALL psionic abilities MUST have the ASPECT_PSIONIC aspect!
diff --git a/code/modules/psionics/abilities/air_bullet.dm b/code/modules/psionics/abilities/air_bullet.dm
new file mode 100644
index 00000000000..60b5ca13b5e
--- /dev/null
+++ b/code/modules/psionics/abilities/air_bullet.dm
@@ -0,0 +1,58 @@
+/singleton/psionic_power/air_bullet
+ name = "Air Bullet"
+ desc = "Wrap air in a psionic bubble, compress it, then send it flying at your enemies. Activate the spell in hand to create up to six air bullets, then \
+ fire them by clicking something."
+ icon_state = "tech_frostaura"
+ point_cost = 3
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/projectile/air_bullet
+
+/obj/item/spell/projectile/air_bullet
+ name = "air bullet"
+ desc = "Not really a big bang attack, but it's actually pretty close."
+ icon_state = "force_missile"
+ cast_methods = CAST_RANGED|CAST_USE
+ aspect = ASPECT_PSIONIC
+ spell_projectile = /obj/item/projectile/air_bullet
+ fire_sound = 'sound/weapons/wave.ogg'
+ cooldown = 5
+ psi_cost = 0
+ var/bullets = 0
+ var/max_bullets = 6
+
+/obj/item/spell/projectile/air_bullet/on_use_cast(mob/user)
+ . = ..()
+ if(!.)
+ return
+ if(!isliving(user))
+ return
+ var/mob/living/L = user
+ if(bullets < max_bullets)
+ if(do_after(L, 0.5 SECONDS))
+ to_chat(L, SPAN_NOTICE("You compress some air bullets between your fingers."))
+ bullets = min(bullets + 2, 6)
+ maptext = SMALL_FONTS(7, bullets)
+ playsound(L, 'sound/weapons/unjam.ogg', 25)
+ L.psi.spend_power(3)
+ if(bullets >= 6)
+ to_chat(L, SPAN_NOTICE("You finish compressing all the bullets you can hold."))
+ return
+ on_use_cast(L)
+
+/obj/item/spell/projectile/air_bullet/on_ranged_cast(atom/hit_atom, mob/living/user, atom/pb_target)
+ if(!bullets)
+ to_chat(user, SPAN_WARNING("You flick your fingers, but find that you don't have any air bullets left!"))
+ return
+ . = ..(hit_atom, user, pb_target)
+ if(.)
+ bullets--
+ maptext = SMALL_FONTS(7, bullets)
+
+/obj/item/projectile/air_bullet
+ name = "air bullet"
+ icon_state = "plasma_bolt"
+ damage = 30
+ armor_penetration = 10
+ impact_sounds = list(BULLET_IMPACT_MEAT = SOUNDS_BULLET_MEAT, BULLET_IMPACT_METAL = SOUNDS_BULLET_METAL)
+ damage_flags = DAMAGE_FLAG_BULLET
+ damage_type = DAMAGE_BRUTE
diff --git a/code/modules/psionics/abilities/assay.dm b/code/modules/psionics/abilities/assay.dm
new file mode 100644
index 00000000000..ebfd4783d52
--- /dev/null
+++ b/code/modules/psionics/abilities/assay.dm
@@ -0,0 +1,80 @@
+/singleton/psionic_power/assay
+ name = "Assay"
+ desc = "Assay a creature's psionic level. Using Assay will also allow you to see psionic auras."
+ icon_state = "wiz_blind"
+ point_cost = 0
+ ability_flags = PSI_FLAG_FOUNDATIONAL
+ spell_path = /obj/item/spell/assay
+
+/obj/item/spell/assay
+ name = "assay"
+ desc = "Read someone's psionic potential."
+ icon_state = "generic"
+ cast_methods = CAST_MELEE|CAST_INNATE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 2
+
+/obj/item/spell/assay/on_innate_cast(mob/user)
+ if(!isliving(user))
+ return
+
+ . = ..()
+ if(!.)
+ return
+
+ var/mob/living/L = user
+
+ to_chat(user, SPAN_NOTICE("You can now see psionic auras."))
+ L.psi.show_auras()
+
+/obj/item/spell/assay/Destroy()
+ if(isliving(owner))
+ var/mob/living/L = owner
+ to_chat(L, SPAN_NOTICE("You can no longer see psionic auras."))
+ L.psi.hide_auras()
+
+ return ..()
+
+/obj/item/spell/assay/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
+ if(!.)
+ return
+
+ if(!ishuman(hit_atom))
+ return
+
+ if(!isliving(user))
+ return
+
+ var/mob/living/carbon/human/target = hit_atom
+
+ if(target.stat == DEAD || HAS_FLAG(target.status_flags, FAKEDEATH))
+ to_chat(user, SPAN_WARNING("Psionic power does not flow through a dead person."))
+ return
+
+ var/psi_blocked = target.is_psi_blocked()
+ if(psi_blocked)
+ to_chat(user, psi_blocked)
+ return
+
+ user.visible_message(SPAN_NOTICE("[user] lays both [user.get_pronoun("his")] palms on [target]'s temples..."),
+ SPAN_NOTICE("You lay your palms on [target]'s temples and begin tracing their Nlom signature..."))
+ if(do_mob(user, target, 4 SECONDS))
+ if(!target.psi)
+ to_chat(user, SPAN_NOTICE("[target] is psionically perceptive, but nothing more: [target.get_pronoun("he")] cannot manipulate the fabric that weaves \
+ this universe."))
+ if(target.psi)
+ switch(target.psi.get_rank())
+ if(PSI_RANK_SENSITIVE)
+ to_chat(user, SPAN_NOTICE("[target] is psionically sensitive, just like your typical Skrell. They can manipulate psionics, but not to a very \
+ precise degree."))
+ if(PSI_RANK_HARMONIOUS)
+ to_chat(user, SPAN_WARNING("[target] is psionically harmonious. This isn't a feat just about anyone can manage: only those born with a certain \
+ psionic attitude among Skrell can reach this level, and with strenuous training. They are capable of using psionics \
+ with very fine precision."))
+ if(PSI_RANK_APEX)
+ to_chat(user, SPAN_DANGER("Your psionic power is dwarfed by [target]. Just like a supernova in the night sky, their signature is absolutely brilliant \
+ beyond comprehension. This is the apex of psionic power, you are sure."))
+ if(PSI_RANK_LIMITLESS)
+ to_chat(user, SPAN_DANGER("A psionic power like this shouldn't be possible... what in the Stars is going on?"))
diff --git a/code/modules/psionics/abilities/barrier.dm b/code/modules/psionics/abilities/barrier.dm
new file mode 100644
index 00000000000..72f39057b1b
--- /dev/null
+++ b/code/modules/psionics/abilities/barrier.dm
@@ -0,0 +1,30 @@
+/singleton/psionic_power/barrier
+ name = "Psionic Barrier"
+ desc = "Activate in hand to give yourself psionic armour. This armour is slightly worse than generic heavy armour, and has a small passive upkeep."
+ icon_state = "const_mend"
+ point_cost = 1
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/barrier
+
+/obj/item/spell/barrier
+ name = "psionic barrier"
+ icon_state = "generic"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 20
+ psi_cost = 30
+
+/obj/item/spell/barrier/on_use_cast(mob/user, bypass_psi_check)
+ if(!ishuman(user))
+ return
+ . = ..()
+ if(!.)
+ return
+
+ var/mob/living/carbon/human/H = user
+ var/armor = H.psi.GetComponent(/datum/component/armor/psionic)
+ if(armor)
+ to_chat(H, SPAN_WARNING("You are already enveloped by psionic armour!"))
+ return
+ to_chat(H, SPAN_NOTICE("You are enveloped by a protective warmth."))
+ H.psi.AddComponent(/datum/component/armor/psionic, list(laser = ARMOR_LASER_MEDIUM, bullet = ARMOR_BALLISTIC_CARBINE, energy = ARMOR_ENERGY_RESISTANT, melee = ARMOR_MELEE_RESISTANT))
diff --git a/code/modules/psionics/abilities/bubble.dm b/code/modules/psionics/abilities/bubble.dm
new file mode 100644
index 00000000000..af2affa892b
--- /dev/null
+++ b/code/modules/psionics/abilities/bubble.dm
@@ -0,0 +1,33 @@
+/singleton/psionic_power/bubble
+ name = "Bubble"
+ desc = "Create a protective bubble around you that removes your need to breathe or wear voidsuits in EVA."
+ icon_state = "tech_condensation"
+ point_cost = 1
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/bubble
+
+/obj/item/spell/bubble
+ name = "bubble"
+ icon_state = "shield"
+ cast_methods = CAST_INNATE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 10
+
+/obj/item/spell/bubble/Destroy()
+ to_chat(owner, SPAN_NOTICE("Your bubble fades away."))
+ REMOVE_TRAIT(owner, TRAIT_NO_BREATHE, TRAIT_SOURCE_PSIONICS)
+ REMOVE_TRAIT(owner, TRAIT_PRESSURE_IMMUNITY, TRAIT_SOURCE_PSIONICS)
+ return ..()
+
+/obj/item/spell/bubble/on_innate_cast(mob/user)
+ if(!ishuman(user))
+ return
+ . = ..()
+ if(!.)
+ return
+
+ var/mob/living/carbon/human/H = user
+ H.visible_message(SPAN_NOTICE("A protective bubble forms around [H]."), SPAN_NOTICE("You form a protective bubble around yourself."))
+ ADD_TRAIT(H, TRAIT_NO_BREATHE, TRAIT_SOURCE_PSIONICS)
+ ADD_TRAIT(H, TRAIT_PRESSURE_IMMUNITY, TRAIT_SOURCE_PSIONICS)
diff --git a/code/modules/psionics/abilities/charge.dm b/code/modules/psionics/abilities/charge.dm
new file mode 100644
index 00000000000..fab1a32235c
--- /dev/null
+++ b/code/modules/psionics/abilities/charge.dm
@@ -0,0 +1,32 @@
+/singleton/psionic_power/charge
+ name = "Charge"
+ desc = "Use this spell on an item with a cell to charge it."
+ icon_state = "wiz_charge"
+ point_cost = 1
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/charge
+
+/obj/item/spell/charge
+ name = "charge"
+ icon_state = "audible_deception"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 5
+ psi_cost = 5
+
+/obj/item/spell/charge/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ var/obj/item/cell/C = hit_atom.get_cell()
+ if(!C)
+ return
+ if(C.percent() >= 100)
+ to_chat(user, SPAN_WARNING("That cell is already charged."))
+ return
+ psi_cost = C.maxcharge * 0.001
+ . = ..()
+ if(!.)
+ return
+ user.visible_message(SPAN_NOTICE("[user] lays [get_pronoun("his")] hand on \the [C]..."), SPAN_NOTICE("You lay your hand on \the [C]..."),)
+ if(do_after(user, 1 SECOND))
+ to_chat(user, SPAN_WARNING("You restore some power to \the [C]."))
+ C.give(C.maxcharge * 0.1)
+ psi_cost = initial(psi_cost)
diff --git a/code/modules/psionics/abilities/command.dm b/code/modules/psionics/abilities/command.dm
new file mode 100644
index 00000000000..32e30242fa6
--- /dev/null
+++ b/code/modules/psionics/abilities/command.dm
@@ -0,0 +1,78 @@
+/singleton/psionic_power/command
+ name = "Command"
+ desc = "Send a psionic command to a target. It must be one word. The suggestion ends when they finish the task, when a minute passes or \
+ when they take notable damage."
+ icon_state = "wiz_blink"
+ spell_path = /obj/item/spell/command
+ ability_flags = PSI_FLAG_EVENT
+
+/obj/item/spell/command
+ name = "command"
+ desc = "Send a psionic command to a target."
+ icon_state = "apportation"
+ cast_methods = CAST_RANGED|CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 50
+
+/obj/item/spell/command/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
+ if(!.)
+ return
+ command(hit_atom, user)
+
+/obj/item/spell/command/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
+ if(!.)
+ return
+ command(hit_atom, user)
+
+/obj/item/spell/command/proc/command(atom/hit_atom, mob/user)
+ if(!isliving(hit_atom))
+ return
+
+ if(!isliving(user))
+ return
+
+ var/mob/living/L = user
+ var/multi_word = L.psi.get_rank() > PSI_RANK_HARMONIOUS
+
+ var/mob/living/target = hit_atom
+ if(target.stat == DEAD)
+ to_chat(user, SPAN_WARNING("Not even a psion of your level can speak to the dead."))
+ return
+
+ var/psi_blocked = target.is_psi_blocked()
+ if(psi_blocked)
+ to_chat(user, psi_blocked)
+ return
+
+ user.visible_message(SPAN_CULT("[user] opens a palm and aims it at [hit_atom]..."))
+ var/text = input("Enter your command. [multi_word ? "This command may be up to 5 words." : "This command may only be one word."]", "Command", null, null)
+ if(!text)
+ return
+ text = formalize_text(text)
+
+ if(target.stat == DEAD)
+ to_chat(user, SPAN_WARNING("Not even a psion of your level can speak to the dead."))
+ return
+
+ log_say("[key_name(user)] issued a [multi_word ? "multi-word" : "single-word"] command to [key_name(target)]: [text]",ckey=key_name(src))
+
+ to_chat(user, SPAN_CULT("You psionically command to [target]: [text]"))
+
+ for (var/mob/M in player_list)
+ if (istype(M, /mob/abstract/new_player))
+ continue
+ else if(M.stat == DEAD && HAS_FLAG(M.client.prefs.toggles, CHAT_GHOSTEARS))
+ to_chat(M, "[user] psionically commands to [target]: [text]")
+
+ var/mob/living/carbon/human/H = target
+ if(H.has_psionics())
+ to_chat(H, SPAN_CULT("You are psionically commanded to carry out the following task: [text]."))
+ else if(target.has_psi_aug())
+ to_chat(H, SPAN_CULT("You sense [user]'s psyche link with your psi-receiver, a command sliding into your mind: [text]"))
+ else
+ to_chat(H, SPAN_CULT("A thought from outside your consciousness slips into your mind: [text]"))
+ to_chat(H, SPAN_DANGER("You are required to follow this command. It ends when a minute has passed, when you carry it out, or when you take a significant amount \
+ of damage. [multi_word ? "You are required to follow up to five words." : "You are required to only follow the first word of the command."]"))
diff --git a/code/modules/psionics/abilities/commune.dm b/code/modules/psionics/abilities/commune.dm
new file mode 100644
index 00000000000..bffdb8b6f6b
--- /dev/null
+++ b/code/modules/psionics/abilities/commune.dm
@@ -0,0 +1,71 @@
+/singleton/psionic_power/commune
+ name = "Commune"
+ desc = "Psionically commune with the target."
+ icon_state = "tech_audibledeception"
+ point_cost = 0
+ ability_flags = PSI_FLAG_FOUNDATIONAL
+ spell_path = /obj/item/spell/commune
+
+/obj/item/spell/commune
+ name = "commune"
+ desc = "Déjà -vu."
+ icon_state = "overload"
+ cast_methods = CAST_RANGED|CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 15
+
+/obj/item/spell/commune/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
+ if(!.)
+ return
+ commune(hit_atom, user)
+
+/obj/item/spell/commune/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
+ if(!.)
+ return
+ commune(hit_atom, user)
+
+/obj/item/spell/commune/proc/commune(atom/hit_atom, mob/user)
+ if(!isliving(hit_atom))
+ return
+
+ var/mob/living/target = hit_atom
+ if(target.stat == DEAD)
+ to_chat(user, SPAN_WARNING("Not even a psion of your level can speak to the dead."))
+ return
+
+ var/psi_blocked = target.is_psi_blocked()
+ if(psi_blocked)
+ to_chat(user, psi_blocked)
+ return
+
+ user.visible_message(SPAN_NOTICE("[user] blinks, their eyes briefly developing an unnatural shine."))
+ var/text = input("What would you like to say?", "Speak to creature", null, null)
+ text = sanitize(text)
+ if(!text)
+ return
+ text = formalize_text(text)
+
+ if(target.stat == DEAD)
+ to_chat(user, SPAN_WARNING("Not even a psion of your level can speak to the dead."))
+ return
+
+ log_say("[key_name(user)] communed to [key_name(target)]: [text]",ckey=key_name(src))
+
+ to_chat(user, SPAN_CULT("You psionically say to [target]: [text]"))
+
+ for (var/mob/M in player_list)
+ if (istype(M, /mob/abstract/new_player))
+ continue
+ else if(M.stat == DEAD && M.client.prefs.toggles & CHAT_GHOSTEARS)
+ to_chat(M, "[user] psionically says to [target]: [text]")
+
+ var/mob/living/carbon/human/H = target
+ if(H.has_psionics())
+ to_chat(H, SPAN_CULT("You instinctively sense [user] passing a thought into your mind: [text]"))
+ else if(target.has_psi_aug())
+ to_chat(H, SPAN_CULT("You sense [user]'s psyche link with your psi-receiver, a thought sliding into your mind: [text]"))
+ else
+ to_chat(H, SPAN_ALIEN("A thought from outside your consciousness slips into your mind: [text]"))
diff --git a/code/modules/psionics/abilities/electrocute.dm b/code/modules/psionics/abilities/electrocute.dm
new file mode 100644
index 00000000000..c8fcbaa7dd6
--- /dev/null
+++ b/code/modules/psionics/abilities/electrocute.dm
@@ -0,0 +1,30 @@
+/singleton/psionic_power/electrocute
+ name = "Electrocute"
+ desc = "Administer a painful amount of psionic shock to the nervous system of a foe in melee range, causing burn and agony damage. "
+ icon_state = "tech_shockaura"
+ point_cost = 2
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/electrocute
+
+/obj/item/spell/electrocute
+ name = "electrocute"
+ icon_state = "chain_lightning"
+ cast_methods = CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 5
+ hitsound = 'sound/effects/psi/power_evoke.ogg'
+ attack_verb = list("lays a palm on")
+
+/obj/item/spell/electrocute/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ if(!ishuman(hit_atom))
+ to_chat(user, SPAN_WARNING("That won't work!"))
+ return
+ . = ..()
+ if(!.)
+ return
+ var/mob/living/carbon/human/H = hit_atom
+ H.adjustHalLoss(20)
+ H.adjustFireLoss(20)
+ H.flash_pain(20)
+ to_chat(H, SPAN_DANGER("A sharp, stinging pain enters your mind!"))
diff --git a/code/modules/psionics/abilities/emotional_suggestion.dm b/code/modules/psionics/abilities/emotional_suggestion.dm
new file mode 100644
index 00000000000..95ec377d7e7
--- /dev/null
+++ b/code/modules/psionics/abilities/emotional_suggestion.dm
@@ -0,0 +1,70 @@
+/singleton/psionic_power/emotional_suggestion
+ name = "Emotional Suggestion"
+ desc = "Allows you to psionically commune with the target."
+ icon_state = "tech_gambit"
+ ability_flags = PSI_FLAG_EVENT|PSI_FLAG_CANON
+ spell_path = /obj/item/spell/emotional_suggestion
+
+/obj/item/spell/emotional_suggestion
+ name = "emotional suggestion"
+ desc = "Suggest an emotion to someone."
+ icon_state = "generic"
+ cast_methods = CAST_RANGED|CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 15
+
+/obj/item/spell/emotional_suggestion/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
+ if(!.)
+ return
+ emotional_suggestion(hit_atom, user)
+
+/obj/item/spell/emotional_suggestion/on_ranged_cast(atom/hit_atom, mob/user)
+ . = ..()
+ if(!.)
+ return
+ emotional_suggestion(hit_atom, user)
+
+/obj/item/spell/emotional_suggestion/proc/emotional_suggestion(atom/hit_atom, mob/user)
+ if(!isliving(hit_atom))
+ return
+
+ var/mob/living/target = hit_atom
+ if(target.stat == DEAD)
+ to_chat(user, SPAN_WARNING("Not even a psion of your level can suggest to the dead."))
+ return
+
+ var/psi_blocked = target.is_psi_blocked()
+ if(psi_blocked)
+ to_chat(user, psi_blocked)
+ return
+
+ user.visible_message(SPAN_NOTICE("[user] blinks, their eyes briefly developing an unnatural shine."))
+ var/text = input("Which emotion would you like to suggest?", "Emotional Suggestion") as null|anything in list("Calm", "Happiness", "Sadness", "Fear", "Anger", "Stress", "Confusion")
+ if(!text)
+ return
+
+ text = lowertext(text)
+
+ if(target.stat == DEAD)
+ to_chat(user, SPAN_WARNING("Not even a psion of your level can suggest to the dead."))
+ return
+
+ log_say("[key_name(user)] suggested an emotion to [key_name(target)]: [text]",ckey=key_name(src))
+
+ to_chat(user, SPAN_CULT("You psionically suggest an emotion to [target]: [text]"))
+
+ for (var/mob/M in player_list)
+ if (istype(M, /mob/abstract/new_player))
+ continue
+ else if(M.stat == DEAD && HAS_FLAG(M.client.prefs.toggles, CHAT_GHOSTEARS))
+ to_chat(M, "[user] psionically suggests an emotion to [target]: [text]")
+
+ var/mob/living/carbon/human/H = target
+ if(H.has_psionics())
+ to_chat(H, SPAN_NOTICE("You feel an emotion of [text] washing through your mind."))
+ else if(target.has_psi_aug())
+ to_chat(H, SPAN_NOTICE("You sense [user]'s psyche link with your psi-receiver, and an emotion envelops your mind: [text]."))
+ else
+ to_chat(H, SPAN_NOTICE("An emotion from outside your consciousness slips into your mind: [text]."))
diff --git a/code/modules/psionics/abilities/focus.dm b/code/modules/psionics/abilities/focus.dm
new file mode 100644
index 00000000000..3ffb245aef3
--- /dev/null
+++ b/code/modules/psionics/abilities/focus.dm
@@ -0,0 +1,28 @@
+/singleton/psionic_power/focus
+ name = "Focus"
+ desc = "Activate this spell in your hand to generate five units of synaptizine in your bloodstream."
+ icon_state = "tech_phaseshift"
+ point_cost = 1
+ ability_flags = PSI_FLAG_EVENT
+ spell_path = /obj/item/spell/focus
+
+/obj/item/spell/focus
+ name = "focus"
+ desc = "Psionic drugs? No way."
+ icon_state = "blink"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 50
+ psi_cost = 20
+
+/obj/item/spell/focus/on_use_cast(mob/user)
+ if(!ishuman(user))
+ return
+ . = ..()
+ if(!.)
+ return
+ var/mob/living/carbon/human/H = user
+ if(do_after(user, 1 SECOND))
+ to_chat(H, SPAN_WARNING("A calm rush envelops your mind.."))
+ H.reagents.add_reagent(/singleton/reagent/synaptizine, 5)
+ H.update_canmove()
diff --git a/code/modules/psionics/abilities/grip.dm b/code/modules/psionics/abilities/grip.dm
new file mode 100644
index 00000000000..d85eb280b25
--- /dev/null
+++ b/code/modules/psionics/abilities/grip.dm
@@ -0,0 +1,51 @@
+/singleton/psionic_power/grip
+ name = "Grip"
+ desc = "Grip a victim with psionic energy. You can squeeze your grip to crush them."
+ icon_state = "ling_bioelectrogenesis"
+ point_cost = 3
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/grip
+
+/obj/item/spell/grip
+ name = "stasis"
+ desc = "General Kenobi..."
+ icon_state = "blink"
+ cast_methods = CAST_RANGED|CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 15
+ psi_cost = 5
+ var/mob/living/victim
+
+/obj/item/spell/grip/Destroy()
+ victim.captured = FALSE
+ victim.update_canmove()
+ victim = null
+ return ..()
+
+/obj/item/spell/grip/on_use_cast(mob/user, bypass_psi_check)
+ if(!victim)
+ to_chat(user, SPAN_WARNING("You need to grip someone first!"))
+ return
+ . = ..()
+ if(!.)
+ return
+ user.visible_message(SPAN_WARNING("[user] squeezes [user.get_pronoun("his")] hand!"), SPAN_WARNING("You squeeze your hand to tighten the psionic force around [victim]."))
+ to_chat(victim, SPAN_DANGER(FONT_HUGE("You are crushed by an invisible force!")))
+ victim.apply_damage(30, DAMAGE_BRUTE, armor_pen = 30, damage_flags = DAMAGE_FLAG_DISPERSED)
+
+/obj/item/spell/grip/on_ranged_cast(atom/hit_atom, mob/user, bypass_psi_check)
+ if(!isliving(hit_atom))
+ return
+ if(victim)
+ to_chat(user, SPAN_WARNING("You can't target more than one person with this!"))
+ return
+ . = ..()
+ if(!.)
+ return
+ var/mob/living/M = hit_atom
+ user.visible_message(SPAN_DANGER("[user] extends [user.get_pronoun("his")] arm and makes a grab motion towards [M]!"),
+ SPAN_DANGER("You extend your arm and grab [M] with your psionic energy!"))
+ to_chat(M, SPAN_DANGER(FONT_HUGE("You feel an invisible force tighten around you!")))
+ M.captured = TRUE
+ M.update_canmove()
+ victim = M
diff --git a/code/modules/psionics/abilities/hollow_purple.dm b/code/modules/psionics/abilities/hollow_purple.dm
new file mode 100644
index 00000000000..e9f356a8a87
--- /dev/null
+++ b/code/modules/psionics/abilities/hollow_purple.dm
@@ -0,0 +1,106 @@
+/singleton/psionic_power/hollow_purple
+ name = "Hollow Technique: Purple"
+ desc = "This technique brings the concept of motion and reversal into reality. Purple is born from merging both infinites: Blue and Red, \
+ to produce an imaginary mass that rushes forth and erases everything in its path. Rather than the attraction of Blue or the repulsion of Red, \
+ Purple is an extraordinarily destructive energy wave of annihilation that rips whatever it hits from existence."
+ icon_state = "hollow_purple"
+ point_cost = 0
+ ability_flags = PSI_FLAG_LIMITLESS
+ minimum_rank = PSI_FLAG_LIMITLESS
+ spell_path = /obj/item/spell/projectile/hollow_purple
+
+/obj/item/spell/projectile/hollow_purple
+ name = "hollow purple"
+ desc = "Throughout Heaven and Earth, I alone am the honored one!"
+ icon_state = "destablize"
+ cast_methods = CAST_RANGED
+ aspect = ASPECT_PSIONIC
+ spell_projectile = /obj/item/projectile/hollow_purple
+ fire_sound = 'sound/magic/WandODeath.ogg'
+ cooldown = 10
+ psi_cost = 50
+ var/stage = 0
+
+/obj/item/spell/projectile/hollow_purple/on_ranged_cast(atom/hit_atom, mob/living/user, atom/pb_target)
+ if(!isliving(user))
+ return
+
+ stage = 1
+ user.visible_message(SPAN_NOTICE("[user] puts [user.get_pronoun("his")] palms together..."))
+ START_PROCESSING(SSprocessing, src)
+ user.say("Cursed Technique Amplification: Blue...")
+ user.set_light(7, 10, COLOR_BLUE)
+ color = COLOR_BLUE
+ if(do_after(user, 4 SECONDS))
+ stage = 2
+ color = COLOR_RED
+ user.say("Cursed Technique Reversal: Red...")
+ user.set_light(0)
+ user.set_light(7, 10, COLOR_RED)
+ if(do_after(user, 4 SECONDS))
+ user.say("Hollow Technique: Purple!")
+ user.set_light(0)
+ user.visible_message(SPAN_CULT("[user] fires a gigantic purple sphere from [user.get_pronoun("his")] hand!"))
+ . = ..()
+
+ STOP_PROCESSING(SSprocessing, src)
+ color = initial(color)
+ user.set_light(0)
+ stage = 0
+
+/obj/item/spell/projectile/hollow_purple/process()
+ if(stage >= 1)
+ for(var/mob/living/L in get_hearers_in_view(7, src))
+ shake_camera(L, 5, 5)
+
+/obj/item/projectile/hollow_purple
+ name = "hollow purple"
+ icon_state = "hollow_purple"
+ color = COLOR_PURPLE
+ damage = 1000
+ armor_penetration = 1000
+ penetrating = 100
+ range = 250
+ accuracy = 100
+ anti_materiel_potential = 100
+ damage_type = DAMAGE_BRUTE
+
+/obj/item/projectile/hollow_purple/Initialize()
+ . = ..()
+ set_light(10, 10, COLOR_PURPLE)
+
+/obj/item/projectile/hollow_purple/Destroy()
+ return ..()
+
+/obj/item/projectile/hollow_purple/on_impact(var/atom/A)
+ if(ismob(A))
+ if(A != firer)
+ var/mob/M = A
+ M.gib()
+ explosion(A, 5, 5, 5)
+ ..()
+
+/obj/item/projectile/hollow_purple/on_hit(atom/target, blocked, def_zone)
+ if(ismob(target))
+ if(target != firer)
+ var/mob/M = target
+ M.gib()
+ explosion(target, 5, 5, 5)
+ ..()
+
+/obj/item/projectile/hollow_purple/check_penetrate(atom/A)
+ on_hit(A)
+ return TRUE
+
+/obj/item/projectile/hollow_purple/after_move()
+ for(var/a in range(3, src))
+ if(isliving(a) && a != firer)
+ var/mob/living/M = a
+ M.gib()
+ playsound(src, 'sound/magic/LightningShock.ogg', 75, 1)
+ else if(isturf(a) || isobj(a))
+ var/atom/A = a
+ if(!A.density)
+ continue
+ A.ex_act(3)
+ playsound(src, 'sound/magic/LightningShock.ogg', 75, 1)
diff --git a/code/modules/psionics/abilities/ion.dm b/code/modules/psionics/abilities/ion.dm
new file mode 100644
index 00000000000..1da9dde5319
--- /dev/null
+++ b/code/modules/psionics/abilities/ion.dm
@@ -0,0 +1,24 @@
+/singleton/psionic_power/ion
+ name = "Ion Blast"
+ desc = "Activate in hand to release a 3x3 ion blast around you. Be careful, this ability is expensive to use."
+ icon_state = "const_mend"
+ point_cost = 3
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/ion
+
+/obj/item/spell/ion
+ name = "ion blast"
+ icon_state = "generic"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 20
+ psi_cost = 30
+
+/obj/item/spell/ion/on_use_cast(mob/user, bypass_psi_check)
+ . = ..()
+ if(!.)
+ return
+
+ user.visible_message(SPAN_DANGER("[user] opens their arms and releases an ion blast around them!"),
+ SPAN_DANGER("You open your arms and release an ion blast around you!"))
+ empulse(get_turf(user), 3, 3)
diff --git a/code/modules/psionics/abilities/jump.dm b/code/modules/psionics/abilities/jump.dm
new file mode 100644
index 00000000000..4eebefb55fc
--- /dev/null
+++ b/code/modules/psionics/abilities/jump.dm
@@ -0,0 +1,22 @@
+/singleton/psionic_power/jump
+ name = "Jump"
+ desc = "Teleport to a destination you click on."
+ icon_state = "tech_dispel"
+ point_cost = 3
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/jump
+
+/obj/item/spell/jump
+ name = "jump"
+ icon_state = "warp_strike"
+ cast_methods = CAST_RANGED
+ aspect = ASPECT_PSIONIC
+ cooldown = 50
+ psi_cost = 15
+
+/obj/item/spell/jump/on_ranged_cast(atom/hit_atom, mob/user, bypass_psi_check)
+ . = ..()
+ if(!.)
+ return
+ user.visible_message(SPAN_WARNING("[user] warps and disappears!"), SPAN_WARNING("You weave the Nlom to bring you to your destination!"))
+ do_teleport(user, hit_atom)
diff --git a/code/modules/psionics/abilities/lightning.dm b/code/modules/psionics/abilities/lightning.dm
new file mode 100644
index 00000000000..d590d22887b
--- /dev/null
+++ b/code/modules/psionics/abilities/lightning.dm
@@ -0,0 +1,59 @@
+/singleton/psionic_power/lightning
+ name = "Lightning"
+ desc = "Fire a concentrated lightning bolt. Activate this spell in hand to switch to a second mode that hits living beings in a 4x3 area in front of you."
+ icon_state = "tech_instabilitytapold"
+ point_cost = 0
+ ability_flags = PSI_FLAG_APEX
+ minimum_rank = PSI_RANK_APEX
+ spell_path = /obj/item/spell/projectile/psi_lightning
+
+/obj/item/spell/projectile/psi_lightning
+ name = "lightning"
+ icon_state = "chain_lightning"
+ cast_methods = CAST_RANGED|CAST_USE
+ aspect = ASPECT_PSIONIC
+ spell_projectile = /obj/item/projectile/beam/psi_lightning
+ fire_sound = 'sound/magic/LightningShock.ogg'
+ cooldown = 10
+ psi_cost = 15
+ var/mode = 0
+
+/obj/item/spell/projectile/psi_lightning/on_use_cast(mob/user, bypass_psi_check)
+ . = ..(user, TRUE)
+ mode = !mode
+ if(mode)
+ to_chat(user, SPAN_NOTICE("You will now fire area-of-effect lightning in a 4x3 area in front of you."))
+ spell_projectile = /obj/item/projectile/beam/psi_lightning/wide
+
+ else
+ to_chat(user, SPAN_NOTICE("You will now fire a normal lightning bolt."))
+ spell_projectile = /obj/item/projectile/beam/psi_lightning
+
+/obj/item/projectile/beam/psi_lightning
+ name = "psionic lightning"
+ damage = 30
+ armor_penetration = 30
+ damage_type = DAMAGE_BURN
+ pass_flags = PASSTABLE | PASSGRILLE | PASSRAILING
+ range = 40
+ accuracy = 100
+
+ muzzle_type = /obj/effect/projectile/muzzle/tesla
+ tracer_type = /obj/effect/projectile/tracer/tesla
+ impact_type = /obj/effect/projectile/impact/tesla
+
+
+/obj/item/projectile/beam/psi_lightning/pellet
+ damage = 20
+ armor_penetration = 20
+
+/obj/item/projectile/beam/psi_lightning/wide
+ damage = 20
+ armor_penetration = 20
+
+/obj/item/projectile/beam/psi_lightning/wide/Initialize()
+ for(var/i = 1 to 4)
+ var/turf/new_turf = get_random_turf_in_range(get_turf(firer), i + rand(0, i), 0, TRUE, FALSE)
+ var/obj/item/projectile/beam/psi_lightning/pellet/pellet = new type(new_turf)
+ var/turf/front_turf = get_step(pellet, pellet.dir)
+ pellet.launch_projectile(front_turf)
diff --git a/code/modules/psionics/abilities/mend.dm b/code/modules/psionics/abilities/mend.dm
new file mode 100644
index 00000000000..b8c54eb15ee
--- /dev/null
+++ b/code/modules/psionics/abilities/mend.dm
@@ -0,0 +1,69 @@
+/singleton/psionic_power/mend
+ name = "Mend"
+ desc = "Mend a creature's wounds. This handles internal wounds as well, such as ruptured organs and broken bones."
+ icon_state = "tech_biomedaura"
+ point_cost = 2
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/mend
+
+/obj/item/spell/mend
+ name = "mend"
+ desc = "Clear!"
+ icon_state = "mend_wounds"
+ cast_methods = CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ cooldown = 50
+ psi_cost = 30
+
+/obj/item/spell/mend/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ if(!ishuman(hit_atom))
+ return
+
+ var/mob/living/carbon/human/H = hit_atom
+ if(!H.has_zona_bovinae())
+ to_chat(user, SPAN_WARNING("Psionic power cannot flow through this being."))
+ return
+
+ if(H.stat == DEAD || H.status_flags & FAKEDEATH)
+ to_chat(user, SPAN_WARNING("Psionic power does not flow through a dead person."))
+ return
+
+ . = ..()
+ if(!.)
+ return
+
+ user.visible_message(SPAN_NOTICE("[user] lays a palm on [H]..."), SPAN_NOTICE("You lay your palm on [H] and get to work."))
+ to_chat(user, SPAN_NOTICE("You start by flowing your regenerative psionic energy through their vital organs..."))
+ for(var/obj/item/organ/O in H.internal_organs)
+ if(do_mob(user, H, 15 SECONDS))
+ if(O.damage > 0) // Fix internal damage
+ to_chat(SPAN_NOTICE("You mend their [O]'s bruising."))
+ O.heal_damage(O.damage)
+ if(HAS_FLAG(O.status, ORGAN_BROKEN))
+ to_chat(SPAN_NOTICE("You restart their [O]'s functionality."))
+ O.status &= ~ORGAN_BROKEN
+ if(O.damage <= 5 && O.organ_tag == BP_EYES) // Fix eyes
+ H.sdisabilities &= ~BLIND
+
+ to_chat(user, SPAN_NOTICE("You continue by flowing your regenerative psionic energy through their limbs..."))
+ for(var/obj/item/organ/external/O in H.organs) // Fix limbs
+ if(!O.robotic < ORGAN_ROBOT) // No robot parts for this.
+ continue
+ if(do_mob(user, H, 15 SECONDS))
+ to_chat(SPAN_NOTICE("You mend their [O]'s bruising."))
+ O.heal_damage(0, O.damage, internal = TRUE, robo_repair = FALSE)
+
+ to_chat(user, SPAN_NOTICE("Finally, you flowing your regenerative psionic energy through their broken limbs..."))
+ for(var/obj/item/organ/E in H.bad_external_organs) // Fix bones
+ var/obj/item/organ/external/affected = E
+ if(do_mob(user, H, 10 SECONDS))
+ if((affected.damage < affected.min_broken_damage * config.organ_health_multiplier) && (affected.status & ORGAN_BROKEN))
+ to_chat(SPAN_NOTICE("You mend their [affected] together."))
+ affected.status &= ~ORGAN_BROKEN
+ if(HAS_FLAG(affected.status, ORGAN_ARTERY_CUT))
+ to_chat(SPAN_NOTICE("You mend a spliced artery in their [affected]."))
+ affected.status &= ~ORGAN_ARTERY_CUT
+ if(HAS_FLAG(affected.status, ORGAN_DEAD))
+ to_chat(SPAN_NOTICE("You mend some necrosis from their [affected]."))
+ affected.status &= ~ORGAN_DEAD
+ user.visible_message(SPAN_NOTICE("[user] raises their palm from [H]."), SPAN_NOTICE("You raise your palm, having finished your work."))
diff --git a/code/modules/psionics/abilities/mind_muddle.dm b/code/modules/psionics/abilities/mind_muddle.dm
new file mode 100644
index 00000000000..d78ce624f4e
--- /dev/null
+++ b/code/modules/psionics/abilities/mind_muddle.dm
@@ -0,0 +1,29 @@
+/singleton/psionic_power/mind_muddle
+ name = "Mind Muddle"
+ desc = "Use this at range to confuse a target and give them a little bit of pain."
+ icon_state = "wiz_tele"
+ point_cost = 1
+ ability_flags = PSI_FLAG_EVENT
+ spell_path = /obj/item/spell/mind_muddle
+
+/obj/item/spell/mind_muddle
+ name = "mind muddle"
+ desc = "Confuse people. That's all it does. Oh, and the pain."
+ icon_state = "warp_strike"
+ cast_methods = CAST_RANGED
+ aspect = ASPECT_PSIONIC
+ cooldown = 20
+ psi_cost = 10
+
+/obj/item/spell/mind_muddle/on_ranged_cast(atom/hit_atom, mob/user, bypass_psi_check)
+ if(!ishuman(hit_atom))
+ to_chat(user, SPAN_WARNING("That won't work on them!"))
+ return
+ . = ..()
+ if(!.)
+ return
+ var/mob/living/carbon/human/H = hit_atom
+ H.confused += 20
+ to_chat(H, SPAN_DANGER("A stinging sensation fills your mind! You feel nauseated..."))
+ H.adjustHalLoss(15)
+
diff --git a/code/modules/psionics/abilities/mirror_shade.dm b/code/modules/psionics/abilities/mirror_shade.dm
new file mode 100644
index 00000000000..93b9c424328
--- /dev/null
+++ b/code/modules/psionics/abilities/mirror_shade.dm
@@ -0,0 +1,53 @@
+/singleton/psionic_power/mirror_shade
+ name = "Mirror Shade"
+ desc = "Activate this spell to generate two psionic copies of yourself that will attack nearby mobs. These clones are not dense, will deal pain damage, and disappear \
+ after thirty seconds."
+ icon_state = "wiz_jaunt"
+ point_cost = 2
+ ability_flags = PSI_FLAG_EVENT
+ spell_path = /obj/item/spell/mirror_shade
+
+/obj/item/spell/mirror_shade
+ name = "mirror shade"
+ desc = "Photocopy yourself."
+ icon_state = "darkness"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 100
+ psi_cost = 20
+
+/obj/item/spell/mirror_shade/on_use_cast(mob/user)
+ . = ..()
+ if(!.)
+ return
+ if(do_after(user, 1 SECOND))
+ to_chat(user, SPAN_WARNING("You weave your psionic force into two copies of yourself!"))
+ for(var/i = 1 to 2)
+ var/mob/living/simple_animal/hostile/mirror_shade/MS = new(pick(get_adjacent_open_turfs(user)))
+ MS.appearance = user.appearance
+ MS.name = user.name
+ MS.owner = user
+
+/mob/living/simple_animal/hostile/mirror_shade
+ damage_type = DAMAGE_PAIN
+ density = FALSE
+ destroy_surroundings = FALSE
+ melee_damage_lower = 10
+ melee_damage_upper = 15
+ attack_emote = "tries shambling towards"
+ attacktext = "phases its arms through"
+ var/mob/living/carbon/human/owner
+
+/mob/living/simple_animal/hostile/mirror_shade/Initialize()
+ . = ..()
+ friends += owner
+ QDEL_IN(src, 30 SECONDS)
+
+/mob/living/simple_animal/hostile/mirror_shade/examine(mob/user)
+ /// Technically suspicious, but these have 30 seconds of lifetime so it's probably fine.
+ return owner.examine(user)
+
+/mob/living/simple_animal/hostile/mirror_shade/Destroy()
+ owner = null
+ visible_message(SPAN_WARNING("[src] dissipates into nothingness, as if it were air all along!"))
+ return ..()
diff --git a/code/modules/psionics/abilities/nlom_eyes.dm b/code/modules/psionics/abilities/nlom_eyes.dm
new file mode 100644
index 00000000000..45f426838b7
--- /dev/null
+++ b/code/modules/psionics/abilities/nlom_eyes.dm
@@ -0,0 +1,76 @@
+/singleton/psionic_power/nlom_eyes
+ name = "Nlom Eyes"
+ desc = "Roughly locate a mob on your z-level."
+ icon_state = "tech_control"
+ point_cost = 2
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/nlom_eyes
+
+/obj/item/spell/nlom_eyes
+ name = "nlom eyes"
+ desc = "Psionic drugs? No way."
+ icon_state = "track"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 5
+ /// The mob we are tracking.
+ var/mob/living/tracked
+ /// If we are tracking a mob.
+ var/tracking = FALSE
+
+/obj/item/spell/nlom_eyes/Destroy()
+ tracked = null
+ tracking = 0
+ STOP_PROCESSING(SSprocessing, src)
+ return ..()
+
+/obj/item/spell/nlom_eyes/on_use_cast(mob/user)
+ if(tracking)
+ tracking = FALSE
+ to_chat(user, SPAN_NOTICE("You stop looking in the Nlom for \the [tracked]'s whereabouts."))
+ tracked = null
+ STOP_PROCESSING(SSprocessing, src)
+ icon_state = "track"
+ return
+ . = ..()
+ if(!.)
+ return
+
+ var/list/mob_choices = list()
+ for(var/mob/living/L in mob_list)
+ if(L == user)
+ continue
+ if(!L.is_psi_blocked())
+ continue
+ if(GET_Z(L) != GET_Z(user))
+ continue
+ mob_choices += L
+ var/choice = input(user, "Decide who to track.", "Nlom Eyes") as null|anything in mob_choices
+ if(choice)
+ tracked = choice
+ tracking = TRUE
+ START_PROCESSING(SSprocessing, src)
+
+/obj/item/spell/track/process()
+ if(!tracking)
+ icon_state = "track"
+ return
+
+ if(!tracked)
+ icon_state = "track_unknown"
+
+ if(GET_Z(tracked) != GET_Z(owner))
+ icon_state = "track_unknown"
+
+ else
+ set_dir(get_dir(src,get_turf(tracked)))
+ switch(get_dist(src,get_turf(tracked)))
+ if(0)
+ icon_state = "track_direct"
+ if(1 to 8)
+ icon_state = "track_close"
+ if(9 to 16)
+ icon_state = "track_medium"
+ if(16 to INFINITY)
+ icon_state = "track_far"
diff --git a/code/modules/psionics/abilities/psi_awakening.dm b/code/modules/psionics/abilities/psi_awakening.dm
new file mode 100644
index 00000000000..f2538620e39
--- /dev/null
+++ b/code/modules/psionics/abilities/psi_awakening.dm
@@ -0,0 +1,43 @@
+/singleton/psionic_power/awaken
+ name = "Awaken"
+ desc = "Stimulate a living being's Zona Bovinae and bring them to Psionically Harmonious rank."
+ icon_state = "tech_corona"
+ point_cost = 0
+ ability_flags = PSI_FLAG_APEX
+ minimum_rank = PSI_RANK_APEX
+ spell_path = /obj/item/spell/awaken
+
+/obj/item/spell/awaken
+ name = "awaken"
+ icon_state = "energy_siphon_drain"
+ cast_methods = CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ cooldown = 20
+ psi_cost = 100
+
+/obj/item/spell/awaken/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ if(!isliving(hit_atom))
+ return
+
+ var/mob/living/L = hit_atom
+ if(!L.has_zona_bovinae())
+ to_chat(user, SPAN_WARNING("This being doesn't have a Zona Bovinae."))
+ return
+
+ if(L.psi && L.psi.get_rank() >= PSI_RANK_HARMONIOUS)
+ to_chat(user, SPAN_WARNING("This wouldn't do anything on them."))
+ return
+
+ . = ..()
+ if(!.)
+ return
+
+ user.visible_message(SPAN_DANGER("[user] grabs [L]'s head..."), SPAN_DANGER("You grab [L]'s head..."))
+ if(do_mob(user, L, 5 SECONDS))
+ L.set_psi_rank(PSI_RANK_HARMONIOUS)
+ L.flash_pain(50)
+ L.adjustHalLoss(20)
+ playsound(get_turf(L), 'sound/effects/psi/power_feedback.ogg', 100)
+ user.visible_message(SPAN_DANGER("[user] unleashes a psionic shock through [L]'s head!"),
+ SPAN_DANGER("You unleash a psionic shock through [L]'s head, targeting their Zona Bovinae and stimulating it it."))
+ to_chat(L, SPAN_HIGHDANGER("A large, painful shock courses through your head. You can see the weave of the universe... and manipulate it."))
diff --git a/code/modules/psionics/abilities/psi_drain.dm b/code/modules/psionics/abilities/psi_drain.dm
new file mode 100644
index 00000000000..aa47363928b
--- /dev/null
+++ b/code/modules/psionics/abilities/psi_drain.dm
@@ -0,0 +1,57 @@
+/singleton/psionic_power/psi_drain
+ name = "Psi-Drain"
+ desc = "Drain psi-stamina from a living being. This can be used both in melee and at range."
+ icon_state = "gen_project"
+ point_cost = 0
+ ability_flags = PSI_FLAG_APEX
+ minimum_rank = PSI_RANK_APEX
+ spell_path = /obj/item/spell/psi_drain
+
+/obj/item/spell/psi_drain
+ name = "psi-drain"
+ icon_state = "chain_lightning"
+ cast_methods = CAST_MELEE|CAST_RANGED
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 5
+
+/obj/item/spell/psi_drain/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ if(!isliving(hit_atom))
+ return
+
+ var/mob/living/L = hit_atom
+
+ if(!L.has_zona_bovinae())
+ to_chat(user, SPAN_WARNING("This being doesn't have a Zona Bovinae."))
+ return
+
+ . = ..()
+ if(!.)
+ return
+
+ psi_drain(L, user)
+
+/obj/item/spell/psi_drain/on_ranged_cast(atom/hit_atom, mob/user, bypass_psi_check)
+ if(!isliving(hit_atom))
+ return
+
+ var/mob/living/L = hit_atom
+ if(!L.has_zona_bovinae())
+ to_chat(user, SPAN_WARNING("This being doesn't have a Zona Bovinae."))
+ return
+
+ . = ..()
+ if(!.)
+ return
+ psi_drain(L, user)
+
+/obj/item/spell/psi_drain/proc/psi_drain(mob/living/L, mob/living/user)
+ user.visible_message(SPAN_DANGER("[user] squeezes [user.get_pronoun("his")] hand at [L]!"), SPAN_DANGER("You squeeze your hand at [L] and siphon [L.get_pronoun("his")] psionic energy!"))
+ var/psi_taken = min(user.psi.max_stamina, rand(10, 25))
+ if(L.psi)
+ L.psi.spend_power(psi_taken)
+ else
+ L.adjustHalLoss(psi_taken * 1.5)
+ L.flash_pain(psi_taken * 1.5)
+ playsound(get_turf(user), 'sound/effects/psi/power_evoke.ogg', 100)
+ to_chat(L, SPAN_DANGER("You feel a headache split apart your mind!"))
diff --git a/code/modules/psionics/abilities/psi_punch.dm b/code/modules/psionics/abilities/psi_punch.dm
new file mode 100644
index 00000000000..94f16151ff4
--- /dev/null
+++ b/code/modules/psionics/abilities/psi_punch.dm
@@ -0,0 +1,31 @@
+/singleton/psionic_power/punch
+ name = "Psi-Punch"
+ desc = "Imbue your fists with psionic power."
+ icon_state = "const_fist"
+ point_cost = 2
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/punch
+
+/obj/item/spell/punch
+ name = "psi-punch"
+ icon_state = "generic"
+ cast_methods = CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ force = 20
+ armor_penetration = 20
+ cooldown = 0
+ psi_cost = 3
+ flags = 0
+ hitsound = 'sound/weapons/resonator_blast.ogg'
+
+/obj/item/spell/punch/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ if(!isliving(hit_atom))
+ return
+ . = ..()
+ if(!.)
+ return
+ var/mob/living/M = hit_atom
+ if(prob(15))
+ M.throw_at(get_edge_target_turf(loc, loc.dir), 2, 7)
+ M.visible_message(SPAN_DANGER("[M] is floored by psychic energy!"), SPAN_DANGER("You are floored by psychic energy!"))
+ M.apply_effect(2, WEAKEN)
diff --git a/code/modules/psionics/abilities/psi_recovery.dm b/code/modules/psionics/abilities/psi_recovery.dm
new file mode 100644
index 00000000000..d9f4af7d7a0
--- /dev/null
+++ b/code/modules/psionics/abilities/psi_recovery.dm
@@ -0,0 +1,37 @@
+/singleton/psionic_power/psi_recovery
+ name = "Psionic Recovery"
+ desc = "Activate in hand to regenerate psi-stamina."
+ icon_state = "swarm_zeropoint"
+ point_cost = 0
+ ability_flags = PSI_FLAG_FOUNDATIONAL
+ spell_path = /obj/item/spell/psi_recovery
+
+/obj/item/spell/psi_recovery
+ name = "psionic recovery"
+ desc = "It's an aura recharge."
+ icon_state = "generic"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 5
+ psi_cost = 0
+
+/obj/item/spell/psi_recovery/on_use_cast(mob/user)
+ . = ..()
+ if(!.)
+ return
+ if(!isliving(user))
+ return
+
+ var/mob/living/L = user
+ L.visible_message(SPAN_NOTICE("[user] puts [user.get_pronoun("his")] hands together and focuses..."),
+ SPAN_NOTICE("You put your hands together and begin focusing on recovering your psionic energy..."))
+ psi_recovery(user)
+
+/obj/item/spell/psi_recovery/proc/psi_recovery(mob/user)
+ var/mob/living/L = user
+ if(do_after(user, 0.5 SECONDS))
+ L.psi.stamina = min(L.psi.max_stamina, L.psi.stamina + rand(1,3))
+ if(L.psi.stamina >= L.psi.max_stamina)
+ to_chat(user, SPAN_NOTICE("You've recovered all your psionic energy."))
+ return TRUE
+ psi_recovery(user)
diff --git a/code/modules/psionics/abilities/psi_search.dm b/code/modules/psionics/abilities/psi_search.dm
new file mode 100644
index 00000000000..1f00890ee04
--- /dev/null
+++ b/code/modules/psionics/abilities/psi_search.dm
@@ -0,0 +1,68 @@
+/singleton/psionic_power/psi_search
+ name = "Psionic Search"
+ desc = "Scan your Z-level for Nlom signatures. The distance will be slightly inaccurate for weak signatures."
+ icon_state = "wiz_shield"
+ point_cost = 0
+ ability_flags = PSI_FLAG_FOUNDATIONAL
+ spell_path = /obj/item/spell/psi_search
+
+/obj/item/spell/psi_search
+ name = "psionic search"
+ desc = "A psionic magnifying glass."
+ icon_state = "generic"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 5
+ psi_cost = 10
+
+/obj/item/spell/psi_search/on_use_cast(mob/user)
+ . = ..()
+ if(!.)
+ return
+ if(!isliving(user))
+ return
+ var/mob/living/L = user
+ L.visible_message(SPAN_NOTICE("[L] puts two fingers to [L.get_pronoun("his")] forehead and focuses..."),
+ SPAN_NOTICE("You put two fingers to your temple and focus on locating Nlom signatures..."))
+ if(do_after(L, 3 SECONDS))
+ var/list/level_humans = list()
+ var/found_apex = FALSE
+ for(var/mob/living/carbon/human/H in human_mob_list)
+ if(H == L)
+ continue
+ if((GET_Z(H) == GET_Z(L)) && !H.is_psi_blocked())
+ if(HAS_TRAIT(H, TRAIT_PSIONIC_SUPPRESSION))
+ continue
+ level_humans |= H
+ if(H.psi)
+ if(H.psi.get_rank() >= PSI_RANK_APEX)
+ found_apex = TRUE
+ if(!length(level_humans))
+ to_chat(L, SPAN_WARNING("The Nlom is quiet and empty here."))
+ return TRUE
+ if(found_apex)
+ to_chat(L, SPAN_DANGER(FONT_HUGE("You reach out into the Nlom and your senses are overwhelmed by a massive signature!")))
+ L.flash_pain(20)
+ L.adjustHalLoss(20)
+ return
+ var/list/signatures = list()
+ var/harmonious_signatures = 0
+ var/sensitive_signatures = 0
+ var/perceptive_signatures = 0
+ for(var/mob/living/carbon/human/H in level_humans)
+ if(!H.psi)
+ perceptive_signatures++
+ continue
+ if(H.psi && H.psi.get_rank() == PSI_RANK_SENSITIVE)
+ sensitive_signatures++
+ continue
+ if(H.psi && H.psi.get_rank() == PSI_RANK_HARMONIOUS)
+ harmonious_signatures++
+ continue
+ if(perceptive_signatures)
+ signatures += "[perceptive_signatures] weak signature[perceptive_signatures > 1 ? "s" : ""]"
+ if(sensitive_signatures)
+ signatures += "[sensitive_signatures] robust signature[sensitive_signatures > 1 ? "s" : ""]"
+ if(harmonious_signatures)
+ signatures += "[harmonious_signatures] very powerful signature[harmonious_signatures > 1 ? "s" : ""]"
+ to_chat(user, SPAN_NOTICE("Reaching out into the Nlom, you sense [english_list(signatures)]."))
diff --git a/code/modules/psionics/abilities/psi_stamina.dm b/code/modules/psionics/abilities/psi_stamina.dm
new file mode 100644
index 00000000000..e8278b2fa47
--- /dev/null
+++ b/code/modules/psionics/abilities/psi_stamina.dm
@@ -0,0 +1,27 @@
+/singleton/psionic_power/psi_stamina
+ name = "Psi-Stamina Weave"
+ desc = "Activate this spell in your hand to regenerate your psi-stamina a little bit."
+ icon_state = "tech_mend_template"
+ point_cost = 1
+ ability_flags = PSI_FLAG_EVENT|PSI_FLAG_CANON
+ spell_path = /obj/item/spell/psi_stamina
+
+/obj/item/spell/psi_stamina
+ name = "psi-stamina weave"
+ desc = "Kind of like charging up your aura."
+ icon_state = "generic"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 0
+ psi_cost = 0
+
+/obj/item/spell/psi_stamina/on_use_cast(mob/user)
+ . = ..()
+ if(!.)
+ return
+ if(do_after(user, 1 SECOND))
+ to_chat(user, SPAN_NOTICE("You feel a bit more refreshed."))
+ owner.psi.stamina = min(owner.psi.max_stamina, owner.psi.stamina + rand(2,5))
+ if(owner.psi.stamina >= owner.psi.max_stamina)
+ to_chat(user, SPAN_NOTICE("You're ready to go again!"))
+ return TRUE
diff --git a/code/modules/psionics/abilities/psi_sunder.dm b/code/modules/psionics/abilities/psi_sunder.dm
new file mode 100644
index 00000000000..ec92daee6bd
--- /dev/null
+++ b/code/modules/psionics/abilities/psi_sunder.dm
@@ -0,0 +1,44 @@
+/singleton/psionic_power/sunder
+ name = "Sunder"
+ desc = "Destroy a living being's Zona Bovinae. This will make them psionically deaf."
+ icon_state = "ling_berserk"
+ point_cost = 0
+ ability_flags = PSI_FLAG_APEX
+ minimum_rank = PSI_RANK_APEX
+ spell_path = /obj/item/spell/sunder
+
+/obj/item/spell/sunder
+ name = "sunder"
+ icon_state = "chain_lightning"
+ cast_methods = CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ cooldown = 30
+ psi_cost = 20
+
+/obj/item/spell/sunder/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ if(!isliving(hit_atom))
+ return
+
+ var/mob/living/L = hit_atom
+ if(!L.has_zona_bovinae())
+ to_chat(user, SPAN_WARNING("This being doesn't have a Zona Bovinae."))
+ return
+
+ . = ..()
+ if(!.)
+ return
+
+ user.visible_message(SPAN_DANGER("[user] grabs [L]'s head..."), SPAN_DANGER("You grab [L]'s head..."))
+ if(do_mob(user, L, 5 SECONDS))
+ if(L.psi)
+ QDEL_NULL(L.psi)
+ ADD_TRAIT(L, TRAIT_PSIONICALLY_DEAF, TRAIT_SOURCE_PSIONICS)
+ L.flash_pain(100)
+ L.adjustHalLoss(50)
+ L.stuttering += 20
+ playsound(get_turf(L), 'sound/effects/psi/power_feedback.ogg', 100)
+ user.visible_message(SPAN_DANGER("[user] unleashes a psionic shock through [L]'s head!"),
+ SPAN_DANGER("You unleash a psionic shock through [L]'s head, targeting their Zona Bovinae and destroying it."))
+ to_chat(L, SPAN_HIGHDANGER("A large, painful shock courses through your head. Part of your brain feels like it's gone."))
+ if(isskrell(L))
+ to_chat(L, SPAN_CULT("You feel absolutely empty..."))
diff --git a/code/modules/psionics/abilities/psi_suppression.dm b/code/modules/psionics/abilities/psi_suppression.dm
new file mode 100644
index 00000000000..758fa93811c
--- /dev/null
+++ b/code/modules/psionics/abilities/psi_suppression.dm
@@ -0,0 +1,27 @@
+/singleton/psionic_power/psi_suppression
+ name = "Psionic Suppression"
+ desc = "Hold in one of your hands to make yourself invisible to Psi-Search."
+ icon_state = "tech_shield"
+ point_cost = 1
+ ability_flags = PSI_FLAG_CANON
+ spell_path = /obj/item/spell/psi_suppression
+
+/obj/item/spell/psi_suppression
+ name = "psionic suppression"
+ icon_state = "generic"
+ cast_methods = CAST_INNATE
+ aspect = ASPECT_PSIONIC
+ psi_cost = 5
+
+/obj/item/spell/psi_suppression/Destroy()
+ to_chat(owner, SPAN_NOTICE("You are no longer hidden from Psi-Search."))
+ REMOVE_TRAIT(owner, TRAIT_PSIONIC_SUPPRESSION, TRAIT_SOURCE_PSIONICS)
+ return ..()
+
+/obj/item/spell/psi_suppression/on_innate_cast(mob/user)
+ . = ..()
+ if(!.)
+ return
+
+ to_chat(user, SPAN_NOTICE("You are now hidden from Psi-Search."))
+ ADD_TRAIT(user, TRAIT_PSIONIC_SUPPRESSION, TRAIT_SOURCE_PSIONICS)
diff --git a/code/modules/psionics/abilities/psi_sword.dm b/code/modules/psionics/abilities/psi_sword.dm
new file mode 100644
index 00000000000..54ef90614c8
--- /dev/null
+++ b/code/modules/psionics/abilities/psi_sword.dm
@@ -0,0 +1,32 @@
+/singleton/psionic_power/psi_sword
+ name = "Psi-Sword"
+ desc = "Create a psionic sword."
+ icon_state = "psiblade"
+ point_cost = 3
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/psi_sword
+
+/obj/item/spell/psi_sword
+ name = "psi-tool"
+ icon_state = "generic"
+ cast_methods = CAST_INNATE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 5
+
+/obj/item/spell/psi_sword/on_innate_cast(mob/user)
+ if(!ishuman(user))
+ return
+ . = ..()
+ if(!.)
+ return
+
+ var/mob/living/carbon/human/H = user
+
+ var/obj/item/psychic_power/psiblade/PT = new(user)
+ if(H.put_in_any_hand_if_possible(PT))
+ qdel(src)
+ return TRUE
+ else
+ to_chat(H, SPAN_WARNING("You need an empty hand for this!"))
+ return FALSE
diff --git a/code/modules/psionics/abilities/psi_tool.dm b/code/modules/psionics/abilities/psi_tool.dm
new file mode 100644
index 00000000000..a0f8388a0ff
--- /dev/null
+++ b/code/modules/psionics/abilities/psi_tool.dm
@@ -0,0 +1,32 @@
+/singleton/psionic_power/psi_tool
+ name = "Psi-Tool"
+ desc = "Create a psionic tool that can change into a screwdriver, crowbar, wrench or wirecutters."
+ icon_state = "psitool"
+ point_cost = 1
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/psi_tool
+
+/obj/item/spell/psi_tool
+ name = "psi-tool"
+ icon_state = "generic"
+ cast_methods = CAST_INNATE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 5
+
+/obj/item/spell/psi_tool/on_innate_cast(mob/user)
+ if(!ishuman(user))
+ return
+ . = ..()
+ if(!.)
+ return
+
+ var/mob/living/carbon/human/H = user
+
+ var/obj/item/psychic_power/tinker/PT = new(user)
+ if(H.put_in_any_hand_if_possible(PT))
+ qdel(src)
+ return TRUE
+ else
+ to_chat(H, SPAN_WARNING("You need an empty hand for this!"))
+ return FALSE
diff --git a/code/modules/psionics/abilities/pull.dm b/code/modules/psionics/abilities/pull.dm
new file mode 100644
index 00000000000..2e1931f5d36
--- /dev/null
+++ b/code/modules/psionics/abilities/pull.dm
@@ -0,0 +1,38 @@
+/singleton/psionic_power/pull
+ name = "Pull"
+ desc = "Pulls the target straight towards the user, smashing windows along the way. If it can be held in your hands, \
+ it goes straight to your hand. Note that you can catch items you pull to yourself if you toggle throw mode before pulling an item."
+ icon_state = "tech_passwall"
+ point_cost = 2
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/pull
+
+/obj/item/spell/pull
+ name = "pull"
+ desc = "Not as cool as the Mass Effect one."
+ icon_state = "control"
+ cast_methods = CAST_RANGED
+ aspect = ASPECT_PSIONIC
+ cooldown = 50
+ psi_cost = 10
+
+/obj/item/spell/pull/on_ranged_cast(atom/hit_atom, mob/user, bypass_psi_check)
+ if(!ismovable(hit_atom))
+ return
+ if(!ishuman(user))
+ return
+ . = ..()
+ if(!.)
+ return
+ var/atom/movable/AM = hit_atom
+ var/mob/living/carbon/human/H = user
+ if(isobj(hit_atom) && w_class < ITEMSIZE_IMMENSE)
+ if(length(get_line(hit_atom, user)))
+ if(H.put_in_any_hand_if_possible(hit_atom))
+ return
+ user.visible_message(SPAN_WARNING("[user] extends [user.get_pronoun("his")] hand at [hit_atom]and pulls!"), SPAN_WARNING("You mimic pulling at [hit_atom]!"))
+ if(ismob(hit_atom))
+ to_chat(hit_atom, SPAN_WARNING("A psychic force pulls you!"))
+ AM.throw_at(user, 10, 7)
+ playsound(user, 'sound/effects/psi/power_evoke.ogg')
+
diff --git a/code/modules/psionics/abilities/read_mind.dm b/code/modules/psionics/abilities/read_mind.dm
new file mode 100644
index 00000000000..ccc2d6f6025
--- /dev/null
+++ b/code/modules/psionics/abilities/read_mind.dm
@@ -0,0 +1,77 @@
+/singleton/psionic_power/read_mind
+ name = "Read Mind"
+ desc = "Rip thoughts from someone's mind. If your rank is Psionically Sensitive, you may only skim the surface thoughts from a person's mind. \
+ If your rank is Psionically Harmonious or above, your target is forced to respond to a five-word question with the truth."
+ icon_state = "tech_illusion"
+ spell_path = /obj/item/spell/read_mind
+ ability_flags = PSI_FLAG_EVENT|PSI_FLAG_CANON
+
+/obj/item/spell/read_mind
+ name = "read mind"
+ desc = "Rip thoughts from someone's mind."
+ icon_state = "generic"
+ cast_methods = CAST_MELEE|CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 50
+
+/obj/item/spell/read_mind/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
+ if(!.)
+ return
+ read_mind(hit_atom, user)
+
+/obj/item/spell/read_mind/proc/read_mind(atom/hit_atom, mob/user)
+ if(!isliving(hit_atom))
+ return
+
+ if(!isliving(user))
+ return
+
+ var/mob/living/target = hit_atom
+ if(target.stat == DEAD)
+ to_chat(user, SPAN_WARNING("Not even a psion of your level can speak to the dead."))
+ return
+
+ var/psi_blocked = target.is_psi_blocked()
+ if(psi_blocked)
+ to_chat(user, psi_blocked)
+ return
+
+ var/safe_mode = FALSE
+ var/mob/living/L = user
+ if(L.psi.get_rank() < PSI_RANK_HARMONIOUS)
+ safe_mode = TRUE
+
+ user.visible_message(SPAN_WARNING("[user] lays a palm on [hit_atom]'s forehead..."))
+ var/question
+ if(!safe_mode)
+ question = sanitize(input(user, "Ask your question.", "Read Mind") as null|text)
+ if((!safe_mode && !question) || user.incapacitated())
+ return TRUE
+
+ var/started_mindread = world.time
+ if(target.has_psi_aug())
+ to_chat(user, SPAN_NOTICE("Your psyche links with [target]'s psi-receiver, seeking [safe_mode ? "their surface thoughts." : "an answer from their mind's surface: [question]"]"))
+ to_chat(target, SPAN_NOTICE("[user]'s psyche links with your psi-receiver. [safe_mode ? "What are you thinking about, currently?" : "You cannot avoid the following question, and must answer truthfully: [question]"]"))
+ else
+ to_chat(user, SPAN_NOTICE("You dip your mentality into the surface layer of \the [target]'s mind, seeking an answer: [question]"))
+ to_chat(target, SPAN_NOTICE("Your mind is compelled to answer. [safe_mode ? "What are you thinking about, currently?" : "You cannot avoid the following question, and must answer truthfully: [question]"]"))
+ var/answer = sanitize(input(target, "[question]\n[safe_mode ? "You must answer with what you are currently thinking about." : "You must answer truthfully."]\nYou have 25 seconds to type a response.", "Read Mind") as null|text)
+ if(!answer || world.time > started_mindread + 25 SECONDS || user.stat != CONSCIOUS)
+ to_chat(user, SPAN_NOTICE("You receive nothing useful from \the [target]."))
+ to_chat(target, SPAN_NOTICE("Your mind blanks out momentarily."))
+ else
+ if(safe_mode)
+ to_chat(user, SPAN_NOTICE("You skim the first thoughts in [target]'s mind: [answer]"))
+ else
+ to_chat(user, SPAN_NOTICE("You pry the answer to your question from [target]'s mind: [answer]"))
+ msg_admin_attack("[key_name(user)] read mind of [key_name(target)] [safe_mode ? "skimming their surface thoughts" : "forcing them to answer truthfully with question \"[question]\""] and [answer?"got answer \"[answer]\".":"got no answer."]")
+ if(safe_mode)
+ target.confused += 15
+ target.adjustBrainLoss(10)
+ to_chat(target, SPAN_WARNING("You feel somewhat nauseated, and a headache's come up too..."))
+ else
+ target.adjustBrainLoss(20)
+ target.confused += 20
+ to_chat(target, SPAN_DANGER("Your head feels like it's going to explode, and you feel nauseated..."))
diff --git a/code/modules/psionics/abilities/rejuvenate.dm b/code/modules/psionics/abilities/rejuvenate.dm
new file mode 100644
index 00000000000..ab92f713ad9
--- /dev/null
+++ b/code/modules/psionics/abilities/rejuvenate.dm
@@ -0,0 +1,38 @@
+/singleton/psionic_power/rejuvenate
+ name = "Rejuvenate"
+ desc = "Restore a creature's blood."
+ icon_state = "tech_oxygenate"
+ point_cost = 1
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/rejuvenate
+
+/obj/item/spell/rejuvenate
+ name = "rejuvenate"
+ desc = "Blood bag who?"
+ icon_state = "mend_life"
+ cast_methods = CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ cooldown = 50
+ psi_cost = 30
+
+/obj/item/spell/rejuvenate/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ if(!ishuman(hit_atom))
+ return
+
+ var/mob/living/carbon/human/target = hit_atom
+ if(!target.has_zona_bovinae())
+ to_chat(user, SPAN_WARNING("Psionic power cannot flow through this being."))
+ return
+
+ if(target.stat == DEAD || target.status_flags & FAKEDEATH)
+ to_chat(user, SPAN_WARNING("Psionic power does not flow through a dead person."))
+ return
+
+ . = ..()
+ if(!.)
+ return
+
+ user.visible_message(SPAN_NOTICE("[user] lays both palms on [target]..."), SPAN_NOTICE("You lay your palms on [target] and get to work."))
+ if(do_mob(user, target, 10 SECONDS))
+ target.restore_blood()
+ user.visible_message(SPAN_NOTICE("[user] removes their palms from [target]."), SPAN_NOTICE("You remove your palms from [target], having restored their blood."))
diff --git a/code/modules/psionics/abilities/rend.dm b/code/modules/psionics/abilities/rend.dm
new file mode 100644
index 00000000000..58bc090a6e8
--- /dev/null
+++ b/code/modules/psionics/abilities/rend.dm
@@ -0,0 +1,64 @@
+/singleton/psionic_power/rend
+ name = "Rend"
+ desc = "Rend an adjacent target's biomolecular state apart. Very powerful, but with an extremely long cooldown and a huge psi-stamina cost. \
+ Activate it in your hand to switch to a structure mode, in which you cannot target living beings (synthetics included) but you can tear apart \
+ walls and airlocks much faster."
+ icon_state = "gen_dissolve"
+ point_cost = 3
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/rend
+
+/obj/item/spell/rend
+ name = "rend"
+ icon_state = "chain_lightning"
+ cast_methods = CAST_USE|CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ force = 40
+ armor_penetration = 30
+ cooldown = 30
+ psi_cost = 35
+ attack_verb = list("rends apart", "disintegrates")
+ hitsound = 'sound/weapons/heavysmash.ogg'
+ flags = 0
+ var/structure_mode = FALSE
+
+/obj/item/spell/rend/on_use_cast(mob/user, bypass_psi_check)
+ . = ..(user, TRUE)
+ structure_mode = !structure_mode
+ if(structure_mode)
+ to_chat(user, SPAN_WARNING("You reconfigure your rending to damage structures. It can no longer be used on organics or synthetics, but it is much cheaper \
+ to damage structures with."))
+ maptext = SMALL_FONTS(7, "S")
+ psi_cost = 15
+ else
+ to_chat(user, SPAN_WARNING("You reconfigure your rending to damage organics again."))
+ maptext = SMALL_FONTS(7, "O")
+ psi_cost = initial(psi_cost)
+
+/obj/item/spell/rend/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ if(structure_mode && isliving(hit_atom))
+ to_chat(user, SPAN_WARNING("Your rending is not configured for organics!"))
+ return
+ . = ..()
+ if(!.)
+ return
+ if(structure_mode)
+ if(iswall(hit_atom))
+ var/turf/simulated/wall/W = hit_atom
+ var/base_time = 5 SECONDS
+ if(W.reinf_material)
+ base_time += (W.reinf_material.hardness / 10) SECONDS
+ user.visible_message(SPAN_WARNING("[user] lays [user.get_pronoun("his")] palms on \the [W] and begins discharging psionic energy on it..."),
+ SPAN_WARNING("You lay your palms on \the [W] and begin permeating psionic energy through its structure..."))
+ if(do_after(user, base_time))
+ user.visible_message(SPAN_WARNING("[user] disintegrates \the [W]!"), SPAN_WARNING("You disintegrate \the [W]!"))
+ W.dismantle_wall(TRUE, FALSE, TRUE)
+ else if(isairlock(hit_atom))
+ var/obj/machinery/door/airlock/A = hit_atom
+ var/base_time = (round(A.maxhealth / 60)) SECONDS
+ user.visible_message(SPAN_WARNING("[user] lays [user.get_pronoun("his")] palms on \the [A] and begins discharging psionic energy on it..."),
+ SPAN_WARNING("You lay your palms on \the [A] and begin permeating psionic energy through its structure..."))
+ if(do_after(user, base_time))
+ user.visible_message(SPAN_WARNING("[user] disintegrates \the [A]!"), SPAN_WARNING("You disintegrate \the [A]!"))
+ playsound(A, 'sound/effects/meteorimpact.ogg')
+ qdel(A)
diff --git a/code/modules/psionics/abilities/shockwave.dm b/code/modules/psionics/abilities/shockwave.dm
new file mode 100644
index 00000000000..15c1c3b099c
--- /dev/null
+++ b/code/modules/psionics/abilities/shockwave.dm
@@ -0,0 +1,42 @@
+/singleton/psionic_power/shockwave
+ name = "Shockwave"
+ desc = "Create a wave of telekinetic energy to pummel the ground in a straight line in the direction you're facing. \
+ Anyone caught in it falls over unless wearing magboots."
+ icon_state = "tech_haste"
+ point_cost = 2
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/shockwave
+
+/obj/item/spell/shockwave
+ name = "shockwave"
+ icon_state = "flame_tongue"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 50
+ psi_cost = 20
+
+/obj/item/spell/shockwave/on_use_cast(mob/user, bypass_psi_check)
+ . = ..()
+ if(!.)
+ return
+
+ var/user_dir = user.dir
+ var/turf/T = get_turf(user)
+ if(isspaceturf(T))
+ to_chat(user, SPAN_WARNING("You charge your shockwave, slam your foot down... and then remember that you're in space."))
+ return
+ user.visible_message(SPAN_DANGER(FONT_HUGE("[user] charges [user.get_pronoun("his")] foot with psionic energy and slams it down!")),
+ SPAN_DANGER(FONT_HUGE("You charge your foot with psionic energy and slam it down!")))
+ playsound(T, 'sound/effects/meteorimpact.ogg', 100)
+ for(var/mob/M in get_hearers_in_view(7, src))
+ shake_camera(M, 5, 5)
+ for(var/i = 1 to 5)
+ T = get_step(T, user_dir)
+ if(isspaceturf(T))
+ break
+ for(var/mob/living/M in T)
+ if(M.Check_Shoegrip())
+ continue
+ M.apply_effect(3, WEAKEN)
+ M.flash_pain(20)
+ to_chat(M, SPAN_DANGER("You get caught in the shockwave and fall down!"))
diff --git a/code/modules/psionics/abilities/singularity.dm b/code/modules/psionics/abilities/singularity.dm
new file mode 100644
index 00000000000..12ca8cc9377
--- /dev/null
+++ b/code/modules/psionics/abilities/singularity.dm
@@ -0,0 +1,59 @@
+/singleton/psionic_power/singularity
+ name = "Singularity"
+ desc = "Creates a psionic illusion. Anyone within four tiles of it is forced to walk towards it. It will dissipate after ten seconds."
+ icon_state = "tech_energysiphon"
+ point_cost = 2
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/singularity
+
+/obj/item/spell/singularity
+ name = "singularity"
+ icon_state = "destablize"
+ cast_methods = CAST_RANGED
+ aspect = ASPECT_PSIONIC
+ cooldown = 200
+ psi_cost = 20
+ var/obj/effect/singularity/singularity
+
+/obj/item/spell/singularity/Destroy()
+ QDEL_NULL(singularity)
+ return ..()
+
+/obj/item/spell/singularity/on_ranged_cast(atom/hit_atom, mob/user, bypass_psi_check)
+ if(!QDELETED(singularity))
+ to_chat(user, SPAN_WARNING("You can't make more than one singularity!"))
+ return
+ . = ..()
+ if(!.)
+ return
+
+ var/obj/effect/singularity/S = new(get_turf(hit_atom))
+ S.creator = user
+ singularity = S
+ playsound(get_turf(user), 'sound/effects/fingersnap.ogg')
+ user.visible_message(SPAN_DANGER(FONT_HUGE("[user] snaps [user.get_pronoun("his")] fingers and generates a hole of psionic energy!")),
+ SPAN_DANGER("You snap your fingers and generate a vortex of psionic energy."))
+
+/obj/effect/singularity
+ name = "psionic singularity"
+ icon_state = "bluestream_fade"
+ var/mob/living/creator
+
+/obj/effect/singularity/process()
+ for(var/mob/living/M in get_hearers_in_LOS(4, src))
+ if(M == creator)
+ continue
+ to_chat(M, SPAN_DANGER("Your eyes can't move away from \the [src]... you feel compelled to move towards it!"))
+ M.AdjustStunned(1)
+ step_towards(M, src)
+
+/obj/effect/singularity/Initialize(mapload, ...)
+ . = ..()
+ QDEL_IN(src, 10 SECONDS)
+ START_PROCESSING(SSprocessing, src)
+
+/obj/effect/singularity/Destroy()
+ visible_message(SPAN_WARNING("\The [src] dissipates harmlessly with a light 'fwoosh' sound."))
+ creator = null
+ STOP_PROCESSING(SSprocessing, src)
+ return ..()
diff --git a/code/modules/psionics/abilities/skinsight.dm b/code/modules/psionics/abilities/skinsight.dm
new file mode 100644
index 00000000000..1966dbe4794
--- /dev/null
+++ b/code/modules/psionics/abilities/skinsight.dm
@@ -0,0 +1,67 @@
+/singleton/psionic_power/skinsight
+ name = "Skinsight"
+ desc = "Get information on a creature's vitals. Activate in your hand to switch between a normal mode or a slower, more detailed mode. Note that the body scan mode \
+ is not 100% accurate with the numbers given."
+ icon_state = "wiz_blind"
+ point_cost = 1
+ ability_flags = PSI_FLAG_EVENT|PSI_FLAG_CANON
+ spell_path = /obj/item/spell/skinsight
+
+/obj/item/spell/skinsight
+ name = "skinsight"
+ desc = "A health analyzer, but cooler."
+ icon_state = "blink"
+ cast_methods = CAST_MELEE|CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 10
+ var/body_scan_mode = FALSE
+
+/obj/item/spell/skinsight/on_use_cast(mob/user)
+ . = ..()
+ body_scan_mode = !body_scan_mode
+ if(body_scan_mode)
+ psi_cost = 30
+ to_chat(user, SPAN_NOTICE("You will now use more psionic energy for a more detailed readout."))
+ else
+ psi_cost = 10
+ to_chat(user, SPAN_NOTICE("You will now use less psionic energy for a more standard readout."))
+
+/obj/item/spell/skinsight/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ if(!ishuman(hit_atom))
+ return
+
+ var/mob/living/carbon/human/target = hit_atom
+ if(!target.has_zona_bovinae())
+ to_chat(user, SPAN_WARNING("Psionic power cannot flow through this being."))
+ return
+
+ if(!isliving(user))
+ return
+
+ if(target.stat == DEAD)
+ to_chat(user, SPAN_WARNING("The total lack of psionic energy flowing through this person can only mean one thing: they are dead."))
+ return
+
+ . = ..()
+ if(!.)
+ return
+
+ if(!body_scan_mode)
+ user.visible_message(SPAN_NOTICE("[user] passes [user.get_pronoun("his")] hand over [target]."), SPAN_NOTICE("You pass your hand over [target]."))
+ health_scan_mob(target, user, TRUE, TRUE)
+ else
+ user.visible_message(SPAN_NOTICE("[user] slowly passes [user.get_pronoun("his")] hand over [target]..."),
+ SPAN_NOTICE("You slowly pass your hand over [target]..."))
+ if(do_mob(user, target, 10 SECONDS))
+ for(var/obj/item/organ/internal/I in target.internal_organs)
+ if(I.is_bruised())
+ to_chat(user, SPAN_WARNING("You notice some irregularity in the psionic flow through their [I]."))
+ if(I.is_broken())
+ to_chat(user, SPAN_WARNING("Your psionic flow through their [I] is highly irregular."))
+ if(I.damage)
+ to_chat(user, SPAN_WARNING("You are sure there is some damage in their [I]."))
+ var/pulse = target.get_pulse()
+ to_chat(user, SPAN_NOTICE("Their pulse is [pulse]."))
+ var/oxygenation = max(min(target.get_blood_oxygenation() + rand(-10, 10), 100), 0)
+ to_chat(user, SPAN_NOTICE("Their oxygenation is more or less [oxygenation]%."))
diff --git a/code/modules/psionics/abilities/spasm.dm b/code/modules/psionics/abilities/spasm.dm
new file mode 100644
index 00000000000..0cc7e961083
--- /dev/null
+++ b/code/modules/psionics/abilities/spasm.dm
@@ -0,0 +1,30 @@
+/singleton/psionic_power/spasm
+ name = "Spasm"
+ desc = "Force a target to drop the items in its hands. Note that this has a hefty power use and cooldown."
+ icon_state = "genetics_closed"
+ point_cost = 1
+ ability_flags = PSI_FLAG_EVENT
+ spell_path = /obj/item/spell/spasm
+
+/obj/item/spell/spasm
+ name = "spasm"
+ desc = "Made you drop your gun."
+ icon_state = "control"
+ cast_methods = CAST_RANGED
+ aspect = ASPECT_PSIONIC
+ cooldown = 100
+ /// This looks like a lot, but this ability is really strong.
+ psi_cost = 20
+
+/obj/item/spell/spasm/on_ranged_cast(atom/hit_atom, mob/user, bypass_psi_check)
+ if(!ishuman(hit_atom))
+ to_chat(user, SPAN_WARNING("That won't work!"))
+ return
+ . = ..()
+ if(!.)
+ return
+ var/mob/living/carbon/human/H = hit_atom
+ H.drop_l_hand()
+ H.drop_r_hand()
+ to_chat(H, SPAN_DANGER("Your mind is filled by an acute pain making you drop what you're holding!"))
+ H.flash_pain(50)
diff --git a/code/modules/psionics/abilities/stasis.dm b/code/modules/psionics/abilities/stasis.dm
new file mode 100644
index 00000000000..5373d2a9871
--- /dev/null
+++ b/code/modules/psionics/abilities/stasis.dm
@@ -0,0 +1,58 @@
+/singleton/psionic_power/stasis
+ name = "Stasis"
+ desc = "Condenses the Nlom field around one person at a time. This immobilises them like an energy net and also applies stasis to them."
+ icon_state = "gen_ice"
+ point_cost = 2
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/stasis
+
+/obj/item/spell/stasis
+ name = "stasis"
+ desc = "Almost as cool as the Mass Effect one."
+ icon_state = "overload"
+ cast_methods = CAST_RANGED
+ aspect = ASPECT_PSIONIC
+ cooldown = 100
+ psi_cost = 20
+ var/obj/effect/energy_net/our_net
+
+/obj/item/spell/stasis/Destroy()
+ qdel(our_net)
+ our_net = null
+ return ..()
+
+/obj/item/spell/stasis/on_ranged_cast(atom/hit_atom, mob/user, bypass_psi_check)
+ if(!isliving(hit_atom))
+ return
+ if(!QDELETED(our_net))
+ to_chat(user, SPAN_WARNING("You can't target more than one person with this!"))
+ return
+ . = ..()
+ if(!.)
+ return
+ var/mob/living/M = hit_atom
+ var/obj/effect/energy_net/EN = locate() in get_turf(M)
+ if(EN)
+ qdel(EN)
+
+ var/turf/T = get_turf(M)
+ if(T)
+ var/obj/effect/energy_net/stasis/net = new(T)
+ net.layer = M.layer + 1
+ M.captured = TRUE
+ M.update_canmove()
+ net.affecting = M
+ our_net = net
+ M.visible_message(SPAN_DANGER("A field of condensed Nlom appears around [M]!"), SPAN_DANGER("You are immobilised by a field of condensed Nlom! You feel your body and mind slow down..."))
+
+/obj/effect/energy_net/stasis
+ name = "condensed nlom field"
+ desc = "A crystallized field of pure Nlom energy."
+ icon_state = "shield2"
+ health = 100
+
+/obj/effect/energy_net/stasis/process()
+ . = ..()
+ if(iscarbon(affecting))
+ var/mob/living/carbon/C = affecting
+ C.SetStasis(10)
diff --git a/code/modules/psionics/abilities/throw.dm b/code/modules/psionics/abilities/throw.dm
new file mode 100644
index 00000000000..651b82a2b20
--- /dev/null
+++ b/code/modules/psionics/abilities/throw.dm
@@ -0,0 +1,93 @@
+/singleton/psionic_power/throw_item
+ name = "Throw"
+ desc = "Click this spell with an item to prepare it to be thrown, then use the spell to throw the item with lethal force."
+ icon_state = "wiz_mm"
+ point_cost = 3
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/projectile/throw_item
+
+/obj/item/spell/projectile/throw_item
+ name = "throw"
+ desc = "Almost as cool as the Mass Effect one, but still not as cool."
+ icon_state = "aspect_bolt"
+ cast_methods = CAST_RANGED
+ aspect = ASPECT_PSIONIC
+ spell_projectile = /obj/item/projectile/psionic_throw
+ fire_sound = 'sound/weapons/wave.ogg'
+ cooldown = 20
+ psi_cost = 20
+ var/obj/item_to_throw
+
+/obj/item/spell/projectile/throw_item/attackby(obj/item/W, mob/user)
+ . = ..()
+ if(!item_to_throw)
+ owner.drop_item(W)
+ W.forceMove(src)
+ to_chat(user, SPAN_NOTICE("You imbue [W] with psionic energy!"))
+ add_overlay("controlled")
+ item_to_throw = W
+
+/obj/item/spell/projectile/throw_item/Destroy()
+ if(item_to_throw)
+ item_to_throw.forceMove(loc)
+ item_to_throw = null
+ return ..()
+
+/obj/item/spell/projectile/throw_item/on_use_cast(mob/user)
+ . = ..(user, TRUE)
+ if(item_to_throw)
+ owner.put_in_hands(item_to_throw)
+ item_to_throw = null
+ cut_overlays()
+
+/obj/item/spell/projectile/throw_item/on_ranged_cast(atom/hit_atom, mob/living/user, atom/pb_target)
+ if(!item_to_throw)
+ to_chat(user, SPAN_WARNING("You need to imbue an item with psionic energy first!"))
+ return
+ . = ..()
+ item_to_throw = null
+ cut_overlays()
+
+/obj/item/spell/projectile/throw_item/make_projectile(obj/item/projectile/projectile_type, mob/living/user)
+ var/obj/item/projectile/psionic_throw/P = ..()
+ if(P && item_to_throw)
+ var/base_damage = item_to_throw.force > 5 ? item_to_throw.force : 10
+ /// Why is it called a fucking divisor? It's a multiplier...
+ var/thrown_force_divisor = 1
+ if(istype(item_to_throw, /obj/item/material/twohanded))
+ var/obj/item/material/twohanded/MT = item_to_throw
+ base_damage = MT.force_wielded > MT.force ? MT.force_wielded : force
+ thrown_force_divisor = MT.thrown_force_divisor
+ /// There are different throwforce calculations, which means that we have to do this manually.
+ if(base_damage < (item_to_throw.throwforce * thrown_force_divisor))
+ base_damage = item_to_throw.throwforce * thrown_force_divisor
+ var/base_armor_penetration
+ if(item_to_throw.armor_penetration > 2)
+ base_armor_penetration = item_to_throw.armor_penetration
+ else
+ base_armor_penetration = 2
+ P.name = item_to_throw.name
+ P.appearance = item_to_throw.appearance
+ P.damage = base_damage * (item_to_throw.w_class / 2)
+ P.armor_penetration = base_armor_penetration * item_to_throw.w_class
+ /// We need to do this again because we're overriding the base make_projectile.
+ if(owner.psi.get_rank() >= PSI_RANK_APEX)
+ if(P.damage)
+ P.damage *= 1.1
+ P.armor_penetration *= 1.1
+ P.damage_flags |= item_to_throw.damage_flags()
+ P.damage_type = item_to_throw.damtype
+ P.source_item = item_to_throw
+ return P
+
+/obj/item/projectile/psionic_throw
+ name = "thrown projectile"
+ damage_type = DAMAGE_BRUTE
+ impact_sounds = list(BULLET_IMPACT_MEAT = SOUNDS_BULLET_MEAT, BULLET_IMPACT_METAL = SOUNDS_BULLET_METAL)
+ var/obj/source_item
+
+/obj/item/projectile/psionic_throw/Destroy()
+ if(source_item)
+ source_item.forceMove(loc)
+ source_item = null
+ return ..()
diff --git a/code/modules/psionics/abilities/time_stop.dm b/code/modules/psionics/abilities/time_stop.dm
new file mode 100644
index 00000000000..b8907f8e906
--- /dev/null
+++ b/code/modules/psionics/abilities/time_stop.dm
@@ -0,0 +1,47 @@
+/singleton/psionic_power/time_stop
+ name = "Time Stop"
+ desc = "Freeze living beings around you in a 5x5 area. This ability is very expensive, so be careful."
+ icon_state = "tech_control"
+ point_cost = 2
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/time_stop
+
+/obj/item/spell/time_stop
+ name = "nlom eyes"
+ desc = "Psionic drugs? No way."
+ icon_state = "track"
+ cast_methods = CAST_USE
+ aspect = ASPECT_PSIONIC
+ cooldown = 10
+ psi_cost = 30
+
+/obj/item/spell/time_stop/Destroy()
+ for(var/mob/living/L in get_hearers_in_view(5, owner))
+ if(L == owner)
+ continue
+ to_chat(L, SPAN_DANGER("Time around you returns to normal!"))
+ L.stunned = 0
+ L.silent = 0
+ return ..()
+
+/obj/item/spell/time_stop/on_use_cast(mob/user)
+ . = ..()
+ if(!.)
+ return
+
+ if(do_after(user, 1 SECOND))
+ user.visible_message(SPAN_DANGER(FONT_HUGE("[user] extends [user.get_pronoun("his")] arms to [user.get_pronoun("his")] sides!")),
+ SPAN_DANGER("You extend your arms to your side and crystallize the Nlom around you!"))
+ time_stop(user)
+
+/obj/item/spell/time_stop/proc/time_stop(mob/living/user)
+ for(var/mob/living/L in get_hearers_in_view(5, user))
+ if(L == user)
+ continue
+ to_chat(L, SPAN_DANGER("Time around you slows down to a crawl..."))
+ L.AdjustStunned(5)
+ L.silent += 30
+
+ if(do_after(user, 1 SECOND))
+ if(user.psi.spend_power(20))
+ time_stop(user)
diff --git a/code/modules/psionics/abilities/warp.dm b/code/modules/psionics/abilities/warp.dm
new file mode 100644
index 00000000000..2c379bd654c
--- /dev/null
+++ b/code/modules/psionics/abilities/warp.dm
@@ -0,0 +1,75 @@
+/singleton/psionic_power/warp
+ name = "Warp"
+ desc = "Warp through objects."
+ icon_state = "tech_blink"
+ point_cost = 4
+ ability_flags = PSI_FLAG_ANTAG
+ spell_path = /obj/item/spell/warp
+
+/obj/item/spell/warp
+ name = "warp"
+ icon_state = "warp_strike"
+ cast_methods = CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ cooldown = 50
+ psi_cost = 10
+ var/maximum_distance = 20 //Measured in tiles.
+ var/busy = FALSE
+
+/obj/item/spell/warp/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ . = ..()
+ if(!.)
+ return
+ if(busy) //Prevent someone from trying to get two uses of the spell from one instance.
+ return FALSE
+ if(!allowed_to_teleport())
+ to_chat(user, SPAN_WARNING("You can't teleport here."))
+ return FALSE
+ var/turf/T = get_turf(hit_atom) //Turf we touched.
+ var/turf/our_turf = get_turf(user) //Where we are.
+ if(!T.density)
+ if(!T.check_density())
+ to_chat(user, SPAN_WARNING("This spell is for solid objects."))
+ return FALSE
+ var/direction = get_dir(our_turf, T)
+ var/turf/checked_turf = T //Turf we're currently checking for density in the loop below.
+ var/turf/found_turf = null //Our destination, if one is found.
+ var/i = maximum_distance
+
+ visible_message(SPAN_NOTICE("[user] rests a hand on \the [hit_atom]."))
+ busy = TRUE
+
+ spark(our_turf, 3, cardinal)
+
+ while(i)
+ checked_turf = get_step(checked_turf, direction) //Advance in the given direction
+ i--
+
+ if(!checked_turf.density) //If we found a destination (a non-dense turf), then we can stop.
+ var/dense_objs_on_turf = 0
+ for(var/atom/movable/stuff in checked_turf.contents) //Make sure nothing dense is where we want to go, like an airlock or window.
+ if(stuff.density)
+ dense_objs_on_turf = 1
+
+ if(!dense_objs_on_turf) //If we found a non-dense turf with nothing dense on it, then that's our destination.
+ found_turf = checked_turf
+ break
+ sleep(10)
+
+ if(found_turf)
+ if(user.loc != our_turf)
+ to_chat(user, SPAN_WARNING("You need to stand still in order to phase through \the [hit_atom]."))
+ return FALSE
+ if(!user.incapacitated())
+ visible_message(SPAN_WARNING("[user] appears to phase through \the [hit_atom]!"))
+ to_chat(user, SPAN_NOTICE("You find a destination on the other side of \the [hit_atom], and phase through it."))
+ spark(src, 5, 0)
+ user.forceMove(found_turf)
+ qdel(src)
+ return TRUE
+ else
+ to_chat(user, SPAN_WARNING("You don't have enough energy to phase through these walls!"))
+ busy = FALSE
+ else
+ to_chat(user, SPAN_NOTICE("You weren't able to find an open space to go to."))
+ busy = FALSE
diff --git a/code/modules/psionics/abilities/zona_absorption.dm b/code/modules/psionics/abilities/zona_absorption.dm
new file mode 100644
index 00000000000..e225ea951d7
--- /dev/null
+++ b/code/modules/psionics/abilities/zona_absorption.dm
@@ -0,0 +1,41 @@
+/singleton/psionic_power/zona_absorption
+ name = "Zona Bovinae Absorption"
+ desc = "Absorb a psionic energy from a being's Zona Bovinae, granting you an extra point to be used in the Point Shop."
+ icon_state = "tech_shield_old"
+ point_cost = 0
+ ability_flags = PSI_FLAG_SPECIAL
+ spell_path = /obj/item/spell/zona_absorption
+
+/obj/item/spell/zona_absorption
+ name = "zona bovinae absorption"
+ icon_state = "chain_lightning"
+ cast_methods = CAST_MELEE
+ aspect = ASPECT_PSIONIC
+ cooldown = 30
+ psi_cost = 10
+
+/obj/item/spell/zona_absorption/on_melee_cast(atom/hit_atom, mob/living/user, def_zone)
+ if(!isliving(hit_atom))
+ return
+ var/mob/living/L = hit_atom
+ if(!L.has_zona_bovinae())
+ to_chat(user, SPAN_WARNING("This being doesn't have a Zona Bovinae."))
+ return
+ if(HAS_TRAIT(L, TRAIT_ZONA_BOVINAE_ABSORBED))
+ to_chat(user, SPAN_WARNING("You already absorbed points from this creature!"))
+ return
+ . = ..()
+ if(!.)
+ return
+
+ user.visible_message(SPAN_DANGER("[user] grabs [L]'s head..."), SPAN_DANGER("You grab [L]'s head..."))
+ if(do_mob(user, L, 5 SECONDS))
+ L.flash_pain(20)
+ L.adjustHalLoss(20)
+ playsound(get_turf(L), 'sound/effects/psi/power_feedback.ogg', 100)
+ user.visible_message(SPAN_DANGER("[user] absorbs tendrils of psionic energy from [L]!"),
+ SPAN_DANGER("You absorb psionic energy from [L], granting you an extra Psionic Point Shop point."))
+ var/mob/living/M = user
+ M.psi.psi_points++
+ to_chat(L, SPAN_DANGER("A splitting headache courses through your mind! Your head feels a bit lighter..."))
+ ADD_TRAIT(L, TRAIT_ZONA_BOVINAE_ABSORBED, TRAIT_SOURCE_PSIONICS)
diff --git a/code/modules/psionics/complexus/complexus.dm b/code/modules/psionics/complexus/complexus.dm
index 617a4a66c57..9acfe6b1427 100644
--- a/code/modules/psionics/complexus/complexus.dm
+++ b/code/modules/psionics/complexus/complexus.dm
@@ -1,35 +1,28 @@
/datum/psi_complexus
var/announced = FALSE // Whether or not we have been announced to our holder yet.
- var/suppressed = TRUE // Whether or not we are suppressing our psi powers.
+ var/suppressed = FALSE // Whether or not we are suppressing our psi powers.
var/use_psi_armor = TRUE // Whether or not we should automatically deflect/block incoming damage.
- var/rebuild_power_cache = TRUE // Whether or not we need to rebuild our cache of psi powers.
- var/rating = 0 // Overall psi rating.
var/cost_modifier = 1 // Multiplier for power use stamina costs.
var/stun = 0 // Number of process ticks we are stunned for.
var/next_power_use = 0 // world.time minimum before next power use.
- var/stamina = 50 // Current psi pool.
- var/max_stamina = 50 // Max psi pool.
+ var/stamina = 100 // Current psi pool.
+ var/max_stamina = 100 // Max psi pool.
+ var/psi_points = 0 // Points spendable in the psi pointshop.
+ var/spent_psi_points = 0 // Need to keep track of if someone has spent psi-points before for things like updating psi-rank.
- var/list/latencies // List of all currently latent faculties.
- var/list/ranks // Assoc list of psi faculties to current rank.
- var/list/base_ranks // Assoc list of psi faculties to base rank, in case reset is needed
+ var/psionic_rank
+ var/last_psionic_rank
var/list/manifested_items // List of atoms manifested/maintained by psychic power.
- var/next_latency_trigger = 0 // world.time minimum before a trigger can be attempted again.
+ var/list/psionic_powers = list() // List of singleton abilities.
var/last_armor_check // world.time of last armor check.
var/last_aura_size
var/last_aura_alpha
var/last_aura_color
var/aura_color = "#ff0022"
- // Cached powers.
- var/list/melee_powers // Powers used in melee range.
- var/list/grab_powers // Powers use by using a grab.
- var/list/ranged_powers // Powers used at range.
- var/list/manifestation_powers // Powers that create an item.
- var/list/powers_by_faculty // All powers within a given faculty.
-
+ var/datum/component/armor/psionic/armor_component
var/obj/screen/psi/hub/ui // Reference to the master psi UI object.
var/mob/living/owner // Reference to our owner.
var/image/_aura_image // Client image
@@ -70,13 +63,17 @@
/datum/psi_complexus/New(var/mob/_owner)
owner = _owner
+ SSpsi.all_psi_complexes |= src
START_PROCESSING(SSpsi, src)
/datum/psi_complexus/Destroy()
destroy_aura_image(_aura_image)
+ SSpsi.all_psi_complexes -= src
+ QDEL_NULL(armor_component)
STOP_PROCESSING(SSpsi, src)
if(owner)
- cancel()
+ if(owner.ability_master)
+ owner.ability_master.remove_all_psionic_abilities()
if(owner.client)
owner.client.screen -= ui
for(var/thing in SSpsi.all_aura_images)
@@ -89,4 +86,5 @@
for(var/thing in manifested_items)
qdel(thing)
manifested_items.Cut()
+
. = ..()
diff --git a/code/modules/psionics/complexus/complexus_helpers.dm b/code/modules/psionics/complexus/complexus_helpers.dm
index d7c81930cde..f635f483254 100644
--- a/code/modules/psionics/complexus/complexus_helpers.dm
+++ b/code/modules/psionics/complexus/complexus_helpers.dm
@@ -1,38 +1,28 @@
-/datum/psi_complexus/proc/cancel()
- sound_to(owner, sound('sound/effects/psi/power_fail.ogg'))
- if(LAZYLEN(manifested_items))
- for(var/thing in manifested_items)
- owner.drop_from_inventory(thing)
- qdel(thing)
- manifested_items = null
-
/datum/psi_complexus/proc/stunned(var/amount)
var/old_stun = stun
stun = max(stun, amount)
if(amount && !old_stun)
to_chat(owner, "Your concentration has been shattered! You cannot focus your psi power!")
ui.update_icon()
- cancel()
/datum/psi_complexus/proc/get_armor(var/armortype)
if(can_use_passive())
last_armor_check = world.time
- return round(Clamp(Clamp(4 * rating, 0, 20) * get_rank(SSpsi.armor_faculty_by_type[armortype]), 0, 100) * (stamina/max_stamina))
+ return round(Clamp(Clamp(4 * get_rank(), 0, 20) * get_rank() / 2, 0, 100) * (stamina/max_stamina))
else
last_armor_check = 0
return 0
-/datum/psi_complexus/proc/get_rank(var/faculty)
- return LAZYACCESS(ranks, faculty)
-
-/datum/psi_complexus/proc/set_rank(var/faculty, var/rank, var/defer_update, var/temporary)
- if(get_rank(faculty) != rank)
- LAZYSET(ranks, faculty, rank)
- if(!temporary)
- LAZYSET(base_ranks, faculty, rank)
+/datum/psi_complexus/proc/set_rank(var/rank, var/defer_update)
+ if(get_rank() != rank)
+ last_psionic_rank = psionic_rank
+ psionic_rank = rank
if(!defer_update)
update()
+/datum/psi_complexus/proc/get_rank()
+ return psionic_rank
+
/datum/psi_complexus/proc/set_cooldown(var/value)
next_power_use = world.time + value
ui.update_icon()
@@ -80,11 +70,13 @@
stunned(value * 2)
set_cooldown(value * 100)
- if(prob(value*10)) owner.emote("scream")
+ if(prob(value*10))
+ owner.emote("scream")
// Your head asplode.
owner.adjustBrainLoss(value)
owner.adjustHalLoss(value * 25) //Ouch.
+ owner.psi.hide_auras()
if(ishuman(owner))
var/mob/living/carbon/human/pop = owner
if(pop.should_have_organ(BP_BRAIN))
@@ -97,8 +89,6 @@
/datum/psi_complexus/proc/reset()
aura_color = initial(aura_color)
- ranks = base_ranks ? base_ranks.Copy() : null
max_stamina = initial(max_stamina)
stamina = min(stamina, max_stamina)
- cancel()
update()
diff --git a/code/modules/psionics/complexus/complexus_latency.dm b/code/modules/psionics/complexus/complexus_latency.dm
index 6d9368f67f9..5637a9befa7 100644
--- a/code/modules/psionics/complexus/complexus_latency.dm
+++ b/code/modules/psionics/complexus/complexus_latency.dm
@@ -1,17 +1,6 @@
-/datum/psi_complexus/proc/check_latency_trigger(var/trigger_strength = 0, var/source, var/redactive = FALSE)
-
- if(!LAZYLEN(latencies) || world.time < next_latency_trigger)
- return FALSE
-
- if(!prob(trigger_strength))
- next_latency_trigger = world.time + rand(100, 300)
- return FALSE
-
- var/faculty = pick(latencies)
+/datum/psi_complexus/proc/check_psionic_trigger(var/trigger_strength = 0, var/source, var/redactive = FALSE)
var/new_rank = rand(2,5)
- owner.set_psi_rank(faculty, new_rank)
- var/datum/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty)
- to_chat(owner, SPAN_DANGER("You scream internally as your [faculty_decl.name] faculty is forced into operancy by [source]!"))
- next_latency_trigger = world.time + rand(600, 1800) * new_rank
+ owner.set_psi_rank(new_rank)
+ to_chat(owner, SPAN_DANGER("You scream internally as your psionics are forced into operancy by [source]!"))
if(!redactive) owner.adjustBrainLoss(rand(trigger_strength * 2, trigger_strength * 4))
return TRUE
diff --git a/code/modules/psionics/complexus/complexus_power_cache.dm b/code/modules/psionics/complexus/complexus_power_cache.dm
deleted file mode 100644
index 47cfccd6658..00000000000
--- a/code/modules/psionics/complexus/complexus_power_cache.dm
+++ /dev/null
@@ -1,49 +0,0 @@
-/datum/psi_complexus/proc/rebuild_power_cache()
- if(rebuild_power_cache)
-
- melee_powers = list()
- grab_powers = list()
- ranged_powers = list()
- manifestation_powers = list()
- powers_by_faculty = list()
-
- for(var/faculty in ranks)
- var/relevant_rank = get_rank(faculty)
- var/datum/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty)
- for(var/thing in faculty_decl.powers)
- var/datum/psionic_power/power = thing
- if(relevant_rank >= power.min_rank)
- if(!powers_by_faculty[power.faculty]) powers_by_faculty[power.faculty] = list()
- powers_by_faculty[power.faculty] += power
- if(power.use_ranged)
- if(!ranged_powers[faculty]) ranged_powers[faculty] = list()
- ranged_powers[faculty] += power
- if(power.use_melee)
- if(!melee_powers[faculty]) melee_powers[faculty] = list()
- melee_powers[faculty] += power
- if(power.use_manifest)
- manifestation_powers += power
- if(power.use_grab)
- if(!grab_powers[faculty]) grab_powers[faculty] = list()
- grab_powers[faculty] += power
- rebuild_power_cache = FALSE
-
-/datum/psi_complexus/proc/get_powers_by_faculty(var/faculty)
- rebuild_power_cache()
- return powers_by_faculty[faculty]
-
-/datum/psi_complexus/proc/get_melee_powers(var/faculty)
- rebuild_power_cache()
- return melee_powers[faculty]
-
-/datum/psi_complexus/proc/get_ranged_powers(var/faculty)
- rebuild_power_cache()
- return ranged_powers[faculty]
-
-/datum/psi_complexus/proc/get_grab_powers(var/faculty)
- rebuild_power_cache()
- return grab_powers[faculty]
-
-/datum/psi_complexus/proc/get_manifestations()
- rebuild_power_cache()
- return manifestation_powers
diff --git a/code/modules/psionics/complexus/complexus_process.dm b/code/modules/psionics/complexus/complexus_process.dm
index b0e18682220..3cfa4250d5d 100644
--- a/code/modules/psionics/complexus/complexus_process.dm
+++ b/code/modules/psionics/complexus/complexus_process.dm
@@ -1,38 +1,15 @@
/datum/psi_complexus/proc/update(var/force)
-
set waitfor = FALSE
- var/last_rating = rating
- var/highest_faculty
- var/highest_rank = 0
- var/combined_rank = 0
- for(var/faculty in ranks)
- var/check_rank = get_rank(faculty)
- if(check_rank == 1)
- LAZYADD(latencies, faculty)
- else
- if(check_rank <= 0)
- ranks -= faculty
- LAZYREMOVE(latencies, faculty)
- combined_rank += check_rank
- if(!highest_faculty || highest_rank < check_rank)
- highest_faculty = faculty
- highest_rank = check_rank
-
- UNSETEMPTY(latencies)
- var/rank_count = max(1, LAZYLEN(ranks))
- if(force || last_rating != Ceiling(combined_rank/rank_count))
- if(highest_rank <= 1)
- if(highest_rank == 0)
- qdel(src)
+ if(force || last_psionic_rank != psionic_rank)
+ if(psionic_rank == 0)
+ qdel(src)
return
else
- rebuild_power_cache = TRUE
sound_to(owner, 'sound/effects/psi/power_unlock.ogg')
- rating = Ceiling(combined_rank/rank_count)
cost_modifier = 1
- if(rating > 1)
- cost_modifier -= min(1, max(0.1, (rating-1) / 10))
+ if(psionic_rank > 1)
+ cost_modifier -= min(1, max(0.1, psionic_rank / 10))
if(!ui)
ui = new(owner)
if(owner.client)
@@ -40,35 +17,60 @@
else
if(owner.client)
owner.client.screen |= ui
- if(!suppressed && owner.client)
- for(var/thing in SSpsi.all_aura_images)
- owner.client.images |= thing
var/image/aura_image = get_aura_image()
- if(rating >= PSI_RANK_PARAMOUNT) // spooky boosters
+ if(psionic_rank >= PSI_RANK_APEX) // spooky boosters
aura_color = "#aaffaa"
aura_image.blend_mode = BLEND_SUBTRACT
else
aura_image.blend_mode = BLEND_ADD
- if(highest_faculty == PSI_COERCION)
- aura_color = "#cc3333"
- else if(highest_faculty == PSI_PSYCHOKINESIS)
- aura_color = "#3333cc"
- else if(highest_faculty == PSI_REDACTION)
- aura_color = "#33cc33"
- else if(highest_faculty == PSI_ENERGISTICS)
+ if(psionic_rank == PSI_RANK_SENSITIVE)
aura_color = "#cccc33"
+ else if(psionic_rank == PSI_RANK_HARMONIOUS)
+ aura_color = "#64c464"
+
+ if(psionic_rank > PSI_RANK_SENSITIVE && last_psionic_rank < PSI_RANK_HARMONIOUS)
+ switch(psionic_rank)
+ if(PSI_RANK_HARMONIOUS)
+ psi_points = PSI_POINTS_HARMONIOUS
+ if(PSI_RANK_APEX)
+ psi_points = PSI_POINTS_APEX
+ if(PSI_RANK_LIMITLESS)
+ psi_points = PSI_POINTS_LIMITLESS
+ /// We had special abilities unique to our level, so get rid of 'em.
+ if(last_psionic_rank > PSI_RANK_HARMONIOUS)
+ wipe_user_abilities()
+
+ if(last_psionic_rank > PSI_RANK_SENSITIVE && psionic_rank < PSI_RANK_HARMONIOUS)
+ psi_points = 0
+ wipe_user_abilities()
if(!announced && owner && 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, SPAN_NOTICE("Left-click your psionic icon to open your Psionic Point Shop, which contains information about your powers. \
+ You can also shift-click your ability icons on the top right to know more about them. \
+ Beware: overusing your gifts can have deadly consequences."))
to_chat(owner, "
")
+ if(get_rank() >= PSI_RANK_SENSITIVE)
+ for(var/singleton/psionic_power/P in GET_SINGLETON_SUBTYPE_LIST(/singleton/psionic_power))
+ if((P.ability_flags & PSI_FLAG_FOUNDATIONAL) || (P.ability_flags & PSI_FLAG_APEX && get_rank() >= PSI_RANK_APEX) || (P.ability_flags & PSI_FLAG_LIMITLESS && get_rank() >= PSI_RANK_LIMITLESS))
+ if(!(P.type in psionic_powers))
+ P.apply(owner)
+
+/datum/psi_complexus/proc/wipe_user_abilities()
+ for(var/obj/screen/ability/obj_based/psionic/P in owner.ability_master.ability_objects)
+ if((P.connected_power.ability_flags & PSI_FLAG_APEX) && get_rank() < PSI_RANK_APEX)
+ owner.ability_master.remove_ability(P)
+ if((P.connected_power.ability_flags & PSI_FLAG_LIMITLESS) && get_rank() < PSI_RANK_LIMITLESS)
+ owner.ability_master.remove_ability(P)
+
/datum/psi_complexus/process()
var/update_hud
+
if(stun)
stun--
if(stun)
@@ -77,6 +79,7 @@
update_hud = TRUE
else
to_chat(owner, SPAN_NOTICE("You have recovered your mental composure."))
+ suppressed = FALSE
update_hud = TRUE
return
@@ -86,11 +89,11 @@
else if(owner?.stat == UNCONSCIOUS)
stamina = min(max_stamina, stamina + rand(3,5))
- if(!owner.nervous_system_failure() && owner.stat == CONSCIOUS && stamina && !suppressed && get_rank(PSI_REDACTION) >= PSI_RANK_OPERANT)
- attempt_regeneration()
+ if(armor_component)
+ spend_power(1)
- var/next_aura_size = max(0.1,((stamina/max_stamina)*min(3,rating))/5)
- var/next_aura_alpha = round(((suppressed ? max(0,rating - 2) : rating)/5)*255)
+ var/next_aura_size = max(0.1, ((stamina / max_stamina)*min(3, psionic_rank)) / 3)
+ var/next_aura_alpha = round(((suppressed ? max(0, psionic_rank - 2) : psionic_rank) / 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
@@ -103,126 +106,3 @@
if(update_hud)
ui.update_icon()
-
-/datum/psi_complexus/proc/attempt_regeneration()
-
- var/heal_general = FALSE
- var/heal_poison = FALSE
- var/heal_internal = FALSE
- var/heal_bleeding = FALSE
- var/heal_rate = 0
- var/mend_prob = 0
-
- var/use_rank = get_rank(PSI_REDACTION)
- if(use_rank >= PSI_RANK_PARAMOUNT)
- heal_general = TRUE
- heal_poison = TRUE
- heal_internal = TRUE
- heal_bleeding = TRUE
- mend_prob = 50
- heal_rate = 7
- else if(use_rank == PSI_RANK_GRANDMASTER)
- heal_poison = TRUE
- heal_internal = TRUE
- heal_bleeding = TRUE
- mend_prob = 20
- heal_rate = 5
- else if(use_rank == PSI_RANK_MASTER)
- heal_internal = TRUE
- heal_bleeding = TRUE
- mend_prob = 10
- heal_rate = 3
- else if(use_rank == PSI_RANK_OPERANT)
- heal_bleeding = TRUE
- mend_prob = 5
- heal_rate = 1
- else
- return
-
- if(!heal_rate || stamina < heal_rate)
- return // Don't backblast from trying to heal ourselves thanks.
-
- if(ishuman(owner))
-
- var/mob/living/carbon/human/H = owner
-
- // Fix some pain.
- if(heal_rate > 0)
- H.shock_stage = max(0, H.shock_stage - max(1, round(heal_rate/2)))
-
- // Mend internal damage.
- if(prob(mend_prob))
-
- // Fix our heart if we're paramount.
- if(heal_general && H.is_asystole() && H.should_have_organ(BP_HEART) && spend_power(heal_rate))
- H.resuscitate()
-
- // Heal organ damage.
- if(heal_internal)
- for(var/obj/item/organ/I in H.internal_organs)
-
- if(BP_IS_ROBOTIC(I))
- continue
-
- if(I.damage > 0 && spend_power(heal_rate))
- I.damage = max(I.damage - heal_rate, 0)
- if(prob(25))
- to_chat(H, SPAN_NOTICE("Your innards itch as your autoredactive faculty mends your [I.name]."))
- return
-
- // Heal broken bones.
- if(H.bad_external_organs.len)
- for(var/obj/item/organ/external/E in H.bad_external_organs)
-
- if(BP_IS_ROBOTIC(E))
- continue
-
- if(heal_internal && (E.status & ORGAN_BROKEN) && E.damage < E.min_broken_damage) // So we don't mend and autobreak.
- if(spend_power(heal_rate))
- if(E.mend_fracture())
- to_chat(H, SPAN_NOTICE("Your autoredactive faculty coaxes together the shattered bones in your [E.name]."))
- return
-
- if(heal_bleeding)
-
- if((E.status & ORGAN_ARTERY_CUT) && spend_power(heal_rate))
- to_chat(H, SPAN_NOTICE("Your autoredactive faculty mends the torn artery in your [E.name], stemming the worst of the bleeding."))
- E.status &= ~ORGAN_ARTERY_CUT
- return
-
- if((E.tendon_status() & TENDON_CUT) && E.tendon.can_recover())
- to_chat(H, SPAN_NOTICE("Your autoredactive faculty repairs the severed tendon in your [E.name]."))
- E.tendon.rejuvenate()
- return TRUE
-
- for(var/datum/wound/W in E.wounds)
-
- if(W.bleeding() && spend_power(heal_rate))
- to_chat(H, SPAN_NOTICE("Your autoredactive faculty knits together severed veins, stemming the bleeding from \a [W.desc] on your [E.name]."))
- W.bleed_timer = 0
- W.clamped = TRUE
- E.status &= ~ORGAN_BLEEDING
- return
-
- // Heal radiation, cloneloss and poisoning.
- if(heal_poison)
-
- if(owner.total_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.total_radiation = max(0, owner.total_radiation - heal_rate)
- return
-
- if(owner.getCloneLoss() && spend_power(heal_rate))
- if(prob(25))
- to_chat(owner, SPAN_NOTICE("Your autoredactive faculty stitches together some of your mangled DNA."))
- owner.adjustCloneLoss(-heal_rate)
- return
-
- // Heal everything left.
- if(heal_general && prob(mend_prob) && (owner.getBruteLoss() || owner.getFireLoss() || owner.getOxyLoss()) && spend_power(heal_rate))
- owner.adjustBruteLoss(-(heal_rate))
- owner.adjustFireLoss(-(heal_rate))
- owner.adjustOxyLoss(-(heal_rate))
- if(prob(25))
- to_chat(owner, SPAN_NOTICE("Your skin crawls as your autoredactive faculty heals your body."))
diff --git a/code/modules/psionics/equipment/cerebro_enhancers.dm b/code/modules/psionics/equipment/cerebro_enhancers.dm
index 36a89e8cfa8..c16405d209d 100644
--- a/code/modules/psionics/equipment/cerebro_enhancers.dm
+++ b/code/modules/psionics/equipment/cerebro_enhancers.dm
@@ -14,10 +14,7 @@
)
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_rank = PSI_RANK_HARMONIOUS
var/boosted_psipower = 120
/obj/item/clothing/head/helmet/space/psi_amp/lesser
@@ -26,51 +23,14 @@
flags_inv = 0
body_parts_covered = 0
- max_boosted_faculties = 1
- boosted_rank = PSI_RANK_MASTER
- unboosted_rank = PSI_RANK_OPERANT
+ boosted_rank = PSI_RANK_HARMONIOUS
boosted_psipower = 50
/obj/item/clothing/head/helmet/space/psi_amp/Initialize()
. = ..()
verbs += /obj/item/clothing/head/helmet/space/psi_amp/proc/integrate
-/obj/item/clothing/head/helmet/space/psi_amp/attack_self(var/mob/user)
-
- if(operating)
- return
-
- if(!canremove)
- deintegrate()
- 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/proc/deintegrate()
-
set name = "Remove Psi-Amp"
set desc = "Removes your psi-amp."
set category = "Abilities"
@@ -121,7 +81,6 @@
canremove = TRUE
/obj/item/clothing/head/helmet/space/psi_amp/proc/integrate()
-
set name = "Integrate Psionic Amplifier"
set desc = "Enhance your brainpower."
set category = "Abilities"
@@ -133,10 +92,6 @@
if(!canremove)
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
-
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."))
@@ -149,11 +104,7 @@
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)
+ H.set_psi_rank(boosted_rank, temporary = TRUE)
if(H.psi)
H.psi.max_stamina = boosted_psipower
H.psi.stamina = H.psi.max_stamina
@@ -165,5 +116,32 @@
operating = FALSE
action_button_name = "Remove Psionic Amplifier"
H.update_action_buttons()
+ H.client.init_verbs()
set_light(0.5, 0.1, 3, 2, l_color = "#880000")
+
+/obj/item/psionic_jumpstarter
+ name = "psionic jumpstarter"
+ desc = "Use this to jumpstart your psionic rank to Psionically Harmonious, enabling you to use the Psionic Point Shop and buy offensive psionic abilities. \
+ This won't work on species with no Zona Bovinae, like synthetics, vaurcae or dionae! This item is definitely not canon."
+ icon = 'icons/obj/clothing/hats.dmi'
+ icon_state = "amp"
+ contained_sprite = FALSE
+
+/obj/item/psionic_jumpstarter/attack_self(mob/user)
+ . = ..()
+ if(!ishuman(user))
+ return
+ var/mob/living/carbon/human/H = user
+ if(!H.has_zona_bovinae())
+ to_chat(H, SPAN_WARNING("You don't have a Zona Bovinae!"))
+ return
+
+ if(H.psi && H.psi.get_rank() >= PSI_RANK_HARMONIOUS)
+ to_chat(H, SPAN_WARNING("You've already awakened your psionic potential!"))
+ return
+
+ H.set_psi_rank(PSI_RANK_HARMONIOUS)
+ H.psi.psi_points = 8
+ to_chat(H, SPAN_NOTICE("You've awakened your psionic potential. Note that you have a reduced point pool than usual."))
+ qdel(src)
diff --git a/code/modules/psionics/equipment/psipower.dm b/code/modules/psionics/equipment/psipower.dm
index 47d537d9cc8..e5ca9d22151 100644
--- a/code/modules/psionics/equipment/psipower.dm
+++ b/code/modules/psionics/equipment/psipower.dm
@@ -54,3 +54,7 @@
host.drop_from_inventory(src)
else
qdel(src)
+
+/obj/item/psychic_power/damage_flags()
+ . = ..()
+ . |= DAMAGE_FLAG_PSIONIC
diff --git a/code/modules/psionics/equipment/psipower_blade.dm b/code/modules/psionics/equipment/psipower_blade.dm
index d73fa24dfd5..aa748370a09 100644
--- a/code/modules/psionics/equipment/psipower_blade.dm
+++ b/code/modules/psionics/equipment/psipower_blade.dm
@@ -1,5 +1,5 @@
/obj/item/psychic_power/psiblade
- name = "psychokinetic slash"
+ name = "psionic blade"
force = 10
sharp = 1
edge = TRUE
@@ -7,21 +7,25 @@
icon_state = "psiblade_short"
hitsound = 'sound/weapons/psisword.ogg'
+/obj/item/psychic_power/psiblade/Initialize()
+ . = ..()
+ switch(owner.psi.get_rank())
+ if(PSI_RANK_SENSITIVE)
+ force = 20
+ armor_penetration = 10
+ if(PSI_RANK_HARMONIOUS)
+ force = 25
+ armor_penetration = 20
+ if(PSI_RANK_APEX)
+ force = 30
+ armor_penetration = 30
+ icon_state = "psiblade_long"
+ if(PSI_RANK_LIMITLESS)
+ force = 40
+ armor_penetration = 40
+ icon_state = "psiblade_long"
+
/obj/item/psychic_power/psiblade/dropped(var/mob/living/user)
. = ..()
playsound(loc, 'sound/effects/psi/power_fail.ogg', 30, 1)
QDEL_IN(src, 1)
-
-/obj/item/psychic_power/psiblade/master
- force = 20
- maintain_cost = 2
-
-/obj/item/psychic_power/psiblade/master/grand
- force = 30
- maintain_cost = 3
- icon_state = "psiblade_long"
-
-/obj/item/psychic_power/psiblade/master/grand/paramount // Silly typechecks because rewriting old interaction code is outside of scope.
- force = 50
- maintain_cost = 4
- icon_state = "psiblade_long"
diff --git a/code/modules/psionics/equipment/psipower_tk.dm b/code/modules/psionics/equipment/psipower_tk.dm
deleted file mode 100644
index b321a78b8e7..00000000000
--- a/code/modules/psionics/equipment/psipower_tk.dm
+++ /dev/null
@@ -1,94 +0,0 @@
-/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 || !istype(focus.loc, /turf) || get_dist(get_turf(focus), get_turf(owner)) > owner.psi.get_rank(PSI_PSYCHOKINESIS))
- owner.drop_from_inventory(src)
- return
- . = ..()
-
-/obj/item/psychic_power/telekinesis/proc/set_focus(var/atom/movable/_focus)
- if(!_focus.simulated || !istype(_focus.loc, /turf))
- return FALSE
-
- var/check_paramount
- if(ismob(_focus))
- var/mob/victim = _focus
- check_paramount = (victim.mob_size >= MOB_MEDIUM)
- else if(isobj(_focus))
- var/obj/thing = _focus
- check_paramount = (thing.w_class >= 5)
- else
- return FALSE
-
- if(_focus.anchored || (check_paramount && owner.psi.get_rank(PSI_PSYCHOKINESIS) < PSI_RANK_PARAMOUNT))
- focus = _focus
- . = attack_self(owner)
- if(!.)
- to_chat(owner, SPAN_WARNING("\The [_focus] is too hefty for you to get a mind-grip on."))
- qdel(src)
- return FALSE
-
- focus = _focus
- overlays.Cut()
- var/image/I = image(icon = focus.icon, icon_state = focus.icon_state)
- I.color = focus.color
- I.overlays = focus.overlays
- overlays += I
- return TRUE
-
-/obj/item/psychic_power/telekinesis/attack_self(var/mob/user)
- user.visible_message(SPAN_NOTICE("\The [user] makes a strange gesture."))
- sparkle()
- return focus.do_simple_ranged_interaction(user)
-
-/obj/item/psychic_power/telekinesis/afterattack(var/atom/target, var/mob/living/user, var/proximity)
- if(!target || !user || (isobj(target) && !isturf(target.loc)) || !user.psi || !user.psi.can_use() || !user.psi.spend_power(5))
- return
-
- user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN)
- user.psi.set_cooldown(5)
-
- var/distance = get_dist(get_turf(user), get_turf(focus ? focus : target))
- if(distance > user.psi.get_rank(PSI_PSYCHOKINESIS))
- to_chat(user, SPAN_WARNING("Your telekinetic power won't reach that far."))
- return FALSE
-
- if(target == focus)
- attack_self(user)
- else
- user.visible_message(SPAN_DANGER("\The [user] gestures sharply!"))
- sparkle()
- if(!istype(target, /turf) && istype(focus,/obj/item) && target.Adjacent(focus))
- var/obj/item/I = focus
- var/resolved = target.attackby(I, user, user:get_organ_target())
- if(!resolved && target && I)
- I.afterattack(target,user,1) // for splashing with beakers
- else
- if(!focus.anchored)
- var/user_rank = owner.psi.get_rank(PSI_PSYCHOKINESIS)
- focus.throw_at(target, user_rank*2, user_rank*10, owner)
- sleep(1)
- sparkle()
-
-/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)
\ No newline at end of file
diff --git a/code/modules/psionics/events/mini_spasm.dm b/code/modules/psionics/events/mini_spasm.dm
index a4c664da8b2..9bc4aa04d09 100644
--- a/code/modules/psionics/events/mini_spasm.dm
+++ b/code/modules/psionics/events/mini_spasm.dm
@@ -48,16 +48,12 @@
else
to_chat(victim, SPAN_DANGER("An indescribable, brain-tearing sound hisses from [icon2html(source, victim)] \the [source], and you collapse in a seizure!"))
victim.seizure()
- var/new_latencies = rand(2,4)
- var/list/faculties = list(PSI_COERCION, PSI_REDACTION, PSI_ENERGISTICS, PSI_PSYCHOKINESIS)
- for(var/i = 1 to new_latencies)
- to_chat(victim, SPAN_DANGER("[pick(psi_operancy_messages)]"))
- victim.adjustBrainLoss(rand(10,20))
- victim.set_psi_rank(pick_n_take(faculties), 1)
- sleep(30)
+ to_chat(victim, SPAN_DANGER(FONT_LARGE("[pick(psi_operancy_messages)]")))
+ victim.adjustBrainLoss(rand(10,20))
+ victim.set_psi_rank(1)
victim.psi.update()
sleep(45)
- victim.psi.check_latency_trigger(100, "a psionic scream", redactive = TRUE)
+ victim.psi.check_psionic_trigger(100, "a psionic scream", redactive = TRUE)
/datum/event/minispasm/end()
command_announcement.Announce( \
diff --git a/code/modules/psionics/faculties/_faculty.dm b/code/modules/psionics/faculties/_faculty.dm
deleted file mode 100644
index c855ac5abef..00000000000
--- a/code/modules/psionics/faculties/_faculty.dm
+++ /dev/null
@@ -1,11 +0,0 @@
-/datum/psionic_faculty
- var/id
- var/name
- var/associated_intent
- var/list/armor_types = list()
- var/list/powers = list()
-
-/datum/psionic_faculty/New()
- ..()
- for(var/atype in armor_types)
- SSpsi.armor_faculty_by_type[atype] = id
diff --git a/code/modules/psionics/faculties/_power.dm b/code/modules/psionics/faculties/_power.dm
deleted file mode 100644
index a6df0e44b2f..00000000000
--- a/code/modules/psionics/faculties/_power.dm
+++ /dev/null
@@ -1,37 +0,0 @@
-/datum/psionic_power
- var/name // Name. If null, psipower won't be generated on roundstart.
- var/faculty // Associated psi faculty.
- var/min_rank // Minimum psi rank to use this power.
- var/cost // Base psi stamina cost for using this power.
- var/cooldown // Deciseconds cooldown after using this power.
- var/admin_log = TRUE // Whether or not using this power prints an admin attack log.
- var/use_ranged // This power functions from a distance.
- var/use_melee // This power functions at melee range.
- var/use_grab // This power has a variant invoked via grab.
- var/use_manifest // This power manifests an item in the user's hands.
- var/use_description // A short description of how to use this power, shown via assay.
- // A sound effect to play when the power is used.
- var/use_sound = 'sound/effects/psi/power_used.ogg'
-
-/datum/psionic_power/proc/invoke(var/mob/living/user, var/atom/target)
-
- if(!user.psi)
- return FALSE
-
- if(faculty && min_rank)
- var/user_rank = user.psi.get_rank(faculty)
- if(user_rank < min_rank)
- return FALSE
-
- if(cost && !user.psi.spend_power(cost))
- return FALSE
-
- return TRUE
-
-/datum/psionic_power/proc/handle_post_power(var/mob/living/user, var/atom/target)
- if(cooldown)
- user.psi.set_cooldown(cooldown)
- if(admin_log && ismob(user) && ismob(target))
- admin_attack_log(user, target, "Used psipower ([name])", "Was subjected to a psipower ([name])", "used a psipower ([name]) on")
- if(use_sound)
- playsound(user.loc, use_sound, 75)
diff --git a/code/modules/psionics/faculties/coercion.dm b/code/modules/psionics/faculties/coercion.dm
deleted file mode 100644
index f23f4887064..00000000000
--- a/code/modules/psionics/faculties/coercion.dm
+++ /dev/null
@@ -1,337 +0,0 @@
-/datum/psionic_faculty/coercion
- id = PSI_COERCION
- name = "Coercion"
- associated_intent = I_DISARM
-
-/datum/psionic_power/coercion
- faculty = PSI_COERCION
-
-/datum/psionic_power/coercion/invoke(var/mob/living/user, var/mob/living/target)
- if (!istype(target))
- to_chat(user, SPAN_WARNING("You cannot mentally attack \the [target]."))
- return FALSE
-
- . = ..()
-
-/datum/psionic_power/coercion/blindstrike
- name = "Blindstrike"
- cost = 8
- cooldown = 120
- use_ranged = TRUE
- use_melee = TRUE
- min_rank = PSI_RANK_GRANDMASTER
- use_description = "Target the eyes on disarm intent and click anywhere to use a radial attack that blinds, deafens and disorients everyone near you."
-
-/datum/psionic_power/coercion/blindstrike/invoke(var/mob/living/user, var/mob/living/target)
- if(user.zone_sel.selecting != BP_EYES)
- return FALSE
- . = ..()
- if(.)
- user.visible_message(SPAN_DANGER("\The [user] suddenly throws back their head, as though screaming silently!"), SPAN_NOTICE("You strike at all around you with a deafening psionic scream!"))
- for(var/mob/living/M in orange(user.psi.get_rank(PSI_COERCION), user))
- if(M == user)
- continue
- if(prob(60) && iscarbon(M))
- var/mob/living/carbon/C = M
- C.emote("scream")
- to_chat(M, SPAN_DANGER("Your senses are blasted into oblivion by a psionic scream!"))
- M.eye_blind = max(M.eye_blind,3)
- M.ear_deaf = max(M.ear_deaf,6)
- if(!M.isSynthetic())
- M.confused = max(M.confused, rand(3,8))
- return TRUE
-
-/datum/psionic_power/coercion/mindread
- name = "Read Mind"
- cost = 30
- cooldown = 150 //It should take a good bit to be able to use this again.
- use_melee = TRUE
- min_rank = PSI_RANK_OPERANT
- use_description = "Target the head on disarm intent at melee range to attempt to read a victim's surface thoughts."
-
-/datum/psionic_power/coercion/mindread/invoke(var/mob/living/user, var/mob/living/target)
- if(!isliving(target) || !istype(target) || user.zone_sel.selecting != BP_HEAD)
- return FALSE
- . = ..()
- if(!.)
- return
-
- if(target.stat == DEAD || (target.status_flags & FAKEDEATH) || !target.client)
- to_chat(user, SPAN_WARNING("[target] is in no state for a mind-read."))
- return TRUE
-
- user.visible_message(SPAN_WARNING("\The [user] touches \the [target]'s temple..."))
- var/question = sanitize(input(user, "Say something?", "Read Mind", "Penny for your thoughts?") as null|text)
- if(!question || user.incapacitated() || !do_mob(user, target, 20))
- return TRUE
- var/psi_blocked = target.is_psi_blocked()
- if(psi_blocked)
- to_chat(user, psi_blocked)
- return TRUE
- var/started_mindread = world.time
- if(target.has_psi_aug())
- to_chat(user, SPAN_NOTICE("Your psyche links with [target]'s psi-receiver, seeking an answer from their mind's surface: [question]"))
- to_chat(target, SPAN_NOTICE("[user]'s psyche links with your psi-receiver, your mind is compelled to answer: [question]"))
- else
- to_chat(user, SPAN_NOTICE("You dip your mentality into the surface layer of \the [target]'s mind, seeking an answer: [question]"))
- to_chat(target, SPAN_NOTICE("Your mind is compelled to answer: [question]"))
- var/answer = sanitize(input(target, "[question]\nYou have 25 seconds to type a response", "Read Mind") as null|text)
- if(!answer || world.time > started_mindread + 25 SECONDS || user.stat != CONSCIOUS)
- to_chat(user, SPAN_NOTICE("You receive nothing useful from \the [target]."))
- to_chat(target, SPAN_NOTICE("Your mind blanks out momentarily."))
- else
- to_chat(user, SPAN_NOTICE("You skim thoughts from the surface of \the [target]'s mind: [answer]"))
- msg_admin_attack("[key_name(user)] read mind of [key_name(target)] with question \"[question]\" and [answer?"got answer \"[answer]\".":"got no answer."]")
- return TRUE
-
-/datum/psionic_power/coercion/agony
- name = "Agony"
- cost = 8
- cooldown = 50
- use_melee = TRUE
- min_rank = PSI_RANK_MASTER
- use_description = "Target the chest or groin on disarm intent to use a melee attack equivalent to a strike from a stun baton."
-
-/datum/psionic_power/coercion/agony/invoke(var/mob/living/user, var/mob/living/target)
- if(!istype(target))
- return FALSE
- if(user.zone_sel.selecting != BP_CHEST && user.zone_sel.selecting != BP_GROIN)
- return FALSE
- . = ..()
- if(.)
- user.visible_message(SPAN_DANGER("\The [target] has been struck by \the [user]!"))
- playsound(user.loc, 'sound/weapons/Egloves.ogg', 50, 1, -1)
- target.stun_effect_act(0, 60, user.zone_sel.selecting)
- return TRUE
-
-/datum/psionic_power/coercion/spasm
- name = "Spasm"
- cost = 15
- cooldown = 100
- use_melee = TRUE
- use_ranged = TRUE
- min_rank = PSI_RANK_MASTER
- use_description = "Target the arms or hands on disarm intent to use a ranged attack that may rip the weapons away from the target."
-
-/datum/psionic_power/coercion/spasm/invoke(var/mob/living/user, var/mob/living/carbon/human/target)
- if(!istype(target))
- return FALSE
-
- if(!(user.zone_sel.selecting in list(BP_L_ARM, BP_R_ARM, BP_L_HAND, BP_R_HAND)))
- 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.l_hand && target.l_hand.simulated && target.unEquip(target.l_hand))
- target.visible_message(SPAN_DANGER("\The [target] drops what they were holding as their left hand spasms!"))
- if(prob(75) && target.r_hand && target.r_hand.simulated && target.unEquip(target.r_hand))
- target.visible_message(SPAN_DANGER("\The [target] drops what they were holding as their right hand spasms!"))
- return TRUE
-
-/datum/psionic_power/coercion/mindslave
- name = "Mindslave"
- cost = 28
- cooldown = 200
- use_grab = TRUE
- min_rank = PSI_RANK_PARAMOUNT
- use_description = "Grab a victim, target the eyes, then use the grab on them while on disarm intent, in order to convert them into a loyal mind-slave. The process takes some time, and failure is punished harshly."
-
-/datum/psionic_power/coercion/mindslave/invoke(var/mob/living/user, var/mob/living/target)
- if(!istype(target) || user.zone_sel.selecting != BP_EYES)
- return FALSE
- . = ..()
- if(.)
- if(target.stat == DEAD || (target.status_flags & FAKEDEATH))
- to_chat(user, SPAN_WARNING("\The [target] is dead!"))
- return TRUE
- if(!target.mind || !target.key)
- to_chat(user, SPAN_WARNING("\The [target] is mindless!"))
- return TRUE
- for (var/obj/item/implant/mindshield/I in target)
- if (I.implanted)
- to_chat(user, SPAN_WARNING("\The [target]'s mind is protected from the mindslaving."))
- return TRUE
-
- user.visible_message(SPAN_DANGER("\The [user] seizes the head of \the [target] in both hands..."))
- to_chat(user, SPAN_WARNING("You plunge your mentality into that of \the [target]..."))
- to_chat(target, SPAN_DANGER("Your mind is invaded by the presence of \the [user]! They are trying to make you a slave!"))
- if(!do_after(user, target.stat == CONSCIOUS ? 80 : 40, target, 0, 1))
- user.psi.backblast(rand(10,25))
- return TRUE
- to_chat(user, SPAN_DANGER("You sear through \the [target]'s neurons, reshaping as you see fit and leaving them subservient to your will!"))
- to_chat(target, SPAN_DANGER("Your defenses have eroded away and \the [user] has made you their mindslave."))
- thralls.add_antagonist(target.mind, TRUE, TRUE, FALSE, TRUE, TRUE)
- return TRUE
-
-/datum/psionic_power/coercion/assay
- name = "Assay"
- cost = 15
- cooldown = 100
- use_grab = TRUE
- min_rank = PSI_RANK_OPERANT
- use_description = "Grab a patient, target the head, then use the grab on them while on disarm intent, in order to perform a deep coercive-redactive probe of their psionic potential."
-
-/datum/psionic_power/coercion/assay/invoke(var/mob/living/user, var/mob/living/target)
- if(user.zone_sel.selecting != BP_HEAD)
- return FALSE
- . = ..()
- if(.)
- user.visible_message(SPAN_WARNING("\The [user] holds the head of \the [target] in both hands..."))
- to_chat(user, SPAN_NOTICE("You insinuate your mentality into that of \the [target]..."))
- to_chat(target, SPAN_WARNING("Your persona is being probed by the psychic lens of \the [user]."))
- if(!do_after(user, (target.stat == CONSCIOUS ? 50 : 25), target, 0, 1))
- user.psi.backblast(rand(5,10))
- return TRUE
- to_chat(user, SPAN_NOTICE("You retreat from \the [target], holding your new knowledge close."))
- to_chat(target, SPAN_DANGER("Your mental complexus is laid bare to judgement of \the [user]."))
- target.show_psi_assay(user)
- return TRUE
-
-/datum/psionic_power/coercion/focus
- name = "Focus"
- cost = 10
- cooldown = 80
- use_grab = TRUE
- min_rank = PSI_RANK_MASTER
- use_description = "Grab a patient, target the mouth, then use the grab on them while on disarm intent, in order to cure ailments of the mind."
-
-/datum/psionic_power/coercion/focus/invoke(var/mob/living/user, var/mob/living/target)
- if(user.zone_sel.selecting != "mouth")
- 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 ? 50 : 25), target, 0, 1))
- user.psi.backblast(rand(5,10))
- return TRUE
- to_chat(user, SPAN_WARNING("You clear \the [target]'s mind of ailments."))
- to_chat(target, SPAN_WARNING("Your mind is cleared of ailments."))
-
- var/coercion_rank = user.psi.get_rank(PSI_COERCION)
- if(coercion_rank >= PSI_RANK_GRANDMASTER)
- target.AdjustParalysis(-1)
- target.drowsiness = 0
- if(istype(target, /mob/living/carbon))
- var/mob/living/carbon/M = target
- M.hallucination = max(M.hallucination, 10)
- return TRUE
-
-/datum/psionic_power/coercion/commune
- name = "Commune"
- cost = 15
- cooldown = 10
- use_melee = TRUE
- use_ranged = TRUE
- min_rank = PSI_RANK_OPERANT
- use_sound = null
- use_description = "Target the mouth and click on a creature on disarm intent to psionically send them a message."
- admin_log = FALSE
-
-/datum/psionic_power/coercion/commune/invoke(var/mob/living/user, var/mob/living/target)
- if((target == user) || user.zone_sel.selecting != BP_MOUTH)
- return FALSE
- . = ..()
- if(.)
- user.visible_message(SPAN_NOTICE("[user] blinks, their eyes briefly developing an unnatural shine."))
- if(target.stat == DEAD)
- to_chat(user, SPAN_CULT("Not even a psion of your level can speak to the dead."))
- return
-
- var/text = input("What would you like to say?", "Speak to creature", null, null)
- text = sanitize(text)
- if(!text)
- return
- text = formalize_text(text)
-
- if(target.stat == DEAD)
- to_chat(user, SPAN_CULT("Not even a psion of your level can speak to the dead."))
- return
-
- var/psi_blocked = target.is_psi_blocked()
- if(psi_blocked)
- to_chat(user, psi_blocked)
- return
-
- log_say("[key_name(user)] communed to [key_name(target)]: [text]",ckey=key_name(src))
-
- to_chat(user, SPAN_CULT("You psionically say to [target]: [text]"))
-
- for (var/mob/M in player_list)
- if (istype(M, /mob/abstract/new_player))
- continue
- else if(M.stat == DEAD && M.client.prefs.toggles & CHAT_GHOSTEARS)
- to_chat(M, "[user] psionically says to [target]: [text]")
-
- var/mob/living/carbon/human/H = target
- if(H.can_commune() || H.psi)
- to_chat(H, SPAN_CULT("You instinctively sense [user] passing a thought into your mind: [text]"))
- else if(target.has_psi_aug())
- to_chat(H, SPAN_CULT("You sense [user]'s psyche link with your psi-receiver, a thought sliding into your mind: [text]"))
- else
- to_chat(H, SPAN_ALIEN("A thought from outside your consciousness slips into your mind: [text]"))
-
-/datum/psionic_power/coercion/psiping
- name = "Psi Ping"
- cost = 5
- cooldown = 30
- use_melee = TRUE
- use_manifest = TRUE
- min_rank = PSI_RANK_OPERANT
- use_sound = null
- use_description = "Activate an empty hand on help intent to detect nearby psionic signatures."
- admin_log = FALSE
-
-/datum/psionic_power/coercion/psiping/invoke(var/mob/living/user, var/mob/living/target)
- if((target && user != target) || user.a_intent != I_HELP)
- return FALSE
- . = ..()
- if(.)
- to_chat(user, SPAN_NOTICE("You take a moment to tune into the local Nlom..."))
- if(!do_after(user, 3 SECONDS))
- return
- var/list/dirs = list()
- var/turf/our_turf = get_turf(user)
- for(var/mob/living/L in range(20))
- var/turf/T = get_turf(L)
- if(!T || L == user || L.stat == DEAD || L.invisibility == INVISIBILITY_LEVEL_TWO)
- continue
- if(!L.is_psi_pingable())
- continue
- var/image/ping_image = image(icon = 'icons/effects/effects.dmi', icon_state = "sonar_ping", loc = our_turf, layer = OBFUSCATION_LAYER + 0.1)
- pixel_shift_to_turf(ping_image, our_turf, T)
- user << ping_image
- QDEL_IN(ping_image, 8)
- var/direction = num2text(get_dir(user, L))
- var/dist
- if(text2num(direction))
- switch(get_dist(user, L) / user.client.view)
- if(0 to 0.2)
- dist = "very close"
- if(0.2 to 0.4)
- dist = "close"
- if(0.4 to 0.6)
- dist = "a little ways away"
- if(0.6 to 0.8)
- dist = "farther away"
- else
- dist = "far away"
- else
- dist = "on top of you"
- LAZYINITLIST(dirs[direction])
- dirs[direction][dist] += 1
- for(var/d in dirs)
- var/list/feedback = list()
- for(var/dst in dirs[d])
- feedback += "[dirs[d][dst]] psionic signature\s [dst],"
- if(feedback.len > 1)
- feedback[feedback.len - 1] += " and"
- to_chat(user, SPAN_NOTICE("You sense " + jointext(feedback, " ") + " towards the [dir2text(text2num(d))]."))
- if(!length(dirs))
- to_chat(user, SPAN_NOTICE("You detect no psionic signatures but your own."))
diff --git a/code/modules/psionics/faculties/energistics.dm b/code/modules/psionics/faculties/energistics.dm
deleted file mode 100644
index 16b651f98e3..00000000000
--- a/code/modules/psionics/faculties/energistics.dm
+++ /dev/null
@@ -1,116 +0,0 @@
-/datum/psionic_faculty/energistics
- id = PSI_ENERGISTICS
- name = "Energistics"
- associated_intent = I_HURT
- armor_types = list("bomb", "laser", "energy")
-
-/datum/psionic_power/energistics
- faculty = PSI_ENERGISTICS
-
-/datum/psionic_power/energistics/electropulse
- name = "Electropulse"
- cost = 40
- cooldown = 100
- use_melee = TRUE
- min_rank = PSI_RANK_MASTER
- use_description = "Target the right hand while on harm intent and click an object to use a melee attack that causes a localized EMP. This activates the EMP function on things, but it takes a while and applies a long cooldown, in addition to being expensive."
-
-/datum/psionic_power/energistics/electropulse/invoke(var/mob/living/user, var/mob/living/target)
- if(user.zone_sel.selecting != BP_R_HAND)
- return FALSE
- if(istype(target, /turf) || ismob(target))
- return FALSE
- . = ..()
- if(.)
- user.visible_message(SPAN_WARNING("\The [user] holds their hands over \the [target]..."))
- if(do_after(user, 100, TRUE, target))
- user.visible_message(SPAN_DANGER("\The [user] releases a gout of arcing lightning over \the [target]!"))
- playsound(target, 'sound/magic/LightningShock.ogg', 75)
- var/severity = 1 + user.psi.get_rank(PSI_ENERGISTICS)
- target.emp_act(severity)
- return TRUE
-
-/datum/psionic_power/energistics/electrocute
- name = "Electrocute"
- cost = 15
- cooldown = 25
- use_melee = TRUE
- min_rank = PSI_RANK_GRANDMASTER
- use_description = "Target the chest or groin while on harm intent to use a melee attack that electrocutes a victim."
-
-/datum/psionic_power/energistics/electrocute/invoke(var/mob/living/user, var/mob/living/target)
- if(user.zone_sel.selecting != BP_CHEST && user.zone_sel.selecting != BP_GROIN)
- return FALSE
- if(istype(target, /turf))
- return FALSE
- . = ..()
- if(.)
- user.visible_message(SPAN_DANGER("\The [user] sends a jolt of electricity arcing into \the [target]!"))
- if(istype(target))
- target.electrocute_act(rand(15,45), user, 1, user.zone_sel.selecting)
- return TRUE
- else if(istype(target, /atom))
- var/obj/item/cell/charging_cell = target.get_cell()
- if(istype(charging_cell))
- charging_cell.give(rand(15,45))
- return TRUE
-
-/datum/psionic_power/energistics/lightning
- name = "Lightning"
- cost = 20
- cooldown = 20
- use_ranged = TRUE
- min_rank = PSI_RANK_MASTER
- use_description = "Use this ranged lightning attack while on harm intent. Your mastery of Energistics will determine how powerful the lightning is. Be wary of overuse, and try not to fry your own brain."
-
-/datum/psionic_power/energistics/lightning/invoke(var/mob/living/user, var/mob/living/target)
- . = ..()
- if(.)
- user.visible_message(SPAN_DANGER("\The [user] suddenly holds their hand out!"))
-
- var/user_rank = user.psi.get_rank(faculty)
- var/obj/item/projectile/pew
- var/pew_sound = 'sound/magic/LightningShock.ogg'
-
- switch(user_rank)
- if(PSI_RANK_PARAMOUNT)
- pew = new /obj/item/projectile/beam/tesla/paramount(get_turf(user))
- pew.name = "thunderstrike"
- pew_sound = 'sound/effects/psi/thunderstrike.ogg'
- if(PSI_RANK_GRANDMASTER)
- pew = new /obj/item/projectile/beam/tesla/grandmaster(get_turf(user))
- pew.name = "lightning shock"
- if(PSI_RANK_MASTER)
- pew = new /obj/item/projectile/beam/tesla/master(get_turf(user))
- pew.name = "lightning beam"
-
- if(istype(pew))
- playsound(pew.loc, pew_sound, 25, 1)
- pew.original = target
- pew.starting = get_turf(user)
- pew.shot_from = user
- pew.launch_projectile(target)
- return TRUE
-
-/datum/psionic_power/energistics/spark
- name = "Spark"
- cost = 1
- cooldown = 1
- use_melee = TRUE
- min_rank = PSI_RANK_OPERANT
- use_description = "Target a non-living target in melee range on harm intent to cause some sparks to appear. This can light fires."
-
-/datum/psionic_power/energistics/spark/invoke(var/mob/living/user, var/mob/living/target)
- if(isnull(target) || istype(target))
- return FALSE
- . = ..()
- if(.)
- if(istype(target,/obj/item/clothing/mask/smokable/cigarette))
- var/obj/item/clothing/mask/smokable/cigarette/S = target
- S.light(SPAN_NOTICE("\The [user] snaps [user.get_pronoun("his")] fingers and \the [S] lights up."))
- playsound(S.loc, /singleton/sound_category/spark_sound, 50, 1)
- return TRUE
- if(!isturf(target))
- return FALSE
- spark(target, 3)
- return TRUE
\ No newline at end of file
diff --git a/code/modules/psionics/faculties/psychokinesis.dm b/code/modules/psionics/faculties/psychokinesis.dm
deleted file mode 100644
index 521fc657456..00000000000
--- a/code/modules/psionics/faculties/psychokinesis.dm
+++ /dev/null
@@ -1,93 +0,0 @@
-/datum/psionic_faculty/psychokinesis
- id = PSI_PSYCHOKINESIS
- name = "Psychokinesis"
- associated_intent = I_GRAB
- armor_types = list("melee", "bullet")
-
-/datum/psionic_power/psychokinesis
- faculty = PSI_PSYCHOKINESIS
- use_sound = null
-
-/datum/psionic_power/psychokinesis/psiblade
- name = "Psiblade"
- cost = 10
- cooldown = 30
- min_rank = PSI_RANK_OPERANT
- use_manifest = TRUE
- use_description = "Click on or otherwise activate an empty hand while on harm intent to manifest a psychokinetic cutting blade. The power the blade will vary based on your mastery of the faculty."
- admin_log = FALSE
-
-/datum/psionic_power/psychokinesis/psiblade/invoke(var/mob/living/user, var/mob/living/target)
- if((target && user != target) || user.a_intent != I_HURT)
- return FALSE
- . = ..()
- if(.)
- switch(user.psi.get_rank(faculty))
- if(PSI_RANK_PARAMOUNT)
- return new /obj/item/psychic_power/psiblade/master/grand/paramount(user, user)
- if(PSI_RANK_GRANDMASTER)
- return new /obj/item/psychic_power/psiblade/master/grand(user, user)
- if(PSI_RANK_MASTER)
- return new /obj/item/psychic_power/psiblade/master(user, user)
- else
- return new /obj/item/psychic_power/psiblade(user, user)
-
-/datum/psionic_power/psychokinesis/tinker
- name = "Tinker"
- cost = 5
- cooldown = 10
- min_rank = PSI_RANK_MASTER
- use_manifest = TRUE
- use_description = "Click on or otherwise activate an empty hand while on grab intent to manifest a psychokinetic tool. Use it in-hand to switch between tool types."
- admin_log = FALSE
-
-/datum/psionic_power/psychokinesis/tinker/invoke(var/mob/living/user, var/mob/living/target)
- if((target && user != target) || user.a_intent != I_GRAB)
- return FALSE
- . = ..()
- if(.)
- return new /obj/item/psychic_power/tinker(user)
-
-/datum/psionic_power/psychokinesis/telekinesis
- name = "Telekinesis"
- cost = 5
- cooldown = 10
- use_ranged = TRUE
- use_manifest = FALSE
- min_rank = PSI_RANK_GRANDMASTER
- use_description = "Click on a distant target while on grab intent to manifest a psychokinetic grip. Use it manipulate objects at a distance."
- admin_log = FALSE
- use_sound = 'sound/effects/psi/power_used.ogg'
- var/global/list/valid_machine_types = list(
- /obj/machinery/door
- )
-
-/datum/psionic_power/psychokinesis/telekinesis/invoke(var/mob/living/user, var/mob/living/target)
- if(user.a_intent != I_GRAB)
- return FALSE
- . = ..()
- if(.)
-
- var/distance = get_dist(user, target)
- if(distance > user.psi.get_rank(PSI_PSYCHOKINESIS))
- to_chat(user, SPAN_WARNING("Your telekinetic power won't reach that far."))
- return FALSE
-
- if(istype(target, /mob) || istype(target, /obj))
- var/obj/item/psychic_power/telekinesis/tk = new(user)
- if(tk.set_focus(target))
- tk.sparkle()
- user.visible_message(SPAN_NOTICE("\The [user] reaches out."))
- return tk
- else if(istype(target, /obj/structure))
- user.visible_message(SPAN_NOTICE("\The [user] makes a strange gesture."))
- var/obj/O = target
- O.attack_hand(user)
- return TRUE
- else if(istype(target, /obj/machinery))
- for(var/mtype in valid_machine_types)
- if(istype(target, mtype))
- var/obj/machinery/machine = target
- machine.attack_hand(user)
- return TRUE
- return FALSE
\ No newline at end of file
diff --git a/code/modules/psionics/faculties/redaction.dm b/code/modules/psionics/faculties/redaction.dm
deleted file mode 100644
index 83156be0dec..00000000000
--- a/code/modules/psionics/faculties/redaction.dm
+++ /dev/null
@@ -1,186 +0,0 @@
-/datum/psionic_faculty/redaction
- id = PSI_REDACTION
- name = "Redaction"
- associated_intent = I_HELP
- armor_types = list("bio", "rad")
-
-/datum/psionic_power/redaction
- faculty = PSI_REDACTION
- admin_log = FALSE
-
-/datum/psionic_power/redaction/proc/check_dead(var/mob/living/target)
- if(!istype(target))
- return FALSE
- if(target.stat == DEAD || (target.status_flags & FAKEDEATH))
- return TRUE
- return FALSE
-
-/datum/psionic_power/redaction/invoke(var/mob/living/user, var/mob/living/target)
- if(check_dead(target))
- return FALSE
- . = ..()
-
-/datum/psionic_power/redaction/skinsight
- name = "Skinsight"
- cost = 3
- cooldown = 30
- use_grab = TRUE
- min_rank = PSI_RANK_OPERANT
- use_description = "Grab a patient, target the chest, then switch to help intent and use the grab on them to perform a check for wounds and damage."
-
-/datum/psionic_power/redaction/skinsight/invoke(var/mob/living/user, var/mob/living/target)
- if(user.zone_sel.selecting != BP_CHEST)
- return FALSE
- . = ..()
- if(.)
- user.visible_message(SPAN_NOTICE("\The [user] rests a hand on \the [target]."))
- health_scan_mob(target, user, TRUE)
- return TRUE
-
-/datum/psionic_power/redaction/mend
- name = "Mend"
- cost = 7
- cooldown = 50
- use_melee = TRUE
- min_rank = PSI_RANK_OPERANT
- use_description = "Target a patient while on help intent at melee range to mend a variety of maladies, such as bleeding, or broken bones. Zone selection will affect which limb will be healed. Higher ranks in this faculty allow you to mend a wider range of problems."
-
-/datum/psionic_power/redaction/mend/invoke(var/mob/living/user, var/mob/living/carbon/human/target)
- if(!istype(user) || !istype(target))
- return FALSE
- . = ..()
- if(.)
- var/obj/item/organ/external/E = target.get_organ(user.zone_sel.selecting)
-
- if(!E || E.is_stump())
- to_chat(user, SPAN_WARNING("They are missing that limb."))
- return TRUE
-
- if(BP_IS_ROBOTIC(E))
- to_chat(user, SPAN_WARNING("That limb is prosthetic."))
- return TRUE
-
- user.visible_message(SPAN_NOTICE("\The [user] rests a hand on \the [target]'s [E.name]..."))
- to_chat(target, SPAN_NOTICE("A healing warmth suffuses you."))
-
- var/redaction_rank = user.psi.get_rank(PSI_REDACTION)
- var/pk_rank = user.psi.get_rank(PSI_PSYCHOKINESIS)
- if(pk_rank >= PSI_RANK_LATENT && redaction_rank >= PSI_RANK_MASTER)
- var/removal_size = Clamp(5-pk_rank, 0, 5)
- var/valid_objects = list()
- for(var/thing in E.implants)
- var/obj/imp = thing
- if(imp.w_class >= removal_size && !istype(imp, /obj/item/implant))
- valid_objects += imp
- if(LAZYLEN(valid_objects))
- var/removing = pick(valid_objects)
- target.remove_implant(removing, TRUE)
- to_chat(user, SPAN_NOTICE("You extend a tendril of psychokinetic-redactive power and carefully tease \the [removing] free of \the [E]."))
- return TRUE
-
- if(redaction_rank >= PSI_RANK_MASTER)
- if(E.status & ORGAN_ARTERY_CUT)
- to_chat(user, SPAN_NOTICE("You painstakingly mend the torn veins in \the [E], stemming the internal bleeding."))
- E.status &= ~ORGAN_ARTERY_CUT
- return TRUE
- if((E.tendon_status() & TENDON_CUT) && E.tendon.can_recover())
- to_chat(user, SPAN_NOTICE("You interleave and repair the severed tendon in \the [E]."))
- E.tendon.rejuvenate()
- return TRUE
- if(E.status & ORGAN_BROKEN)
- to_chat(user, SPAN_NOTICE("You coax shattered bones to come together and fuse, mending the break."))
- E.status &= ~ORGAN_BROKEN
- E.stage = 0
- return TRUE
-
- for(var/datum/wound/W in E.wounds)
- if(W.bleeding())
- if(redaction_rank >= PSI_RANK_MASTER || W.wound_damage() < 30)
- to_chat(user, SPAN_NOTICE("You knit together severed veins and broken flesh, stemming the bleeding."))
- W.bleed_timer = 0
- W.clamped = TRUE
- E.status &= ~ORGAN_BLEEDING
- return TRUE
- else
- to_chat(user, SPAN_WARNING("This [W.desc] is beyond your power to heal."))
-
- if(redaction_rank >= PSI_RANK_GRANDMASTER)
- for(var/obj/item/organ/internal/I in E.internal_organs)
- if(!BP_IS_ROBOTIC(I) && I.damage > 0)
- to_chat(user, SPAN_NOTICE("You encourage the damaged tissue of \the [I] to repair itself."))
- var/heal_rate = redaction_rank
- I.damage = max(0, I.damage - rand(heal_rate,heal_rate*2))
- return TRUE
-
- to_chat(user, SPAN_NOTICE("You can find nothing within \the [target]'s [E.name] to mend."))
- return FALSE
-
-/datum/psionic_power/redaction/cleanse
- name = "Cleanse"
- cost = 9
- cooldown = 60
- use_melee = TRUE
- min_rank = PSI_RANK_GRANDMASTER
- use_description = "Target a patient while on help intent at melee range to cleanse radiation and genetic damage from a patient."
-
-/datum/psionic_power/redaction/cleanse/invoke(var/mob/living/user, var/mob/living/carbon/human/target)
- if(!istype(user) || !istype(target))
- return FALSE
- . = ..()
- if(.)
- // No messages, as Mend procs them even if it fails to heal anything, and Cleanse is always checked after Mend.
- var/removing = rand(20,25)
- if(target.total_radiation)
- to_chat(user, SPAN_NOTICE("You repair some of the radiation-damaged tissue within \the [target]..."))
- if(target.total_radiation > removing)
- target.total_radiation -= removing
- else
- target.total_radiation = 0
- return TRUE
- if(target.getCloneLoss())
- to_chat(user, SPAN_NOTICE("You stitch together some of the mangled DNA within \the [target]..."))
- if(target.getCloneLoss() >= removing)
- target.adjustCloneLoss(-removing)
- else
- target.adjustCloneLoss(-(target.getCloneLoss()))
- return TRUE
- to_chat(user, SPAN_NOTICE("You can find no genetic damage or radiation to heal within \the [target]."))
- return TRUE
-
-/datum/psionic_power/revive
- name = "Revive"
- cost = 25
- cooldown = 80
- use_grab = TRUE
- min_rank = PSI_RANK_PARAMOUNT
- faculty = PSI_REDACTION
- use_description = "Obtain a grab on a dead target, target the head, then select help intent and use the grab against them to attempt to bring them back to life. The process is lengthy and failure is punished harshly."
- admin_log = FALSE
-
-/datum/psionic_power/revive/invoke(var/mob/living/user, var/mob/living/target)
- if(!isliving(target) || !istype(target) || user.zone_sel.selecting != BP_HEAD)
- return FALSE
- . = ..()
- if(.)
- if(target.stat != DEAD && !(target.status_flags & FAKEDEATH))
- to_chat(user, SPAN_WARNING("This person is already alive!"))
- return TRUE
-
- if((world.time - target.timeofdeath) > 6000)
- 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..."))
- if(!do_after(user, 100, target, 0, 1))
- user.psi.backblast(rand(10,25))
- return TRUE
-
- for(var/mob/abstract/observer/G in dead_mob_list)
- if(G.mind && G.mind.current == target && G.client)
- to_chat(G, FONT_LARGE(SPAN_NOTICE("Your body has been revived, Re-Enter Corpse to return to it.")))
- break
- to_chat(target, FONT_LARGE(SPAN_NOTICE("Life floods back into your body!")))
- target.visible_message(SPAN_NOTICE("\The [target] shudders violently!"))
- target.adjustOxyLoss(-rand(15,20))
- target.basic_revival()
- return TRUE
diff --git a/code/modules/psionics/interface/ui_hub.dm b/code/modules/psionics/interface/ui_hub.dm
index decf4a61f8d..e899336a181 100644
--- a/code/modules/psionics/interface/ui_hub.dm
+++ b/code/modules/psionics/interface/ui_hub.dm
@@ -1,6 +1,6 @@
/obj/screen/psi/hub
name = "Psi"
- icon_state = "psi_suppressed"
+ icon_state = "psi_active"
screen_loc = "EAST-1:28,CENTER-3:11"
hidden = FALSE
maptext_x = 6
@@ -13,7 +13,6 @@
START_PROCESSING(SSprocessing, src)
/obj/screen/psi/hub/update_icon()
-
if(!owner.psi)
return
@@ -30,25 +29,68 @@
return
if(!owner.psi)
return
- maptext = "[round((owner.psi.stamina/owner.psi.max_stamina)*100)]%"
+ maptext = SMALL_FONTS(7, "[round((owner.psi.stamina/owner.psi.max_stamina)*100)]%")
update_icon()
/obj/screen/psi/hub/Click(var/location, var/control, var/params)
- var/list/click_params = params2list(params)
- if(click_params["shift"])
- owner.show_psi_assay(owner)
- return
+ ui_interact(owner)
+ update_icon()
- if(owner.psi.suppressed && owner.psi.stun)
- to_chat(owner, "You are dazed and reeling, and cannot muster enough focus to do that!")
- return
+/obj/screen/psi/hub/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if (!ui)
+ ui = new(user, src, "PsionicShop", "Psionic Point Shop", 400, 500)
+ ui.open()
- 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
- sound_to(owner, sound('sound/effects/psi/power_unlock.ogg'))
- owner.psi.show_auras()
- update_icon()
\ No newline at end of file
+/obj/screen/psi/hub/ui_state(mob/user)
+ return conscious_state
+
+/obj/screen/psi/hub/ui_status(mob/user, datum/ui_state/state)
+ return UI_INTERACTIVE
+
+/obj/screen/psi/hub/ui_data(mob/user)
+ var/list/data = list()
+ var/owner_rank = owner.psi.get_rank()
+ data["available_psionics"] = list()
+ data["psi_rank"] = psychic_ranks_to_strings[owner_rank]
+ data["psi_points"] = owner.psi.psi_points
+ data["bought_powers"] = owner.psi.psionic_powers
+ for(var/singleton/psionic_power/P in GET_SINGLETON_SUBTYPE_LIST(/singleton/psionic_power))
+ if(HAS_FLAG(P.ability_flags, PSI_FLAG_SPECIAL) && !(P.type in owner.psi.psionic_powers))
+ continue
+ if(owner_rank < P.minimum_rank)
+ continue
+ /// Apex and Limitless abilities are automatically given, but we want them to have said abilities in the point shop so the users know what they do.
+ if(owner_rank < PSI_RANK_APEX && HAS_FLAG(P.ability_flags, PSI_FLAG_APEX))
+ continue
+ if(owner_rank < PSI_RANK_LIMITLESS && HAS_FLAG(P.ability_flags, PSI_FLAG_LIMITLESS))
+ continue
+ if(NOT_FLAG(P.ability_flags, PSI_FLAG_CANON))
+ if(owner_rank < PSI_RANK_HARMONIOUS && HAS_FLAG(P.ability_flags, PSI_FLAG_EVENT))
+ continue
+ if(owner_rank < PSI_RANK_HARMONIOUS && HAS_FLAG(P.ability_flags, PSI_FLAG_ANTAG))
+ continue
+ data["available_psionics"] += list(
+ list(
+ "name" = P.name,
+ "desc" = P.desc,
+ "point_cost" = P.point_cost,
+ "minimum_rank" = P.minimum_rank,
+ "path" = P.type
+ )
+ )
+ return data
+
+/obj/screen/psi/hub/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return TRUE
+
+ switch(action)
+ if("buy")
+ var/psionic_path = text2path(params["buy"])
+ if(ispath(psionic_path))
+ var/singleton/psionic_power/P = GET_SINGLETON(psionic_path)
+ P.apply(owner)
+ to_chat(owner, SPAN_NOTICE("You are now capable of using [P.name]."))
+ return TRUE
diff --git a/code/modules/psionics/mob/armour.dm b/code/modules/psionics/mob/armour.dm
new file mode 100644
index 00000000000..f8938c8143d
--- /dev/null
+++ b/code/modules/psionics/mob/armour.dm
@@ -0,0 +1,13 @@
+/datum/component/armor/psionic
+ full_block_message = "You block the blow with your mind!"
+ partial_block_message = "You soften the blow with your mind!"
+
+/datum/component/armor/psionic/Initialize(list/armor, armor_type)
+ . = ..()
+ var/datum/psi_complexus/PC = parent
+ PC.armor_component = src
+
+/datum/component/armor/psionic/Destroy()
+ var/datum/psi_complexus/PC = parent
+ PC.armor_component = null
+ return ..()
diff --git a/code/modules/psionics/mob/mob.dm b/code/modules/psionics/mob/mob.dm
index 5455c80dec1..0b95e90b064 100644
--- a/code/modules/psionics/mob/mob.dm
+++ b/code/modules/psionics/mob/mob.dm
@@ -5,16 +5,17 @@
..()
if(psi)
psi.update(TRUE)
- if(!psi.suppressed)
- psi.show_auras()
/mob/living/Destroy()
QDEL_NULL(psi)
. = ..()
-/mob/living/proc/set_psi_rank(var/faculty, var/rank, var/take_larger, var/defer_update, var/temporary)
+/mob/living/proc/set_psi_rank(var/rank, var/defer_update, var/temporary)
+ if(HAS_TRAIT(src, TRAIT_PSIONICALLY_DEAF))
+ to_chat(src, SPAN_WARNING("Something tingles in your head."))
+ 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)
+ var/current_rank = psi.get_rank()
+ if(current_rank != rank && current_rank < rank)
+ psi.set_rank(rank, defer_update, temporary)
diff --git a/code/modules/psionics/mob/mob_assay.dm b/code/modules/psionics/mob/mob_assay.dm
deleted file mode 100644
index 18239838bd6..00000000000
--- a/code/modules/psionics/mob/mob_assay.dm
+++ /dev/null
@@ -1,79 +0,0 @@
-/mob/living/proc/show_psi_assay(var/mob/viewer)
-
- if(!viewer) viewer = usr
-
- var/use_He_is = "You are"
- var/use_He_has = "You have"
- if(istype(machine) || viewer != src)
- use_He_is = "[get_pronoun("He")] [get_pronoun("is")]"
- use_He_has = "[get_pronoun("He")] [get_pronoun("has")]"
-
- 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
-
- if(viewer != usr && thralls.is_antagonist(mind) && ishuman(viewer))
- var/mob/living/H = viewer
- if(H.psi && H.psi.get_rank(PSI_REDACTION) >= PSI_RANK_GRANDMASTER)
- dat += "Their mind has been cored like an apple, and enslaved by another operant psychic."
-
- if(!use_rating)
- switch(effective_rating)
- if(1)
- use_rating = "[effective_rating]-Epsilon"
- rating_descriptor = "This indicates the presence of minor latent psi potential with little or no operant capabilities."
- if(2)
- use_rating = "[effective_rating]-Delta"
- rating_descriptor = "This indicates the presence of minor psi capabilities of the Operant rank or higher."
- if(3)
- use_rating = "[effective_rating]-Gamma"
- rating_descriptor = "This indicates the presence of psi capabilities of the Master rank or higher."
- if(4)
- use_rating = "[effective_rating]-Beta"
- rating_descriptor = "This indicates the presence of significant psi capabilities of the Grandmaster rank or higher."
- if(5)
- use_rating = "[effective_rating]-Alpha"
- rating_descriptor = "This indicates the presence of major psi capabilities of the Paramount Grandmaster rank or higher."
- else
- use_rating = "[effective_rating]-Lambda"
- rating_descriptor = "This indicates the presence of trace latent psi capabilities."
-
- dat += "[use_He_has] an overall psi rating of [use_rating].
[rating_descriptor]
"
-
- dat += "[use_He_is] currently [psi.suppressed ? "suppressing" : "not suppressing"] your psychic operancy.
"
- dat += "[use_He_has] [psi.stamina]/[psi.max_stamina] psi stamina remaining.
"
- dat += "
"
-
- for(var/faculty_id in psi.ranks)
- var/datum/psionic_faculty/faculty = SSpsi.get_faculty(faculty_id)
- if(psi.ranks[faculty.id] > 0)
- dat += "[use_He_is] assayed at the rank of [psychic_ranks_to_strings[psi.ranks[faculty.id]]] for the [faculty.name] faculty.
"
- else
- dat += "[use_He_has] no notable power within the [faculty.name] faculty.
"
- dat += "
"
-
- if(viewer == usr)
- dat += "Psi-power Usage |
"
- for(var/faculty_id in psi.ranks)
- var/list/check_powers = psi.get_powers_by_faculty(faculty_id)
- if(LAZYLEN(check_powers))
- var/datum/psionic_faculty/faculty = SSpsi.get_faculty(faculty_id)
- dat += "| [use_He_has] access to the following psi-powers within the [faculty.name] faculty: |
"
- for(var/datum/psionic_power/power in check_powers)
- dat += "| [power.name] | [power.use_description] |
"
- dat += "
"
-
- var/datum/browser/popup = new(viewer, "psi_assay_\ref[src]", "Psi-Assay")
- popup.set_content(jointext(dat,null))
- popup.open()
diff --git a/code/modules/psionics/mob/mob_helpers.dm b/code/modules/psionics/mob/mob_helpers.dm
index 0d7393cc0f2..3b06d4dcc9f 100644
--- a/code/modules/psionics/mob/mob_helpers.dm
+++ b/code/modules/psionics/mob/mob_helpers.dm
@@ -6,21 +6,24 @@
return psiaug && !psiaug.is_broken()
/mob/living/proc/is_psi_blocked()
- return !can_commune()
+ return !has_psionics()
/mob/living/carbon/is_psi_blocked()
- if(!psi && !has_psi_aug())
- if(isSynthetic())
- return SPAN_ALIEN("Reaching out, your mind grasps at nothing.")
- if (isvaurca(src))
- return SPAN_CULT("You reach out into the Nlom; your call sails right through and yields no response.")
- if (is_diona())
- return SPAN_ALIEN("[src]'s mind is incompatible, formless.")
+ if(HAS_TRAIT(src, TRAIT_PSIONICALLY_DEAF))
+ return SPAN_WARNING("[src]'s mind is inaccessible, like hitting a brick wall.")
for (var/obj/item/implant/mindshield/I in src)
if (I.implanted)
return SPAN_WARNING("[src]'s mind is inaccessible, like hitting a brick wall.")
return FALSE
+/mob/living/proc/has_zona_bovinae()
+ return TRUE
+
+/mob/living/carbon/has_zona_bovinae()
+ if(HAS_TRAIT(src, TRAIT_PSIONICALLY_DEAF))
+ return FALSE
+ return TRUE
+
/mob/living/proc/is_psi_pingable()
return !is_psi_blocked()
diff --git a/code/modules/psionics/mob/mob_interactions.dm b/code/modules/psionics/mob/mob_interactions.dm
deleted file mode 100644
index 2496604bc4d..00000000000
--- a/code/modules/psionics/mob/mob_interactions.dm
+++ /dev/null
@@ -1,37 +0,0 @@
-#define INVOKE_PSI_POWERS(holder, powers, target, return_on_invocation) \
- if(holder && holder.psi && holder.psi.can_use()) { \
- for(var/thing in powers) { \
- var/datum/psionic_power/power = thing; \
- var/obj/item/result = power.invoke(holder, target); \
- if(result) { \
- power.handle_post_power(holder, target); \
- if(istype(result)) { \
- sound_to(holder, sound('sound/effects/psi/power_evoke.ogg')); \
- LAZYADD(holder.psi.manifested_items, result); \
- holder.put_in_hands(result); \
- } \
- return return_on_invocation; \
- } \
- } \
- }
-
-/mob/living/UnarmedAttack(var/atom/A, var/proximity)
- . = ..()
- if(. && psi)
- INVOKE_PSI_POWERS(src, psi.get_melee_powers(SSpsi.faculties_by_intent[a_intent]), A, FALSE)
-
-/mob/living/RangedAttack(var/atom/A, var/params)
- if(psi)
- INVOKE_PSI_POWERS(src, psi.get_ranged_powers(SSpsi.faculties_by_intent[a_intent]), A, TRUE)
- . = ..()
-
-/mob/living/proc/check_psi_grab(var/obj/item/grab/grab)
- if(psi && ismob(grab.affecting))
- INVOKE_PSI_POWERS(src, psi.get_grab_powers(SSpsi.faculties_by_intent[a_intent]), grab.affecting, FALSE)
-
-/mob/living/attack_empty_hand(var/bp_hand)
- if(psi)
- INVOKE_PSI_POWERS(src, psi.get_manifestations(), src, FALSE)
- . = ..()
-
-#undef INVOKE_PSI_POWERS
\ No newline at end of file
diff --git a/code/modules/shareddream/dream_entry.dm b/code/modules/shareddream/dream_entry.dm
index ae8807d1f97..6250b748db1 100644
--- a/code/modules/shareddream/dream_entry.dm
+++ b/code/modules/shareddream/dream_entry.dm
@@ -17,7 +17,7 @@ var/list/dream_entries = list()
// If they're an Unconsious person with the abillity to do Skrellepathy.
// If either changes, they should be nocked back to the real world.
var/mob/living/carbon/human/srom_puller = srom_pulled_by?.resolve()
- if((can_commune() || (srom_puller && Adjacent(srom_puller))) && stat == UNCONSCIOUS && sleeping > 1)
+ if((has_psionics() || (srom_puller && Adjacent(srom_puller))) && stat == UNCONSCIOUS && sleeping > 1)
if(!istype(bg) && client) // Don't spawn a brainghost if we're not logged in.
bg = new /mob/living/brain_ghost(src) // Generate a new brainghost.
if(isnull(bg)) // Prevents you from getting kicked if the brain ghost didn't spawn - geeves
@@ -30,7 +30,7 @@ var/list/dream_entries = list()
to_chat(bg, "Whilst in shared dreaming, you find it difficult to hide your secrets.")
if(willfully_sleeping)
to_chat(bg, "To wake up, use the \"Awaken\" verb in the IC tab.")
- if(!srom_pulling && can_commune())
+ if(!srom_pulling && has_psionics())
var/obj/item/grab/G = r_hand
if(!G)
G = l_hand
@@ -50,7 +50,7 @@ var/list/dream_entries = list()
if(istype(bg) || force_wakeup)
// If we choose to be asleep, keep sleeping.
if(willfully_sleeping && sleeping && stat == UNCONSCIOUS)
- if(can_commune() || srom_pulled_by)
+ if(has_psionics() || srom_pulled_by)
sleeping = 5
return
for(var/thing in SSpsi.processing)
diff --git a/html/changelogs/mattatlas-kyoushikimurasaki.yml b/html/changelogs/mattatlas-kyoushikimurasaki.yml
new file mode 100644
index 00000000000..755752b6e53
--- /dev/null
+++ b/html/changelogs/mattatlas-kyoushikimurasaki.yml
@@ -0,0 +1,51 @@
+################################
+# Example Changelog File
+#
+# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb.
+#
+# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.)
+# When it is, any changes listed below will disappear.
+#
+# Valid Prefixes:
+# bugfix
+# wip (For works in progress)
+# tweak
+# soundadd
+# sounddel
+# rscadd (general adding of nice things)
+# rscdel (general deleting of nice things)
+# imageadd
+# imagedel
+# maptweak
+# spellcheck (typo fixes)
+# experiment
+# balance
+# admin
+# backend
+# security
+# refactor
+#################################
+
+# Your name.
+author: MattAtlas, Butterrobber202
+
+# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again.
+delete-after: True
+
+# Any changes you've made. See valid prefix list above.
+# INDENT WITH TWO SPACES. NOT TABS. SPACES.
+# SCREW THIS UP AND IT WON'T WORK.
+# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries.
+# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog.
+changes:
+ - experiment: "Psionics have been reworked to use the Technomancer ability system. This means that you get physical ability objects in your hand to use (in hand, melee, or ranged)."
+ - rscadd: "You can shift click psionic abilities at the top to obtain info about them."
+ - rscadd: "Suppressing your psionics is no longer a thing. Your psionics are only suppressed if you are stunned through overusing them, and they will automatically unsuppress once you recover. Psionic suppression has been introduced instead: using it will hide you from Psi-Search."
+ - rscadd: "Added a psionic point shop, opened by clicking your psionic hub icon. This is also usable by non-antags, but they can't buy anything: it's still useful to use it to know what your powers do and how to use them!"
+ - rscadd: "Antagonists get points usable in the psionic point shop. This varies per antag: Loners get 8, borer hosts get 3 (increased by borers with Advance Psionics)."
+ - tweak: "Reworked the Psi ranks. The canon ones are Psionically Sensitive (Skrell), Psionically Harmonious (exceptional Skrell born with better psionic control), Psionic Apex (unknown to everyone, really). There's also an admin-only non-canon Limitless level."
+ - rscadd: "Every 30 minutes, psionics will be pinged in their chat about their emotional state, which can be one from a selection. A few minutes after, the responses will be tallied together and the winning emotion will be broadcasted. Higher psionic rank means higher vote value."
+ - rscadd: "Added a psionic jumpstarter to the antagonist uplink for 19 telecrystals. It will boost you to Psionically Harmonious, but with only 8 psi points."
+ - rscadd: "Added a truckload of new psionic abilities. No, really. They're a lot."
+ - rscadd: "Skrell may choose additional psionics to spawn with in their loadout."
+ - rscadd: "Added mechanical support for the lack of a Zona Bovinae, mainly for vaurcae and dionae. In certain cases, a Zona Bovinae may even be removed from someone..."
diff --git a/icons/mob/items/lefthand_spells.dmi b/icons/mob/items/lefthand_spells.dmi
index 3c7ab3d4af5..9b7bdd5eda1 100644
Binary files a/icons/mob/items/lefthand_spells.dmi and b/icons/mob/items/lefthand_spells.dmi differ
diff --git a/icons/mob/items/righthand_spells.dmi b/icons/mob/items/righthand_spells.dmi
index fe934f64850..21c50cac9f8 100644
Binary files a/icons/mob/items/righthand_spells.dmi and b/icons/mob/items/righthand_spells.dmi differ
diff --git a/icons/mob/screen_spells.dmi b/icons/mob/screen_spells.dmi
index 4dd42bac21d..cb642db9aaa 100644
Binary files a/icons/mob/screen_spells.dmi and b/icons/mob/screen_spells.dmi differ
diff --git a/icons/obj/projectiles.dmi b/icons/obj/projectiles.dmi
index 9ca7b95dbe5..d36f9006e48 100644
Binary files a/icons/obj/projectiles.dmi and b/icons/obj/projectiles.dmi differ
diff --git a/tgui/packages/tgui/interfaces/PsionicShop.tsx b/tgui/packages/tgui/interfaces/PsionicShop.tsx
new file mode 100644
index 00000000000..fa1182e0a2a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PsionicShop.tsx
@@ -0,0 +1,113 @@
+import { useBackend, useLocalState } from '../backend';
+import { BlockQuote, Box, Button, Input, NoticeBox, Section } from '../components';
+import { Window } from '../layouts';
+
+export type PsiData = {
+ psi_rank: string;
+ psi_points: number;
+ available_psionics: Psionic[];
+ bought_powers: string[];
+};
+
+type Psionic = {
+ name: string;
+ desc: string;
+ point_cost: number;
+ minimum_rank: string;
+ path: string;
+};
+
+export const PsionicShop = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const [searchTerm, setSearchTerm] = useLocalState(
+ context,
+ `searchTerm`,
+ ``
+ );
+
+ return (
+
+
+ {
+ setSearchTerm(value);
+ }}
+ value={searchTerm}
+ />
+ }>
+
+ You are{' '}
+
+ {data.psi_rank}
+
+ .
+
+ {data.psi_points ? (
+
+ You have{' '}
+
+ {data.psi_points}
+ {' '}
+ points left.
+
+ ) : (
+ ''
+ )}
+
+ {data.available_psionics && data.available_psionics.length ? (
+
+ ) : (
+ There are no psionics available.
+ )}
+
+
+
+ );
+};
+
+export const PsionicsList = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const [searchTerm, setSearchTerm] = useLocalState(
+ context,
+ `searchTerm`,
+ ``
+ );
+
+ return (
+
+ {data.available_psionics
+ .filter(
+ (psi) => psi.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1
+ )
+ .map((psi) => (
+ data.psi_points ||
+ data.bought_powers.includes(psi.path)
+ }
+ onClick={() => act('buy', { buy: psi.path })}
+ />
+ }>
+ {psi.desc}
+
+ ))}
+
+ );
+};