From 7241a2485a84c44940e3fd92410bd5f8851c7770 Mon Sep 17 00:00:00 2001 From: Mycah142 Date: Mon, 18 May 2026 21:55:56 -0400 Subject: [PATCH 01/10] Merge branch 'psionics-port' of https://github.com/Humonitarian/CEV-Eris into psionics-port --- cev_eris.dme | 37 +++ code/__DEFINES/damage_organs.dm | 3 +- code/__DEFINES/gamemode.dm | 3 + code/__DEFINES/inventory_sizes.dm | 5 +- code/__DEFINES/materials.dm | 6 +- code/__DEFINES/maths.dm | 4 + code/__DEFINES/mutations.dm | 2 + code/__DEFINES/psi.dm | 17 ++ code/__DEFINES/subsystems-priority.dm | 1 + code/__HELPERS/matrices.dm | 28 +- code/__HELPERS/overlays.dm | 26 ++ code/__HELPERS/turfs.dm | 2 +- code/_onclick/click.dm | 5 + .../hud/screen_objects/base_screen_objects.dm | 28 +- code/_onclick/other_mobs.dm | 12 + code/controllers/subsystems/processing/psi.dm | 46 +++ code/datums/security_state.dm | 7 + code/game/antagonist/antagonist.dm | 2 +- .../antagonist/station/psionic/paramount.dm | 129 ++++++++ code/game/antagonist/station/thrall.dm | 30 ++ code/game/jobs/job/job.dm | 34 +++ code/game/objects/items/stacks/stack.dm | 9 + code/game/objects/items/weapons/setupdisk.dm | 15 + code/game/objects/items/weapons/tools/karl.dm | 2 +- code/modules/genetics/mutations_T2.dm | 6 + code/modules/genetics/mutations_T4.dm | 5 + .../mob/living/carbon/human/human_damage.dm | 2 +- .../mob/living/carbon/human/human_organs.dm | 6 + .../carbon/superior_animal/roach/roachmind.dm | 6 +- code/modules/mob/mob.dm | 2 + code/modules/mob/mob_grab.dm | 8 +- .../organs/internal/internal_wounds/eyes.dm | 2 +- code/modules/projectiles/guns/energy/laser.dm | 2 +- code/modules/projectiles/projectile.dm | 4 +- code/modules/projectiles/projectile/beams.dm | 39 ++- code/modules/psionics/complexus/complexus.dm | 99 ++++++ .../psionics/complexus/complexus_helpers.dm | 122 ++++++++ .../psionics/complexus/complexus_latency.dm | 18 ++ .../complexus/complexus_power_cache.dm | 49 +++ .../psionics/complexus/complexus_process.dm | 270 ++++++++++++++++ .../psionics/complexus/complexus_topic.dm | 20 ++ .../psionics/equipment/cerebro_enhancers.dm | 167 ++++++++++ .../equipment/foundation_implanter.dm | 18 ++ .../psionics/equipment/foundation_weapon.dm | 24 ++ code/modules/psionics/equipment/implant.dm | 104 +++++++ code/modules/psionics/equipment/null_ammo.dm | 23 ++ code/modules/psionics/equipment/psimeter.dm | 72 +++++ code/modules/psionics/equipment/psimonitor.dm | 117 +++++++ code/modules/psionics/equipment/psipower.dm | 62 ++++ .../psionics/equipment/psipower_blade.dm | 25 ++ .../psionics/equipment/psipower_tinker.dm | 5 + .../modules/psionics/equipment/psipower_tk.dm | 105 +++++++ code/modules/psionics/events/_psi.dm | 24 ++ code/modules/psionics/events/mini_spasm.dm | 67 ++++ code/modules/psionics/events/psi_balm.dm | 20 ++ code/modules/psionics/events/psi_wail.dm | 17 ++ code/modules/psionics/faculties/_faculty.dm | 11 + code/modules/psionics/faculties/_power.dm | 49 +++ code/modules/psionics/faculties/coercion.dm | 287 ++++++++++++++++++ .../modules/psionics/faculties/energistics.dm | 115 +++++++ .../psionics/faculties/psychokinesis.dm | 93 ++++++ code/modules/psionics/faculties/redaction.dm | 186 ++++++++++++ code/modules/psionics/interface/ui.dm | 21 ++ code/modules/psionics/interface/ui_hub.dm | 71 +++++ code/modules/psionics/interface/ui_toggles.dm | 45 +++ code/modules/psionics/mob/mob.dm | 73 +++++ code/modules/psionics/mob/mob_assay.dm | 93 ++++++ code/modules/psionics/mob/mob_interactions.dm | 41 +++ code/modules/psionics/null/_null.dm | 30 ++ code/modules/psionics/null/chemistry.dm | 88 ++++++ code/modules/psionics/null/flooring.dm | 20 ++ code/modules/psionics/null/material.dm | 32 ++ code/modules/psionics/null/material_sheet.dm | 22 ++ code/modules/psionics/null/material_weapon.dm | 11 + code/modules/psionics/null/turf_floor.dm | 7 + code/modules/psionics/null/turf_wall.dm | 18 ++ code/modules/psionics/null/~null.dm | 3 + code/modules/sanity/sanity_mob.dm | 8 +- html/changelogs/archive/2025-03.yml | 42 +++ icons/HUD/psi.dmi | Bin 0 -> 6050 bytes icons/effects/psi_aura_small.dmi | Bin 0 -> 1301 bytes icons/effects/psi_aura_small3.dmi | Bin 0 -> 3594 bytes icons/obj/guns/foundation.dmi | Bin 0 -> 938 bytes icons/obj/machines/psimeter.dmi | Bin 0 -> 1213 bytes icons/obj/psychic_powers.dmi | Bin 0 -> 1551 bytes sound/effects/psiblast.ogg | Bin 0 -> 41353 bytes 86 files changed, 3299 insertions(+), 30 deletions(-) create mode 100644 code/__DEFINES/psi.dm create mode 100644 code/__HELPERS/overlays.dm create mode 100644 code/controllers/subsystems/processing/psi.dm create mode 100644 code/game/antagonist/station/psionic/paramount.dm create mode 100644 code/game/antagonist/station/thrall.dm create mode 100644 code/modules/psionics/complexus/complexus.dm create mode 100644 code/modules/psionics/complexus/complexus_helpers.dm create mode 100644 code/modules/psionics/complexus/complexus_latency.dm create mode 100644 code/modules/psionics/complexus/complexus_power_cache.dm create mode 100644 code/modules/psionics/complexus/complexus_process.dm create mode 100644 code/modules/psionics/complexus/complexus_topic.dm create mode 100644 code/modules/psionics/equipment/cerebro_enhancers.dm create mode 100644 code/modules/psionics/equipment/foundation_implanter.dm create mode 100644 code/modules/psionics/equipment/foundation_weapon.dm create mode 100644 code/modules/psionics/equipment/implant.dm create mode 100644 code/modules/psionics/equipment/null_ammo.dm create mode 100644 code/modules/psionics/equipment/psimeter.dm create mode 100644 code/modules/psionics/equipment/psimonitor.dm create mode 100644 code/modules/psionics/equipment/psipower.dm create mode 100644 code/modules/psionics/equipment/psipower_blade.dm create mode 100644 code/modules/psionics/equipment/psipower_tinker.dm create mode 100644 code/modules/psionics/equipment/psipower_tk.dm create mode 100644 code/modules/psionics/events/_psi.dm create mode 100644 code/modules/psionics/events/mini_spasm.dm create mode 100644 code/modules/psionics/events/psi_balm.dm create mode 100644 code/modules/psionics/events/psi_wail.dm create mode 100644 code/modules/psionics/faculties/_faculty.dm create mode 100644 code/modules/psionics/faculties/_power.dm create mode 100644 code/modules/psionics/faculties/coercion.dm create mode 100644 code/modules/psionics/faculties/energistics.dm create mode 100644 code/modules/psionics/faculties/psychokinesis.dm create mode 100644 code/modules/psionics/faculties/redaction.dm create mode 100644 code/modules/psionics/interface/ui.dm create mode 100644 code/modules/psionics/interface/ui_hub.dm create mode 100644 code/modules/psionics/interface/ui_toggles.dm create mode 100644 code/modules/psionics/mob/mob.dm create mode 100644 code/modules/psionics/mob/mob_assay.dm create mode 100644 code/modules/psionics/mob/mob_interactions.dm create mode 100644 code/modules/psionics/null/_null.dm create mode 100644 code/modules/psionics/null/chemistry.dm create mode 100644 code/modules/psionics/null/flooring.dm create mode 100644 code/modules/psionics/null/material.dm create mode 100644 code/modules/psionics/null/material_sheet.dm create mode 100644 code/modules/psionics/null/material_weapon.dm create mode 100644 code/modules/psionics/null/turf_floor.dm create mode 100644 code/modules/psionics/null/turf_wall.dm create mode 100644 code/modules/psionics/null/~null.dm create mode 100644 icons/HUD/psi.dmi create mode 100644 icons/effects/psi_aura_small.dmi create mode 100644 icons/effects/psi_aura_small3.dmi create mode 100644 icons/obj/guns/foundation.dmi create mode 100644 icons/obj/machines/psimeter.dmi create mode 100644 icons/obj/psychic_powers.dmi create mode 100644 sound/effects/psiblast.ogg diff --git a/cev_eris.dme b/cev_eris.dme index ec1885774cd..c9f1afd544a 100644 --- a/cev_eris.dme +++ b/cev_eris.dme @@ -88,6 +88,7 @@ #include "code\__DEFINES\process_scheduler.dm" #include "code\__DEFINES\projectile_defines.dm" #include "code\__DEFINES\protect.dm" +#include "code\__DEFINES\psi.dm" #include "code\__DEFINES\qdel.dm" #include "code\__DEFINES\reagents.dm" #include "code\__DEFINES\research.dm" @@ -158,6 +159,7 @@ #include "code\__HELPERS\mobs.dm" #include "code\__HELPERS\nameof.dm" #include "code\__HELPERS\names.dm" +#include "code\__HELPERS\overlays.dm" #include "code\__HELPERS\sanitize_values.dm" #include "code\__HELPERS\screen_atoms.dm" #include "code\__HELPERS\shell.dm" @@ -369,6 +371,7 @@ #include "code\controllers\subsystems\processing\objs.dm" #include "code\controllers\subsystems\processing\processing.dm" #include "code\controllers\subsystems\processing\reagents.dm" +#include "code\controllers\subsystems\processing\psi.dm" #include "code\controllers\subsystems\processing\turf.dm" #include "code\controllers\subsystems\processing\organs\internal_wounds.dm" #include "code\controllers\subsystems\tickets\mentor_tickets.dm" @@ -675,6 +678,8 @@ #include "code\game\antagonist\station\inquisitor.dm" #include "code\game\antagonist\station\marshal.dm" #include "code\game\antagonist\station\rogue_ai.dm" +#include "code\game\antagonist\station\thrall.dm" +#include "code\game\antagonist\station\psionic\paramount.dm" #include "code\game\antagonist\station\revolutionary\excel_faction.dm" #include "code\game\antagonist\station\revolutionary\excelsior.dm" #include "code\game\area\ai_monitored.dm" @@ -2685,6 +2690,38 @@ #include "code\modules\projectiles\projectile\plasma.dm" #include "code\modules\projectiles\projectile\projectilegrenades.dm" #include "code\modules\projectiles\projectile\special.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\complexus\complexus_topic.dm" +#include "code\modules\psionics\equipment\cerebro_enhancers.dm" +#include "code\modules\psionics\equipment\foundation_implanter.dm" +#include "code\modules\psionics\equipment\foundation_weapon.dm" +#include "code\modules\psionics\equipment\implant.dm" +#include "code\modules\psionics\equipment\null_ammo.dm" +#include "code\modules\psionics\equipment\psimeter.dm" +#include "code\modules\psionics\equipment\psimonitor.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\faculties\_faculty.dm" +#include "code\modules\psionics\faculties\_power.dm" +#include "code\modules\psionics\faculties\coercion.dm" +#include "code\modules\psionics\faculties\energistics.dm" +#include "code\modules\psionics\faculties\psychokinesis.dm" +#include "code\modules\psionics\faculties\redaction.dm" +#include "code\modules\psionics\interface\ui.dm" +#include "code\modules\psionics\interface\ui_hub.dm" +#include "code\modules\psionics\interface\ui_toggles.dm" +#include "code\modules\psionics\mob\mob.dm" +#include "code\modules\psionics\mob\mob_assay.dm" +#include "code\modules\psionics\mob\mob_interactions.dm" +#include "code\modules\psionics\null\_null.dm" +#include "code\modules\psionics\null\~null.dm" #include "code\modules\pulsar_engine\_defines.dm" #include "code\modules\pulsar_engine\pulsar_consoles.dm" #include "code\modules\pulsar_engine\pulsar_events.dm" diff --git a/code/__DEFINES/damage_organs.dm b/code/__DEFINES/damage_organs.dm index 8a8295d3236..b34f7f2aa7d 100644 --- a/code/__DEFINES/damage_organs.dm +++ b/code/__DEFINES/damage_organs.dm @@ -110,7 +110,7 @@ #define BP_IS_REMOVED(org) (org.nature == MODIFICATION_REMOVED) #define BP_IS_ASSISTED(org) (org.nature == MODIFICATION_ASSISTED) #define BP_IS_LIFELIKE(org) (org.nature == MODIFICATION_LIFELIKE) - +#define BP_IS_CRYSTAL(org) (org.nature == MODIFICATION_CRYSTAL) // Organ defines. #define ORGAN_CUT_AWAY (1<<0) @@ -139,6 +139,7 @@ #define MODIFICATION_SILICON 2 // Fully robotic, no organic parts #define MODIFICATION_LIFELIKE 3 // Robotic, made to appear organic #define MODIFICATION_REMOVED 4 // Removed completly +#define MODIFICATION_CRYSTAL 5 // Doesn't sound healthy // Damage above this value must be repaired with surgery. #define ROBOLIMB_SELF_REPAIR_CAP 30 diff --git a/code/__DEFINES/gamemode.dm b/code/__DEFINES/gamemode.dm index 02ea42c1a8e..c88b36acf96 100644 --- a/code/__DEFINES/gamemode.dm +++ b/code/__DEFINES/gamemode.dm @@ -53,6 +53,8 @@ #define ROLE_BANTYPE_CARRION ROLE_CARRION #define ROLE_BANTYPE_CREW_SIDED ROLE_MARSHAL #define ROLE_BANTYPE_BLITZ ROLE_BLITZ +#define ROLE_BANTYPE_PARAMOUNT ROLE_PARAMOUNT +#define ROLE_BANTYPE_THRALL ROLE_THRALL // antag template macros. commented out ones are leftover from baycode #define ROLE_BORER "Cortical Borer" @@ -85,6 +87,7 @@ #define FACTION_EXCELSIOR "excelsior" #define FACTION_BORERS "borers" #define FACTION_SERBS "serbians" +#define FACTION_FOUNDERS "founders" #define FACTION_PIRATES "pirates" #define FACTION_NEOTHEOLOGY "neotheologists" diff --git a/code/__DEFINES/inventory_sizes.dm b/code/__DEFINES/inventory_sizes.dm index 1dbb22b9749..4f511282cfc 100644 --- a/code/__DEFINES/inventory_sizes.dm +++ b/code/__DEFINES/inventory_sizes.dm @@ -16,7 +16,8 @@ #define ITEM_SIZE_COLOSSAL 7 /// Something so large that it extends beyond the confines of its tile, (e.g. scrap beacon) #define ITEM_SIZE_TITANIC 8 - +/// Use this to forbid item from being placed in a container. +#define ITEM_SIZE_NO_CONTAINER INFINITY #define BASE_STORAGE_COST(w_class) (2**(w_class-1)) //1,2,4,8,16,... @@ -29,4 +30,4 @@ #define DEFAULT_HUGE_STORAGE BASE_STORAGE_CAPACITY(5) //40 after BASE_STORAGE_CAPACITY calculation #define DEFAULT_BULKY_STORAGE BASE_STORAGE_CAPACITY(4) //30 after BASE_STORAGE_CAPACITY calculation #define DEFAULT_NORMAL_STORAGE BASE_STORAGE_CAPACITY(3) //20 after BASE_STORAGE_CAPACITY calculation -#define DEFAULT_SMALL_STORAGE BASE_STORAGE_CAPACITY(2) //10 after BASE_STORAGE_CAPACITY calculation \ No newline at end of file +#define DEFAULT_SMALL_STORAGE BASE_STORAGE_CAPACITY(2) //10 after BASE_STORAGE_CAPACITY calculation diff --git a/code/__DEFINES/materials.dm b/code/__DEFINES/materials.dm index c6e71ef66e5..b6cc1f74d8a 100644 --- a/code/__DEFINES/materials.dm +++ b/code/__DEFINES/materials.dm @@ -28,6 +28,8 @@ #define MATERIAL_CARPET "carpet" #define MATERIAL_BIOMATTER "biomatter" #define MATERIAL_COMPRESSED "compressed matter" +#define MATERIAL_NULLGLASS "nullglass" +#define MATERIAL_CRYSTAL "crystal" #define MATERIAL_LIST list(\ MATERIAL_STEEL,\ @@ -57,7 +59,9 @@ MATERIAL_VOXALLOY,\ MATERIAL_CARPET,\ MATERIAL_BIOMATTER,\ - MATERIAL_COMPRESSED) + MATERIAL_COMPRESSED,\ + MATERIAL_NULLGLASS,\ + MATERIAL_CRYSTAL) #define PLASTEEL_COLOUR "#8e9799" //Color to exactly match concept art #b0bcbe diff --git a/code/__DEFINES/maths.dm b/code/__DEFINES/maths.dm index f663712b1ee..b9153f8b781 100644 --- a/code/__DEFINES/maths.dm +++ b/code/__DEFINES/maths.dm @@ -231,6 +231,10 @@ if(isnum(num)) return round(num)+1 +// Slightly more accurate than n_ceil +/proc/ceil(number) + return -round(-number) + // Find leftmost bit using bitshifting /proc/leftmost_bit(num) var/pos = 0 diff --git a/code/__DEFINES/mutations.dm b/code/__DEFINES/mutations.dm index eded0d60e36..bbd9e5b8de4 100644 --- a/code/__DEFINES/mutations.dm +++ b/code/__DEFINES/mutations.dm @@ -14,6 +14,7 @@ #define MUTATION_NOPRINTS /datum/mutation/t2/noprints #define MUTATION_ROACH_PHEROMONES /datum/mutation/t2/roach_pheromones #define MUTATION_SPIDER_PHEROMONES /datum/mutation/t2/spider_pheromones +#define MUTATION_PSI_LOW /datum/mutation/t2/psionic_compatibility_low #define MUTATION_REJECT /datum/mutation/t3/reject #define MUTATION_GREATER_HEALING /datum/mutation/t3/healing_factor @@ -29,6 +30,7 @@ #define MUTATION_PHAZING /datum/mutation/t4/phazing #define MUTATION_MORPH /datum/mutation/t4/morph #define MUTATION_REMOTESEE /datum/mutation/t4/remoteobserve +#define MUTATION_PSI_HIGH /datum/mutation/t4/psionic_compatibility_high // Sdisabilities. // TODO: To be replaced with status effects -- KIROV #define BLIND 0x1 diff --git a/code/__DEFINES/psi.dm b/code/__DEFINES/psi.dm new file mode 100644 index 00000000000..c03bbe7d3d3 --- /dev/null +++ b/code/__DEFINES/psi.dm @@ -0,0 +1,17 @@ +#define PSI_IMPLANT_AUTOMATIC "Security Level Derived" +#define PSI_IMPLANT_SHOCK "Issue Neural Shock" +#define PSI_IMPLANT_WARN "Issue Reprimand" +#define PSI_IMPLANT_LOG "Log Incident" +#define PSI_IMPLANT_DISABLED "Disabled" + +#define PSI_COERCION "coercion" +#define PSI_PSYCHOKINESIS "psychokinesis" +#define PSI_REDACTION "redaction" +#define PSI_ENERGISTICS "energistics" + +#define PSI_RANK_BLUNT 0 +#define PSI_RANK_LATENT 1 +#define PSI_RANK_OPERANT 2 +#define PSI_RANK_MASTER 3 +#define PSI_RANK_GRANDMASTER 4 +#define PSI_RANK_PARAMOUNT 5 \ No newline at end of file diff --git a/code/__DEFINES/subsystems-priority.dm b/code/__DEFINES/subsystems-priority.dm index 2494829a1ac..ad81013ee26 100644 --- a/code/__DEFINES/subsystems-priority.dm +++ b/code/__DEFINES/subsystems-priority.dm @@ -44,6 +44,7 @@ var/list/bitflags = list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 #define FIRE_PRIORITY_PROCESSING 30 // Generic datum processor. Replaces objects processor. #define FIRE_PRIORITY_GARBAGE 25 // Garbage collection. #define FIRE_PRIORITY_VINES 25 // Spreading vine effects. +#define FIRE_PRIORITY_PSYCHICS 25 // Psychic complexus processing. #define FIRE_PRIORITY_TURF 20 // Radioactive walls/blob. #define FIRE_PRIORITY_NANO 20 // Updates to nanoui uis. #define FIRE_PRIORITY_EVAC 20 // Processes the evac controller. diff --git a/code/__HELPERS/matrices.dm b/code/__HELPERS/matrices.dm index 5e66c27b4ba..1b0440d04b5 100644 --- a/code/__HELPERS/matrices.dm +++ b/code/__HELPERS/matrices.dm @@ -93,7 +93,6 @@ return list(1,0,0, 0,1,0, 0,0,1) //Moves all colors angle degrees around the color wheel while maintaining intensity of the color and not affecting whites -//TODO: Need a version that only affects one color (ie shift red to blue but leave greens and blues alone) /proc/color_rotation(angle) if(angle == 0) return color_identity() @@ -178,6 +177,33 @@ var/list/delta_index = list( else if (target.y > y) pixel_y += offset +/// Clears the matrix's a-f variables to identity. +/matrix/proc/Clear() + a = 1 + b = 0 + c = 0 + d = 0 + e = 1 + f = 0 + return src + +/// Runs Scale, Turn, and Translate if supplied parameters, then multiplies by others if set. +/matrix/proc/Update(scale_x, scale_y, rotation, offset_x, offset_y, list/others) + var/x_null = isnull(scale_x) + var/y_null = isnull(scale_y) + if (!x_null || !y_null) + Scale(x_null ? 1 : scale_x, y_null ? 1 : scale_y) + if (!isnull(rotation)) + Turn(rotation) + if (offset_x || offset_y) + Translate(offset_x || 0, offset_y || 0) + if (islist(others)) + for (var/other in others) + Multiply(other) + else if (others) + Multiply(others) + return src + //Does nothing /proc/color_matrix_identity() return list(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0) diff --git a/code/__HELPERS/overlays.dm b/code/__HELPERS/overlays.dm new file mode 100644 index 00000000000..abaa0d64c85 --- /dev/null +++ b/code/__HELPERS/overlays.dm @@ -0,0 +1,26 @@ +// These exist for Baycode compatibility + +/atom/proc/ClearOverlays() + cut_overlays() + +/atom/proc/AddOverlays(overlay) + overlays += overlay + +/atom/proc/CopyOverlays(atom/other, clear = FALSE) + if(clear) + cut_overlays() + overlays += other.overlays + +/image/proc/AddOverlays(sources) + overlays += sources + +/image/proc/CutOverlays(sources) + overlays -= sources + +/image/proc/ClearOverlays() + cut_overlays() + +/image/proc/CopyOverlays(atom/other, clear = FALSE) + if(clear) + cut_overlays() + overlays += other.overlays diff --git a/code/__HELPERS/turfs.dm b/code/__HELPERS/turfs.dm index 2eed4a83737..3dda693b027 100644 --- a/code/__HELPERS/turfs.dm +++ b/code/__HELPERS/turfs.dm @@ -46,7 +46,7 @@ return FALSE return TRUE -/proc/clear_interior(turf/T) +/proc/clear_interior(var/turf/T) if (turf_clear(T)) if (!turf_is_external(T)) return TRUE diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 1aa9dc9717e..29c760f5027 100755 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -159,12 +159,15 @@ to_chat(src, span_warning("You cannot use \the [W] while lying down!")) return 1 + message_admins("Turf check") // TODOPSI: REMOVE //Atoms on turfs (not on your person) // A is a turf or is on a turf, or in something on a turf (pen in a box); but not something in something on a turf (pen in a box in a backpack) sdepth = A.storage_depth_turf() if(isturf(A) || isturf(A.loc) || (sdepth != -1 && sdepth <= 1)) + message_admins("Passed check") // TODOPSI: REMOVE var/adjacent = A.Adjacent(src) if(adjacent) // see adjacent.dm + message_admins("Click is adjacent") // TODOPSI: REMOVE if(W) // Return 1 in attackby() to prevent afterattack() effects (when safely moving items for example) var/resolved = (SEND_SIGNAL_OLD(W, COMSIG_IATTACK, A, src, params)) || (SEND_SIGNAL_OLD(A, COMSIG_ATTACKBY, W, src, params)) @@ -180,9 +183,11 @@ return else // non-adjacent click if(W) + message_admins("Hand is occupied") // TODOPSI: REMOVE if(W.double_tact(src, A)) W.afterattack(A, src, 0, params) // 0: not Adjacent else + message_admins("We are trying a ranged attack") // TODOPSI: REMOVE setClickCooldown(DEFAULT_ATTACK_COOLDOWN) // no ranged spam RangedAttack(A, params) return 1 diff --git a/code/_onclick/hud/screen_objects/base_screen_objects.dm b/code/_onclick/hud/screen_objects/base_screen_objects.dm index cd645c38f36..1d1427d9658 100644 --- a/code/_onclick/hud/screen_objects/base_screen_objects.dm +++ b/code/_onclick/hud/screen_objects/base_screen_objects.dm @@ -303,13 +303,33 @@ icon_state = _icon_state parentmob = _parentmob -/atom/movable/screen/inventory/Click() +// BIG RISK CHANGE in PSIONICS - could potentially break fullauto like any change to clicks, as usual +/obj/screen/inventory/Click() // At this point in client Click() code we have passed the 1/10 sec check and little else // We don't even know if it's a middle click - if(!usr.can_click()) return TRUE - if(usr.stat || usr.paralysis || usr.stunned || usr.weakened) return TRUE + if(!usr.can_click()) + return TRUE + if(usr.stat || usr.paralysis || usr.stunned || usr.weakened) + return TRUE switch(name) - if("hand") usr:swap_hand() + if("r_hand") + if(iscarbon(usr)) + var/mob/living/carbon/C = usr + if(C.hand) + C.activate_hand("r") + else + C.attack_empty_hand(slot_r_hand) + if("l_hand") + if(iscarbon(usr)) + var/mob/living/carbon/C = usr + if(!C.hand) + C.activate_hand("l") + else + C.attack_empty_hand(slot_l_hand) + if("swap") + usr:swap_hand() + if("hand") + usr:swap_hand() else usr.attack_ui(slot_id) return TRUE diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm index bc6ddb7f732..4cbfbc12dac 100644 --- a/code/_onclick/other_mobs.dm +++ b/code/_onclick/other_mobs.dm @@ -33,6 +33,16 @@ // if(interaction_flags_atom & INTERACT_ATOM_ATTACK_HAND) . = _try_interact(user) + +/** + * Called when a mob attempts to use an empty hand on itself. + * + * **Parameters**: + * - `bp_hand` (string, `BP_R_HAND` or `BP_L_HAND`) - The targeted and used hand's bodypart slot. + */ +/mob/proc/attack_empty_hand(bp_hand) + return + //Return a non FALSE value to cancel whatever called this from propagating, if it respects it. /atom/proc/_try_interact(mob/user) if(is_admin(user) && isghost(user)) //admin abuse @@ -137,6 +147,8 @@ leap(A) return + . = ..() // living/RangedAttack -> psi powers + // if((LASER in mutations) && a_intent == I_HURT) // LaserEyes(A) // moved into a proc below if(get_active_mutation(src, MUTATION_TELEKINESIS)) diff --git a/code/controllers/subsystems/processing/psi.dm b/code/controllers/subsystems/processing/psi.dm new file mode 100644 index 00000000000..e9069565f80 --- /dev/null +++ b/code/controllers/subsystems/processing/psi.dm @@ -0,0 +1,46 @@ +GLOBAL_LIST_INIT(psychic_ranks_to_strings, list("Latent", "Operant", "Masterclass", "Grandmasterclass", "Paramount")) + +//This right here is why porting is hard +/datum/var/abstract_type +/datum/proc/IsAbstract() + SHOULD_NOT_OVERRIDE(TRUE) + return type == abstract_type +/proc/is_abstract(datum/thing) + if (ispath(thing)) + return thing == initial(thing.abstract_type) + if (istype(thing)) + return thing.IsAbstract() +//This is a TODO to kill, literally only used once + +PROCESSING_SUBSYSTEM_DEF(psi) + name = "Psychics" + priority = SS_PRIORITY_PSYCHICS + flags = SS_POST_FIRE_TIMING | SS_BACKGROUND + + var/list/faculties_by_id = list() + var/list/faculties_by_name = list() + var/list/all_aura_images = list() + var/list/all_psi_complexes = list() + var/list/psi_dampeners = list() + var/list/psi_monitors = list() + var/list/armour_faculty_by_type = list() + var/list/faculties_by_intent = list() + +/datum/controller/subsystem/processing/psi/proc/get_faculty(faculty) + return faculties_by_name[faculty] || faculties_by_id[faculty] + +/datum/controller/subsystem/processing/psi/Initialize(start_uptime) + var/list/faculties = decls_repository.get_decls_of_subtype(/decl/psionic_faculty) + for(var/ftype in faculties) + var/decl/psionic_faculty/faculty = faculties[ftype] + faculties_by_id[faculty.id] = faculty + faculties_by_name[faculty.name] = faculty + faculties_by_intent[faculty.associated_intent] = faculty.id + + var/list/powers = decls_repository.get_decls_of_subtype(/decl/psionic_power) + for(var/ptype in powers) + var/decl/psionic_power/power = powers[ptype] + if(!is_abstract(power) && power.faculty) + var/decl/psionic_faculty/faculty = get_faculty(power.faculty) + if(faculty) + faculty.powers |= power diff --git a/code/datums/security_state.dm b/code/datums/security_state.dm index 743ed195f9b..1f803a9cd94 100644 --- a/code/datums/security_state.dm +++ b/code/datums/security_state.dm @@ -150,6 +150,7 @@ var/down_description var/up_color var/down_color + var/psionic_control_level = PSI_IMPLANT_WARN // Called when we're switching from a lower security level to this one. /decl/security_level/proc/switching_up_to() @@ -219,6 +220,8 @@ light_color_alarm = COLOR_LIGHTING_BLUE_MACHINERY light_color_status_display = COLOR_LIGHTING_BLUE_MACHINERY + psionic_control_level = PSI_IMPLANT_LOG + overlay_alarm = "alarm_blue" overlay_firealarm = "overlay_blue" @@ -238,6 +241,8 @@ light_color_alarm = COLOR_LIGHTING_RED_MACHINERY light_color_status_display = COLOR_LIGHTING_RED_MACHINERY + psionic_control_level = PSI_IMPLANT_DISABLED + overlay_alarm = "alarm_red" overlay_firealarm = "overlay_red" @@ -257,6 +262,8 @@ light_color_alarm = COLOR_LIGHTING_RED_MACHINERY light_color_status_display = COLOR_LIGHTING_BLUE_MACHINERY + psionic_control_level = PSI_IMPLANT_DISABLED + overlay_alarm = "alarm_delta" overlay_firealarm = "overlay_delta" diff --git a/code/game/antagonist/antagonist.dm b/code/game/antagonist/antagonist.dm index 6222231157c..29efa1aced0 100755 --- a/code/game/antagonist/antagonist.dm +++ b/code/game/antagonist/antagonist.dm @@ -14,7 +14,7 @@ GLOBAL_LIST_EMPTY(antagonists) var/flags = 0 // Various runtime options. var/list/restricted_jobs = list() // Jobs that technically cannot be this antagonist (like AI-carrion) - var/list/protected_jobs = list() // As above, but this jobs are rewstricted ideologically (like Security Officer-contractor) + var/list/protected_jobs = list() // As above, but this jobs are restricted ideologically (like Security Officer-contractor) var/list/story_ineligible = list() // Denies the job from getting the antag status by story teller itself but allows become antag via different means. // Strings. diff --git a/code/game/antagonist/station/psionic/paramount.dm b/code/game/antagonist/station/psionic/paramount.dm new file mode 100644 index 00000000000..3c7a3605478 --- /dev/null +++ b/code/game/antagonist/station/psionic/paramount.dm @@ -0,0 +1,129 @@ +/datum/antagonist/paramount + id = ROLE_PARAMOUNT + role_text = "Paramount" + role_text_plural = "Paramounts" + protected_jobs = list(JOBS_SECURITY, JOBS_COMMAND, "Moebius Bio-Engineer", JOBS_CHURCH) + restricted_jobs = list("AI", "Robot") + bantype = ROLE_BANTYPE_PARAMOUNT + antaghud_indicator = "hudmalai" + + welcome_text = "Awake our Paramount almighty. Let the prison of your thoughts break open and enter our service again.
\ + The Founders see the open vastness of the Null Sector as a treasure cove of knowledge and entropic phenomena, gored open by the fleets of ignorants and barbarians.
\ + Let no seeds of hatred take root over this profanity, and cherish such an occasion! In their foolishness, they expose their inferior minds to us!
\ + Go through the visions not meant for these mere mortals, and collect her gifts for us to plunder." + + possible_objectives = list( + /datum/objective/assassinate = 30, + /datum/objective/harm = 15, + /datum/objective/steal = 30 + ) + + survive_objective = /datum/objective/escape + + stat_modifiers = list( + STAT_COG = 30 + ) + +/datum/antagonist/paramount/can_become_antag(datum/mind/M, mob/report) + if(!..()) + if (report) + to_chat(report, SPAN_NOTICE("Failure: Parent can_become_antag returned false")) + return FALSE + if(M.current.get_core_implant(/obj/item/implant/core_implant/cruciform)) + if (report) + to_chat(report, SPAN_NOTICE("Failure: [M] has a cruciform and this antag prohibits it")) + return FALSE + return TRUE + +/datum/antagonist/paramount/equip() + var/mob/living/carbon/human/H = owner.current + + for(var/name in stat_modifiers) + H.stats.changeStat(name, stat_modifiers[name]) + + if(!owner.current) + return FALSE + +// owner.equip_to_slot_or_del(new /obj/item/clothing/head/helmet/space/psi_amp(owner), slot_head) + H.set_psi_rank(PSI_REDACTION, 5, defer_update = TRUE) // TODO: set these to 3 if this is ever made a real antag + H.set_psi_rank(PSI_COERCION, 5, defer_update = TRUE) + H.set_psi_rank(PSI_PSYCHOKINESIS, 5, defer_update = TRUE) + H.set_psi_rank(PSI_ENERGISTICS, 5, defer_update = TRUE) + H.set_psi_power(2, FALSE, defer_update = TRUE) + if(!get_active_mutation(H, MUTATION_PSI_HIGH)) + var/datum/mutation/M = new MUTATION_PSI_HIGH + M.imprint(H) + H.psi.update(TRUE) + +/* + owner.equip_to_slot_or_del(new /obj/item/clothing/under/psysuit(owner), slot_w_uniform) + owner.equip_to_slot_or_del(new /obj/item/clothing/suit/wizrobe/psypurple(owner), slot_wear_suit) + owner.equip_to_slot_or_del(new /obj/item/clothing/shoes/jackboots(owner), slot_shoes) + owner.equip_to_slot_or_del(new /obj/item/storage/backpack/satchel(owner), slot_back) + var/obj/item/clothing/gloves/color/gloves = new() + gloves.color = COLOR_GRAY80 + owner.equip_to_slot_or_del(gloves, slot_gloves) +*/ + return TRUE + +/datum/antagonist/paramount/greet() + if(!owner || !owner.current) + return + + var/mob/player = owner.current + + // Basic intro text. + to_chat(player, SPAN_DANGER("You are a [role_text]!")) + + to_chat(player, "Awake our Paramount almighty. Let the prison of your thoughts break open and enter our service again.
\ + The Founders see the open vastness of the Null Sector as a treasure cove of knowledge and entropic phenomena, gored open by the fleets of ignorants and barbarians.
\ + Let no seeds of hatred take root over this profanity, and cherish such an occasion! In their foolishness, they expose their inferior minds to us!
\ + Go through the visions not meant for these mere mortals, and collect her gifts for us to plunder.") + + to_chat(player, SPAN_NOTICE("Shift + left click the Psionic HUD icon to get started.")) + + show_objectives() + printTip() + + return TRUE + + +/datum/antagonist/paramount/create_objectives() + + if(!..()) + return + // Copied from ninja for now. + var/objective_list = list(1,2,3) + for(var/i=rand(2,3),i>0,i--) + switch(pick(objective_list)) + if(1)//Kill + var/datum/objective/assassinate/objective = new + objective.owner = owner + objective.target = objective.find_target() + if(objective.target != "Free Objective") + owner.individual_objectives += objective + else + i++ + objective_list -= 1 // No more than one kill objective + if(2)//Protect + var/datum/objective/protect/objective = new + objective.owner = owner + objective.target = objective.find_target() + if(objective.target != "Free Objective") + owner.individual_objectives += objective + else + i++ + objective_list -= 3 + if(3)//Harm + var/datum/objective/harm/objective = new + objective.owner = owner + objective.target = objective.find_target() + if(objective.target != "Free Objective") + owner.individual_objectives += objective + else + i++ + objective_list -= 4 + + var/datum/objective/survive/objective = new + objective.owner = owner + owner.individual_objectives += objective diff --git a/code/game/antagonist/station/thrall.dm b/code/game/antagonist/station/thrall.dm new file mode 100644 index 00000000000..d12a53918f4 --- /dev/null +++ b/code/game/antagonist/station/thrall.dm @@ -0,0 +1,30 @@ +/datum/antagonist/thrall + role_text = "Thrall" + role_text_plural = "Thralls" + welcome_text = "Your mind is no longer solely your own..." + id = ROLE_THRALL + antaghud_indicator = "hudmalai" + restricted_jobs = list("AI", "Robot") + + var/list/thrall_controllers = list() + +/datum/antagonist/thrall/create_objectives() + var/mob/living/controller = thrall_controllers["\ref[owner]"] + if(!controller) + return // Someone is playing with buttons they shouldn't be. + var/datum/objective/obey = new + obey.owner = owner + obey.explanation_text = "Obey your master, [controller.real_name], in all things." + owner.individual_objectives |= obey + +/datum/antagonist/thrall/create_antagonist(datum/mind/owner, ignore_role, do_not_equip, move_to_spawn, do_not_announce, preserve_appearance, mob/new_controller) + if(!new_controller) + return 0 + . = ..() + if(.) thrall_controllers["\ref[owner]"] = new_controller + +/datum/antagonist/thrall/greet() + . = ..() + var/mob/living/controller = thrall_controllers["\ref[owner]"] + if(controller) + to_chat(owner, SPAN_DANGER("Your will has been subjugated by that of [controller.real_name]. Obey them in all things.")) diff --git a/code/game/jobs/job/job.dm b/code/game/jobs/job/job.dm index bf0b6a54551..25f222132f4 100644 --- a/code/game/jobs/job/job.dm +++ b/code/game/jobs/job/job.dm @@ -70,6 +70,20 @@ /// Does this job allows loadout ? var/loadout_allowed = TRUE + var/create_record = 1 // Do we announce/make records for people who spawn on this job? + var/list/also_known_languages = list() // additional chance based languages to all jobs. + + var/list/psi_faculties // Starting psi faculties, if any. + var/psi_latency_chance = 0 // Chance of an additional psi latency, if any. +// var/give_psionic_implant_on_join = TRUE // If psionic, will be implanted for control. + + var/account_allowed = 1 // Does this job type come with a station account? + var/wage = WAGE_LABOUR // How much base wage does this job recieve per payday + var/initial_balance = -1 // If set to a value other than -1, overrides the wage based initial balance calculation + + var/outfit_type // The outfit the employee will be dressed in, if any + + var/loadout_allowed = TRUE // Does this job allows loadout ? var/description = "" var/duties = "" var/loyalties = "" @@ -107,6 +121,26 @@ for(var/perk in perks) target.stats.addPerk(perk) + //PSI stuff used to be in equip, putting it here because it makes more sense here + if(psi_latency_chance && prob(psi_latency_chance)) + target.set_psi_rank(pick(PSI_COERCION, PSI_REDACTION, PSI_ENERGISTICS, PSI_PSYCHOKINESIS), 1, defer_update = TRUE) + if(islist(psi_faculties)) + for(var/psi in psi_faculties) + target.set_psi_rank(psi, psi_faculties[psi], take_larger = TRUE, defer_update = TRUE) +/* if(H.psi) + H.psi.update() + if(give_psionic_implant_on_join) + var/obj/item/implant/psi_control/imp = new + imp.implanted(H) + imp.forceMove(H) + imp.imp_in = H + imp.implanted = TRUE + var/obj/item/organ/external/affected = H.get_organ(BP_HEAD) + if(affected) + affected.implants += imp + imp.part = affected + to_chat(H, SPAN_DANGER("As a registered psionic, you are fitted with a psi-dampening control implant. Using psi-power while the implant is active will result in neural shocks and your violation being reported.")) +*/ return TRUE /datum/job/proc/add_additiional_language(mob/living/carbon/human/target) diff --git a/code/game/objects/items/stacks/stack.dm b/code/game/objects/items/stacks/stack.dm index 96805c25892..d5571122b98 100644 --- a/code/game/objects/items/stacks/stack.dm +++ b/code/game/objects/items/stacks/stack.dm @@ -50,6 +50,15 @@ if (rand_min || rand_max) amount = rand(rand_min, rand_max) amount = round(amount, 1) //Just in case + update_icon() + if(automerge) + return INITIALIZE_HINT_LATELOAD + +//do this a little later because trying to merge with uninitialized stacks is an easy way to cause runtimes +/obj/item/stack/LateInitialize() + . = ..() + if(automerge) + merge_loc_stacks() /obj/item/stack/update_icon() if(novariants) diff --git a/code/game/objects/items/weapons/setupdisk.dm b/code/game/objects/items/weapons/setupdisk.dm index a4240d14c5a..5bdf481cc66 100644 --- a/code/game/objects/items/weapons/setupdisk.dm +++ b/code/game/objects/items/weapons/setupdisk.dm @@ -128,6 +128,20 @@ /datum/computer_file/program/records ) +/obj/item/computer_hardware/hard_drive/portable/advanced/setup/library + name = "libraric setup disk" + desc = "A removable disk used to store large amounts of data.\nThis one contains all the files necessary to set up a library console." + default_files = list( + /datum/computer_file/program/email_client, + /datum/computer_file/program/chatclient, + /datum/computer_file/program/wordprocessor, + /datum/computer_file/program/newsbrowser, + /datum/computer_file/program/nttransfer, + /datum/computer_file/program/camera_monitor, + /datum/computer_file/program/records, + /datum/computer_file/program/library + ) + /obj/item/computer_hardware/hard_drive/portable/advanced/setup/trade name = "trade setup disk" desc = "A removable disk used to store large amounts of data.\nThis one contains all the files necessary to set up a trade console." @@ -137,3 +151,4 @@ name = "order setup disk" desc = "A removable disk used to store large amounts of data.\nThis one contains all the files necessary to set up an order console." default_files = list(/datum/computer_file/program/trade/order) + diff --git a/code/game/objects/items/weapons/tools/karl.dm b/code/game/objects/items/weapons/tools/karl.dm index f0abebbad5d..a347afd8ecd 100644 --- a/code/game/objects/items/weapons/tools/karl.dm +++ b/code/game/objects/items/weapons/tools/karl.dm @@ -113,7 +113,7 @@ attack_self(user) isPumping = FALSE else - to_chat(user, span_notice("\The [src]'s cell is fully charged.")) + to_chat(user, SPAN_NOTICE("\The [src]'s cell is fully charged.")) else to_chat(user, span_notice("\The [src] is missing a cell to recharge.")) return diff --git a/code/modules/genetics/mutations_T2.dm b/code/modules/genetics/mutations_T2.dm index e73f42d94ee..e0dea5e41fb 100644 --- a/code/modules/genetics/mutations_T2.dm +++ b/code/modules/genetics/mutations_T2.dm @@ -64,3 +64,9 @@ /datum/mutation/t2/spider_pheromones/cleanse(mob/living/carbon/user) if(..()) remove_verb(user, /mob/living/carbon/human/proc/spider_pheromones) + +/datum/mutation/t2/psionic_compatibility_low + name = "Psionic Attunement" + desc = "Improves the brain's connections to psionic energies, unlocking Operant level Psionic powers without the need for psionic drugs." + NSA_load = 0 + diff --git a/code/modules/genetics/mutations_T4.dm b/code/modules/genetics/mutations_T4.dm index 37d66de133c..aa24f00afc3 100644 --- a/code/modules/genetics/mutations_T4.dm +++ b/code/modules/genetics/mutations_T4.dm @@ -50,3 +50,8 @@ /datum/mutation/t4/morph/cleanse(mob/living/carbon/user) if(..()) remove_verb(user, /mob/living/carbon/human/proc/morph) + +/datum/mutation/t4/psionic_compatibility_high + name = "Total Psionic Harmony" + desc = "Attunes the body to Psionic powers, unlocking Master level Psionic powers without the need for psionic drugs." + NSA_load = 0 diff --git a/code/modules/mob/living/carbon/human/human_damage.dm b/code/modules/mob/living/carbon/human/human_damage.dm index 31925f29416..dc5435e2b1b 100644 --- a/code/modules/mob/living/carbon/human/human_damage.dm +++ b/code/modules/mob/living/carbon/human/human_damage.dm @@ -379,7 +379,7 @@ This function restores all organs. organ = def_zone else if(!def_zone) - if(!(damagetype == TOX)) + if(!damagetype == TOX) // global application def_zone = ran_zone(def_zone) organ = get_organ(check_zone(def_zone)) diff --git a/code/modules/mob/living/carbon/human/human_organs.dm b/code/modules/mob/living/carbon/human/human_organs.dm index 77ac6facece..997b22aa1ff 100644 --- a/code/modules/mob/living/carbon/human/human_organs.dm +++ b/code/modules/mob/living/carbon/human/human_organs.dm @@ -240,3 +240,9 @@ return E.limb_efficiency return 0 +/mob/living/carbon/human/proc/unmutate(var/amount = -1) // -1 means all of them + var/list/IOlist = internal_organs + while(IOlist.len && amount) + var/obj/item/organ/internal/I = pick_n_take(IOlist) + I.unmutate() + amount-- diff --git a/code/modules/mob/living/carbon/superior_animal/roach/roachmind.dm b/code/modules/mob/living/carbon/superior_animal/roach/roachmind.dm index 4df847b22bc..ba3e3c4c15d 100644 --- a/code/modules/mob/living/carbon/superior_animal/roach/roachmind.dm +++ b/code/modules/mob/living/carbon/superior_animal/roach/roachmind.dm @@ -15,7 +15,7 @@ . = ..() if(length(members)) CRASH("Overmind type /ref[src] destroyed with members still in existence!") - + /datum/overmind/roachmind/Destroy() for(var/mob/living/carbon/superior_animal/roach/tonotify in members) tonotify.leaveOvermind() @@ -146,7 +146,7 @@ else if((!QDELETED(potentialsuperior.overseer)) && length(potentialsuperior.overseer.members) < ROACHMINDCAP) // skip over any full or dead overminds joinOvermind(potentialsuperior.overseer) return TRUE // job done, we got one - + if((type == /mob/living/carbon/superior_animal/roach/fuhrer) || (type == /mob/living/carbon/superior_animal/roach/kaiser)) // we can start our own var/datum/overmind/roachmind/newoverseer = new() newoverseer.leader = src @@ -195,7 +195,7 @@ if(ready in viewers(casualty)) // can ready see the roach? supportMove(ready, casualty) break // we got a healer on the way - + else return FALSE diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index d49e44b695f..8d1abbdc757 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -386,6 +386,8 @@ var/obj/item/W = get_active_held_item() if (W) W.attack_self(src) + else + attack_empty_hand() /mob/verb/toggle_flashlight() diff --git a/code/modules/mob/mob_grab.dm b/code/modules/mob/mob_grab.dm index b53d5c97a4a..7ac972e1e80 100644 --- a/code/modules/mob/mob_grab.dm +++ b/code/modules/mob/mob_grab.dm @@ -38,8 +38,12 @@ /obj/proc/affect_grab(mob/user, mob/target, state) return FALSE -/obj/item/grab/resolve_attackby(obj/O, mob/user, click_params) - if(ismob(O)) +/obj/item/grab/resolve_attackby(atom/A, mob/user, var/click_params) + if(ishuman(user) && affecting == A) + var/mob/living/carbon/human/H = user + if (H.check_psi_grab(src)) + return TRUE + if(ismob(A)) return ..() if(get_dist(O, affecting) > 1) return TRUE diff --git a/code/modules/organs/internal/internal_wounds/eyes.dm b/code/modules/organs/internal/internal_wounds/eyes.dm index 674c2f295f2..70ba166305b 100644 --- a/code/modules/organs/internal/internal_wounds/eyes.dm +++ b/code/modules/organs/internal/internal_wounds/eyes.dm @@ -15,7 +15,7 @@ name = "corneal erosion" hal_damage = IWOUND_HEAVY_DAMAGE -/datum/internal_wound/organic/eyes_blunt/iris //iris hehehehe +/datum/internal_wound/organic/eyes_blunt/iris //iris hehehehe name = "iris tears" // Sharp diff --git a/code/modules/projectiles/guns/energy/laser.dm b/code/modules/projectiles/guns/energy/laser.dm index 027b64dfe49..f97927ece9c 100644 --- a/code/modules/projectiles/guns/energy/laser.dm +++ b/code/modules/projectiles/guns/energy/laser.dm @@ -246,7 +246,7 @@ fire_sound = 'sound/weapons/lasercannonfire.ogg' item_charge_meter = FALSE origin_tech = list(TECH_COMBAT = 4, TECH_MATERIAL = 3, TECH_POWER = 3, TECH_COVERT = 5) - projectile_type = /obj/item/projectile/beam/psychic/heavylaser + projectile_type = /obj/item/projectile/beam/psychic/heavy w_class = ITEM_SIZE_NORMAL force = WEAPON_FORCE_NORMAL slot_flags = SLOT_BELT|SLOT_BACK diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index ab221d82712..b861c7ef643 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -394,7 +394,9 @@ if(istype(src, /obj/item/projectile/beam/psychic) && istype(target_mob, /mob/living/carbon/human)) var/obj/item/projectile/beam/psychic/psy = src var/mob/living/carbon/human/H = target_mob - if(psy.contractor && result && (H.sanity.level <= 0)) + if(psy.psionic_user && result && (H.sanity.level <= 0)) + psy.psionic_user.reg_break(H) + else if(psy.contractor && result && (H.sanity.level <= 0) && holder) psy.holder.reg_break(H) if(result == PROJECTILE_STOP) diff --git a/code/modules/projectiles/projectile/beams.dm b/code/modules/projectiles/projectile/beams.dm index 371d61e1a3d..978ff8ebaa7 100644 --- a/code/modules/projectiles/projectile/beams.dm +++ b/code/modules/projectiles/projectile/beams.dm @@ -90,6 +90,7 @@ icon_state = "psychic_heavylaser" var/obj/item/gun/energy/psychic/holder var/contractor = FALSE //Check if it's a contractor psychic beam + var/mob/living/psionic_user damage_types = list(PSY = 30) armor_divisor = ARMOR_PEN_MAX recoil = 2 @@ -99,21 +100,23 @@ impact_type = /obj/effect/projectile/psychic_laser_heavy/impact /obj/item/projectile/beam/psychic/launch_from_gun(atom/target, mob/user, obj/item/gun/launcher, target_zone, x_offset=0, y_offset=0, angle_offset) - holder = launcher - if(holder && holder.contractor) - contractor = holder.contractor + if(!launcher && user) + psionic_user = user + else + holder = launcher + if(holder && holder.contractor) + contractor = holder.contractor ..() -/obj/item/projectile/beam/psychic/heavylaser +/obj/item/projectile/beam/psychic/heavy name = "psychic heavy laser" - icon_state = "psychic_heavylaser" damage_types = list(PSY = 40) contractor = TRUE recoil = 3 - muzzle_type = /obj/effect/projectile/psychic_laser_heavy/muzzle - tracer_type = /obj/effect/projectile/psychic_laser_heavy/tracer - impact_type = /obj/effect/projectile/psychic_laser_heavy/impact +/obj/item/projectile/beam/psychic/light + name = "psychic light laser" + damage_types = list(PSY = 20) /obj/item/projectile/beam/xray name = "xray beam" @@ -225,3 +228,23 @@ muzzle_type = /obj/effect/projectile/stun/muzzle tracer_type = /obj/effect/projectile/stun/tracer impact_type = /obj/effect/projectile/stun/impact + +/obj/item/projectile/beam/energistic + name = "low intensity energy beam" + damage_types = list(BURN = 25) + armor_divisor = 0.4 // Basic operative armor reduces damage to 0 + +/obj/item/projectile/beam/energistic/master + name = "energy beam" + damage_types = list(BURN = 30) + armor_divisor = 0.8 // A combat RIG reduces damage to 0 while at max durability + +/obj/item/projectile/beam/energistic/grandmaster + name = "high intensity energy beam" + damage_types = list(BURN = 35) + armor_divisor = 1.2 + +/obj/item/projectile/beam/energistic/paramount + name = "hyper energy beam" + damage_types = list(BURN = 50) + armor_divisor = 1.2 // Inquisitors beware diff --git a/code/modules/psionics/complexus/complexus.dm b/code/modules/psionics/complexus/complexus.dm new file mode 100644 index 00000000000..efe089ddcf3 --- /dev/null +++ b/code/modules/psionics/complexus/complexus.dm @@ -0,0 +1,99 @@ +/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/use_psi_armour = 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/armor_cost = 0 // Amount of power to substract this tick from psi armor blocking damage. + var/heat_buildup = 0 // Amount of heat building up - translates to NSA + + var/power_level = 0 // Psi Power Level determined by Genetics, and similar factors. + var/power_bonus = 0 // Not normally measurable Psi Power obtained from external means. As a result of coding method, lost permanently if all ranks are set to 0 or lower. + + 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/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/last_aura_size + var/last_aura_alpha + var/last_aura_color + var/aura_color = "#ff0022" + + // Cached powers. + var/list/melee_powers // Powers used in melee range. + var/list/grab_powers // Powers use by using a grab. + var/list/ranged_powers // Powers used at range. + var/list/manifestation_powers // Powers that create an item. + var/list/powers_by_faculty // All powers within a given faculty. + + var/obj/screen/psi/hub/ui // Reference to the master psi UI object. + var/mob/living/owner // Reference to our owner. + var/image/_aura_image // Client image + +/datum/psi_complexus/proc/get_aura_image() + if(_aura_image && !istype(_aura_image)) + var/atom/A = _aura_image + log_debug("Non-image found in psi complexus: \ref[A] - \the [A] - [istype(A) ? A.type : "non-atom"]") + destroy_aura_image(_aura_image) + _aura_image = null + if(!_aura_image) + _aura_image = create_aura_image(owner) + return _aura_image + +/proc/create_aura_image(newloc) + RETURN_TYPE(/image) + var/image/aura_image = image(loc = newloc, icon = 'icons/effects/psi_aura_small.dmi', icon_state = "aura") + aura_image.blend_mode = BLEND_MULTIPLY + aura_image.appearance_flags = PIXEL_SCALE | NO_CLIENT_COLOR | RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM + aura_image.layer = TURF_LAYER + 0.5 + aura_image.alpha = 0 + aura_image.pixel_x = -64 + aura_image.pixel_y = -64 + aura_image.mouse_opacity = 0 + aura_image.appearance_flags = PIXEL_SCALE + for(var/thing in SSpsi.processing) + var/datum/psi_complexus/psychic = thing + if(psychic.owner.client && !psychic.suppressed) + psychic.owner.client.images += aura_image + SSpsi.all_aura_images[aura_image] = TRUE + return aura_image + +/proc/destroy_aura_image(image/aura_image) + for(var/thing in SSpsi.processing) + var/datum/psi_complexus/psychic = thing + if(psychic.owner.client) + psychic.owner.client.images -= aura_image + SSpsi.all_aura_images -= aura_image + +// TODO: Implement armor mechanics +/datum/psi_complexus/New(mob/_owner) + owner = _owner + START_PROCESSING(SSpsi, src) +// set_extension(src, /datum/extension/armor/psionic) + +/datum/psi_complexus/Destroy() + destroy_aura_image(_aura_image) + STOP_PROCESSING(SSpsi, src) + if(owner) + cancel() + if(owner.client) + owner.client.screen -= list(ui, ui?.components) + for(var/thing in SSpsi.all_aura_images) + owner.client.images -= thing + QDEL_NULL(ui) + owner.psi = null + owner = null + + if(manifested_items) + for(var/thing in manifested_items) + qdel(thing) + manifested_items.Cut() + . = ..() diff --git a/code/modules/psionics/complexus/complexus_helpers.dm b/code/modules/psionics/complexus/complexus_helpers.dm new file mode 100644 index 00000000000..3ea6de50a96 --- /dev/null +++ b/code/modules/psionics/complexus/complexus_helpers.dm @@ -0,0 +1,122 @@ +/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(amount) + var/old_stun = stun + stun = max(stun, amount) + if(amount && !old_stun) + to_chat(owner, SPAN_DANGER("Your concentration has been shattered! You cannot focus your psi power!")) + ui.update_icon() + cancel() + +/datum/psi_complexus/proc/get_armour(armourtype) + if(use_psi_armour && can_use_passive()) + return round(clamp(clamp(4 * rating, 0, 20) * get_rank(SSpsi.armour_faculty_by_type[armourtype]), 0, 100) * (stamina/max_stamina)) + else + return 0 + +/datum/psi_complexus/proc/get_rank(faculty) + return LAZYACCESS(ranks, faculty) + +/datum/psi_complexus/proc/set_rank(faculty, rank, defer_update, temporary) + if(get_rank(faculty) != rank) + LAZYSET(ranks, faculty, rank) + if(!temporary) + LAZYSET(base_ranks, faculty, rank) + if(!defer_update) + update() + +/datum/psi_complexus/proc/set_power(amount = 0, additive = FALSE, defer_update) + power_bonus = additive ? (power_bonus + amount) : amount + if(!defer_update) + update() + +/datum/psi_complexus/proc/set_cooldown(value) + next_power_use = world.time + value + ui.update_icon() + +/datum/psi_complexus/proc/can_use_passive() + return (owner.stat == CONSCIOUS && !suppressed && !stun) + +/datum/psi_complexus/proc/can_use(incapacitation_flags) + return (owner.stat == CONSCIOUS && (!incapacitation_flags || !owner.incapacitated(incapacitation_flags)) && !suppressed && !stun && world.time >= next_power_use) + +/datum/psi_complexus/proc/spend_power(value = 0, check_incapacitated) + . = FALSE + if(isnull(check_incapacitated)) + check_incapacitated = (INCAPACITATION_STUNNED|INCAPACITATION_UNCONSCIOUS) + if(can_use(check_incapacitated)) + //value = max(1, ceil(value * cost_modifier)) + value = max(1,ceil(value)) + if(value <= stamina) + stamina -= value + ui.update_icon() + . = TRUE + else + backblast(abs(stamina - value)) + stamina = 0 + . = FALSE + ui.update_icon() + +/datum/psi_complexus/proc/spend_power_armor(value = 0) + armor_cost += value + +/datum/psi_complexus/proc/hide_auras() + if(owner.client) + for(var/thing in SSpsi.all_aura_images) + owner.client.images -= thing + +/datum/psi_complexus/proc/show_auras() + if(owner.client) + for(var/image/I in SSpsi.all_aura_images) + owner.client.images |= I + +/datum/psi_complexus/proc/backblast(var/value) + + // Can't backblast if you're controlling your power. + if(!owner || suppressed) + return FALSE + + // NSA effect + heat_buildup += value + + // Apply armor from cognition + value = max(value - CLAMP(owner.stats.getStat(STAT_COG) / 40, 0, 2) - 2, 0) + if(!value) + to_chat(owner, SPAN_WARNING("You feel energistic dissipate across your psyche.")) + set_cooldown(100 / power_level) + return FALSE // Completely blocked + + sound_to(owner, sound('sound/effects/psi/power_feedback.ogg')) + to_chat(owner, SPAN_DANGER("Wild energistic feedback blasts across your psyche!")) + stunned(value * 2) + set_cooldown(value * 100 / power_level) + + if(prob(value*10)) owner.emote("scream") + + + // Your head asplode. + owner.adjustBrainLoss(value * 3 - CLAMP(owner.stats.getStat(STAT_TGH) / 8, 0, 10)) // Most blasts will cause around 24 brainloss at most, reduced by both cognition and toughness (max 6 by cognition, max 10 by toughness) + + if(ishuman(owner)) + var/mob/living/carbon/human/pop = owner + if(pop.random_organ_by_process(BP_BRAIN)) + var/obj/item/organ/internal/vital/brain/sponge = pop.random_organ_by_process(BP_BRAIN) + if(sponge && sponge.is_broken()) + var/obj/item/organ/external/affecting = pop.get_organ(sponge.parent) + if(affecting && !affecting.is_stump()) + affecting.droplimb(FALSE, DROPLIMB_BLUNT) + if(sponge) qdel(sponge) + +/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 new file mode 100644 index 00000000000..0d9bddd620e --- /dev/null +++ b/code/modules/psionics/complexus/complexus_latency.dm @@ -0,0 +1,18 @@ +/datum/psi_complexus/proc/check_latency_trigger(trigger_strength = 0, source, redactive = FALSE) + + if(!LAZYLEN(latencies) || world.time < next_latency_trigger) + return FALSE + + if(!prob(trigger_strength)) + next_latency_trigger = world.time + rand(100, 300) + return FALSE + + var/faculty = pick(latencies) + var/new_rank = rand(2,5) + owner.set_psi_rank(faculty, new_rank) + var/decl/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty) + to_chat(owner, SPAN_DANGER("You scream internally as your [faculty_decl.name] faculty is forced into operancy by [source]!")) + next_latency_trigger = world.time + rand(600, 1800) * new_rank + if(!redactive) owner.adjustBrainLoss(rand(trigger_strength * 2, trigger_strength * 4)) + log_and_message_admins("gained the [faculty_decl.name] psionic faculty by: [source].", owner) + return TRUE diff --git a/code/modules/psionics/complexus/complexus_power_cache.dm b/code/modules/psionics/complexus/complexus_power_cache.dm new file mode 100644 index 00000000000..8b6f68ab6ae --- /dev/null +++ b/code/modules/psionics/complexus/complexus_power_cache.dm @@ -0,0 +1,49 @@ +/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/decl/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty) + for(var/thing in faculty_decl.powers) + var/decl/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(faculty) + rebuild_power_cache() + return powers_by_faculty[faculty] + +/datum/psi_complexus/proc/get_melee_powers(faculty) + rebuild_power_cache() + return melee_powers[faculty] + +/datum/psi_complexus/proc/get_ranged_powers(faculty) + rebuild_power_cache() + return ranged_powers[faculty] + +/datum/psi_complexus/proc/get_grab_powers(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 new file mode 100644 index 00000000000..8d445bf321f --- /dev/null +++ b/code/modules/psionics/complexus/complexus_process.dm @@ -0,0 +1,270 @@ +/datum/psi_complexus/proc/update(force) + + set waitfor = FALSE + + var/last_rating = rating + var/highest_faculty + var/highest_rank = 0 + var/combined_rank = 0 + for(var/faculty in ranks) + var/check_rank = get_rank(faculty) + if(check_rank == 1) + LAZYADD(latencies, faculty) + else + if(check_rank <= 0) + ranks -= faculty + LAZYREMOVE(latencies, faculty) + combined_rank += check_rank + if(!highest_faculty || highest_rank < check_rank) + highest_faculty = faculty + highest_rank = check_rank + + // To determine powerlevel we add up every source of power: this will be Chem Signals, Genetic Mutations (only the highest grade), and Perks + power_level = (get_active_mutation(owner, MUTATION_PSI_HIGH) ? 3 : (get_active_mutation(owner, MUTATION_PSI_LOW) ? 2 : 0)) + power_bonus + + UNSETEMPTY(latencies) + var/rank_count = max(1, LAZYLEN(ranks)) + if(force || last_rating != ceil(combined_rank/rank_count)) + if(highest_rank <= 1) + if(highest_rank == 0) + qdel(src) + return + else + rebuild_power_cache = TRUE + sound_to(owner, 'sound/effects/psi/power_unlock.ogg') + rating = ceil(combined_rank/rank_count) + + // We use a rating system relying on Genetics - the power_level variable + max_stamina = 10 + power_level * 15 // Operant: 40, Paramount: 85 + + //cost_modifier = 1 + //if(power_level > 1) + // cost_modifier -= min(1, max(0.1, (power_level-1) / 10)) + if(!ui) + ui = new(owner) + if(owner.client) + owner.client.screen += ui.components + owner.client.screen += ui + else + if(owner.client) + owner.client.screen |= ui.components + 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(power_level >= PSI_RANK_PARAMOUNT) // spooky boosters + aura_color = "#000000" // black + + aura_image.blend_mode = BLEND_ADD + + if(!highest_faculty) + aura_color = "#cc00cc" // magenta - generalist + else if(highest_faculty == PSI_COERCION) + aura_color = "#3333cc" // blue - disarm + else if(highest_faculty == PSI_PSYCHOKINESIS) + aura_color = "#cccc33" // yellow - grab + else if(highest_faculty == PSI_REDACTION) + aura_color = "#33cc33" // green - help + else if(highest_faculty == PSI_ENERGISTICS) + aura_color = "#cc3333" // red - harm + + aura_image.pixel_x = -64 - owner.default_pixel_x + aura_image.pixel_y = -64 - owner.default_pixel_y + + 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, "
") + +/datum/psi_complexus/Process() + var/update_hud + var/recovery_bonus = 1 + CLAMP(owner.stats.getStat(STAT_COG) / 160, 0, 0.5) // Used for heat dissipation + + if(iscarbon(owner)) + var/mob/living/carbon/C = owner + C.metabolism_effects.adjust_nsa(heat_buildup, "Psi Complexus") + + if(armor_cost) + //var/value = max(1, ceil(armor_cost * cost_modifier)) + var/value = max(1, ceil(armor_cost)) + if(value <= stamina) + stamina -= value + else + backblast(abs(stamina - value)) + stamina = 0 + update_hud = TRUE + armor_cost = 0 + + if(stun) + stun-- + if(stun) + if(!suppressed) + suppressed = TRUE + update_hud = TRUE + else + to_chat(owner, SPAN_NOTICE("You have recovered your mental composure.")) + update_hud = TRUE + else + var/psi_leech = owner.do_psionics_check() + if(psi_leech) + if(stamina > 10) + stamina = max(0, stamina - rand(15,20)) + to_chat(owner, SPAN_DANGER("You feel your psi-power leeched away by \the [psi_leech]...")) + else + stamina++ + else if(stamina < max_stamina) + if(owner.stat == CONSCIOUS) + stamina = min(max_stamina, stamina + rand(1,power_level+1)) // Operant avg is 2, Paramount is 3.5 + else if(owner.stat == UNCONSCIOUS) + stamina = min(max_stamina, stamina + rand(power_level,power_level*2)) + else if(stamina > max_stamina) + stamina = max_stamina + if(heat_buildup) + if(owner.stat == CONSCIOUS) + heat_buildup = max(0, heat_buildup - recovery_bonus) + else if(owner.stat == UNCONSCIOUS) + heat_buildup = min(0, heat_buildup - rand(1,3) * recovery_bonus) + + if(owner.stat == CONSCIOUS && stamina && !suppressed && get_rank(PSI_REDACTION) >= PSI_RANK_OPERANT) + attempt_regeneration() + + var/next_aura_size = max(0.1,((stamina/max_stamina)*power_level)/2) + var/next_aura_alpha = round(((suppressed ? max(0,rating - 2) : rating)/5)*255) + + if(next_aura_alpha != last_aura_alpha || next_aura_size != last_aura_size || aura_color != last_aura_color) + last_aura_size = next_aura_size + last_aura_alpha = next_aura_alpha + last_aura_color = aura_color + animate( + get_aura_image(), + alpha = next_aura_alpha, + transform = matrix().Update(scale_x = next_aura_size, scale_y = next_aura_size), + color = aura_color, + time = 3 + ) + + if(update_hud) + ui.update_icon() + +/datum/psi_complexus/proc/attempt_regeneration() + + var/heal_general = FALSE + var/heal_poison = FALSE + var/heal_internal = FALSE + var/heal_bleeding = FALSE + var/heal_rate = 0 + var/mend_prob = 0 + + 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)) + + // Attempt miracle if we're paramount. + if(heal_general && H.stat == DEAD) + var/obj/item/organ/internal/vital/heart_organ = H.random_organ_by_process(OP_HEART) + var/obj/item/organ/internal/vital/brain_organ = H.random_organ_by_process(BP_BRAIN) + + if(!H.is_asystole() && !(heart_organ && brain_organ) || (heart_organ.is_broken() || brain_organ.is_broken())) + H.visible_message(SPAN_WARNING("\The [src] cannot be resuscitated in this state!")) + else if (spend_power(heal_rate)) + H.resuscitate() + + // Heal organ damage. + if(heal_internal) + for(var/obj/item/organ/internal/I in H.internal_organs) + + if(BP_IS_ROBOTIC(I) || BP_IS_CRYSTAL(I)) + continue + + if(I && istype(I)) + var/list/current_wounds = I.GetComponents(/datum/component/internal_wound) + if(LAZYLEN(current_wounds) && prob(heal_rate * 10) && spend_power(heal_rate)) + SEND_SIGNAL_OLD(I, COMSIG_IORGAN_REMOVE_WOUND, pick(current_wounds)) + to_chat(H, SPAN_NOTICE("Your innards itch as your autoredactive faculty mends your [I.name].")) + + // Heal broken bones. + if(length(H.bad_external_organs)) + 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 * ORGAN_HEALTH_MULTIPLIER)) // So we don't mend and autobreak. + if(spend_power(heal_rate)) + if(E.mend_fracture()) + to_chat(H, SPAN_NOTICE("Your autoredactive faculty coaxes together the shattered bones in your [E.name].")) + return + + if(heal_bleeding) + + for(var/datum/wound/W in E.wounds) + + if(W.bleeding() && spend_power(heal_rate)) + to_chat(H, SPAN_NOTICE("Your autoredactive faculty knits together severed veins, stemming the bleeding from \a [W.desc] on your [E.name].")) + W.bleed_timer = 0 + W.clamped = TRUE + E.status &= ~ORGAN_BLEEDING + return + + // Heal radiation, cloneloss and poisoning. + if(heal_poison) + + if(owner.radiation && spend_power(heal_rate)) + if(prob(25)) + to_chat(owner, SPAN_NOTICE("Your autoredactive faculty repairs some of the radiation damage to your body.")) + owner.radiation = max(0, owner.radiation - heal_rate) + 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/complexus/complexus_topic.dm b/code/modules/psionics/complexus/complexus_topic.dm new file mode 100644 index 00000000000..b72659b4380 --- /dev/null +++ b/code/modules/psionics/complexus/complexus_topic.dm @@ -0,0 +1,20 @@ +/datum/psi_complexus/CanUseTopic(mob/user, datum/topic_state/state = GLOB.default_state) + return (user.client && check_rights(R_ADMIN, FALSE, user.client)) + +/datum/psi_complexus/Topic(href, list/href_list) + . = ..() + if(!. && check_rights(R_ADMIN)) + if(href_list && href_list["remove_psionics"]) + if(!QDELETED(src) && !QDELETED(owner?.psi) && owner.psi == src) + log_and_message_admins("removed all psionics from [key_name(owner)].") + to_chat(owner, SPAN_NOTICE("Your psionic powers vanish abruptly, leaving you cold and empty.")) + QDEL_NULL(owner.psi) + . = TRUE + if(href_list && href_list["trigger_psi_latencies"]) + log_and_message_admins("triggered psi latencies for [key_name(owner)].") + check_latency_trigger(100, "outside intervention", redactive = TRUE) + . = TRUE + if(.) + var/datum/admins/admin_datum = admin_datums[usr.ckey] + if(istype(admin_datum)) + admin_datum.show_player_panel(owner) diff --git a/code/modules/psionics/equipment/cerebro_enhancers.dm b/code/modules/psionics/equipment/cerebro_enhancers.dm new file mode 100644 index 00000000000..2c64e773e94 --- /dev/null +++ b/code/modules/psionics/equipment/cerebro_enhancers.dm @@ -0,0 +1,167 @@ +//Psi-boosting item (antag only) +/obj/item/clothing/head/helmet/space/psi_amp + name = "cerebro-energetic enhancer" + desc = "A matte-black, eyeless cerebro-energetic enhancement helmet. It uses highly sophisticated, and illegal, techniques to drill into your brain and install psi-infected AIs into the fluid cavities between your lobes." + action_button_name = "Install Boosters" + icon_state = "cerebro" + + item_state_slots = list( + slot_l_hand_str = "helmet", + slot_r_hand_str = "helmet" + ) + + var/operating = FALSE + var/list/boosted_faculties + var/boosted_rank = PSI_RANK_PARAMOUNT + var/unboosted_rank = PSI_RANK_MASTER + var/max_boosted_faculties = 3 + var/boosted_psipower = 120 + +/obj/item/clothing/head/helmet/space/psi_amp/lesser + name = "psionic amplifier" + desc = "A crown-of-thorns cerebro-energetic enhancer that interfaces directly with the brain, isolating and strengthening psionic signals. It kind of looks like a tiara having sex with an industrial robot." + icon_state = "amp" + flags_inv = 0 + body_parts_covered = 0 + + max_boosted_faculties = 1 + boosted_rank = PSI_RANK_MASTER + unboosted_rank = PSI_RANK_OPERANT + boosted_psipower = 50 + +/obj/item/clothing/head/helmet/space/psi_amp/Initialize() + . = ..() + verbs += /obj/item/clothing/head/helmet/space/psi_amp/proc/integrate + +/obj/item/clothing/head/helmet/space/psi_amp/attack_self(mob/user) + + if(operating) + return + + 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/decl/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 = "Enhance your brainpower." + set category = "Abilities" + set src in usr + + if(operating) + return + + if(canremove) + return + + var/mob/living/carbon/human/H = loc + if(!istype(H) || H.head != src) + canremove = TRUE + return + + to_chat(H, SPAN_WARNING("You feel a strange tugging sensation as \the [src] begins removing the slave-minds from your brain...")) + playsound(H, 'sound/weapons/circsawhit.ogg', 50, 1, -1) + operating = TRUE + + sleep(80) + + if(H.psi) + H.psi.reset() + + to_chat(H, SPAN_NOTICE("\The [src] chimes quietly as it finishes removing the slave-minds from your brain.")) + + canremove = TRUE + operating = FALSE + + verbs -= /obj/item/clothing/head/helmet/space/psi_amp/proc/deintegrate + verbs |= /obj/item/clothing/head/helmet/space/psi_amp/proc/integrate + + action_button_name = "Integrate Psionic Amplifier" + H.update_action_buttons() + + set_light(0) + +/obj/item/clothing/head/helmet/space/psi_amp/Move() + var/lastloc = loc + . = ..() + if(.) + var/mob/living/carbon/human/H = lastloc + if(istype(H) && H.psi) + H.psi.reset() + H = loc + if(!istype(H) || H.head != src) + 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" + set src in usr + + if(operating) + return + + 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.")) + return + + canremove = FALSE + operating = TRUE + to_chat(H, SPAN_WARNING("You feel a series of sharp pinpricks as \the [src] anaesthetises your scalp before drilling down into your brain.")) + playsound(H, 'sound/weapons/circsawhit.ogg', 50, 1, -1) + + sleep(80) + + for(var/faculty in list(PSI_COERCION, PSI_PSYCHOKINESIS, PSI_REDACTION, PSI_ENERGISTICS)) + if(faculty in boosted_faculties) + H.set_psi_rank(faculty, boosted_rank, take_larger = TRUE, temporary = TRUE) + else + H.set_psi_rank(faculty, unboosted_rank, take_larger = TRUE, temporary = TRUE) + if(H.psi) + H.psi.max_stamina = boosted_psipower // TODO: look at this - humon + H.psi.stamina = H.psi.max_stamina + H.psi.update(force = TRUE) + + to_chat(H, SPAN_NOTICE("You experience a brief but powerful wave of deja vu as \the [src] finishes modifying your brain.")) + verbs |= /obj/item/clothing/head/helmet/space/psi_amp/proc/deintegrate + verbs -= /obj/item/clothing/head/helmet/space/psi_amp/proc/integrate + operating = FALSE + action_button_name = "Remove Psionic Amplifier" + H.update_action_buttons() + + set_light(0.5, 0.1, 3, 2, l_color = "#880000") diff --git a/code/modules/psionics/equipment/foundation_implanter.dm b/code/modules/psionics/equipment/foundation_implanter.dm new file mode 100644 index 00000000000..d83f8fc2a1a --- /dev/null +++ b/code/modules/psionics/equipment/foundation_implanter.dm @@ -0,0 +1,18 @@ +/obj/item/implanter/psi + name = "psi-null implanter" + desc = "An implant gun customized to interact with psi dampeners." + var/implanter_mode = PSI_IMPLANT_AUTOMATIC + +/obj/item/implanter/psi/attack_self(mob/user) + var/choice = input("Select a new implant mode.", "Psi Dampener") as null|anything in list(PSI_IMPLANT_AUTOMATIC, PSI_IMPLANT_SHOCK, PSI_IMPLANT_WARN, PSI_IMPLANT_LOG, PSI_IMPLANT_DISABLED) + if(!choice || user != loc) return + var/obj/item/implant/psi_control/I = implant + if(!istype(I)) + to_chat(user, SPAN_WARNING("The implanter reports there is no compatible implant loaded.")) + return + I.psi_mode = choice + to_chat(user, SPAN_NOTICE("You set \the [src] to configure implants with the '[I.psi_mode]' setting.")) + +/obj/item/implanter/psi/New() + ..() + implant = new /obj/item/implant/psi_control(src) diff --git a/code/modules/psionics/equipment/foundation_weapon.dm b/code/modules/psionics/equipment/foundation_weapon.dm new file mode 100644 index 00000000000..b11ff649a47 --- /dev/null +++ b/code/modules/psionics/equipment/foundation_weapon.dm @@ -0,0 +1,24 @@ +/obj/item/gun/projectile/revolver/foundation + name = "\improper Foundation revolver" + icon = 'icons/obj/guns/foundation.dmi' + icon_state = "foundation" + desc = "The CF 'Troubleshooter', a compact plastic-composite weapon designed for concealed carry by Cuchulain Foundation field agents. Smells faintly of copper." + ammo_type = /obj/item/ammo_casing/magnum/nullglass + +/obj/item/gun/projectile/revolver/foundation/disrupts_psionics() + return FALSE + +/obj/item/storage/briefcase/foundation + name = "\improper Foundation briefcase" + desc = "A handsome black leather briefcase embossed with a stylized radio telescope." + icon_state = "fbriefcase" + item_state = "fbriefcase" + +/obj/item/storage/briefcase/foundation/disrupts_psionics() + return FALSE + +/obj/item/storage/briefcase/foundation/New() + ..() + new /obj/item/ammo_magazine/speedloader/magnum/nullglass(src) + new /obj/item/gun/projectile/revolver/foundation(src) + make_exact_fit() diff --git a/code/modules/psionics/equipment/implant.dm b/code/modules/psionics/equipment/implant.dm new file mode 100644 index 00000000000..170983e6152 --- /dev/null +++ b/code/modules/psionics/equipment/implant.dm @@ -0,0 +1,104 @@ +/obj/item/implant/psi_control + name = "psi dampener implant" + desc = "A safety implant for registered psi-operants." + + var/overload = 0 + var/max_overload = 100 + var/psi_mode = PSI_IMPLANT_AUTOMATIC + + +/obj/item/implant/psi_control/Initialize() + . = ..() + SSpsi.psi_dampeners += src + +/obj/item/implant/psi_control/Destroy() + SSpsi.psi_dampeners -= src + . = ..() + +/obj/item/implant/psi_control/emp_act() + ..() + update_functionality() + +/obj/item/implant/psi_control/meltdown() + . = ..() + update_functionality() + +/obj/item/implant/psi_control/disrupts_psionics() + var/use_psi_mode = get_psi_mode() + return (!malfunction && (use_psi_mode == PSI_IMPLANT_SHOCK || use_psi_mode == PSI_IMPLANT_WARN)) ? src : FALSE + +/obj/item/implant/psi_control/on_uninstall() + var/mob/living/M = wearer + if(disrupts_psionics() && istype(M) && M.psi) + to_chat(M, SPAN_NOTICE("You feel the chilly shackles around your psionic faculties fade away.")) + . = ..() + +/obj/item/implant/psi_control/proc/update_functionality(silent) + var/mob/living/M = wearer + if(get_psi_mode() == PSI_IMPLANT_DISABLED || malfunction) + if(implanted && !silent && istype(M) && M.psi) + to_chat(M, SPAN_NOTICE("You feel the chilly shackles around your psionic faculties fade away.")) + else + if(implanted && !silent && istype(M) && M.psi) + to_chat(M, SPAN_NOTICE("Bands of hollow ice close themselves around your psionic faculties.")) + +/obj/item/implant/psi_control/meltdown() + if(!malfunction) + overload = 100 + if(wearer) + for(var/thing in SSpsi.psi_monitors) + var/obj/machinery/psi_monitor/monitor = thing + monitor.report_failure(src) + . = ..() + +/obj/item/implant/psi_control/proc/get_psi_mode() + if(psi_mode == PSI_IMPLANT_AUTOMATIC) + var/decl/security_state/security_state = decls_repository.get_decl(GLOB.maps_data.security_state) + return security_state.current_security_level.psionic_control_level + return psi_mode + +/obj/item/implant/psi_control/withstand_psi_stress(stress, atom/source) + + var/use_psi_mode = get_psi_mode() + + if(malfunction || use_psi_mode == PSI_IMPLANT_DISABLED) + return stress + + . = 0 + + if(stress > 0) + + // If we're disrupting psionic attempts at the moment, we might overload. + if(disrupts_psionics()) + var/overload_amount = round(stress/10) + if(overload_amount > 0) + overload += overload_amount + if(overload >= 100) + if(wearer) + to_chat(wearer, SPAN_DANGER("Your psi dampener overloads violently!")) + meltdown() + update_functionality() + return + if(wearer) + if(overload >= 75 && overload < 100) + to_chat(wearer, SPAN_DANGER("Your psi dampener is searing hot!")) + else if(overload >= 50 && overload < 75) + to_chat(wearer, SPAN_WARNING("Your psi dampener is uncomfortably hot...")) + else if(overload >= 25 && overload < 50) + to_chat(wearer, SPAN_WARNING("You feel your psi dampener heating up...")) + + // If all we're doing is logging the incident then just pass back stress without changing it. + if(source && source == wearer && implanted) + for(var/thing in SSpsi.psi_monitors) + var/obj/machinery/psi_monitor/monitor = thing + monitor.report_violation(src, stress) + if(use_psi_mode == PSI_IMPLANT_LOG) + return stress + else if(use_psi_mode == PSI_IMPLANT_SHOCK) + to_chat(wearer, SPAN_DANGER("Your psi dampener punishes you with a violent neural shock!")) + wearer.flash(5, FALSE , TRUE , TRUE , 10) + if(isliving(wearer)) + var/mob/living/M = wearer + if(M.psi) M.psi.stunned(5) + else if(use_psi_mode == PSI_IMPLANT_WARN) + to_chat(wearer, SPAN_WARNING("Your psi dampener primly informs you it has reported this violation.")) diff --git a/code/modules/psionics/equipment/null_ammo.dm b/code/modules/psionics/equipment/null_ammo.dm new file mode 100644 index 00000000000..e6b09bf0cfe --- /dev/null +++ b/code/modules/psionics/equipment/null_ammo.dm @@ -0,0 +1,23 @@ +/obj/item/projectile/bullet/magnum/nullglass + name = ".40 nullglass bullet" + damage_types = list(BRUTE = 15, HALLOSS = 6) +//TODO: uncomment var/shrapnel_type = /obj/item/material/shard/nullglass + embed = FALSE + +/obj/item/projectile/bullet/magnum/nullglass/disrupts_psionics() + return src + +/obj/item/projectile/bullet/magnum/on_hit(mob/living/target, def_zone = BP_CHEST) + // if(istype(target)) + //TODO: uncomment var/obj/item/material/shard/nullglass/N = new(null) + // target.embed(N, def_zone) + +/obj/item/ammo_casing/magnum/nullglass + desc = "A revolver bullet casing with a nullglass coating." + projectile_type = /obj/item/projectile/bullet/magnum/nullglass + +/obj/item/ammo_casing/magnum/nullglass/disrupts_psionics() + return src + +/obj/item/ammo_magazine/speedloader/magnum/nullglass + ammo_type = /obj/item/ammo_casing/magnum/nullglass diff --git a/code/modules/psionics/equipment/psimeter.dm b/code/modules/psionics/equipment/psimeter.dm new file mode 100644 index 00000000000..6dd5959d54d --- /dev/null +++ b/code/modules/psionics/equipment/psimeter.dm @@ -0,0 +1,72 @@ +/obj/machinery/psi_meter + name = "psi-meter" + desc = "A bulky psi-meter for conducting assays of psi-operants." + icon = 'icons/obj/machines/psimeter.dmi' + icon_state = "meter_on" + use_power = ACTIVE_POWER_USE + anchored = TRUE + density = TRUE + opacity = FALSE + + var/list/last_assay + var/mob/living/last_assayed + +/obj/machinery/psi_meter/update_icon() + ..() + if(use_power && operable()) + icon_state = "meter_on" + else + icon_state = "meter_off" + +/obj/machinery/psi_meter/attack_hand(mob/user) + add_fingerprint(user) + interact(user) + +/obj/machinery/psi_meter/interact(mob/user) + + if(!use_power) return + + var/list/dat = list() + if(LAZYLEN(last_assay)) + dat = last_assay + else + dat += "

