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
+4 -4
View File
@@ -2,7 +2,7 @@
// Draws a box showing the limits of movement while scanning something.
// Only the client supplied will see the box.
/obj/item/cataloguer/proc/draw_box(atom/A, box_size, client/C)
/proc/draw_box(atom/A, box_size, client/C)
. = list()
// Things moved with pixel_[x|y] will move the box, so this is to correct that.
var/pixel_x_correction = -A.pixel_x
@@ -47,7 +47,7 @@
#undef ICON_SIZE
// Draws an individual segment of the box.
/obj/item/cataloguer/proc/draw_line(atom/A, line_dir, line_pixel_x, line_pixel_y, client/C)
/proc/draw_line(atom/A, line_dir, line_pixel_x, line_pixel_y, client/C)
var/image/line = image(icon = 'icons/effects/effects.dmi', loc = A, icon_state = "stripes", dir = line_dir)
line.pixel_x = line_pixel_x
line.pixel_y = line_pixel_y
@@ -58,11 +58,11 @@
return line
// Removes the box that was generated before from the client.
/obj/item/cataloguer/proc/delete_box(list/box_segments, client/C)
/proc/delete_box(list/box_segments, client/C)
for(var/i in box_segments)
C.images -= i
qdel(i)
/obj/item/cataloguer/proc/color_box(list/box_segments, new_color, new_time)
/proc/color_box(list/box_segments, new_color, new_time)
for(var/i in box_segments)
animate(i, color = new_color, time = new_time)
+78 -19
View File
@@ -315,31 +315,86 @@
icon = 'icons/obj/entrepreneur.dmi'
icon_state = "emf"
var/emf = 5
var/turf/last_used = 0
var/emf_change = 0
///If our scanner will accurately detect ghosts or not.
var/advanced = FALSE
///How far our we search. 0 = own turf.
var/detection_range = 0
///How often we can use the EMF actively.
COOLDOWN_DECLARE(scan_cooldown)
/obj/item/entrepreneur/emf/examine(mob/user)
. = ..()
switch(emf)
if(-1000 to 20)
. += span_info("The EMF reader is very low, reading [emf]mG.")
if(20 to 40)
. += span_info("The EMF reader is low, reading [emf]mG.")
if(40 to 60)
. += span_info(span_red("The EMF reader is reading moderate interference, reading [emf]mG."))
if(60 to 80)
. += span_info(span_red("The EMF reader is reading high interference, reading [emf]mG."))
if(80 to 1000)
. += span_info(span_red("The EMF reader is reading extremely high interference, reading [emf]mG."))
/obj/item/entrepreneur/emf/Initialize(mapload)
. = ..()
emf = rand(1,100)
START_PROCESSING(SSobj, src)
/obj/item/entrepreneur/emf/process()
search_for_ghosts()
/obj/item/entrepreneur/emf/attack_self(mob/user)
. = ..(user)
if(.)
return TRUE
if(!last_used)
emf = rand(1,100)
last_used = get_turf(user)
var/current_used = get_turf(user)
var/mob/observer/spooky = locate() in current_used
if(last_used != current_used)
if(emf >= 100)
emf = 100
if(emf <= 20)
emf = 20
if(spooky)
if(COOLDOWN_FINISHED(src, scan_cooldown))
search_for_ghosts(user)
else
to_chat(user, span_warning("Your EMF scanner is recharging. The current reading is [emf]mG."))
/obj/item/entrepreneur/emf/proc/search_for_ghosts(mob/user)
var/turf/our_turf = get_turf(src)
if(!our_turf)
return
///How many ghosts are around us.
var/ghosts_present = 0
///How much we'll increase/decrease the EMF reading.
var/emf_change = 0
//Loop for the 'advanced' emfs, which detect actual ghosts/phasers.
if(advanced)
for(var/mob/living/entity in range(detection_range, our_turf))
if(isobserver(entity))
var/mob/observer/dead/ghost = entity
if(ghost.following || !ghost.interact_with_world || ghost.admin_ghosted) //Ghosts orbiting us or someone else, or have opted out of interactions.
continue
ghosts_present++
if(entity.is_incorporeal())
ghosts_present++
if(emf >= 100)
emf = 100
if(emf <= 20)
emf = 20
if(ghosts_present)
if(advanced)
emf_change = rand(3 * ghosts_present, 10 * ghosts_present)
else
emf_change = rand(-15,20) //Trend upwards but not by enough to prove ghosts actually exist
else
if(advanced)
emf_change = rand(-5, -1)
else
emf_change = rand(-20,15) //Trend downwards
last_used = get_turf(user)
emf = (emf + emf_change)
update_icon()
to_chat(user, span_notice("You update the EMF scanner and check the reading. It reads [emf]mG!"))
emf = (emf + emf_change)
update_icon()
if(user)
to_chat(user, span_notice("You update the EMF scanner and check the reading. It reads [emf]mG!"))
COOLDOWN_START(src, scan_cooldown, 5 SECONDS)
/obj/item/entrepreneur/emf/update_icon()
switch(emf)
@@ -361,10 +416,14 @@
icon = 'icons/obj/entrepreneur.dmi'
icon_state = "spirit_board"
var/list/possible_results = list("A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","Yes","No","1","2","3","4","5","6","7","8","9","0","Nothing")
///What the next letter/number will be.
var/next_result = 0
///If ghosts can interact with it.
var/ghost_enabled = 1
///If the board will always display what ghosts put
var/accurate = FALSE
/obj/item/entrepreneur/spirit_board/attackby(obj/item/reagent_containers/food/drinks/W as obj, mob/living/user as mob)
/obj/item/entrepreneur/spirit_board/attackby(obj/item/reagent_containers/food/drinks/W, mob/living/user)
if(!istype(user))
return 0
if(!istype(W))
@@ -392,7 +451,7 @@
to_chat(user, span_warning("You cannot interact with this board because you are banned from playing ghost roles."))
return
next_result = tgui_input_list(user, "What should it land on next?", "Next result", possible_results)
if(!is_admin(user)) //admins can bypass this for event stuff
if(!is_admin(user) || !accurate) //admins can bypass this for event stuff
if(prob(25))
next_result = 0 //25% chance for the ghost to fail to manipulate the board
-4
View File
@@ -184,7 +184,6 @@
w_class = ITEMSIZE_NORMAL
requires_wield = FALSE
/obj/item/kinetic_crusher/machete
// general purpose. cleaves though
name = "proto-kinetic machete"
@@ -209,9 +208,6 @@
update_item_state = FALSE
slot_flags = SLOT_BELT
/obj/item/kinetic_crusher/machete/gauntlets
// did someone say single target damage
name = "\improper proto-kinetic gear"
+33 -16
View File
@@ -11,8 +11,8 @@
icon = 'icons/mob/ghost.dmi'
icon_state = "ghost"
stat = DEAD
canmove = 0
blinded = 0
canmove = FALSE
blinded = FALSE
anchored = TRUE // don't get pushed around
var/list/visibleChunks = list()
var/datum/visualnet/ghost/visualnet
@@ -24,21 +24,23 @@
var/started_as_observer //This variable is set to 1 when you enter the game as an observer.
//If you died in the game and are a ghsot - this will remain as null.
//Note that this is not a reliable way to determine if admins started as observers, since they change mobs a lot.
var/has_enabled_antagHUD = 0
var/medHUD = 0
var/secHUD = 0
var/antagHUD = 0
universal_speak = 1
var/has_enabled_antagHUD = FALSE
var/medHUD = FALSE
var/secHUD = FALSE
var/antagHUD = FALSE
universal_speak = TRUE
var/atom/movable/following = null
var/admin_ghosted = 0
var/anonsay = 0
var/ghostvision = 1 //is the ghost able to see things humans can't?
var/admin_ghosted = FALSE
var/anonsay = FALSE
var/ghostvision = TRUE //is the ghost able to see things humans can't?
var/lighting_alpha = 255
incorporeal_move = 1
var/is_manifest = 0 //If set to 1, the ghost is able to whisper. Usually only set if a cultist drags them through the veil.
var/toggled_invisible = 0
incorporeal_move = TRUE
/// If set to TRUE, the ghost is able to whisper. Usually only set if a cultist drags them through the veil.
var/is_manifest = FALSE
var/toggled_invisible = FALSE
var/ghost_sprite = null
/// If TRUE, the ghost can be interacted with by the corporeal world (ghost traps, photon pack, etc)
var/interact_with_world = TRUE
var/last_revive_notification = null // world.time of last notification, used to avoid spamming players from defibs or cloners.
var/cleanup_timer // Refernece to a timer that will delete this mob if no client returns
var/selecting_ghostrole = FALSE
@@ -568,11 +570,11 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
return ..()
/mob/proc/check_holy(var/turf/T)
return 0
return FALSE
/mob/observer/dead/check_holy(var/turf/T)
if(check_rights_for(src.client, R_ADMIN|R_FUN|R_EVENT))
return 0
return FALSE
return (T && T.holy) && (is_manifest || (mind in GLOB.cult.current_antagonists))
@@ -783,6 +785,21 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
var/image/J = image('icons/mob/mob.dmi', loc = src, icon_state = icon)
client.images += J
/mob/observer/dead/verb/toggle_interactions()
set name = "Toggle Interactions"
set desc = "Allows you to toggle if you wish for the corporeal world to interact with you!"
set category = "Ghost.Settings"
toggle_ghost_interactions()
/mob/observer/dead/proc/toggle_ghost_interactions()
if(is_manifest)
to_chat(src, span_info("You are currently manifested into the world and can not toggle this!"))
return
interact_with_world = !interact_with_world
to_chat(src, span_info("You will [interact_with_world ? "now" : "no longer"] be able to be interacted with by the corporeal world!"))
/mob/observer/dead/verb/toggle_visibility()
set name = "Toggle Visibility"
+1 -1
View File
@@ -1438,7 +1438,7 @@
if(!.)
return
client.screen.Remove(GLOB.global_hud.blurry, GLOB.global_hud.druggy, GLOB.global_hud.vimpaired, GLOB.global_hud.darkMask, GLOB.global_hud.nvg, GLOB.global_hud.thermal, GLOB.global_hud.meson, GLOB.global_hud.science, GLOB.global_hud.material, GLOB.global_hud.whitense)
client.screen.Remove(GLOB.global_hud.blurry, GLOB.global_hud.druggy, GLOB.global_hud.vimpaired, GLOB.global_hud.darkMask, GLOB.global_hud.nvg, GLOB.global_hud.thermal, GLOB.global_hud.meson, GLOB.global_hud.science, GLOB.global_hud.material, GLOB.global_hud.whitense, GLOB.global_hud.heavy_whitense)
if(istype(client.eye,/obj/machinery/camera))
var/obj/machinery/camera/cam = client.eye
+3
View File
@@ -285,3 +285,6 @@
var/vent_crawl_time = 4.5 SECONDS // Time to animate entering a vent
VAR_PRIVATE/is_motion_tracking = FALSE // Prevent multiple unsubs and resubs, also used to check if the vis layer is enabled, use has_motiontracking() to get externally.
VAR_PRIVATE/wants_to_see_motion_echos = TRUE
var/is_slipping = FALSE
var/slip_protect = 1
+2
View File
@@ -389,6 +389,8 @@
is_leaving_belly = FALSE
return
is_leaving_belly = FALSE
if(isghosttrap(mob.loc))
return
var/turf/mobloc = get_turf(mob)
if(incorporeal_speed)
@@ -334,6 +334,16 @@
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_PISTOL
)
/datum/design_techweb/pistol_mag_9mm_spectral
SET_AMMO_DESIGN_NAMEDESC("pistol magazine (9mm spectral)")
id = "pistol_mag_9mm_spectral"
materials = list(MAT_STEEL = 750)
build_type = AUTOLATHE
build_path = /obj/item/ammo_magazine/m9mm/spectral
category = list(
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_PISTOL
)
// Small mags for small or old guns. These are all hidden because they are traitor mags and will otherwise just clutter the Autolathe.
/datum/design_techweb/pistol_mag_compact_9mm
@@ -478,6 +488,16 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SECURITY | DEPARTMENT_BITFLAG_SCIENCE
/datum/design_techweb/smg_mag_9mm_spectral
SET_AMMO_DESIGN_NAMEDESC("SMG magazine (9mm spectral)")
id = "smg_mag_9mm_spectral"
materials = list(MAT_STEEL = 1500)
build_type = AUTOLATHE
build_path = /obj/item/ammo_magazine/m9mml/spectral
category = list(
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_RIFLE
)
/////// 10mm
/datum/design_techweb/smg_mag_10m
@@ -987,6 +1007,16 @@
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_LOADERS
)
/datum/design_techweb/loader_357_spectral
SET_AMMO_DESIGN_NAMEDESC("speedloader (.357 spectral)")
id = "loader_357_spectral"
materials = list(MAT_STEEL = 1575)
build_type = AUTOLATHE
build_path = /obj/item/ammo_magazine/s357/spectral
category = list(
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_LOADERS
)
/////// 44
/datum/design_techweb/loader_44
@@ -1013,6 +1043,16 @@
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_LOADERS
)
/datum/design_techweb/loader_44_spectral
SET_AMMO_DESIGN_NAMEDESC("speedloader (.44 spectral)")
id = "loader_44_spectral"
materials = list(MAT_STEEL = 1575)
build_type = AUTOLATHE
build_path = /obj/item/ammo_magazine/s44/spectral
category = list(
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_LOADERS
)
///////////////////
/*Rifle Ammoboxes*/
///////////////////
@@ -1071,6 +1111,16 @@
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_BOXES
)
/datum/design_techweb/ammobox_38_spectral
SET_AMMO_DESIGN_NAMEDESC("ammo box (.38 spectral)")
id = "ammobox_38_spectral"
materials = list(MAT_STEEL = 1440)
build_type = AUTOLATHE
build_path = /obj/item/ammo_magazine/ammo_box/b38/spectral
category = list(
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_BOXES
)
/*
* 10mm
*/
@@ -1148,6 +1198,16 @@
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_BOXES
)
/datum/design_techweb/ammobox_44_spectral
SET_AMMO_DESIGN_NAMEDESC("ammo box (.44 spectral)")
id = "ammobox_44_spectral"
materials = list(MAT_STEEL = 1440)
build_type = AUTOLATHE
build_path = /obj/item/ammo_magazine/ammo_box/b44/spectral
category = list(
RND_SUBCATEGORY_WEAPONS_AMMO + RND_SUBCATEGORY_WEAPONS_AMMO_BOXES
)
/*
* .45
*/
@@ -1103,3 +1103,60 @@
RND_CATEGORY_WEAPONS + RND_SUBCATEGORY_WEAPONS_RANGED
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
//Standalone photon rifle. Not unlockable for now.
/datum/design_techweb/photon_rifle
name = "Photon Rifle"
id = "ghost_gun"
build_type = PROTOLATHE
materials = list(MAT_STEEL = 8000, MAT_GLASS = 8000, MAT_PHORON = 8000, MAT_DIAMOND = 6000, MAT_VERDANTIUM = 6000) //Expensive.
build_path = /obj/item/ghost_catcher
category = list(
RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_MISC
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
/datum/design_techweb/photon_pack
name = "Proton Pack"
id = "ghost_pack"
build_type = PROTOLATHE
materials = list(MAT_STEEL = 4000, MAT_GLASS = 4000, MAT_PHORON = 4000, MAT_DIAMOND = 2000, MAT_VERDANTIUM = 2000) //Expensive.
build_path = /obj/item/proton_pack
category = list(
RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_MISC
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
/datum/design_techweb/spectral_trap
name = "Spectral Trap"
id = "ghost_trap"
build_type = PROTOLATHE
materials = list(MAT_STEEL = 2000, MAT_GLASS = 2000, MAT_PHORON = 1000, MAT_DIAMOND = 500)
build_path = /obj/item/ghost_trap
category = list(
RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_MISC
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
/datum/design_techweb/advanced_emf
name = "Advanced EMF Reader"
id = "ghost_emf"
build_type = PROTOLATHE
materials = list(MAT_STEEL = 5000, MAT_GLASS = 5000) //Assumed you're making this with the ghost_gun, so it has similar values.
build_path = /obj/item/entrepreneur/emf/professional
category = list(
RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_MISC
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
/datum/design_techweb/spectral_goggles
name = "Spectral Goggles"
id = "ghost_goggles"
build_type = PROTOLATHE
materials = list(MAT_STEEL = 2000, MAT_GLASS = 2000, MAT_PHORON = 2000, MAT_DIAMOND = 500) //Assumed you're making this with the ghost_gun, so it has similar values.
build_path = /obj/item/clothing/glasses/ghost
category = list(
RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_MISC
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
@@ -0,0 +1,22 @@
//For spooky, ghost related experiments.
/datum/experiment/ghost_capture
name = "Spectral Capture"
description = "There's supposed 'ghosts' and 'specters' roaming around this section of space! Let's capture one of these entities for science!"
exp_tag = "Spectral"
allowed_experimentors = list(/obj/machinery/rnd/server)
/datum/experiment/ghost_capture/is_complete()
return completed
/datum/experiment/ghost_capture/New()
..()
RegisterSignal(SSdcs, COMSIG_GLOB_GHOST_CAPTURED, PROC_REF(ghost_captured))
RegisterSignal(SSdcs, COMSIG_GLOB_WIGHT_CAPTURED, PROC_REF(ghost_captured))
return TRUE
/datum/experiment/ghost_capture/proc/ghost_captured(datum/source, mob/captured_entity)
SIGNAL_HANDLER
// We don't actually care about the mob that's captured, just that something was captured.
completed = TRUE
UnregisterSignal(SSdcs, COMSIG_GLOB_GHOST_CAPTURED)
UnregisterSignal(SSdcs, COMSIG_GLOB_WIGHT_CAPTURED)
+7
View File
@@ -24,6 +24,13 @@
stored_research.techweb_servers |= src
name += " [num2hex(rand(1,65535), -1)]" //gives us a random four-digit hex number as part of the name. Y'know, for fluff.
//So we can be listening for experiments that are global.
AddComponent(/datum/component/experiment_handler, \
allowed_experiments = list(/datum/experiment/ghost_capture), \
config_mode = EXPERIMENT_CONFIG_UI, \
config_flags = EXPERIMENT_CONFIG_ALWAYS_ACTIVE)
/obj/machinery/rnd/server/Destroy()
if(stored_research)
stored_research.techweb_servers -= src
@@ -178,3 +178,49 @@
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_4_POINTS)
announce_channels = list(CHANNEL_SCIENCE)
*/
//Ghost catching stuff! Disabled on Virgo, but used downstream.
/datum/techweb_node/ghost_basic
id = TECHWEB_NODE_GHOST_BASIC
display_name = "Spectral Detection and Containment"
description = "Determining high-energy signatures indicative of spectral entities and developing methods to safely contain them."
prereq_ids = list(TECHWEB_NODE_APPLIED_BLUESPACE, TECHWEB_NODE_APPLIED_ANOMALY_HARVESTING)
design_ids = list(
"ghost_trap",
"ghost_emf",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_5_POINTS)
announce_channels = list(CHANNEL_SCIENCE, CHANNEL_SECURITY)
hidden = TRUE //Hidden on Virgo
/datum/techweb_node/ghost_advanced
id = TECHWEB_NODE_GHOST_ADVANCED
display_name = "Spectral Hunting"
description = "Developing advanced weaponry for tracking and engaging spectral entities."
prereq_ids = list(TECHWEB_NODE_GHOST_BASIC)
design_ids = list(
"ghost_pack",
"ghost_goggles",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_5_POINTS)
announce_channels = list(CHANNEL_SCIENCE, CHANNEL_SECURITY)
required_experiments = list(/datum/experiment/ghost_capture)
hidden = TRUE //Hidden on Virgo
/datum/techweb_node/ghost_rounds
id = TECHWEB_NODE_GHOST_ROUNDS
display_name = "Spectral Rounds"
description = "Having reached the pinnacle of spectral research, we can now produce specialized ammunition for ghost hunting."
prereq_ids = list(TECHWEB_NODE_GHOST_ADVANCED)
design_ids = list(
"pistol_mag_9mm_spectral",
"smg_mag_9mm_spectral",
"loader_357_spectral",
"loader_44_spectral",
"ammobox_38_spectral",
"ammobox_44_spectral",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_5_POINTS)
announce_channels = list(CHANNEL_SCIENCE, CHANNEL_SECURITY)
//required_experiments = list(ADD_A_GOOD_EXPERIMENT_HERE)
hidden = TRUE //Hidden on Virgo
-5
View File
@@ -1,5 +0,0 @@
// Slipnoms from chompstation downstream, credit to cadyn for the original PR.
/mob/living
var/is_slipping = FALSE
var/slip_protect = 1