From 709e9d130c15066dba605e7db2d54606ebcf5469 Mon Sep 17 00:00:00 2001 From: Will <7099514+Willburd@users.noreply.github.com> Date: Thu, 15 May 2025 03:17:39 -0400 Subject: [PATCH] Hallucination Refactor (#17661) * begone accursed beast * cleanup and notes * screen object fix * varset clear * don't raw send sounds, spark on door sound, pain hallucination * lengthen this a bit * more pain messages, become jittery * note * lets clean that up to * Revert "lets clean that up to" This reverts commit 2409e4187006d9d0e29a5385a7037f17f07f0f1d. * cleanup and define * use accessors * death isn't working so have toxins instead, it's scarier * don't actually clean these up, because of how xenochi works * additional protection * hunger joke * Use get_component instead of a cache * empty scope removed * Updated xenochi code to use hallu component --------- Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> --- code/__defines/hallucination.dm | 8 + code/controllers/subsystems/motion_tracker.dm | 8 +- code/datums/components/species/xenochimera.dm | 192 +------ code/game/machinery/doors/airlock.dm | 1 + .../game/objects/effects/client_only_image.dm | 23 + code/game/objects/effects/motion_echo.dm | 23 +- code/modules/flufftext/Hallucination.dm | 497 ++++-------------- code/modules/flufftext/fake_attacker.dm | 218 ++++++++ .../modules/flufftext/hallucination_events.dm | 302 +++++++++++ code/modules/mob/living/carbon/human/life.dm | 22 +- code/modules/mob/living/living_defines.dm | 1 - vorestation.dme | 4 + 12 files changed, 689 insertions(+), 610 deletions(-) create mode 100644 code/__defines/hallucination.dm create mode 100644 code/game/objects/effects/client_only_image.dm create mode 100644 code/modules/flufftext/fake_attacker.dm create mode 100644 code/modules/flufftext/hallucination_events.dm diff --git a/code/__defines/hallucination.dm b/code/__defines/hallucination.dm new file mode 100644 index 0000000000..8bd739f1ac --- /dev/null +++ b/code/__defines/hallucination.dm @@ -0,0 +1,8 @@ +#define HALLUCINATION_THRESHOLD 20 +#define XENOCHIFERAL_THRESHOLD 10 + +#define HUD_HALLUCINATION_NONE 0 +#define HUD_HALLUCINATION_CRIT 1 +#define HUD_HALLUCINATION_TOXIN 2 +#define HUD_HALLUCINATION_OXY 3 +#define HUD_HALLUCINATION_ONFIRE 4 diff --git a/code/controllers/subsystems/motion_tracker.dm b/code/controllers/subsystems/motion_tracker.dm index 2d3127143b..5cd4b1459a 100644 --- a/code/controllers/subsystems/motion_tracker.dm +++ b/code/controllers/subsystems/motion_tracker.dm @@ -43,10 +43,12 @@ SUBSYSTEM_DEF(motiontracker) if(Rt && At && count) while(count-- > 0) // Place at root turf offset from signal responder's turf using px offsets. So it will show up over visblocking. - var/image/motion_echo/E = new /image/motion_echo('icons/effects/effects.dmi', Rt, "motion_echo", OBFUSCATION_LAYER, SOUTH) + var/image/client_only/motion_echo/E = new /image/client_only/motion_echo('icons/effects/effects.dmi', Rt, "motion_echo", OBFUSCATION_LAYER, SOUTH) E.place_from_root(At) - for(var/datum/weakref/C in clients) - E.append_client(C) + for(var/datum/weakref/CW in clients) + var/client/C = CW?.resolve() + if(C) + E.append_client(C) currentrun.Remove(key) expended_echos[key] = data if(MC_TICK_CHECK) diff --git a/code/datums/components/species/xenochimera.dm b/code/datums/components/species/xenochimera.dm index 1bfe5e77b3..b6927b16dd 100644 --- a/code/datums/components/species/xenochimera.dm +++ b/code/datums/components/species/xenochimera.dm @@ -237,196 +237,10 @@ if(!owner.stat) owner.emote("twitch") - /datum/component/xenochimera/proc/handle_feral() - if(owner.handling_hal) return - owner.handling_hal = 1 - - if(owner.client && feral >= 10) // largely a copy of handle_hallucinations() without the fake attackers. Unlike hallucinations, only fires once - if they're still feral they'll get hit again anyway. - spawn(rand(200,500)/(feral/10)) - if(!feral) return //just to avoid fuckery in the event that they un-feral in the time it takes for the spawn to proc - var/halpick = rand(1,100) - switch(halpick) - if(0 to 15) //15% chance - //Screwy HUD - //to_chat(src, "Screwy HUD") - owner.hal_screwyhud = pick(1,2,3,3,4,4) - spawn(rand(100,250)) - owner.hal_screwyhud = 0 - if(16 to 25) //10% chance - //Strange items - //to_chat(src, "Traitor Items") - if(!owner.halitem) - owner.halitem = new - var/list/slots_free = list(ui_lhand,ui_rhand) - if(owner.l_hand) slots_free -= ui_lhand - if(owner.r_hand) slots_free -= ui_rhand - if(!owner.belt) slots_free += ui_belt - if(!owner.l_store) slots_free += ui_storage1 - if(!owner.r_store) slots_free += ui_storage2 - if(slots_free.len) - owner.halitem.screen_loc = pick(slots_free) - owner.halitem.layer = 50 - switch(rand(1,6)) - if(1) //revolver - owner.halitem.icon = 'icons/obj/gun.dmi' - owner.halitem.icon_state = "revolver" - owner.halitem.name = "Revolver" - if(2) //c4 - owner.halitem.icon = 'icons/obj/assemblies.dmi' - owner.halitem.icon_state = "plastic-explosive0" - owner.halitem.name = "Mysterious Package" - if(prob(25)) - owner.halitem.icon_state = "c4small_1" - if(3) //sword - owner.halitem.icon = 'icons/obj/weapons.dmi' - owner.halitem.icon_state = "sword1" - owner.halitem.name = "Sword" - if(4) //stun baton - owner.halitem.icon = 'icons/obj/weapons.dmi' - owner.halitem.icon_state = "stunbaton" - owner.halitem.name = "Stun Baton" - if(5) //emag - owner.halitem.icon = 'icons/obj/card.dmi' - owner.halitem.icon_state = "emag" - owner.halitem.name = "Cryptographic Sequencer" - if(6) //flashbang - owner.halitem.icon = 'icons/obj/grenade.dmi' - owner.halitem.icon_state = "flashbang1" - owner.halitem.name = "Flashbang" - if(owner.client) owner.client.screen += owner.halitem - spawn(rand(100,250)) - if(owner.client) - owner.client.screen -= owner.halitem - owner.halitem = null - if(26 to 35) //10% chance - //Flashes of danger - //to_chat(src, "Danger Flash") - if(!owner.halimage) - var/list/possible_points = list() - for(var/turf/simulated/floor/F in view(owner,world.view)) - possible_points += F - if(possible_points.len) - var/turf/simulated/floor/target = pick(possible_points) - - switch(rand(1,3)) - if(1) - //to_chat(src, "Space") - owner.halimage = image('icons/turf/space.dmi',target,"[rand(1,25)]",TURF_LAYER) - if(2) - //to_chat(src, "Fire") - owner.halimage = image('icons/effects/fire.dmi',target,"1",TURF_LAYER) - if(3) - //to_chat(src, "C4") - owner.halimage = image('icons/obj/assemblies.dmi',target,"plastic-explosive2",OBJ_LAYER+0.01) - - - if(owner.client) owner.client.images += owner.halimage - spawn(rand(10,50)) //Only seen for a brief moment. - if(owner.client) owner.client.images -= owner.halimage - owner.halimage = null - - if(36 to 55) //20% chance - //Strange audio - //to_chat(src, "Strange Audio") - switch(rand(1,12)) - if(1) owner << 'sound/machines/door/old_airlock.ogg' - if(2) - if(prob(50))owner << 'sound/effects/Explosion1.ogg' - else owner << 'sound/effects/Explosion2.ogg' - if(3) owner << 'sound/effects/explosionfar.ogg' - if(4) owner << 'sound/effects/Glassbr1.ogg' - if(5) owner << 'sound/effects/Glassbr2.ogg' - if(6) owner << 'sound/effects/Glassbr3.ogg' - if(7) owner << 'sound/machines/twobeep.ogg' - if(8) owner << 'sound/machines/door/windowdoor.ogg' - if(9) - //To make it more realistic, I added two gunshots (enough to kill) - owner << 'sound/weapons/Gunshot1.ogg' - spawn(rand(10,30)) - owner << 'sound/weapons/Gunshot2.ogg' - if(10) owner << 'sound/weapons/smash.ogg' - if(11) - //Same as above, but with tasers. - owner << 'sound/weapons/Taser.ogg' - spawn(rand(10,30)) - owner << 'sound/weapons/Taser.ogg' - //Rare audio - if(12) - //These sounds are (mostly) taken from Hidden: Source - var/list/creepyasssounds = list('sound/effects/ghost.ogg', 'sound/effects/ghost2.ogg', 'sound/effects/Heart Beat.ogg', 'sound/effects/screech.ogg',\ - 'sound/hallucinations/behind_you1.ogg', 'sound/hallucinations/behind_you2.ogg', 'sound/hallucinations/far_noise.ogg', 'sound/hallucinations/growl1.ogg', 'sound/hallucinations/growl2.ogg',\ - 'sound/hallucinations/growl3.ogg', 'sound/hallucinations/im_here1.ogg', 'sound/hallucinations/im_here2.ogg', 'sound/hallucinations/i_see_you1.ogg', 'sound/hallucinations/i_see_you2.ogg',\ - 'sound/hallucinations/look_up1.ogg', 'sound/hallucinations/look_up2.ogg', 'sound/hallucinations/over_here1.ogg', 'sound/hallucinations/over_here2.ogg', 'sound/hallucinations/over_here3.ogg',\ - 'sound/hallucinations/turn_around1.ogg', 'sound/hallucinations/turn_around2.ogg', 'sound/hallucinations/veryfar_noise.ogg', 'sound/hallucinations/wail.ogg') - owner << pick(creepyasssounds) - if(56 to 60) //5% chance - //Flashes of danger - //to_chat(src, "Danger Flash") - if(!owner.halbody) - var/list/possible_points = list() - for(var/turf/simulated/floor/F in view(owner,world.view)) - possible_points += F - if(possible_points.len) - var/turf/simulated/floor/target = pick(possible_points) - switch(rand(1,4)) - if(1) - owner.halbody = image('icons/mob/human.dmi',target,"husk_l",TURF_LAYER) - if(2,3) - owner.halbody = image('icons/mob/human.dmi',target,"husk_s",TURF_LAYER) - if(4) - owner.halbody = image('icons/mob/alien.dmi',target,"alienother",TURF_LAYER) - // if(5) - // halbody = image('xcomalien.dmi',target,"chryssalid",TURF_LAYER) - - if(owner.client) owner.client.images += owner.halbody - spawn(rand(50,80)) //Only seen for a brief moment. - if(owner.client) owner.client.images -= owner.halbody - owner.halbody = null - if(61 to 85) //25% chance - //food - if(!owner.halbody) - var/list/possible_points = list() - for(var/turf/simulated/floor/F in view(owner,world.view)) - possible_points += F - if(possible_points.len) - var/turf/simulated/floor/target = pick(possible_points) - switch(rand(1,10)) - if(1) - owner.halbody = image('icons/mob/animal.dmi',target,"cow",TURF_LAYER) - if(2) - owner.halbody = image('icons/mob/animal.dmi',target,"chicken",TURF_LAYER) - if(3) - owner.halbody = image('icons/obj/food.dmi',target,"bigbiteburger",TURF_LAYER) - if(4) - owner.halbody = image('icons/obj/food.dmi',target,"meatbreadslice",TURF_LAYER) - if(5) - owner.halbody = image('icons/obj/food.dmi',target,"sausage",TURF_LAYER) - if(6) - owner.halbody = image('icons/obj/food.dmi',target,"bearmeat",TURF_LAYER) - if(7) - owner.halbody = image('icons/obj/food.dmi',target,"fishfillet",TURF_LAYER) - if(8) - owner.halbody = image('icons/obj/food.dmi',target,"meat",TURF_LAYER) - if(9) - owner.halbody = image('icons/obj/food.dmi',target,"meatstake",TURF_LAYER) - if(10) - owner.halbody = image('icons/obj/food.dmi',target,"monkeysdelight",TURF_LAYER) - - if(owner.client) owner.client.images += owner.halbody - spawn(rand(50,80)) //Only seen for a brief moment. - if(owner.client) owner.client.images -= owner.halbody - owner.halbody = null - if(86 to 100) //15% chance - //hear voices. Could make the voice pick from nearby creatures, but nearby creatures make feral hallucinations rare so don't bother. - var/list/hiddenspeakers = list("Someone distant", "A voice nearby","A familiar voice", "An echoing voice", "A cautious voice", "A scared voice", "Someone around the corner", "Someone", "Something", "Something scary", "An urgent voice", "An angry voice") - var/list/speakerverbs = list("calls out", "yells", "screams", "exclaims", "shrieks", "shouts", "hisses", "snarls") - var/list/spookyphrases = list("It's over here!","Stop it!", "Hunt it down!", "Get it!", "Quick, over here!", "Anyone there?", "Who's there?", "Catch that thing!", "Stop it! Kill it!", "Anyone there?", "Where is it?", "Find it!", "There it is!") - to_chat(owner, span_game(span_say(span_name(pick(hiddenspeakers)) + " [pick(speakerverbs)], \"[pick(spookyphrases)]\""))) - - - owner.handling_hal = 0 - return + if(QDELETED(owner) || owner.get_hallucination_component() || !owner.client || feral < XENOCHIFERAL_THRESHOLD) + return + owner.LoadComponent(/datum/component/hallucinations/xenochimera) /obj/screen/xenochimera icon = 'icons/mob/chimerahud.dmi' diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index d40fab23a8..2e70cac02b 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -672,6 +672,7 @@ About the new airlock wires panel: return else if(user.hallucination > 50 && prob(10) && src.operating == 0) to_chat(user, span_danger("You feel a powerful shock course through your body!")) + user.playsound_local(get_turf(user), get_sfx("sparks"), vol = 75) user.halloss += 10 user.stunned += 10 return diff --git a/code/game/objects/effects/client_only_image.dm b/code/game/objects/effects/client_only_image.dm new file mode 100644 index 0000000000..3b87c721c1 --- /dev/null +++ b/code/game/objects/effects/client_only_image.dm @@ -0,0 +1,23 @@ +/* +* Sending images to clients can cause memory leaks if not handled safely. +* This is a wrapper for handling it safely. Mostly used by self deleting effects. +*/ +/image/client_only + var/list/clients = list() + +/image/client_only/proc/append_client(var/client/C) + C.images += src + clients.Add(WEAKREF(C)) + +/image/client_only/Destroy(force) + . = ..() + for(var/datum/weakref/CW in clients) + var/client/C = CW?.resolve() + if(C) + C.images -= src + +// Mostly for motion echos, but someone will probably find another use for it... So parent type gets it instead! +/image/client_only/proc/place_from_root(var/turf/At) + var/rand_limit = 12 + pixel_x += ((At.x - loc.x) * 32) + rand(-rand_limit,rand_limit) + pixel_y += ((At.y - loc.y) * 32) + rand(-rand_limit,rand_limit) diff --git a/code/game/objects/effects/motion_echo.dm b/code/game/objects/effects/motion_echo.dm index 19face9b88..211af8975c 100644 --- a/code/game/objects/effects/motion_echo.dm +++ b/code/game/objects/effects/motion_echo.dm @@ -1,27 +1,8 @@ -/image/motion_echo +/image/client_only/motion_echo plane = PLANE_FULLSCREEN mouse_opacity = MOUSE_OPACITY_TRANSPARENT appearance_flags = (RESET_COLOR|PIXEL_SCALE|KEEP_APART) - var/list/clients = list() -/image/motion_echo/New(icon, loc, icon_state, layer, dir) +/image/client_only/motion_echo/New(icon, loc, icon_state, layer, dir) . = ..() QDEL_IN(src, 2 SECONDS) - -/image/motion_echo/proc/place_from_root(var/turf/At) - var/rand_limit = 12 - pixel_x += ((At.x - loc.x) * 32) + rand(-rand_limit,rand_limit) - pixel_y += ((At.y - loc.y) * 32) + rand(-rand_limit,rand_limit) - -/image/motion_echo/proc/append_client(var/datum/weakref/C) - var/client/CW = C?.resolve() - if(CW) - CW.images += src - clients.Add(C) - -/image/motion_echo/Destroy(force) - . = ..() - for(var/datum/weakref/C in clients) - var/client/CW = C?.resolve() - if(CW) - CW.images -= src diff --git a/code/modules/flufftext/Hallucination.dm b/code/modules/flufftext/Hallucination.dm index 50ef7ee243..a5c3204dc3 100644 --- a/code/modules/flufftext/Hallucination.dm +++ b/code/modules/flufftext/Hallucination.dm @@ -8,407 +8,132 @@ Brief flashes of fire/space/bombs/c4/dangerous shit (done) Items that are rare/traitorous/don't exist appearing in your inventory slots (done) Strange audio (should be rare) (done) Gunshots/explosions/opening doors/less rare audio (done) - */ -/mob/living/carbon/var - image/halimage - image/halbody - obj/halitem - hal_screwyhud = 0 //1 - critical, 2 - dead, 3 - oxygen indicator, 4 - toxin indicator - handling_hal = 0 - hal_crit = 0 +/datum/component/hallucinations + dupe_mode = COMPONENT_DUPE_UNIQUE // First come first serve -/mob/living/carbon/proc/handle_hallucinations() - if(handling_hal) return - handling_hal = 1 - while(client && hallucination > 20) - sleep(rand(200,500)/(hallucination/25)) - var/halpick = rand(1,100) - switch(halpick) - if(0 to 15) - //Screwy HUD - //to_chat(src, "Screwy HUD") - hal_screwyhud = pick(1,2,3,3,4,4) - spawn(rand(100,250)) - hal_screwyhud = 0 - if(16 to 25) - //Strange items - //to_chat(src, "Traitor Items") - if(!halitem) - halitem = new - var/list/slots_free = list(ui_lhand,ui_rhand) - if(l_hand) slots_free -= ui_lhand - if(r_hand) slots_free -= ui_rhand - if(ishuman(src)) - var/mob/living/carbon/human/H = src - if(!H.belt) slots_free += ui_belt - if(!H.l_store) slots_free += ui_storage1 - if(!H.r_store) slots_free += ui_storage2 - if(slots_free.len) - halitem.screen_loc = pick(slots_free) - halitem.hud_layerise() - switch(rand(1,6)) - if(1) //revolver - halitem.icon = 'icons/obj/gun.dmi' - halitem.icon_state = "revolver" - halitem.name = "Revolver" - if(2) //c4 - halitem.icon = 'icons/obj/assemblies.dmi' - halitem.icon_state = "plastic-explosive0" - halitem.name = "Mysterious Package" - if(prob(25)) - halitem.icon_state = "c4small_1" - if(3) //sword - halitem.icon = 'icons/obj/weapons.dmi' - halitem.icon_state = "sword1" - halitem.name = "Sword" - if(4) //stun baton - halitem.icon = 'icons/obj/weapons.dmi' - halitem.icon_state = "stunbaton" - halitem.name = "Stun Baton" - if(5) //emag - halitem.icon = 'icons/obj/card.dmi' - halitem.icon_state = "emag" - halitem.name = "Cryptographic Sequencer" - if(6) //flashbang - halitem.icon = 'icons/obj/grenade.dmi' - halitem.icon_state = "flashbang1" - halitem.name = "Flashbang" - if(client) client.screen += halitem - spawn(rand(100,250)) - if(client) - client.screen -= halitem - halitem = null - if(26 to 40) - //Flashes of danger - //to_chat(src, "Danger Flash") - if(!halimage) - var/list/possible_points = list() - for(var/turf/simulated/floor/F in view(src,world.view)) - possible_points += F - if(possible_points.len) - var/turf/simulated/floor/target = pick(possible_points) + VAR_PRIVATE/mob/living/carbon/human/our_human = null + VAR_PRIVATE/datum/weakref/halimage + VAR_PRIVATE/datum/weakref/halbody + VAR_PRIVATE/list/halitem = list() // weakref pair of obj-key, client-value - switch(rand(1,3)) - if(1) - //to_chat(src, "Space") - halimage = image('icons/turf/space.dmi',target,"[rand(1,25)]",TURF_LAYER) - if(2) - //to_chat(src, "Fire") - halimage = image('icons/effects/fire.dmi',target,"1",TURF_LAYER) - if(3) - //to_chat(src, "C4") - halimage = image('icons/obj/assemblies.dmi',target,"plastic-explosive2",OBJ_LAYER+0.01) + VAR_PRIVATE/hal_crit = FALSE + VAR_PRIVATE/hal_screwyhud = HUD_HALLUCINATION_NONE +/datum/component/hallucinations/Initialize() + if(!ishuman(parent)) + return COMPONENT_INCOMPATIBLE + our_human = parent + make_timer() - if(client) client.images += halimage - spawn(rand(10,50)) //Only seen for a brief moment. - if(client) client.images -= halimage - halimage = null - - - if(41 to 65) - //Strange audio - //to_chat(src, "Strange Audio") - switch(rand(1,12)) - if(1) src << 'sound/machines/door/old_airlock.ogg' - if(2) - if(prob(50))src << 'sound/effects/Explosion1.ogg' - else src << 'sound/effects/Explosion2.ogg' - if(3) src << 'sound/effects/explosionfar.ogg' - if(4) src << 'sound/effects/Glassbr1.ogg' - if(5) src << 'sound/effects/Glassbr2.ogg' - if(6) src << 'sound/effects/Glassbr3.ogg' - if(7) src << 'sound/machines/twobeep.ogg' - if(8) src << 'sound/machines/door/windowdoor.ogg' - if(9) - //To make it more realistic, I added two gunshots (enough to kill) - src << 'sound/weapons/Gunshot1.ogg' - spawn(rand(10,30)) - src << 'sound/weapons/Gunshot2.ogg' - if(10) src << 'sound/weapons/smash.ogg' - if(11) - //Same as above, but with tasers. - src << 'sound/weapons/Taser.ogg' - spawn(rand(10,30)) - src << 'sound/weapons/Taser.ogg' - //Rare audio - if(12) -//These sounds are (mostly) taken from Hidden: Source - var/list/creepyasssounds = list('sound/effects/ghost.ogg', 'sound/effects/ghost2.ogg', 'sound/effects/Heart Beat.ogg', 'sound/effects/screech.ogg',\ - 'sound/hallucinations/behind_you1.ogg', 'sound/hallucinations/behind_you2.ogg', 'sound/hallucinations/far_noise.ogg', 'sound/hallucinations/growl1.ogg', 'sound/hallucinations/growl2.ogg',\ - 'sound/hallucinations/growl3.ogg', 'sound/hallucinations/im_here1.ogg', 'sound/hallucinations/im_here2.ogg', 'sound/hallucinations/i_see_you1.ogg', 'sound/hallucinations/i_see_you2.ogg',\ - 'sound/hallucinations/look_up1.ogg', 'sound/hallucinations/look_up2.ogg', 'sound/hallucinations/over_here1.ogg', 'sound/hallucinations/over_here2.ogg', 'sound/hallucinations/over_here3.ogg',\ - 'sound/hallucinations/turn_around1.ogg', 'sound/hallucinations/turn_around2.ogg', 'sound/hallucinations/veryfar_noise.ogg', 'sound/hallucinations/wail.ogg') - src << pick(creepyasssounds) - if(66 to 70) - //Flashes of danger - //to_chat(src, "Danger Flash") - if(!halbody) - var/list/possible_points = list() - for(var/turf/simulated/floor/F in view(src,world.view)) - possible_points += F - if(possible_points.len) - var/turf/simulated/floor/target = pick(possible_points) - switch(rand(1,4)) - if(1) - halbody = image('icons/mob/human.dmi',target,"husk_l",TURF_LAYER) - if(2,3) - halbody = image('icons/mob/human.dmi',target,"husk_s",TURF_LAYER) - if(4) - halbody = image('icons/mob/alien.dmi',target,"alienother",TURF_LAYER) - // if(5) - // halbody = image('xcomalien.dmi',target,"chryssalid",TURF_LAYER) - - if(client) client.images += halbody - spawn(rand(50,80)) //Only seen for a brief moment. - if(client) client.images -= halbody - halbody = null - if(71 to 72) - //Fake death -// src.sleeping_willingly = 1 - SetSleeping(20) - hal_crit = 1 - hal_screwyhud = 1 - spawn(rand(50,100)) -// src.sleeping_willingly = 0 - SetSleeping(0) - hal_crit = 0 - hal_screwyhud = 0 - handling_hal = 0 - - - - -/*obj/machinery/proc/mockpanel(list/buttons,start_txt,end_txt,list/mid_txts) - - if(!mocktxt) - - mocktxt = "" - - var/possible_txt = list("Launch Escape Pods","Self-Destruct Sequence","\[Swipe ID\]","De-Monkify",\ - "Reticulate Splines","Plasma","Open Valve","Lockdown","Nerf Airflow","Kill Traitor","Nihilism",\ - "OBJECTION!","Arrest Stephen Bowman","Engage Anti-Trenna Defenses","Increase Site Manager IQ","Retrieve Arms",\ - "Play Charades","Oxygen","Inject BeAcOs","Ninja Lizards","Limit Break","Build Sentry") - - if(mid_txts) - while(mid_txts.len) - var/mid_txt = pick(mid_txts) - mocktxt += mid_txt - mid_txts -= mid_txt - - while(buttons.len) - - var/button = pick(buttons) - - var/button_txt = pick(possible_txt) - - mocktxt += "[button_txt]
" - - buttons -= button - possible_txt -= button_txt - - return start_txt + mocktxt + end_txt + "" - -/proc/check_panel(mob/M) - if (ishuman(M) || isAI(M)) - if(M.hallucination < 15) - return 1 - return 0*/ - -/obj/effect/fake_attacker - icon = null - icon_state = null - name = "" - desc = "" - density = FALSE - anchored = TRUE - opacity = 0 - var/mob/living/carbon/human/my_target = null - var/weapon_name = null - var/obj/item/weap = null - var/image/stand_icon = null - var/image/currentimage = null - var/icon/base = null - var/s_tone - var/mob/living/clone = null - var/image/left - var/image/right - var/image/up - var/collapse - var/image/down - - var/health = 100 - -/obj/effect/fake_attacker/attackby(var/obj/item/P as obj, mob/user as mob) - step_away(src,my_target,2) - for(var/mob/M in oviewers(world.view,my_target)) - to_chat(M, span_bolddanger("[my_target] flails around wildly.")) - my_target.show_message(span_bolddanger("[src] has been attacked by [my_target] "), 1) //Lazy. - - src.health -= P.force - - - return - -/obj/effect/fake_attacker/Crossed(var/mob/M, somenumber) - if(M == my_target) - step_away(src,my_target,2) - if(prob(30)) - for(var/mob/O in oviewers(world.view , my_target)) - to_chat(O, span_bolddanger("[my_target] stumbles around.")) - -/obj/effect/fake_attacker/Initialize(mapload) +/datum/component/hallucinations/Destroy(force) + if(halitem.len) + remove_hallucination_item() + our_human = null . = ..() - QDEL_IN(src, 30 SECONDS) - step_away(src,my_target,2) - addtimer(CALLBACK(src, PROC_REF(attack_loop)), rand(5, 10), TIMER_DELETE_ME) -/obj/effect/fake_attacker/Destroy() - if(my_target) - my_target.hallucinations -= src - return ..() +/datum/component/hallucinations/proc/make_timer() + PROTECTED_PROC(TRUE) + addtimer(CALLBACK(src, PROC_REF(trigger)), ((rand(20,50) SECONDS) / (min(our_human.hallucination,100)/25)), TIMER_DELETE_ME) +/datum/component/hallucinations/proc/get_fakecrit() + SHOULD_NOT_OVERRIDE(TRUE) + return hal_crit -/obj/effect/fake_attacker/proc/updateimage() -// qdel(src.currentimage) +/datum/component/hallucinations/proc/get_hud_state() + SHOULD_NOT_OVERRIDE(TRUE) + return hal_screwyhud - - if(src.dir == NORTH) - qdel(src.currentimage) - src.currentimage = new /image(up,src) - else if(src.dir == SOUTH) - qdel(src.currentimage) - src.currentimage = new /image(down,src) - else if(src.dir == EAST) - qdel(src.currentimage) - src.currentimage = new /image(right,src) - else if(src.dir == WEST) - qdel(src.currentimage) - src.currentimage = new /image(left,src) - my_target << currentimage - - -/obj/effect/fake_attacker/proc/attack_loop() - if(src.health < 0) - collapse() - addtimer(CALLBACK(src, PROC_REF(attack_loop)), rand(5, 10), TIMER_DELETE_ME) +///////////////////////////////////////////////////////////////////////////////////////////////////// +// Traditional hallucinations +///////////////////////////////////////////////////////////////////////////////////////////////////// +/mob/living/carbon/proc/handle_hallucinations() + if(get_hallucination_component() || !client) return - if(get_dist(src,my_target) > 1) - src.set_dir(get_dir(src,my_target)) - step_towards(src,my_target) - updateimage() - else - if(prob(15)) - if(weapon_name) - my_target << sound(pick('sound/weapons/genhit1.ogg', 'sound/weapons/genhit2.ogg', 'sound/weapons/genhit3.ogg')) - my_target.show_message(span_bolddanger("[my_target] has been attacked with [weapon_name] by [src.name]!"), 1) - my_target.halloss += 8 - if(prob(20)) my_target.eye_blurry += 3 - if(prob(33)) - if(!locate(/obj/effect/overlay) in my_target.loc) - fake_blood(my_target) + LoadComponent(/datum/component/hallucinations) + +/mob/living/carbon/proc/get_hallucination_component() + RETURN_TYPE(/datum/component/hallucinations) + return GetComponent(/datum/component/hallucinations) + +/datum/component/hallucinations/proc/trigger() + PROTECTED_PROC(TRUE) + if(QDELETED(our_human)) + qdel(src) + return + if(!our_human.client) + qdel(src) + return + if(our_human.hallucination < HALLUCINATION_THRESHOLD) + qdel(src) + return + handle_hallucinating() + make_timer() + +/datum/component/hallucinations/proc/handle_hallucinating() + PROTECTED_PROC(TRUE) + var/halpick = rand(1,100) + switch(halpick) + if(0 to 15) + event_hudscrew() + if(16 to 25) + event_fake_item() + if(26 to 40) + event_flash_environmental_threats() + if(41 to 65) + event_strange_sound() + if(66 to 70) + if(prob(20) && our_human.nutrition < 100) + event_hunger() // Hungi, meant for xenochi but this is too funny to pass up else - my_target << sound(pick('sound/weapons/punch1.ogg','sound/weapons/punch2.ogg','sound/weapons/punch3.ogg','sound/weapons/punch4.ogg')) - my_target.show_message(span_bolddanger("[src.name] has punched [my_target]!"), 1) - my_target.halloss += 4 - if(prob(33)) - if(!locate(/obj/effect/overlay) in my_target.loc) - fake_blood(my_target) + event_flash_monsters() + if(71 to 72) + event_sleeping() + if(73 to 75) // If you don't want hallucination beatdowns, comment this out + event_attacker() + if(76 to 80) + event_painmessage() - if(prob(15)) - step_away(src,my_target,2) - addtimer(CALLBACK(src, PROC_REF(attack_loop)), rand(5, 10), TIMER_DELETE_ME) +///////////////////////////////////////////////////////////////////////////////////////////////////// +// Xenochimera hallucinations +// Unlike normal hallucinations this one is triggered from handle_feral randomly. +// So it destroys itself after it triggers, freeing up space for the next run of it. +///////////////////////////////////////////////////////////////////////////////////////////////////// +/datum/component/hallucinations/xenochimera/make_timer() + var/datum/component/xenochimera/XC = our_human.GetComponent(/datum/component/xenochimera) + var/F = XC ? (XC.feral / 10) : 1 + addtimer(CALLBACK(src, PROC_REF(trigger)), ((rand(20,50) SECONDS) / F), TIMER_DELETE_ME) -/obj/effect/fake_attacker/proc/collapse() - collapse = 1 - updateimage() - -/proc/fake_blood(var/mob/target) - var/obj/effect/overlay/O = new/obj/effect/overlay(target.loc) - O.name = "blood" - var/image/I = image('icons/effects/blood.dmi',O,"floor[rand(1,7)]",O.dir,1) - target << I - QDEL_IN(O, 30 SECONDS) - -var/list/non_fakeattack_weapons = list(/obj/item/gun/projectile, /obj/item/ammo_magazine/s357,\ - /obj/item/gun/energy/crossbow, /obj/item/melee/energy/sword,\ - /obj/item/storage/box/syndicate, /obj/item/storage/box/emps,\ - /obj/item/cartridge/syndicate, /obj/item/clothing/under/chameleon,\ - /obj/item/clothing/shoes/syndigaloshes, /obj/item/card/id/syndicate,\ - /obj/item/clothing/mask/gas/voice, /obj/item/clothing/glasses/thermal,\ - /obj/item/chameleon, /obj/item/card/emag,\ - /obj/item/storage/toolbox/syndicate, /obj/item/aiModule,\ - /obj/item/radio/headset/syndicate, /obj/item/plastique,\ - /obj/item/powersink, /obj/item/storage/box/syndie_kit,\ - /obj/item/toy/syndicateballoon, /obj/item/gun/energy/captain,\ - /obj/item/hand_tele, /obj/item/rcd, /obj/item/tank/jetpack,\ - /obj/item/clothing/under/rank/captain, /obj/item/aicard,\ - /obj/item/clothing/shoes/magboots, /obj/item/areaeditor/blueprints, /obj/item/disk/nuclear,\ - /obj/item/clothing/suit/space/void, /obj/item/tank) - -/proc/fake_attack(var/mob/living/target) - var/list/possible_clones = new/list() - var/mob/living/carbon/human/clone = null - var/clone_weapon = null - - for(var/mob/living/carbon/human/H in living_mob_list) - if(H.stat || H.lying) - continue - possible_clones += H -// clone = H -// break //changed the code a bit. Less randomised, but less work to do. Should be ok, world.contents aren't stored in any particular order. - - if(!possible_clones.len) +/datum/component/hallucinations/xenochimera/trigger() + if(QDELETED(our_human)) + qdel(src) return - clone = pick(possible_clones) - if(!clone) + if(!our_human.client) + qdel(src) return + var/datum/component/xenochimera/XC = our_human.GetComponent(/datum/component/xenochimera) + if(!XC || XC.feral < XENOCHIFERAL_THRESHOLD) + qdel(src) + return + handle_hallucinating() + QDEL_IN(src,rand(3,9)SECONDS) - //var/obj/effect/fake_attacker/F = new/obj/effect/fake_attacker(outside_range(target)) - var/obj/effect/fake_attacker/F = new/obj/effect/fake_attacker(target.loc) - if(clone.l_hand) - if(!(locate(clone.l_hand) in non_fakeattack_weapons)) - clone_weapon = clone.l_hand.name - F.weap = clone.l_hand - else if (clone.r_hand) - if(!(locate(clone.r_hand) in non_fakeattack_weapons)) - clone_weapon = clone.r_hand.name - F.weap = clone.r_hand - - F.name = clone.name - F.my_target = target - F.weapon_name = clone_weapon - target.hallucinations += F - - - F.left = image(clone,dir = WEST) - F.right = image(clone,dir = EAST) - F.up = image(clone,dir = NORTH) - F.down = image(clone,dir = SOUTH) - -// F.base = new /icon(clone.stand_icon) -// F.currentimage = new /image(clone) - -/* - - - - F.left = new /icon(clone.stand_icon,dir=WEST) - for(var/icon/i in clone.overlays) - F.left.Blend(i) - F.up = new /icon(clone.stand_icon,dir=NORTH) - for(var/icon/i in clone.overlays) - F.up.Blend(i) - F.down = new /icon(clone.stand_icon,dir=SOUTH) - for(var/icon/i in clone.overlays) - F.down.Blend(i) - F.right = new /icon(clone.stand_icon,dir=EAST) - for(var/icon/i in clone.overlays) - F.right.Blend(i) - - target << F.up - */ - - F.updateimage() +/datum/component/hallucinations/xenochimera/handle_hallucinating() + var/halpick = rand(1,100) + switch(halpick) + if(0 to 15) //15% chance + //Screwy HUD + event_hudscrew() + if(16 to 25) //10% chance + event_fake_item() + if(26 to 35) //10% chance + event_flash_environmental_threats() + if(36 to 55) //20% chance + event_strange_sound() + if(56 to 60) //5% chance + event_flash_monsters() + if(61 to 85) //25% chance + event_hunger() + if(86 to 100) //15% chance + event_hear_voices() diff --git a/code/modules/flufftext/fake_attacker.dm b/code/modules/flufftext/fake_attacker.dm new file mode 100644 index 0000000000..f8a3fc2c5e --- /dev/null +++ b/code/modules/flufftext/fake_attacker.dm @@ -0,0 +1,218 @@ +/obj/effect/fake_attacker // I did want to use /image/client_only but this needs to be somewhat more complex + name = "" + desc = "" + density = FALSE + anchored = TRUE + opacity = 0 + VAR_PRIVATE/list/clients = list() + VAR_PRIVATE/list/image/dir_images = list() + +/obj/effect/fake_attacker/process() + . = ..() + // Passive cleanup + for(var/datum/weakref/C in clients) + var/client/CW = C?.resolve() + if(isnull(CW)) + clients.Remove(C) + if(!clients.len) + qdel(src) + +/obj/effect/fake_attacker/set_dir(newdir) + if(!(newdir in GLOB.cardinal)) + newdir &= ~(EAST|WEST) // Don't allow diagonals, prefer north/south + if(!newdir) + newdir = SOUTH + var/updatesprite = (dir != newdir) + . = ..() + if(updatesprite) + for(var/datum/weakref/C in clients) + var/client/CW = C?.resolve() + clear_images_from_client(CW) + assign_image_to_client(CW) + +/obj/effect/fake_attacker/Moved(atom/old_loc, direction, forced, movetime) + . = ..() + var/turf_move = isturf(loc) && isturf(old_loc) + for(var/img in dir_images) + var/image/G = dir_images[img] + G.loc = loc + if(turf_move) + var/MT = 0.7 SECONDS + G.pixel_x = (old_loc.x - loc.x) * WORLD_ICON_SIZE + G.pixel_y = (old_loc.y - loc.y) * WORLD_ICON_SIZE + animate(G, pixel_x = 0, time = MT, flags = ANIMATION_PARALLEL) + animate(G, pixel_y = 0, time = MT, flags = ANIMATION_PARALLEL) + +/obj/effect/fake_attacker/Destroy(force) + . = ..() + clear_every_clients_images() + qdel_all_images() + +/obj/effect/fake_attacker/proc/create_images_from(var/atom/clone) + SHOULD_NOT_OVERRIDE(TRUE) + dir_images["[NORTH]"] = image(clone,dir = NORTH) + dir_images["[SOUTH]"] = image(clone,dir = SOUTH) + dir_images["[EAST]"] = image(clone,dir = EAST) + dir_images["[WEST]"] = image(clone,dir = WEST) + for(var/img in dir_images) + var/image/G = dir_images[img] + G.layer = clone.layer + G.plane = clone.plane + G.appearance_flags = clone.appearance_flags + G.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + G.loc = loc + +/obj/effect/fake_attacker/proc/append_client(var/client/C) + SHOULD_NOT_OVERRIDE(TRUE) + clients.Add(WEAKREF(C)) + assign_image_to_client(C) + +/obj/effect/fake_attacker/proc/assign_image_to_client(var/client/C) + PRIVATE_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + if(!C) + return + if(!dir_images.len) + return + var/image/I = dir_images["[dir]"] + if(I) + C.images += I + +/obj/effect/fake_attacker/proc/clear_every_clients_images() + for(var/datum/weakref/C in clients) + clear_images_from_client(C?.resolve()) + +/obj/effect/fake_attacker/proc/clear_images_from_client(var/client/C) + PRIVATE_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + if(!C) + return + if(!dir_images.len) + return + for(var/img in dir_images) + C.images -= dir_images[img] + +/obj/effect/fake_attacker/proc/qdel_all_images() + PRIVATE_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + for(var/img in dir_images) + qdel_null(dir_images[img]) + dir_images.Cut() + + + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Hallucination attackers with AI behaviors +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/mob/proc/create_hallucination_attacker(var/turf/T = null,var/mob/living/carbon/human/clone = null, var/forced_type = null) + SHOULD_NOT_OVERRIDE(TRUE) + if(!client) + return null + + if(!clone) + // Get a randomized clone from the living mob's list, must be standing + var/list/possible_clones = new/list() + for(var/mob/living/carbon/human/H in living_mob_list) + if(H.stat || H.lying) + continue + possible_clones += H + if(!possible_clones.len) + return null + clone = pick(possible_clones) + if(!clone) + return null + + if(!T) + // Get the target's turf, then make some random steps to get away from them, respecting walls + T = get_turf(clone) + var/turf/CT = T + for(var/i = 0 to 8) + var/Cdir = pick(GLOB.cardinal) + if(prob(30)) + Cdir = clone.dir // Results in hallucinations somewhat being in front of you + var/turf/NT = get_step(CT,Cdir) + if(!NT.density) + CT = NT + T = CT + + if(!forced_type) + // Picking from all available options + var/list/get_types = subtypesof(/obj/effect/fake_attacker/human) + forced_type = pick(get_types) + // Finally! After a thousand years I'm finally free to conquer EARTH! + return new forced_type(T,src,clone) + +/obj/effect/fake_attacker/human + VAR_PROTECTED/datum/weakref/target = null + var/requires_hallucinating = TRUE // Mob will qdel if the target is not hallucinating if this is true + +/obj/effect/fake_attacker/human/Initialize(mapload,var/mob/targeting_mob,var/atom/clone_appearance_from) + . = ..() + START_PROCESSING(SSobj, src) + set_target(targeting_mob) + create_images_from(clone_appearance_from) + append_client(targeting_mob.client) + name = clone_appearance_from.name + // Usually we want to face our target for maximum spooky effect + set_dir(get_dir(src,targeting_mob)) + +/obj/effect/fake_attacker/human/Destroy() + STOP_PROCESSING(SSobj, src) + . = ..() + +/obj/effect/fake_attacker/human/process() + // check if valid + var/mob/living/M = target?.resolve() + if(!M) + qdel(src) + return null + if(requires_hallucinating) + if(!ishuman(M)) + qdel(src) + return null + var/mob/living/carbon/human/H = M + if(!H.hallucination) + qdel(src) + return null + + return M + +/obj/effect/fake_attacker/human/proc/set_target(var/mob/M) + target = WEAKREF(M) + + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Attacker: Performs hostile shoves and attacks +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/obj/effect/fake_attacker/human/attacker/process() + var/mob/living/M = ..() + + if(get_dist(src,M) > 1) + step_towards(src,M) + + else if(prob(15)) + M << sound(pick('sound/weapons/punch1.ogg','sound/weapons/punch2.ogg','sound/weapons/punch3.ogg','sound/weapons/punch4.ogg')) + M.show_message(span_bolddanger("\The [src] has punched \the [M]!"), 1) + M.halloss += 4 + + if(prob(15)) + step_away(src,M) + + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Fleeing: Runs away when you get close +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/obj/effect/fake_attacker/human/fleeing + VAR_PRIVATE/flee = FALSE + +/obj/effect/fake_attacker/human/fleeing/process() + var/mob/living/M = ..() + set_dir(get_dir(src,M)) + + if(get_dist(src,M) < 6 || flee) + flee = TRUE + step_away(src,M) + + if(get_dist(src,M) > 10 || get_dist(src,M) < 2 || (flee && prob(10))) + target = null + qdel(src) diff --git a/code/modules/flufftext/hallucination_events.dm b/code/modules/flufftext/hallucination_events.dm new file mode 100644 index 0000000000..354701b715 --- /dev/null +++ b/code/modules/flufftext/hallucination_events.dm @@ -0,0 +1,302 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////// +// Hallucination events +///////////////////////////////////////////////////////////////////////////////////////////////////// +/datum/component/hallucinations/proc/event_hudscrew() + PROTECTED_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + //Screwy HUD + //to_chat(our_human, "Screwy HUD") + hal_screwyhud = pick(list( + HUD_HALLUCINATION_CRIT, + HUD_HALLUCINATION_TOXIN, + HUD_HALLUCINATION_OXY, + HUD_HALLUCINATION_ONFIRE + )) + VARSET_IN(src, hal_screwyhud, HUD_HALLUCINATION_NONE, rand(15,35) SECONDS) + +/datum/component/hallucinations/proc/event_painmessage() + PROTECTED_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + //Fake pain messages + //to_chat(our_human, "fake messages") + var/list/pain_message = list("The inside of your head hurts...", + "Your stomach hurts.", + "You feel sick.", + "Your upper body hurts.", + "Your head hurts.", + "Your arm hurts.", + "Your leg hurts.", + "Your lower body hurts.", + "Your foot hurts.", + "Your hand hurts.", + "The pain is excruciating!", + "Your whole body is going numb!", + "You feel like you could die any moment now!", + "Please, just end the pain!", + "You feel a sudden pain across your body.", + "You cringe as a violent pain takes over your body.", + "It feels like your body is eating itself inside out.", + "IT HURTS.", + "Your body feels as if it's trying to rip itself open..." + ) + if(prob(20) && our_human.jitteriness < 50) + our_human.make_jittery(120) + to_chat(our_human, span_danger(pick(pain_message))) + +/datum/component/hallucinations/proc/event_fake_item() + PROTECTED_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + //Strange items + //to_chat(our_human, "Traitor Items") + if(halitem.len) + return + + var/list/slots_free = list(ui_lhand,ui_rhand) + if(our_human.l_hand) + slots_free -= ui_lhand + if(our_human.r_hand) + slots_free -= ui_rhand + if(!our_human.belt) + slots_free += ui_belt + if(!our_human.l_store) + slots_free += ui_storage1 + if(!our_human.r_store) + slots_free += ui_storage2 + + if(!slots_free.len) + return + + var/obj/CI = new() + CI.screen_loc = pick(slots_free) + CI.hud_layerise() + switch(rand(1,6)) + if(1) //revolver + CI.icon = 'icons/obj/gun.dmi' + CI.icon_state = "revolver" + CI.name = "Revolver" + if(2) //c4 + CI.icon = 'icons/obj/assemblies.dmi' + CI.icon_state = "plastic-explosive0" + CI.name = "Mysterious Package" + if(prob(25)) + CI.icon_state = "c4small_1" + if(3) //sword + CI.icon = 'icons/obj/weapons.dmi' + CI.icon_state = "longsword" + CI.name = "Sword" + if(4) //stun baton + CI.icon = 'icons/obj/weapons.dmi' + CI.icon_state = "stunbaton" + CI.name = "Stun Baton" + if(5) //emag + CI.icon = 'icons/obj/card.dmi' + CI.icon_state = "emag" + CI.name = "Cryptographic Sequencer" + if(6) //flashbang + CI.icon = 'icons/obj/grenade.dmi' + CI.icon_state = "flashbang1" + CI.name = "Flashbang" + halitem[WEAKREF(CI)] = WEAKREF(our_human.client) + our_human.client.screen += CI + addtimer(CALLBACK(src, PROC_REF(remove_hallucination_item)), rand(10,25) SECONDS, TIMER_DELETE_ME) + +/datum/component/hallucinations/proc/remove_hallucination_item() + // I can't manage this with /image/client_only due to screenloc, so key-value weakref pair it is! Called on both timer and destroying this component. + PRIVATE_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + var/datum/weakref/first = halitem[1] + var/obj/itm = first?.resolve() + var/datum/weakref/CW = halitem[first] + var/client/C = CW?.resolve() + C.screen -= itm + qdel(itm) + halitem.Cut() + +/datum/component/hallucinations/proc/event_strange_sound() + PROTECTED_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + + //Strange audio + var/send_sound + switch(rand(1,12)) + if(1) + send_sound = 'sound/machines/door/old_airlock.ogg' + if(2) + if(prob(50)) + send_sound = 'sound/effects/Explosion1.ogg' + else + send_sound = 'sound/effects/Explosion2.ogg' + if(3) + send_sound = 'sound/effects/explosionfar.ogg' + if(4) + send_sound = 'sound/effects/Glassbr1.ogg' + if(5) + send_sound = 'sound/effects/Glassbr2.ogg' + if(6) + send_sound = 'sound/effects/Glassbr3.ogg' + if(7) + send_sound = 'sound/machines/twobeep.ogg' + if(8) + send_sound = 'sound/machines/door/windowdoor.ogg' + if(9) + //To make it more realistic, I added two gunshots (enough to kill) + send_sound = 'sound/weapons/Gunshot1.ogg' + addtimer(CALLBACK(src, PROC_REF(secondary_sound), 'sound/weapons/Gunshot2.ogg'), rand(1,3) SECONDS, TIMER_DELETE_ME) + if(10) + send_sound = 'sound/weapons/smash.ogg' + if(11) + //Same as above, but with tasers. + send_sound = 'sound/weapons/Taser.ogg' + addtimer(CALLBACK(src, PROC_REF(secondary_sound), 'sound/weapons/Taser.ogg'), rand(1,3) SECONDS, TIMER_DELETE_ME) + //Rare audio + if(12) +//These sounds are (mostly) taken from Hidden: Source + var/list/creepyasssounds = list('sound/effects/ghost.ogg', 'sound/effects/ghost2.ogg', 'sound/effects/Heart Beat.ogg', 'sound/effects/screech.ogg',\ + 'sound/hallucinations/behind_you1.ogg', 'sound/hallucinations/behind_you2.ogg', 'sound/hallucinations/far_noise.ogg', 'sound/hallucinations/growl1.ogg', 'sound/hallucinations/growl2.ogg',\ + 'sound/hallucinations/growl3.ogg', 'sound/hallucinations/im_here1.ogg', 'sound/hallucinations/im_here2.ogg', 'sound/hallucinations/i_see_you1.ogg', 'sound/hallucinations/i_see_you2.ogg',\ + 'sound/hallucinations/look_up1.ogg', 'sound/hallucinations/look_up2.ogg', 'sound/hallucinations/over_here1.ogg', 'sound/hallucinations/over_here2.ogg', 'sound/hallucinations/over_here3.ogg',\ + 'sound/hallucinations/turn_around1.ogg', 'sound/hallucinations/turn_around2.ogg', 'sound/hallucinations/veryfar_noise.ogg', 'sound/hallucinations/wail.ogg') + send_sound = pick(creepyasssounds) + + our_human.playsound_local(get_turf(our_human), send_sound, vol = 75, channel = CHANNEL_AMBIENCE_FORCED) + +/datum/component/hallucinations/proc/secondary_sound(var/sound_path) + PRIVATE_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + our_human.playsound_local(get_turf(our_human), sound_path, vol = 75, channel = CHANNEL_AMBIENCE_FORCED) + +/datum/component/hallucinations/proc/event_flash_environmental_threats() + PROTECTED_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + //Flashes of danger + //to_chat(our_human, "Danger Flash") + if(halimage?.resolve()) + return + + var/list/possible_points = list() + for(var/turf/simulated/floor/F in view(our_human,world.view)) + possible_points += F + if(!possible_points.len) + return + + var/turf/simulated/floor/target = pick(possible_points) + var/image/client_only/CI + switch(rand(1,3)) + if(1) + //to_chat(our_human, "Space") + CI = new('icons/turf/space.dmi',target,"[rand(1,25)]",TURF_LAYER) + if(2) + //to_chat(our_human, "Fire") + CI = new('icons/effects/fire.dmi',target,"1",TURF_LAYER) + if(3) + //to_chat(our_human, "C4") + CI = new('icons/obj/assemblies.dmi',target,"plastic-explosive2",OBJ_LAYER+0.01) + halimage = WEAKREF(CI) + CI.append_client(our_human.client) + QDEL_IN(CI, rand(1,5) SECONDS) //Only seen for a brief moment. + +/datum/component/hallucinations/proc/event_flash_monsters() + PROTECTED_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + //Flashes of danger + //to_chat(our_human, "Danger Flash") + if(halbody?.resolve()) + return + + var/list/possible_points = list() + for(var/turf/simulated/floor/F in view(our_human,world.view)) + possible_points += F + if(!possible_points.len) + return + + var/turf/simulated/floor/target = pick(possible_points) + var/image/client_only/CI + switch(rand(1,4)) + if(1) + CI = new('icons/mob/human.dmi',target,"husk_l",TURF_LAYER) + if(2,3) + CI = new('icons/mob/human.dmi',target,"husk_s",TURF_LAYER) + if(4) + CI = new('icons/mob/alien.dmi',target,"alienother",TURF_LAYER) +// if(5) +// CI = new('xcomalien.dmi',target,"chryssalid",TURF_LAYER) + halbody = WEAKREF(CI) + CI.append_client(our_human.client) + QDEL_IN(CI, rand(5,8) SECONDS) //Only seen for a brief moment. + +/datum/component/hallucinations/proc/event_sleeping() + PROTECTED_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + //Fake death + //to_chat(our_human, "fake death") + our_human.SetSleeping(20) + hal_crit = TRUE + hal_screwyhud = HUD_HALLUCINATION_CRIT + addtimer(CALLBACK(src, PROC_REF(reset_hallucination_sleeping)), rand(5,10) SECONDS, TIMER_DELETE_ME) + +/datum/component/hallucinations/proc/reset_hallucination_sleeping() + PRIVATE_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + our_human.SetSleeping(0) + hal_crit = FALSE + hal_screwyhud = HUD_HALLUCINATION_NONE + +/datum/component/hallucinations/proc/event_attacker() + PROTECTED_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + var/attacker = our_human.create_hallucination_attacker(forced_type = /obj/effect/fake_attacker/human/attacker) // Currently just uses the attacker type for now, remove argument to use any other subtype + if(attacker) + QDEL_IN(attacker, rand(25,30) SECONDS) + +///////////////////////////////////////////////////////////////////////////////////////////////////// +// Xenochimera feral events +///////////////////////////////////////////////////////////////////////////////////////////////////// +/datum/component/hallucinations/proc/event_hunger() + PROTECTED_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + //food + if(halbody?.resolve()) + return + + var/list/possible_points = list() + for(var/turf/simulated/floor/F in view(our_human,world.view)) + possible_points += F + if(!possible_points.len) + return + + var/turf/simulated/floor/target = pick(possible_points) + var/image/client_only/CI + switch(rand(1,10)) + if(1) + CI = new('icons/mob/animal.dmi',target,"cow",TURF_LAYER) + if(2) + CI = new('icons/mob/animal.dmi',target,"chicken",TURF_LAYER) + if(3) + CI = new('icons/obj/food.dmi',target,"bigbiteburger",TURF_LAYER) + if(4) + CI = new('icons/obj/food.dmi',target,"meatbreadslice",TURF_LAYER) + if(5) + CI = new('icons/obj/food.dmi',target,"sausage",TURF_LAYER) + if(6) + CI = new('icons/obj/food.dmi',target,"bearmeat",TURF_LAYER) + if(7) + CI = new('icons/obj/food.dmi',target,"fishfillet",TURF_LAYER) + if(8) + CI = new('icons/obj/food.dmi',target,"meat",TURF_LAYER) + if(9) + CI = new('icons/obj/food.dmi',target,"meatstake",TURF_LAYER) + if(10) + CI = new('icons/obj/food.dmi',target,"monkeysdelight",TURF_LAYER) + + halbody = WEAKREF(CI) + CI.append_client(our_human.client) + QDEL_IN(CI, rand(5,8) SECONDS) //Only seen for a brief moment. + +/datum/component/hallucinations/proc/event_hear_voices() + PROTECTED_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + //hear voices. Could make the voice pick from nearby creatures, but nearby creatures make feral hallucinations rare so don't bother. + var/list/hiddenspeakers = list("Someone distant", "A voice nearby","A familiar voice", "An echoing voice", "A cautious voice", "A scared voice", "Someone around the corner", "Someone", "Something", "Something scary", "An urgent voice", "An angry voice") + var/list/speakerverbs = list("calls out", "yells", "screams", "exclaims", "shrieks", "shouts", "hisses", "snarls") + var/list/spookyphrases = list("It's over here!","Stop it!", "Hunt it down!", "Get it!", "Quick, over here!", "Anyone there?", "Who's there?", "Catch that thing!", "Stop it! Kill it!", "Anyone there?", "Where is it?", "Find it!", "There it is!") + to_chat(our_human, span_game(span_say(span_name(pick(hiddenspeakers)) + " [pick(speakerverbs)], \"[pick(spookyphrases)]\""))) diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm index 66775d49ee..e550c29947 100644 --- a/code/modules/mob/living/carbon/human/life.dm +++ b/code/modules/mob/living/carbon/human/life.dm @@ -773,6 +773,11 @@ spawn(0) emote(pick("giggle", "laugh")) breath.adjust_gas(GAS_N2O, -breath.gas[GAS_N2O]/6, update = 0) //update after + if(get_hallucination_component()?.get_hud_state() == HUD_HALLUCINATION_OXY) + throw_alert("oxy", /obj/screen/alert/not_enough_atmos) + else if(get_hallucination_component()?.get_hud_state() == HUD_HALLUCINATION_TOXIN) + throw_alert("tox_in_air", /obj/screen/alert/tox_in_air) + // Were we able to breathe? if (failed_inhale || failed_exhale) failed_last_breath = 1 @@ -1300,20 +1305,15 @@ Paralyse(3) if(hallucination) - if(hallucination >= 20 && !(species.flags & (NO_POISON|IS_PLANT|NO_HALLUCINATION)) ) - if(!handling_hal) - spawn handle_hallucinations() //The not boring kind! + if(hallucination >= HALLUCINATION_THRESHOLD && !(species.flags & (NO_POISON|IS_PLANT|NO_HALLUCINATION)) ) + handle_hallucinations() /* Stop spinning the view, it breaks too much. if(client && prob(5)) client.dir = pick(2,4,8) spawn(rand(20,50)) client.dir = 1 */ - hallucination = max(0, hallucination - 2) - else - for(var/atom/a in hallucinations) - qdel(a) //Brain damage from Oxyloss if(should_have_organ(O_BRAIN)) @@ -1377,7 +1377,7 @@ //Are they SSD? If so we'll keep them asleep but work off some of that sleep var in case of stoxin or similar. if(client || sleeping > 3) handle_sleeping() - if( prob(2) && health && !hal_crit && client ) + if( prob(2) && health && !get_hallucination_component()?.get_fakecrit() && client ) spawn(0) emote("snore") //CONSCIOUS @@ -1583,7 +1583,7 @@ clear_fullscreen("fear") if(healths) - if (chem_effects[CE_PAINKILLER] > 100) + if(chem_effects[CE_PAINKILLER] > 100) healths.icon_state = "health_numb" else // Generate a by-limb health display. @@ -1605,10 +1605,12 @@ health_images += E.get_damage_hud_image(limb_trauma_val) // Apply a fire overlay if we're burning. - if(on_fire) + if(on_fire || get_hallucination_component()?.get_hud_state() == HUD_HALLUCINATION_ONFIRE) health_images += image('icons/mob/OnFire.dmi',"[get_fire_icon_state()]") // Show a general pain/crit indicator if needed. + if(get_hallucination_component()?.get_hud_state() == HUD_HALLUCINATION_CRIT) + trauma_val = 2 if(trauma_val) if(!(species.flags & NO_PAIN)) if(trauma_val > 0.7) diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index d9b4106bcf..0ffc996108 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -22,7 +22,6 @@ var/max_nutrition = MAX_NUTRITION var/hallucination = 0 //Directly affects how long a mob will hallucinate for - var/list/atom/hallucinations = list() //A list of hallucinated people that try to attack the mob. See /obj/effect/fake_attacker in hallucinations.dm var/last_special = 0 //Used by the resist verb, likely used to prevent players from bypassing next_move by logging in/out. var/base_attack_cooldown = DEFAULT_ATTACK_COOLDOWN diff --git a/vorestation.dme b/vorestation.dme index 9eec024887..ebf53bf541 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -74,6 +74,7 @@ #include "code\__defines\fonts.dm" #include "code\__defines\footsteps.dm" #include "code\__defines\gamemode.dm" +#include "code\__defines\hallucination.dm" #include "code\__defines\holder.dm" #include "code\__defines\holomap.dm" #include "code\__defines\hoses.dm" @@ -1270,6 +1271,7 @@ #include "code\game\objects\trash_eating.dm" #include "code\game\objects\weapons.dm" #include "code\game\objects\effects\bump_teleporter.dm" +#include "code\game\objects\effects\client_only_image.dm" #include "code\game\objects\effects\confetti_vr.dm" #include "code\game\objects\effects\effect_system.dm" #include "code\game\objects\effects\explosion_particles.dm" @@ -2565,7 +2567,9 @@ #include "code\modules\fishing\fishing_rod_vr.dm" #include "code\modules\fishing\fishing_vr.dm" #include "code\modules\flufftext\Dreaming.dm" +#include "code\modules\flufftext\fake_attacker.dm" #include "code\modules\flufftext\Hallucination.dm" +#include "code\modules\flufftext\hallucination_events.dm" #include "code\modules\flufftext\look_up.dm" #include "code\modules\flufftext\TextFilters.dm" #include "code\modules\food\food.dm"