TELESTO Mark I Psi-Meter


" + var/found + for(var/mob/living/H in range(1, src)) + found = TRUE + dat += "" + dat += "
Candidates
[H.name]Conduct Assay" + if(!found) + dat += "
No candidates found.
" + + var/datum/browser/popup = new(user, "psi_assay_\ref[src]", "Psi-Assay") + popup.set_content(jointext(dat,null)) + popup.open() + +/obj/machinery/psi_meter/Topic(href, href_list) + . = ..() + if(!.) + + if(!issilicon(usr) && !Adjacent(usr)) + return FALSE + + var/refresh + if(href_list["print"]) + if(last_assay) + var/obj/item/paper/P = new(loc) + P.name = "paper - Psi-Assay ([last_assayed.name])" + P.info = jointext(last_assay - last_assay[length(last_assay)],null) // Last line is 'print | clear' link line. + return TRUE + + if(href_list["clear"]) + last_assay = null + refresh = TRUE + else if(href_list["assay"]) + last_assayed = locate(href_list["assay"]) + if(istype(last_assayed)) + last_assayed.show_psi_assay(usr, src) + refresh = TRUE + + if(refresh) + interact(usr) + return TRUE diff --git a/code/modules/psionics/equipment/psimonitor.dm b/code/modules/psionics/equipment/psimonitor.dm new file mode 100644 index 00000000000..04f80afa6c2 --- /dev/null +++ b/code/modules/psionics/equipment/psimonitor.dm @@ -0,0 +1,117 @@ +/obj/machinery/psi_monitor + name = "psionic implant monitor" + icon = 'icons/obj/machines/psimeter.dmi' + icon_state = "meter_on" + use_power = ACTIVE_POWER_USE + anchored = TRUE + density = TRUE + opacity = FALSE + req_access = list(list(access_psychiatrist, access_captain, access_cmo, access_hos)) + + var/list/psi_violations = list() + var/show_violations = FALSE + var/authorized + +/obj/machinery/psi_monitor/New() + SSpsi.psi_monitors += src + ..() + +/obj/machinery/psi_monitor/emag_act(remaining_charges, mob/user) + if(!emagged) + emagged = TRUE + remaining_charges-- + req_access.Cut() + to_chat(user, SPAN_NOTICE("You short out the access protocols.")) + return TRUE + return FALSE + +/obj/machinery/psi_monitor/Topic(href, href_list) + + . = ..() + if(!.) + + if(href_list["login"]) + + var/obj/item/card/id/ID = usr.GetIdCard() + if(!ID || !allowed(usr)) + to_chat(usr, SPAN_WARNING("Access denied.")) + else + authorized = "[ID.registered_name] ([ID.assignment])" + . = 1 + + else if(href_list["logout"]) + authorized = FALSE + . = 1 + + else if(href_list["show_violations"]) + show_violations = (href_list["show_violations"] == "1") + . = 1 + + else if(href_list["remove_violation"]) + var/remove_ind = text2num(href_list["remove_violation"]) + if(remove_ind > 0 && remove_ind <= length(psi_violations)) + psi_violations.Cut(remove_ind, remove_ind++) + . = 1 + + else if(href_list["change_mode"]) + var/obj/item/implant/psi_control/I = locate(href_list["change_mode"]) + if(I.wearer && !I.malfunction) + var/choice = input("Select a new implant mode.", "Psi Dampener") as null|anything in list(PSI_IMPLANT_AUTOMATIC, PSI_IMPLANT_SHOCK, PSI_IMPLANT_WARN, PSI_IMPLANT_LOG, PSI_IMPLANT_DISABLED) + if(choice && I && I.wearer && !I.malfunction) + I.psi_mode = choice + I.update_functionality() + . = 1 + + if(. && usr) + interact(usr) + +/obj/machinery/psi_monitor/attack_hand(mob/user) + add_fingerprint(user) + interact(user) + +/obj/machinery/psi_monitor/interact(mob/user) + + var/list/dat = list() + dat += "

