diff --git a/_maps/map_files/AsteroidStation/AsteroidStation.dmm b/_maps/map_files/AsteroidStation/AsteroidStation.dmm index fddacfce19ad7..b84ce09af048f 100644 --- a/_maps/map_files/AsteroidStation/AsteroidStation.dmm +++ b/_maps/map_files/AsteroidStation/AsteroidStation.dmm @@ -18891,6 +18891,7 @@ /obj/structure/disposalpipe/segment{ dir = 9 }, +/obj/machinery/psi_monitor, /turf/open/floor/plasteel/cafeteria, /area/crew_quarters/heads/cmo) "fbD" = ( @@ -41824,7 +41825,6 @@ /turf/open/floor/eighties, /area/maintenance/starboard/fore) "mmq" = ( -/obj/structure/bookcase/random/reference, /turf/open/floor/wood, /area/medical/psych) "mmr" = ( @@ -81236,7 +81236,7 @@ dir = 4; pixel_x = -24 }, -/obj/structure/bookcase/random/reference, +/obj/machinery/psionic_awakener, /turf/open/floor/wood, /area/medical/psych) "yfr" = ( diff --git a/_maps/map_files/DonutStation/DonutStation.dmm b/_maps/map_files/DonutStation/DonutStation.dmm index 4e5d06f810856..7f6fdad75fef1 100644 --- a/_maps/map_files/DonutStation/DonutStation.dmm +++ b/_maps/map_files/DonutStation/DonutStation.dmm @@ -955,7 +955,7 @@ /turf/open/floor/plasteel, /area/space/nearstation) "art" = ( -/obj/structure/bookcase/random/nonfiction, +/obj/machinery/psionic_awakener, /turf/open/floor/wood, /area/medical/psych) "arA" = ( @@ -11776,6 +11776,7 @@ name = "Chief Medical Officer RC"; pixel_x = -32 }, +/obj/machinery/psi_monitor, /turf/open/floor/plasteel/cafeteria, /area/crew_quarters/heads/cmo) "eLb" = ( diff --git a/_maps/map_files/GaxStation/GaxStation.dmm b/_maps/map_files/GaxStation/GaxStation.dmm index a1b6c990ee7d6..e8daacb103721 100644 --- a/_maps/map_files/GaxStation/GaxStation.dmm +++ b/_maps/map_files/GaxStation/GaxStation.dmm @@ -7336,6 +7336,7 @@ /obj/machinery/light_switch{ pixel_y = 24 }, +/obj/machinery/psi_monitor, /turf/open/floor/plasteel, /area/security/checkpoint/medical) "dxA" = ( @@ -21992,6 +21993,7 @@ pixel_x = 26; pixel_y = -4 }, +/obj/structure/closet/crate/freezer/blood, /turf/open/floor/plasteel/white, /area/medical/surgery) "kwm" = ( @@ -37507,10 +37509,10 @@ /turf/open/floor/plating, /area/maintenance/starboard/fore) "sep" = ( -/obj/structure/closet/crate/freezer/blood, /obj/effect/turf_decal/trimline/blue/filled/line/lower{ dir = 6 }, +/obj/machinery/psionic_awakener, /turf/open/floor/plasteel/white, /area/medical/sleeper) "seF" = ( diff --git a/_maps/map_files/IceMeta/IceMeta.dmm b/_maps/map_files/IceMeta/IceMeta.dmm index 33122bc0dea2a..037a7d52c2ba2 100644 --- a/_maps/map_files/IceMeta/IceMeta.dmm +++ b/_maps/map_files/IceMeta/IceMeta.dmm @@ -2165,6 +2165,7 @@ /obj/structure/disposalpipe/segment{ dir = 10 }, +/obj/machinery/psionic_awakener, /turf/open/floor/wood, /area/medical/psych) "aGg" = ( @@ -67267,6 +67268,10 @@ }, /turf/open/floor/plasteel, /area/engine/atmos/hfr) +"toR" = ( +/obj/machinery/psi_monitor, +/turf/open/floor/wood, +/area/crew_quarters/heads/cmo) "toS" = ( /obj/machinery/atmospherics/pipe/simple/general/visible{ dir = 10 @@ -240625,7 +240630,7 @@ mgB qdA nTH wgD -dCO +toR mCm oqY vTt diff --git a/_maps/map_files/YogStation/YogStation.dmm b/_maps/map_files/YogStation/YogStation.dmm index a317383404694..15c70062eed97 100644 --- a/_maps/map_files/YogStation/YogStation.dmm +++ b/_maps/map_files/YogStation/YogStation.dmm @@ -51135,7 +51135,7 @@ /turf/open/floor/plating, /area/maintenance/port) "pxM" = ( -/obj/structure/bookcase/random/religion, +/obj/machinery/psionic_awakener, /turf/open/floor/wood, /area/medical/psych) "pxQ" = ( @@ -67418,7 +67418,6 @@ /turf/open/floor/plasteel, /area/security/prison) "vtO" = ( -/obj/structure/bookcase/random/nonfiction, /turf/open/floor/wood, /area/medical/psych) "vtR" = ( @@ -69949,6 +69948,13 @@ }, /turf/open/floor/plasteel, /area/security/prison) +"wlB" = ( +/obj/effect/turf_decal/tile/blue/opposingcorners{ + dir = 1 + }, +/obj/machinery/psi_monitor, +/turf/open/floor/plasteel/cafeteria, +/area/crew_quarters/heads/cmo) "wlK" = ( /obj/machinery/camera/motion{ c_tag = "Armory External"; @@ -114338,7 +114344,7 @@ jXL wgu pbD elr -vcC +wlB dmN lcC guV diff --git a/code/__DEFINES/atom_hud.dm b/code/__DEFINES/atom_hud.dm index 46d0e60bfd387..816bd1d1d908c 100644 --- a/code/__DEFINES/atom_hud.dm +++ b/code/__DEFINES/atom_hud.dm @@ -45,7 +45,9 @@ /// Displays launchpads' targeting reticle #define DIAG_LAUNCHPAD_HUD "23" //for antag huds. these are used at the /mob level -#define ANTAG_HUD "24" +#define ANTAG_HUD "23" +/// psi control implant +#define IMPPSI_HUD "24" //by default everything in the hud_list of an atom is an image //a value in hud_list with one of these will change that behavior diff --git a/code/__DEFINES/modular_computer.dm b/code/__DEFINES/modular_computer.dm index 60ccaac8473c9..613a2a26fa9f3 100644 --- a/code/__DEFINES/modular_computer.dm +++ b/code/__DEFINES/modular_computer.dm @@ -29,6 +29,7 @@ #define PROGRAM_CATEGORY_ENGINEERING "Engineering" #define PROGRAM_CATEGORY_SUPPLY "Supply" #define PROGRAM_CATEGORY_SCIENCE "Science" +#define PROGRAM_CATEGORY_CREW "Crew" ///The default amount a program should take in cell use. #define PROGRAM_BASIC_CELL_USE 15 diff --git a/code/__DEFINES/psi.dm b/code/__DEFINES/psi.dm new file mode 100644 index 0000000000000..1b07aa257e694 --- /dev/null +++ b/code/__DEFINES/psi.dm @@ -0,0 +1,20 @@ +#define PSI_COERCION "coercion" +#define PSI_PSYCHOKINESIS "psychokinesis" +#define PSI_REDACTION "redaction" +#define PSI_ENERGISTICS "energistics" + +#define PSI_RANK_BLUNT 0 +#define PSI_RANK_LATENT 1 +#define PSI_RANK_OPERANT 2 +#define PSI_RANK_MASTER 3 +#define PSI_RANK_GRANDMASTER 4 +#define PSI_RANK_PARAMOUNT 5 + +#define PSI_IMPLANT_AUTOMATIC "Security Level Derived" +#define PSI_IMPLANT_SHOCK "Issue Neural Shock" +#define PSI_IMPLANT_WARN "Issue Reprimand" +#define PSI_IMPLANT_LOG "Log Incident" +#define PSI_IMPLANT_DISABLED "Disabled" + +#define COMSIG_PSI_SELECTION "select action" +#define COMSIG_PSI_INVOKE "invoke selected" diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 17917291db59e..7e0d3b4ec2811 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -799,6 +799,10 @@ /// determines whether or not objects are haunted and teleport/attack randomly #define TRAIT_HAUNTED "haunted" +//quirk traits +#define TRAIT_PSIONICALLY_DEAFENED "Psionically Deafened" +/// Can never have psionics, but is pretty much immune to direct tampering by them +#define TRAIT_PSIONICALLY_IMMUNE "Psionically Immune" /// This mob always lands on their feet when they fall, for better or for worse. #define TRAIT_CATLIKE_GRACE "catlike_grace" diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index 0f654affe820d..4e24f06f025e4 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -284,6 +284,8 @@ var/obj/item/I = hud.mymob.get_active_held_item() if(I) I.Click(location, control, params) + else + hud.mymob.attack_empty_hand(hud.mymob.active_hand_index) else hud.mymob.swap_hand(held_index) return 1 diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm index 861b63bdd61a0..ad20ae6bae355 100644 --- a/code/_onclick/other_mobs.dm +++ b/code/_onclick/other_mobs.dm @@ -5,6 +5,9 @@ Otherwise pretty standard. */ /mob/living/carbon/human/UnarmedAttack(atom/A, proximity, modifiers) + if(psi) + if(SEND_SIGNAL(src, COMSIG_PSI_INVOKE, A, proximity, modifiers)) + return if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED)) if(src == A) check_self_for_injuries() @@ -47,6 +50,14 @@ A.attack_hand(src, modifiers) +/mob/living/carbon/human/attack_empty_hand() + if(psi) + SEND_SIGNAL(src, COMSIG_PSI_SELECTION) + +/mob/living/carbon/human/RangedAttack(atom/A, params) + if(psi) + SEND_SIGNAL(src, COMSIG_PSI_INVOKE, A, FALSE, params) + //Return TRUE to cancel other attack hand effects that respect it. /atom/proc/attack_hand(mob/user, modifiers) . = FALSE @@ -57,6 +68,9 @@ if(interaction_flags_atom & INTERACT_ATOM_ATTACK_HAND) . = _try_interact(user, modifiers) +/mob/proc/attack_empty_hand(hand) + return + /// When the user uses their hand on an item while holding right-click /// Returns a SECONDARY_ATTACK_* value. /atom/proc/attack_hand_secondary(mob/user, modifiers) diff --git a/code/controllers/subsystem/backrooms.dm b/code/controllers/subsystem/backrooms.dm index 77b33bc135199..e2dcadfe0fcba 100644 --- a/code/controllers/subsystem/backrooms.dm +++ b/code/controllers/subsystem/backrooms.dm @@ -8,12 +8,12 @@ SUBSYSTEM_DEF(backrooms) //associative list of objects and how much they sell for var/list/golden_loot = list( - /obj/item/statuebust = 1000, - /obj/item/reagent_containers/food/snacks/urinalcake = 1000, + /obj/item/statuebust = 2000, + /obj/item/reagent_containers/food/snacks/urinalcake = 2000, /obj/item/bigspoon = 4000, /obj/item/reagent_containers/food/snacks/burger/rat = 1200, /obj/item/extinguisher = 2500, - /obj/item/toy/plush/lizard/azeel = 5000 + /obj/item/toy/plush/lizard/azeel = 10000 ) /datum/controller/subsystem/backrooms/Initialize(timeofday) diff --git a/code/controllers/subsystem/processing/psi.dm b/code/controllers/subsystem/processing/psi.dm new file mode 100644 index 0000000000000..eab50c4bca8cc --- /dev/null +++ b/code/controllers/subsystem/processing/psi.dm @@ -0,0 +1,38 @@ +GLOBAL_LIST_INIT(psychic_ranks_to_strings, list("Latent", "Operant", "Masterclass", "Grandmasterclass", "Paramount")) + +PROCESSING_SUBSYSTEM_DEF(psi) + name = "Psychics" + // priority = SS_PRIORITY_PSYCHICS + flags = SS_POST_FIRE_TIMING | SS_BACKGROUND + + var/list/faculties_by_id = list() + var/list/faculties_by_name = list() + var/list/all_aura_images = list() + var/list/all_psi_complexes = list() + var/list/psi_dampeners = list() + var/list/psi_monitors = list() + var/list/armour_faculty_by_type = list() + var/list/faculties_by_intent = list() + +/datum/controller/subsystem/processing/psi/New() + NEW_SS_GLOBAL(SSpsi) + +/datum/controller/subsystem/processing/psi/proc/get_faculty(faculty) + return faculties_by_name[faculty] || faculties_by_id[faculty] + +/datum/controller/subsystem/processing/psi/Initialize() + . = ..() + + var/list/faculties = subtypesof(/datum/psionic_faculty) + for(var/ftype in faculties) + var/datum/psionic_faculty/faculty = new ftype + faculties_by_id[faculty.id] = faculty + faculties_by_name[faculty.name] = faculty + faculties_by_intent[faculty.associated_intent] = faculty.id + + var/list/powers = subtypesof(/datum/psionic_power) + for(var/ptype in powers) + var/datum/psionic_power/power = new ptype + if(power.faculty) + var/datum/psionic_faculty/faculty = get_faculty(power.faculty) + faculty?.powers |= power diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm index dd301aeb2a0b3..3c70584d3aede 100644 --- a/code/controllers/subsystem/processing/quirks.dm +++ b/code/controllers/subsystem/processing/quirks.dm @@ -28,7 +28,7 @@ PROCESSING_SUBSYSTEM_DEF(quirks) list("Cybernetic Organ (Lungs)", "Body Purist"), list("Cybernetic Organ (Heart)", "Body Purist"), list("Cybernetic Organ (Liver)", "Body Purist"), - list("Upgraded Cybernetic Organ", "Body Purist") + list("Upgraded Cybernetic Organ", "Body Purist"), ) /datum/controller/subsystem/processing/quirks/Initialize(timeofday) diff --git a/code/datums/mapgen/dungeon_generators/maintenance_generator/maintenance_room_themes/random.dm b/code/datums/mapgen/dungeon_generators/maintenance_generator/maintenance_room_themes/random.dm index 47a27c2ae52c1..fcea6caa7454b 100644 --- a/code/datums/mapgen/dungeon_generators/maintenance_generator/maintenance_room_themes/random.dm +++ b/code/datums/mapgen/dungeon_generators/maintenance_generator/maintenance_room_themes/random.dm @@ -14,7 +14,7 @@ list(/obj/structure/table, /obj/item/reagent_containers/glass/bottle/nutrient/ez, /obj/item/reagent_containers/glass/bottle/nutrient/rh) = 1, list(/obj/structure/table, /obj/item/cultivator, /obj/item/hatchet) = 1, /obj/item/reagent_containers/glass/bottle/ammonia = 1, - /obj/item/reagent_containers/glass/bottle/diethylamine = 1, + /obj/item/reagent_containers/glass/bottle/diethylamine = 1 ) weighted_mob_spawn_list = list( @@ -42,6 +42,7 @@ /obj/machinery/power/port_gen/pacman = 1, /obj/structure/frame/machine = 2, /obj/structure/frame/computer = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) /datum/dungeon_room_theme/maintenance/material_storeroom/pre_initialize() @@ -103,6 +104,7 @@ /mob/living/simple_animal/mouse = 3, /mob/living/simple_animal/opossum = 1, /mob/living/simple_animal/hostile/retaliate/goat = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner ) /datum/dungeon_room_theme/maintenance/junk/pre_initialize() @@ -128,6 +130,7 @@ /obj/machinery/iv_drip = 2, /obj/machinery/stasis = 1, /obj/machinery/sleeper = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) weighted_mob_spawn_list = list( /mob/living/simple_animal/hostile/zombie = 3, @@ -179,6 +182,7 @@ /obj/item/weldingtool/largetank = 1, /obj/structure/frame/machine = 1, /obj/structure/frame/computer = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) weighted_mob_spawn_list = list( /mob/living/simple_animal/hostile/hivebot = 5, @@ -210,6 +214,7 @@ /obj/structure/spider/stickyweb = 5, /obj/structure/spider/cocoon = 5, /obj/structure/spider/spiderling = 2, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) weighted_mob_spawn_list = list( @@ -233,6 +238,7 @@ list(/obj/structure/table, /obj/item/reagent_containers/glass/beaker/waterbottle, /obj/item/reagent_containers/food/snacks/monkeycube) = 1, /obj/structure/frame/machine = 1, /obj/structure/frame/computer = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) weighted_mob_spawn_list = list( @@ -274,7 +280,8 @@ /obj/structure/frame/machine = 3, /obj/structure/frame/computer = 2, /obj/effect/spawner/lootdrop/random_anomaly_core = 1, - /obj/effect/mine/stun + /obj/effect/mine/stun = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) weighted_mob_spawn_list = list( /mob/living/simple_animal/hostile/hivebot = 5, @@ -304,7 +311,8 @@ /obj/item/coin/silver = 5, /obj/item/stack/sheet/mineral/gold = 10, ///this DLC is about letting go, letting go of poverty!!! /obj/item/stack/spacecash/c1000 = 2, - /obj/item/stack/sheet/mineral/diamond = 5 + /obj/item/stack/sheet/mineral/diamond = 5, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) weighted_mob_spawn_list = list( /mob/living/simple_animal/hostile/hivebot/range = 2, @@ -318,7 +326,8 @@ /obj/structure/closet/secure_closet/freezer/fridge = 1, /obj/item/storage/box/donkpockets = 1, /obj/item/kitchen/knife = 1, - /obj/effect/spawner/lootdrop/random_meat = 5 + /obj/effect/spawner/lootdrop/random_meat = 5, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) weighted_mob_spawn_list = list( @@ -340,7 +349,8 @@ list(/obj/structure/rack , /obj/item/clothing/suit/armor/vest, /obj/item/clothing/head/helmet/riot) = 1, /obj/structure/frame/machine = 1, /obj/structure/frame/computer = 1, - /obj/effect/mine/stun + /obj/effect/mine/stun, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) weighted_mob_spawn_list = list( @@ -381,7 +391,7 @@ . = ..() for(var/i in 1 to 5) if(prob(10)) - weighted_feature_spawn_list[/obj/item/gun/energy/laser/scattershot ]++ + weighted_feature_spawn_list[/obj/item/gun/energy/laser/scattershot]++ else weighted_feature_spawn_list[/obj/item/melee/spear/bonespear/chitinspear]++ @@ -393,7 +403,8 @@ /obj/machinery/autolathe = 1, /obj/item/stack/sheet/glass/fifty = 1, /obj/item/stack/sheet/metal/fifty = 1, - /obj/item/stack/sheet/mineral/silver/fifty = 1 + /obj/item/stack/sheet/mineral/silver/fifty = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) ///eskimo enemy variety, they came here for the winter /datum/dungeon_room_theme/maintenance/eskimo @@ -413,7 +424,8 @@ weighted_feature_spawn_list = list( /obj/effect/mine/kickmine = 1, /obj/effect/mine/creampie = 7, - /obj/effect/spawner/lootdrop/random_anomaly_core = 1 + /obj/effect/spawner/lootdrop/random_anomaly_core = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) ///mineral room 2 but with danger involved /datum/dungeon_room_theme/maintenance/mineral_room @@ -423,7 +435,8 @@ /obj/item/stack/sheet/mineral/plasma = 5, /obj/item/stack/sheet/mineral/gold = 5, /obj/item/stack/sheet/mineral/silver = 5, - /obj/item/stack/sheet/mineral/mythril = 1 + /obj/item/stack/sheet/mineral/mythril = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) weighted_mob_spawn_list = list( @@ -435,7 +448,8 @@ weighted_feature_spawn_list = list( /obj/item/clothing/gloves/combat = 1, /obj/item/kitchen/knife/combat = 1, - /obj/machinery/atmospherics/components/unary/vent_pump/on = 1 + /obj/machinery/atmospherics/components/unary/vent_pump/on = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) @@ -449,7 +463,8 @@ /obj/item/reagent_containers/food/snacks/pizza = 1, /obj/item/circuitboard/machine/griddle = 1, /obj/item/clothing/suit/toggle/chef = 1, - /obj/item/clothing/suit/apron/chef = 1 + /obj/item/clothing/suit/apron/chef = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) ///cuackles played this once /datum/dungeon_room_theme/maintenance/oxygen_included @@ -458,14 +473,15 @@ /obj/structure/tank_dispenser = 1, /obj/item/tank/internals/emergency_oxygen = 3, /obj/item/tank/internals/emergency_oxygen/double = 1, - /obj/item/tank/internals/emergency_oxygen/vox = 1 + /obj/item/tank/internals/emergency_oxygen/vox = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) ///we couldnt afford the surgery tools /datum/dungeon_room_theme/maintenance/medical_surgical weighted_possible_floor_types = list( /turf/open/floor/plasteel/white = 3, /turf/open/floor/plasteel = 5, - /turf/open/floor/plating = 3, + /turf/open/floor/plating = 3, ) weighted_feature_spawn_list = list( @@ -473,4 +489,5 @@ /obj/item/storage/firstaid/toxin = 1, /obj/machinery/computer/operating = 1, /obj/structure/table/optable = 1, + /obj/effect/spawner/lootdrop/nullspace_crystal_spawner = 1 ) diff --git a/code/datums/mind.dm b/code/datums/mind.dm index 14ec04bef624c..21b11c340353f 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -498,6 +498,13 @@ if(!check_rights(R_ADMIN)) return + if(current && isliving(current)) + if(href_list["set_psi_faculty"] && href_list["set_psi_faculty_rank"]) + current.set_psi_rank(href_list["set_psi_faculty"], text2num(href_list["set_psi_faculty_rank"])) + message_admins("[key_name_admin(usr)] set [key_name(current)]'s [href_list["set_psi_faculty"]] faculty to [text2num(href_list["set_psi_faculty_rank"])].") + log_admin("[key_name_admin(usr)] set [key_name(current)]'s [href_list["set_psi_faculty"]] faculty to [text2num(href_list["set_psi_faculty_rank"])].") + return TRUE + var/self_antagging = usr == current if(href_list["add_antag"]) diff --git a/code/datums/mutations/_combined.dm b/code/datums/mutations/_combined.dm index 4c65979db12ba..cd1b54f561d8d 100644 --- a/code/datums/mutations/_combined.dm +++ b/code/datums/mutations/_combined.dm @@ -13,13 +13,13 @@ /* RECIPES */ -/datum/generecipe/shock - required = "/datum/mutation/human/insulated; /datum/mutation/human/radioactive" - result = SHOCKTOUCH +// /datum/generecipe/shock +// required = "/datum/mutation/human/insulated; /datum/mutation/human/radioactive" +// result = SHOCKTOUCH -/datum/generecipe/shockfar - required = "/datum/mutation/human/shock; /datum/mutation/human/telekinesis" - result = SHOCKTOUCHFAR +// /datum/generecipe/shockfar +// required = "/datum/mutation/human/shock; /datum/mutation/human/telekinesis" +// result = SHOCKTOUCHFAR /datum/generecipe/antiglow required = "/datum/mutation/human/glow; /datum/mutation/human/void" diff --git a/code/datums/mutations/telekinesis.dm b/code/datums/mutations/telekinesis.dm index 476705d93dbdc..d53ec7bfb9b1d 100644 --- a/code/datums/mutations/telekinesis.dm +++ b/code/datums/mutations/telekinesis.dm @@ -3,6 +3,7 @@ name = "Telekinesis" desc = "A strange mutation that allows the holder to interact with objects through thought." quality = POSITIVE + locked = TRUE difficulty = 18 text_gain_indication = span_notice("You feel smarter!") limb_req = BODY_ZONE_HEAD diff --git a/code/datums/mutations/telepathy.dm b/code/datums/mutations/telepathy.dm index de01fff9fb301..ae008d8fce95a 100644 --- a/code/datums/mutations/telepathy.dm +++ b/code/datums/mutations/telepathy.dm @@ -2,9 +2,10 @@ name = "Telepathy" desc = "A rare mutation that allows the user to telepathically communicate to others." quality = POSITIVE + locked = TRUE text_gain_indication = span_notice("You can hear your own voice echoing in your mind!") text_lose_indication = span_notice("You don't hear your mind echo anymore.") difficulty = 12 power_path = /datum/action/cooldown/spell/list_target/telepathy instability = 10 - energy_coeff = 1 \ No newline at end of file + energy_coeff = 1 diff --git a/code/datums/mutations/touch.dm b/code/datums/mutations/touch.dm index 09d01af9af571..19fc4d4b21295 100644 --- a/code/datums/mutations/touch.dm +++ b/code/datums/mutations/touch.dm @@ -2,7 +2,6 @@ name = "Shock Touch" desc = "The affected can channel excess electricity through their hands without shocking themselves, allowing them to shock others." quality = POSITIVE - locked = TRUE difficulty = 16 text_gain_indication = span_notice("You feel power flow through your hands.") text_lose_indication = span_notice("The energy in your hands subsides.") diff --git a/code/datums/traits/negative.dm b/code/datums/traits/negative.dm index 93752f2014a37..557987cc1cf8a 100644 --- a/code/datums/traits/negative.dm +++ b/code/datums/traits/negative.dm @@ -985,3 +985,21 @@ if(!old_limb.is_organic_limb()) cybernetics_level-- update_mood() + + +/datum/quirk/psionically_deafened + name = "Psionically Deafened" + desc = "You were born within a region of space with no known bluespace activity. You cannot awaken as a psionic with this quirk." + icon = "hand-back-fist" + value = -1 + mob_trait = TRAIT_PSIONICALLY_DEAFENED + gain_text = span_notice("You feel a sense of numbness in your thoughts.") + lose_text = span_notice("You feel like a weight has receeded from your mind.") + medical_record_text = "Patient demonstrates unusually stagnant brain patterns." + +/datum/quirk/psionically_deafened/check_quirk(datum/preferences/prefs) + var/datum/species/species_type = prefs.read_preference(/datum/preference/choiced/species) + + if(species_type == /datum/species/ipc) // IPCs cant use psionics normally + return "You have no brain!" + return FALSE diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 2d25ea88acbe5..77a8e214bd1ad 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -1386,3 +1386,6 @@ if(destination) forceMove(destination) return TRUE + +/atom/movable/proc/do_simple_ranged_interaction(mob/user) + return FALSE diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm index 45b6908f48f9a..991e4aeb37451 100644 --- a/code/game/data_huds.dm +++ b/code/game/data_huds.dm @@ -47,7 +47,7 @@ hud_icons = list(ID_HUD) /datum/atom_hud/data/human/security/advanced - hud_icons = list(ID_HUD, IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD, WANTED_HUD, NANITE_HUD) + hud_icons = list(ID_HUD, IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD, WANTED_HUD, NANITE_HUD, IMPPSI_HUD) /datum/atom_hud/data/human/security/advanced/hos hud_icons = list(ID_HUD, IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD, WANTED_HUD, NANITE_HUD, STATUS_HUD, HEALTH_HUD) @@ -212,7 +212,7 @@ Security HUDs! Basic mode shows only the job. /mob/living/proc/sec_hud_set_implants() var/image/holder - for(var/i in list(IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD)) + for(var/i in list(IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD, IMPPSI_HUD)) holder = hud_list[i] holder.icon_state = null set_hud_image_inactive(i) @@ -231,6 +231,12 @@ Security HUDs! Basic mode shows only the job. holder.pixel_y = IC.Height() - world.icon_size holder.icon_state = "hud_imp_chem" set_hud_image_active(IMPCHEM_HUD) + else if(istype(I, /obj/item/implant/psi_control)) + holder = hud_list[IMPPSI_HUD] + var/icon/IC = icon(icon, icon_state, dir) + holder.pixel_y = IC.Height() - world.icon_size + holder.icon_state = "hud_imp_psi" + set_hud_image_active(IMPPSI_HUD) if(HAS_TRAIT(src, TRAIT_MINDSHIELD)) holder = hud_list[IMPLOYAL_HUD] diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index 04711dc951bee..c4e5091e55397 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -199,6 +199,14 @@ return ..() +/obj/machinery/door/do_simple_ranged_interaction(mob/user) + if(!requiresID() || allowed(null)) + if(density) + open() + else + close() + return TRUE + /obj/machinery/door/proc/try_to_activate_door(mob/user) add_fingerprint(user) if(operating || (obj_flags & EMAGGED)) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index a4af6863e9bf6..65444eb9e7f1d 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -510,6 +510,11 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) R.activate_module(src) R.hud_used.update_robot_modules_display() +/obj/item/do_simple_ranged_interaction(mob/user) + if(user) + attack_self(user) + return TRUE + /obj/item/proc/GetDeconstructableContents() return get_all_contents() - src diff --git a/code/game/objects/items/circuitboards/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machine_circuitboards.dm index 7b014b3b06778..4545fd1da5b7f 100644 --- a/code/game/objects/items/circuitboards/machine_circuitboards.dm +++ b/code/game/objects/items/circuitboards/machine_circuitboards.dm @@ -871,6 +871,17 @@ /obj/item/stock_parts/manipulator = 1, /obj/item/stack/cable_coil = 1, /obj/item/stack/sheet/glass = 2) + +/obj/item/circuitboard/machine/psionic_awakener + name = "Psionic Awakener (Machine Board)" + greyscale_colors = CIRCUIT_COLOR_MEDICAL + build_path = /obj/machinery/psionic_awakener + req_components = list( + /obj/item/stock_parts/matter_bin = 1, + /obj/item/stock_parts/manipulator = 1, + /obj/item/stack/cable_coil = 1, + /obj/item/stack/sheet/nullglass = 2) + // yogs start - vr sleeper /obj/item/circuitboard/machine/vr_sleeper name = "VR Sleeper (Machine Board)" diff --git a/code/game/objects/items/implants/implant_psi.dm b/code/game/objects/items/implants/implant_psi.dm new file mode 100644 index 0000000000000..af93d3c5958c9 --- /dev/null +++ b/code/game/objects/items/implants/implant_psi.dm @@ -0,0 +1,173 @@ +/obj/item/implant/psi_control + name = "psi dampener implant" + desc = "A safety implant for registered psi-operants." + implant_color = "n" + + var/overload = 0 + var/max_overload = 100 + var/cooldown_rate = 10 + var/psi_mode = PSI_IMPLANT_AUTOMATIC + var/list/logs + +/obj/item/implant/psi_control/get_data() + var/dat = {"Implant Specifications:
+ Name: Nanotrasen Psionic Mitigation Implant
+ Life: Ten years.
+ Important Notes: Psionic personel injected with this device can have their psionic potental di.
+
+ Implant Details:
+ Function: Contains a small shard of nullglass that prevents those implanted from being able to use psionic powers.
+ Special Features: Will prevent and log the use of psionics.
+ Integrity: Implant will last so long as the device is inside the bloodstream."} + return dat + +/obj/item/implant/psi_control/Initialize(mapload) + . = ..() + SSpsi.psi_dampeners += src + SSpsi.processing += src + +/obj/item/implant/psi_control/Destroy() + SSpsi.psi_dampeners -= src + SSpsi.processing -= src + . = ..() + +/obj/item/implant/psi_control/process() + ..() + overload = max(overload - cooldown_rate, 0) + +/obj/item/implant/psi_control/disrupts_psionics() + if(!imp_in) + return FALSE + var/use_psi_mode = get_psi_mode() + return (use_psi_mode == PSI_IMPLANT_SHOCK || use_psi_mode == PSI_IMPLANT_WARN) ? src : FALSE + +/obj/item/implant/psi_control/removed(mob/living/source, silent = FALSE, special = 0) + var/mob/living/M = imp_in + if(!silent && disrupts_psionics() && istype(M) && M.psi) + to_chat(M, span_notice("You feel the chilly shackles around your psionic faculties fade away.")) + . = ..() + +/obj/item/implant/psi_control/proc/update_functionality(silent) + var/mob/living/M = imp_in + if(silent || !M || !M.psi) + return + if(get_psi_mode() == PSI_IMPLANT_DISABLED) + to_chat(M, span_notice("You feel the chilly shackles around your psionic faculties fade away.")) + else + to_chat(M, span_notice("Bands of hollow ice close themselves around your psionic faculties.")) + +/obj/item/implant/psi_control/proc/meltdown() + overload = 100 + if(imp_in) + report_failure() + psi_mode = PSI_IMPLANT_DISABLED + update_functionality() + +/obj/item/implant/psi_control/proc/get_psi_mode() + if(psi_mode == PSI_IMPLANT_AUTOMATIC) + switch(SSsecurity_level.get_current_level_as_number()) + if(SEC_LEVEL_GREEN) + return PSI_IMPLANT_SHOCK + if(SEC_LEVEL_BLUE) + return PSI_IMPLANT_WARN + else + return PSI_IMPLANT_LOG + + return psi_mode + +/obj/item/implant/psi_control/withstand_psi_stress(stress, atom/source) + if(source != imp_in) + return + + var/use_psi_mode = get_psi_mode() + + if(use_psi_mode == PSI_IMPLANT_DISABLED) + return stress + + . = 0 + + if(stress) + + // If we're disrupting psionic attempts at the moment, we might overload. + if(disrupts_psionics()) + var/overload_amount = FLOOR(stress, 10) + if(overload_amount) + overload += overload_amount + if(overload >= 100) + if(imp_in) + to_chat(imp_in, span_danger("Your psi dampener overloads violently!")) + meltdown() + update_functionality() + return + if(imp_in) + if(overload >= 75 && overload < 100) + to_chat(imp_in, span_danger("Your psi dampener is searing hot!")) + else if(overload >= 50 && overload < 75) + to_chat(imp_in, span_warning("Your psi dampener is uncomfortably hot...")) + else if(overload >= 25 && overload < 50) + to_chat(imp_in, span_warning("You feel your psi dampener heating up...")) + + // If all we're doing is logging the incident then just pass back stress without changing it. + if(source && source == imp_in) + report_violation(stress) + switch(use_psi_mode) + if(PSI_IMPLANT_LOG) + return stress + if(PSI_IMPLANT_SHOCK) + to_chat(imp_in, span_danger("Your psi dampener punishes you with a violent neural shock!")) + imp_in.electrocute_act(5, src) + if(isliving(imp_in)) + var/mob/living/M = imp_in + if(M.psi) M.psi.stunned(5) + if(PSI_IMPLANT_WARN) + to_chat(imp_in, span_warning("Your psi dampener primly informs you it has reported this violation.")) + +/obj/item/implant/psi_control/proc/report_failure() + LAZYADD(logs, "Critical system failure - [imp_in.name].") + +/obj/item/implant/psi_control/proc/report_violation(stress) + LAZYADD(logs, "Sigma [round(stress/10)] event - [imp_in.name].") + +/obj/item/implant/psi_control/psych + psi_mode = PSI_IMPLANT_LOG + +/obj/item/implanter/psi_control + name = "implanter (psi dampener)" + imp_type = /obj/item/implant/psi_control + +/obj/item/implantcase/psi_control + name = "implant case - 'Psi Dampener'" + desc = "A glass case containing a psi dampener implant." + imp_type = /obj/item/implant/psi_control + +/obj/item/implant/nullglass + name = "nullglass shard" + desc = "A shard of psionic inhibiting glass." + implant_color = "n" + var/stress_left = 100 + var/lifespan = 1 MINUTES + +/obj/item/implant/nullglass/Initialize(mapload) + . = ..() + QDEL_IN(src, lifespan) + +/obj/item/implant/nullglass/disrupts_psionics() + if(imp_in) + return src + +/obj/item/implant/nullglass/withstand_psi_stress(stress, atom/source) + if(source != imp_in) + return stress + + . = max(stress - stress_left, 0) + stress_left -= stress + if(imp_in) + if(stress_left > 0 && stress_left < 25) + to_chat(imp_in, span_danger("You feel a searing hot piece of glass in your body!")) + else if(stress_left >= 25 && stress_left < 50) + to_chat(imp_in, span_warning("You feel a piece of glass in your body getting uncomfortably hot...")) + else if(stress_left >= 50) + to_chat(imp_in, span_warning("You feel a piece of glass in your body heating up...")) + if(stress_left <= 0) + to_chat(imp_in, span_danger("You hear a piece of glass shatter in your body!")) + qdel(src) diff --git a/code/game/objects/items/stacks/sheets/glass.dm b/code/game/objects/items/stacks/sheets/glass.dm index c53825bafe640..690dab2ed0129 100644 --- a/code/game/objects/items/stacks/sheets/glass.dm +++ b/code/game/objects/items/stacks/sheets/glass.dm @@ -250,6 +250,29 @@ GLOBAL_LIST_INIT(plastitaniumglass_recipes, list( recipes = GLOB.plastitaniumglass_recipes return ..() +GLOBAL_LIST_INIT(nullglass_recipes, list ( \ + new/datum/stack_recipe("nullglass tile", /obj/item/stack/tile/mineral/nullglass, time = 0), \ +)) + +/obj/item/stack/sheet/nullglass + name = "nullglass" + desc = "A glass sheet made out of a strange black glass capable of nullifying magic." + singular_name = "nullglass sheet" + icon = 'yogstation/icons/obj/stack_objects.dmi' + icon_state = "sheet-nullglass" + item_state = "sheet-plastitaniumglass" + materials = list(/datum/material/glass=MINERAL_MATERIAL_AMOUNT) + merge_type = /obj/item/stack/sheet/nullglass + grind_results = list(/datum/reagent/water/holywater = 1) + matter_amount = 4 + +/obj/item/stack/sheet/nullglass/fifty + amount = 50 + +/obj/item/stack/sheet/nullglass/Initialize(mapload, new_amount, merge = TRUE) + recipes = GLOB.nullglass_recipes + return ..() + /obj/item/shard name = "shard" desc = "A nasty looking shard of glass." @@ -357,3 +380,12 @@ GLOBAL_LIST_INIT(plastitaniumglass_recipes, list( materials = list(/datum/material/plasma=MINERAL_MATERIAL_AMOUNT * 0.5, /datum/material/glass=MINERAL_MATERIAL_AMOUNT) icon_prefix = "plasma" weld_material = /obj/item/stack/sheet/plasmaglass + +/obj/item/shard/nullglass + name = "null shard" + desc = "A nasty looking shard of nullglass." + icon_state = "nulllarge" + icon_prefix = "null" + +/obj/item/shard/nullglass/disrupts_psionics() + return src diff --git a/code/game/objects/items/storage/boxes.dm b/code/game/objects/items/storage/boxes.dm index 32f4de02bc081..a7601d231f73e 100644 --- a/code/game/objects/items/storage/boxes.dm +++ b/code/game/objects/items/storage/boxes.dm @@ -434,6 +434,34 @@ /obj/item/implanter = 1) generate_items_inside(items_inside,src) +/obj/item/storage/box/psiimp + name = "boxed psi dampener implant kit" + desc = "Box full of implants to protect the mentaly gifted." + illustration = "implant" + +/obj/item/storage/box/psiimp/PopulateContents() + var/static/items_inside = list( + /obj/item/implantcase/psi_control = 4, + /obj/item/implanter = 1, + /obj/item/implantpad = 1) + generate_items_inside(items_inside,src) + +/obj/item/storage/box/nullglass + name = "box of nullglass shells" + desc = "A box full of beanbag shells designed for shotguns. The box itself is designed for holding any kind of shotgun shell." + icon_state = "rubbershot_box" + illustration = null + +/obj/item/storage/box/nullglass/PopulateContents() + . = ..() + var/datum/component/storage/STR = GetComponent(/datum/component/storage) + STR.max_items = 7 + STR.set_holdable(list(/obj/item/ammo_casing/shotgun)) + +/obj/item/storage/box/nullglass/PopulateContents() + for(var/i in 1 to 7) + new /obj/item/ammo_casing/shotgun/nullglass(src) + /obj/item/storage/box/bodybags name = "body bags" desc = "The label indicates that it contains body bags." diff --git a/code/game/objects/items/storage/firstaid.dm b/code/game/objects/items/storage/firstaid.dm index 9b7967dd85419..f0ba8182ff5b9 100644 --- a/code/game/objects/items/storage/firstaid.dm +++ b/code/game/objects/items/storage/firstaid.dm @@ -683,6 +683,14 @@ for(var/i in 1 to 5) new /obj/item/reagent_containers/pill/aranesp(src) +/obj/item/storage/pill_bottle/three_eye + name = "bottle of Three Eye pills" + desc = "Highly illegal drug. Stimulates rarely used portions of the brain." + +/obj/item/storage/pill_bottle/three_eye/PopulateContents() + for(var/i in 1 to 5) + new /obj/item/reagent_containers/pill/three_eye(src) + /obj/item/storage/pill_bottle/psicodine name = "bottle of psicodine pills" desc = "Contains pills used to treat mental distress and traumas." diff --git a/code/game/objects/items/two_handed/spears.dm b/code/game/objects/items/two_handed/spears.dm index 674df7c98e808..5f4b68c188664 100644 --- a/code/game/objects/items/two_handed/spears.dm +++ b/code/game/objects/items/two_handed/spears.dm @@ -242,6 +242,15 @@ force_wielded = 8 can_be_explosive = FALSE +/obj/item/melee/spear/nullglass + icon = 'icons/obj/weapons/spears.dmi' + icon_state = "spearnull0" + base_icon_state = "nullglass_spear" + var/psi_stress = 0 + +/obj/item/twohanded/spear/nullglass/disrupts_psionics() + return src + /obj/item/melee/spear/plugged_musket name = "plugged maintenance musket" desc = "A maintenance musket with a plug bayonet." diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm index 9742a0d4332ab..53a99f9952736 100644 --- a/code/game/objects/items/weaponry.dm +++ b/code/game/objects/items/weaponry.dm @@ -249,6 +249,25 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 resistance_flags = FIRE_PROOF var/block_force = 20 +/obj/item/claymore/nullglass + name = "nullglass claymore" + icon_state = "claymore_nullglass" + item_state = "claymore_nullglass" + force = 20 + throwforce = 5 + block_force = 15 + var/shatter_chance = 30 + +/obj/item/claymore/nullglass/disrupts_psionics() + return src + +/obj/item/claymore/nullglass/attack(mob/living/target, mob/living/user) + . = ..() + if(prob(shatter_chance)) + var/obj/item/implant/nullglass/imp = new() + imp.implant(target) + playsound(loc, 'sound/effects/glass_step.ogg', 30, TRUE) + /obj/item/katana/Initialize(mapload) . = ..() if(!block_force) diff --git a/code/game/objects/structures/bedsheet_bin.dm b/code/game/objects/structures/bedsheet_bin.dm index b20091ff24557..bfe30e0bfdd66 100644 --- a/code/game/objects/structures/bedsheet_bin.dm +++ b/code/game/objects/structures/bedsheet_bin.dm @@ -455,6 +455,29 @@ LINEN BINS add_fingerprint(user) +/obj/structure/bedsheetbin/do_simple_ranged_interaction(mob/user) + if(amount >= 1) + amount-- + + var/obj/item/bedsheet/B + if(sheets.len) + B = sheets[sheets.len] + sheets.Remove(B) + + else + B = new /obj/item/bedsheet(loc) + + B.forceMove(drop_location()) + to_chat(user, span_notice("You telekinetically remove [B] from [src].")) + update_icon() + + if(hidden) + hidden.forceMove(drop_location()) + hidden = null + + + add_fingerprint(user) + /obj/item/bedsheet/adjusted slot_flags = ITEM_SLOT_HEAD flags_inv = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEGLOVES|HIDEJUMPSUIT|HIDENECK|HIDEFACIALHAIR|HIDESUITSTORAGE diff --git a/code/game/objects/structures/extinguisher.dm b/code/game/objects/structures/extinguisher.dm index 9fee168cabc06..45e8e5a99c6e4 100644 --- a/code/game/objects/structures/extinguisher.dm +++ b/code/game/objects/structures/extinguisher.dm @@ -106,6 +106,15 @@ else toggle_cabinet(user) +/obj/structure/extinguisher_cabinet/do_simple_ranged_interaction(mob/user) + if(stored_extinguisher) + stored_extinguisher.forceMove(loc) + stored_extinguisher = null + opened = 1 + playsound(loc, 'sound/machines/click.ogg', 15, 1, -3) + update_icon() + else + toggle_cabinet(user) /obj/structure/extinguisher_cabinet/attack_paw(mob/user) return attack_hand(user) diff --git a/code/game/objects/structures/flora.dm b/code/game/objects/structures/flora.dm index 1f0e619dcf093..07491c3367894 100644 --- a/code/game/objects/structures/flora.dm +++ b/code/game/objects/structures/flora.dm @@ -22,21 +22,24 @@ /obj/structure/flora/tree/attackby(obj/item/W, mob/user, params) if(log_amount && (!(flags_1 & NODECONSTRUCT_1))) - if(W.is_sharp() && W.force > 0) + if((W.is_sharp() && W.force) || (W.tool_behaviour == TOOL_HATCHET)) + var/duration = 20 SECONDS + if((W.tool_behaviour == TOOL_HATCHET) || istype(W, /obj/item/fireaxe)) + duration /= 40 //Much better with hatchets and axes. + else + duration /= W.force if(W.hitsound) playsound(get_turf(src), W.hitsound, 100, 0, 0) - user.visible_message(span_notice("[user] begins to cut down [src] with [W]."),span_notice("You begin to cut down [src] with [W]."), "You hear the sound of sawing.") - if(do_after(user, (1000 / W.force), src)) //5 seconds with 20 force, 8 seconds with a hatchet, 20 seconds with a shard. + user.visible_message(span_notice("[user] begins to cut down [src] with [W]."),span_notice("You begin to cut down [src] with [W]."), "You hear the sound of chopping.") + if(do_after(user, duration, src)) user.visible_message(span_notice("[user] fells [src] with the [W]."),span_notice("You fell [src] with the [W]."), "You hear the sound of a tree falling.") playsound(get_turf(src), 'sound/effects/meteorimpact.ogg', 100 , 0, 0) for(var/i=1 to log_amount) new /obj/item/grown/log/tree(get_turf(src)) - var/obj/structure/flora/stump/S = new(loc) S.name = "[name] stump" qdel(src) - else return ..() diff --git a/code/game/turfs/closed/wall/mineral_walls.dm b/code/game/turfs/closed/wall/mineral_walls.dm index 6edec65e9c255..eff5520ac6fab 100644 --- a/code/game/turfs/closed/wall/mineral_walls.dm +++ b/code/game/turfs/closed/wall/mineral_walls.dm @@ -173,11 +173,17 @@ canSmoothWith = SMOOTH_GROUP_BAMBOO_WALLS /turf/closed/wall/mineral/wood/attackby(obj/item/W, mob/user) - if(W.is_sharp() && W.force) - var/duration = (48/W.force) * 2 //In seconds, for now. - if(istype(W, /obj/item/hatchet) || istype(W, /obj/item/fireaxe)) - duration /= 4 //Much better with hatchets and axes. - if(do_after(user, duration*10, src)) //Into deciseconds. + if((W.is_sharp() && W.force) || (W.tool_behaviour == TOOL_HATCHET)) + var/duration = 20 SECONDS + if((W.tool_behaviour == TOOL_HATCHET) || istype(W, /obj/item/fireaxe)) + duration /= 40 //Much better with hatchets and axes. + else + duration /= W.force + if(W.hitsound) + playsound(get_turf(src), W.hitsound, 100, 0, 0) + user.visible_message(span_notice("[user] begins to cut down [src] with [W]."),span_notice("You begin to cut down [src] with [W]."), "You hear the sound of chopping.") + if(do_after(user, duration, src)) + user.visible_message(span_notice("[user] fells [src] with the [W]."),span_notice("You fell [src] with the [W]."), "You hear the sound of a tree falling.") dismantle_wall(FALSE,FALSE) return return ..() diff --git a/code/game/turfs/closed/walls.dm b/code/game/turfs/closed/walls.dm index d6893c18ff9e4..bc1e2be416567 100644 --- a/code/game/turfs/closed/walls.dm +++ b/code/game/turfs/closed/walls.dm @@ -260,6 +260,22 @@ return FALSE /turf/closed/wall/proc/try_decon(obj/item/I, mob/user, turf/T, modifiers) + + if(istype(I, /obj/item/psychic_power/psiblade)) + var/obj/item/psychic_power/psiblade/blade = I + if(!blade.can_break_wall) + return + to_chat(user, span_notice("You sink [blade] into [src] and begin trying to rip out the support frame...")) + playsound(src, 'sound/items/Welder.ogg', 100, 1) + + if(!do_after(user, blade.wall_break_time, src)) + return + + to_chat(user, span_notice("You tear through [src]'s support system and plating!")) + dismantle_wall(TRUE) + user.visible_message(span_warning("[src] was torn open by [user]!")) + playsound(src, 'sound/items/Welder.ogg', 100, 1) + if(!(modifiers && modifiers[RIGHT_CLICK])) return FALSE diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index c392c21bda0fc..5659946f69506 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -133,6 +133,26 @@ body += "Redeem Antag Token | " body += "See Antag Tokens" + body += "

" + body += "Psionics:
" + if(isliving(M)) + var/mob/living/psyker = M + if(psyker.psi) + body += "Remove psionics.
" + body += "Trigger latencies.
" + body += "" + for(var/faculty in list(PSI_COERCION, PSI_PSYCHOKINESIS, PSI_REDACTION, PSI_ENERGISTICS)) + var/datum/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty) + var/faculty_rank = psyker.psi ? psyker.psi.get_rank(faculty) : 0 + body += "" + for(var/i = 1 to LAZYLEN(GLOB.psychic_ranks_to_strings)) + var/psi_title = GLOB.psychic_ranks_to_strings[i] + if(i == faculty_rank) + psi_title = "[psi_title]" + body += "" + body += "" + body += "
[faculty_decl.name][psi_title]
" + if (M.client) if(!isnewplayer(M)) body += "

" diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index b96e21262f1d4..f8a3487752d78 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -569,6 +569,20 @@ Game() // updates the main game menu HandleFSecret() + else if(href_list["remove_psionics"]) + var/mob/living/psyker = locate(href_list["remove_psionics"]) + if(psyker?.psi && !QDELETED(psyker.psi)) + to_chat(psyker, span_notice("Your psionic powers vanish abruptly, leaving you cold and empty.")) + log_admin("[key_name(usr)] removed all psionics from [key_name(psyker)].") + message_admins(span_adminnotice("[key_name_admin(usr)] removed all psionics from [key_name(psyker)].")) + QDEL_NULL(psyker.psi) + + else if(href_list["trigger_psi_latencies"]) + var/datum/psi_complexus/psi = locate(href_list["trigger_psi_latencies"]) + log_admin("[key_name(usr)] triggered psi latencies for [key_name(psi.owner)].") + message_admins(span_adminnotice("[key_name_admin(usr)] triggered psi latencies for [key_name(psi.owner)].")) + psi.check_latency_trigger(100, "outside intervention", force = TRUE) + else if(href_list["monkeyone"]) if(!check_rights(R_SPAWN)) return diff --git a/code/modules/antagonists/revenant/revenant.dm b/code/modules/antagonists/revenant/revenant.dm index cc9d70b167269..af038a9934dfe 100644 --- a/code/modules/antagonists/revenant/revenant.dm +++ b/code/modules/antagonists/revenant/revenant.dm @@ -72,6 +72,7 @@ . = ..() flags_1 |= RAD_NO_CONTAMINATE_1 ADD_TRAIT(src, TRAIT_SIXTHSENSE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_PSIONICALLY_IMMUNE, INNATE_TRAIT) var/datum/action/cooldown/spell/list_target/telepathy/revenant/telepathy = new(src) telepathy.Grant(src) diff --git a/code/modules/client/client_colour.dm b/code/modules/client/client_colour.dm index c59259bc307b3..11e4165e5b4c4 100644 --- a/code/modules/client/client_colour.dm +++ b/code/modules/client/client_colour.dm @@ -107,6 +107,10 @@ colour = list(rgb(77,77,77), rgb(150,150,150), rgb(28,28,28), rgb(0,0,0)) priority = INFINITY //we can't see colors anyway! +/datum/client_colour/thirdeye + colour = list(rgb(77, 77, 77), rgb(128, 75, 150), rgb(28,28,28), rgb(0,0,0)) + priority = 300 + // Duplicate so it doesn't conflict with monochromacy quirk /datum/client_colour/monochrome_infra colour = list(rgb(77,77,77), rgb(150,150,150), rgb(28,28,28), rgb(0,0,0)) diff --git a/code/modules/hydroponics/grown/towercap.dm b/code/modules/hydroponics/grown/towercap.dm index 415c95e0a7216..cab2a3c31ed86 100644 --- a/code/modules/hydroponics/grown/towercap.dm +++ b/code/modules/hydroponics/grown/towercap.dm @@ -52,7 +52,7 @@ /obj/item/reagent_containers/food/snacks/grown/wheat)) /obj/item/grown/log/attackby(obj/item/W, mob/user, params) - if(W.is_sharp()) + if(W.is_sharp() || (W.tool_behaviour == TOOL_HATCHET)) user.show_message(span_notice("You make [plank_name] out of \the [src]!"), MSG_VISUAL) var/seed_modifier = 0 if(seed) diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index a5a06a5d5b7e0..04d4d50641914 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -179,6 +179,11 @@ H.dna.species.after_equip_job(src, H, visualsOnly) + if(H.psi && H.psi.has_rank_above(PSI_RANK_OPERANT)) + var/obj/item/implant/psi_control/I = new(H) + if(!I.implant(H, null)) + qdel(I) // For odd casses like the psych + if(!visualsOnly && announce) announce(H) diff --git a/code/modules/jobs/job_types/geneticist.dm b/code/modules/jobs/job_types/geneticist.dm index 9140376187a49..5a5d5180962bb 100644 --- a/code/modules/jobs/job_types/geneticist.dm +++ b/code/modules/jobs/job_types/geneticist.dm @@ -13,9 +13,8 @@ outfit = /datum/outfit/job/geneticist - added_access = list(ACCESS_CHEMISTRY, ACCESS_RESEARCH) - base_access = list(ACCESS_MEDICAL, ACCESS_SCIENCE, ACCESS_GENETICS, ACCESS_CLONING, ACCESS_MORGUE, ACCESS_MECH_MEDICAL) - + added_access = list(ACCESS_CHEMISTRY, ACCESS_XENOBIOLOGY, ACCESS_ROBO_CONTROL, ACCESS_TECH_STORAGE) + base_access = list(ACCESS_MEDICAL, ACCESS_MORGUE, ACCESS_GENETICS, ACCESS_CLONING, ACCESS_MECH_MEDICAL, ACCESS_RESEARCH) paycheck = PAYCHECK_MEDIUM paycheck_department = ACCOUNT_MED diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 68770af76f140..f0fe3433b0b83 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -249,12 +249,14 @@ if(restrained()) changeNext_move(CLICK_CD_BREAKOUT) last_special = world.time + CLICK_CD_BREAKOUT - var/buckle_cd = 600 + var/buckle_cd = 1 MINUTES + if(psi?.can_use()) + buckle_cd = max(0, buckle_cd - ((10 SECONDS) * psi.get_rank(PSI_PSYCHOKINESIS))) if(handcuffed) var/obj/item/restraints/O = src.get_item_by_slot(ITEM_SLOT_HANDCUFFED) buckle_cd = O.breakouttime visible_message(span_warning("[src] attempts to unbuckle [p_them()]self!"), \ - span_notice("You attempt to unbuckle yourself... (This will take around [round(buckle_cd/10,1)] second\s, and you need to stay still.)")) + span_notice("You attempt to unbuckle yourself... (This will take around [DisplayTimeText(buckle_cd)] second\s, and you need to stay still.)")) if(do_after(src, buckle_cd, src, timed_action_flags = IGNORE_HELD_ITEM)) if(!buckled) return @@ -303,6 +305,11 @@ return I.item_flags |= BEING_REMOVED breakouttime = I.breakouttime + + if(psi?.can_use()) + var/psi_mod = (1 - (psi.get_rank(PSI_PSYCHOKINESIS)*0.2)) + breakouttime = max(5, breakouttime * psi_mod) + if(!cuff_break) visible_message(span_warning("[src] attempts to remove [I]!")) to_chat(src, span_notice("You attempt to remove [I]... (This will take around [DisplayTimeText(breakouttime)] and you need to stand still.)")) @@ -314,7 +321,7 @@ else if(cuff_break == FAST_CUFFBREAK) breakouttime = 5 SECONDS visible_message(span_warning("[src] is trying to break [I]!")) - to_chat(src, span_notice("You attempt to break [I]... (This will take around 5 seconds and you need to stand still.)")) + to_chat(src, span_notice("You attempt to break [I]... (This will take around [DisplayTimeText(breakouttime)] and you need to stand still.)")) if(do_after(src, breakouttime, src, timed_action_flags = IGNORE_HELD_ITEM)) clear_cuffs(I, cuff_break) else diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index 96daf8fafa135..392a9c69c3adf 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -682,3 +682,22 @@ user.visible_message(span_danger("[user] grasps at [user.p_their()] [grasped_part.name], trying to stop the bleeding."), span_notice("You grab hold of your [grasped_part.name] tightly."), vision_distance=COMBAT_MESSAGE_RANGE) playsound(get_turf(src), 'sound/weapons/thudswoosh.ogg', 50, TRUE, -1) return TRUE + +/// Exploads the head of the mob +/mob/living/carbon/proc/explode_head(delete_brain) + var/obj/item/bodypart/head = get_bodypart(BODY_ZONE_HEAD) + var/obj/item/organ/brain/brain = getorganslot(ORGAN_SLOT_BRAIN) + if(!istype(head)) + return FALSE + if(delete_brain && (brain in head.get_organs())) + qdel(brain) + head.drop_limb() + head.drop_organs(src, TRUE) + qdel(head) + spawn_gibs() + +/// Causes the mob to have a seizure +/mob/living/carbon/proc/seizure(unconscious = 20 SECONDS) + visible_message(span_danger("[src] starts having a seizure!")) + Unconscious(unconscious) + SEND_SIGNAL(src, COMSIG_ADD_MOOD_EVENT, "seizure", /datum/mood_event/epilepsy) diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm index 716589fe99ffa..3e4a56080b4b3 100644 --- a/code/modules/mob/living/carbon/human/_species.dm +++ b/code/modules/mob/living/carbon/human/_species.dm @@ -210,6 +210,14 @@ GLOBAL_LIST_EMPTY(features_by_species) //The component to add when swimming var/swimming_component = /datum/component/swimming + // Psi Stuff + /// Prob chance that mobs of this species have latent psionics + var/latency_chance = 50 + /// List of faculties that can be chosen for random psionics + var/possible_faculties = list(PSI_COERCION, PSI_PSYCHOKINESIS, PSI_REDACTION, PSI_ENERGISTICS) + /// What level starting faculties are at + var/starting_psi_level = PSI_RANK_LATENT + var/smells_like = "something alien" //Should we preload this species's organs? @@ -511,6 +519,7 @@ GLOBAL_LIST_EMPTY(features_by_species) instantiated_abilities += ability C.add_movespeed_modifier(MOVESPEED_ID_SPECIES, TRUE, 100, override=TRUE, multiplicative_slowdown=speedmod, movetypes=(~FLYING)) + C.regenerate_icons() SEND_SIGNAL(C, COMSIG_SPECIES_GAIN, src, old_species) diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index 876399540bd01..0170e47e0c460 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -48,6 +48,12 @@ if(spec_return) return spec_return + if((!P.disrupts_psionics() && psi && psi.handle_block_chance(P) && psi.spend_power(round(P.damage/4), round(P.damage/20)))) + P.firer = src + P.setAngle(rand(0, 360)) + visible_message(span_danger("[src] deflects [P]!")) + return BULLET_ACT_FORCE_PIERCE + if(!(P.original == src && P.firer == src)) //can't block or reflect when shooting yourself var/shield_check = check_shields(P, P.damage, "the [P.name]", PROJECTILE_ATTACK, P.armour_penetration, P.damage_type) if(shield_check & SHIELD_DODGE) // skill issue, just dodge diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm index d5601d06b6d09..05d2ef3e2b12b 100644 --- a/code/modules/mob/living/carbon/human/human_defines.dm +++ b/code/modules/mob/living/carbon/human/human_defines.dm @@ -1,5 +1,5 @@ /mob/living/carbon/human - hud_possible = list(HEALTH_HUD,STATUS_HUD,ID_HUD,WANTED_HUD,IMPLOYAL_HUD,IMPCHEM_HUD,IMPTRACK_HUD, NANITE_HUD, DIAG_NANITE_FULL_HUD,ANTAG_HUD,GLAND_HUD,SENTIENT_DISEASE_HUD) + hud_possible = list(HEALTH_HUD,STATUS_HUD,ID_HUD,WANTED_HUD,IMPLOYAL_HUD,IMPCHEM_HUD, IMPTRACK_HUD, IMPPSI_HUD, NANITE_HUD, DIAG_NANITE_FULL_HUD,ANTAG_HUD,GLAND_HUD,SENTIENT_DISEASE_HUD) hud_type = /datum/hud/human pressure_resistance = 25 can_buckle = TRUE diff --git a/code/modules/mob/living/carbon/human/species_types/ethereal.dm b/code/modules/mob/living/carbon/human/species_types/ethereal.dm index 69710b36a3f41..8d1c2677a7c8e 100644 --- a/code/modules/mob/living/carbon/human/species_types/ethereal.dm +++ b/code/modules/mob/living/carbon/human/species_types/ethereal.dm @@ -39,6 +39,10 @@ swimming_component = /datum/component/swimming/ethereal wings_icon = "Ethereal" wings_detail = "Etherealdetails" + + latency_chance = 90 + possible_faculties = list(PSI_ENERGISTICS, PSI_REDACTION) + starting_psi_level = PSI_RANK_LATENT var/max_range = 5 var/max_power = 2 diff --git a/code/modules/mob/living/carbon/human/species_types/humans.dm b/code/modules/mob/living/carbon/human/species_types/humans.dm index 4da5fef95d6d8..b19257658a381 100644 --- a/code/modules/mob/living/carbon/human/species_types/humans.dm +++ b/code/modules/mob/living/carbon/human/species_types/humans.dm @@ -10,6 +10,8 @@ liked_food = JUNKFOOD | FRIED | GRILLED changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_MAGIC | MIRROR_PRIDE | ERT_SPAWN | RACE_SWAP | SLIME_EXTRACT species_language_holder = /datum/language_holder/english + possible_faculties = list(PSI_COERCION, PSI_REDACTION, PSI_PSYCHOKINESIS, PSI_ENERGISTICS) + starting_psi_level = PSI_RANK_LATENT smells_like = "soap and superiority" diff --git a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm index 1ed6a54a442ca..571e86fb67595 100644 --- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm @@ -36,6 +36,8 @@ var/heat_stunmod = 0 var/last_heat_stunmod = 0 var/regrowtimer + possible_faculties = list(PSI_COERCION, PSI_REDACTION, PSI_PSYCHOKINESIS, PSI_ENERGISTICS) + starting_psi_level = PSI_RANK_LATENT smells_like = "putrid scales" diff --git a/code/modules/mob/living/carbon/human/species_types/mothmen.dm b/code/modules/mob/living/carbon/human/species_types/mothmen.dm index 71dbd6c68c36c..e92ddbe15a634 100644 --- a/code/modules/mob/living/carbon/human/species_types/mothmen.dm +++ b/code/modules/mob/living/carbon/human/species_types/mothmen.dm @@ -23,6 +23,10 @@ mutanteyes = /obj/item/organ/eyes/moth changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_MAGIC | MIRROR_PRIDE | ERT_SPAWN | RACE_SWAP | SLIME_EXTRACT species_language_holder = /datum/language_holder/mothmen + + latency_chance = 75 + possible_faculties = list(PSI_COERCION, PSI_REDACTION, PSI_PSYCHOKINESIS) + starting_psi_level = PSI_RANK_LATENT deathsound = 'sound/voice/moth/moth_death.ogg' diff --git a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm index ade9a58e7d293..be8e1ccf7b027 100644 --- a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm +++ b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm @@ -31,7 +31,9 @@ survival_box_replacements = list(items_to_delete = list(/obj/item/clothing/mask/breath, /obj/item/tank/internals/emergency_oxygen),\ new_items = list(/obj/item/tank/internals/plasmaman/belt)) screamsound = list('sound/voice/plasmaman/plasmeme_scream_1.ogg', 'sound/voice/plasmaman/plasmeme_scream_2.ogg', 'sound/voice/plasmaman/plasmeme_scream_3.ogg') - + latency_chance = 35 + possible_faculties = list(PSI_COERCION, PSI_PSYCHOKINESIS, PSI_ENERGISTICS) + starting_psi_level = PSI_RANK_LATENT smells_like = "plasma-caked calcium" /// If the bones themselves are burning clothes won't help you much diff --git a/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm b/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm index 8d16f1f25d68d..f71edc483e401 100644 --- a/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm +++ b/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm @@ -43,6 +43,10 @@ mutant_bodyparts = list("tail_polysmorph", "dome", "dorsal_tubes", "teeth") default_features = list("tail_polysmorph" = "Polys", "dome" = "None", "dorsal_tubes" = "No", "teeth" = "None") changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_MAGIC | MIRROR_PRIDE | ERT_SPAWN | RACE_SWAP | SLIME_EXTRACT + + latency_chance = 90 + possible_faculties = list(PSI_COERCION, PSI_PSYCHOKINESIS) + starting_psi_level = PSI_RANK_LATENT smells_like = "charred, acidic meat" diff --git a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm index a2eaf030e5d37..e20111770b552 100644 --- a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm @@ -195,6 +195,7 @@ TRAIT_GENELESS, TRAIT_NOCRITDAMAGE, TRAIT_NOGUNS, + TRAIT_PSIONICALLY_DEAFENED, //no doubling up psionic powers TRAIT_SPECIESLOCK //never let them swap off darkspawn, it can cause issues ) mutanteyes = /obj/item/organ/eyes/darkspawn diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 6b5d9a8134fd4..1637a2eafdc72 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1075,6 +1075,8 @@ amount -= RAD_BACKGROUND_RADIATION // This will always be at least 1 because of how skin protection is calculated var/blocked = getarmor(null, RAD) + if(psi) + blocked = min(armor + psi.get_armour(RAD), 100) if(amount > RAD_BURN_THRESHOLD) apply_damage((amount-RAD_BURN_THRESHOLD)/RAD_BURN_THRESHOLD, BURN, null, blocked) diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 9fe25f343ebff..de87251b83976 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -6,6 +6,12 @@ if(status_flags & GODMODE) visible_message(span_danger("A strange force protects [src], [p_they()] can't be damaged!"), span_userdanger("A strange force protects you!")) return armor + if(psi) + var/psi_armor = psi.get_armour(attack_flag) + if(psi_armor && psi.spend_power(10)) + to_chat(src, span_warning("You soften the blow with your mind!")) + armor = min(armor + psi_armor, 100) + if(armor > 0 && armour_penetration) //WE HAVE ARMOR if(armour_penetration <= -100) // < -100 AP, no penetration on anything armor = 100 diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 55df3115cea93..b1e7cbb5d422b 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -691,6 +691,8 @@ if(I) I.attack_self(src) update_inv_hands() + else + attack_empty_hand(active_hand_index) /** * Get the notes of this mob diff --git a/code/modules/modular_computers/file_system/programs/psi_monitor.dm b/code/modules/modular_computers/file_system/programs/psi_monitor.dm new file mode 100644 index 0000000000000..67f1ddb5e5036 --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/psi_monitor.dm @@ -0,0 +1,114 @@ +/obj/machinery/psi_monitor + name = "psionic implant monitor" + icon = 'icons/obj/machines/psimeter.dmi' + icon_state = "meter_on" + anchored = TRUE + density = TRUE + opacity = FALSE + req_access = list(ACCESS_CMO) + + 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(obj_flags & EMAGGED) + return FALSE + obj_flags |= EMAGGED + remaining_charges-- + req_access.Cut() + to_chat(user, span_notice("You short out the access protocols.")) + return TRUE + + +/obj/machinery/psi_monitor/Topic(href, href_list) + + . = ..() + if(!.) + + if(href_list["login"]) + + var/obj/item/card/id/ID = usr.get_idcard() + if(!ID || !allowed(usr)) + to_chat(usr, span_notice("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/implant = locate(href_list["change_mode"]) + if(implant.imp_in) + 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 && implant && implant.imp_in) + implant.psi_mode = choice + implant.update_functionality() + . = 1 + + if(. && usr) + interact(usr) + +/obj/machinery/psi_monitor/ui_interact(mob/user) + interact(user) + return TRUE + +/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/implant = thing + if(!implant.imp_in) + continue + dat += "" + dat += "" + dat += "" + dat += "
OperantSystem loadMode
[implant.imp_in.name][implant.overload]%[authorized ? "[implant.psi_mode]" : "[implant.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/implant) + psi_violations += span_redtext("Critical system failure - [implant.imp_in.name].") + +/obj/machinery/psi_monitor/proc/report_violation(obj/item/implant/psi_control/implant, stress) + psi_violations += "Sigma [round(stress/10)] event - [implant.imp_in.name]." diff --git a/code/modules/projectiles/ammunition/ballistic/revolver.dm b/code/modules/projectiles/ammunition/ballistic/revolver.dm index aa98196f2cadc..d3ce06a1db12b 100644 --- a/code/modules/projectiles/ammunition/ballistic/revolver.dm +++ b/code/modules/projectiles/ammunition/ballistic/revolver.dm @@ -42,6 +42,13 @@ caliber = CALIBER_44MAG projectile_type = /obj/projectile/bullet/m44 +// revolver? + +/obj/item/ammo_casing/a357/nullglass + name = ".357 NULL bullet casing" + desc = "A .357 NULL bullet casing." + projectile_type = /obj/projectile/bullet/a357/nullglass + // 7.62x38mmR (Nagant Revolver) /obj/item/ammo_casing/n762 diff --git a/code/modules/projectiles/ammunition/ballistic/shotgun.dm b/code/modules/projectiles/ammunition/ballistic/shotgun.dm index de7538c5a49ec..d032fbc7a2dd6 100644 --- a/code/modules/projectiles/ammunition/ballistic/shotgun.dm +++ b/code/modules/projectiles/ammunition/ballistic/shotgun.dm @@ -143,6 +143,14 @@ pellets = 4 variance = 30 +/obj/item/ammo_casing/shotgun/nullglass + name = "nullglass buckshot shell" + desc = "A buckshot shell loaded with shells of nullglass that disrupt psionic." + icon_state = "mshell" // Temp + projectile_type = /obj/projectile/bullet/pellet/nullglass + pellets = 6 + variance = 25 + /obj/item/ammo_casing/shotgun/techshell name = "unloaded technological shell" desc = "A high-tech shotgun shell which can be loaded with materials to produce unique effects." diff --git a/code/modules/projectiles/boxes_magazines/ammo_boxes.dm b/code/modules/projectiles/boxes_magazines/ammo_boxes.dm index 948f6e3c2c34b..d404af2b66111 100644 --- a/code/modules/projectiles/boxes_magazines/ammo_boxes.dm +++ b/code/modules/projectiles/boxes_magazines/ammo_boxes.dm @@ -55,6 +55,13 @@ max_ammo = 6 multiple_sprites = AMMO_BOX_PER_BULLET +/obj/item/ammo_box/a357/nullglass + name = "speed loader (.357 NULL)" + desc = "A seven-shot speed loader designed for .357 revolvers. \ + These rounds trade damage for the ability to disrupt psionics." + icon_state = "357null" + ammo_type = /obj/item/ammo_casing/a357/nullglass + // .38 special loaders /obj/item/ammo_box/c38 diff --git a/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm b/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm index aa8e6514d69fa..4d687d61a9f1e 100644 --- a/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm +++ b/code/modules/projectiles/boxes_magazines/internal/_cylinder.dm @@ -59,3 +59,6 @@ if(!give_round(new load_type(src))) break update_appearance(UPDATE_ICON) + +/obj/item/ammo_box/magazine/internal/cylinder/nullglass + ammo_type = /obj/item/ammo_casing/a357/nullglass diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm index 9c5044a1a94b4..d6b295f2ffe24 100644 --- a/code/modules/projectiles/guns/ballistic.dm +++ b/code/modules/projectiles/guns/ballistic.dm @@ -588,7 +588,7 @@ GLOBAL_LIST_INIT(gun_saw_types, typecacheof(list( ///Handles all the logic of sawing off guns, /obj/item/gun/ballistic/proc/sawoff(mob/user, obj/item/saw) - if(!saw.is_sharp() || !is_type_in_typecache(saw, GLOB.gun_saw_types)) //needs to be sharp. Otherwise turned off eswords can cut this. + if((!saw.is_sharp() || !is_type_in_typecache(saw, GLOB.gun_saw_types)) && (saw.tool_behaviour != TOOL_SAW)) //needs to be sharp. Otherwise turned off eswords can cut this. return if(sawn_off) to_chat(user, span_warning("\The [src] is already shortened!")) diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm index 9c7e08f0d957a..7b980f06c7e07 100644 --- a/code/modules/projectiles/guns/ballistic/revolver.dm +++ b/code/modules/projectiles/guns/ballistic/revolver.dm @@ -71,6 +71,9 @@ if (current_skin) . += "It can be spun with alt+click" +/obj/item/gun/ballistic/revolver/nullglass + mag_type = /obj/item/ammo_box/magazine/internal/cylinder/nullglass + /obj/item/gun/ballistic/revolver/ultrasecure pin = /obj/item/firing_pin/fucked diff --git a/code/modules/projectiles/projectile/bullets/shotgun.dm b/code/modules/projectiles/projectile/bullets/shotgun.dm index ef855bbf36915..1a3cd49ebd3d8 100644 --- a/code/modules/projectiles/projectile/bullets/shotgun.dm +++ b/code/modules/projectiles/projectile/bullets/shotgun.dm @@ -223,3 +223,20 @@ damage_type = STAMINA // Doesn't do "real" damage sharpness = SHARP_NONE armour_penetration = -40 // Energy armor is usually very low so uhh + + +/obj/projectile/bullet/pellet/nullglass + name = "nullglass pellet" + damage = 6 + wound_bonus = 3 + bare_wound_bonus = 3 + +/obj/projectile/bullet/pellet/nullglass/disrupts_psionics() + return src + +/obj/projectile/bullet/pellet/nullglass/on_hit(atom/target) + . = ..() + if(prob(10)) + var/obj/item/implant/nullglass/imp = new() + imp.implant(target) + playsound(loc, 'sound/effects/glass_step.ogg', 30, TRUE) diff --git a/code/modules/psionics/complexus/complexus.dm b/code/modules/psionics/complexus/complexus.dm new file mode 100644 index 0000000000000..06454e893a901 --- /dev/null +++ b/code/modules/psionics/complexus/complexus.dm @@ -0,0 +1,181 @@ +/datum/psi_complexus + /// Whether or not we have been announced to our holder yet. + var/announced = FALSE + /// Whether or not we are suppressing our psi powers. + var/suppressed = TRUE + /// Whether or not we should automatically deflect/block incoming damage. + var/use_psi_armour = TRUE + /// Whether or not we should automatically heal damage damage. + var/use_autoredaction = TRUE + /// Whether or not zorch uses lethal projectiles. + var/zorch_harm = FALSE + /// What amount of heat the user wants to stop at. + var/limiter = 100 + /// Whether or not we need to rebuild our cache of psi powers. + var/rebuild_power_cache = TRUE + + /// Overall psi rating. + var/rating = 0 + /// Multiplier for power use stamina costs. + var/cost_modifier = 1 + /// Number of process ticks we are stunned for. + var/stun = 0 + /// world.time minimum before next power use. + var/next_power_use = 0 + + // Stamina / Heat + /// Current psi pool. + var/stamina = 50 + /// Max psi pool. + var/max_stamina = 50 + /// Multiplier for the recharge rate of psi heat. + var/stamina_recharge_mult = 1 + /// Current psi heat. + var/heat = 0 + /// Max psi heat. 100 is safe, 300 has minor consequences, 500 is dangerous, max is death. + var/max_heat = 500 + /// Multiplier for the decay rate of psi heat. + var/heat_decay_mult = 1 + + /// List of all currently latent faculties. + var/list/latencies + /// Assoc list of psi faculties to current rank. + var/list/ranks + /// Assoc list of psi faculties to base rank, in case reset is needed + var/list/base_ranks + /// List of atoms manifested/maintained by psychic power. + var/list/manifested_items + /// world.time minimum before a trigger can be attempted again. + var/next_latency_trigger = 0 + var/last_aura_size + var/last_aura_alpha + var/last_aura_color + var/aura_color = "#ff0022" + + // Cached powers. + var/list/learned_powers // All powers known + var/list/powers_by_faculty + var/datum/psionic_power/selected_power // Power currently selected + + var/obj/screen/psi/hub/ui // Reference to the master psi UI object. + var/mob/living/owner // Reference to our owner. + var/datum/mind/thinker + var/image/_aura_image // Client image + var/obj/effect/overlay/aura/image_holder //holder so we don't have to apply the image directly to the mob, making it's clickbox massive + +/datum/psi_complexus/New(mob/M) + owner = M + image_holder = new(src) + owner.vis_contents += image_holder + START_PROCESSING(SSpsi, src) + RegisterSignal(owner, COMSIG_PSI_SELECTION, PROC_REF(select_power)) + RegisterSignal(owner, COMSIG_PSI_INVOKE, PROC_REF(invoke_power)) + thinker = owner.mind + if(thinker && istype(thinker)) + RegisterSignal(thinker, COMSIG_MIND_TRANSFERRED, PROC_REF(mind_swap)) + +/datum/psi_complexus/proc/mind_swap(datum/mind/brain, mob/living/oldbody) + if(!brain || !istype(brain) || !brain.current) + return + UnregisterSignal(owner, COMSIG_PSI_SELECTION) + UnregisterSignal(owner, COMSIG_PSI_INVOKE) + owner.vis_contents -= image_holder + owner.psi = null + owner = brain.current + owner.vis_contents += image_holder + RegisterSignal(owner, COMSIG_PSI_SELECTION, PROC_REF(select_power)) + RegisterSignal(owner, COMSIG_PSI_INVOKE, PROC_REF(invoke_power)) + owner.psi = src + update(TRUE, TRUE) + +/datum/psi_complexus/Destroy() + destroy_aura_image(_aura_image) + STOP_PROCESSING(SSpsi, src) + if(thinker && istype(thinker)) + UnregisterSignal(thinker, COMSIG_MIND_TRANSFERRED) + if(owner) + UnregisterSignal(owner, COMSIG_PSI_SELECTION) + UnregisterSignal(owner, COMSIG_PSI_INVOKE) + cancel() + if(owner.client) + owner.client.screen -= ui.components + owner.client.screen -= ui + for(var/thing in SSpsi.all_aura_images) + owner.client.images -= thing + QDEL_NULL(ui) + owner.psi = null + owner = null + QDEL_NULL(image_holder) + + if(manifested_items) + for(var/thing in manifested_items) + qdel(thing) + manifested_items.Cut() + . = ..() + +/datum/psi_complexus/proc/select_power(mob/user) + if(suppressed) + return + rebuild_power_cache() + if(!LAZYLEN(learned_powers)) + return + var/list/choice_list = LAZYCOPY(learned_powers) + for(var/datum/psionic_power/I as anything in choice_list) + choice_list[I] = image(I.icon, null, I.icon_state) + var/selection = show_radial_menu(user, user, choice_list, null, 40, tooltips = TRUE, autopick_single_option = FALSE) + selected_power = selection + if(selection) //wipe the selected power unless something was actually chosen + selected_power.on_select(user) + user.balloon_alert(user, "Selected [selected_power.name]") + +/datum/psi_complexus/proc/invoke_power(mob/user, atom/target, proximity, parameters) + if(suppressed) + return + if(!selected_power) + return + . = selected_power.invoke(user, target, proximity, parameters) + if(.) + selected_power.handle_post_power(user, target) + +/datum/psi_complexus/proc/get_aura_image() + if(_aura_image && !istype(_aura_image)) + var/atom/A = _aura_image + destroy_aura_image(_aura_image) + _aura_image = null + CRASH("Non-image found in psi complexus: \ref[A] - \the [A] - [istype(A) ? A.type : "non-atom"]") + if(!_aura_image) + _aura_image = create_aura_image(image_holder) + return _aura_image + +/obj/effect/overlay/aura + name = "" + // icon = 'icons/effects/psi_aura_small.dmi' + // icon_state = "aura" + // pixel_x = -64 + // pixel_y = -64 + plane = GAME_PLANE + appearance_flags = NO_CLIENT_COLOR | RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + blend_mode = BLEND_MULTIPLY + layer = TURF_LAYER + 0.5 + alpha = 0 + +/proc/create_aura_image(newloc) + var/image/aura_image = image('icons/effects/psi_aura_small.dmi', newloc, "aura") + aura_image.blend_mode = BLEND_MULTIPLY + aura_image.appearance_flags = NO_CLIENT_COLOR | RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM + aura_image.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + aura_image.layer = TURF_LAYER + 0.5 + aura_image.alpha = 0 + aura_image.pixel_x = -64 + aura_image.pixel_y = -64 + for(var/datum/psi_complexus/psychic in SSpsi.processing) + if(!psychic.suppressed) + psychic?.owner?.client?.images += aura_image + SSpsi.all_aura_images[aura_image] = TRUE + return aura_image + +/proc/destroy_aura_image(image/aura_image) + for(var/datum/psi_complexus/psychic in SSpsi.processing) + psychic?.owner?.client?.images -= aura_image + SSpsi.all_aura_images -= aura_image diff --git a/code/modules/psionics/complexus/complexus_helpers.dm b/code/modules/psionics/complexus/complexus_helpers.dm new file mode 100644 index 0000000000000..f741fb34826b2 --- /dev/null +++ b/code/modules/psionics/complexus/complexus_helpers.dm @@ -0,0 +1,191 @@ +/datum/psi_complexus/proc/cancel() + SEND_SOUND(owner, sound('sound/effects/psi/power_fail.ogg')) + if(LAZYLEN(manifested_items)) + for(var/thing in manifested_items) + owner.dropItemToGround(thing) + qdel(thing) + manifested_items = null + +/datum/psi_complexus/proc/stunned(amount) + var/old_stun = stun + stun = max(stun, amount) + if(amount && !old_stun) + to_chat(owner, span_danger("Your concentration has been shattered! You cannot focus your psi power!")) + ui.update_icon() + cancel() + +/datum/psi_complexus/proc/get_armour(armourtype) + if(!use_psi_armour || !can_use_passive()) + return 0 + + /** + * rating multiplied by the rank of that specific faculty, multiplied by stamina percentage + */ + return round(clamp(clamp(4 * rating, 0, 20) * get_rank(SSpsi.armour_faculty_by_type[armourtype]), 0, 100) * (stamina/max_stamina)) + +/datum/psi_complexus/proc/handle_block_chance(obj/projectile/projectile) + if(!use_psi_armour || !can_use_passive()) + return FALSE + + var/effective_rank + var/chance = 0 + + if(istype(projectile, /obj/projectile/beam) || istype(projectile, /obj/projectile/energy)) + effective_rank = get_rank(PSI_ENERGISTICS) + else + effective_rank = get_rank(PSI_PSYCHOKINESIS) + + switch(effective_rank) + if(PSI_RANK_OPERANT) + chance = 1 + if(PSI_RANK_MASTER) + chance = 10 + if(PSI_RANK_GRANDMASTER) + chance = 50 + if(PSI_RANK_PARAMOUNT) + chance = 90 + + return prob(chance) + +/datum/psi_complexus/proc/get_rank(faculty) + return LAZYACCESS(ranks, faculty) + +/datum/psi_complexus/proc/set_rank(faculty, rank, defer_update, temporary) + if(get_rank(faculty) != rank) + LAZYSET(ranks, faculty, rank) + if(!temporary) + LAZYSET(base_ranks, faculty, rank) + if(!defer_update) + update() + +/datum/psi_complexus/proc/set_cooldown(value) + next_power_use = world.time + value + ui.update_icon() + +/datum/psi_complexus/proc/can_use_passive() + return (owner.stat == CONSCIOUS && !suppressed && !stun) + +/datum/psi_complexus/proc/can_use(incapacitation_flags) + return (owner.stat == CONSCIOUS && !suppressed && !stun && world.time >= next_power_use) + +/datum/psi_complexus/proc/spend_power(stamina_cost = 0, heat_cost = 0) + . = FALSE + if(!can_use()) + return FALSE + + // Focus + stamina_cost = max(1, CEILING(stamina_cost * cost_modifier, 1)) + if(stamina < stamina_cost) + return FALSE + if((heat + heat_cost) >= limiter) + to_chat(owner, "Your limiter prevents you from performing that.") + return FALSE + adjust_stamina(-stamina_cost) + adjust_heat(heat_cost) + handle_heat_effects() + + ui.update_icon() + return TRUE + +/datum/psi_complexus/proc/set_stamina(value = 0) + stamina = clamp(value, 0, max_stamina) + +/datum/psi_complexus/proc/adjust_stamina(value = 0) + set_stamina(stamina + value) + +/datum/psi_complexus/proc/set_heat(value = 0) + heat = clamp(value, 0, max_heat) + +/datum/psi_complexus/proc/adjust_heat(value = 0) + set_heat(heat + value) + +/datum/psi_complexus/proc/hide_auras() + if(owner.client) + for(var/image/I in SSpsi.all_aura_images) + owner.client.images -= I + +/datum/psi_complexus/proc/show_auras() + if(owner.client) + for(var/image/I in SSpsi.all_aura_images) + owner.client.images |= I + +/datum/psi_complexus/proc/handle_heat_effects(effective_heat) + if(!owner) + return FALSE + if(!effective_heat) + effective_heat = heat + if(effective_heat < 100) + return + // The Fun Effects (500 heat) + if(effective_heat >= max_heat) + switch(pick(1, 2)) + //1, Your head asplode / you are gibbed + if(1) + if(iscarbon(owner)) + var/mob/living/carbon/C = owner + C.explode_head() + else + owner.gib() + //2, Your psi powers are too strained, causing them to disapear forever + if(2) + qdel(src) + + //Less fun effects + switch(rand(1, effective_heat - 100)) + // Your nose bleeds a little. + if(1 to 20) + var/mob/living/carbon/human/H + if(istype(H) && (H.dna.species.species_traits & NOBLOOD)) + return + to_chat(owner,span_warning("Your nose begins to bleed...")) + owner.add_splatter_floor(small_drip = TRUE) + // Your get a headache. Yes this is stolen from disease code, sue me + if(21 to 500) + switch(effective_heat) + if(0 to 200) + to_chat(owner, span_warning("[pick("Your head hurts.", "Your head pounds.")]")) + adjust_stamina(rand(-5, -1)) + if(201 to 400) + to_chat(owner, span_warning("[pick("Your head hurts a lot.", "Your head pounds incessantly.")]")) + adjust_stamina(rand(-10, -5)) + owner.adjustStaminaLoss(25) + if(401 to 500) + to_chat(owner, span_userdanger("[pick("You feel a burning knife inside your brain!", "A wave of pain fills your head!")]")) + adjust_stamina(rand(-15, -10)) + owner.Stun(3.5 SECONDS) + +/datum/psi_complexus/proc/backblast(value) + + // Can't backblast if you're controlling your power. + if(!owner || suppressed) + return FALSE + + SEND_SOUND(owner, sound('sound/effects/psi/power_feedback.ogg')) + to_chat(owner, span_danger("Wild energistic feedback blasts across your psyche!")) + stunned(value * 2) + set_cooldown(value * 100) + + if(prob(value*10)) + owner.emote("scream") + adjust_heat(value * 10) + // Your head asplode. + owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, value) + if(ishuman(owner)) + var/mob/living/carbon/human/pop = owner + var/obj/item/organ/brain/sponge = pop.getorganslot(ORGAN_SLOT_BRAIN) + if(sponge && pop.getOrganLoss(ORGAN_SLOT_BRAIN) >= sponge.maxHealth) + pop.explode_head() + +/datum/psi_complexus/proc/has_rank_above(required_rank) + for(var/faculty in ranks) + if(required_rank <= get_rank(faculty)) + return TRUE + +/datum/psi_complexus/proc/reset() + aura_color = initial(aura_color) + ranks = base_ranks ? base_ranks.Copy() : null + max_stamina = initial(max_stamina) + set_stamina(stamina) + set_heat(heat) + cancel() + update() diff --git a/code/modules/psionics/complexus/complexus_latency.dm b/code/modules/psionics/complexus/complexus_latency.dm new file mode 100644 index 0000000000000..988491c12ac77 --- /dev/null +++ b/code/modules/psionics/complexus/complexus_latency.dm @@ -0,0 +1,36 @@ +/datum/psi_complexus/proc/check_latency_trigger(trigger_strength = 0, source, brain_damage = 0, force = FALSE) + + if(!LAZYLEN(latencies)) + return FALSE + + if(brain_damage) //don't force it when it's not had time to rest + owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(brain_damage/2, brain_damage)) + + if(world.time < next_latency_trigger && !force) + if(brain_damage) + to_chat(owner, span_danger("Your head throbs as [source] messes with your brain!")) + return + + next_latency_trigger = world.time + rand(10 SECONDS, 30 SECONDS) + + if(prob(trigger_strength)) + var/faculty = pick(latencies) + + var/new_rank = PSI_RANK_OPERANT + switch(rand(0, 10000)) //i intially tried using a weighted list with pickweight, but i kept getting out of bounds errors for some reason + if(0) //weighted so you can still roll grandmaster, but at an incredibly rare chance + new_rank = PSI_RANK_GRANDMASTER + if(1 to 100) + new_rank = PSI_RANK_MASTER + else + new_rank = PSI_RANK_OPERANT + + owner.set_psi_rank(faculty, new_rank) + var/datum/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty) + to_chat(owner, span_danger("You scream internally as your [faculty_decl.name] faculty is forced into operancy by [source]!")) + next_latency_trigger = world.time + (rand(60 SECONDS, 180 SECONDS) * new_rank) + else if(brain_damage) + to_chat(owner, span_danger("Your head throbs as [source] messes with your brain!")) + return FALSE + + return TRUE diff --git a/code/modules/psionics/complexus/complexus_power_cache.dm b/code/modules/psionics/complexus/complexus_power_cache.dm new file mode 100644 index 0000000000000..b3476b45eb74d --- /dev/null +++ b/code/modules/psionics/complexus/complexus_power_cache.dm @@ -0,0 +1,24 @@ +/datum/psi_complexus/proc/rebuild_power_cache() + if(rebuild_power_cache) + + learned_powers = list() + powers_by_faculty = list() + + for(var/faculty in ranks) + var/relevant_rank = get_rank(faculty) + var/datum/psionic_faculty/faculty_decl = SSpsi.get_faculty(faculty) + if(!faculty_decl) //if it's not initialized yet, come back later + continue + for(var/P in faculty_decl.powers) + var/datum/psionic_power/power = P + if(!power.min_rank) //if a minimum rank wasn't set, it's probably either bad coding or a parent used for typepathing, so don't include it + continue + + if(relevant_rank >= power.min_rank) + LAZYADD(powers_by_faculty[power.faculty], power) + LAZYADD(learned_powers, power) + rebuild_power_cache = FALSE + +/datum/psi_complexus/proc/get_powers_by_faculty(faculty) + rebuild_power_cache() + return powers_by_faculty[faculty] diff --git a/code/modules/psionics/complexus/complexus_process.dm b/code/modules/psionics/complexus/complexus_process.dm new file mode 100644 index 0000000000000..0dd169e39e541 --- /dev/null +++ b/code/modules/psionics/complexus/complexus_process.dm @@ -0,0 +1,222 @@ +/datum/psi_complexus/proc/update(force, silent) + + set waitfor = FALSE + + if(HAS_TRAIT(owner, TRAIT_PSIONICALLY_IMMUNE) || HAS_TRAIT(owner, TRAIT_PSIONICALLY_DEAFENED)) //no psionics for you + qdel(src) + return + + var/last_rating = rating + var/highest_faculty + var/highest_rank = 0 + var/combined_rank = 0 + for(var/faculty in ranks) + var/check_rank = get_rank(faculty) + if(check_rank == PSI_RANK_LATENT && !LAZYFIND(latencies, faculty)) //if they're latent and it's not already on the list, add it to latencies + LAZYADD(latencies, faculty) + else if(check_rank != PSI_RANK_LATENT) + if(check_rank <= PSI_RANK_BLUNT) //if they're not capable at all, remove it entirely + ranks -= faculty + LAZYREMOVE(latencies, faculty) //remove it from the list because they aren't latent anymore + combined_rank += check_rank + + if(!highest_faculty || highest_rank < check_rank) + highest_faculty = faculty + highest_rank = check_rank + + UNSETEMPTY(latencies) + var/rank_count = max(1, LAZYLEN(ranks)) + rebuild_power_cache = TRUE + rebuild_power_cache() + if(force || last_rating != CEILING(combined_rank/rank_count, 1)) + if(highest_rank <= 1) + if(highest_rank == 0) + qdel(src) + return + if(!silent) + SEND_SOUND(owner, 'sound/effects/psi/power_unlock.ogg') + rating = CEILING(combined_rank/rank_count, 1) + cost_modifier = 1 + if(rating > 1) + cost_modifier -= min(1, max(0.1, (rating-1) / 10)) + if(!ui) + ui = new(owner) + if(owner.client) + owner.client.screen += ui.components + owner.client.screen += ui + if(!suppressed && owner.client) + for(var/image/I in SSpsi.all_aura_images) + owner.client.images |= I + var/image/aura_image = get_aura_image() + aura_image.blend_mode = BLEND_ADD + switch(highest_faculty) + if(PSI_COERCION) + aura_color = "#cc3333" + if(PSI_PSYCHOKINESIS) + aura_color = "#3333cc" + if(PSI_REDACTION) + aura_color = "#33cc33" + if(PSI_ENERGISTICS) + aura_color = "#cccc33" + + if(!announced && owner?.client && !QDELETED(src)) + announced = TRUE + to_chat(owner, "
") + to_chat(owner, span_notice("You are psionic, touched by powers beyond understanding.")) + to_chat(owner, span_notice("Shift-left-click your Psi icon on the bottom right to view a summary of how to use them, or left click it to suppress or unsuppress your psionics. Beware: overusing your gifts can have deadly consequences.")) + to_chat(owner, "
") + +/datum/psi_complexus/process() + if(HAS_TRAIT(owner, TRAIT_PSIONICALLY_IMMUNE) || HAS_TRAIT(owner, TRAIT_PSIONICALLY_DEAFENED)) //no psionics for you + qdel(src) + return + + var/update_hud + if(stun) + stun-- + if(stun) + suppressed = TRUE + else + to_chat(owner, span_notice("You have recovered your mental composure.")) + update_hud = TRUE + return + + if(stamina < max_stamina) + adjust_stamina((owner.stat == CONSCIOUS ? rand(1,3) : rand(3,5)) * stamina_recharge_mult) + + if(heat) + adjust_heat(((owner.stat == CONSCIOUS ? -1 : -3)) * heat_decay_mult) + + if(owner.stat == CONSCIOUS && stamina && use_autoredaction && !suppressed && get_rank(PSI_REDACTION) >= PSI_RANK_OPERANT) + attempt_regeneration() + + var/next_aura_size = max(0.1, ( (stamina/max_stamina) * min(2, rating) ) /2) + var/next_aura_alpha = round(((suppressed ? max(0,rating - 2) : rating)/5)*255) + + if(next_aura_alpha != last_aura_alpha || next_aura_size != last_aura_size || aura_color != last_aura_color) + last_aura_size = next_aura_size + last_aura_alpha = next_aura_alpha + last_aura_color = aura_color + var/matrix/M = matrix() + if(next_aura_size != 1) + M.Scale(next_aura_size) + animate(get_aura_image(), alpha = next_aura_alpha, transform = M, color = aura_color, time = 3) + + if(update_hud) + ui.update_icon() + +/** + * Priority order is + * -Stopped heart (actively dying) + * -Regular damage (probably dying) + * -Wounds (might bleed out) + * -Tox/Clone/Rad (You're probably not hurt a different way when you need to heal this) + * -Organ damage (just keep us busy and topped off) + */ +/datum/psi_complexus/proc/attempt_regeneration() + + var/heal_paramount = FALSE + var/heal_poison = FALSE + var/heal_internal = FALSE + var/heal_rate = 0 + var/mend_prob = 0 + + switch(get_rank(PSI_REDACTION)) + if(PSI_RANK_PARAMOUNT) + heal_paramount = TRUE + heal_poison = TRUE + heal_internal = TRUE + mend_prob = 50 + heal_rate = 10 + if(PSI_RANK_GRANDMASTER) + heal_poison = TRUE + heal_internal = TRUE + mend_prob = 20 + heal_rate = 7 + if(PSI_RANK_MASTER) + heal_internal = TRUE + mend_prob = 10 + heal_rate = 4 + if(PSI_RANK_OPERANT) + mend_prob = 5 + heal_rate = 1 + else + return + + if(!heal_rate || stamina < heal_rate) + return // Don't backblast from trying to heal ourselves thanks. + + if(prob(mend_prob)) + // Fix our heart if we're paramount. + if(heal_paramount && ishuman(owner)) + var/mob/living/carbon/human/H = owner + var/obj/item/organ/heart/should_beat = H.getorganslot(ORGAN_SLOT_HEART) + if(should_beat && !should_beat.beating && spend_power(heal_rate)) + should_beat.Restart() + + // Heal actual damage + if((owner.getBruteLoss() || owner.getFireLoss() || owner.getOxyLoss()) && spend_power(heal_rate)) + owner.heal_ordered_damage(heal_rate, list(BRUTE, BURN, OXY), BODYPART_ANY) //it gets to heal robotic parts because otherwise it'd suck you dry trying to fix unfixable limbs + new /obj/effect/temp_visual/heal(get_turf(owner), "#33cc33") + if(prob(25)) + to_chat(owner, span_notice("Your skin crawls as your autoredactive faculty heals your body.")) + + // Repair wounds + if(ishuman(owner)) + var/mob/living/carbon/human/H = owner + for(var/datum/wound/W in H.all_wounds) + if(!spend_power(heal_rate)) + return + + if(W.blood_flow) + W.blood_flow -= (heal_rate * 0.5) + if(prob(25)) + to_chat(owner, span_notice("Your autoredactive faculty stems the flow of blood from your [W.limb].")) + return + + if(istype(W, /datum/wound/burn)) + var/datum/wound/burn/degree = W + degree.sanitization += (heal_rate * 0.5) + degree.flesh_healing += (heal_rate * 0.5) + if(prob(25)) + to_chat(owner, span_notice("Your autoredactive faculty cleans and mends the burn on your [W.limb].")) + return + + if(istype(W, /datum/wound/blunt)) + qdel(W) + playsound(H, 'sound/surgery/bone3.ogg', 25) + to_chat(owner, span_notice("Your autoredactive faculty snaps the bones in your [W.limb] back into place.")) + return + + // Heal radiation, cloneloss and poisoning. + if(heal_poison) + if(owner.getToxLoss() && spend_power(heal_rate)) + if(prob(25)) + to_chat(owner, span_notice("Your autoredactive faculty purges foreign toxins in your body.")) + owner.adjustToxLoss(heal_rate, TRUE, TRUE) + + if(owner.getCloneLoss() && spend_power(heal_rate)) + if(prob(25)) + to_chat(owner, span_notice("Your autoredactive faculty stitches together some of your mangled DNA.")) + owner.adjustCloneLoss(-(heal_rate/2)) + return + + if(owner.radiation && spend_power(heal_rate)) + if(prob(25)) + to_chat(owner, span_notice("Your autoredactive faculty repairs some of the radiation damage to your body.")) + owner.radiation = max(0, owner.radiation - (heal_rate * 5)) + return + + // Heal organ damage. + if(heal_internal && ishuman(owner)) + var/mob/living/carbon/human/H = owner + for(var/obj/item/organ/I in H.internal_organs) + + if(I.organ_flags & ORGAN_SYNTHETIC) + continue + + if(I.damage > 0 && spend_power(heal_rate)) + I.applyOrganDamage(-heal_rate) + if(prob(25)) + to_chat(H, span_notice("Your innards itch as your autoredactive faculty mends your [I.name].")) + return diff --git a/code/modules/psionics/equipment/cerebro_enhancers.dm b/code/modules/psionics/equipment/cerebro_enhancers.dm new file mode 100644 index 0000000000000..e63c258424451 --- /dev/null +++ b/code/modules/psionics/equipment/cerebro_enhancers.dm @@ -0,0 +1,146 @@ +//Psi-boosting item (antag only) +/obj/item/clothing/head/helmet/space/psi_amp + name = "cerebro-energetic enhancer" + desc = "A matte-black, eyeless cerebro-energetic enhancement helmet. It uses highly sophisticated, and illegal, techniques to drill into your brain and install psi-infected AIs into the fluid cavities between your lobes." + //actions_types = list(/datum/action/item_action/toggle_helmet_light) + icon_state = "cerebro" + + var/operating = FALSE + var/list/boosted_faculties + var/boosted_rank = PSI_RANK_PARAMOUNT + var/unboosted_rank = PSI_RANK_MASTER + var/max_boosted_faculties = 3 + var/boosted_psipower = 120 + var/paramount_check = FALSE + +/obj/item/clothing/head/helmet/space/psi_amp/verb/integrate_action() + set name = "Integrate" + set category = null + set src in usr + integrate() + +/obj/item/clothing/head/helmet/space/psi_amp/attack_self(mob/user) + + if(operating) + return + + var/mob/living/carbon/human/H = loc + if(istype(H) && H.head == src) + integrate() + return + + var/choice = input("Select a brainboard to install or remove.","Psionic Amplifier") as null|anything in SSpsi.faculties_by_name + if(!choice) + return + + var/removed + var/slots_left = max_boosted_faculties - LAZYLEN(boosted_faculties) + var/datum/psionic_faculty/faculty = SSpsi.get_faculty(choice) + if(faculty.id in boosted_faculties) + LAZYREMOVE(boosted_faculties, faculty.id) + removed = TRUE + else + if(slots_left <= 0) + to_chat(user, span_warning("There are no slots left to install brainboards into.")) + return + LAZYADD(boosted_faculties, faculty.id) + UNSETEMPTY(boosted_faculties) + + slots_left = max_boosted_faculties - LAZYLEN(boosted_faculties) + to_chat(user, span_notice("You [removed ? "remove" : "install"] the [choice] brainboard [removed ? "from" : "in"] \the [src]. There [slots_left!=1 ? "are" : "is"] [slots_left] slot\s left.")) + +/obj/item/clothing/head/helmet/space/psi_amp/AltClick(mob/user) + . = ..() + if(operating) + deintegrate() + else + integrate() + +/obj/item/clothing/head/helmet/space/psi_amp/proc/deintegrate() + if(operating) + return + + var/mob/living/carbon/human/H = loc + if(!istype(H)) + return + + to_chat(H, span_warning("You feel a strange tugging sensation as \the [src] begins removing the slave-minds from your brain...")) + playsound(H, 'sound/weapons/circsawhit.ogg', 50, 1, -1) + operating = TRUE + + sleep(80) + + if(H.psi) + H.psi.reset() + + to_chat(H, span_notice("\The [src] chimes quietly as it finishes removing the slave-minds from your brain.")) + + REMOVE_TRAIT(src, TRAIT_NODROP, TRAIT_GENERIC) + operating = FALSE + + set_light(0) + +/obj/item/clothing/head/helmet/space/psi_amp/Move() + var/lastloc = loc + . = ..() + if(.) + var/mob/living/carbon/human/H = lastloc + if(istype(H) && H.psi) + H.psi.reset() + H = loc + if(!istype(H) || H.head != src) + REMOVE_TRAIT(src, TRAIT_NODROP, TRAIT_GENERIC) + +/obj/item/clothing/head/helmet/space/psi_amp/proc/integrate() + if(operating) + return + + var/mob/living/carbon/human/H = loc + + if(!istype(H) || H.head != src) + to_chat(usr, span_warning("\The [src] must be worn on your head in order to be activated.")) + return + + if(LAZYLEN(boosted_faculties) < max_boosted_faculties) + to_chat(usr, span_notice("You still have [max_boosted_faculties - LAZYLEN(boosted_faculties)] facult[LAZYLEN(boosted_faculties) == 1 ? "y" : "ies"] to select. Use \the [src] in-hand to select them.")) + return + + ADD_TRAIT(src, TRAIT_NODROP, TRAIT_GENERIC) + operating = TRUE + to_chat(H, span_warning("You feel a series of sharp pinpricks as \the [src] anaesthetises your scalp before drilling down into your brain.")) + playsound(H, 'sound/weapons/circsawhit.ogg', 50, 1, -1) + + sleep(80) + + for(var/faculty in list(PSI_COERCION, PSI_PSYCHOKINESIS, PSI_REDACTION, PSI_ENERGISTICS)) + if(faculty in boosted_faculties) + H.set_psi_rank(faculty, boosted_rank, take_larger = TRUE, temporary = TRUE) + else + H.set_psi_rank(faculty, unboosted_rank, take_larger = TRUE, temporary = TRUE) + if(H.psi) + H.psi.max_stamina = boosted_psipower + H.psi.set_stamina(H.psi.max_stamina) + H.psi.update(force = TRUE) + + to_chat(H, span_notice("You experience a brief but powerful wave of deja vu as \the [src] finishes modifying your brain.")) + operating = FALSE + H.update_action_buttons() + + set_light(0.5, 0.1, 3, 2, l_color = "#880000") + +/obj/item/clothing/head/helmet/space/psi_amp/lesser + max_boosted_faculties = 1 + boosted_rank = PSI_RANK_MASTER + unboosted_rank = PSI_RANK_OPERANT + boosted_psipower = 50 + +/obj/item/clothing/head/helmet/space/psi_amp/lesser/crown + name = "psionic amplifier" + desc = "A crown-of-thorns cerebro-energetic enhancer that interfaces directly with the brain, isolating and strengthening psionic signals. It kind of looks like a tiara having sex with an industrial robot." + icon_state = "amp" + flags_cover = NONE + flags_inv = 0 + body_parts_covered = 0 + +/obj/item/clothing/head/helmet/space/psi_amp/paramount + paramount_check = TRUE diff --git a/code/modules/psionics/equipment/psipower.dm b/code/modules/psionics/equipment/psipower.dm new file mode 100644 index 0000000000000..23fe640d78783 --- /dev/null +++ b/code/modules/psionics/equipment/psipower.dm @@ -0,0 +1,39 @@ +/obj/item/psychic_power + name = "psychic power" + icon = 'icons/obj/psychic_powers.dmi' + anchored = TRUE + var/maintain_cost = 3 + var/mob/living/owner + item_flags = DROPDEL + +/obj/item/psychic_power/New(mob/living/L) + owner = L + if(!istype(owner)) + qdel(src) + return + START_PROCESSING(SSprocessing, src) + ..() + +/obj/item/psychic_power/Destroy() + if(istype(owner) && owner.psi) + LAZYREMOVE(owner.psi.manifested_items, src) + UNSETEMPTY(owner.psi.manifested_items) + STOP_PROCESSING(SSprocessing, src) + . = ..() + +/obj/item/psychic_power/throw_at(atom/target, range, speed, mob/thrower, spin, diagonals_first, datum/callback/callback, force, quickstart) + SEND_SOUND(thrower, sound('sound/effects/psi/power_fail.ogg', volume = 50)) + qdel(src) + +/obj/item/psychic_power/attack_self(mob/user) + SEND_SOUND(user, sound('sound/effects/psi/power_fail.ogg', volume = 50)) + user.dropItemToGround(src) + +/obj/item/psychic_power/process() + if(!owner || loc != owner || !(src in owner.held_items)) + if(ishuman(loc)) + var/mob/living/carbon/human/host = loc + host.remove_embedded_object(src) + qdel(src) + else if(istype(owner) && !owner?.psi?.spend_power(maintain_cost)) + qdel(src) diff --git a/code/modules/psionics/equipment/psipower_baton.dm b/code/modules/psionics/equipment/psipower_baton.dm new file mode 100644 index 0000000000000..94a4c4ecfd7e5 --- /dev/null +++ b/code/modules/psionics/equipment/psipower_baton.dm @@ -0,0 +1,50 @@ +/obj/item/melee/classic_baton/psibaton + name = "psychokinetic bash" + desc = "A psiokenetic truncheon for beating psycho scum." + force = 0 + stamina_damage = 10 + icon = 'icons/obj/psychic_powers.dmi' + icon_state = "psibaton" + item_state = "psibaton" + lefthand_file = 'icons/mob/inhands/weapons/melee_lefthand.dmi' + righthand_file = 'icons/mob/inhands/weapons/melee_righthand.dmi' + hitsound = 'sound/effects/psi/psisword.ogg' + drop_sound = 'sound/effects/psi/power_fail.ogg' + item_flags = DROPDEL + var/maintain_cost = 3 + var/mob/living/owner + +/obj/item/melee/classic_baton/psibaton/New(mob/living/L) + owner = L + if(!istype(owner)) + qdel(src) + return + START_PROCESSING(SSprocessing, src) + ..() + +/obj/item/melee/classic_baton/psibaton/Destroy() + if(istype(owner) && owner.psi) + LAZYREMOVE(owner.psi.manifested_items, src) + UNSETEMPTY(owner.psi.manifested_items) + STOP_PROCESSING(SSprocessing, src) + . = ..() + +/obj/item/melee/classic_baton/psibaton/throw_at(atom/target, range, speed, mob/thrower, spin, diagonals_first, datum/callback/callback, force, quickstart) + SEND_SOUND(thrower, sound('sound/effects/psi/power_fail.ogg', volume = 50)) + qdel(src) + +/obj/item/melee/classic_baton/psibaton/attack_self(mob/user) + SEND_SOUND(user, sound('sound/effects/psi/power_fail.ogg', volume = 50)) + user.dropItemToGround(src) + +/obj/item/melee/classic_baton/psibaton/process() + if(istype(owner)) + if(!owner?.psi?.spend_power(maintain_cost)) + qdel(src) + if(!owner || loc != owner || !(src in owner.held_items)) + if(ishuman(loc)) + var/mob/living/carbon/human/host = loc + host.remove_embedded_object(src) + host.dropItemToGround(src) + else + qdel(src) diff --git a/code/modules/psionics/equipment/psipower_blade.dm b/code/modules/psionics/equipment/psipower_blade.dm new file mode 100644 index 0000000000000..294ddaaf15759 --- /dev/null +++ b/code/modules/psionics/equipment/psipower_blade.dm @@ -0,0 +1,12 @@ +/obj/item/psychic_power/psiblade + name = "psychokinetic slash" + force = 10 + sharpness = SHARP_EDGED + icon_state = "psiblade_short" + item_state = "psiblade" + lefthand_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi' + righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi' + hitsound = 'sound/effects/psi/psisword.ogg' + var/can_break_wall = FALSE + var/wall_break_time = 6 SECONDS + drop_sound = 'sound/effects/psi/power_fail.ogg' diff --git a/code/modules/psionics/equipment/psipower_tinker.dm b/code/modules/psionics/equipment/psipower_tinker.dm new file mode 100644 index 0000000000000..93462ea86e9a8 --- /dev/null +++ b/code/modules/psionics/equipment/psipower_tinker.dm @@ -0,0 +1,30 @@ +/obj/item/psychic_power/tinker + name = "psychokinetic crowbar" + icon_state = "tinker" + force = 0 + tool_behaviour = TOOL_CROWBAR + usesound = 'sound/weapons/etherealhit.ogg' + var/list/possible_tools + +/obj/item/psychic_power/tinker/attack_self() + + if(!owner || loc != owner) + return + + var/list/choice_list = LAZYCOPY(possible_tools) + for(var/I as anything in choice_list) + choice_list[I] = image(icon, null, I) + var/choice = show_radial_menu(owner, owner, choice_list, null, 40, tooltips = TRUE) + + if(!choice) + return + + if(!owner || loc != owner) + return + + tool_behaviour = choice + name = "psychokinetic [tool_behaviour]" + icon_state = "[tool_behaviour]" + update_icon() + to_chat(owner, span_notice("You begin emulating \a [tool_behaviour].")) + owner.playsound_local(soundin = 'sound/effects/psi/power_fabrication.ogg') diff --git a/code/modules/psionics/equipment/psipower_tk.dm b/code/modules/psionics/equipment/psipower_tk.dm new file mode 100644 index 0000000000000..63db8d50ea711 --- /dev/null +++ b/code/modules/psionics/equipment/psipower_tk.dm @@ -0,0 +1,111 @@ +/obj/item/psychic_power/telekinesis + name = "telekinetic grip" + maintain_cost = 3 + icon_state = "telekinesis" + var/atom/movable/focus + +/obj/item/psychic_power/telekinesis/Destroy() + focus = null + . = ..() + +/obj/item/psychic_power/telekinesis/process() + if(!focus || !isturf(focus.loc) || !valid_distance(owner, focus)) + qdel(src) + else if(!owner || loc != owner || !(src in owner.held_items)) + if(ishuman(loc)) + var/mob/living/carbon/human/host = loc + host.remove_embedded_object(src) + qdel(src) + +/obj/item/psychic_power/telekinesis/proc/set_focus(atom/movable/_focus) + + if(!isturf(_focus.loc)) + return FALSE + + var/check_paramount + if(isliving(_focus)) + var/mob/living/victim = _focus + check_paramount = (victim.mob_size >= MOB_SIZE_HUMAN) + else if(isitem(_focus)) + var/obj/item/thing = _focus + check_paramount = (thing.w_class >= WEIGHT_CLASS_BULKY) + else + return FALSE + + if(_focus.anchored || (check_paramount && owner.psi.get_rank(PSI_PSYCHOKINESIS) < PSI_RANK_PARAMOUNT)) + focus = _focus + . = attack_self(owner) + if(!.) + to_chat(owner, span_warning("\The [_focus] is too hefty for you to get a mind-grip on.")) + qdel(src) + return FALSE + + focus = _focus + overlays.Cut() + var/image/I = image(icon = focus.icon, icon_state = focus.icon_state) + I.color = focus.color + I.overlays = focus.overlays + overlays += I + return TRUE + +/obj/item/psychic_power/telekinesis/proc/valid_distance(mob/living/user, atom/target) + var/distance = get_dist(get_turf(user), get_turf(focus ? focus : target)) + return (distance <= (owner.psi.get_rank(PSI_PSYCHOKINESIS) * 2)) + +/obj/item/psychic_power/telekinesis/attack_self(mob/user) + user.visible_message(span_notice("[user] makes a strange gesture.")) + sparkle() + return focus.do_simple_ranged_interaction(user) + +/obj/item/psychic_power/telekinesis/afterattack(atom/target, mob/living/user, proximity) + if(!target || !user || (isobj(target) && !isturf(target.loc))) + return + + if(!valid_distance(user, target)) + to_chat(user, span_warning("Your telekinetic power won't reach that far.")) + qdel(src) + return + + if(!user.psi || !user.psi.can_use()) + return + + if(!user.psi.spend_power(5)) + qdel(src) + return + + user.psi.set_cooldown(5) + + if(target == focus) + attack_self(user) + else + user.visible_message(span_danger("[user] gestures sharply!")) + sparkle() + if(!isturf(target) && istype(focus,/obj/item) && target.Adjacent(focus)) + var/obj/item/I = focus + var/resolved = target.attackby(I, user, user.zone_selected) + if(!resolved && target && I) + I.afterattack(target,user,1) // for splashing with beakers + else + if(!focus.anchored) + var/user_rank = owner.psi.get_rank(PSI_PSYCHOKINESIS) + focus.throw_at(target, user_rank*2, user_rank, owner, callback = CALLBACK(src, PROC_REF(end_throw))) + +/obj/item/psychic_power/telekinesis/proc/end_throw() + sparkle() + if(!focus || !isturf(focus.loc) || !valid_distance(owner, focus)) + qdel(src) + +/obj/item/psychic_power/telekinesis/proc/sparkle() + set waitfor = 0 + if(focus) + var/obj/effect/overlay/O = new /obj/effect/overlay(get_turf(focus)) + O.name = "sparkles" + O.anchored = 1 + O.density = 0 + O.layer = FLY_LAYER + //O.set_dir(pick(cardinal)) + O.icon = 'icons/effects/effects.dmi' + O.icon_state = "nothing" + flick("empdisable",O) + sleep(5) + qdel(O) diff --git a/code/modules/psionics/events/_psi.dm b/code/modules/psionics/events/_psi.dm new file mode 100644 index 0000000000000..99a9d7932c527 --- /dev/null +++ b/code/modules/psionics/events/_psi.dm @@ -0,0 +1,22 @@ +// /datum/round_event/psi +// startWhen = 30 +// endWhen = 120 + +// /datum/round_event/psi/announce() +// priority_announce( +// "A localized disruption within the neighboring psionic continua has been detected. All psi-operant crewmembers +// are advised to cease any sensitive activities and report to medical personnel in case of damage.", "Central Command Higher Dimensional Affairs") + +// /datum/round_event/psi/end() +// priority_announce( +// "The psi-disturbance has ended and baseline normality has been re-asserted. +// Anything you still can't cope with is therefore your own problem.", "Central Command Higher Dimensional Affairs") + +// /datum/round_event/psi/tick() +// for(var/thing in SSpsi.processing) +// if(!istype(thing, /datum/psi_complexus)) +// continue +// apply_psi_effect(thing) + +// /datum/round_event/psi/proc/apply_psi_effect(datum/psi_complexus/psi) +// return diff --git a/code/modules/psionics/events/mini_spasms.dm b/code/modules/psionics/events/mini_spasms.dm new file mode 100644 index 0000000000000..6c8bfe736d3b1 --- /dev/null +++ b/code/modules/psionics/events/mini_spasms.dm @@ -0,0 +1,73 @@ +// /datum/round_event_control/minispasm +// name = "Minispasms" +// typepath = /datum/round_event/minispasm +// weight = 8 +// max_occurrences = 1 +// earliest_start = 30 MINUTES + +// /datum/round_event/minispasm +// startWhen = 60 +// endWhen = 90 +// var/static/list/psi_operancy_messages = list( +// "There's something in your skull!", +// "Something is eating your thoughts!", +// "You can feel your brain being rewritten!", +// "Something is crawling over your frontal lobe!", +// "THE SIGNAL THE SIGNAL THE SIGNAL THE SIGNAL THE", +// "Something is drilling through your skull!", +// "Your head feels like it's going to implode!", +// "Thousands of ants are tunneling in your head!" +// ) + +// /datum/round_event/minispasm/announce() +// priority_announce( +// "PRIORITY ALERT: SIGMA-[rand(50,80)] NON-STANDARD PSIONIC SIGNAL-WAVE TRANSMISSION DETECTED - 97% MATCH, NON-VARIANT +// SIGNAL SOURCE TRIANGULATED TO DISTANT SITE: All personnel are advised to avoid +// exposure to active audio transmission equipment including radio headsets and intercoms +// for the duration of the signal broadcast.", +// "Central Command Higher Dimensional Affairs") + +// /datum/round_event/minispasm/start() +// var/list/victims = list() +// for(var/obj/item/radio/radio in world) +// if(radio.on) +// for(var/mob/living/victim in range(radio.canhear_range, radio.loc)) +// if(!isnull(victims[victim]) || victim.stat != CONSCIOUS || HAS_TRAIT(victim, TRAIT_DEAF)) +// continue +// victims[victim] = radio +// for(var/thing in victims) +// var/mob/living/victim = thing +// var/obj/item/radio/source = victims[victim] +// INVOKE_ASYNC(src, PROC_REF(do_spasm), victim, source) + +// /datum/round_event/minispasm/proc/do_spasm(mob/living/victim, obj/item/radio/source) +// if(HAS_TRAIT(src, TRAIT_PSIONICALLY_DEAFENED) || HAS_TRAIT(src, TRAIT_PSIONICALLY_IMMUNE)) +// return +// if(!victim.mind) +// return +// if(victim.psi) +// playsound(source, 'sound/creatures/narsie_rises.ogg', 75) //LOUD AS FUCK BOY +// to_chat(victim, span_danger("A hauntingly familiar sound hisses from \icon[source] \the [source], and your vision flickers!")) +// victim.psi.backblast(rand(5,15)) +// victim.Paralyze(0.5 SECONDS) +// victim.adjust_jitter(10 SECONDS) +// else +// victim.visible_message(span_danger("[victim] starts having a seizure!"), span_userdanger("An indescribable, brain-tearing sound hisses from \icon[source] \the [source], and you collapse in a seizure!")) +// victim.Unconscious(20 SECONDS) +// victim.adjust_jitter(1 SECONDS) +// SEND_SIGNAL(victim, COMSIG_ADD_MOOD_EVENT, "minispasm", /datum/mood_event/epilepsy) +// var/new_latencies = rand(1,2) +// var/list/faculties = list(PSI_COERCION, PSI_REDACTION, PSI_ENERGISTICS, PSI_PSYCHOKINESIS) +// for(var/i = 1 to new_latencies) +// to_chat(victim, span_danger("[pick(psi_operancy_messages)]")) +// victim.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(10,20)) +// victim.set_psi_rank(pick_n_take(faculties), PSI_RANK_LATENT) +// sleep(30) +// victim.psi.update() +// sleep(4.5 SECONDS) +// victim.psi.check_latency_trigger(100, "a psionic scream") + +// /datum/round_event/minispasm/end() +// priority_announce( +// "PRIORITY ALERT: SIGNAL BROADCAST HAS CEASED. Personnel are cleared to resume use of non-hardened radio transmission equipment. Have a nice day.", +// "Central Command Higher Dimensional Affairs") diff --git a/code/modules/psionics/events/psi_balm.dm b/code/modules/psionics/events/psi_balm.dm new file mode 100644 index 0000000000000..2ff3b3febd345 --- /dev/null +++ b/code/modules/psionics/events/psi_balm.dm @@ -0,0 +1,27 @@ +// /datum/round_event_control/balm +// name = "Psi Balm" +// typepath = /datum/round_event/psi/balm +// weight = 20 +// max_occurrences = 3 +// max_alert = SEC_LEVEL_DELTA + +// /datum/round_event/psi/balm +// var/static/list/balm_messages = list( +// "A soothing balm washes over your psyche.", +// "For a moment, you can hear a distant, familiar voice singing a quiet lullaby.", +// "A sense of peace and comfort falls over you like a warm blanket." +// ) + +// /datum/round_event/psi/balm/apply_psi_effect(datum/psi_complexus/psi) +// var/soothed +// if(psi.stun > 1) +// psi.stun-- +// soothed = TRUE +// else if(psi.stamina < psi.max_stamina) +// psi.adjust_stamina(rand(1,3)) +// soothed = TRUE +// else if(psi.owner.getOrganLoss(ORGAN_SLOT_BRAIN) > 0) +// psi.owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, -1) +// soothed = TRUE +// if(soothed && prob(10)) +// to_chat(psi.owner, span_notice("[pick(balm_messages)]")) diff --git a/code/modules/psionics/events/psi_wail.dm b/code/modules/psionics/events/psi_wail.dm new file mode 100644 index 0000000000000..7b64fc43cdc59 --- /dev/null +++ b/code/modules/psionics/events/psi_wail.dm @@ -0,0 +1,27 @@ +// /datum/round_event_control/wail +// name = "Psi Wail" +// typepath = /datum/round_event/psi/wail +// weight = 20 +// max_occurrences = 3 +// max_alert = SEC_LEVEL_DELTA + +// /datum/round_event/psi/wail +// var/static/list/whine_messages = list( +// "A nerve-tearing psychic whine intrudes on your thoughts.", +// "A horrible, distracting humming sound breaks your train of thought.", +// "Your head aches as a psychic wail intrudes on your psyche." +// ) + +// /datum/round_event/psi/wail/apply_psi_effect(datum/psi_complexus/psi) +// var/annoyed +// if(prob(1)) +// psi.stunned(1) +// annoyed = TRUE +// else if(prob(10)) +// psi.adjust_heat(rand(1,3)) +// annoyed = TRUE +// else if(psi.stamina) +// psi.adjust_stamina(-rand(1,3)) +// annoyed = TRUE +// if(annoyed && prob(1)) +// to_chat(psi.owner, span_notice("[pick(whine_messages)]")) diff --git a/code/modules/psionics/faculties/_faculty.dm b/code/modules/psionics/faculties/_faculty.dm new file mode 100644 index 0000000000000..28827b91f6a70 --- /dev/null +++ b/code/modules/psionics/faculties/_faculty.dm @@ -0,0 +1,11 @@ +/datum/psionic_faculty + var/id + var/name + var/associated_intent + var/list/armour_types = list() + var/list/powers = list() + +/datum/psionic_faculty/New() + ..() + for(var/atype in armour_types) + SSpsi.armour_faculty_by_type[atype] = id diff --git a/code/modules/psionics/faculties/_power.dm b/code/modules/psionics/faculties/_power.dm new file mode 100644 index 0000000000000..ab5ba1ac3d928 --- /dev/null +++ b/code/modules/psionics/faculties/_power.dm @@ -0,0 +1,63 @@ +/datum/psionic_power + /// Name. If null, psipower won't be generated on roundstart. + var/name + /// Associated psi faculty. + var/faculty + /// File to pull the ability icon from. + var/icon = 'icons/obj/psychic_powers.dmi' + /// Sprite of the ability itself. + var/icon_state = "base_power" + /// Minimum psi rank to use this power. + var/min_rank + /// Base psi stamina cost for using this power. + var/cost + /// Base heat gained for using this power. + var/heat + /// Deciseconds cooldown after using this power. + var/cooldown + /// Whether or not using this power prints an admin attack log. + var/admin_log = TRUE + /// A short description of how to use this power, shown via assay. + var/use_description + /// A sound effect to play when the power is used. + var/use_sound = 'sound/effects/psi/power_used.ogg' + +/datum/psionic_power/proc/invoke(mob/living/user, atom/target, proximity, parameters) + + if(!user.psi) + return FALSE + + if(faculty && min_rank) + var/user_rank = user.psi.get_rank(faculty) + if(user_rank < min_rank) + return FALSE + + if(isitem(target))//don't invoke if we're clicking in our inventory + var/obj/item/thing = target + if(thing in user.get_all_contents()) + return FALSE + + if(cost && !user.psi.spend_power(cost, heat)) + return FALSE + + var/user_psi_leech = user.do_psionics_check(cost, user) + if(user_psi_leech) + to_chat(user, span_warning("Your power is leeched away by \the [user_psi_leech] as fast as you can focus it...")) + return FALSE + + if(target.do_psionics_check(cost, user)) + to_chat(user, span_warning("Your power skates across \the [target], but cannot get a grip...")) + return FALSE + + return TRUE + +/datum/psionic_power/proc/handle_post_power(mob/living/user, atom/target) + if(cooldown) + user.psi.set_cooldown(cooldown) + if(admin_log && ismob(user) && ismob(target)) + log_attack("[user] Used psipower ([name]) on [target]") + if(use_sound) + playsound(user.loc, use_sound, 75) + +/datum/psionic_power/proc/on_select(mob/living/user) + return TRUE diff --git a/code/modules/psionics/faculties/coercion.dm b/code/modules/psionics/faculties/coercion.dm new file mode 100644 index 0000000000000..3c63fff33c33a --- /dev/null +++ b/code/modules/psionics/faculties/coercion.dm @@ -0,0 +1,321 @@ +#define COGMANIP_HYPNOTIZE "Hypnotize" +#define COGMANIP_ERASE_MEMORY "Erase Memory" +#define COGMANIP_THRALL "Thrall" + +/datum/psionic_faculty/coercion + id = PSI_COERCION + name = "Coercion" + +/datum/psionic_power/coercion + faculty = PSI_COERCION + +/datum/psionic_power/coercion/invoke(mob/living/user, mob/living/target, proximity, parameters) + if (!istype(target)) + to_chat(user, span_warning("You cannot mentally attack \the [target].")) + return FALSE + if(HAS_TRAIT(target, TRAIT_PSIONICALLY_IMMUNE)) + to_chat(user, span_warning("[target]'s unnatural anatomy refuses the psionic tampering")) + return FALSE + . = ..() + +/datum/psionic_power/coercion/commune + name = "Commune" + cost = 10 + cooldown = 5 SECONDS + min_rank = PSI_RANK_OPERANT + icon_state = "coe_commune" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click on a creature on to psionically send them a message." + +/datum/psionic_power/coercion/commune/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(!istype(target) || user == target) + return FALSE + . = ..() + if(.) + user.visible_message(span_notice("[user] touches their fingers to their temple.")) + var/text = pretty_filter(stripped_input(user, "What would you like to say?", "Speak to creature", null, null)) + + if(!text) + return + + if(target.stat == DEAD) + to_chat(user, span_cult("Not even a psion of your level can speak to the dead.")) + return TRUE + + if (issilicon(target)) + to_chat(user, span_warning("This can only be used on living organisms.")) + return TRUE + + log_say("[key_name(user)] communed to [key_name(target)]: [text]") + to_chat(user, span_warning("You succeed in sending your commune to [target]! Now to see if they listen...")) + + for (var/mob/M in GLOB.dead_mob_list) + to_chat(M,span_notice("[user] psionically says to [target]: [text]")) + + var/mob/living/carbon/human/H = target + to_chat(H, span_notice("You feel something crawl behind your eyes, hearing: [text]")) + if(istype(H)) + if(prob(10) && !(H.dna.species.species_traits & NOBLOOD)) + to_chat(H, span_warning("Your nose begins to bleed...")) + H.add_splatter_floor(small_drip = TRUE) + else if(prob(25)) + to_chat(H, span_warning("Your head hurts...")) + else if(prob(50)) + to_chat(H, span_warning("Your mind buzzes...")) + +/datum/psionic_power/coercion/assay + name = "Assay" + cost = 15 + cooldown = 5 SECONDS + min_rank = PSI_RANK_OPERANT + icon_state = "coe_assay" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click on a target in order to perform a deep coercive-redactive probe of their psionic potential." + +/datum/psionic_power/coercion/assay/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(!istype(target) || user == target) + return FALSE + . = ..() + if(.) + user.visible_message(span_warning("\The [user] taps into the the head of \the [target]...")) + to_chat(user, span_notice("You insinuate your mentality into that of \the [target]...")) + to_chat(target, span_warning("Your persona is being probed by the psychic lens of \the [user].")) + var/speed = (4 - (user.psi.get_rank(PSI_COERCION) - 1)) SECONDS + if(!do_after(user, speed, target, FALSE)) + user.psi.backblast(rand(5,10)) + return TRUE + to_chat(user, span_notice("You retreat from \the [target], holding your new knowledge close.")) + to_chat(target, span_danger("Your mental complexus is laid bare to judgement of \the [user].")) + target.show_psi_assay(user) + return TRUE + +// /datum/psionic_power/coercion/psiping +// name = "Psi-ping" +// cost = 50 +// heat = 20 +// cooldown = 20 SECONDS +// min_rank = PSI_RANK_OPERANT +// icon_state = "coe_psiping" +// use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click on yourself with an empty hand to detect nearby psionic signatures." +// var/searching = FALSE + +// /datum/psionic_power/coercion/psiping/invoke(mob/living/user, mob/living/target, proximity, parameters) +// if(user != target || searching) +// return FALSE +// . = ..() +// if(.) +// to_chat(user, span_notice("You take a moment to tune into the local Nlom...")) +// searching = TRUE +// if(!do_after(user, 3 SECONDS, user)) +// searching = FALSE +// return FALSE +// searching = FALSE +// var/list/dirs = list() +// for(var/mob/living/L in GLOB.mob_living_list) +// var/turf/T = get_turf(L) +// if(!T || L == user || L.stat == DEAD || issilicon(L) || !(L.psi || isdarkspawn(L)) || (L.z != user.z)) +// continue +// /* +// var/image/ping_image = image(icon = 'icons/effects/effects.dmi', icon_state = "sonar_ping", loc = user) +// ping_image.plane = LIGHTING_LAYER+1 +// ping_image.layer = LIGHTING_LAYER+1 +// ping_image.pixel_x = (T.x - user.x) * 32 +// ping_image.pixel_y = (T.y - user.y) * 32 +// user << ping_image +// addtimer(CALLBACK(GLOBAL_PROC, /proc/qdel, ping_image), 8) +// */ +// var/direction = num2text(angle2dir(Get_Angle(user, L))) +// var/dist +// if(text2num(direction)) +// switch(get_dist(user, L)) +// if(0 to 10) +// dist = "very close" +// if(10 to 20) +// dist = "close" +// if(20 to 30) +// dist = "a little ways away" +// if(30 to 40) +// dist = "farther away" +// else +// dist = "far away" +// else +// dist = "on top of you" +// LAZYINITLIST(dirs[direction]) +// dirs[direction][dist] += 1 +// if(length(dirs)) +// var/list/feedback = list() +// feedback += "You sense..." +// for(var/d in dirs) +// feedback += "[capitalize(dir2text(text2num(d)))]:" +// for(var/dst in dirs[d]) +// feedback += "[dirs[d][dst]] psionic signature\s [dst]." + +// to_chat(user, span_notice(feedback.Join("
"))) +// else +// to_chat(user, span_notice("You detect no psionic signatures but your own.")) +// return TRUE + +/datum/psionic_power/coercion/agony + name = "Agony" + cost = 20 + heat = 20 + cooldown = 2 SECONDS + min_rank = PSI_RANK_OPERANT + icon_state = "coe_agony" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, attack someone while in combat mode to deal minor stamina damage. Higher psi levels augment the damage done." + +/datum/psionic_power/coercion/agony/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(!istype(target) || !proximity || user == target || !user.combat_mode) + return FALSE + . = ..() + if(.) + user.do_attack_animation(target, ATTACK_EFFECT_PUNCH) + user.visible_message("\The [target] has been struck by \the [user]!") + playsound(user.loc, 'sound/weapons/Egloves.ogg', 50, 1, -1) + target.apply_damage(20 * (user.psi.get_rank(PSI_COERCION) - 1), STAMINA, BODY_ZONE_CHEST) + return TRUE + +/datum/psionic_power/coercion/spasm + name = "Spasm" + cost = 15 + heat = 10 + cooldown = 10 SECONDS + min_rank = PSI_RANK_MASTER + icon_state = "coe_spasm" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target a creature to use a ranged attack that may rip the weapons away from the target." + +/datum/psionic_power/coercion/spasm/invoke(mob/living/user, mob/living/carbon/human/target, proximity, parameters) + if(!istype(target) || user == target || !user.combat_mode) + return FALSE + . = ..() + + if(.) + to_chat(user, span_danger("You lash out, stabbing into \the [target] with a lance of psi-power.")) + to_chat(target, span_danger("The muscles in your arms cramp horrendously!")) + if(prob(75)) + target.emote("scream") + if(prob(75) && target.held_items[1] && target.dropItemToGround(target.get_item_for_held_index(1))) + target.visible_message(span_danger("\The [target] drops what they were holding as their left hand spasms!")) + if(prob(75) && target.held_items[2] && target.dropItemToGround(target.get_item_for_held_index(2))) + target.visible_message(span_danger("\The [target] drops what they were holding as their right hand spasms!")) + return TRUE + +/datum/psionic_power/coercion/focus + name = "Focus" + cost = 10 + cooldown = 8 SECONDS + min_rank = PSI_RANK_MASTER + icon_state = "coe_focus" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click on someone in order to cure ailments of the mind." + +/datum/psionic_power/coercion/focus/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(!istype(target) || !proximity || user == target) + return FALSE + . = ..() + if(.) + user.visible_message(span_warning("\The [user] holds the head of \the [target] in both hands...")) + to_chat(user, span_notice("You probe \the [target]'s mind for various ailments..")) + to_chat(target, span_warning("Your mind is being cleansed of ailments by \the [user].")) + if(!do_after(user, (target.stat == CONSCIOUS ? 4 SECONDS : 2 SECONDS), target, FALSE)) + user.psi.backblast(rand(5,10)) + return TRUE + to_chat(user, span_warning("You clear \the [target]'s mind of ailments.")) + to_chat(target, span_warning("Your mind is cleared of ailments.")) + + var/resilience = TRAUMA_RESILIENCE_BASIC + var/coercion_rank = user.psi.get_rank(PSI_COERCION) + if(coercion_rank >= PSI_RANK_GRANDMASTER) + target.SetParalyzed(0) + resilience = TRAUMA_RESILIENCE_SURGERY + if(coercion_rank >= PSI_RANK_PARAMOUNT) + target.SetAllImmobility(0) + resilience = TRAUMA_RESILIENCE_LOBOTOMY + + target.SetDaze(0) + if(istype(target, /mob/living/carbon)) + var/mob/living/carbon/M = target + M.cure_trauma_type(resilience = resilience) + M.adjust_hallucinations(10 SECONDS) + return TRUE + +/datum/psionic_power/coercion/mindread + name = "Read Mind" + cost = 25 + heat = 15 + cooldown = 25 SECONDS //It should take a WHILE to be able to use this again. + min_rank = PSI_RANK_MASTER + icon_state = "coe_mindread" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click on someone in melee range to attempt to read a surface level thought." + +/datum/psionic_power/coercion/mindread/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(!istype(target) || target == user || !proximity) + return FALSE + . = ..() + if(!.) + return + + if(target.stat == DEAD || (HAS_TRAIT(target, TRAIT_FAKEDEATH)) || !target.client) + to_chat(user, span_warning("\The [target] is in no state for a mind-read.")) + return FALSE + + user.visible_message(span_warning("\The [user] touches \the [target]'s temple...")) + var/question = input(user, "Say something?", "Read Mind", "Penny for your thoughts?") as null|text + if(!question || user.incapacitated() || !do_after(user, 20)) + return TRUE + + var/started_mindread = world.time + to_chat(user, span_notice("You dip your mentality into the surface layer of \the [target]'s mind, seeking an answer: [question]")) + to_chat(target, span_hypnophrase("Your mind is compelled to answer: [question]")) // I wonder how this will go down with the playerbase + + var/answer = input(target, question, "Read Mind") as null|text + if(!answer || world.time > started_mindread + 25 SECONDS || user.stat != CONSCIOUS || target.stat == DEAD) + to_chat(user, span_notice("You receive nothing useful from \the [target].")) + else + to_chat(user, span_notice("You skim thoughts from the surface of \the [target]'s mind: [answer]")) + log_game("[key_name(user)] read mind of [key_name(target)] with question \"[question]\" and [answer? "got answer \"[answer]\".":"got no answer."]") + return TRUE + +/datum/psionic_power/coercion/blindstrike + name = "Blindstrike" + cost = 8 + heat = 15 + cooldown = 10 SECONDS + min_rank = PSI_RANK_GRANDMASTER + icon_state = "coe_blindstrike" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click anywhere to use a radial attack that blinds, deafens and disorients everyone near you." + +/datum/psionic_power/coercion/blindstrike/invoke(mob/living/user, mob/living/target, proximity, parameters) + . = ..() + if(.) + user.visible_message(span_danger("\The [user] suddenly throws back their head, as though screaming silently!"), span_danger("You strike at all around you with a deafening psionic scream!")) + for(var/mob/living/M in orange(user, user.psi.get_rank(PSI_COERCION))) + if(M == user) + continue + if(HAS_TRAIT(M, TRAIT_PSIONICALLY_IMMUNE)) + continue + M.emote("scream") + to_chat(M, span_danger("Your senses are blasted into oblivion by a psionic scream!")) + M.blind_eyes(1 SECONDS) + M.adjust_confusion(10 SECONDS) + return TRUE + +/datum/psionic_power/coercion/dis_arm + name = "Dis-Arm" + cost = 40 + heat = 50 + cooldown = 100 SECONDS + min_rank = PSI_RANK_PARAMOUNT + icon_state = "coe_disarm" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then click your target with combat mode to Psionically rip their arms off." + +/datum/psionic_power/coercion/dis_arm/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(!user.combat_mode || user == target) + return FALSE + . = ..() + if(.) + user.visible_message(span_danger("\The [user] grows two psionic arms, ripping [target]'s arms off!")) + to_chat(user, span_danger("You channel your full mental might into ripping and tearing!")) + var/mob/living/carbon/CM = target + for(var/obj/item/bodypart/bodypart in CM.bodyparts) + if(!(bodypart.body_part & (HEAD|CHEST|LEGS))) + if(bodypart.dismemberable) + bodypart.dismember() + return TRUE diff --git a/code/modules/psionics/faculties/energistics.dm b/code/modules/psionics/faculties/energistics.dm new file mode 100644 index 0000000000000..038bdd2398cdf --- /dev/null +++ b/code/modules/psionics/faculties/energistics.dm @@ -0,0 +1,126 @@ +/datum/psionic_faculty/energistics + id = PSI_ENERGISTICS + name = "Energistics" + armour_types = list(BOMB, LASER, ENERGY, FIRE) + +/datum/psionic_power/energistics + faculty = PSI_ENERGISTICS + +/datum/psionic_power/energistics/electrocute + name = "Electrocute" + cost = 10 + heat = 30 + cooldown = 7.5 SECONDS + min_rank = PSI_RANK_OPERANT + icon_state = "ene_ele" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, in an empty, then Enter combat mode to use a melee attack that electrocutes a victim, or charge an energy cell." + +/datum/psionic_power/energistics/electrocute/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(!user.combat_mode || !istype(target) || !proximity) + return FALSE + . = ..() + if(.) + var/psyrank = user.psi.get_rank(PSI_ENERGISTICS) + if(istype(target)) + user.visible_message(span_danger("\The [user] sends a jolt of electricity arcing into \the [target]!")) + target.electrocute_act(rand(psyrank * 5, psyrank * 10), user, 1, user.zone_selected, stun = (psyrank >= PSI_RANK_PARAMOUNT)) + return TRUE + else if(isatom(target)) + var/obj/item/stock_parts/cell/charging_cell = target.get_cell() + if(istype(charging_cell)) + user.visible_message(span_danger("\The [user] sends a jolt of electricity arcing into \the [target], charging it!")) + charging_cell.give(rand(psyrank * 5, psyrank * 10)) + return TRUE + else + return FALSE + +/datum/psionic_power/energistics/spark + name = "Spark" + cost = 1 + cooldown = 1 SECONDS + min_rank = PSI_RANK_OPERANT + icon_state = "ene_spark" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target a non-living thing in melee range with combat mode on to cause some sparks to appear. This can light fires." + +/datum/psionic_power/energistics/spark/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(!user.combat_mode || isnull(target) || istype(target) || !proximity) + return FALSE + . = ..() + if(.) + if(istype(target,/obj/item/clothing/mask/cigarette)) + var/obj/item/clothing/mask/cigarette/S = target + S.light("[user] snaps \his fingers and \the [S.name] lights up.") + user.emote("snap") + playsound(S.loc, "sparks", 50, 1) + else + var/datum/effect_system/spark_spread/s = new /datum/effect_system/spark_spread + s.set_up(user.psi.get_rank(PSI_ENERGISTICS), 1, target) + s.start() + return TRUE + +/datum/psionic_power/energistics/zorch + name = "Zorch" + cost = 15 + heat = 15 + cooldown = 2 SECONDS + min_rank = PSI_RANK_MASTER + icon_state = "ene_zorch" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then use this ranged laser attack with combat mode on. Your mastery of Energistics will determine how powerful the laser is. Be wary of overuse, and try not to fry your own brain." + +/datum/psionic_power/energistics/zorch/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(!user.combat_mode) + return FALSE + . = ..() + if(.) + if(HAS_TRAIT(user, TRAIT_PACIFISM) && user.psi.zorch_harm) + to_chat(user, span_notice("You manage to stop yourself before firing a harmful laser from your eyes, you don't want to risk harming anyone...")) + + var/user_rank = user.psi.get_rank(faculty) + var/obj/projectile/pew + var/pew_sound + + if(user.psi.zorch_harm) + pew = new /obj/projectile/beam/laser(get_turf(user)) + else + pew = new /obj/projectile/beam/disabler(get_turf(user)) + + switch(user_rank) + if(PSI_RANK_PARAMOUNT) + pew.damage = 30 + pew.name = "gigawatt mental laser" + pew_sound = 'sound/weapons/lasercannonfire.ogg' + if(PSI_RANK_GRANDMASTER) + pew.damage = 20 + pew.name = "megawatt mental laser" + pew_sound = 'sound/weapons/Laser.ogg' + if(PSI_RANK_MASTER) + pew.damage = 10 + pew.name = "mental laser" + pew_sound = 'sound/weapons/Taser.ogg' + + if(istype(pew)) + playsound(pew.loc, pew_sound, 25, 1) + pew.original = target + pew.starting = get_turf(user) + pew.firer = user + pew.fire(Get_Angle(user, target)) + user.visible_message(span_danger("[user]'s eyes flare with light!")) + return TRUE + +/datum/psionic_power/energistics/disrupt + name = "Disrupt" + cost = 20 + heat = 20 + cooldown = 10 SECONDS + min_rank = PSI_RANK_GRANDMASTER + icon_state = "ene_disrupt" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then attack a target while in combat mode to cause a localized electromagnetic pulse." + +/datum/psionic_power/energistics/disrupt/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(!user.combat_mode || !istype(target) || !proximity) + return FALSE + . = ..() + if(.) + user.visible_message("\The [user] releases a gout of crackling static and arcing lightning over \the [target]!") + empulse(target, 5, 1) + return TRUE diff --git a/code/modules/psionics/faculties/psychokenisis.dm b/code/modules/psionics/faculties/psychokenisis.dm new file mode 100644 index 0000000000000..e427672ad5662 --- /dev/null +++ b/code/modules/psionics/faculties/psychokenisis.dm @@ -0,0 +1,135 @@ +/datum/psionic_faculty/psychokinesis + id = PSI_PSYCHOKINESIS + name = "Psychokinesis" + armour_types = list(MELEE, BULLET, WOUND) + +/datum/psionic_power/psychokinesis + faculty = PSI_PSYCHOKINESIS + use_sound = null + +/datum/psionic_power/psychokinesis/psiblade + name = "Psiblade/Psibaton" + cost = 10 + cooldown = 3 SECONDS + min_rank = PSI_RANK_OPERANT + icon_state = "psy_blade" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, summon a psiblade, or psibaton if the user is a pacifist. The power the blade/baton will vary based on your mastery of the faculty." + use_sound = 'sound/effects/psi/power_fabrication.ogg' + admin_log = FALSE + +/datum/psionic_power/psychokinesis/psiblade/invoke(mob/living/user, mob/living/target, proximity, parameters) + return FALSE + +/datum/psionic_power/psychokinesis/psiblade/on_select(mob/living/user) + . = ..() + if(.) + playsound(user.loc, use_sound, 75) + if(HAS_TRAIT(user, TRAIT_PACIFISM)) + var/obj/item/melee/classic_baton/psibaton/baton = new /obj/item/melee/classic_baton/psibaton(user, user) + user.put_in_hands(baton) + switch(user.psi.get_rank(faculty)) + if(PSI_RANK_PARAMOUNT) + baton.stamina_damage = 50 + if(PSI_RANK_GRANDMASTER) + baton.stamina_damage = 40 + if(PSI_RANK_MASTER) + baton.stamina_damage = 25 + else + baton.stamina_damage = 15 + return baton + else + var/obj/item/psychic_power/psiblade/blade = new /obj/item/psychic_power/psiblade(user, user) + user.put_in_hands(blade) + switch(user.psi.get_rank(faculty)) + if(PSI_RANK_PARAMOUNT) + blade.can_break_wall = TRUE + blade.wall_break_time = 3 SECONDS + blade.force = 40 + blade.armour_penetration = 30 + blade.AddComponent(/datum/component/cleave_attack, arc_size=180, requires_wielded=TRUE) + if(PSI_RANK_GRANDMASTER) + blade.can_break_wall = TRUE + blade.force = 24 + blade.armour_penetration = 30 + blade.AddComponent(/datum/component/cleave_attack, arc_size=180, requires_wielded=TRUE) + if(PSI_RANK_MASTER) + blade.force = 18 + else + blade.force = 12 + return blade + +/datum/psionic_power/psychokinesis/tinker + name = "Tinker" + cost = 5 + cooldown = 10 + min_rank = PSI_RANK_OPERANT + icon_state = "psy_tinker" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, Use it in-hand to switch between tool types, different tools are available at different psi levels." + use_sound = 'sound/effects/psi/power_fabrication.ogg' + admin_log = FALSE + +/datum/psionic_power/psychokinesis/tinker/invoke(mob/living/user, mob/living/target, proximity, parameters) + return FALSE + +/datum/psionic_power/psychokinesis/tinker/on_select(mob/living/user) + . = ..() + if(.) + playsound(user.loc, use_sound, 75) + var/obj/item/psychic_power/tinker/tool = new(user) + user.put_in_hands(tool) + switch(user.psi.get_rank(faculty)) + if(PSI_RANK_PARAMOUNT) + tool.possible_tools = list(TOOL_SCREWDRIVER, TOOL_CROWBAR, TOOL_WRENCH, TOOL_WIRECUTTER, TOOL_WELDER, TOOL_MULTITOOL, TOOL_SCALPEL, TOOL_HEMOSTAT, TOOL_RETRACTOR, TOOL_CAUTERY, TOOL_SAW, TOOL_DRILL, TOOL_BONESET, TOOL_MINING, TOOL_SHOVEL, TOOL_HATCHET) + tool.toolspeed = 0.25 + if(PSI_RANK_GRANDMASTER) + tool.possible_tools = list(TOOL_SCREWDRIVER, TOOL_CROWBAR, TOOL_WRENCH, TOOL_WIRECUTTER, TOOL_SCALPEL, TOOL_HEMOSTAT, TOOL_RETRACTOR, TOOL_CAUTERY, TOOL_SAW, TOOL_DRILL, TOOL_MINING, TOOL_SHOVEL, TOOL_HATCHET) + tool.toolspeed = 0.5 + if(PSI_RANK_MASTER) + tool.possible_tools = list(TOOL_SCREWDRIVER, TOOL_CROWBAR, TOOL_WRENCH, TOOL_WIRECUTTER, TOOL_SCALPEL, TOOL_HEMOSTAT, TOOL_CAUTERY, TOOL_MINING, TOOL_SHOVEL, TOOL_HATCHET) + tool.toolspeed = 1 + if(PSI_RANK_OPERANT) + tool.possible_tools = list(TOOL_SCREWDRIVER, TOOL_CROWBAR, TOOL_WRENCH, TOOL_MINING, TOOL_SHOVEL) + tool.toolspeed = 1.5 + return tool + +/datum/psionic_power/psychokinesis/telekinesis + name = "Telekinesis" + cost = 5 + cooldown = 1 SECONDS + min_rank = PSI_RANK_MASTER + icon_state = "psy_tele" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, to manifest a psychokinetic grip. Use it manipulate objects at a distance." + admin_log = FALSE + use_sound = 'sound/effects/psi/power_used.ogg' + var/list/valid_types = list( //a list of all + /obj/machinery/door, + /obj/structure/window, + /obj/structure/closet, + /obj/structure/chair + ) + +/datum/psionic_power/psychokinesis/telekinesis/New() + . = ..() + valid_types = typecacheof(valid_types) + +/datum/psionic_power/psychokinesis/telekinesis/invoke(mob/living/user, atom/target, proximity, parameters) + var/distance = get_dist(user, target) + if(distance > (user.psi.get_rank(PSI_PSYCHOKINESIS) * 2)) + to_chat(user, span_warning("Your telekinetic power won't reach that far.")) + return FALSE + if((istype(target, /obj/machinery) || istype(target, /obj/structure)) && !is_type_in_typecache(target, valid_types)) + return FALSE + . = ..() + if(.) + if(istype(target, /obj/structure) || istype(target, /obj/machinery)) + user.visible_message(span_notice("\The [user] makes a strange gesture.")) + user.UnarmedAttack(target, TRUE) + return TRUE + else if(istype(target, /mob) || istype(target, /obj)) + var/obj/item/psychic_power/telekinesis/tk = new(user) + user.put_in_hands(tk) + if(tk.set_focus(target)) + tk.sparkle() + user.visible_message(span_notice("\The [user] reaches out.")) + return TRUE + return FALSE diff --git a/code/modules/psionics/faculties/redaction.dm b/code/modules/psionics/faculties/redaction.dm new file mode 100644 index 0000000000000..bcea5c91dc16c --- /dev/null +++ b/code/modules/psionics/faculties/redaction.dm @@ -0,0 +1,186 @@ +/datum/psionic_faculty/redaction + id = PSI_REDACTION + name = "Redaction" + armour_types = list(BIO, RAD, ACID) + +/datum/psionic_power/redaction + faculty = PSI_REDACTION + admin_log = FALSE + +/datum/psionic_power/redaction/proc/check_dead(mob/living/target) + if(!istype(target)) + return FALSE + if(target.stat == DEAD || HAS_TRAIT(target, TRAIT_FAKEDEATH)) + return TRUE + return FALSE + +/datum/psionic_power/redaction/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(HAS_TRAIT(target, TRAIT_PSIONICALLY_IMMUNE)) + to_chat(user, span_warning("[target]'s unnatural anatomy refuses the psionic tampering")) + return FALSE + if(check_dead(target)) + return FALSE + . = ..() + +/datum/psionic_power/redaction/skinsight + name = "Skinsight" + cost = 3 + heat = 1 + cooldown = 3 SECONDS + min_rank = PSI_RANK_OPERANT + icon_state = "redac_skinsight" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target the mob you wish to scan with combat mode off. Higher psi levels provide more information." + +/datum/psionic_power/redaction/skinsight/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(user.combat_mode || !istype(target) || !proximity) + return FALSE + . = ..() + if(.) + user.visible_message(span_notice("\The [user] rests a hand on \the [target].")) + healthscan(user, target, user.psi.get_rank(PSI_REDACTION) >= PSI_RANK_GRANDMASTER) + return TRUE + +/datum/psionic_power/redaction/mend + name = "Mend" + cost = 7 + heat = 10 + cooldown = 5 SECONDS + min_rank = PSI_RANK_OPERANT + icon_state = "redac_mend" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target the mob you wish to heal with combat mode off. Higher psi levels provide further healing." + +/datum/psionic_power/redaction/mend/invoke(mob/living/user, mob/living/carbon/human/target, proximity, parameters) + if(user.combat_mode || !istype(target) || !proximity || !..()) + return FALSE + + user.visible_message(span_notice("\The [user] rests a hand on \the [target]...")) + to_chat(target, span_notice("A healing warmth suffuses you.")) + new /obj/effect/temp_visual/heal(get_turf(target), "#33cc33") + + var/pk_rank = user.psi.get_rank(PSI_PSYCHOKINESIS) + var/redaction_rank = user.psi.get_rank(PSI_REDACTION) + + + if(pk_rank >= PSI_RANK_LATENT && redaction_rank >= PSI_RANK_MASTER) //realistically, not likely to happen, and not even that powerful + var/removal_size = clamp(5-pk_rank, 0, 5) + var/list/embedded_list = list() + var/obj/item/bodypart/body_part + for(var/obj/item/bodypart/part in target.bodyparts) + for(var/obj/item/embedded in part.embedded_objects) + if(embedded.w_class >= removal_size) + embedded_list += embedded + if(LAZYLEN(embedded_list)) + var/removed_item = pick(embedded_list) + body_part = target.get_embedded_part(removed_item) + target.remove_embedded_object(removed_item, get_turf(target)) + to_chat(user, span_notice("You extend a tendril of psychokinetic-redactive power and carefully tease \the [removed_item] free of [target]'s [body_part].")) + return TRUE + + if(target.heal_ordered_damage(redaction_rank * 10, list(BRUTE, BURN)) > 0) //this returns a number greater than 0 if it does any healing, we've already spent the psi, so we can afford to do this + to_chat(user, span_notice("You patch up some of the damage to [target].")) + return TRUE + + if(redaction_rank >= PSI_RANK_GRANDMASTER) + // Repair wounds + if(ishuman(target)) + var/mob/living/carbon/human/H = target + for(var/datum/wound/W in H.all_wounds) + if(W.blood_flow) + W.blood_flow -= (redaction_rank * 0.5) + if(prob(25)) + to_chat(user, span_notice("You stem the flow of blood streaming from [target]'s [W.limb].")) + return TRUE + + if(istype(W, /datum/wound/burn)) + var/datum/wound/burn/degree = W + degree.sanitization += (redaction_rank * 0.5) + degree.flesh_healing += (redaction_rank * 0.5) + if(prob(25)) + to_chat(user, span_notice("You clean and mend the burns on [target]'s [W.limb].")) + return TRUE + + if(istype(W, /datum/wound/blunt)) + qdel(W) + playsound(H, 'sound/surgery/bone3.ogg', 25) + to_chat(user, span_notice("You snap the bones in [target]'s [W.limb] back into place.")) + return TRUE + + // Repair internal organs + for(var/obj/item/organ/O in target.internal_organs) + if(O.damage > 0) + var/heal = redaction_rank * 10 + to_chat(user, span_notice("You encourage the damaged tissues of \the [O] to repair itself.")) + O.applyOrganDamage(-rand(heal, heal * 2)) + return TRUE + + to_chat(user, span_notice("You can find nothing within \the [target] to mend.")) + return FALSE + +/datum/psionic_power/revive + name = "Revive" + cost = 25 + heat = 100 + cooldown = 8 SECONDS + min_rank = PSI_RANK_OPERANT + faculty = PSI_REDACTION + icon_state = "redac_revive" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target the mob you wish to revive with combat mode off. Has several limiting factors. Higher psi levels upgrade the revive." + admin_log = FALSE + +/datum/psionic_power/revive/invoke(mob/living/user, mob/living/target, proximity, parameters) + if(user.combat_mode || !istype(target) || !proximity || isipc(target)) + return FALSE + . = ..() + if(.) + if(target.stat != DEAD && !HAS_TRAIT(target, TRAIT_FAKEDEATH)) + to_chat(user, span_warning("This person is already alive!")) + return TRUE + + var/is_paramount = user.psi.get_rank(PSI_REDACTION) >= PSI_RANK_PARAMOUNT + + if(!is_paramount && (world.time - target.timeofdeath) > DEFIB_TIME_LIMIT) + to_chat(user, span_warning("\The [target] has been dead for too long to revive.")) + return TRUE + + user.visible_message(span_notice("\The [user] splays out their hands over \the [target]'s body...")) + target.notify_ghost_cloning("Your heart is being revived!") + target.grab_ghost() + if(!do_after(user, 10 SECONDS, target, FALSE)) + user.psi.backblast(rand(10,25)) + return TRUE + + to_chat(target, span_notice("Life floods back into your body!")) + target.visible_message(span_notice("\The [target] shudders violently!")) + target.adjustOxyLoss(-rand(15,20)) + target.revive(is_paramount) + return TRUE + +/datum/psionic_power/redaction/cleanse + name = "Cleanse" + cost = 9 + heat = 15 + cooldown = 6 SECONDS + min_rank = PSI_RANK_MASTER + icon_state = "redac_cleanse" + use_description = "Activate the power with the 'use' key (initially bound to Z) in an empty hand, then target the mob you wish cleanse with combat mode off. Cleanses radiation, clone damage, and toxins. Higher psi levels provide further cleansing." + +/datum/psionic_power/redaction/cleanse/invoke(mob/living/user, mob/living/carbon/human/target, proximity, parameters) + if(user.combat_mode || !istype(target) || !proximity) + return FALSE + . = ..() + if(.) + var/removing = (user.psi.get_rank(PSI_REDACTION) - 1) * 10 + if(target.radiation) + to_chat(user, span_notice("You repair some of the radiation-damaged tissue within \the [target]...")) + target.radiation = max(target.radiation - removing, 0) + return TRUE + if(target.getCloneLoss()) + to_chat(user, span_notice("You stitch together some of the mangled DNA within \the [target]...")) + target.adjustCloneLoss(-removing) + return TRUE + if(target.getToxLoss()) + to_chat(user, span_notice("You expunge some of the toxins within \the [target]...")) + target.adjustToxLoss(-removing, TRUE, TRUE) + return TRUE + to_chat(user, span_notice("You can find no impurities to cleanse from \the [target].")) + return TRUE diff --git a/code/modules/psionics/interfaces/ui.dm b/code/modules/psionics/interfaces/ui.dm new file mode 100644 index 0000000000000..1285f4c9aa13d --- /dev/null +++ b/code/modules/psionics/interfaces/ui.dm @@ -0,0 +1,24 @@ +/obj/screen/psi + icon = 'icons/mob/screen_psi.dmi' + var/mob/living/owner + var/hidden = TRUE + +/obj/screen/psi/New(var/mob/living/_owner) + loc = null + owner = _owner + update_icon() + +/obj/screen/psi/Destroy() + if(owner && owner.client) + owner.client.screen -= src + . = ..() + +/obj/screen/psi/update_icon() + . = ..() + handle_visibility() + +/obj/screen/psi/proc/handle_visibility() + if(hidden) + invisibility = 101 + else + invisibility = 0 diff --git a/code/modules/psionics/interfaces/ui_hub.dm b/code/modules/psionics/interfaces/ui_hub.dm new file mode 100644 index 0000000000000..a2b79874cd96a --- /dev/null +++ b/code/modules/psionics/interfaces/ui_hub.dm @@ -0,0 +1,102 @@ +/obj/screen/psi/hub + name = "Psi" + icon_state = "psi_suppressed" + screen_loc = "EAST-1:28,CENTER-4" + plane = HUD_PLANE + hidden = FALSE + maptext_x = 6 + maptext_y = -8 + var/image/on_cooldown + var/mutable_appearance/heat_bar + var/mutable_appearance/heat_bar_filling + var/list/components + +/obj/screen/psi/hub/New(mob/living/_owner) + on_cooldown = image(icon, "cooldown") + heat_bar = mutable_appearance(icon, "heat_bar") + heat_bar.pixel_y += 28 + heat_bar_filling = mutable_appearance(icon, "") + heat_bar_filling.pixel_y += 28 + components = list( + new /obj/screen/psi/limiter(_owner), + new /obj/screen/psi/armour(_owner), + new /obj/screen/psi/autoredaction(_owner), + new /obj/screen/psi/zorch_harm(_owner), + new /obj/screen/psi/toggle_psi_menu(_owner, src) + ) + ..() + START_PROCESSING(SSprocessing, src) + +/obj/screen/psi/hub/update_icon() + ..() + if(!owner.psi) + return + cut_overlays() + icon_state = owner.psi.suppressed ? "psi_suppressed" : "psi_active" + if(world.time < owner.psi.next_power_use) + add_overlay(on_cooldown) + heat_bar_filling.icon_state = "heat_[round(owner.psi.heat / 5, 5)]" + switch(owner.psi.heat) + if(400 to 500) + heat_bar_filling.color = "#FF0033" + if(300 to 400) + heat_bar_filling.color = "#FF9933" + if(100 to 300) + heat_bar_filling.color = "#00FF33" + if(0 to 100) + heat_bar_filling.color = "#6699FF" + add_overlay(heat_bar) + add_overlay(heat_bar_filling) + var/offset = 1 + for(var/thing in components) + var/obj/screen/psi/component = thing + component.update_icon() + if(!component.invisibility) + component.screen_loc = "EAST-[++offset]:28,CENTER-4" + +/obj/screen/psi/hub/Destroy() + STOP_PROCESSING(SSprocessing, src) + owner = null + for(var/thing in components) + qdel(thing) + components.Cut() + . = ..() + +/obj/screen/psi/hub/process() + if(!istype(owner)) + qdel(src) + return + if(!owner.psi) + return + maptext = "[round((owner.psi.stamina/owner.psi.max_stamina)*100)]%" + update_icon() + +/obj/screen/psi/hub/MouseEntered(location, control, params) + . = ..() + openToolTip(usr, src, params, title = "[owner.mind.name]'s Psi Complexus", content = "Shift Click To Open The Psi Complexus\nStamina: [(owner.psi.stamina/owner.psi.max_stamina)*100]%\nHeat: [owner.psi.heat]\nStunned: [owner.psi.stun ? "True" : "False"]\n") + +/obj/screen/psi/hub/MouseExited(location, control, params) + . = ..() + closeToolTip(usr) + +/obj/screen/psi/hub/Click(location, control, params) + var/list/click_params = params2list(params) + if(click_params["shift"]) + owner.psi.ui_interact(owner) + return + if(owner.stat != CONSCIOUS) + return + + if(owner.psi.suppressed && owner.psi.stun) + to_chat(owner, "You are dazed and reeling, and cannot muster enough focus to do that!") + return + + owner.psi.suppressed = !owner.psi.suppressed + to_chat(owner, "You are [owner.psi.suppressed ? "now suppressing" : "no longer suppressing"] your psi-power.") + if(owner.psi.suppressed) + owner.psi.cancel() + owner.psi.hide_auras() + else + SEND_SOUND(owner, sound('sound/effects/psi/power_unlock.ogg')) + owner.psi.show_auras() + update_icon() diff --git a/code/modules/psionics/interfaces/ui_toggle.dm b/code/modules/psionics/interfaces/ui_toggle.dm new file mode 100644 index 0000000000000..420f9c925129a --- /dev/null +++ b/code/modules/psionics/interfaces/ui_toggle.dm @@ -0,0 +1,126 @@ +// Begin psi armour toggle. +/obj/screen/psi/armour + plane = HUD_PLANE + name = "Psi-Armour" + icon_state = "psiarmour_off" + +/obj/screen/psi/armour/update_icon() + ..() + //everything but coercion gets psi armour + if(!(owner.psi.get_rank(PSI_ENERGISTICS) >= PSI_RANK_OPERANT || owner.psi.get_rank(PSI_PSYCHOKINESIS) >= PSI_RANK_OPERANT || owner.psi.get_rank(PSI_REDACTION) >= PSI_RANK_OPERANT)) + invisibility = 101 + if(invisibility == 0) + icon_state = owner.psi.use_psi_armour ? "psiarmour_on" : "psiarmour_off" + +/obj/screen/psi/armour/Click() + if(!owner.psi || owner.stat != CONSCIOUS) + return + owner.psi.use_psi_armour = !owner.psi.use_psi_armour + to_chat(owner, span_notice("You will [owner.psi.use_psi_armour ? "now" : "no longer"] use your psionics to deflect or block incoming attacks.")) + update_icon() + +// End psi armour toggle. + +// Begin autoredaction toggle. +/obj/screen/psi/autoredaction + plane = HUD_PLANE + name = "Autoredaction" + icon_state = "healing_off" + +/obj/screen/psi/autoredaction/update_icon() + ..() + if(owner.psi.get_rank(PSI_REDACTION) < PSI_RANK_OPERANT) //only redaction gets autoredaction + invisibility = 101 + if(invisibility == 0) + icon_state = owner.psi.use_autoredaction ? "healing_on" : "healing_off" + +/obj/screen/psi/autoredaction/Click() + if(!owner.psi || owner.stat != CONSCIOUS) + return + owner.psi.use_autoredaction = !owner.psi.use_autoredaction + to_chat(owner, span_notice("You will [owner.psi.use_autoredaction ? "now" : "no longer"] use your psionics to regenerate.")) + update_icon() + +// End autoredaction toggle. + +// Begin zorch harm toggle. +/obj/screen/psi/zorch_harm + name = "Zorch Mode" + icon_state = "zorch_disable" + plane = HUD_PLANE + +/obj/screen/psi/zorch_harm/update_icon() + ..() + if(owner.psi.get_rank(PSI_ENERGISTICS) < PSI_RANK_MASTER) //only energistics get zorch + invisibility = 101 + if(invisibility == 0) + icon_state = owner.psi.zorch_harm ? "zorch_harm" : "zorch_disable" + +/obj/screen/psi/zorch_harm/Click() + if(!owner.psi || owner.stat != CONSCIOUS) + return + owner.psi.zorch_harm = !owner.psi.zorch_harm + to_chat(owner, span_notice("You will now fire [owner.psi.zorch_harm ? "lethal" : "non-lethal"] lasers with your psionics.")) + update_icon() + +// End zorch harm toggle. + +// Begin limiter toggle. +/obj/screen/psi/limiter + plane = HUD_PLANE + name = "Psi-Limiter" + icon_state = "limiter_100" + +/obj/screen/psi/limiter/update_icon() + ..() + if(invisibility == 0) + switch(owner.psi.limiter) + if(100) + icon_state = "limiter_100" + if(300) + icon_state = "limiter_300" + if(INFINITY) + icon_state = "limiter_500" + +/obj/screen/psi/limiter/Click() + if(!owner.psi) + return + switch(owner.psi.limiter) + if(100) + owner.psi.limiter = 300 + if(300) + owner.psi.limiter = INFINITY + if(INFINITY) + owner.psi.limiter = 100 + if(owner.psi.limiter == INFINITY) + to_chat(owner, span_warning("You release your self imposed shackles!")) + else + to_chat(owner, span_notice("Your mental limiters will stop you at [owner.psi.limiter] heat.")) + update_icon() + +// End limiter toggle. + +// Menu toggle. +/obj/screen/psi/toggle_psi_menu + name = "Show/Hide Psi UI" + icon_state = "arrow_left" + var/obj/screen/psi/hub/controller + plane = HUD_PLANE + +/obj/screen/psi/toggle_psi_menu/New(mob/living/_owner, obj/screen/psi/hub/_controller) + controller = _controller + ..(_owner) + +/obj/screen/psi/toggle_psi_menu/Click() + var/set_hidden = !hidden + for(var/thing in controller.components) + var/obj/screen/psi/psi = thing + psi.hidden = set_hidden + controller.update_icon() + +/obj/screen/psi/toggle_psi_menu/handle_visibility() + if(hidden) + icon_state = "arrow_left" + else + icon_state = "arrow_right" +// End menu toggle. diff --git a/code/modules/psionics/mob/mob.dm b/code/modules/psionics/mob/mob.dm new file mode 100644 index 0000000000000..8968a982c360b --- /dev/null +++ b/code/modules/psionics/mob/mob.dm @@ -0,0 +1,46 @@ +/mob/living + var/datum/psi_complexus/psi + +/mob/living/Login() + . = ..() + if(psi) + psi.update(TRUE) + if(!psi.suppressed) + psi.show_auras() + +/mob/living/Destroy() + QDEL_NULL(psi) + . = ..() + +/mob/living/proc/set_psi_rank(faculty, rank, take_larger, defer_update, temporary) + if(isipc(src)) + return + if(!psi) + psi = new(src) + var/current_rank = psi.get_rank(faculty) + if(current_rank != rank && (!take_larger || current_rank < rank)) + psi.set_rank(faculty, rank, defer_update, temporary) + +/mob/living/carbon/human + /// Whether or not it's tried to apply the species based latency + var/tried_species = FALSE + +/mob/living/carbon/human/Login() //happens here because psi sorta relies on the thing having a mind + . = ..() + if(HAS_TRAIT(src, TRAIT_PSIONICALLY_DEAFENED) || HAS_TRAIT(src, TRAIT_PSIONICALLY_IMMUNE)) + return + if(!psi && !tried_species) + tried_species = TRUE + var/datum/species/dude = dna.species + + var/list/latencies = dude.possible_faculties + latencies = latencies.Copy() + if(!length(latencies)) + return + + if(prob(dude.latency_chance)) + set_psi_rank(pick_n_take(latencies), dude.starting_psi_level) + + if(prob(dude.latency_chance * 0.1)) //really low chance of getting two if you're tuned + set_psi_rank(pick(latencies), dude.starting_psi_level) + diff --git a/code/modules/psionics/mob/mob_assay.dm b/code/modules/psionics/mob/mob_assay.dm new file mode 100644 index 0000000000000..11987e3469e67 --- /dev/null +++ b/code/modules/psionics/mob/mob_assay.dm @@ -0,0 +1,153 @@ +/mob/living/proc/show_psi_assay(var/mob/viewer) + + if(!viewer) viewer = usr + + var/use_He_is = "You are" + var/use_He_has = "You have" + var/use_Your = "your" + if(istype(machine) || viewer != src) + use_He_is = "[p_they(TRUE)] [p_are()]" + use_He_has = "[p_they(TRUE)] [p_have()]" + use_Your = "[p_their()]" + + var/list/dat = list() + + dat += "

Summary

" + dat += "
" + + if(psi) + + // Hi Warhammer 40k rating system, how are you? + // I hope you get along with the Galactic Milieu metapsychics. + var/use_rating + var/effective_rating = psi.rating + if(effective_rating > 1 && psi.suppressed) + effective_rating = max(0, psi.rating-2) + var/rating_descriptor + /* FIX THIS + if(viewer != usr && thralls?.mind?.has_antag_datum() && ishuman(viewer)) + var/mob/living/H = viewer + if(H.psi && H.psi.get_rank(PSI_REDACTION) >= PSI_RANK_GRANDMASTER) + dat += "Their mind has been cored like an apple, and enslaved by another operant psychic." + */ + if(!use_rating) + switch(effective_rating) + if(1) + use_rating = "[effective_rating]-Epsilon" + rating_descriptor = "This indicates the presence of minor latent psi potential with little or no operant capabilities." + if(2) + use_rating = "[effective_rating]-Delta" + rating_descriptor = "This indicates the presence of minor psi capabilities of the Operant rank or higher." + if(3) + use_rating = "[effective_rating]-Gamma" + rating_descriptor = "This indicates the presence of psi capabilities of the Master rank or higher." + if(4) + use_rating = "[effective_rating]-Beta" + rating_descriptor = "This indicates the presence of significant psi capabilities of the Grandmaster rank or higher." + if(5) + use_rating = "[effective_rating]-Alpha" + rating_descriptor = "This indicates the presence of major psi capabilities of the Paramount rank or higher." + else + use_rating = "[effective_rating]-Lambda" + rating_descriptor = "This indicates the presence of trace latent psi capabilities." + + dat += "[use_He_has] an overall psi rating of [use_rating].
[rating_descriptor]
" + + if(psi.announced) + dat += "[use_He_is] currently [psi.suppressed ? "suppressing" : "not suppressing"] [use_Your] psychic operancy.
" + dat += "[use_He_has] [psi.stamina]/[psi.max_stamina] psi stamina remaining.
" + dat += "
" + + for(var/faculty_id in psi.ranks) + var/datum/psionic_faculty/faculty = SSpsi.get_faculty(faculty_id) + if(psi.ranks[faculty.id] > 0) + dat += "[use_He_is] assayed at the rank of [GLOB.psychic_ranks_to_strings[psi.ranks[faculty.id]]] for the [faculty.name] faculty.
" + else + dat += "[use_He_has] no notable power within the [faculty.name] faculty.
" + dat += "
" + + if(viewer == usr) + dat += "" + for(var/faculty_id in psi.ranks) + var/list/check_powers = psi.get_powers_by_faculty(faculty_id) + if(LAZYLEN(check_powers)) + var/datum/psionic_faculty/faculty = SSpsi.get_faculty(faculty_id) + dat += "" + for(var/datum/psionic_power/power in check_powers) + dat += "" + dat += "

Psi-power Usage

[use_He_has] access to the following psi-powers within the [faculty.name] faculty:
[power.name][power.use_description]
" + + var/datum/browser/popup = new(viewer, "psi_assay_\ref[src]", "Psi-Assay") + popup.set_content(jointext(dat,null)) + popup.open() + +/datum/psi_complexus/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PsionicComplexus", "Psi Complexus") + ui.open() + +/datum/psi_complexus/ui_data(mob/user) + var/list/data = list() + var/use_rating + var/effective_rating = rating + if(effective_rating > 1 && suppressed) + effective_rating = max(0, rating-2) + var/rating_descriptor + if(!use_rating) + switch(effective_rating) + if(1) + use_rating = "[effective_rating]-Epsilon" + rating_descriptor = "This indicates the presence of minor latent psi potential with little or no operant capabilities." + if(2) + use_rating = "[effective_rating]-Delta" + rating_descriptor = "This indicates the presence of minor psi capabilities of the Operant rank or higher." + if(3) + use_rating = "[effective_rating]-Gamma" + rating_descriptor = "This indicates the presence of psi capabilities of the Master rank or higher." + if(4) + use_rating = "[effective_rating]-Beta" + rating_descriptor = "This indicates the presence of significant psi capabilities of the Grandmaster rank or higher." + if(5) + use_rating = "[effective_rating]-Alpha" + rating_descriptor = "This indicates the presence of major psi capabilities of the Paramount rank or higher." + else + use_rating = "[effective_rating]-Lambda" + rating_descriptor = "This indicates the presence of trace latent psi capabilities." + + data["use_rating"] = use_rating + data["rating_descriptor"] = rating_descriptor + data["faculties"] = list() + for(var/faculty_id in ranks) + var/list/check_powers = get_powers_by_faculty(faculty_id) + if(LAZYLEN(check_powers)) + var/list/details = list() + var/datum/psionic_faculty/faculty = SSpsi.get_faculty(faculty_id) + details["name"] += faculty.name + details["rank"] += ranks[faculty_id] + for(var/datum/psionic_power/power in check_powers) + var/list/power_data = list() + power_data["name"] = power.name + power_data["description"] = power.use_description + details["powers"] += list(power_data) + data["faculties"] += list(details) + return data + +/datum/psi_complexus/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/sheetmaterials) + ) + +/datum/psi_complexus/ui_act(action, params) + . = ..() + if(.) + return + +/datum/psi_complexus/ui_state() + return GLOB.always_state + +/datum/asset/spritesheet/psi_icons + name = "psi_icons" + +/datum/asset/spritesheet/psi_icons/create_spritesheets() + InsertAll("", 'icons/obj/psychic_powers.dmi') diff --git a/code/modules/psionics/null/_null.dm b/code/modules/psionics/null/_null.dm new file mode 100644 index 0000000000000..3b4b09e07253c --- /dev/null +++ b/code/modules/psionics/null/_null.dm @@ -0,0 +1,28 @@ +/atom/proc/disrupts_psionics() + for(var/atom/movable/AM in contents) + if(!istype(AM) || AM == src) + continue + var/disrupted_by = AM.disrupts_psionics() + if(disrupted_by) + return disrupted_by + return FALSE + +/atom/proc/do_psionics_check(var/stress, var/atom/source) + var/turf/T = get_turf(src) + if(istype(T) && T != src) + var/V = T.do_psionics_check(stress, source) + if(V) + return V + stress = withstand_psi_stress(stress, source) + var/V = disrupts_psionics() + return V + +/atom/proc/withstand_psi_stress(var/stress, var/atom/source) + . = max(stress, 0) + if(.) + for(var/thing in contents) + var/atom/movable/AM = thing + if(istype(AM) && AM != src && AM.disrupts_psionics()) + . = AM.withstand_psi_stress(., source) + if(. <= 0) + break diff --git a/code/modules/psionics/null/chemistry.dm b/code/modules/psionics/null/chemistry.dm new file mode 100644 index 0000000000000..68db9e953f315 --- /dev/null +++ b/code/modules/psionics/null/chemistry.dm @@ -0,0 +1,35 @@ +/datum/reagent/crystal + name = "crystallizing agent" + taste_description = "sharpness" + reagent_state = LIQUID + color = "#13bc5e" + +/datum/reagent/crystal/affect_blood(var/mob/living/carbon/M, var/alien, var/removed) + var/result_mat = (M.psi || (M.mind && GLOB.wizards.is_antagonist(M.mind))) ? MATERIAL_NULLGLASS : MATERIAL_CRYSTAL + if(ishuman(M)) + var/mob/living/carbon/human/H = M + if(prob(5)) + var/obj/item/organ/external/E = pick(H.organs) + if(!E || E.is_stump() || BP_IS_ROBOTIC(E)) + return + if(BP_IS_CRYSTAL(E)) + E.heal_damage(rand(3,5), rand(3,5)) + if(BP_IS_BRITTLE(E) && prob(5)) + E.status &= ~ORGAN_BRITTLE + else if(E.organ_tag != BP_CHEST && E.organ_tag != BP_GROIN) + to_chat(H, SPAN_DANGER("Your [E.name] is being lacerated from within!")) + if(H.can_feel_pain()) + H.emote("scream") + if(prob(25)) + for(var/i = 1 to rand(3,5)) + new /obj/item/weapon/material/shard(get_turf(E), result_mat) + E.droplimb(0, DROPLIMB_BLUNT) + else + E.take_external_damage(rand(20,30), 0) + E.status |= ORGAN_CRYSTAL + E.status |= ORGAN_BRITTLE + return + to_chat(M, SPAN_DANGER("Your flesh is being lacerated from within!")) + M.adjustBruteLoss(rand(3,6)) + if(prob(10)) + new /obj/item/weapon/material/shard(get_turf(M), result_mat) diff --git a/code/modules/psionics/null/flooring.dm b/code/modules/psionics/null/flooring.dm new file mode 100644 index 0000000000000..b7751417da57a --- /dev/null +++ b/code/modules/psionics/null/flooring.dm @@ -0,0 +1,17 @@ +/turf/open/floor + var/psi_null + +/turf/open/floor/disrupts_psionics() + return (psi_null ? src : FALSE) + +/turf/open/floor/nullglass + name = "nullglass plating" + desc = "You can hear the tiles whispering..." + icon_state = "light_off" + psi_null = TRUE + floor_tile = /obj/item/stack/tile/mineral/nullglass + +/obj/item/stack/tile/mineral/nullglass + name = "nullglass floor tile" + icon_state = "tile_e" + turf_type = /turf/open/floor/nullglass diff --git a/code/modules/reagents/chemistry/reagents/drink_reagents.dm b/code/modules/reagents/chemistry/reagents/drink_reagents.dm index 8403e5b44ca88..94191e95d6723 100644 --- a/code/modules/reagents/chemistry/reagents/drink_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/drink_reagents.dm @@ -1134,3 +1134,56 @@ glass_icon_state = "cucumber_lemonade" glass_name = "cucumber lemonade" glass_desc = "Lemonade, with added cucumber." + +/** + * basic wakefulness chem + * addiction gets less bad over time rather than the inverse + */ +/datum/reagent/drug/caffeine + name = "Caffeine" + description = "Slightly increases wakefulness. If overdosed it will cause jitters and heart problems." + reagent_state = SOLID //powder in pure form + color = "#ffffff" //very white + metabolization_rate = REAGENTS_METABOLISM + overdose_threshold = 20 //please don't consume pure caffeine + addiction_threshold = 30 //not easy to get addicted to unless you have way too much + trippy = FALSE + var/list/overdose_text = list("Your head pounds.", "You feel lethargic.", "You feel drowsy.", "You feel weak.", "You just want to sleep.") + +/datum/reagent/drug/caffeine/on_mob_life(mob/living/carbon/M) + . = ..() + if(prob(1)) + var/caffeine_message = pick("You feel alert.") + to_chat(M, span_notice("[caffeine_message]")) + M.adjust_drowsiness(-6 SECONDS * REM) + M.AdjustSleeping(-4 SECONDS, FALSE) + M.adjust_dizzy(-4 SECONDS * REM) + +/datum/reagent/drug/caffeine/overdose_process(mob/living/M) + . = ..() + M.set_jitter_if_lower(20 SECONDS) + M.adjustOrganLoss(ORGAN_SLOT_HEART, 1.25*REM) + +/datum/reagent/drug/caffeine/proc/apply_drowsy(mob/living/M) + M.adjust_drowsiness_up_to(3 SECONDS * REM, 10 SECONDS) + if(prob(50)) + to_chat(M, span_warning(pick(overdose_text))) + +/** + * doesn't call the parent addiction acts because it doesn't function the same way + */ +/datum/reagent/drug/caffeine/addiction_act_stage1(mob/living/M) + if(prob(75) && iscarbon(M)) + apply_drowsy(M) + +/datum/reagent/drug/caffeine/addiction_act_stage2(mob/living/M) + if(prob(60) && iscarbon(M)) + apply_drowsy(M) + +/datum/reagent/drug/caffeine/addiction_act_stage3(mob/living/M) + if(prob(45) && iscarbon(M)) + apply_drowsy(M) + +/datum/reagent/drug/caffeine/addiction_act_stage4(mob/living/M) + if(prob(30) && iscarbon(M)) + apply_drowsy(M) diff --git a/code/modules/reagents/chemistry/reagents/drug_reagents.dm b/code/modules/reagents/chemistry/reagents/drug_reagents.dm index 06f1d08eebe07..68a33157633fc 100644 --- a/code/modules/reagents/chemistry/reagents/drug_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/drug_reagents.dm @@ -795,40 +795,6 @@ ..() . = 1 -/** - * basic wakefulness chem - * addiction gets less bad over time rather than the inverse - */ -/datum/reagent/drug/caffeine - name = "Caffeine" - description = "Slightly increases wakefulness. If overdosed it will cause jitters and heart problems." - reagent_state = SOLID //powder in pure form - color = "#ffffff" //very white - metabolization_rate = REAGENTS_METABOLISM - overdose_threshold = 20 //please don't consume pure caffeine - addiction_threshold = 20 //the addiction isn't that dangerous - trippy = FALSE - var/list/overdose_text = list("Your head pounds.", "You feel lethargic.", "You feel drowsy.", "You feel weak.", "You just want to sleep.") - -/datum/reagent/drug/caffeine/on_mob_life(mob/living/carbon/M) - . = ..() - if(prob(1)) - var/caffeine_message = pick("You feel alert.") - to_chat(M, span_notice("[caffeine_message]")) - M.adjust_drowsiness(-6 SECONDS * REM) - M.AdjustSleeping(-4 SECONDS, FALSE) - M.adjust_dizzy(-4 SECONDS * REM) - -/datum/reagent/drug/caffeine/overdose_process(mob/living/M) - . = ..() - M.set_jitter_if_lower(20 SECONDS) - M.adjustOrganLoss(ORGAN_SLOT_HEART, 1.25*REM) - -/datum/reagent/drug/caffeine/proc/apply_drowsy(mob/living/M) - M.adjust_drowsiness_up_to(3 SECONDS * REM, 10 SECONDS) - if(prob(50)) - to_chat(M, span_warning(pick(overdose_text))) - /** * doesn't call the parent addiction acts because it doesn't function the same way */ @@ -847,3 +813,90 @@ /datum/reagent/drug/caffeine/addiction_act_stage4(mob/living/M) if(prob(45) && iscarbon(M)) apply_drowsy(M) + + +/datum/reagent/drug/three_eye + name = "Three Eye" + taste_description = "liquid starlight" + description = "Three Eye is one of the most notorious narcotics to ever come out of the independant habitats, allowing those who take it to see through walls." + reagent_state = LIQUID + color = "#ccccff" + metabolization_rate = REAGENTS_METABOLISM + overdose_threshold = 25 + + // M A X I M U M C H E E S E + var/global/list/dose_messages = list( + "Your name is called. It is your time.", + "You are dissolving. Your hands are wax...", + "It all runs together. It all mixes.", + "It is done. It is over. You are done. You are over.", + "You won't forget. Don't forget. Don't forget.", + "Light seeps across the edges of your vision...", + "Something slides and twitches within your sinus cavity...", + "Your bowels roil. It waits within.", + "Your gut churns. You are heavy with potential.", + "Your heart flutters. It is winged and caged in your chest.", + "There is a precious thing, behind your eyes.", + "Everything is ending. Everything is beginning.", + "Nothing ends. Nothing begins.", + "Wake up. Please wake up.", + "Stop it! You're hurting them!", + "It's too soon for this. Please go back.", + "We miss you. Where are you?", + "Come back from there. Please." + ) + + var/global/list/overdose_messages = list( + "THE SIGNAL THE SIGNAL THE SIGNAL THE SIGNAL", + "IT CRIES IT CRIES IT WAITS IT CRIES", + "NOT YOURS NOT YOURS NOT YOURS NOT YOURS", + "THAT IS NOT FOR YOU", + "IT RUNS IT RUNS IT RUNS IT RUNS", + "THE BLOOD THE BLOOD THE BLOOD THE BLOOD", + "THE LIGHT THE DARK A STAR IN CHAINS" + ) + + COOLDOWN_DECLARE(next_trigger) + var/cooldown_duration = 15 SECONDS + +/datum/reagent/drug/three_eye/on_mob_metabolize(mob/living/L) + . = ..() + ADD_TRAIT(L, TRAIT_THERMAL_VISION, type) + L.add_client_colour(/datum/client_colour/thirdeye) + L.update_sight() + +/datum/reagent/drug/three_eye/on_mob_end_metabolize(mob/living/L) + REMOVE_TRAIT(L, TRAIT_THERMAL_VISION, type) + L.remove_client_colour(/datum/client_colour/thirdeye) + L.update_sight() + return ..() + +/datum/reagent/drug/three_eye/on_mob_life(mob/living/carbon/M) + . = ..() + M.adjust_hallucinations_up_to(10 SECONDS, 50 SECONDS) + M.adjust_jitter_up_to(3 SECONDS, 10 SECONDS) + M.adjust_dizzy_up_to(3 SECONDS, 10 SECONDS) + if(prob(0.1)) + seizure(M) + M.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(8, 12)) + if(prob(5)) + to_chat(M, span_warning("[pick(dose_messages)]")) + +/datum/reagent/drug/three_eye/overdose_process(mob/living/M) + . = ..() + M.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(1, 5)) + if(prob(10)) + seizure(M) + if(prob(10)) + to_chat(M, span_danger("[pick(overdose_messages)]")) + return + if(!COOLDOWN_FINISHED(src, next_trigger)) + return + COOLDOWN_START(src, next_trigger, cooldown_duration) + if(M.psi) + M.psi.check_latency_trigger(30, "a Three Eye overdose", 30) + +/datum/reagent/drug/three_eye/proc/seizure(mob/living/M) + M.visible_message(span_danger("[M] starts having a seizure!"), span_userdanger("You have a seizure!")) + M.Unconscious(10 SECONDS) + M.adjust_jitter(10 SECONDS) diff --git a/code/modules/reagents/chemistry/recipes/drugs.dm b/code/modules/reagents/chemistry/recipes/drugs.dm index be345727ab00c..aa3e8e8eb0000 100644 --- a/code/modules/reagents/chemistry/recipes/drugs.dm +++ b/code/modules/reagents/chemistry/recipes/drugs.dm @@ -12,7 +12,6 @@ mix_message = "The mixture violently reacts, leaving behind a few crystalline shards." required_temp = 390 - /datum/chemical_reaction/krokodil name = "Krokodil" id = /datum/reagent/drug/krokodil @@ -80,3 +79,10 @@ id = /datum/reagent/drug/blue_eye results = list(/datum/reagent/drug/blue_eye = 5) required_reagents = list(/datum/reagent/medicine/diphenhydramine = 1, /datum/reagent/bluespace = 2, /datum/reagent/iodine = 1, /datum/reagent/gas/hydrogen = 1, /datum/reagent/consumable/sugar = 1) + +/datum/chemical_reaction/three_eye + name = "Three-Eye" + id = /datum/reagent/drug/three_eye + results = list(/datum/reagent/drug/three_eye = 3) + required_reagents = list(/datum/reagent/drug/blue_eye = 3, /datum/reagent/toxin/mindbreaker = 3, /datum/reagent/medicine/neurine = 3) + required_temp = 333 diff --git a/code/modules/reagents/reagent_containers/pill.dm b/code/modules/reagents/reagent_containers/pill.dm index 3e4f12fa63f11..69f86de89126c 100644 --- a/code/modules/reagents/reagent_containers/pill.dm +++ b/code/modules/reagents/reagent_containers/pill.dm @@ -256,6 +256,12 @@ icon_state = "pill_happy" list_reagents = list(/datum/reagent/drug/happiness = 10) +/obj/item/reagent_containers/pill/three_eye + name = "strange pill" + desc = "The surface of this unlabelled pill crawls against your skin." + icon_state = "pill12" + list_reagents = list(/datum/reagent/drug/three_eye = 10) + /obj/item/reagent_containers/pill/floorpill name = "floorpill" desc = "A strange pill found in the depths of maintenance." diff --git a/code/modules/research/designs/autolathe_designs.dm b/code/modules/research/designs/autolathe_designs.dm index 2fc7d0a542aa6..8fea0f94deeab 100644 --- a/code/modules/research/designs/autolathe_designs.dm +++ b/code/modules/research/designs/autolathe_designs.dm @@ -965,6 +965,20 @@ build_path = /obj/item/ammo_casing/a357 category = list("hacked", "Security") +/obj/projectile/bullet/a357/nullglass + name = ".357 NULL bullet" + damage = 30 + +/obj/projectile/bullet/a357/nullglass/disrupts_psionics() + return src + +/obj/projectile/bullet/a357/nullglass/on_hit(atom/target) + . = ..() + if(prob(50)) + var/obj/item/implant/nullglass/imp = new() + imp.implant(target) + playsound(loc, 'sound/effects/glass_step.ogg', 30, TRUE) + /datum/design/a357/ironfeather name = ".357 Ironfeather Bullet" id = "a357_ironfeather" diff --git a/code/modules/surgery/amputation.dm b/code/modules/surgery/amputation.dm index 618f73d01d3e9..69b79c853449d 100644 --- a/code/modules/surgery/amputation.dm +++ b/code/modules/surgery/amputation.dm @@ -26,7 +26,7 @@ /datum/surgery_step/sever_limb name = "sever limb" - implements = list(TOOL_SCALPEL = 100, TOOL_SAW = 100, /obj/item/melee/arm_blade = 80, /obj/item/melee/chainsaw = 80, /obj/item/mounted_chainsaw = 80, /obj/item/fireaxe = 50, /obj/item/hatchet = 40, /obj/item/kitchen/knife/butcher = 25) + implements = list(TOOL_SCALPEL = 100, TOOL_SAW = 100, /obj/item/melee/arm_blade = 80, /obj/item/melee/chainsaw = 80, /obj/item/mounted_chainsaw = 80, /obj/item/fireaxe = 50, TOOL_HATCHET = 40, /obj/item/kitchen/knife/butcher = 25) time = 6.4 SECONDS preop_sound = 'sound/surgery/scalpel1.ogg' success_sound = 'sound/surgery/organ2.ogg' diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm index 9691c7f672fe6..f29992c4ef13b 100644 --- a/code/modules/surgery/bodyparts/_bodyparts.dm +++ b/code/modules/surgery/bodyparts/_bodyparts.dm @@ -518,24 +518,27 @@ var/armor_ablation = 0 var/injury_mod = 0 - if(owner && ishuman(owner)) - var/mob/living/carbon/human/H = owner - - if(H?.physiology?.armor?.wound)//if there is any innate wound armor (poly or genetics) - armor_ablation += H.physiology.armor.getRating(WOUND) - - var/list/clothing = H.clothingonpart(body_part) - for(var/c in clothing) - var/obj/item/clothing/C = c - // unlike normal armor checks, we tabluate these piece-by-piece manually so we can also pass on appropriate damage the clothing's limbs if necessary - armor_ablation += C.armor.getRating(WOUND) - if(wounding_type == WOUND_SLASH) - C.take_damage_zone(body_zone, damage, BRUTE) - else if(wounding_type == WOUND_BURN && damage >= 10) // lazy way to block freezing from shredding clothes without adding another var onto apply_damage() - C.take_damage_zone(body_zone, damage, BURN) - - if(!armor_ablation) - injury_mod += bare_wound_bonus + if(owner) + if(owner.psi) + armor_ablation += owner.psi.get_armour(WOUND) + if(ishuman(owner)) + var/mob/living/carbon/human/H = owner + + if(H?.physiology?.armor?.wound)//if there is any innate wound armor (poly or genetics) + armor_ablation += H.physiology.armor.getRating(WOUND) + + var/list/clothing = H.clothingonpart(body_part) + for(var/c in clothing) + var/obj/item/clothing/C = c + // unlike normal armor checks, we tabluate these piece-by-piece manually so we can also pass on appropriate damage the clothing's limbs if necessary + armor_ablation += C.armor.getRating(WOUND) + if(wounding_type == WOUND_SLASH) + C.take_damage_zone(body_zone, damage, BRUTE) + else if(wounding_type == WOUND_BURN && damage >= 10) // lazy way to block freezing from shredding clothes without adding another var onto apply_damage() + C.take_damage_zone(body_zone, damage, BURN) + + if(!armor_ablation) + injury_mod += bare_wound_bonus injury_mod -= armor_ablation injury_mod += wound_bonus diff --git a/code/modules/surgery/lipoplasty.dm b/code/modules/surgery/lipoplasty.dm index bb0b5ebdfbc61..68f6f0eb31f2e 100644 --- a/code/modules/surgery/lipoplasty.dm +++ b/code/modules/surgery/lipoplasty.dm @@ -28,7 +28,7 @@ //cut fat /datum/surgery_step/cut_fat name = "cut excess fat" - implements = list(TOOL_SCALPEL = 100, /obj/item/hatchet = 35, /obj/item/kitchen/knife/butcher = 25) + implements = list(TOOL_SCALPEL = 100, TOOL_HATCHET = 35, /obj/item/kitchen/knife/butcher = 25) time = 6.4 SECONDS preop_sound = 'sound/surgery/scalpel1.ogg' success_sound = 'sound/surgery/scalpel2.ogg' diff --git a/code/modules/surgery/mechanic_steps.dm b/code/modules/surgery/mechanic_steps.dm index 0d76573a3c86c..c3bb323b584f9 100644 --- a/code/modules/surgery/mechanic_steps.dm +++ b/code/modules/surgery/mechanic_steps.dm @@ -18,7 +18,7 @@ /datum/surgery_step/mechanic_open/tool_check(mob/user, obj/item/tool) if(istype(tool)) - if(!tool.is_sharp()) + if(implement_type == /obj/item && !tool.is_sharp()) return FALSE if(tool.usesound) preop_sound = tool.usesound @@ -45,7 +45,7 @@ /datum/surgery_step/mechanic_close/tool_check(mob/user, obj/item/tool) if(istype(tool)) - if(!tool.is_sharp()) + if(implement_type == /obj/item && !tool.is_sharp()) return FALSE if(tool.usesound) preop_sound = tool.usesound diff --git a/code/modules/surgery/organic_steps.dm b/code/modules/surgery/organic_steps.dm index 084b48e0de16f..d542ee9c63ad8 100644 --- a/code/modules/surgery/organic_steps.dm +++ b/code/modules/surgery/organic_steps.dm @@ -120,7 +120,7 @@ name = "saw bone" implements = list(TOOL_SAW = 100, /obj/item/melee/transforming/energy/sword/cyborg/saw = 100, /obj/item/melee/arm_blade = 75, /obj/item/mounted_chainsaw = 65, /obj/item/melee/chainsaw = 50, - /obj/item/fireaxe = 50, /obj/item/hatchet = 35, /obj/item/kitchen/knife/butcher = 25) + /obj/item/fireaxe = 50, TOOL_HATCHET = 35, /obj/item/kitchen/knife/butcher = 25) time = 5.4 SECONDS preop_sound = list( /obj/item/circular_saw = 'sound/surgery/saw.ogg', diff --git a/code/modules/vending/security.dm b/code/modules/vending/security.dm index 9b561e265a5f2..9ba62370bf1dc 100644 --- a/code/modules/vending/security.dm +++ b/code/modules/vending/security.dm @@ -10,6 +10,7 @@ products = list(/obj/item/clothing/head/helmet/plated = 6, /obj/item/clothing/suit/armor/plated = 6, /obj/item/restraints/handcuffs = 8, + /obj/item/implanter/psi_control = 3, /obj/item/clothing/neck/anti_magic_collar = 3, /obj/item/restraints/handcuffs/cable/zipties = 10, /obj/item/grenade/flashbang = 4, diff --git a/icons/effects/psi_aura_small.dmi b/icons/effects/psi_aura_small.dmi new file mode 100644 index 0000000000000..f5e222b61f5ff Binary files /dev/null and b/icons/effects/psi_aura_small.dmi differ diff --git a/icons/mob/clothing/head/head.dmi b/icons/mob/clothing/head/head.dmi index 6710eb3d925f8..8b4eda3147496 100644 Binary files a/icons/mob/clothing/head/head.dmi and b/icons/mob/clothing/head/head.dmi differ diff --git a/icons/mob/hud.dmi b/icons/mob/hud.dmi index 3731e47bd1c8e..7fd2cb6a5628d 100644 Binary files a/icons/mob/hud.dmi and b/icons/mob/hud.dmi differ diff --git a/icons/mob/inhands/misc/sheets_lefthand.dmi b/icons/mob/inhands/misc/sheets_lefthand.dmi index 96310e6a934e7..2d86b0b0657ad 100644 Binary files a/icons/mob/inhands/misc/sheets_lefthand.dmi and b/icons/mob/inhands/misc/sheets_lefthand.dmi differ diff --git a/icons/mob/inhands/misc/sheets_righthand.dmi b/icons/mob/inhands/misc/sheets_righthand.dmi index d0823c6a26110..d08de7f774439 100644 Binary files a/icons/mob/inhands/misc/sheets_righthand.dmi and b/icons/mob/inhands/misc/sheets_righthand.dmi differ diff --git a/icons/mob/inhands/weapons/melee_lefthand.dmi b/icons/mob/inhands/weapons/melee_lefthand.dmi index dd0e7cfc86b0f..2a30de4649127 100644 Binary files a/icons/mob/inhands/weapons/melee_lefthand.dmi and b/icons/mob/inhands/weapons/melee_lefthand.dmi differ diff --git a/icons/mob/inhands/weapons/melee_righthand.dmi b/icons/mob/inhands/weapons/melee_righthand.dmi index 6b66977d77a07..6ff5b86d65902 100644 Binary files a/icons/mob/inhands/weapons/melee_righthand.dmi and b/icons/mob/inhands/weapons/melee_righthand.dmi differ diff --git a/icons/mob/inhands/weapons/swords_lefthand.dmi b/icons/mob/inhands/weapons/swords_lefthand.dmi index d37c6a3960e6d..bad6742d8ab0c 100644 Binary files a/icons/mob/inhands/weapons/swords_lefthand.dmi and b/icons/mob/inhands/weapons/swords_lefthand.dmi differ diff --git a/icons/mob/inhands/weapons/swords_righthand.dmi b/icons/mob/inhands/weapons/swords_righthand.dmi index 1164a7cb7db2b..0f36ce1a39c07 100644 Binary files a/icons/mob/inhands/weapons/swords_righthand.dmi and b/icons/mob/inhands/weapons/swords_righthand.dmi differ diff --git a/icons/mob/screen_psi.dmi b/icons/mob/screen_psi.dmi new file mode 100644 index 0000000000000..44b3941ead8b7 Binary files /dev/null and b/icons/mob/screen_psi.dmi differ diff --git a/icons/obj/clothing/hats/hats.dmi b/icons/obj/clothing/hats/hats.dmi index 81a4088cf212b..7c1e102e8d6a3 100644 Binary files a/icons/obj/clothing/hats/hats.dmi and b/icons/obj/clothing/hats/hats.dmi differ diff --git a/icons/obj/implants.dmi b/icons/obj/implants.dmi index bcb69a7c44481..fbfacf07e029f 100644 Binary files a/icons/obj/implants.dmi and b/icons/obj/implants.dmi differ diff --git a/icons/obj/machines/psimeter.dmi b/icons/obj/machines/psimeter.dmi new file mode 100644 index 0000000000000..d39e351f2cfa0 Binary files /dev/null and b/icons/obj/machines/psimeter.dmi differ diff --git a/icons/obj/psychic_powers.dmi b/icons/obj/psychic_powers.dmi new file mode 100644 index 0000000000000..22876436397db Binary files /dev/null and b/icons/obj/psychic_powers.dmi differ diff --git a/icons/obj/shards.dmi b/icons/obj/shards.dmi index 94d1602fa007f..c4b6b618c45a6 100644 Binary files a/icons/obj/shards.dmi and b/icons/obj/shards.dmi differ diff --git a/icons/obj/telescience.dmi b/icons/obj/telescience.dmi index 8241d42b2fac8..7e5ee24d4dc0d 100644 Binary files a/icons/obj/telescience.dmi and b/icons/obj/telescience.dmi differ diff --git a/icons/obj/weapons/baton.dmi b/icons/obj/weapons/baton.dmi index 348db83f41ba0..269abe1ac284d 100644 Binary files a/icons/obj/weapons/baton.dmi and b/icons/obj/weapons/baton.dmi differ diff --git a/icons/obj/weapons/longsword.dmi b/icons/obj/weapons/longsword.dmi index a8f7496ac4504..7bba8e2b055c4 100644 Binary files a/icons/obj/weapons/longsword.dmi and b/icons/obj/weapons/longsword.dmi differ diff --git a/icons/obj/weapons/spears.dmi b/icons/obj/weapons/spears.dmi index 321ef21ec7b8d..9c75ac7e08ee6 100644 Binary files a/icons/obj/weapons/spears.dmi and b/icons/obj/weapons/spears.dmi differ diff --git a/sound/effects/psi/power_evoke.ogg b/sound/effects/psi/power_evoke.ogg new file mode 100644 index 0000000000000..37d9c5a54013c Binary files /dev/null and b/sound/effects/psi/power_evoke.ogg differ diff --git a/sound/effects/psi/power_fabrication.ogg b/sound/effects/psi/power_fabrication.ogg new file mode 100644 index 0000000000000..8720d196a80f3 Binary files /dev/null and b/sound/effects/psi/power_fabrication.ogg differ diff --git a/sound/effects/psi/power_fail.ogg b/sound/effects/psi/power_fail.ogg new file mode 100644 index 0000000000000..75364171dd3fe Binary files /dev/null and b/sound/effects/psi/power_fail.ogg differ diff --git a/sound/effects/psi/power_feedback.ogg b/sound/effects/psi/power_feedback.ogg new file mode 100644 index 0000000000000..139dfba0bd46f Binary files /dev/null and b/sound/effects/psi/power_feedback.ogg differ diff --git a/sound/effects/psi/power_unlock.ogg b/sound/effects/psi/power_unlock.ogg new file mode 100644 index 0000000000000..3ba24f81b38d3 Binary files /dev/null and b/sound/effects/psi/power_unlock.ogg differ diff --git a/sound/effects/psi/power_used.ogg b/sound/effects/psi/power_used.ogg new file mode 100644 index 0000000000000..aa0978f14bca6 Binary files /dev/null and b/sound/effects/psi/power_used.ogg differ diff --git a/sound/effects/psi/psisword.ogg b/sound/effects/psi/psisword.ogg new file mode 100644 index 0000000000000..a89b39723034a Binary files /dev/null and b/sound/effects/psi/psisword.ogg differ diff --git a/tgui/packages/tgui/interfaces/PsionicAwakener.js b/tgui/packages/tgui/interfaces/PsionicAwakener.js new file mode 100644 index 0000000000000..f188a62d34b7a --- /dev/null +++ b/tgui/packages/tgui/interfaces/PsionicAwakener.js @@ -0,0 +1,129 @@ +import { useBackend } from '../backend'; +import { Box, Button, LabeledList, ProgressBar, Section, AnimatedNumber } from '../components'; +import { Window } from '../layouts'; + +export const PsionicAwakener = (props, context) => { + const { act, data } = useBackend(context); + + const { + open, + occupant = {}, + occupied, + ready, + timeleft, + result, + active_treatment, + treatment_cost, + nullspace, + nullspace_max, + } = data; + + const treatments = data.treatments || []; + + return ( + + +
+ {occupant.stat} + + )}> + {!!occupied && ( + + + + + {!!result && ( + + {result} + + )} + + )} +
+
act('door')} /> + )}> + + + { + nullspace ? nullspace : 0}/{nullspace_max} + + + + {active_treatment} + + + {treatment_cost ? treatment_cost : "nothing"} + + +
+
+ {treatments.map(treatment => ( +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/PsionicComplexus.tsx b/tgui/packages/tgui/interfaces/PsionicComplexus.tsx new file mode 100644 index 0000000000000..3b4850e57fb5f --- /dev/null +++ b/tgui/packages/tgui/interfaces/PsionicComplexus.tsx @@ -0,0 +1,75 @@ +import { useBackend } from '../backend'; +import { Section, Stack, Table, Collapsible } from '../components'; +import { TableCell, TableRow } from '../components/Table'; +import { Window } from '../layouts'; + +type Data = { + antag_name: string; + loud: boolean; + faculties: Psi_Faculty[]; + use_rating: string; + rating_descriptor: string; +}; + +type User = { + psi_stamina: number; + supressing: boolean; + known_powers: Psi_Power; + psi_faculties: Psi_Faculty[]; + +}; + +type Psi_Power = { + name: string; + description: string; + +}; + +type Psi_Faculty = { + name: string; + rank: number; + powers: Psi_Power[]; + +} + +export const PsionicComplexus = (props, context) => { + const { data } = useBackend(context); + const { faculties = [], use_rating, rating_descriptor } = data; + return ( + + + + +
+

+ { + "Psi-Rating: " + use_rating + } +

+ +
+ +
+ + {faculties.map(faculty => ( +
+ {faculty.powers.map(power => ( + +
+ {power.description} +
+
+ ))} +
+ + ))} +
+
+ +
+
+ ); +}; + diff --git a/yogstation.dme b/yogstation.dme index 14aa3c5a0e37c..51c525845ffe1 100644 --- a/yogstation.dme +++ b/yogstation.dme @@ -123,6 +123,7 @@ #include "code\__DEFINES\procpath.dm" #include "code\__DEFINES\profile.dm" #include "code\__DEFINES\projectiles.dm" +#include "code\__DEFINES\psi.dm" #include "code\__DEFINES\qdel.dm" #include "code\__DEFINES\radiation.dm" #include "code\__DEFINES\radio.dm" @@ -499,6 +500,7 @@ #include "code\controllers\subsystem\processing\plumbing.dm" #include "code\controllers\subsystem\processing\processing.dm" #include "code\controllers\subsystem\processing\projectiles.dm" +#include "code\controllers\subsystem\processing\psi.dm" #include "code\controllers\subsystem\processing\quirks.dm" #include "code\controllers\subsystem\processing\slowprocess.dm" #include "code\controllers\subsystem\processing\station.dm" @@ -1453,6 +1455,7 @@ #include "code\game\objects\items\implants\implant_mindshield.dm" #include "code\game\objects\items\implants\implant_mindshieldtot.dm" #include "code\game\objects\items\implants\implant_misc.dm" +#include "code\game\objects\items\implants\implant_psi.dm" #include "code\game\objects\items\implants\implant_spell.dm" #include "code\game\objects\items\implants\implant_stealth.dm" #include "code\game\objects\items\implants\implant_storage.dm" @@ -3245,6 +3248,7 @@ #include "code\modules\modular_computers\file_system\programs\ntpda_msg.dm" #include "code\modules\modular_computers\file_system\programs\paperworkprinter.dm" #include "code\modules\modular_computers\file_system\programs\portrait_printer.dm" +#include "code\modules\modular_computers\file_system\programs\psi_monitor.dm" #include "code\modules\modular_computers\file_system\programs\radar.dm" #include "code\modules\modular_computers\file_system\programs\robotact.dm" #include "code\modules\modular_computers\file_system\programs\themeify.dm" @@ -3548,6 +3552,34 @@ #include "code\modules\projectiles\projectile\special\rocket.dm" #include "code\modules\projectiles\projectile\special\temperature.dm" #include "code\modules\projectiles\projectile\special\wormhole.dm" +#include "code\modules\psionics\complexus\complexus.dm" +#include "code\modules\psionics\complexus\complexus_helpers.dm" +#include "code\modules\psionics\complexus\complexus_latency.dm" +#include "code\modules\psionics\complexus\complexus_power_cache.dm" +#include "code\modules\psionics\complexus\complexus_process.dm" +#include "code\modules\psionics\equipment\cerebro_enhancers.dm" +#include "code\modules\psionics\equipment\psipower.dm" +#include "code\modules\psionics\equipment\psipower_baton.dm" +#include "code\modules\psionics\equipment\psipower_blade.dm" +#include "code\modules\psionics\equipment\psipower_tinker.dm" +#include "code\modules\psionics\equipment\psipower_tk.dm" +#include "code\modules\psionics\events\_psi.dm" +#include "code\modules\psionics\events\mini_spasms.dm" +#include "code\modules\psionics\events\psi_balm.dm" +#include "code\modules\psionics\events\psi_wail.dm" +#include "code\modules\psionics\faculties\_faculty.dm" +#include "code\modules\psionics\faculties\_power.dm" +#include "code\modules\psionics\faculties\coercion.dm" +#include "code\modules\psionics\faculties\energistics.dm" +#include "code\modules\psionics\faculties\psychokenisis.dm" +#include "code\modules\psionics\faculties\redaction.dm" +#include "code\modules\psionics\interfaces\ui.dm" +#include "code\modules\psionics\interfaces\ui_hub.dm" +#include "code\modules\psionics\interfaces\ui_toggle.dm" +#include "code\modules\psionics\mob\mob.dm" +#include "code\modules\psionics\mob\mob_assay.dm" +#include "code\modules\psionics\null\_null.dm" +#include "code\modules\psionics\null\flooring.dm" #include "code\modules\reagents\chem_splash.dm" #include "code\modules\reagents\reagent_containers.dm" #include "code\modules\reagents\reagent_dispenser.dm" @@ -4109,6 +4141,7 @@ #include "yogstation\code\game\gamemodes\vampire\vampire_objectives.dm" #include "yogstation\code\game\gamemodes\vampire\vampire_other.dm" #include "yogstation\code\game\gamemodes\vampire\vampire_powers.dm" +#include "yogstation\code\game\machinery\psionic_awakener.dm" #include "yogstation\code\game\machinery\suit_storage_unit.dm" #include "yogstation\code\game\machinery\computer\arcade.dm" #include "yogstation\code\game\machinery\computer\atmos_sim.dm" @@ -4180,6 +4213,7 @@ #include "yogstation\code\game\objects\items\robot\robot_parts.dm" #include "yogstation\code\game\objects\items\stacks\dilithiumcrystal.dm" #include "yogstation\code\game\objects\items\stacks\sheets\leather.dm" +#include "yogstation\code\game\objects\items\stacks\sheets\nullspace_crystals.dm" #include "yogstation\code\game\objects\items\stacks\tiles\tile_types.dm" #include "yogstation\code\game\objects\items\storage\backpack.dm" #include "yogstation\code\game\objects\items\storage\bags.dm" diff --git a/yogstation/code/game/machinery/psionic_awakener.dm b/yogstation/code/game/machinery/psionic_awakener.dm new file mode 100644 index 0000000000000..fe992896c622e --- /dev/null +++ b/yogstation/code/game/machinery/psionic_awakener.dm @@ -0,0 +1,315 @@ +#define AWAKENER_TRIGGER "Awaken Latencies" +#define AWAKENER_COERCION "Reinforce Coercion" +#define AWAKENER_REDACTION "Reinforce Redaction" +#define AWAKENER_ENERGISTICS "Reinforce Energistics" +#define AWAKENER_PSYCHOKINESIS "Reinforce Psychokinesis" + +/obj/machinery/psionic_awakener + name = "psionic awakener" + desc = "An enclosed machine used trigger psionic latencies." + icon = 'icons/obj/machines/sleeper.dmi' + icon_state = "oldpod" + base_icon_state = "oldpod" + density = FALSE + state_open = TRUE + circuit = /obj/item/circuitboard/machine/psionic_awakener + clicksound = 'sound/machines/pda_button1.ogg' + + var/enter_message = "As the lid slams shut you become acutely aware of how dark and quiet it is inside." + var/open_sound = 'sound/machines/podopen.ogg' + var/close_sound = 'sound/machines/podclose.ogg' + + /// how much brain damage it does if it tries to unlock potential + var/brain_damage = 60 //effectively 50 because the default components reduce it by 10 + /// % chance of psionic power triggering being successful + var/trigger_power = 40 //effectively 50 because the default components increase it by 10 + + /// maximum amount of nullspace dust the machine can hold + var/nullspace_max = 200 + /// current amount of nullspace dust the machine has + var/nullspace_dust = 0 + + /// list of all treatments the awakener is capable of + var/list/treatments = list( + AWAKENER_TRIGGER = 0, + AWAKENER_COERCION = PSI_COERCION, + AWAKENER_REDACTION = PSI_REDACTION, + AWAKENER_ENERGISTICS = PSI_ENERGISTICS, + AWAKENER_PSYCHOKINESIS = PSI_PSYCHOKINESIS + ) + + /// currently selected outcome from pressing the button + var/active_treatment = "none" + + /// text print of the most recent activation result + var/recent_result + + COOLDOWN_DECLARE(next_trigger) + var/cooldown_duration = 10 SECONDS + +/obj/machinery/psionic_awakener/Initialize(mapload) + . = ..() + occupant_typecache = GLOB.typecache_living + update_appearance(UPDATE_ICON) + +/obj/machinery/psionic_awakener/update_icon_state() + icon_state = "[base_icon_state][state_open ? "-open" : null]" + return ..() + +/obj/machinery/psionic_awakener/RefreshParts() + var/E + for(var/obj/item/stock_parts/manipulator/B in component_parts) + E += B.rating + var/F + for(var/obj/item/stock_parts/matter_bin/B in component_parts) + F += B.rating + + brain_damage = initial(brain_damage) - (10 * E) + trigger_power = initial(trigger_power) + (10 * F) + +/obj/machinery/psionic_awakener/container_resist(mob/living/user) + visible_message(span_notice("[occupant] emerges from [src]!"), span_notice("You climb out of [src]!")) + open_machine() + +/obj/machinery/psionic_awakener/Exited(atom/movable/user) + if (!state_open && user == occupant) + container_resist(user) + +/obj/machinery/psionic_awakener/relaymove(mob/user) + if (!state_open) + container_resist(user) + +/obj/machinery/psionic_awakener/open_machine() + recent_result = null + if(!state_open && !panel_open) + flick("[base_icon_state]-anim", src) + if(open_sound) + playsound(src, open_sound, 40) + ..() + +/obj/machinery/psionic_awakener/close_machine(mob/user) + if((isnull(user) || istype(user)) && state_open && !panel_open) + flick("[base_icon_state]-anim", src) + ..(user) + var/mob/living/mob_occupant = occupant + if(mob_occupant && mob_occupant.stat != DEAD) + to_chat(occupant, "[enter_message]") + if(close_sound) + playsound(src, close_sound, 40) + +/obj/machinery/psionic_awakener/emp_act(severity) + . = ..() + if (. & EMP_PROTECT_SELF) + return + if(is_operational() && occupant) + open_machine() + +/obj/machinery/psionic_awakener/emag_act(mob/user, obj/item/card/emag/emag_card) + if(obj_flags & EMAGGED) + return FALSE + to_chat(user, span_danger("You disable the safeties of [src]...")) + obj_flags |= EMAGGED + return TRUE + +/obj/machinery/psionic_awakener/MouseDrop_T(mob/target, mob/user) + if(user.stat || !Adjacent(user) || !user.Adjacent(target) || !iscarbon(target) || !user.IsAdvancedToolUser()) + return + if(isliving(user)) + var/mob/living/L = user + if(!(L.mobility_flags & MOBILITY_STAND)) + return + close_machine(target) + +/obj/machinery/psionic_awakener/screwdriver_act(mob/living/user, obj/item/I) + . = TRUE + if(..()) + return + if(occupant) + to_chat(user, span_warning("[src] is currently occupied!")) + return + if(state_open) + to_chat(user, span_warning("[src] must be closed to [panel_open ? "close" : "open"] its maintenance hatch!")) + return + if(default_deconstruction_screwdriver(user, "[base_icon_state]-o", base_icon_state, I)) + return + return FALSE + +/obj/machinery/psionic_awakener/wrench_act(mob/living/user, obj/item/I) + . = ..() + if(default_change_direction_wrench(user, I)) + return TRUE + +/obj/machinery/psionic_awakener/crowbar_act(mob/living/user, obj/item/I) + . = ..() + if(default_pry_open(I)) + return TRUE + if(default_deconstruction_crowbar(I)) + return TRUE + +/obj/machinery/psionic_awakener/default_pry_open(obj/item/I) //wew + . = !(state_open || panel_open || (flags_1 & NODECONSTRUCT_1)) && I.tool_behaviour == TOOL_CROWBAR + if(.) + I.play_tool_sound(src, 50) + visible_message(span_notice("[usr] pries open [src]."), span_notice("You pry open [src].")) + open_machine() + +/obj/machinery/psionic_awakener/ui_state(mob/user) + return GLOB.notcontained_state + +/obj/machinery/psionic_awakener/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PsionicAwakener", name) + ui.open() + +/obj/machinery/psionic_awakener/AltClick(mob/user) + if(!user.canUseTopic(src, !issilicon(user))) + return + if(state_open) + close_machine() + else + open_machine() + +/obj/machinery/psionic_awakener/examine(mob/user) + . = ..() + . += span_notice("Alt-click [src] to [state_open ? "close" : "open"] it.") + +/obj/machinery/psionic_awakener/ui_data(mob/user) + var/list/data = list() + data["occupied"] = occupant ? 1 : 0 + data["open"] = state_open + data["ready"] = COOLDOWN_FINISHED(src, next_trigger) + data["timeleft"] = (COOLDOWN_TIMELEFT(src, next_trigger))/10 + data["result"] = recent_result + data["nullspace"] = nullspace_dust + data["nullspace_max"] = nullspace_max + data["active_treatment"] = active_treatment + + data["treatments"] = list() + for(var/T in treatments) + data["treatments"] += T + + data["occupant"] = list() + var/mob/living/mob_occupant = occupant + if(mob_occupant) + data["occupant"]["name"] = mob_occupant.name + switch(mob_occupant.stat) + if(CONSCIOUS) + data["occupant"]["stat"] = "Conscious" + data["occupant"]["statstate"] = "good" + if(SOFT_CRIT) + data["occupant"]["stat"] = "Conscious" + data["occupant"]["statstate"] = "average" + if(UNCONSCIOUS) + data["occupant"]["stat"] = "Unconscious" + data["occupant"]["statstate"] = "average" + if(DEAD) + data["occupant"]["stat"] = "Dead" + data["occupant"]["statstate"] = "bad" + data["occupant"]["brainLoss"] = mob_occupant.getOrganLoss(ORGAN_SLOT_BRAIN) + + data["treatment_cost"] = get_cost(mob_occupant) + return data + +/obj/machinery/psionic_awakener/proc/get_cost(mob/living/mob_occupant) + var/faculty = treatments[active_treatment] + var/cost = 0 + if(faculty) + cost = 50 //costs 50 base to unlock a specific faculty + if(mob_occupant.psi) + var/faculty_rank = mob_occupant.psi.get_rank(faculty) + cost += max(faculty_rank-1, 0) * 35 //cost 35 more dust per rank beyond that + if(faculty_rank >= PSI_RANK_GRANDMASTER) //could maybe tweak this to be exponential scaling rather than linear + cost += 50 //extra 50 to go to paramount (since paramount is fuckin strong) + if(faculty_rank >= PSI_RANK_PARAMOUNT) + cost = 9999999 + return cost + +/obj/machinery/psionic_awakener/ui_act(action, params) + if(..()) + return + var/mob/living/mob_occupant = occupant + switch(action) + if("door") + if(state_open) + close_machine() + else + open_machine() + . = TRUE + if("set") + var/treatment = params["treatment"] + if(!is_operational() || isnull(treatment)) + return + active_treatment = treatment + . = TRUE + if("activate") + if(!is_operational() || !mob_occupant || !mob_occupant.mind) + return + + switch(active_treatment) + if("none") + return + if(AWAKENER_TRIGGER) + trigger_psionics(mob_occupant) + else + empower_psionics(mob_occupant) + . = TRUE + +/obj/machinery/psionic_awakener/proc/trigger_psionics(mob/living/mob_occupant) + if(!mob_occupant || mob_occupant.stat == DEAD || !COOLDOWN_FINISHED(src, next_trigger)) + return + COOLDOWN_START(src, next_trigger, cooldown_duration) + + if(!mob_occupant.psi || !LAZYLEN(mob_occupant.psi.latencies)) + visible_message(span_notice("[src] whirrs quietly as it fails to detect any untapped psionic potential.")) + playsound(src, 'sound/effects/psi/power_fail.ogg', 50, TRUE, 2) + recent_result = "Incapable" + return + + var/actual_power = trigger_power + if(obj_flags & EMAGGED) + actual_power = 100 + + if(obj_flags & EMAGGED) + playsound(src, 'sound/effects/gravhit.ogg', 30, TRUE, 5) + visible_message(span_notice("[src] makes some unusual noises.")) + mob_occupant.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(brain_damage, brain_damage * 2)) + + if(mob_occupant?.psi?.check_latency_trigger(actual_power, name, brain_damage)) + visible_message(span_notice("[src] whirrs loudly as it successfully triggers latent psionic abilities in [mob_occupant].")) + playsound(src, 'sound/effects/psi/power_evoke.ogg', 50, TRUE, 2) + playsound(src, 'sound/effects/psi/power_fabrication.ogg', 50, TRUE, 2) + log_admin("[name] triggered psi latencies for [key_name(mob_occupant)].") + message_admins(span_adminnotice("[ADMIN_FLW(name)] triggered psi latencies for [key_name(mob_occupant)].")) + recent_result = "Successful" + else + visible_message(span_notice("[src] whirrs quietly as it fails to unlock any psionic potential.")) + playsound(src, 'sound/effects/psi/power_fail.ogg', 50, TRUE, 2) + recent_result = "Failure" + +/obj/machinery/psionic_awakener/proc/empower_psionics(mob/living/mob_occupant) + if(!mob_occupant || mob_occupant.stat == DEAD || !COOLDOWN_FINISHED(src, next_trigger)) + return + + var/faculty = treatments[active_treatment] + var/cost = get_cost(mob_occupant) + + if(nullspace_dust < cost) + return + + COOLDOWN_START(src, next_trigger, cooldown_duration) + + nullspace_dust -= cost + + + var/new_rank = max(mob_occupant?.psi?.get_rank(faculty) + 1, PSI_RANK_OPERANT) + mob_occupant.set_psi_rank(faculty, new_rank) + mob_occupant.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(brain_damage, brain_damage * 2)) + to_chat(mob_occupant, span_danger("Your head throbs as [src] messes with your brain!")) + + visible_message(span_notice("[src] whirrs loudly as it successfully [new_rank == PSI_RANK_OPERANT ? "awakens" : "reinforces"] [mob_occupant]'s [faculty] faculty.")) + playsound(src, 'sound/effects/psi/power_evoke.ogg', 50, TRUE, 2) + playsound(src, 'sound/effects/psi/power_fabrication.ogg', 50, TRUE, 2) + log_admin("[name] upgraded psi [faculty] for [key_name(mob_occupant)].") + message_admins(span_adminnotice("[ADMIN_FLW(name)] upgraded psi [faculty] for [key_name(mob_occupant)].")) + recent_result = "Successful" diff --git a/yogstation/code/game/objects/items/stacks/sheets/nullspace_crystals.dm b/yogstation/code/game/objects/items/stacks/sheets/nullspace_crystals.dm new file mode 100644 index 0000000000000..1d14ce9b7b44f --- /dev/null +++ b/yogstation/code/game/objects/items/stacks/sheets/nullspace_crystals.dm @@ -0,0 +1,48 @@ +/obj/item/nullspace_crystal + name = "null skull" + desc = "a skull of an ancient psionic user, grants a small amount of nulldust when ground up." + icon = 'icons/obj/telescience.dmi' + icon_state = "null_skull" + w_class = WEIGHT_CLASS_TINY + ///how much nullspace dust does each skull give when used on the psionic awakener + var/dust = 5 + +/obj/item/nullspace_crystal/attack_atom(atom/attacked_atom, mob/living/user, params) + if(istype(attacked_atom, /obj/machinery/psionic_awakener)) + var/obj/machinery/psionic_awakener/cart = attacked_atom + cart.nullspace_dust += dust + to_chat(user, span_notice("You force the [name] into the psionic awakener's grinding port, crushing it to microscopic pieces.")) + qdel(src) + return + . = ..() + +/obj/item/nullspace_crystal/brilliant + name = "fresh null skull" + desc = "a fresh skull of a weak psionic user, grants a fair amount of nulldust when ground up." + icon_state = "fresh_null_skull" + w_class = WEIGHT_CLASS_TINY + dust = 10 + +/obj/item/nullspace_crystal/prismatic + name = "aged null skull" + desc = "an older skull of an adept psionic user, grants a lot of nulldust when ground up." + icon_state = "aged_null_skull" + w_class = WEIGHT_CLASS_SMALL + dust = 15 + +/obj/item/nullspace_crystal/true + name = "living null skull" + desc = "a pitch black skull of a powerful psionic user, looking into it's eye sockets make your cerebellum burn. Grants a huge boon of nulldust when ground up." + icon_state = "psionic_null_skull" + w_class = WEIGHT_CLASS_NORMAL + dust = 50 + +/obj/effect/spawner/lootdrop/nullspace_crystal_spawner + name = "nullskull spawner" + lootdoubles = FALSE + + loot = list( + /obj/item/nullspace_crystal = 75, + /obj/item/nullspace_crystal/brilliant = 20, + /obj/item/nullspace_crystal/prismatic = 4, + /obj/item/nullspace_crystal/true = 1) diff --git a/yogstation/code/modules/antagonists/darkspawn/darkspawn_progenitor.dm b/yogstation/code/modules/antagonists/darkspawn/darkspawn_progenitor.dm index 7806848b9ad7d..bf207e1b15aad 100644 --- a/yogstation/code/modules/antagonists/darkspawn/darkspawn_progenitor.dm +++ b/yogstation/code/modules/antagonists/darkspawn/darkspawn_progenitor.dm @@ -91,6 +91,7 @@ //add passive traits, elements, and components ADD_TRAIT(src, TRAIT_HOLY, INNATE_TRAIT) //sorry no magic ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT) //so people can actually look at the sprite without the weird bobbing up and down + ADD_TRAIT(src, TRAIT_PSIONICALLY_IMMUNE, INNATE_TRAIT) //so no psionic fuckery can happen AddElement(/datum/element/death_explosion, 20, 20, 20) //with INFINITY health, they're not really able to die, but IF THEY DO AddComponent(/datum/component/light_eater) diff --git a/yogstation/code/modules/jobs/job_types/psychiatrist.dm b/yogstation/code/modules/jobs/job_types/psychiatrist.dm index 1bdb441a54067..c13ed0c8b2d3b 100644 --- a/yogstation/code/modules/jobs/job_types/psychiatrist.dm +++ b/yogstation/code/modules/jobs/job_types/psychiatrist.dm @@ -40,6 +40,13 @@ /datum/job/psych/proc/GaxStationChanges() // I'M SORRY return TRUE +/datum/job/psych/after_spawn(mob/living/H, mob/M, latejoin = FALSE) + . = ..() + if(!isipc(H)) + H.set_psi_rank(PSI_REDACTION, PSI_RANK_OPERANT) + if(H.psi) + to_chat(M, "You are psionically awakened, part of a tiny minority, and you are the first and only exposure most of the crew will have to the mentally gifted.") + /datum/outfit/job/psych name = "Psych" jobtype = /datum/job/psych @@ -49,3 +56,4 @@ l_hand = /obj/item/storage/briefcase glasses = /obj/item/clothing/glasses/regular ears = /obj/item/radio/headset/headset_med + implants = list(/obj/item/implant/psi_control/psych) diff --git a/yogstation/code/modules/mob/living/carbon/human/species_types/plantpeople.dm b/yogstation/code/modules/mob/living/carbon/human/species_types/plantpeople.dm index 7371d816dc5e7..29bd1e062fe57 100644 --- a/yogstation/code/modules/mob/living/carbon/human/species_types/plantpeople.dm +++ b/yogstation/code/modules/mob/living/carbon/human/species_types/plantpeople.dm @@ -31,6 +31,10 @@ wings_icon = "Plant" wings_detail = "Plantdetails" inert_mutation = SAPBLOOD + + latency_chance = 50 + possible_faculties = list(PSI_REDACTION) + starting_psi_level = PSI_RANK_LATENT var/no_light_heal = FALSE var/light_heal_multiplier = 1 diff --git a/yogstation/code/modules/mob/living/carbon/human/species_types/preternis/preternis.dm b/yogstation/code/modules/mob/living/carbon/human/species_types/preternis/preternis.dm index 6c806c3c11e32..b6d4b2fdd1ea3 100644 --- a/yogstation/code/modules/mob/living/carbon/human/species_types/preternis/preternis.dm +++ b/yogstation/code/modules/mob/living/carbon/human/species_types/preternis/preternis.dm @@ -27,6 +27,11 @@ punchstunthreshold = 7 //technically better stunning siemens_coeff = 1.75 //Circuits REALLY don't like extra electricity flying around + //psionics + latency_chance = 100 + possible_faculties = list(PSI_COERCION) + starting_psi_level = PSI_RANK_LATENT + //organs mutanteyes = /obj/item/organ/eyes/robotic/preternis mutantlungs = /obj/item/organ/lungs/preternis diff --git a/yogstation/code/modules/mob/living/carbon/human/species_types/vox.dm b/yogstation/code/modules/mob/living/carbon/human/species_types/vox.dm index b6cb8a8ffd49b..40132ae87d95a 100644 --- a/yogstation/code/modules/mob/living/carbon/human/species_types/vox.dm +++ b/yogstation/code/modules/mob/living/carbon/human/species_types/vox.dm @@ -42,6 +42,10 @@ smells_like = "musty quills" liked_food = MEAT | FRIED species_language_holder = /datum/language_holder/vox + + latency_chance = 35 + possible_faculties = list(PSI_COERCION, PSI_REDACTION) + starting_psi_level = PSI_RANK_LATENT /datum/species/vox/get_species_description() return "The Vox are remnants of an ancient race, that originate from arkships. \ diff --git a/yogstation/icons/obj/stack_objects.dmi b/yogstation/icons/obj/stack_objects.dmi index 3825fad165bc1..dabdfbf5a97bc 100644 Binary files a/yogstation/icons/obj/stack_objects.dmi and b/yogstation/icons/obj/stack_objects.dmi differ