Ghost Busting (#18854)

* bustin

makes me feel good

* Update vorestation.dme

* Update trap.dm

* more

* Edits

* Make this work on incorp entities

* Update entrepreneur_items.dm

* Ghost Capture!

* awwa

* Update entrepreneur_items.dm

* Update entrepreneur_items.dm

* vore

* Other ways to spawn it

* Spectral shot

* ammo

* yeh

* get these

* Update ammo.dm

* AAAA

* Update weapons.dm

* Update weapons.dm

* beeem

* better

* Update weapons.dm

* more stuffs

* make sure this is unticked

* Update supplypack.dm

* spooky

* yeh

* mob sprites

* aaa

* Update research_nodes.dm

* Remove RND_CATEGORY_INITIAL from ammunition designs

* ?.

* subtype
This commit is contained in:
Cameron Lennox
2026-04-02 08:40:32 -04:00
committed by GitHub
parent ce1ce403a9
commit 981aaee9fa
44 changed files with 886 additions and 54 deletions
@@ -69,7 +69,6 @@
/obj/item/personal_shield_generator/loaded //starts with a cell
bcell = /obj/item/cell/device/shield_generator/backpack
/obj/item/personal_shield_generator/update_icon()
if(shield_active)
icon_state = "shieldpack_basic_on"
@@ -75,6 +75,10 @@
name = "entertainment intercom"
frequency = ENT_FREQ
/obj/item/radio/intercom/science
name = "station intercom (Science)"
channels=list("Science")
/obj/item/radio/intercom/omni
name = "global announcer"
/obj/item/radio/intercom/omni/Initialize(mapload)
@@ -298,7 +298,7 @@ GLOBAL_DATUM(autospeaker, /mob/living/silicon/ai/announcer)
if(!GLOB.autospeaker)
return
var/datum/radio_frequency/connection = null
if(channel && channels && channels.len > 0)
if(channel && channels && LAZYLEN(channels))
if(channel == "department")
channel = channels[1]
connection = secure_radio_connections[channel]
@@ -325,7 +325,7 @@ GLOBAL_DATUM(autospeaker, /mob/living/silicon/ai/announcer)
return radio_connection
// Otherwise, if a channel is specified, look for it.
if(channels && channels.len > 0)
if(channels && LAZYLEN(channels))
if (message_mode == "department") // Department radio shortcut
message_mode = channels[1]
@@ -0,0 +1,92 @@
//9mm
/obj/item/ammo_magazine/m9mm/spectral
name = "magazine (9mm specter shot)"
ammo_type = /obj/item/ammo_casing/a9mm/spectral
/obj/item/ammo_magazine/m9mml/spectral
name = "compact magazine (9mm specter shot)"
ammo_type = /obj/item/ammo_casing/a9mm/spectral
/obj/item/ammo_casing/a9mm/spectral
desc = "A 9mm spectral bullet casing. Hits incorporeal entities and brings them into the corporeal realm, but near-useless against corporeal ones."
icon_state = "r-casing"
projectile_type = /obj/item/projectile/bullet/spectral
//357
/obj/item/ammo_magazine/s357/spectral
name = "speedloader (.357 specter shot)"
icon_state = "T38"
ammo_type = /obj/item/ammo_casing/a357/spectral
/obj/item/ammo_casing/a357/spectral
desc = "A .357 spectral bullet casing. Hits incorporeal entities and brings them into the corporeal realm, but near-useless against corporeal ones."
caliber = ".357"
icon_state = "r-casing"
projectile_type = /obj/item/projectile/bullet/spectral/strong
matter = list(MAT_STEEL = 210)
//38
/obj/item/ammo_magazine/s38/spectral
name = "speedloader (.38 spectral)"
icon_state = "T38"
ammo_type = /obj/item/ammo_casing/a38/spectral
/obj/item/ammo_magazine/ammo_box/b38/spectral
name = "ammo box (.38 spectral)"
icon_state = "pistol_p"
ammo_type = /obj/item/ammo_casing/a38/spectral
max_ammo = 24
/obj/item/ammo_casing/a38/spectral
desc = "A .38 rubber bullet casing. Hits incorporeal entities and brings them into the corporeal realm, but near-useless against corporeal ones."
icon_state = "empcasing"
projectile_type = /obj/item/projectile/bullet/spectral
//44
/obj/item/ammo_magazine/s44/spectral
name = "speedloader (.44 specter shot)"
icon_state = "R44"
ammo_type = /obj/item/ammo_casing/a44/spectral
/obj/item/ammo_magazine/ammo_box/b44/spectral
name = "ammo box (.44 specter shot)"
icon_state = "box44-rubber"
ammo_type = /obj/item/ammo_casing/a44/spectral
max_ammo = 24
/obj/item/ammo_casing/a44/spectral
desc = "A .44 bullet casing. Hits incorporeal entities and brings them into the corporeal realm, but near-useless against corporeal ones."
caliber = ".44"
icon_state = "r-casing"
projectile_type = /obj/item/projectile/bullet/spectral/strong
matter = list(MAT_STEEL = 210)
//The actual spectral bullet.
/obj/item/projectile/bullet/spectral
damage = 1 //Very, very weak.
hud_state = "smg_light"
hits_phased = TRUE
dephasing = TRUE
var/phaser_damage = 10
var/weaken_duration = 5
var/stun_duration = 3
/obj/item/projectile/bullet/spectral/attack_mob(mob/living/target_mob, distance, miss_modifier)
var/incorp = is_incorporeal()
if(incorp)
damage = phaser_damage
. = ..()
if(. && incorp)
target_mob.Stun(stun_duration)
target_mob.Weaken(weaken_duration)
/obj/item/projectile/bullet/spectral/can_embed()
return FALSE
//Subtypes
/obj/item/projectile/bullet/spectral/strong
damage = 5
phaser_damage = 25
stun_duration = 5
weaken_duration = 10
@@ -0,0 +1,5 @@
/obj/item/entrepreneur/emf/professional
name = "Professional EMF scanner"
desc = "A handheld device used for detecting disturbances to electromagnetic fields. This one boasts 'improved ghost detection' functions."
advanced = TRUE
detection_range = 7
@@ -0,0 +1,28 @@
/obj/item/clothing/glasses/ghost
name = "spectral field analysis goggles"
desc = "A pair of goggles claiming to let you see into the beyond!"
description_fluff = "A pair of goggles created by K.E's Spectral-Division, a popular item among ghost hunters and paranormal investigators. \
Due to their extremely poor visual clarity and the extreme static coming from the display, users often claim to see 'specters' and 'spirits' \
while wearing them with no physcial proof to back up these claims, as any attempt to take a photo or record through the goggles results in \
results in a blurry, static-filled mess."
icon_state = "proton_goggles"
item_state_slots = list(slot_r_hand_str = "glasses", slot_l_hand_str = "glasses")
darkness_view = 7 //So you can really see those ghosts~
see_invisible = SEE_INVISIBLE_CULT
toggleable = 1
actions_types = list(/datum/action/item_action/toggle_goggles)
off_state = "denight"
flash_protection = FLASH_PROTECTION_VULNERABLE
enables_planes = list(VIS_GHOSTS)
///If we can see without static or not.
var/static_vision = TRUE
/obj/item/clothing/glasses/ghost/Initialize(mapload)
. = ..()
if(static_vision)
overlay = GLOB.global_hud.heavy_whitense //obscures your vision of the real realm AND makes actually seeing ghosts difficult.
/obj/item/clothing/glasses/ghost/advanced
name = "improved spectral field analysis goggles"
desc = "A pair of goggles claiming to let you see into the beyond! This one allows for extra clarity."
static_vision = FALSE
@@ -0,0 +1,12 @@
/datum/supply_pack/sci/spectral
name = "Spectral Countermeasure Crate"
desc = "A crate containing various items used for ghost hunting and countering paranormal activity."
contains = list(
/obj/item/entrepreneur/emf/professional = 5,
/obj/item/clothing/glasses/ghost = 1,
/obj/item/ghost_trap = 5,
/obj/item/proton_pack = 1
)
cost = 1500 //You can see ghosts with this!
containertype = /obj/structure/closet/crate/secure/weapon
containername = "Spectral Countermeasure Crate"
@@ -0,0 +1,250 @@
/obj/item/ghost_trap
name = "spectral trap"
desc = "A mechanically activated 'ghost trap'."
description_fluff = "A 'ghost trap' created by Krow Enterprise's Spectral Division. Used by self proclaimed ghost-hunters and \
paranormal investigators to supposedly capture spirits and specters."
throw_speed = 5
throw_range = 7
gender = PLURAL
icon = 'icons/obj/ghost_trap.dmi'
icon_state = "item"
randpixel = 0
center_of_mass_x = 0
center_of_mass_y = 0
throwforce = 0
w_class = ITEMSIZE_NORMAL
var/deployed = FALSE
///The entity we currently have captured.
var/datum/weakref/captured_entity
var/obj/item/radio/intercom/science/ghost_reporter
/obj/item/ghost_trap/Initialize(mapload)
. = ..()
if(deployed)
update_icon()
ghost_reporter = new(null)
START_PROCESSING(SSobj, src)
/obj/item/ghost_trap/Destroy()
STOP_PROCESSING(SSobj, src)
var/mob/our_entity = captured_entity?.resolve()
if(our_entity)
REMOVE_TRAIT(our_entity, TRAIT_NO_TRANSFORM, src)
our_entity.forceMove(get_turf(src))
captured_entity = null
QDEL_NULL(ghost_reporter)
. = ..()
/obj/item/ghost_trap/verb/release_occupant()
set src in oview(1)
set category = "Object"
set name = "Relase Entity"
release_entity(usr)
/obj/item/ghost_trap/proc/release_entity(mob/living/user)
if(!isliving(user)) //no ghosts
return
if((user.loc == src))
to_chat(user, span_warning("You need to be outside \the [src] to do this."))
return
if(captured_entity)
var/mob/our_entity = captured_entity.resolve()
if(our_entity && (our_entity.loc == src))
REMOVE_TRAIT(our_entity, TRAIT_NO_TRANSFORM, src)
captured_entity = null
our_entity.forceMove(get_turf(src))
update_icon()
return
to_chat(user, span_info("There appears to be nothing in the trap!"))
return
/obj/item/ghost_trap/update_icon()
..()
if(deployed)
icon_state = "on"
return
if(captured_entity)
var/mob/our_entity = captured_entity.resolve()
if(our_entity)
icon_state = "item_captured"
return
icon_state = initial(icon_state)
return
icon_state = initial(icon_state)
/obj/item/ghost_trap/start_active
deployed = TRUE
/obj/item/ghost_trap/process()
if(captured_entity)
var/mob/our_entity = captured_entity.resolve()
if(our_entity && our_entity.loc != src)
REMOVE_TRAIT(our_entity, TRAIT_NO_TRANSFORM, src)
captured_entity = null
announce_escape(our_entity)
update_icon()
/obj/item/ghost_trap/proc/announce_escape(mob/our_entity)
var/area/our_area = get_area(src)
log_and_message_admins("[our_entity] escaped \the [name] at \the [our_area]", our_entity)
ghost_reporter.autosay("Attention: Spectral event detected. Captured entity at [our_area] has breached containment.", "Spectral Trap", "Science", using_map.get_map_levels(z))
/obj/item/ghost_trap/proc/can_use(mob/user)
return (user.IsAdvancedToolUser() && !isAI(user) && !user.stat && !user.restrained())
/obj/item/ghost_trap/attack_self(mob/user)
. = ..()
if(.)
return
if(captured_entity)
var/mob/our_entity = captured_entity.resolve()
if(our_entity)
to_chat(user, "You are unable to use \the [src]! It beeps that it an entity contained inside!")
return
if(!deployed && can_use(user))
user.visible_message(
span_danger("[user] starts to deploy \the [src]."),
span_danger("You begin deploying \the [src]!")
)
if(do_after(user, 6 SECONDS, target = src))
user.visible_message(
span_danger("[user] has deployed \the [src]."),
span_danger("You have deployed \the [src]!")
)
playsound(src, 'sound/machines/click.ogg', 70, 1)
deployed = TRUE
user.drop_from_inventory(src)
update_icon()
anchored = TRUE
log_and_message_admins("has set up a [name] at \the [get_area(loc)]", user)
/obj/item/ghost_trap/container_resist(mob/living/escapee)
if(!ismob(escapee))
return
visible_message(span_danger("Lights flicker and buzzers beep from \the [src], alerting that a containment breach is imminent!"))
if(do_after(escapee, 2 MINUTES, target = src)) //Escape!
REMOVE_TRAIT(escapee, TRAIT_NO_TRANSFORM, src)
captured_entity = null
escapee.forceMove(get_turf(src))
announce_escape(escapee)
visible_message(span_danger("A loud buzzer rings out as \the [src] suddenly opens, alerting that a containment breach has ocurred!"))
update_icon()
/obj/item/ghost_trap/attack_hand(mob/user)
if(has_buckled_mobs() && can_use(user))
user.visible_message(
span_notice("[user] begins freeing something from \the [src]."),
span_notice("You carefully begin to free something from \the [src]."),
)
if(do_after(user, 6 SECONDS, target = src))
user.visible_message(span_notice("Something has been freed from \the [src] by [user]."))
for(var/A in buckled_mobs)
unbuckle_mob(A)
anchored = FALSE
deployed = FALSE
else if(deployed && can_use(user))
user.visible_message(
span_danger("[user] starts to deactivate \the [src]."),
span_notice("You begin deactivate \the [src]!")
)
playsound(src, 'sound/machines/click.ogg', 50, 1)
if(do_after(user, 6 SECONDS, target = src))
user.visible_message(
span_danger("[user] has deactivated \the [src]."),
span_notice("You have deactivated \the [src]!")
)
deployed = FALSE
anchored = FALSE
update_icon()
else
..()
/obj/item/ghost_trap/proc/catch_ghost(mob/passing_entity)
if(!ismob(passing_entity)) //wtf did you do
return
captured_entity = WEAKREF(passing_entity)
if(isliving(passing_entity))
var/mob/living/living_entity = passing_entity
var/datum/component/shadekin/SK = living_entity.get_shadekin_component()
living_entity.phase_in(get_turf(src), SK)
passing_entity.forceMove(src)
var/area/our_area = get_area(src)
ghost_reporter.autosay("Attention: Spectral event detected. Trap activated at [our_area.name]", "Spectral Trap", "Science", using_map.get_map_levels(z))
ADD_TRAIT(passing_entity, TRAIT_NO_TRANSFORM, src)
to_chat(passing_entity, span_danger("You feel a sudden sensation pulling you into \the [src]!"))
if(isobserver(passing_entity))
to_chat(passing_entity, span_info("((You are incapable of moving or 'jumping' to turf by clicking, but can still escape via teleport or orbit!))"))
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_GHOST_CAPTURED, passing_entity)
/obj/item/ghost_trap/Crossed(atom/movable/AM)
if(istype(AM, /obj/effect/shadow_wight))
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_WIGHT_CAPTURED, AM)
visible_message(span_danger("A flurry of beams shoot into the air from \the [src] and into [AM], capturing and disintegrating it!"))
return
if(!ismob(AM)) //Only affects mobs!
return
//Ghost catching.
var/mob/passing_entity = AM
if(isobserver(passing_entity))
var/mob/observer/dead/ghost = AM
if(ghost.admin_ghosted || !ghost.interact_with_world)
return
//Catching phasers.
if(!passing_entity.is_incorporeal())
return
if(deployed)
visible_message(span_danger("A flurry of beams shoot into the air from \the [src]!"))
SSmotiontracker.ping(src,100) // Clunk!
catch_ghost(passing_entity)
deployed = FALSE
anchored = FALSE
update_icon()
log_and_message_admins("has been captured at \the [get_area(loc)] by the [name], last touched by [forensic_data?.get_lastprint()]", passing_entity)
/obj/item/ghost_trap/verb/hidden_vore()
set src in oview(1)
set category = "Object"
set name = "Eat Entity"
eat_entity(usr)
/obj/item/ghost_trap/proc/eat_entity(mob/living/user)
if(!isliving(user)) //no ghosts
return
if((user in contents))
to_chat(user, span_warning("You need to be inside \the [src] to do this."))
return
if(captured_entity)
var/mob/our_entity = captured_entity.resolve()
if(our_entity && (our_entity.loc == src) && our_entity.devourable)
REMOVE_TRAIT(our_entity, TRAIT_NO_TRANSFORM, src)
captured_entity = null
user.begin_instant_nom(user, our_entity, user, user.vore_selected)
return
to_chat(user, span_info("There appears to be nothing in the trap to eat!"))
return
@@ -0,0 +1,157 @@
/obj/item/ghost_catcher
name = "proton rifle"
desc = "A two handed device, used for 'catching ghosts'. Weakens the entity initially grabbed."
icon = 'icons/obj/guns/ghost_beam.dmi'
icon_state = "ghost_beam"
w_class = ITEMSIZE_NORMAL
force = 0
slot_flags = SLOT_BELT
var/grab_range = 5 // How many tiles away it can grab. Changing this also changes the box size.
/// Stops multiple grabbs if set to TRUE
var/busy = FALSE
/// The entity we are currently grabbing.
var/datum/weakref/grabbed_entity
/// How far we can move an entity in one go!
var/max_move_distance = 1
/// If we're held in two hands or not...Used until we get two handed component.
var/wielded = FALSE
COOLDOWN_DECLARE(ghost_cooldown)
COOLDOWN_DECLARE(click_cooldown)
//TODO:
// Make ghosts/phased entities slow when grabbed
// Make it so it searches in an AOE and grabs thing.
/obj/item/ghost_catcher/Destroy()
grabbed_entity = null
return ..()
/obj/item/ghost_catcher/update_icon()
if(busy)
icon_state = "ghost_beam_active"
else
icon_state = initial(icon_state)
/obj/item/ghost_catcher/update_held_icon()
var/mob/living/M = loc
if(istype(M) && M.can_wield_item(src) && is_held_twohanded(M))
wielded = TRUE
else
wielded = FALSE
..()
/obj/item/ghost_catcher/afterattack(atom/target, mob/user, proximity_flag)
if(!wielded)
to_chat(user, span_warning("You need to hold \the [src] in two hands to use it!"))
return
if(!COOLDOWN_FINISHED(src, ghost_cooldown))
to_chat(user, span_warning("The [src] is recharging!"))
return
if(!COOLDOWN_FINISHED(src, click_cooldown))
to_chat(user, span_warning("The [src] needs time between movements!"))
return
if(isturf(target) && (!target.incorporeal_grab()))
var/turf/T = target
if(!busy)
for(var/mob/entity in range(1, T)) //We'll let you grab things ON the tile or AROUND the tile you click on.
if(entity.incorporeal_grab())
target = entity
break
if(!ismob(target))
to_chat(user, span_warning("No valid target in range! Beginning cooldown."))
COOLDOWN_START(src, ghost_cooldown, 3 SECONDS) //Prevent from spamclicking to find ghosts.
return
else
if(grabbed_entity)
var/atom/movable/entity = grabbed_entity.resolve()
if(get_dist(T, entity) > max_move_distance)
to_chat(user, span_warning("\The [src] is unable to pull the entity that far!"))
return
entity.forceMove(T)
COOLDOWN_START(src, click_cooldown, 1 SECOND)
return
// Things that invalidate the scan immediately.
if(busy)
to_chat(user, span_warning("\The [src] is already grabbing an entity!"))
return
if(!target.incorporeal_grab(user)) // This will tell the user what is wrong.
return
if(get_dist(target, user) > grab_range)
to_chat(user, span_warning("You are too far away from \the [target] to catalogue it. Get closer."))
return
// Start the special effects.
busy = TRUE
update_icon()
var/datum/beam/scan_beam = user.Beam(target, icon_state = "curse1", time = 60 SECONDS)
var/filter = filter(type = "outline", size = 1, color = "#330099")
target.filters += filter
var/list/box_segments = list()
if(user.client)
box_segments = draw_box(target, grab_range, user.client)
color_box(box_segments, "#330099", 60 SECONDS)
playsound(src, 'sound/machines/beep.ogg', 50)
grabbed_entity = WEAKREF(target)
if(isliving(target))
var/mob/living/target_mob = target
target_mob.Weaken(3)
target_mob.Stun(3)
to_chat(target, span_danger("You feel yourself weakened from the [src]'s beam!"))
// The delay, and test for if the scan succeeds or not.
if(do_after(user, 60 SECONDS, target, timed_action_flags = IGNORE_USER_LOC_CHANGE|IGNORE_TARGET_LOC_CHANGE, max_distance = grab_range))
to_chat(user, span_warning("With a buzz, \the [src] flashes red, the beam on \the [target] has broken!"))
playsound(src, 'sound/machines/buzz-two.ogg', 50)
color_box(box_segments, "#330099", 3)
busy = FALSE
// Now clean up the effects.
update_icon()
QDEL_NULL(scan_beam)
if(target)
target.filters -= filter
if(user.client) // If for some reason they logged out mid-scan the box will be gone anyways.
delete_box(box_segments, user.client)
grabbed_entity = null
COOLDOWN_START(src, ghost_cooldown, 10 SECONDS) // Arbitrary cooldown to prevent spam. Adjust as needed.
/atom/proc/incorporeal_grab(mob/user)
if(is_incorporeal())
return TRUE
return FALSE
/mob/observer/dead/incorporeal_grab(mob/user) // Dead mobs can't be scanned.
if(admin_ghosted || !interact_with_world)
to_chat(user, span_notice("No entity detected!")) //The goggles ARE listed as unreliable. You could just be seeing static.
return FALSE
return ..()
//The backpack the gun is (typically) attached to!
/obj/item/proton_pack
name = "proton pack"
desc = "A complex backpack that houses a miniature antimatter reactor to power a spectral capture device."
icon = 'icons/obj/technomancer.dmi'
default_worn_icon = 'icons/inventory/back/mob.dmi'
icon_state = "technomancer_core"
item_state = "proton_pack"
slot_flags = SLOT_BACK
preserve_item = 1
w_class = ITEMSIZE_LARGE
var/obj/item/ghost_catcher/who_ya_gunna_call = /obj/item/ghost_catcher
/obj/item/proton_pack/Initialize(mapload)
AddComponent(/datum/component/tethered_item, who_ya_gunna_call)
. = ..()
/obj/item/proton_pack/attack_hand(mob/living/user)
// See important note in tethered_item.dm
if(SEND_SIGNAL(src,COMSIG_ITEM_ATTACK_SELF,user) & COMPONENT_CANCEL_ATTACK_CHAIN)
return TRUE
. = ..()