Psi Dampener Monitor

" + if(authorized) + dat += "[authorized]Logout" + else + dat += "Login" + + dat += "

Active Psionic Dampeners


" + dat += "
" + dat += "" + for(var/thing in SSpsi.psi_dampeners) + var/obj/item/implant/psi_control/I = thing + if(!I.wearer) + continue + dat += "" + if(I.malfunction) + dat += "" + else + dat += "" + dat += "" + dat += "
OperantSystem loadMode
[I.wearer.name]ERRORERROR[I.overload]%[authorized ? "[I.psi_mode]" : "[I.psi_mode]"]

" + + if(show_violations) + dat += "

Psionic Control Violations -


" + if(length(psi_violations)) + for(var/i = 1 to length(psi_violations)) + var/entry = psi_violations[i] + dat += "" + else + dat += "" + dat += "

[entry]
[authorized ? "Remove" : ""]
None reported.

" + else + dat += "

Psionic Control Violations +


" + + var/datum/browser/popup = new(user, "psi_monitor_\ref[src]", "Psi-Monitor") + popup.set_content(jointext(dat,null)) + popup.open() + + +/obj/machinery/psi_monitor/proc/report_failure(obj/item/implant/psi_control/I) + psi_violations += SPAN_NOTICE("Critical system failure - [I.wearer.name].") + +/obj/machinery/psi_monitor/proc/report_violation(obj/item/implant/psi_control/I, stress) + psi_violations += "Sigma [round(stress/10)] event - [I.wearer.name]." diff --git a/code/modules/psionics/equipment/psipower.dm b/code/modules/psionics/equipment/psipower.dm new file mode 100644 index 00000000000..2f3b502fa4d --- /dev/null +++ b/code/modules/psionics/equipment/psipower.dm @@ -0,0 +1,62 @@ +/obj/item/psychic_power + name = "psychic power" + icon = 'icons/obj/psychic_powers.dmi' + //atom_flags = 0 + anchored = TRUE + var/maintain_cost = 3 + var/mob/living/owner + +/obj/item/psychic_power/New(mob/living/_owner) + owner = _owner + if(!istype(owner)) + qdel(src) + return + START_PROCESSING(SSprocessing, src) + ..() + +/obj/item/psychic_power/Destroy() + if(istype(owner) && owner.psi) + LAZYREMOVE(owner.psi.manifested_items, src) + UNSETEMPTY(owner.psi.manifested_items) + STOP_PROCESSING(SSprocessing, src) + . = ..() + +/obj/item/psychic_power/get_storage_cost() + return ITEM_SIZE_NO_CONTAINER + +/obj/item/psychic_power/attack_self(mob/user) + sound_to(owner, 'sound/effects/psi/power_fail.ogg') + user.drop_from_inventory(src) + +/obj/item/psychic_power/attack(mob/living/M, mob/living/user, params) + . = ..(M, user, params) + if(M.do_psionics_check(max(force, maintain_cost), user)) + to_chat(user, SPAN_DANGER("\The [src] flickers violently out of phase!")) + return TRUE + +/obj/item/psychic_power/afterattack(atom/target, mob/living/user, proximity, params) + if(target.do_psionics_check(max(force, maintain_cost), user)) + to_chat(user, SPAN_DANGER("\The [src] flickers violently out of phase!")) + return + . = ..(target, user, proximity, params) + +/obj/item/psychic_power/dropped() + ..() + qdel(src) + +/obj/item/psychic_power/Process() + if(istype(owner)) + owner.psi.spend_power(maintain_cost) + if(!owner || owner.do_psionics_check(maintain_cost, owner) || !is_held()) + if(istype(loc,/mob/living)) + var/mob/living/carbon/human/host = loc + if(istype(host)) + for(var/obj/item/organ/external/organ in host.organs) + for(var/obj/item/O in organ.implants) + if(O == src) + organ.implants -= src + host.pinned -= src + host.embedded -= src + host.drop_from_inventory(src) + else + qdel(src) diff --git a/code/modules/psionics/equipment/psipower_blade.dm b/code/modules/psionics/equipment/psipower_blade.dm new file mode 100644 index 00000000000..9175870a5ea --- /dev/null +++ b/code/modules/psionics/equipment/psipower_blade.dm @@ -0,0 +1,25 @@ +/obj/item/psychic_power/psiblade + name = "psychokinetic slash" + force = WEAPON_FORCE_PAINFUL + armor_divisor = ARMOR_PEN_MAX // Psionic blades mostly ignore armour, but don't deal too much damage + sharp = TRUE + edge = TRUE + maintain_cost = 1 + icon_state = "psiblade_short" + +/obj/item/psychic_power/psiblade/master + force = WEAPON_FORCE_DANGEROUS + armor_divisor = ARMOR_PEN_MAX + maintain_cost = 1 + +/obj/item/psychic_power/psiblade/master/grand + force = WEAPON_FORCE_ROBUST + armor_divisor = ARMOR_PEN_MAX + maintain_cost = 1 + icon_state = "psiblade_long" + +/obj/item/psychic_power/psiblade/master/grand/paramount // Silly typechecks because rewriting old interaction code is outside of scope. + force = WEAPON_FORCE_BRUTAL + armor_divisor = ARMOR_PEN_MAX + maintain_cost = 1 + icon_state = "psiblade_long" diff --git a/code/modules/psionics/equipment/psipower_tinker.dm b/code/modules/psionics/equipment/psipower_tinker.dm new file mode 100644 index 00000000000..f3a813bec40 --- /dev/null +++ b/code/modules/psionics/equipment/psipower_tinker.dm @@ -0,0 +1,5 @@ +/obj/item/psychic_power/tinker + name = "psychokinetic crowbar" + icon_state = "tinker" + force = 1 + tool_qualities = list(QUALITY_SCREW_DRIVING = 50, QUALITY_BOLT_TURNING = 50, QUALITY_DRILLING = 20, QUALITY_WELDING = 30, QUALITY_CAUTERIZING = 10) diff --git a/code/modules/psionics/equipment/psipower_tk.dm b/code/modules/psionics/equipment/psipower_tk.dm new file mode 100644 index 00000000000..3e56a502d8e --- /dev/null +++ b/code/modules/psionics/equipment/psipower_tk.dm @@ -0,0 +1,105 @@ +/obj/item/psychic_power/telekinesis + name = "telekinetic grip" + maintain_cost = 6 + 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(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.")) + return FALSE + + focus = _focus + cut_overlays() + var/image/I = image(icon = focus.icon, icon_state = focus.icon_state) + I.color = focus.color + I.CopyOverlays(focus) + AddOverlays(I) + return TRUE + +/obj/item/psychic_power/telekinesis/attack_self(mob/user) + user.visible_message(SPAN_NOTICE("\The [user] makes a strange gesture.")) + sparkle() + return focus.attack_tk(user) + +/obj/item/psychic_power/telekinesis/afterattack(atom/target, mob/living/user, proximity) + + if(!target || !user || (isobj(target) && !isturf(target.loc)) || !user.psi || !user.psi.can_use() || !user.psi.spend_power(8)) + return + + user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN * 2) + user.psi.set_cooldown(8) + + var/user_psi_leech = user.do_psionics_check(5, user) + if(user_psi_leech) + to_chat(user, SPAN_WARNING("You reach for \the [target] but your telekinetic power is leeched away by \the [user_psi_leech]...")) + return + + if(target.do_psionics_check(5, user)) + to_chat(user, SPAN_WARNING("Your telekinetic power skates over \the [target] but cannot get a grip...")) + return + + 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*3, owner) + sleep(1) + sparkle() + owner.drop_from_inventory(src) + +/obj/item/psychic_power/telekinesis/proc/sparkle() + set waitfor = 0 + if(focus) + var/obj/effect/overlay/O = new /obj/effect/overlay(get_turf(focus)) + O.name = "sparkles" + O.anchored = TRUE + O.density = FALSE + O.layer = FLY_LAYER + O.set_dir(pick(GLOB.cardinal)) + O.icon = 'icons/effects/effects.dmi' + O.icon_state = "nothing" + flick("empdisable",O) + sleep(5) + qdel(O) diff --git a/code/modules/psionics/events/_psi.dm b/code/modules/psionics/events/_psi.dm new file mode 100644 index 00000000000..14fa2cef20c --- /dev/null +++ b/code/modules/psionics/events/_psi.dm @@ -0,0 +1,24 @@ +/datum/event/psi + startWhen = 30 + endWhen = 120 + +/datum/event/psi/announce() + priority_announcement.Announce( \ + "A localized disruption within the neighboring psionic continua has been detected. All psi-operant crewmembers \ + are advised to cease any sensitive activities and report to medical personnel in case of damage.", \ + "Cuchulain Sensor Array Automated Message" \ + ) + +/datum/event/psi/end() + priority_announcement.Announce( \ + "The psi-disturbance has ended and baseline normality has been re-asserted. \ + Anything you still can't cope with is therefore your own problem.", \ + "Cuchulain Sensor Array Automated Message" \ + ) + +/datum/event/psi/tick() + for(var/thing in SSpsi.processing) + apply_psi_effect(thing) + +/datum/event/psi/proc/apply_psi_effect(datum/psi_complexus/psi) + return diff --git a/code/modules/psionics/events/mini_spasm.dm b/code/modules/psionics/events/mini_spasm.dm new file mode 100644 index 00000000000..ab3270cdaee --- /dev/null +++ b/code/modules/psionics/events/mini_spasm.dm @@ -0,0 +1,67 @@ +/datum/event/minispasm + startWhen = 60 + endWhen = 90 + var/static/list/psi_operancy_messages = list( + "There's something in your skull!", + "Something is eating your thoughts!", + "You can feel your brain being rewritten!", + "Something is crawling over your frontal lobe!", + "THE SIGNAL THE SIGNAL THE SIGNAL THE SIGNAL THE" + ) + +/datum/event/minispasm/announce() + priority_announcement.Announce( \ + "PRIORITY ALERT: SIGMA-[rand(50,80)] PSIONIC SIGNAL LOCAL TRAMISSION DETECTED (97% MATCH, NONVARIANT) \ + (SIGNAL SOURCE TRIANGULATED ADJACENT LOCAL SITE): All personnel are advised to avoid \ + exposure to active audio transmission equipment including radio headsets and intercoms \ + for the duration of the signal broadcast.", \ + "Cuchulain Sensor Array Automated Message" \ + ) + +/datum/event/minispasm/start() + var/list/victims = list() + for(var/obj/item/device/radio/radio in GLOB.listening_objects) + if(radio.on) + for(var/mob/living/victim in range(radio.canhear_range, radio.loc)) + if(isnull(victims[victim]) && victim.stat == CONSCIOUS && !victim.ear_deaf) + victims[victim] = radio + for(var/thing in victims) + var/mob/living/victim = thing + var/obj/item/device/radio/source = victims[victim] + do_spasm(victim, source) + +/datum/event/minispasm/proc/do_spasm(mob/living/victim, obj/item/device/radio/source) + set waitfor = 0 + + if(iscarbon(victim) && !victim.isSynthetic()) + var/list/disabilities = list(NEARSIGHTED, EPILEPSY, NERVOUS) + for(var/disability in disabilities) + if(victim.disabilities & disability) + disabilities -= disability + if(length(disabilities)) + victim.disabilities |= pick(disabilities) + + if(victim.psi) + to_chat(victim, SPAN_DANGER("A hauntingly familiar sound hisses from [icon2html(source, victim)] \the [source], and your vision flickers!")) + victim.psi.backblast(rand(5,15)) + victim.Paralyse(5) + victim.make_jittery(100) + 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(FONT_LARGE(pick(psi_operancy_messages)))) + victim.adjustBrainLoss(rand(10,20)) + victim.set_psi_rank(pick_n_take(faculties), 1) + sleep(30) + victim.psi.update() + sleep(45) + victim.psi.check_latency_trigger(100, "a psionic scream", redactive = TRUE) + +/datum/event/minispasm/end() + priority_announcement.Announce( \ + "PRIORITY ALERT: SIGNAL BROADCAST HAS CEASED. Personnel are cleared to resume use of non-hardened radio transmission equipment. Have a nice day.", \ + "Cuchulain Sensor Array Automated Message" \ + ) diff --git a/code/modules/psionics/events/psi_balm.dm b/code/modules/psionics/events/psi_balm.dm new file mode 100644 index 00000000000..6859e9203e6 --- /dev/null +++ b/code/modules/psionics/events/psi_balm.dm @@ -0,0 +1,20 @@ +/datum/event/psi/balm + var/static/list/balm_messages = list( + "A soothing balm washes over your psyche.", + "For a moment, you can hear a distant, familiar voice singing a quiet lullaby.", + "A sense of peace and comfort falls over you like a warm blanket." + ) + +/datum/event/psi/balm/apply_psi_effect(datum/psi_complexus/psi) + var/soothed + if(psi.stun > 1) + psi.stun-- + soothed = TRUE + else if(psi.stamina < psi.max_stamina) + psi.stamina = min(psi.max_stamina, psi.stamina + rand(1,3)) + soothed = TRUE + else if(psi.owner.getBrainLoss() > 0) + psi.owner.adjustBrainLoss(-1) + soothed = TRUE + if(soothed && prob(10)) + to_chat(psi.owner, SPAN_NOTICE("[pick(balm_messages)]")) diff --git a/code/modules/psionics/events/psi_wail.dm b/code/modules/psionics/events/psi_wail.dm new file mode 100644 index 00000000000..59f2691c386 --- /dev/null +++ b/code/modules/psionics/events/psi_wail.dm @@ -0,0 +1,17 @@ +/datum/event/psi/wail + var/static/list/whine_messages = list( + "A nerve-tearing psychic whine intrudes on your thoughts.", + "A horrible, distracting humming sound breaks your train of thought.", + "Your head aches as a psychic wail intrudes on your psyche." + ) + +/datum/event/psi/wail/apply_psi_effect(datum/psi_complexus/psi) + var/annoyed + if(prob(1)) + psi.stunned(1) + annoyed = TRUE + else if(psi.stamina > 0) + psi.stamina = max(0, psi.stamina - rand(1,3)) + annoyed = TRUE + if(annoyed && prob(1)) + to_chat(psi.owner, SPAN_NOTICE("[pick(whine_messages)]")) diff --git a/code/modules/psionics/faculties/_faculty.dm b/code/modules/psionics/faculties/_faculty.dm new file mode 100644 index 00000000000..83bee78ef9a --- /dev/null +++ b/code/modules/psionics/faculties/_faculty.dm @@ -0,0 +1,11 @@ +/decl/psionic_faculty + var/id + var/name + var/associated_intent + var/list/armour_types = list() + var/list/powers = list() + +/decl/psionic_faculty/New() + ..() + for(var/atype in armour_types) + SSpsi.armour_faculty_by_type[atype] = id diff --git a/code/modules/psionics/faculties/_power.dm b/code/modules/psionics/faculties/_power.dm new file mode 100644 index 00000000000..e366e316fee --- /dev/null +++ b/code/modules/psionics/faculties/_power.dm @@ -0,0 +1,49 @@ +/decl/psionic_power + abstract_type = /decl/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' + +/decl/psionic_power/proc/invoke(mob/living/user, atom/target) + + message_admins("User is [user], target is [target], power is [src.type]") + + 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 + + var/user_psi_leech = user.do_psionics_check(cost, user) + if(user_psi_leech) + to_chat(user, SPAN_WARNING("Your power is leeched away by \the [user_psi_leech] as fast as you can focus it...")) + return FALSE + + if(target.do_psionics_check(cost, user)) + to_chat(user, SPAN_WARNING("Your power skates across \the [target], but cannot get a grip...")) + return FALSE + + return TRUE + +/decl/psionic_power/proc/handle_post_power(mob/living/user, atom/target) + if(cooldown) + user.psi.set_cooldown(cooldown) + if(admin_log && ismob(user) && ismob(target)) + 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 new file mode 100644 index 00000000000..1f04961c60f --- /dev/null +++ b/code/modules/psionics/faculties/coercion.dm @@ -0,0 +1,287 @@ +/decl/psionic_faculty/coercion + id = PSI_COERCION + name = "Coercion" + associated_intent = I_DISARM + armour_types = list(PSY) + +/decl/psionic_power/coercion + faculty = PSI_COERCION + abstract_type = /decl/psionic_power/coercion + +/decl/psionic_power/coercion/invoke(mob/living/user, mob/living/target) + if (!istype(target)) + to_chat(user, SPAN_WARNING("You cannot mentally attack \the [target].")) + return FALSE + + . = ..() + if(. && target.deflect_psionic_attack(user)) + return FALSE + +/decl/psionic_power/coercion/blindstrike + name = "Blindstrike" + cost = 8 + cooldown = 120 + use_ranged = TRUE + use_melee = TRUE + min_rank = PSI_RANK_GRANDMASTER + use_description = "Target the eyes or mouth on disarm intent and click anywhere to use a radial attack that blinds, deafens and disorients everyone near you." + +/decl/psionic_power/coercion/blindstrike/invoke(mob/living/user, mob/living/target) + if(user.targeted_organ != BP_MOUTH && user.targeted_organ != BP_EYES) + return FALSE + . = ..() + if(.) + user.visible_message(SPAN_DANGER("\The [user] suddenly throws back their head, as though screaming silently!")) + to_chat(user, SPAN_DANGER("You strike at all around you with a deafening psionic scream!")) + for(var/mob/living/M in orange(user, user.psi.get_rank(PSI_COERCION))) + if(M == user) + continue + /* TODO: implement resistance to these effects via stat + immunity (NT) + var/blocked = 100 * M.get_blocked_ratio(null, PSI) + if(prob(blocked)) + to_chat(M, SPAN_DANGER("A psionic onslaught strikes your mind, but you withstand it!")) + continue + */ + if(prob(60) && iscarbon(M)) + var/mob/living/carbon/C = M + if(!(C.species && (C.species.flags & NO_PAIN))) + M.emote("scream") + to_chat(M, SPAN_DANGER("Your senses are blasted into oblivion by a psionic scream!")) + M.flash(3, FALSE , FALSE , FALSE , 10) + M.eye_blind = max(M.eye_blind,3) + M.ear_deaf = max(M.ear_deaf,6) + M.confused = rand(3,8) + return TRUE + +/decl/psionic_power/coercion/mindread + name = "Read Mind" + cost = 6 + cooldown = 80 + 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." + +/decl/psionic_power/coercion/mindread/invoke(mob/living/user, mob/living/target) + if(!isliving(target) || !istype(target) || user.targeted_organ != BP_HEAD) + return FALSE + . = ..() + if(!.) + return + + if(target.stat == DEAD || (target.status_flags & FAKEDEATH) || !target.client) + to_chat(user, SPAN_WARNING("\The [target] is in no state for a mind-ream.")) + return TRUE + + user.visible_message(SPAN_WARNING("\The [user] touches \the [target]'s temple...")) + var/question = input(user, "Say something?", "Read Mind", "Penny for your thoughts?") as null|text + if(!question || user.incapacitated() || !do_after(user, 2 SECONDS, target, TRUE, TRUE, INCAPACITATION_DEFAULT, TRUE)) + return TRUE + + var/started_mindread = world.time + to_chat(user, SPAN_NOTICE("You dip your mentality into the surface layer of \the [target]'s mind, seeking an answer: [question]")) + to_chat(target, SPAN_NOTICE("Your mind is compelled to answer: [question]")) + + var/answer = input(target, question, "Read Mind") as null|text + if(!answer || world.time > started_mindread + 60 SECONDS || user.stat != CONSCIOUS || target.stat == DEAD) + to_chat(user, SPAN_NOTICE("You receive nothing useful from \the [target].")) + else + to_chat(user, SPAN_NOTICE("You skim thoughts from the surface of \the [target]'s mind: [answer]")) + 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 + +/decl/psionic_power/coercion/agony + name = "Agony" + cost = 8 + cooldown = 50 + use_melee = TRUE + min_rank = PSI_RANK_OPERANT + use_description = "Target the chest or groin on disarm intent to use a melee attack equivalent to a strike from a stun baton." + +/decl/psionic_power/coercion/agony/invoke(mob/living/user, mob/living/target) + if(!istype(target)) + return FALSE + if(user.targeted_organ != BP_CHEST && user.targeted_organ != BP_GROIN) + return FALSE + . = ..() + if(.) + user.visible_message(SPAN_DANGER("\The [target] has been struck by \the [user]!")) + message_admins("\The [target] has been struck by \the [user]!") + playsound(user.loc, 'sound/weapons/Egloves.ogg', 50, 1, -1) + target.stun_effect_act(0, 20 * user.psi.get_rank(PSI_COERCION), user.targeted_organ) // 40 agony damage by default - same as stunbaton. +20 for each rank above operant. + return TRUE + +/decl/psionic_power/coercion/psiblast + name = "Psiblast" + cost = 20 + cooldown = 20 + use_ranged = TRUE + min_rank = PSI_RANK_MASTER + use_description = "Use this ranged psi attack while on disarm intent, targeting the chest or groin. Your mastery of Coercion will determine how powerful the laser is. Be wary of overuse, and try not to fry your own brain." + +/decl/psionic_power/coercion/psiblast/invoke(mob/living/user, mob/living/target) + if(!istype(target)) + return FALSE + if(user.targeted_organ != BP_CHEST && user.targeted_organ != BP_GROIN) + return FALSE + . = ..() + if(.) + user.visible_message(SPAN_DANGER("\The [user]'s eyes flare with light!")) + + var/user_rank = user.psi.get_rank(faculty) + var/obj/item/projectile/pew + var/pew_sound + + switch(user_rank) + if(PSI_RANK_PARAMOUNT) + pew = new /obj/item/projectile/beam/psychic/heavy(get_turf(user)) + pew_sound = 'sound/effects/psiblast.ogg' + if(PSI_RANK_GRANDMASTER) + pew = new /obj/item/projectile/beam/psychic(get_turf(user)) + pew_sound = 'sound/effects/psiblast.ogg' + if(PSI_RANK_MASTER) + pew = new /obj/item/projectile/beam/psychic/light(get_turf(user)) + pew_sound = 'sound/effects/psiblast.ogg' + + if(istype(pew)) + playsound(pew.loc, pew_sound, 25, 1) + pew.original = target + pew.current = target + pew.starting = get_turf(user) + pew.shot_from = user + pew.launch(target, user.targeted_organ, (target.x-user.x), (target.y-user.y)) + return TRUE + +/decl/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." + +/decl/psionic_power/coercion/spasm/invoke(mob/living/user, mob/living/carbon/human/target) + if(!istype(target)) + return FALSE + + if(!(user.targeted_organ in list(BP_L_ARM, BP_R_ARM))) + 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") + + for(var/obj/item/I in list(target.get_active_hand(), target.get_inactive_hand())) + if(istype(I, /obj/item/grab)) //did they grab someone? + target.break_all_grabs(user) //See about breaking grips or pulls + else + target.unEquip(I) + target.visible_message(SPAN_DANGER("\The [target] drops \the [I] as their hand spasms!")) + return + return TRUE + +/decl/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." + +/decl/psionic_power/coercion/mindslave/invoke(mob/living/user, mob/living/target) + if(!istype(target) || user.targeted_organ != 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/datum/antagonist/A in target.mind.antagonist) + if(A.id == ROLE_THRALL) + to_chat(user, SPAN_WARNING("\The [target] is already in thrall to someone!")) + 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 ? 8 : 4) SECONDS, target, TRUE, TRUE, INCAPACITATION_DEFAULT, TRUE)) + 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.")) + make_antagonist(target.mind, ROLE_THRALL) + return TRUE + +/decl/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." + +/decl/psionic_power/coercion/assay/invoke(mob/living/user, mob/living/target) + if(user.targeted_organ != 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 ? 5 : 2.5) SECONDS, target, TRUE, TRUE, INCAPACITATION_DEFAULT, TRUE)) + 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 + +/decl/psionic_power/coercion/focus + name = "Focus" + cost = 10 + cooldown = 80 + use_grab = TRUE + min_rank = PSI_RANK_OPERANT + 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." + +/decl/psionic_power/coercion/focus/invoke(mob/living/user, mob/living/target) + if(user.targeted_organ != BP_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 ? 5 : 2.5) SECONDS, target, TRUE, TRUE, INCAPACITATION_DEFAULT, TRUE)) + 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.drowsyness = 0 + + if(istype(target, /mob/living/carbon)) + var/mob/living/carbon/C = target + C.adjust_hallucination(-30) + + if(istype(target, /mob/living/carbon/human)) + var/mob/living/carbon/human/H = target + var/datum/sanity/S = H.sanity + + if(coercion_rank >= PSI_RANK_MASTER) + S.restoreLevel(coercion_rank * 15 - 35) // 10 for master, 40 for paramount + if(coercion_rank >= PSI_RANK_GRANDMASTER && S.breakdowns.len) + S.fix_breakdowns() + + return TRUE diff --git a/code/modules/psionics/faculties/energistics.dm b/code/modules/psionics/faculties/energistics.dm new file mode 100644 index 00000000000..2914f064dbb --- /dev/null +++ b/code/modules/psionics/faculties/energistics.dm @@ -0,0 +1,115 @@ +/decl/psionic_faculty/energistics + id = PSI_ENERGISTICS + name = "Energistics" + associated_intent = I_HURT + armour_types = list("bomb", "laser", "energy") + +/decl/psionic_power/energistics + faculty = PSI_ENERGISTICS + abstract_type = /decl/psionic_power/energistics + +/decl/psionic_power/energistics/disrupt + name = "Disrupt" + cost = 10 + cooldown = 100 + use_melee = TRUE + min_rank = PSI_RANK_MASTER + use_description = "Target the head, eyes or mouth while on harm intent to use a melee attack that causes a localized electromagnetic pulse." + +/decl/psionic_power/energistics/disrupt/invoke(mob/living/user, mob/living/target) + if(user.targeted_organ != BP_HEAD && user.targeted_organ != BP_EYES && user.targeted_organ != BP_MOUTH) + return FALSE + if(istype(target, /turf)) + return FALSE + . = ..() + if(.) + user.visible_message(SPAN_DANGER("\The [user] releases a gout of crackling static and arcing lightning over \the [target]!")) + empulse(target, 0, 1) + return TRUE + +/decl/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." + +/decl/psionic_power/energistics/electrocute/invoke(mob/living/user, mob/living/target) + if(user.targeted_organ != BP_CHEST && user.targeted_organ != BP_GROIN) + return FALSE + if(!isliving(target)) + 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.targeted_organ) + return TRUE + +/decl/psionic_power/energistics/zorch + name = "Zorch" + cost = 10 + cooldown = 10 + use_ranged = TRUE + min_rank = PSI_RANK_OPERANT + use_description = "Use this ranged laser attack while on harm intent. Your mastery of Energistics will determine how powerful the laser is. Be wary of overuse, and try not to fry your own brain." + +/decl/psionic_power/energistics/zorch/invoke(mob/living/user, mob/living/target) + . = ..() + if(.) + user.visible_message(SPAN_DANGER("\The [user]'s eyes flare with light!")) + + var/user_rank = user.psi.get_rank(faculty) + var/obj/item/projectile/pew + var/pew_sound + + switch(user_rank) + if(PSI_RANK_PARAMOUNT) + pew = new /obj/item/projectile/beam/energistic/paramount(get_turf(user)) + pew_sound = 'sound/weapons/lasercannonfire.ogg' + if(PSI_RANK_GRANDMASTER) + pew = new /obj/item/projectile/beam/energistic/grandmaster(get_turf(user)) + pew_sound = 'sound/weapons/Laser.ogg' + if(PSI_RANK_MASTER) + pew = new /obj/item/projectile/beam/energistic/master(get_turf(user)) + pew_sound = 'sound/weapons/Laser.ogg' + if(PSI_RANK_OPERANT) + pew = new /obj/item/projectile/beam/energistic(get_turf(user)) + pew_sound = 'sound/weapons/Taser.ogg' + + if(istype(pew)) + playsound(pew.loc, pew_sound, 25, 1) + pew.original = target + pew.current = target + pew.starting = get_turf(user) + pew.shot_from = user + pew.launch(target, user.targeted_organ, (target.x-user.x), (target.y-user.y)) + return TRUE + +/decl/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." + +/decl/psionic_power/energistics/spark/invoke(mob/living/user, 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("[user] snaps \his fingers and \the [S.name] lights up.") + playsound(S.loc, "sparks", 50, 1) + else if(istype(target,/obj/item/cell) && user.psi.get_rank(PSI_ENERGISTICS) >= PSI_RANK_MASTER) + var/obj/item/cell/charging_cell = target.get_cell() + if(istype(charging_cell)) + charging_cell.give(rand(15,45)) + user.visible_message(SPAN_DANGER("\The [user] sends a jolt of electricity arcing into \the [target]!")) + else + var/datum/effect/effect/system/spark_spread/sparks = new () + sparks.set_up(3, 0, get_turf(target)) + sparks.start() + return TRUE diff --git a/code/modules/psionics/faculties/psychokinesis.dm b/code/modules/psionics/faculties/psychokinesis.dm new file mode 100644 index 00000000000..926624aba70 --- /dev/null +++ b/code/modules/psionics/faculties/psychokinesis.dm @@ -0,0 +1,93 @@ +/decl/psionic_faculty/psychokinesis + id = PSI_PSYCHOKINESIS + name = "Psychokinesis" + associated_intent = I_GRAB + armour_types = list("melee", "bullet") + +/decl/psionic_power/psychokinesis + faculty = PSI_PSYCHOKINESIS + use_manifest = TRUE + use_sound = null + abstract_type = /decl/psionic_power/psychokinesis + +/decl/psionic_power/psychokinesis/psiblade + name = "Psiblade" + cost = 10 + cooldown = 30 + min_rank = PSI_RANK_OPERANT + use_description = "Click on or otherwise activate an empty hand while on harm intent to manifest a psychokinetic cutting blade. The power the blade will vary based on your mastery of the faculty." + admin_log = FALSE + +/decl/psionic_power/psychokinesis/psiblade/invoke(mob/living/user, 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) + +/decl/psionic_power/psychokinesis/tinker + name = "Tinker" + cost = 5 + cooldown = 10 + min_rank = PSI_RANK_MASTER + use_description = "Click on or otherwise activate an empty hand while on help intent to manifest a psychokinetic tool." + admin_log = FALSE + +/decl/psionic_power/psychokinesis/tinker/invoke(mob/living/user, mob/living/target) + if((target && user != target) || user.a_intent != I_HELP) + return FALSE + . = ..() + if(.) + return new /obj/item/psychic_power/tinker(user) + +/decl/psionic_power/psychokinesis/telekinesis + name = "Telekinesis" + cost = 5 + cooldown = 6 + 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/static/list/valid_machine_types = list( + /obj/machinery/door + ) + +/decl/psionic_power/psychokinesis/telekinesis/invoke(mob/living/user, 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 diff --git a/code/modules/psionics/faculties/redaction.dm b/code/modules/psionics/faculties/redaction.dm new file mode 100644 index 00000000000..1128da096e5 --- /dev/null +++ b/code/modules/psionics/faculties/redaction.dm @@ -0,0 +1,186 @@ +/decl/psionic_faculty/redaction + id = PSI_REDACTION + name = "Redaction" + associated_intent = I_HELP + armour_types = list("bio", "rad") + +/decl/psionic_power/redaction + faculty = PSI_REDACTION + admin_log = FALSE + +/decl/psionic_power/redaction/proc/check_dead(mob/living/target) + if(!istype(target)) + return FALSE + if(target.stat == DEAD || (target.status_flags & FAKEDEATH)) + return TRUE + return FALSE + +/decl/psionic_power/redaction/invoke(mob/living/user, mob/living/target) + if(check_dead(target)) + return FALSE + . = ..() + +/decl/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." + +/decl/psionic_power/redaction/skinsight/invoke(mob/living/user, mob/living/target) + if(user.targeted_organ != BP_CHEST) + return FALSE + . = ..() + if(.) + user.visible_message(SPAN_NOTICE("\The [user] rests a hand on \the [target].")) + to_chat(user, medical_scan_results(target, TRUE, STAT_LEVEL_GODLIKE)) + return TRUE + +/decl/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. Higher ranks in this faculty allow you to mend a wider range of problems." + +/decl/psionic_power/redaction/mend/invoke(mob/living/user, mob/living/carbon/human/target) + if(!istype(user) || !istype(target)) + return FALSE + . = ..() + if(.) + var/obj/item/organ/external/E = target.get_organ(user.targeted_organ) + + 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_OPERANT) + var/removal_size = clamp(5-pk_rank, 0, 5) + + for(var/obj/item/organ/external/limb in target) + for(var/obj/thing in limb.implants) + if(!istype(thing, /obj/item/implant) && thing.w_class >= removal_size) + limb.remove_item(thing) + + if(!prob(redaction_rank * 22)) // 56% chance of damage for operant, 34% for master, 12% for grandmaster + limb.take_damage(rand(5, 10)) + to_chat(target, SPAN_DANGER("[thing.name] rips through your [limb.name].")) + to_chat(user, SPAN_NOTICE("You extend a tendril of psychokinetic-redactive power and rip out \the [thing.name].")) + else + to_chat(user, SPAN_NOTICE("You extend a tendril of psychokinetic-redactive power and carefully tease \the [thing.name] free of \the [limb.name].")) + + if(redaction_rank >= PSI_RANK_MASTER) + 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_NOTICE("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) && !BP_IS_CRYSTAL(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 + +/decl/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." + +/decl/psionic_power/redaction/cleanse/invoke(mob/living/user, 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.radiation) + to_chat(user, SPAN_NOTICE("You repair some of the radiation-damaged tissue within \the [target]...")) + if(target.radiation > removing) + target.radiation -= removing + else + target.radiation = 0 + return TRUE + if(target.getCloneLoss()) + to_chat(user, SPAN_NOTICE("You stitch together some of the mangled DNA within \the [target]...")) + if(target.getCloneLoss() >= removing) + target.adjustCloneLoss(-removing) + else + target.adjustCloneLoss(-(target.getCloneLoss())) + return TRUE + to_chat(user, SPAN_NOTICE("You can find no genetic damage or radiation to heal within \the [target].")) + return TRUE + +/decl/psionic_power/revive + name = "Revive" + cost = 40 // This ability is literally an exploit on its own + cooldown = 80 + use_grab = TRUE + min_rank = PSI_RANK_PARAMOUNT // But at least it is limited to the HIGHLY red paragraph antagonist + 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 = TRUE + +/decl/psionic_power/revive/invoke(mob/living/user, mob/living/target) + if(!isliving(target) || !istype(target) || user.targeted_organ != 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(ishuman(user)) + var/mob/living/carbon/human/H = user + + var/obj/item/organ/internal/vital/brain_organ = H.random_organ_by_process(BP_BRAIN) + + if(!(brain_organ)) + to_chat(user, SPAN_WARNING("You sense no brain.")) + return FALSE + + if(!target.is_asystole()) + to_chat(user, SPAN_WARNING("This person is still alive!")) + return FALSE + + if(world.time >= (target.timeofdeath + NECROZTIME)) + to_chat(user, SPAN_WARNING("This person has been dead for too long.")) + return FALSE + + user.visible_message(SPAN_NOTICE("\The [user] splays out their hands over \the [target]'s body...")) + if(!do_after(user, 10 SECONDS, target, TRUE, TRUE, INCAPACITATION_DEFAULT, TRUE)) + user.psi.backblast(rand(10,25)) + return TRUE + + target.rejuvenate() + return TRUE diff --git a/code/modules/psionics/interface/ui.dm b/code/modules/psionics/interface/ui.dm new file mode 100644 index 00000000000..22a8c16002a --- /dev/null +++ b/code/modules/psionics/interface/ui.dm @@ -0,0 +1,21 @@ +/obj/screen/psi + icon = 'icons/HUD/psi.dmi' + var/mob/living/owner + var/hidden = TRUE + +/obj/screen/psi/Initialize(mapload) + . = ..() + owner = loc + loc = null + update_icon() + +/obj/screen/psi/Destroy() + if(owner && owner.client) + owner.client.screen -= src + . = ..() + +/obj/screen/psi/update_icon() + if(hidden) + invisibility = INVISIBILITY_MAXIMUM + else + invisibility = 0 diff --git a/code/modules/psionics/interface/ui_hub.dm b/code/modules/psionics/interface/ui_hub.dm new file mode 100644 index 00000000000..43bcac342ab --- /dev/null +++ b/code/modules/psionics/interface/ui_hub.dm @@ -0,0 +1,71 @@ +/obj/screen/psi/hub + name = "Psi" + icon_state = "psi_suppressed" + screen_loc = "EAST-1:28,CENTER-3:11" + hidden = FALSE + maptext_x = 6 + maptext_y = -8 + var/image/on_cooldown + var/list/components + +/obj/screen/psi/hub/Initialize(mapload) + . = ..() + on_cooldown = image(icon, "cooldown") + components = list( + new /obj/screen/psi/armour(owner), + new /obj/screen/psi/toggle_psi_menu(owner, src) + ) + START_PROCESSING(SSprocessing, src) + +/obj/screen/psi/hub/update_icon() + ..() + if(!owner.psi) + return + + icon_state = owner.psi.suppressed ? "psi_suppressed" : "psi_active" + if(world.time < owner.psi.next_power_use) + AddOverlays(on_cooldown) + else + ClearOverlays() + var/offset = 1 + for(var/thing in components) + var/obj/screen/psi/component = thing + component.update_icon() + if(!component.invisibility) component.screen_loc = "EAST-[++offset]:28,CENTER-3:11" + +/obj/screen/psi/hub/Destroy() + STOP_PROCESSING(SSprocessing, src) + owner = null + for(var/thing in components) + qdel(thing) + components.Cut() + . = ..() + +/obj/screen/psi/hub/Process() + if(!istype(owner)) + qdel(src) + return + if(!owner.psi) + return + maptext = "[round((owner.psi.stamina/owner.psi.max_stamina)*100)]%" + update_icon() + +/obj/screen/psi/hub/Click(location, control, click_params) + var/list/params = params2list(click_params) + if(params["shift"]) + owner.show_psi_assay(owner) + return + + if(owner.psi.suppressed && owner.psi.stun) + to_chat(owner, SPAN_WARNING("You are dazed and reeling, and cannot muster enough focus to do that!")) + return + + owner.psi.suppressed = !owner.psi.suppressed + to_chat(owner, SPAN_NOTICE("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() diff --git a/code/modules/psionics/interface/ui_toggles.dm b/code/modules/psionics/interface/ui_toggles.dm new file mode 100644 index 00000000000..0b2d6f17cb3 --- /dev/null +++ b/code/modules/psionics/interface/ui_toggles.dm @@ -0,0 +1,45 @@ +// Begin psi armour toggle. +/obj/screen/psi/armour + name = "Psi-Armour" + icon_state = "psiarmour_off" + +/obj/screen/psi/armour/update_icon() + ..() + if(invisibility == 0) + icon_state = owner.psi.use_psi_armour ? "psiarmour_on" : "psiarmour_off" + +/obj/screen/psi/armour/Click() + if(!owner.psi) + return + owner.psi.use_psi_armour = !owner.psi.use_psi_armour + if(owner.psi.use_psi_armour) + to_chat(owner, SPAN_NOTICE("You will now use your psionics to deflect or block incoming attacks.")) + else + to_chat(owner, SPAN_NOTICE("You will no longer use your psionics to deflect or block incoming attacks.")) + update_icon() + +// End psi armour toggle. + +// Menu toggle. +/obj/screen/psi/toggle_psi_menu + name = "Show/Hide Psi UI" + icon_state = "arrow_left" + var/obj/screen/psi/hub/controller + +/obj/screen/psi/toggle_psi_menu/Initialize(mapload, obj/screen/psi/hub/_controller) + . = ..() + controller = _controller + +/obj/screen/psi/toggle_psi_menu/Click() + var/set_hidden = !hidden + for(var/thing in controller.components) + var/obj/screen/psi/psi = thing + psi.hidden = set_hidden + controller.update_icon() + +/obj/screen/psi/toggle_psi_menu/update_icon() + if(hidden) + icon_state = "arrow_left" + else + icon_state = "arrow_right" +// End menu toggle. diff --git a/code/modules/psionics/mob/mob.dm b/code/modules/psionics/mob/mob.dm new file mode 100644 index 00000000000..60104d66e10 --- /dev/null +++ b/code/modules/psionics/mob/mob.dm @@ -0,0 +1,73 @@ +/mob/living + var/datum/psi_complexus/psi + +/mob/living/Login() + . = ..() + if(psi) + psi.update(TRUE) + if(!psi.suppressed) + psi.show_auras() + +/mob/living/Destroy() + QDEL_NULL(psi) + . = ..() + +/mob/living/proc/set_psi_rank(faculty, rank, take_larger, defer_update, temporary) + if(!src.targeted_organ) // What is this for? + to_chat(src, SPAN_NOTICE("You feel something strange brush against your mind... but your brain is not able to grasp it.")) + return + if(!psi) + psi = new(src) + var/current_rank = psi.get_rank(faculty) + if(current_rank != rank && (!take_larger || current_rank < rank)) + psi.set_rank(faculty, rank, defer_update, temporary) + +/mob/living/proc/set_psi_power(amount = 0, additive = FALSE, defer_update = FALSE) + if(!src.targeted_organ) // What is this for? x2 + to_chat(src, SPAN_NOTICE("You feel something strange brush against your mind... but your brain is not able to grasp it.")) + return + if(!psi) + psi = new(src) + psi.set_power(amount, additive, defer_update) + +/mob/living/proc/deflect_psionic_attack(attacker) + +/* TODO: replace with our version of psi protection, taking NT into account + var/blocked = 100 * get_blocked_ratio(null, PSY) + if(prob(blocked)) + if(attacker) + to_chat(attacker, SPAN_WARNING("Your mental attack is deflected by \the [src]'s defenses!")) + to_chat(src, SPAN_DANGER("\The [attacker] strikes out with a mental attack, but you deflect it!")) + return TRUE +*/ + return FALSE + +// TODO: design objective for breaking X amount of people's minds. Until then this just reports to admins. +/mob/living/proc/reg_break(mob/living/carbon/human/victim) + log_admin("LOG: [src] ([src.ckey]) broke the mind of [victim] using psionic powers.") + message_admins("LOG: [src] ([src.ckey]) broke the mind of [victim] using psionic powers.") +/* + if(victim.get_species() != SPECIES_HUMAN) + return + + if(!contract) + return + + if(victim == src) + return + + // Give a signal that the break was registred and counted towards objective + to_chat(src, SPAN_DANGER("[src] you feel you've succeeded in your goal.")) + + victims |= victim + + if(contract.completed) + to_chat(owner.current, SPAN_DANGER("Somebody all ready have comleted targeted contract.")) + contract = null + START_PROCESSING(SSobj, src) + + else if(victims.len >= contract.count) + contract.report(src) + contract = null + START_PROCESSING(SSobj, src) +*/ diff --git a/code/modules/psionics/mob/mob_assay.dm b/code/modules/psionics/mob/mob_assay.dm new file mode 100644 index 00000000000..c33aa91aa0e --- /dev/null +++ b/code/modules/psionics/mob/mob_assay.dm @@ -0,0 +1,93 @@ +/mob/living/proc/show_psi_assay(mob/viewer, obj/machinery/psi_meter/machine) + + if(!viewer) viewer = usr + + 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(mind && !psi.suppressed) + var/is_thrall = FALSE + var/is_paramount = FALSE + for(var/datum/antagonist/A in mind.antagonist) + if(A.id == ROLE_THRALL) + is_thrall = TRUE + if(A.id == ROLE_PARAMOUNT) + is_paramount = TRUE + if(is_paramount) + use_rating = SPAN_NOTICE("[effective_rating]-Alpha-Plus") + rating_descriptor = "This indicates a completely deviant psi complexus, either beyond or outside anything currently recorded. Approach with care." + // This space intentionally left blank (for Omega-Minus psi vampires. todo) + if(viewer != usr && is_thrall && ishuman(viewer)) + var/mob/living/H = viewer + if(H.psi && H.psi.get_rank(PSI_REDACTION) >= PSI_RANK_GRANDMASTER) + dat += SPAN_NOTICE("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 = SPAN_NOTICE("[effective_rating]-Gamma") + rating_descriptor = "This indicates the presence of minor psi capabilities of the Operant rank or higher." + if(3) + use_rating = SPAN_WARNING("[effective_rating]-Delta") + rating_descriptor = "This indicates the presence of psi capabilities of the Master rank or higher." + if(4) + use_rating = SPAN_WARNING("[effective_rating]-Beta") + rating_descriptor = "This indicates the presence of significant psi capabilities of the Grandmaster rank or higher." + if(5) + use_rating = SPAN_DANGER("[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 += "They have an overall psi rating of [use_rating].
[rating_descriptor]
" + + if(!istype(machine)) + + dat += "They are currently [psi.suppressed ? "suppressing" : "not suppressing"] your psychic operancy.
" + dat += "They have [psi.stamina / psi.max_stamina * 100]% psi stamina remaining.
" + dat += "
" + + for(var/faculty_id in psi.ranks) + var/decl/psionic_faculty/faculty = SSpsi.get_faculty(faculty_id) + if(psi.ranks[faculty.id] > 0) + dat += "They are assayed at the rank of [GLOB.psychic_ranks_to_strings[psi.ranks[faculty.id]]] for the [faculty.name] faculty.
" + else + dat += "They have no notable power within the [faculty.name] faculty.
" + dat += "
" + + if(viewer == usr) + dat += "" + for(var/faculty_id in psi.ranks) + var/list/check_powers = psi.get_powers_by_faculty(faculty_id) + if(LAZYLEN(check_powers)) + var/decl/psionic_faculty/faculty = SSpsi.get_faculty(faculty_id) + dat += "" + for(var/decl/psionic_power/power in check_powers) + dat += "" + dat += "

Psi-power Usage

They have access to the following psi-powers within the [faculty.name] faculty:
[power.name][power.use_description]
" + else + dat += "They have no notable psychic latency or operancy." + + if(istype(machine)) + dat += "Print Clear Buffer" + machine.last_assay = dat + return + + 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_interactions.dm b/code/modules/psionics/mob/mob_interactions.dm new file mode 100644 index 00000000000..578a620e4cd --- /dev/null +++ b/code/modules/psionics/mob/mob_interactions.dm @@ -0,0 +1,41 @@ +#define INVOKE_PSI_POWERS(holder, powers, target, return_on_invocation) \ + if(holder && holder.psi && holder.psi.can_use()) { \ + for(var/thing in powers) { \ + var/decl/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(atom/A, proximity) + . = ..() + if(. && psi) + INVOKE_PSI_POWERS(src, psi.get_melee_powers(SSpsi.faculties_by_intent[a_intent]), A, FALSE) + src.visible_message(SPAN_NOTICE("Holder is [src]. Target is [A].")) // TODO: REMOVE LINE + +/mob/living/RangedAttack(atom/A, params) + if(psi) + INVOKE_PSI_POWERS(src, psi.get_ranged_powers(SSpsi.faculties_by_intent[a_intent]), A, TRUE) + src.visible_message(SPAN_NOTICE("Holder is [src]. Target is [A].")) // TODO: REMOVE LINE + . = ..() + +/mob/living/proc/check_psi_grab(obj/item/grab/grab) + src.visible_message(SPAN_NOTICE("Holder is [src]. Target is [grab.affecting].")) // TODO: REMOVE LINE + 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(bp_hand) + if(psi) + INVOKE_PSI_POWERS(src, psi.get_manifestations(), src, FALSE) + src.visible_message(SPAN_NOTICE("Holder is [src]. Target is [src].")) // TODO: REMOVE LINE + . = ..() + +#undef INVOKE_PSI_POWERS diff --git a/code/modules/psionics/null/_null.dm b/code/modules/psionics/null/_null.dm new file mode 100644 index 00000000000..2f22237d7e0 --- /dev/null +++ b/code/modules/psionics/null/_null.dm @@ -0,0 +1,30 @@ +/** + * Whether or not this atom or its contents will disrupt psionics. Top-level proc recursively checks all contents. + * + * Returns instance of `/atom/movable` or `FALSE`. Either the atom that can disrupt psionics, or `FALSE` if nothing will + * disrupt. + */ +/atom/proc/disrupts_psionics() + for(var/thing in contents) + var/atom/movable/AM = thing + var/disrupted_by = AM.disrupts_psionics() + if(disrupted_by) + return disrupted_by + return FALSE + +/atom/proc/do_psionics_check(stress, atom/source) + var/turf/T = get_turf(src) + if(istype(T) && T != src) + return T.do_psionics_check(stress, source) + withstand_psi_stress(stress, source) + . = disrupts_psionics() + +/atom/proc/withstand_psi_stress(stress, atom/source) + . = max(stress, 0) + if(.) + for(var/thing in contents) + var/atom/movable/AM = thing + if(istype(AM) && AM != src && AM.disrupts_psionics()) + . = AM.withstand_psi_stress(., source) + if(. <= 0) + break diff --git a/code/modules/psionics/null/chemistry.dm b/code/modules/psionics/null/chemistry.dm new file mode 100644 index 00000000000..ac68eb778a5 --- /dev/null +++ b/code/modules/psionics/null/chemistry.dm @@ -0,0 +1,88 @@ +/datum/chemical_reaction/nullglass + name = "Soulstone" + result = null + required_reagents = list(/datum/reagent/blood = 15, /datum/reagent/crystal = 1) + result_amount = 1 + +/datum/chemical_reaction/nullglass/get_reaction_flags(datum/reagents/holder) + for(var/datum/reagent/blood/blood in holder.reagent_list) + var/weakref/donor_ref = islist(blood.data) && blood.data["donor"] + if(istype(donor_ref)) + var/mob/living/donor = donor_ref.resolve() + if(istype(donor) && (donor.psi || (donor.mind && GLOB.wizards.is_antagonist(donor.mind)))) + return TRUE + +/datum/chemical_reaction/nullglass/on_reaction(datum/reagents/holder, created_volume, reaction_flags) + var/location = get_turf(holder.my_atom) + if(reaction_flags) + for(var/i = 1, i <= created_volume, i++) + new /obj/item/device/soulstone(location) + else + for(var/i = 1, i <= created_volume*2, i++) + new /obj/item/material/shard(location, MATERIAL_CRYSTAL) + +/datum/reagent/crystal + name = "crystallizing agent" + taste_description = "sharpness" + reagent_state = LIQUID + color = "#13bc5e" + +/datum/reagent/crystal/affect_blood(mob/living/carbon/M, removed) + var/result_mat = (M.psi || (M.mind && GLOB.wizards.is_antagonist(M.mind))) ? MATERIAL_NULLGLASS : MATERIAL_CRYSTAL + if(ishuman(M)) + var/mob/living/carbon/human/H = M + for(var/obj/item/organ/external/E in shuffle(H.organs.Copy())) + if(E.is_stump()) + continue + + if(BP_IS_CRYSTAL(E)) + if((E.brute_dam + E.burn_dam) > 0) + if(prob(35)) + to_chat(M, SPAN_NOTICE("You feel a crawling sensation as fresh crystal grows over your [E.name].")) + E.heal_damage(rand(5,8), rand(5,8)) + break + if(BP_IS_BRITTLE(E)) + E.status &= ~ORGAN_BRITTLE + break + else if(prob(15)) + to_chat(H, SPAN_DANGER("Your [E.name] is being lacerated from within!")) + if(E.can_feel_pain()) + H.emote("scream") + if(prob(25)) + for(var/i = 1 to rand(1,3)) + new /obj/item/material/shard(get_turf(E), result_mat) + E.take_external_damage(rand(50,70), 0) + else + E.take_external_damage(rand(20,30), 0) + E.status |= ORGAN_CRYSTAL + E.status |= ORGAN_BRITTLE + break + + for(var/obj/item/organ/internal/I in shuffle(H.internal_organs.Copy())) + if(I.organ_tag == BP_BRAIN) + continue + + if(BP_IS_CRYSTAL(I)) + if(prob(35)) + to_chat(M, SPAN_NOTICE("You feel a deep, sharp tugging sensation as your [I.name] is mended.")) + I.heal_damage(rand(1,3)) + break + if(BP_IS_BRITTLE(I)) + I.status &= ~ORGAN_BRITTLE + break + else if(prob(15)) + to_chat(H, SPAN_DANGER("You feel visceral, sharp twisting within your body!")) + if(I.can_feel_pain()) + H.emote("scream") + if(prob(25)) + I.take_internal_damage(rand(10,15), 0) + else + I.take_internal_damage(rand(5,10), 0) + I.status |= ORGAN_CRYSTAL + I.status |= ORGAN_BRITTLE + break + else + to_chat(M, SPAN_DANGER("Your flesh is being lacerated from within!")) + M.adjustBruteLoss(rand(3,6)) + if(prob(10)) + new /obj/item/material/shard(get_turf(M), result_mat) diff --git a/code/modules/psionics/null/flooring.dm b/code/modules/psionics/null/flooring.dm new file mode 100644 index 00000000000..4e9e3c84f87 --- /dev/null +++ b/code/modules/psionics/null/flooring.dm @@ -0,0 +1,20 @@ +/decl/flooring + var/psi_null + +/decl/flooring/proc/is_psi_null() + return psi_null + +/decl/flooring/tiling/nullglass + name = "nullglass plating" + desc = "You can hear the tiles whispering..." + icon_base = "nullglass" + color = COLOR_NULLGLASS + has_damage_range = null + flags = TURF_REMOVE_SCREWDRIVER + build_type = /obj/item/stack/tile/floor_nullglass + psi_null = TRUE + +/obj/item/stack/tile/floor_nullglass + name = "nullglass floor tile" + icon_state = "tile_nullglass" + matter = list(MATERIAL_NULLGLASS = 937.5) diff --git a/code/modules/psionics/null/material.dm b/code/modules/psionics/null/material.dm new file mode 100644 index 00000000000..983fc94ee92 --- /dev/null +++ b/code/modules/psionics/null/material.dm @@ -0,0 +1,32 @@ +/material + var/is_psionic_nullifier + +/material/proc/is_psi_null() + return is_psionic_nullifier + +/material/nullglass + is_psionic_nullifier = TRUE + +/material/nullglass + name = MATERIAL_NULLGLASS + icon_colour = COLOR_NULLGLASS + conductive = 1 + stack_type = /obj/item/stack/material/nullglass + flags = MATERIAL_BRITTLE + opacity = 0.5 + integrity = 30 + shard_type = SHARD_SHARD + tableslam_noise = 'sound/effects/Glasshit.ogg' + hardness = 80 + weight = 25 + sheet_icon_base = "diamond" + sheet_singular_name = "gem" + sheet_plural_name = "gems" + door_icon_base = "stone" + destruction_desc = "shatters" + hitsound = 'sound/effects/Glasshit.ogg' + hidden_from_codex = TRUE + +/material/nullglass/generate_recipes() + . = ..() + . += new /datum/stack_recipe/tile/nullglass(src) diff --git a/code/modules/psionics/null/material_sheet.dm b/code/modules/psionics/null/material_sheet.dm new file mode 100644 index 00000000000..455cbb258e2 --- /dev/null +++ b/code/modules/psionics/null/material_sheet.dm @@ -0,0 +1,22 @@ +/obj/item/stack/material/withstand_psi_stress(stress, atom/source) + . = ..(stress, source) + if(amount > 0 && . > 0 && disrupts_psionics()) + if(. > amount) + use(amount) + . -= amount + else + use(stress) + . = 0 + +/obj/item/stack/material/disrupts_psionics() + return (material && material.is_psi_null()) ? src : FALSE + +/obj/item/stack/material/nullglass + name = "nullglass" + icon_state = "diamond" + plural_icon_state = "diamond-mult" + max_icon_state = "diamond-max" + default_type = MATERIAL_NULLGLASS + +/obj/item/stack/material/nullglass/fifty + amount = 50 diff --git a/code/modules/psionics/null/material_weapon.dm b/code/modules/psionics/null/material_weapon.dm new file mode 100644 index 00000000000..24ef29c6c87 --- /dev/null +++ b/code/modules/psionics/null/material_weapon.dm @@ -0,0 +1,11 @@ +/obj/item/material/disrupts_psionics() + return (material && material.is_psi_null()) ? src : FALSE + +/obj/item/material/withstand_psi_stress(stress, atom/source) + . = ..(stress, source) + if(!health_dead() && . > 0 && disrupts_psionics()) + damage_health(.) + . = max(0, -(get_current_health())) + +/obj/item/material/shard/nullglass/New(newloc) + ..(newloc, MATERIAL_NULLGLASS) diff --git a/code/modules/psionics/null/turf_floor.dm b/code/modules/psionics/null/turf_floor.dm new file mode 100644 index 00000000000..b60aada63de --- /dev/null +++ b/code/modules/psionics/null/turf_floor.dm @@ -0,0 +1,7 @@ +/turf/simulated/floor/disrupts_psionics() + return (flooring && flooring.is_psi_null()) ? src : ..() + +/turf/simulated/floor/tiled/nullglass + name = "nullglass floor" + icon_state = "nullglass" + initial_flooring = /decl/flooring/tiling/nullglass diff --git a/code/modules/psionics/null/turf_wall.dm b/code/modules/psionics/null/turf_wall.dm new file mode 100644 index 00000000000..c047beb0b03 --- /dev/null +++ b/code/modules/psionics/null/turf_wall.dm @@ -0,0 +1,18 @@ +/turf/simulated/wall/disrupts_psionics() + return ((material && material.is_psi_null()) || (reinf_material && reinf_material.is_psi_null())) ? src : ..() + +/turf/simulated/wall/withstand_psi_stress(stress, atom/source) + . = ..(stress, source) + if(. > 0 && disrupts_psionics()) + var/cap = material.integrity + if(reinf_material) cap += reinf_material.integrity + var/stress_total = get_damage_value() + . + damage_health(.) + . = max(0, -(cap-stress_total)) + +/turf/simulated/wall/nullglass + color = "#ff6088" + +/turf/simulated/wall/nullglass/Initialize(ml) + color = null + ..(ml, MATERIAL_NULLGLASS) diff --git a/code/modules/psionics/null/~null.dm b/code/modules/psionics/null/~null.dm new file mode 100644 index 00000000000..9269576877c --- /dev/null +++ b/code/modules/psionics/null/~null.dm @@ -0,0 +1,3 @@ +#undef PSI_IMPLANT_SHOCK +#undef PSI_IMPLANT_WARN +#undef PSI_IMPLANT_DISABLED \ No newline at end of file diff --git a/code/modules/sanity/sanity_mob.dm b/code/modules/sanity/sanity_mob.dm index c3b7f092359..3c22e5108c6 100644 --- a/code/modules/sanity/sanity_mob.dm +++ b/code/modules/sanity/sanity_mob.dm @@ -187,6 +187,11 @@ GLOBAL_VAR_INIT(GLOBAL_INSIGHT_MOD, 1) if(!B.update()) breakdowns -= B +/datum/sanity/proc/fix_breakdowns(positives_too = FALSE) + for(var/datum/breakdown/B in breakdowns) + if(positives_too || !istype(B, /datum/breakdown/positive)) + B.conclude() + /datum/sanity/proc/handle_Insight() var/moralist_factor = 1 var/style_factor = owner.get_style_factor() @@ -439,8 +444,9 @@ GLOBAL_VAR_INIT(GLOBAL_INSIGHT_MOD, 1) return updateLevel(amount) +// This is most likely meant to raise your sanity to a minimum - before Psionics update this did the opposite, lowering you to a maximum. Not sure why it was like that when this is a reward for completing breakdowns - Humon /datum/sanity/proc/restoreLevel(amount) - if(level <= amount) + if(level >= amount) return updateLevel(amount) diff --git a/html/changelogs/archive/2025-03.yml b/html/changelogs/archive/2025-03.yml index 5160afa38f2..3bcb450941e 100644 --- a/html/changelogs/archive/2025-03.yml +++ b/html/changelogs/archive/2025-03.yml @@ -4,3 +4,45 @@ VmpOS2NHSkhkejA9: - tweak: Things that affect your screen color no longer affect your HUD - balance: Oborin syndrome no longer affects your HUD +2025-03-17: + Chickenish: + - balance: did something with poison + - code_imp: added means for external progression of wounds + - rscadd: Improved Roach AI + - balance: Kaiser now smokes more often + - bugfix: Sleeping Creatures should now wake up when they take damage. + Mycah142: + - bugfix: Mannequins no longer process Life() or sanity, nor appear on Baptismal + record + VmpOS2NHSkhkejA9: + - rscadd: Gold golems; lethal ranged enemies that summon spikes under their target + - balance: massive difficulty increase for cave mining; Spawns have been reworked + to spawn golems in groups with types dependent on seismic level. Additionally, + new types have been added, some have been reworked and their AI has been tuned + up to let them see further and attack in coordinated groups, making it far more + difficult to pick them off alone. + - rscdel: Miner's shotgun and jackhammer. + - imageadd: added icons for gold golem spikes + - code_imp: Basic grab functionality for superior animals; their AI still isn't + set to do this, so it must be trigged externally by each mob that intends to + grab. + - rscdel: Tile-based resources. Nothing actually used them, anyway. + - tweak: Ammo is compressed into stacks when dumped from boxes and magazines. + - tweak: Material stacks merge when moving over each other and when created. + - balance: Ammo piles deal no fall damage unless there's 10 or more casings in the + pile. + - balance: Stacks deal fall damage based on the number of sheets in the stack. + - balance: Osmium deals 2.5x more fall damage than other stack types. +2025-03-22: + Chickenish: + - balance: eating toxic mushrooms is now a bad idea. + - bugfix: fixed damage, probably. + Mycah142: + - rscdel: Removed roach hats +2025-03-30: + Chickenish: + - rscadd: Added setup disks, the hot old thing! + - tweak: Techno Storage no longer contains setup boards for the medical or security + records. These have been replaced with appropriate setup disks. + - balance: Groin efficiency now affects your getting up speed. If you have none, + you can't do it! diff --git a/icons/HUD/psi.dmi b/icons/HUD/psi.dmi new file mode 100644 index 0000000000000000000000000000000000000000..569d374de74cf48f048ebe5acf83369ffeca9642 GIT binary patch literal 6050 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 LNkvXXu0mjfRD@lD literal 0 HcmV?d00001 diff --git a/icons/effects/psi_aura_small3.dmi b/icons/effects/psi_aura_small3.dmi new file mode 100644 index 0000000000000000000000000000000000000000..f5e222b61f5ff79daadc01cfc7fa6cd8c32ea515 GIT binary patch literal 3594 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 Dnh48Y literal 0 HcmV?d00001 diff --git a/icons/obj/guns/foundation.dmi b/icons/obj/guns/foundation.dmi new file mode 100644 index 0000000000000000000000000000000000000000..0b5f2e696e7bc51aff4b97cf533334c2c100b212 GIT binary patch literal 938 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=T~#3wB`&GO$wiq3C7Jno3=9=> zrk>i!b;v-(?V;s&&AMB$QKe^rW#xQwEX9NA?Ak}Stwrv*+&R~{{&?!HTdZl@ zzC75vG-^Si9nX?^C%s#az4Xv3+U0(8d8crj+x3Z`GIK9Tc(z_mz4x2h>26o~q^zHw z#=o~bjV^9{YWnB+@%%lwp~#Fo zx@&Jqj8_bpSFi>g z=2;--z-!zP#bCPqZ`l++KHk^m9}ipS)mqLzzL_!YK{w<2EPm@b(`PRzDaf#3eerCj z!NXl_I@~!?Yo#w|{SDI6S>`^YF!jKy2g&NWdS#cMZM)fWVII>W4XIwY5Ux%aCWZy8v`l=R zO{7|vWo}Lvab-OJ`NY#A3BLBt+v@iBJ1uNj|NJ>a!{(DIPi^M$ZdkXawDfG$h8^60 zfZ9SrOKbFmIvoNY394xm?gec) z1!qa;J0$$)Uol-v@^kU->pw-V?8rNLzt(Ve{fECdzW!-^*Kk>XzCD}eJbs(=@+%nq q%ii1b6xqmE%$xsLj|hu?a?k0p+j!u3=L%rvWAJqKb6Mw<&;$UbkFRS0 literal 0 HcmV?d00001 diff --git a/icons/obj/machines/psimeter.dmi b/icons/obj/machines/psimeter.dmi new file mode 100644 index 0000000000000000000000000000000000000000..d39e351f2cfa065a361125848a56c1d27e534836 GIT binary patch literal 1213 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 Wt=%d~|KHtad7A8Ng3;3zp#KM<<- Date: Mon, 18 May 2026 21:56:06 -0400 Subject: [PATCH 02/10] whoops --- code/__DEFINES/gamemode.dm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/__DEFINES/gamemode.dm b/code/__DEFINES/gamemode.dm index c88b36acf96..94b88cb50f3 100644 --- a/code/__DEFINES/gamemode.dm +++ b/code/__DEFINES/gamemode.dm @@ -65,6 +65,8 @@ // #define ROLE_DEATHSQUAD "Death Squad" // #define ROLE_ARTIST "Artist" #define ROLE_MERCENARY "Serbian Mercenary" +#define ROLE_PARAMOUNT "Paramount" +#define ROLE_THRALL "Thrall" #define ROLE_PIRATE "Pirate" #define ROLE_CARRION "Carrion" #define ROLE_MONKEY "Monkey" From 08cb4fc6e3b2a3b64102446fabb964ee48ae7b5d Mon Sep 17 00:00:00 2001 From: Mycah142 Date: Tue, 19 May 2026 16:02:53 -0400 Subject: [PATCH 03/10] gotta rebase probably. --- code/__DEFINES/maths.dm | 4 ---- code/__HELPERS/turfs.dm | 2 +- code/_onclick/hud/screen_objects/base_screen_objects.dm | 3 ++- code/controllers/subsystems/processing/psi.dm | 8 ++++---- code/game/antagonist/station/thrall.dm | 2 +- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/code/__DEFINES/maths.dm b/code/__DEFINES/maths.dm index b9153f8b781..f663712b1ee 100644 --- a/code/__DEFINES/maths.dm +++ b/code/__DEFINES/maths.dm @@ -231,10 +231,6 @@ if(isnum(num)) return round(num)+1 -// Slightly more accurate than n_ceil -/proc/ceil(number) - return -round(-number) - // Find leftmost bit using bitshifting /proc/leftmost_bit(num) var/pos = 0 diff --git a/code/__HELPERS/turfs.dm b/code/__HELPERS/turfs.dm index 3dda693b027..2eed4a83737 100644 --- a/code/__HELPERS/turfs.dm +++ b/code/__HELPERS/turfs.dm @@ -46,7 +46,7 @@ return FALSE return TRUE -/proc/clear_interior(var/turf/T) +/proc/clear_interior(turf/T) if (turf_clear(T)) if (!turf_is_external(T)) return TRUE diff --git a/code/_onclick/hud/screen_objects/base_screen_objects.dm b/code/_onclick/hud/screen_objects/base_screen_objects.dm index 1d1427d9658..9f7138d2c24 100644 --- a/code/_onclick/hud/screen_objects/base_screen_objects.dm +++ b/code/_onclick/hud/screen_objects/base_screen_objects.dm @@ -330,7 +330,8 @@ usr:swap_hand() if("hand") usr:swap_hand() - else usr.attack_ui(slot_id) + else + usr.attack_ui(slot_id) return TRUE /atom/movable/screen/inventory/hand diff --git a/code/controllers/subsystems/processing/psi.dm b/code/controllers/subsystems/processing/psi.dm index e9069565f80..44552495192 100644 --- a/code/controllers/subsystems/processing/psi.dm +++ b/code/controllers/subsystems/processing/psi.dm @@ -1,20 +1,20 @@ GLOBAL_LIST_INIT(psychic_ranks_to_strings, list("Latent", "Operant", "Masterclass", "Grandmasterclass", "Paramount")) //This right here is why porting is hard -/datum/var/abstract_type +/datum/var/abstract_check /datum/proc/IsAbstract() SHOULD_NOT_OVERRIDE(TRUE) - return type == abstract_type + return type == abstract_check /proc/is_abstract(datum/thing) if (ispath(thing)) - return thing == initial(thing.abstract_type) + return thing == initial(thing.abstract_check) if (istype(thing)) return thing.IsAbstract() //This is a TODO to kill, literally only used once PROCESSING_SUBSYSTEM_DEF(psi) name = "Psychics" - priority = SS_PRIORITY_PSYCHICS + priority = FIRE_PRIORITY_PSYCHICS flags = SS_POST_FIRE_TIMING | SS_BACKGROUND var/list/faculties_by_id = list() diff --git a/code/game/antagonist/station/thrall.dm b/code/game/antagonist/station/thrall.dm index d12a53918f4..cc4815f882e 100644 --- a/code/game/antagonist/station/thrall.dm +++ b/code/game/antagonist/station/thrall.dm @@ -17,7 +17,7 @@ obey.explanation_text = "Obey your master, [controller.real_name], in all things." owner.individual_objectives |= obey -/datum/antagonist/thrall/create_antagonist(datum/mind/owner, ignore_role, do_not_equip, move_to_spawn, do_not_announce, preserve_appearance, mob/new_controller) +/datum/antagonist/thrall/create_antagonist(datum/mind/owner, datum/faction/new_faction, doequip = TRUE, announce = TRUE, update = TRUE, check = TRUE, mob/new_controller) if(!new_controller) return 0 . = ..() From 2a6d0b29ae0afd68579c5fba553fecc78474b1ec Mon Sep 17 00:00:00 2001 From: Mycah142 Date: Tue, 19 May 2026 18:10:26 -0400 Subject: [PATCH 04/10] compiles locally --- .../hud/screen_objects/base_screen_objects.dm | 2 +- code/game/antagonist/station/psionic/paramount.dm | 6 +----- code/game/antagonist/station/thrall.dm | 6 ++++-- code/game/jobs/job/job.dm | 9 --------- code/game/objects/items/weapons/setupdisk.dm | 14 -------------- code/modules/mob/mob_grab.dm | 8 ++++---- code/modules/projectiles/projectile.dm | 2 +- .../psionics/complexus/complexus_process.dm | 2 +- code/modules/psionics/complexus/complexus_topic.dm | 4 ++-- code/modules/psionics/events/_psi.dm | 10 ++++++---- code/modules/psionics/faculties/_power.dm | 2 +- code/modules/psionics/faculties/coercion.dm | 4 ++-- code/modules/psionics/faculties/energistics.dm | 2 +- code/modules/psionics/faculties/psychokinesis.dm | 2 +- 14 files changed, 25 insertions(+), 48 deletions(-) diff --git a/code/_onclick/hud/screen_objects/base_screen_objects.dm b/code/_onclick/hud/screen_objects/base_screen_objects.dm index 9f7138d2c24..9d6f4bd3cd0 100644 --- a/code/_onclick/hud/screen_objects/base_screen_objects.dm +++ b/code/_onclick/hud/screen_objects/base_screen_objects.dm @@ -304,7 +304,7 @@ parentmob = _parentmob // BIG RISK CHANGE in PSIONICS - could potentially break fullauto like any change to clicks, as usual -/obj/screen/inventory/Click() +/atom/movable/screen/inventory/Click() // At this point in client Click() code we have passed the 1/10 sec check and little else // We don't even know if it's a middle click if(!usr.can_click()) diff --git a/code/game/antagonist/station/psionic/paramount.dm b/code/game/antagonist/station/psionic/paramount.dm index 3c7a3605478..1107714c56b 100644 --- a/code/game/antagonist/station/psionic/paramount.dm +++ b/code/game/antagonist/station/psionic/paramount.dm @@ -88,7 +88,7 @@ return TRUE -/datum/antagonist/paramount/create_objectives() +/datum/antagonist/paramount/create_objectives(survive = TRUE) if(!..()) return @@ -123,7 +123,3 @@ else i++ objective_list -= 4 - - var/datum/objective/survive/objective = new - objective.owner = owner - owner.individual_objectives += objective diff --git a/code/game/antagonist/station/thrall.dm b/code/game/antagonist/station/thrall.dm index cc4815f882e..3fb2db2a27d 100644 --- a/code/game/antagonist/station/thrall.dm +++ b/code/game/antagonist/station/thrall.dm @@ -3,12 +3,13 @@ role_text_plural = "Thralls" welcome_text = "Your mind is no longer solely your own..." id = ROLE_THRALL + bantype = ROLE_BANTYPE_THRALL antaghud_indicator = "hudmalai" restricted_jobs = list("AI", "Robot") var/list/thrall_controllers = list() -/datum/antagonist/thrall/create_objectives() +/datum/antagonist/thrall/create_objectives(survive = FALSE) var/mob/living/controller = thrall_controllers["\ref[owner]"] if(!controller) return // Someone is playing with buttons they shouldn't be. @@ -21,7 +22,8 @@ if(!new_controller) return 0 . = ..() - if(.) thrall_controllers["\ref[owner]"] = new_controller + if(.) + thrall_controllers["\ref[owner]"] = new_controller /datum/antagonist/thrall/greet() . = ..() diff --git a/code/game/jobs/job/job.dm b/code/game/jobs/job/job.dm index 25f222132f4..c578e0f8152 100644 --- a/code/game/jobs/job/job.dm +++ b/code/game/jobs/job/job.dm @@ -70,20 +70,11 @@ /// Does this job allows loadout ? var/loadout_allowed = TRUE - var/create_record = 1 // Do we announce/make records for people who spawn on this job? - var/list/also_known_languages = list() // additional chance based languages to all jobs. var/list/psi_faculties // Starting psi faculties, if any. var/psi_latency_chance = 0 // Chance of an additional psi latency, if any. // var/give_psionic_implant_on_join = TRUE // If psionic, will be implanted for control. - var/account_allowed = 1 // Does this job type come with a station account? - var/wage = WAGE_LABOUR // How much base wage does this job recieve per payday - var/initial_balance = -1 // If set to a value other than -1, overrides the wage based initial balance calculation - - var/outfit_type // The outfit the employee will be dressed in, if any - - var/loadout_allowed = TRUE // Does this job allows loadout ? var/description = "" var/duties = "" var/loyalties = "" diff --git a/code/game/objects/items/weapons/setupdisk.dm b/code/game/objects/items/weapons/setupdisk.dm index 5bdf481cc66..28492912931 100644 --- a/code/game/objects/items/weapons/setupdisk.dm +++ b/code/game/objects/items/weapons/setupdisk.dm @@ -128,20 +128,6 @@ /datum/computer_file/program/records ) -/obj/item/computer_hardware/hard_drive/portable/advanced/setup/library - name = "libraric setup disk" - desc = "A removable disk used to store large amounts of data.\nThis one contains all the files necessary to set up a library console." - default_files = list( - /datum/computer_file/program/email_client, - /datum/computer_file/program/chatclient, - /datum/computer_file/program/wordprocessor, - /datum/computer_file/program/newsbrowser, - /datum/computer_file/program/nttransfer, - /datum/computer_file/program/camera_monitor, - /datum/computer_file/program/records, - /datum/computer_file/program/library - ) - /obj/item/computer_hardware/hard_drive/portable/advanced/setup/trade name = "trade setup disk" desc = "A removable disk used to store large amounts of data.\nThis one contains all the files necessary to set up a trade console." diff --git a/code/modules/mob/mob_grab.dm b/code/modules/mob/mob_grab.dm index 7ac972e1e80..74e1858cbb5 100644 --- a/code/modules/mob/mob_grab.dm +++ b/code/modules/mob/mob_grab.dm @@ -38,17 +38,17 @@ /obj/proc/affect_grab(mob/user, mob/target, state) return FALSE -/obj/item/grab/resolve_attackby(atom/A, mob/user, var/click_params) +/obj/item/grab/resolve_attackby(obj/A, mob/user, var/click_params) if(ishuman(user) && affecting == A) var/mob/living/carbon/human/H = user if (H.check_psi_grab(src)) return TRUE if(ismob(A)) return ..() - if(get_dist(O, affecting) > 1) + if(get_dist(A, affecting) > 1) return TRUE - if(istype(O, /obj) || istype(O, /turf/wall/low)) - if(O.affect_grab(assailant, affecting, state)) + if(istype(A, /obj) || istype(A, /turf/wall/low)) + if(A.affect_grab(assailant, affecting, state)) qdel(src) return TRUE diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index b861c7ef643..998b4a80586 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -396,7 +396,7 @@ var/mob/living/carbon/human/H = target_mob if(psy.psionic_user && result && (H.sanity.level <= 0)) psy.psionic_user.reg_break(H) - else if(psy.contractor && result && (H.sanity.level <= 0) && holder) + else if(psy.contractor && result && (H.sanity.level <= 0) && psy.holder) //Previously just holder, 'missing var' psy.holder.reg_break(H) if(result == PROJECTILE_STOP) diff --git a/code/modules/psionics/complexus/complexus_process.dm b/code/modules/psionics/complexus/complexus_process.dm index 8d445bf321f..55be6595018 100644 --- a/code/modules/psionics/complexus/complexus_process.dm +++ b/code/modules/psionics/complexus/complexus_process.dm @@ -217,7 +217,7 @@ continue if(I && istype(I)) - var/list/current_wounds = I.GetComponents(/datum/component/internal_wound) + var/list/current_wounds = I.wounddatums if(LAZYLEN(current_wounds) && prob(heal_rate * 10) && spend_power(heal_rate)) SEND_SIGNAL_OLD(I, COMSIG_IORGAN_REMOVE_WOUND, pick(current_wounds)) to_chat(H, SPAN_NOTICE("Your innards itch as your autoredactive faculty mends your [I.name].")) diff --git a/code/modules/psionics/complexus/complexus_topic.dm b/code/modules/psionics/complexus/complexus_topic.dm index b72659b4380..9e5707d365c 100644 --- a/code/modules/psionics/complexus/complexus_topic.dm +++ b/code/modules/psionics/complexus/complexus_topic.dm @@ -1,4 +1,4 @@ -/datum/psi_complexus/CanUseTopic(mob/user, datum/topic_state/state = GLOB.default_state) +/datum/psi_complexus/CanUseTopic(mob/user, datum/nano_topic_state/state = GLOB.default_state) return (user.client && check_rights(R_ADMIN, FALSE, user.client)) /datum/psi_complexus/Topic(href, list/href_list) @@ -15,6 +15,6 @@ check_latency_trigger(100, "outside intervention", redactive = TRUE) . = TRUE if(.) - var/datum/admins/admin_datum = admin_datums[usr.ckey] + var/datum/admins/admin_datum = GLOB.admin_datums[usr.ckey] if(istype(admin_datum)) admin_datum.show_player_panel(owner) diff --git a/code/modules/psionics/events/_psi.dm b/code/modules/psionics/events/_psi.dm index 14fa2cef20c..23024fe1c4d 100644 --- a/code/modules/psionics/events/_psi.dm +++ b/code/modules/psionics/events/_psi.dm @@ -3,17 +3,19 @@ endWhen = 120 /datum/event/psi/announce() - priority_announcement.Announce( \ + minor_announce( \ "A localized disruption within the neighboring psionic continua has been detected. All psi-operant crewmembers \ are advised to cease any sensitive activities and report to medical personnel in case of damage.", \ - "Cuchulain Sensor Array Automated Message" \ + "Cuchulain Sensor Array Automated Message", \ + 'sound/misc/notice1.ogg' ) /datum/event/psi/end() - priority_announcement.Announce( \ + minor_announce( \ "The psi-disturbance has ended and baseline normality has been re-asserted. \ Anything you still can't cope with is therefore your own problem.", \ - "Cuchulain Sensor Array Automated Message" \ + "Cuchulain Sensor Array Automated Message", \ + 'sound/misc/notice2.ogg' ) /datum/event/psi/tick() diff --git a/code/modules/psionics/faculties/_power.dm b/code/modules/psionics/faculties/_power.dm index e366e316fee..e16dce27892 100644 --- a/code/modules/psionics/faculties/_power.dm +++ b/code/modules/psionics/faculties/_power.dm @@ -1,5 +1,5 @@ /decl/psionic_power - abstract_type = /decl/psionic_power + abstract_check = /decl/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. diff --git a/code/modules/psionics/faculties/coercion.dm b/code/modules/psionics/faculties/coercion.dm index 1f04961c60f..524e2b5df7b 100644 --- a/code/modules/psionics/faculties/coercion.dm +++ b/code/modules/psionics/faculties/coercion.dm @@ -6,7 +6,7 @@ /decl/psionic_power/coercion faculty = PSI_COERCION - abstract_type = /decl/psionic_power/coercion + abstract_check = /decl/psionic_power/coercion /decl/psionic_power/coercion/invoke(mob/living/user, mob/living/target) if (!istype(target)) @@ -175,7 +175,7 @@ if(prob(75)) target.emote("scream") - for(var/obj/item/I in list(target.get_active_hand(), target.get_inactive_hand())) + for(var/obj/item/I in list(target.get_active_held_item(), target.get_inactive_held_item())) if(istype(I, /obj/item/grab)) //did they grab someone? target.break_all_grabs(user) //See about breaking grips or pulls else diff --git a/code/modules/psionics/faculties/energistics.dm b/code/modules/psionics/faculties/energistics.dm index 2914f064dbb..6cc54510078 100644 --- a/code/modules/psionics/faculties/energistics.dm +++ b/code/modules/psionics/faculties/energistics.dm @@ -6,7 +6,7 @@ /decl/psionic_power/energistics faculty = PSI_ENERGISTICS - abstract_type = /decl/psionic_power/energistics + abstract_check = /decl/psionic_power/energistics /decl/psionic_power/energistics/disrupt name = "Disrupt" diff --git a/code/modules/psionics/faculties/psychokinesis.dm b/code/modules/psionics/faculties/psychokinesis.dm index 926624aba70..6e9ba65f959 100644 --- a/code/modules/psionics/faculties/psychokinesis.dm +++ b/code/modules/psionics/faculties/psychokinesis.dm @@ -8,7 +8,7 @@ faculty = PSI_PSYCHOKINESIS use_manifest = TRUE use_sound = null - abstract_type = /decl/psionic_power/psychokinesis + abstract_check = /decl/psionic_power/psychokinesis /decl/psionic_power/psychokinesis/psiblade name = "Psiblade" From b901555ee5b29bcdf6dedd55a79fd57b7ccbb86c Mon Sep 17 00:00:00 2001 From: Mycah142 Date: Tue, 19 May 2026 19:41:28 -0400 Subject: [PATCH 05/10] Subsystem addition --- code/__DEFINES/subsystems.dm | 1 + code/controllers/subsystems/processing/psi.dm | 1 + code/modules/psionics/events/_psi.dm | 12 ++++++++++++ 3 files changed, 14 insertions(+) diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index a053e2eb069..67e381f482a 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -127,6 +127,7 @@ #define INIT_ORDER_LANGUAGE 20 #define INIT_ORDER_JOBS 16 #define INIT_ORDER_CHAR_SETUP 14 +#define INIT_ORDER_PSYCHICS 12 #define INIT_ORDER_MACHINES 7 #define INIT_ORDER_TIMER 1 #define INIT_ORDER_DEFAULT 0 diff --git a/code/controllers/subsystems/processing/psi.dm b/code/controllers/subsystems/processing/psi.dm index 44552495192..c9adf989889 100644 --- a/code/controllers/subsystems/processing/psi.dm +++ b/code/controllers/subsystems/processing/psi.dm @@ -16,6 +16,7 @@ PROCESSING_SUBSYSTEM_DEF(psi) name = "Psychics" priority = FIRE_PRIORITY_PSYCHICS flags = SS_POST_FIRE_TIMING | SS_BACKGROUND + init_order = INIT_ORDER_PSYCHICS var/list/faculties_by_id = list() var/list/faculties_by_name = list() diff --git a/code/modules/psionics/events/_psi.dm b/code/modules/psionics/events/_psi.dm index 23024fe1c4d..001470c5ae3 100644 --- a/code/modules/psionics/events/_psi.dm +++ b/code/modules/psionics/events/_psi.dm @@ -1,3 +1,15 @@ +//Oh boy their brains ASSPLODE + +/datum/storyevent/psi + id = "psi" + name = "Psionic Disruption" + + event_type =/datum/event/psi + event_pools = list(EVENT_LEVEL_MODERATE = POOL_THRESHOLD_MODERATE) + tags = list(TAG_DESTRUCTIVE, TAG_NEGATIVE) + +///////////////////////////////////////////////////////// + /datum/event/psi startWhen = 30 endWhen = 120 From f678aae629c8316e183a1e1b3915751d29eb5376 Mon Sep 17 00:00:00 2001 From: Mycah142 Date: Tue, 19 May 2026 19:52:31 -0400 Subject: [PATCH 06/10] initialize and linters --- cev_eris.dme | 10 ++++++++++ code/controllers/subsystems/processing/psi.dm | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cev_eris.dme b/cev_eris.dme index c9f1afd544a..64d1b373f7a 100644 --- a/cev_eris.dme +++ b/cev_eris.dme @@ -2708,6 +2708,9 @@ #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" @@ -2721,6 +2724,13 @@ #include "code\modules\psionics\mob\mob_assay.dm" #include "code\modules\psionics\mob\mob_interactions.dm" #include "code\modules\psionics\null\_null.dm" +#include "code\modules\psionics\null\chemistry.dm" +#include "code\modules\psionics\null\flooring.dm" +#include "code\modules\psionics\null\material.dm" +#include "code\modules\psionics\null\material_sheet.dm" +#include "code\modules\psionics\null\material_weapon.dm" +#include "code\modules\psionics\null\turf_floor.dm" +#include "code\modules\psionics\null\turf_wall.dm" #include "code\modules\psionics\null\~null.dm" #include "code\modules\pulsar_engine\_defines.dm" #include "code\modules\pulsar_engine\pulsar_consoles.dm" diff --git a/code/controllers/subsystems/processing/psi.dm b/code/controllers/subsystems/processing/psi.dm index c9adf989889..fed449bec7b 100644 --- a/code/controllers/subsystems/processing/psi.dm +++ b/code/controllers/subsystems/processing/psi.dm @@ -30,7 +30,7 @@ PROCESSING_SUBSYSTEM_DEF(psi) /datum/controller/subsystem/processing/psi/proc/get_faculty(faculty) return faculties_by_name[faculty] || faculties_by_id[faculty] -/datum/controller/subsystem/processing/psi/Initialize(start_uptime) +/datum/controller/subsystem/processing/psi/Initialize(timeofday) var/list/faculties = decls_repository.get_decls_of_subtype(/decl/psionic_faculty) for(var/ftype in faculties) var/decl/psionic_faculty/faculty = faculties[ftype] @@ -45,3 +45,5 @@ PROCESSING_SUBSYSTEM_DEF(psi) var/decl/psionic_faculty/faculty = get_faculty(power.faculty) if(faculty) faculty.powers |= power + ..() + From de5049b46270a191348023413c75a0e0057aa9e3 Mon Sep 17 00:00:00 2001 From: Mycah142 Date: Tue, 19 May 2026 19:58:35 -0400 Subject: [PATCH 07/10] initialize and linters --- code/controllers/subsystems/processing/psi.dm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/controllers/subsystems/processing/psi.dm b/code/controllers/subsystems/processing/psi.dm index c9adf989889..fed449bec7b 100644 --- a/code/controllers/subsystems/processing/psi.dm +++ b/code/controllers/subsystems/processing/psi.dm @@ -30,7 +30,7 @@ PROCESSING_SUBSYSTEM_DEF(psi) /datum/controller/subsystem/processing/psi/proc/get_faculty(faculty) return faculties_by_name[faculty] || faculties_by_id[faculty] -/datum/controller/subsystem/processing/psi/Initialize(start_uptime) +/datum/controller/subsystem/processing/psi/Initialize(timeofday) var/list/faculties = decls_repository.get_decls_of_subtype(/decl/psionic_faculty) for(var/ftype in faculties) var/decl/psionic_faculty/faculty = faculties[ftype] @@ -45,3 +45,5 @@ PROCESSING_SUBSYSTEM_DEF(psi) var/decl/psionic_faculty/faculty = get_faculty(power.faculty) if(faculty) faculty.powers |= power + ..() + From a454f0156914d4f05d57bb1233088164a646789e Mon Sep 17 00:00:00 2001 From: Mycah142 Date: Tue, 19 May 2026 20:13:32 -0400 Subject: [PATCH 08/10] comment out contradictions --- code/modules/psionics/events/mini_spasm.dm | 126 +++++++------- code/modules/psionics/null/chemistry.dm | 164 +++++++++--------- code/modules/psionics/null/flooring.dm | 36 ++-- code/modules/psionics/null/material.dm | 58 ++++--- code/modules/psionics/null/material_sheet.dm | 40 +++-- code/modules/psionics/null/material_weapon.dm | 20 ++- code/modules/psionics/null/turf_floor.dm | 14 +- code/modules/psionics/null/turf_wall.dm | 32 ++-- 8 files changed, 254 insertions(+), 236 deletions(-) diff --git a/code/modules/psionics/events/mini_spasm.dm b/code/modules/psionics/events/mini_spasm.dm index ab3270cdaee..4a04e946217 100644 --- a/code/modules/psionics/events/mini_spasm.dm +++ b/code/modules/psionics/events/mini_spasm.dm @@ -1,67 +1,71 @@ -/datum/event/minispasm - startWhen = 60 - endWhen = 90 - var/static/list/psi_operancy_messages = list( - "There's something in your skull!", - "Something is eating your thoughts!", - "You can feel your brain being rewritten!", - "Something is crawling over your frontal lobe!", - "THE SIGNAL THE SIGNAL THE SIGNAL THE SIGNAL THE" - ) +//////////TODO: Check for future Psionic use -/datum/event/minispasm/announce() - priority_announcement.Announce( \ - "PRIORITY ALERT: SIGMA-[rand(50,80)] PSIONIC SIGNAL LOCAL TRAMISSION DETECTED (97% MATCH, NONVARIANT) \ - (SIGNAL SOURCE TRIANGULATED ADJACENT LOCAL SITE): All personnel are advised to avoid \ - exposure to active audio transmission equipment including radio headsets and intercoms \ - for the duration of the signal broadcast.", \ - "Cuchulain Sensor Array Automated Message" \ - ) +// /datum/event/minispasm +// startWhen = 60 +// endWhen = 90 +// var/static/list/psi_operancy_messages = list( +// "There's something in your skull!", +// "Something is eating your thoughts!", +// "You can feel your brain being rewritten!", +// "Something is crawling over your frontal lobe!", +// "THE SIGNAL THE SIGNAL THE SIGNAL THE SIGNAL THE" +// ) -/datum/event/minispasm/start() - var/list/victims = list() - for(var/obj/item/device/radio/radio in GLOB.listening_objects) - if(radio.on) - for(var/mob/living/victim in range(radio.canhear_range, radio.loc)) - if(isnull(victims[victim]) && victim.stat == CONSCIOUS && !victim.ear_deaf) - victims[victim] = radio - for(var/thing in victims) - var/mob/living/victim = thing - var/obj/item/device/radio/source = victims[victim] - do_spasm(victim, source) +// /datum/event/minispasm/announce() +// minor_announce( \ +// "PRIORITY ALERT: SIGMA-[rand(50,80)] PSIONIC SIGNAL LOCAL TRAMISSION DETECTED (97% MATCH, NONVARIANT) \ +// (SIGNAL SOURCE TRIANGULATED ADJACENT LOCAL SITE): All personnel are advised to avoid \ +// exposure to active audio transmission equipment including radio headsets and intercoms \ +// for the duration of the signal broadcast.", \ +// "Cuchulain Sensor Array Automated Message", \ +// 'sound/misc/notice1.ogg' +// ) -/datum/event/minispasm/proc/do_spasm(mob/living/victim, obj/item/device/radio/source) - set waitfor = 0 +// /datum/event/minispasm/start() +// var/list/victims = list() +// for(var/obj/item/device/radio/radio in GLOB.listening_objects) +// if(radio.on) +// for(var/mob/living/victim in range(radio.canhear_range, radio.loc)) +// if(isnull(victims[victim]) && victim.stat == CONSCIOUS && !victim.ear_deaf) +// victims[victim] = radio +// for(var/thing in victims) +// var/mob/living/victim = thing +// var/obj/item/device/radio/source = victims[victim] +// do_spasm(victim, source) - if(iscarbon(victim) && !victim.isSynthetic()) - var/list/disabilities = list(NEARSIGHTED, EPILEPSY, NERVOUS) - for(var/disability in disabilities) - if(victim.disabilities & disability) - disabilities -= disability - if(length(disabilities)) - victim.disabilities |= pick(disabilities) +// /datum/event/minispasm/proc/do_spasm(mob/living/victim, obj/item/device/radio/source) +// set waitfor = 0 - if(victim.psi) - to_chat(victim, SPAN_DANGER("A hauntingly familiar sound hisses from [icon2html(source, victim)] \the [source], and your vision flickers!")) - victim.psi.backblast(rand(5,15)) - victim.Paralyse(5) - victim.make_jittery(100) - 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(FONT_LARGE(pick(psi_operancy_messages)))) - victim.adjustBrainLoss(rand(10,20)) - victim.set_psi_rank(pick_n_take(faculties), 1) - sleep(30) - victim.psi.update() - sleep(45) - victim.psi.check_latency_trigger(100, "a psionic scream", redactive = TRUE) +// if(iscarbon(victim) && !victim.isSynthetic()) +// var/list/disabilities = list(NEARSIGHTED, EPILEPSY, NERVOUS) +// for(var/disability in disabilities) +// if(victim.disabilities & disability) +// disabilities -= disability +// if(length(disabilities)) +// victim.disabilities |= pick(disabilities) -/datum/event/minispasm/end() - priority_announcement.Announce( \ - "PRIORITY ALERT: SIGNAL BROADCAST HAS CEASED. Personnel are cleared to resume use of non-hardened radio transmission equipment. Have a nice day.", \ - "Cuchulain Sensor Array Automated Message" \ - ) +// if(victim.psi) +// to_chat(victim, SPAN_DANGER("A hauntingly familiar sound hisses from [icon2html(source, victim)] \the [source], and your vision flickers!")) +// victim.psi.backblast(rand(5,15)) +// victim.Paralyse(5) +// victim.make_jittery(100) +// 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(FONT_LARGE(pick(psi_operancy_messages)))) +// victim.adjustBrainLoss(rand(10,20)) +// victim.set_psi_rank(pick_n_take(faculties), 1) +// sleep(30) +// victim.psi.update() +// sleep(45) +// victim.psi.check_latency_trigger(100, "a psionic scream", redactive = TRUE) + +// /datum/event/minispasm/end() +// minor_announce( \ +// "PRIORITY ALERT: SIGNAL BROADCAST HAS CEASED. Personnel are cleared to resume use of non-hardened radio transmission equipment. Have a nice day.", \ +// "Cuchulain Sensor Array Automated Message", +// 'sound/misc/notice2.ogg' \ +// ) diff --git a/code/modules/psionics/null/chemistry.dm b/code/modules/psionics/null/chemistry.dm index ac68eb778a5..b9b77a744dd 100644 --- a/code/modules/psionics/null/chemistry.dm +++ b/code/modules/psionics/null/chemistry.dm @@ -1,88 +1,90 @@ -/datum/chemical_reaction/nullglass - name = "Soulstone" - result = null - required_reagents = list(/datum/reagent/blood = 15, /datum/reagent/crystal = 1) - result_amount = 1 +//////////TODO: Check for future Psionic use -/datum/chemical_reaction/nullglass/get_reaction_flags(datum/reagents/holder) - for(var/datum/reagent/blood/blood in holder.reagent_list) - var/weakref/donor_ref = islist(blood.data) && blood.data["donor"] - if(istype(donor_ref)) - var/mob/living/donor = donor_ref.resolve() - if(istype(donor) && (donor.psi || (donor.mind && GLOB.wizards.is_antagonist(donor.mind)))) - return TRUE +// /datum/chemical_reaction/nullglass +// name = "Soulstone" +// result = null +// required_reagents = list(/datum/reagent/blood = 15, /datum/reagent/crystal = 1) +// result_amount = 1 -/datum/chemical_reaction/nullglass/on_reaction(datum/reagents/holder, created_volume, reaction_flags) - var/location = get_turf(holder.my_atom) - if(reaction_flags) - for(var/i = 1, i <= created_volume, i++) - new /obj/item/device/soulstone(location) - else - for(var/i = 1, i <= created_volume*2, i++) - new /obj/item/material/shard(location, MATERIAL_CRYSTAL) +// /datum/chemical_reaction/nullglass/get_reaction_flags(datum/reagents/holder) +// for(var/datum/reagent/blood/blood in holder.reagent_list) +// var/weakref/donor_ref = islist(blood.data) && blood.data["donor"] +// if(istype(donor_ref)) +// var/mob/living/donor = donor_ref.resolve() +// if(istype(donor) && (donor.psi || (donor.mind && GLOB.wizards.is_antagonist(donor.mind)))) +// return TRUE -/datum/reagent/crystal - name = "crystallizing agent" - taste_description = "sharpness" - reagent_state = LIQUID - color = "#13bc5e" +// /datum/chemical_reaction/nullglass/on_reaction(datum/reagents/holder, created_volume, reaction_flags) +// var/location = get_turf(holder.my_atom) +// if(reaction_flags) +// for(var/i = 1, i <= created_volume, i++) +// new /obj/item/device/soulstone(location) +// else +// for(var/i = 1, i <= created_volume*2, i++) +// new /obj/item/material/shard(location, MATERIAL_CRYSTAL) -/datum/reagent/crystal/affect_blood(mob/living/carbon/M, removed) - var/result_mat = (M.psi || (M.mind && GLOB.wizards.is_antagonist(M.mind))) ? MATERIAL_NULLGLASS : MATERIAL_CRYSTAL - if(ishuman(M)) - var/mob/living/carbon/human/H = M - for(var/obj/item/organ/external/E in shuffle(H.organs.Copy())) - if(E.is_stump()) - continue +// /datum/reagent/crystal +// name = "crystallizing agent" +// taste_description = "sharpness" +// reagent_state = LIQUID +// color = "#13bc5e" - if(BP_IS_CRYSTAL(E)) - if((E.brute_dam + E.burn_dam) > 0) - if(prob(35)) - to_chat(M, SPAN_NOTICE("You feel a crawling sensation as fresh crystal grows over your [E.name].")) - E.heal_damage(rand(5,8), rand(5,8)) - break - if(BP_IS_BRITTLE(E)) - E.status &= ~ORGAN_BRITTLE - break - else if(prob(15)) - to_chat(H, SPAN_DANGER("Your [E.name] is being lacerated from within!")) - if(E.can_feel_pain()) - H.emote("scream") - if(prob(25)) - for(var/i = 1 to rand(1,3)) - new /obj/item/material/shard(get_turf(E), result_mat) - E.take_external_damage(rand(50,70), 0) - else - E.take_external_damage(rand(20,30), 0) - E.status |= ORGAN_CRYSTAL - E.status |= ORGAN_BRITTLE - break +// /datum/reagent/crystal/affect_blood(mob/living/carbon/M, removed) +// var/result_mat = (M.psi || (M.mind && GLOB.wizards.is_antagonist(M.mind))) ? MATERIAL_NULLGLASS : MATERIAL_CRYSTAL +// if(ishuman(M)) +// var/mob/living/carbon/human/H = M +// for(var/obj/item/organ/external/E in shuffle(H.organs.Copy())) +// if(E.is_stump()) +// continue - for(var/obj/item/organ/internal/I in shuffle(H.internal_organs.Copy())) - if(I.organ_tag == BP_BRAIN) - continue +// if(BP_IS_CRYSTAL(E)) +// if((E.brute_dam + E.burn_dam) > 0) +// if(prob(35)) +// to_chat(M, SPAN_NOTICE("You feel a crawling sensation as fresh crystal grows over your [E.name].")) +// E.heal_damage(rand(5,8), rand(5,8)) +// break +// if(BP_IS_BRITTLE(E)) +// E.status &= ~ORGAN_BRITTLE +// break +// else if(prob(15)) +// to_chat(H, SPAN_DANGER("Your [E.name] is being lacerated from within!")) +// if(E.can_feel_pain()) +// H.emote("scream") +// if(prob(25)) +// for(var/i = 1 to rand(1,3)) +// new /obj/item/material/shard(get_turf(E), result_mat) +// E.take_external_damage(rand(50,70), 0) +// else +// E.take_external_damage(rand(20,30), 0) +// E.status |= ORGAN_CRYSTAL +// E.status |= ORGAN_BRITTLE +// break - if(BP_IS_CRYSTAL(I)) - if(prob(35)) - to_chat(M, SPAN_NOTICE("You feel a deep, sharp tugging sensation as your [I.name] is mended.")) - I.heal_damage(rand(1,3)) - break - if(BP_IS_BRITTLE(I)) - I.status &= ~ORGAN_BRITTLE - break - else if(prob(15)) - to_chat(H, SPAN_DANGER("You feel visceral, sharp twisting within your body!")) - if(I.can_feel_pain()) - H.emote("scream") - if(prob(25)) - I.take_internal_damage(rand(10,15), 0) - else - I.take_internal_damage(rand(5,10), 0) - I.status |= ORGAN_CRYSTAL - I.status |= ORGAN_BRITTLE - break - else - to_chat(M, SPAN_DANGER("Your flesh is being lacerated from within!")) - M.adjustBruteLoss(rand(3,6)) - if(prob(10)) - new /obj/item/material/shard(get_turf(M), result_mat) +// for(var/obj/item/organ/internal/I in shuffle(H.internal_organs.Copy())) +// if(I.organ_tag == BP_BRAIN) +// continue + +// if(BP_IS_CRYSTAL(I)) +// if(prob(35)) +// to_chat(M, SPAN_NOTICE("You feel a deep, sharp tugging sensation as your [I.name] is mended.")) +// I.heal_damage(rand(1,3)) +// break +// if(BP_IS_BRITTLE(I)) +// I.status &= ~ORGAN_BRITTLE +// break +// else if(prob(15)) +// to_chat(H, SPAN_DANGER("You feel visceral, sharp twisting within your body!")) +// if(I.can_feel_pain()) +// H.emote("scream") +// if(prob(25)) +// I.take_internal_damage(rand(10,15), 0) +// else +// I.take_internal_damage(rand(5,10), 0) +// I.status |= ORGAN_CRYSTAL +// I.status |= ORGAN_BRITTLE +// break +// else +// to_chat(M, SPAN_DANGER("Your flesh is being lacerated from within!")) +// M.adjustBruteLoss(rand(3,6)) +// if(prob(10)) +// new /obj/item/material/shard(get_turf(M), result_mat) diff --git a/code/modules/psionics/null/flooring.dm b/code/modules/psionics/null/flooring.dm index 4e9e3c84f87..374adf98177 100644 --- a/code/modules/psionics/null/flooring.dm +++ b/code/modules/psionics/null/flooring.dm @@ -1,20 +1,22 @@ -/decl/flooring - var/psi_null +//////////TODO: Check for future Psionic use -/decl/flooring/proc/is_psi_null() - return psi_null +// /decl/flooring +// var/psi_null -/decl/flooring/tiling/nullglass - name = "nullglass plating" - desc = "You can hear the tiles whispering..." - icon_base = "nullglass" - color = COLOR_NULLGLASS - has_damage_range = null - flags = TURF_REMOVE_SCREWDRIVER - build_type = /obj/item/stack/tile/floor_nullglass - psi_null = TRUE +// /decl/flooring/proc/is_psi_null() +// return psi_null -/obj/item/stack/tile/floor_nullglass - name = "nullglass floor tile" - icon_state = "tile_nullglass" - matter = list(MATERIAL_NULLGLASS = 937.5) +// /decl/flooring/tiling/nullglass +// name = "nullglass plating" +// desc = "You can hear the tiles whispering..." +// icon_base = "nullglass" +// color = COLOR_NULLGLASS +// has_damage_range = null +// flags = TURF_REMOVE_SCREWDRIVER +// build_type = /obj/item/stack/tile/floor_nullglass +// psi_null = TRUE + +// /obj/item/stack/tile/floor_nullglass +// name = "nullglass floor tile" +// icon_state = "tile_nullglass" +// matter = list(MATERIAL_NULLGLASS = 937.5) diff --git a/code/modules/psionics/null/material.dm b/code/modules/psionics/null/material.dm index 983fc94ee92..384e5acbbab 100644 --- a/code/modules/psionics/null/material.dm +++ b/code/modules/psionics/null/material.dm @@ -1,32 +1,34 @@ -/material - var/is_psionic_nullifier +//////////TODO: Check for future Psionic use -/material/proc/is_psi_null() - return is_psionic_nullifier +// /material +// var/is_psionic_nullifier -/material/nullglass - is_psionic_nullifier = TRUE +// /material/proc/is_psi_null() +// return is_psionic_nullifier -/material/nullglass - name = MATERIAL_NULLGLASS - icon_colour = COLOR_NULLGLASS - conductive = 1 - stack_type = /obj/item/stack/material/nullglass - flags = MATERIAL_BRITTLE - opacity = 0.5 - integrity = 30 - shard_type = SHARD_SHARD - tableslam_noise = 'sound/effects/Glasshit.ogg' - hardness = 80 - weight = 25 - sheet_icon_base = "diamond" - sheet_singular_name = "gem" - sheet_plural_name = "gems" - door_icon_base = "stone" - destruction_desc = "shatters" - hitsound = 'sound/effects/Glasshit.ogg' - hidden_from_codex = TRUE +// /material/nullglass +// is_psionic_nullifier = TRUE -/material/nullglass/generate_recipes() - . = ..() - . += new /datum/stack_recipe/tile/nullglass(src) +// /material/nullglass +// name = MATERIAL_NULLGLASS +// icon_colour = COLOR_NULLGLASS +// conductive = 1 +// stack_type = /obj/item/stack/material/nullglass +// flags = MATERIAL_BRITTLE +// opacity = 0.5 +// integrity = 30 +// shard_type = SHARD_SHARD +// tableslam_noise = 'sound/effects/Glasshit.ogg' +// hardness = 80 +// weight = 25 +// sheet_icon_base = "diamond" +// sheet_singular_name = "gem" +// sheet_plural_name = "gems" +// door_icon_base = "stone" +// destruction_desc = "shatters" +// hitsound = 'sound/effects/Glasshit.ogg' +// hidden_from_codex = TRUE + +// /material/nullglass/generate_recipes() +// . = ..() +// . += new /datum/stack_recipe/tile/nullglass(src) diff --git a/code/modules/psionics/null/material_sheet.dm b/code/modules/psionics/null/material_sheet.dm index 455cbb258e2..1357c6cd259 100644 --- a/code/modules/psionics/null/material_sheet.dm +++ b/code/modules/psionics/null/material_sheet.dm @@ -1,22 +1,24 @@ -/obj/item/stack/material/withstand_psi_stress(stress, atom/source) - . = ..(stress, source) - if(amount > 0 && . > 0 && disrupts_psionics()) - if(. > amount) - use(amount) - . -= amount - else - use(stress) - . = 0 +//////////TODO: Check for future Psionic use -/obj/item/stack/material/disrupts_psionics() - return (material && material.is_psi_null()) ? src : FALSE +// /obj/item/stack/material/withstand_psi_stress(stress, atom/source) +// . = ..(stress, source) +// if(amount > 0 && . > 0 && disrupts_psionics()) +// if(. > amount) +// use(amount) +// . -= amount +// else +// use(stress) +// . = 0 -/obj/item/stack/material/nullglass - name = "nullglass" - icon_state = "diamond" - plural_icon_state = "diamond-mult" - max_icon_state = "diamond-max" - default_type = MATERIAL_NULLGLASS +// /obj/item/stack/material/disrupts_psionics() +// return (material && material.is_psi_null()) ? src : FALSE -/obj/item/stack/material/nullglass/fifty - amount = 50 +// /obj/item/stack/material/nullglass +// name = "nullglass" +// icon_state = "diamond" +// plural_icon_state = "diamond-mult" +// max_icon_state = "diamond-max" +// default_type = MATERIAL_NULLGLASS + +// /obj/item/stack/material/nullglass/fifty +// amount = 50 diff --git a/code/modules/psionics/null/material_weapon.dm b/code/modules/psionics/null/material_weapon.dm index 24ef29c6c87..e2cd0c7a96f 100644 --- a/code/modules/psionics/null/material_weapon.dm +++ b/code/modules/psionics/null/material_weapon.dm @@ -1,11 +1,13 @@ -/obj/item/material/disrupts_psionics() - return (material && material.is_psi_null()) ? src : FALSE +//////////TODO: Check for future Psionic use -/obj/item/material/withstand_psi_stress(stress, atom/source) - . = ..(stress, source) - if(!health_dead() && . > 0 && disrupts_psionics()) - damage_health(.) - . = max(0, -(get_current_health())) +// /obj/item/material/disrupts_psionics() +// return (material && material.is_psi_null()) ? src : FALSE -/obj/item/material/shard/nullglass/New(newloc) - ..(newloc, MATERIAL_NULLGLASS) +// /obj/item/material/withstand_psi_stress(stress, atom/source) +// . = ..(stress, source) +// if(!health_dead() && . > 0 && disrupts_psionics()) +// damage_health(.) +// . = max(0, -(get_current_health())) + +// /obj/item/material/shard/nullglass/New(newloc) +// ..(newloc, MATERIAL_NULLGLASS) diff --git a/code/modules/psionics/null/turf_floor.dm b/code/modules/psionics/null/turf_floor.dm index b60aada63de..da95dc53025 100644 --- a/code/modules/psionics/null/turf_floor.dm +++ b/code/modules/psionics/null/turf_floor.dm @@ -1,7 +1,9 @@ -/turf/simulated/floor/disrupts_psionics() - return (flooring && flooring.is_psi_null()) ? src : ..() +//////////TODO: Check for future Psionic use -/turf/simulated/floor/tiled/nullglass - name = "nullglass floor" - icon_state = "nullglass" - initial_flooring = /decl/flooring/tiling/nullglass +// /turf/simulated/floor/disrupts_psionics() +// return (flooring && flooring.is_psi_null()) ? src : ..() + +// /turf/simulated/floor/tiled/nullglass +// name = "nullglass floor" +// icon_state = "nullglass" +// initial_flooring = /decl/flooring/tiling/nullglass diff --git a/code/modules/psionics/null/turf_wall.dm b/code/modules/psionics/null/turf_wall.dm index c047beb0b03..db47cc340f0 100644 --- a/code/modules/psionics/null/turf_wall.dm +++ b/code/modules/psionics/null/turf_wall.dm @@ -1,18 +1,20 @@ -/turf/simulated/wall/disrupts_psionics() - return ((material && material.is_psi_null()) || (reinf_material && reinf_material.is_psi_null())) ? src : ..() +//////////TODO: Check for future Psionic use -/turf/simulated/wall/withstand_psi_stress(stress, atom/source) - . = ..(stress, source) - if(. > 0 && disrupts_psionics()) - var/cap = material.integrity - if(reinf_material) cap += reinf_material.integrity - var/stress_total = get_damage_value() + . - damage_health(.) - . = max(0, -(cap-stress_total)) +// /turf/simulated/wall/disrupts_psionics() +// return ((material && material.is_psi_null()) || (reinf_material && reinf_material.is_psi_null())) ? src : ..() -/turf/simulated/wall/nullglass - color = "#ff6088" +// /turf/simulated/wall/withstand_psi_stress(stress, atom/source) +// . = ..(stress, source) +// if(. > 0 && disrupts_psionics()) +// var/cap = material.integrity +// if(reinf_material) cap += reinf_material.integrity +// var/stress_total = get_damage_value() + . +// damage_health(.) +// . = max(0, -(cap-stress_total)) -/turf/simulated/wall/nullglass/Initialize(ml) - color = null - ..(ml, MATERIAL_NULLGLASS) +// /turf/simulated/wall/nullglass +// color = "#ff6088" + +// /turf/simulated/wall/nullglass/Initialize(ml) +// color = null +// ..(ml, MATERIAL_NULLGLASS) From dc264fdc640fc68fd0336995a79af17fb9af22d9 Mon Sep 17 00:00:00 2001 From: Mycah142 Date: Tue, 19 May 2026 20:21:09 -0400 Subject: [PATCH 09/10] linter pls --- cev_eris.dme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cev_eris.dme b/cev_eris.dme index 64d1b373f7a..e3e5c6e1579 100644 --- a/cev_eris.dme +++ b/cev_eris.dme @@ -370,8 +370,8 @@ #include "code\controllers\subsystems\processing\nano.dm" #include "code\controllers\subsystems\processing\objs.dm" #include "code\controllers\subsystems\processing\processing.dm" -#include "code\controllers\subsystems\processing\reagents.dm" #include "code\controllers\subsystems\processing\psi.dm" +#include "code\controllers\subsystems\processing\reagents.dm" #include "code\controllers\subsystems\processing\turf.dm" #include "code\controllers\subsystems\processing\organs\internal_wounds.dm" #include "code\controllers\subsystems\tickets\mentor_tickets.dm" From 29baa3bc760cd720f5550937bd168b6001dfe9d8 Mon Sep 17 00:00:00 2001 From: Mycah142 Date: Tue, 19 May 2026 20:31:12 -0400 Subject: [PATCH 10/10] roleset event for antag --- code/game/gamemodes/roleset/simple.dm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/code/game/gamemodes/roleset/simple.dm b/code/game/gamemodes/roleset/simple.dm index aa7cfdad40b..0f0910f1a5d 100644 --- a/code/game/gamemodes/roleset/simple.dm +++ b/code/game/gamemodes/roleset/simple.dm @@ -131,3 +131,11 @@ base_quantity = 2 scaling_threshold = 15 + +/datum/storyevent/roleset/psionic + id = "paramount" + name = "paramount" + role_id = ROLE_PARAMOUNT + + base_quantity = 2 + scaling_threshold = 10