diff --git a/code/__defines/dcs/signals/signals_trasheating.dm b/code/__defines/dcs/signals/signals_trasheating.dm index fa1e42c737..ca9cb61893 100644 --- a/code/__defines/dcs/signals/signals_trasheating.dm +++ b/code/__defines/dcs/signals/signals_trasheating.dm @@ -14,3 +14,7 @@ #define COMSIG_ITEM_AFTER_TRASH_EAT_HIDE_MESSAGE (1<<0) ///called after mob eats item: (obj/item/thing) #define COMSIG_MOB_AFTER_TRASH_EATING "mob_after_trash_eating" +///called when a ghost or phaser is captured by a ghosttrap: (mob/passing_entity) +#define COMSIG_GLOB_GHOST_CAPTURED "ghost_captured_by_trap" +///Called when a shadow wright passes by a ghosttrap: (obj/effect/shadow_wight) +#define COMSIG_GLOB_WIGHT_CAPTURED "wight_captured_by_trap" diff --git a/code/__defines/research/techweb_nodes.dm b/code/__defines/research/techweb_nodes.dm index 31f2217e80..a4db6d9e1c 100644 --- a/code/__defines/research/techweb_nodes.dm +++ b/code/__defines/research/techweb_nodes.dm @@ -162,3 +162,6 @@ #define TECHWEB_NODE_RSF "rapid_service_fab" #define TECHWEB_NODE_MATERIALSHEETS "material_sheets" #define TECHWEB_NODE_SMARTFRIDGES "smartfridges" +#define TECHWEB_NODE_GHOST_BASIC "ghost_basics" +#define TECHWEB_NODE_GHOST_ADVANCED "ghost_advanced" +#define TECHWEB_NODE_GHOST_ROUNDS "ghost_rounds" diff --git a/code/_macros.dm b/code/_macros.dm index 42682d5c8f..50a02c9f2d 100644 --- a/code/_macros.dm +++ b/code/_macros.dm @@ -31,3 +31,4 @@ #define isrobotmultibelt(A) istype(A, /obj/item/robotic_multibelt) #define isgripperpocket(A) istype(A, /obj/item/storage/internal/gripper) #define iscapturecrystal(A) istype(A, /obj/item/capture_crystal) +#define isghosttrap(A) istype(A, /obj/item/ghost_trap) diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm index 25427aa0eb..9926c6e39a 100644 --- a/code/_onclick/hud/hud.dm +++ b/code/_onclick/hud/hud.dm @@ -8,6 +8,7 @@ GLOBAL_LIST_INIT(global_huds, list( GLOB.global_hud.druggy, GLOB.global_hud.blurry, GLOB.global_hud.whitense, + GLOB.global_hud.heavy_whitense, GLOB.global_hud.vimpaired, GLOB.global_hud.darkMask, GLOB.global_hud.centermarker, @@ -28,6 +29,7 @@ GLOBAL_LIST_INIT(global_huds, list( var/atom/movable/screen/druggy var/atom/movable/screen/blurry var/atom/movable/screen/whitense + var/atom/movable/screen/heavy_whitense var/list/vimpaired var/list/darkMask var/atom/movable/screen/centermarker @@ -70,6 +72,11 @@ GLOBAL_LIST_INIT(global_huds, list( whitense.icon = 'icons/effects/static.dmi' whitense.icon_state = "1 light" + //static overlay effect for cameras and the like + heavy_whitense = new /atom/movable/screen/global_screen() + heavy_whitense.icon = 'icons/effects/static.dmi' + heavy_whitense.icon_state = "1 heavy" + //darksight 'hanger' for attached icons darksight = new /atom/movable/screen() darksight.icon = null diff --git a/code/_onclick/observer.dm b/code/_onclick/observer.dm index aad65fdcd6..8425a4e558 100644 --- a/code/_onclick/observer.dm +++ b/code/_onclick/observer.dm @@ -20,7 +20,8 @@ if(A == mind.current || (mind.current in A)) // double click your corpse or whatever holds it reenter_corpse() // (cloning scanner, body bag, closet, mech, etc) return - + if(isghosttrap(src.loc)) + return // Things you might plausibly want to follow if(istype(A,/atom/movable)) ManualFollow(A) diff --git a/code/game/objects/items/devices/personal_shield_generator_vr.dm b/code/game/objects/items/devices/personal_shield_generator_vr.dm index 96bffc988b..1596c9dde6 100644 --- a/code/game/objects/items/devices/personal_shield_generator_vr.dm +++ b/code/game/objects/items/devices/personal_shield_generator_vr.dm @@ -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" diff --git a/code/game/objects/items/devices/radio/intercom.dm b/code/game/objects/items/devices/radio/intercom.dm index 590ceadc41..be5aec3d29 100644 --- a/code/game/objects/items/devices/radio/intercom.dm +++ b/code/game/objects/items/devices/radio/intercom.dm @@ -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) diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm index 18d5325248..3f1b6601de 100644 --- a/code/game/objects/items/devices/radio/radio.dm +++ b/code/game/objects/items/devices/radio/radio.dm @@ -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] diff --git a/code/game/objects/items/ghost_hunting/ammo.dm b/code/game/objects/items/ghost_hunting/ammo.dm new file mode 100644 index 0000000000..0ae542eb80 --- /dev/null +++ b/code/game/objects/items/ghost_hunting/ammo.dm @@ -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 diff --git a/code/game/objects/items/ghost_hunting/equipment.dm b/code/game/objects/items/ghost_hunting/equipment.dm new file mode 100644 index 0000000000..8a00ca8e00 --- /dev/null +++ b/code/game/objects/items/ghost_hunting/equipment.dm @@ -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 diff --git a/code/game/objects/items/ghost_hunting/goggles.dm b/code/game/objects/items/ghost_hunting/goggles.dm new file mode 100644 index 0000000000..73410fdf7f --- /dev/null +++ b/code/game/objects/items/ghost_hunting/goggles.dm @@ -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 diff --git a/code/game/objects/items/ghost_hunting/supplypack.dm b/code/game/objects/items/ghost_hunting/supplypack.dm new file mode 100644 index 0000000000..69411fd1b2 --- /dev/null +++ b/code/game/objects/items/ghost_hunting/supplypack.dm @@ -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" diff --git a/code/game/objects/items/ghost_hunting/trap.dm b/code/game/objects/items/ghost_hunting/trap.dm new file mode 100644 index 0000000000..9ebddd9cf3 --- /dev/null +++ b/code/game/objects/items/ghost_hunting/trap.dm @@ -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 diff --git a/code/game/objects/items/ghost_hunting/weapons.dm b/code/game/objects/items/ghost_hunting/weapons.dm new file mode 100644 index 0000000000..ad96421003 --- /dev/null +++ b/code/game/objects/items/ghost_hunting/weapons.dm @@ -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 + . = ..() diff --git a/code/modules/catalogue/cataloguer_visuals.dm b/code/modules/catalogue/cataloguer_visuals.dm index c13419133d..c1781af71d 100644 --- a/code/modules/catalogue/cataloguer_visuals.dm +++ b/code/modules/catalogue/cataloguer_visuals.dm @@ -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) diff --git a/code/modules/entrepreneur/entrepreneur_items.dm b/code/modules/entrepreneur/entrepreneur_items.dm index 1d55a6646c..c62d5c8302 100644 --- a/code/modules/entrepreneur/entrepreneur_items.dm +++ b/code/modules/entrepreneur/entrepreneur_items.dm @@ -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 diff --git a/code/modules/mining/kinetic_crusher.dm b/code/modules/mining/kinetic_crusher.dm index ed4ff4ac5d..d154819635 100644 --- a/code/modules/mining/kinetic_crusher.dm +++ b/code/modules/mining/kinetic_crusher.dm @@ -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" diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 29039455bb..6707bd88ef 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -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" diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm index df406e9e4a..04c2f8393e 100644 --- a/code/modules/mob/living/carbon/human/life.dm +++ b/code/modules/mob/living/carbon/human/life.dm @@ -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 diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 8413980fd5..ddad47e0d6 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -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 diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index 9215213f73..203d5a682d 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -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) diff --git a/code/modules/research/tg/designs/ammunition_designs.dm b/code/modules/research/tg/designs/ammunition_designs.dm index 5a9018166d..6a819ff43d 100644 --- a/code/modules/research/tg/designs/ammunition_designs.dm +++ b/code/modules/research/tg/designs/ammunition_designs.dm @@ -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 */ diff --git a/code/modules/research/tg/designs/weapon_designs.dm b/code/modules/research/tg/designs/weapon_designs.dm index 35d5847f09..af7d7a4742 100644 --- a/code/modules/research/tg/designs/weapon_designs.dm +++ b/code/modules/research/tg/designs/weapon_designs.dm @@ -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 diff --git a/code/modules/research/tg/experisci/experiment/instances/spectral.dm b/code/modules/research/tg/experisci/experiment/instances/spectral.dm new file mode 100644 index 0000000000..53c440277b --- /dev/null +++ b/code/modules/research/tg/experisci/experiment/instances/spectral.dm @@ -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) diff --git a/code/modules/research/tg/server.dm b/code/modules/research/tg/server.dm index f6ba9de14a..cda3f61bd1 100644 --- a/code/modules/research/tg/server.dm +++ b/code/modules/research/tg/server.dm @@ -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 diff --git a/code/modules/research/tg/techwebs/nodes/research_nodes.dm b/code/modules/research/tg/techwebs/nodes/research_nodes.dm index ff5d36c3a5..4337223e90 100644 --- a/code/modules/research/tg/techwebs/nodes/research_nodes.dm +++ b/code/modules/research/tg/techwebs/nodes/research_nodes.dm @@ -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 diff --git a/code/modules/vore/eating/slipvore_vr.dm b/code/modules/vore/eating/slipvore_vr.dm index 2e73a815bf..e69de29bb2 100644 --- a/code/modules/vore/eating/slipvore_vr.dm +++ b/code/modules/vore/eating/slipvore_vr.dm @@ -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 diff --git a/icons/effects/beam.dmi b/icons/effects/beam.dmi index 4d0710885f..754e8434b0 100644 Binary files a/icons/effects/beam.dmi and b/icons/effects/beam.dmi differ diff --git a/icons/inventory/back/mob.dmi b/icons/inventory/back/mob.dmi index cc83b48ef4..09967245a4 100644 Binary files a/icons/inventory/back/mob.dmi and b/icons/inventory/back/mob.dmi differ diff --git a/icons/inventory/eyes/item.dmi b/icons/inventory/eyes/item.dmi index 0cd74763b7..e5dc92fafc 100644 Binary files a/icons/inventory/eyes/item.dmi and b/icons/inventory/eyes/item.dmi differ diff --git a/icons/inventory/eyes/mob.dmi b/icons/inventory/eyes/mob.dmi index 06b7d95969..d4c4590990 100644 Binary files a/icons/inventory/eyes/mob.dmi and b/icons/inventory/eyes/mob.dmi differ diff --git a/icons/inventory/eyes/mob_akula.dmi b/icons/inventory/eyes/mob_akula.dmi index 3395e50b4d..6fd724720d 100644 Binary files a/icons/inventory/eyes/mob_akula.dmi and b/icons/inventory/eyes/mob_akula.dmi differ diff --git a/icons/inventory/eyes/mob_fennec.dmi b/icons/inventory/eyes/mob_fennec.dmi index e0dece3e15..b1120fe041 100644 Binary files a/icons/inventory/eyes/mob_fennec.dmi and b/icons/inventory/eyes/mob_fennec.dmi differ diff --git a/icons/inventory/eyes/mob_fox.dmi b/icons/inventory/eyes/mob_fox.dmi index e0dece3e15..b1120fe041 100644 Binary files a/icons/inventory/eyes/mob_fox.dmi and b/icons/inventory/eyes/mob_fox.dmi differ diff --git a/icons/inventory/eyes/mob_nevrean.dmi b/icons/inventory/eyes/mob_nevrean.dmi index f4771ab49b..6a1fdf0972 100644 Binary files a/icons/inventory/eyes/mob_nevrean.dmi and b/icons/inventory/eyes/mob_nevrean.dmi differ diff --git a/icons/inventory/eyes/mob_sergal.dmi b/icons/inventory/eyes/mob_sergal.dmi index a009e2bdbd..1fc2200064 100644 Binary files a/icons/inventory/eyes/mob_sergal.dmi and b/icons/inventory/eyes/mob_sergal.dmi differ diff --git a/icons/inventory/eyes/mob_tajaran.dmi b/icons/inventory/eyes/mob_tajaran.dmi index df6acb5dc5..a11a9f37d6 100644 Binary files a/icons/inventory/eyes/mob_tajaran.dmi and b/icons/inventory/eyes/mob_tajaran.dmi differ diff --git a/icons/inventory/eyes/mob_teshari.dmi b/icons/inventory/eyes/mob_teshari.dmi index 005a87fb59..fcf486b961 100644 Binary files a/icons/inventory/eyes/mob_teshari.dmi and b/icons/inventory/eyes/mob_teshari.dmi differ diff --git a/icons/inventory/eyes/mob_unathi.dmi b/icons/inventory/eyes/mob_unathi.dmi index b1f6acd95d..20095669b6 100644 Binary files a/icons/inventory/eyes/mob_unathi.dmi and b/icons/inventory/eyes/mob_unathi.dmi differ diff --git a/icons/inventory/eyes/mob_vox.dmi b/icons/inventory/eyes/mob_vox.dmi index 091a6f927f..e82ec04a4b 100644 Binary files a/icons/inventory/eyes/mob_vox.dmi and b/icons/inventory/eyes/mob_vox.dmi differ diff --git a/icons/inventory/eyes/mob_vulpkanin.dmi b/icons/inventory/eyes/mob_vulpkanin.dmi index 583d66a90a..df7c15cb67 100644 Binary files a/icons/inventory/eyes/mob_vulpkanin.dmi and b/icons/inventory/eyes/mob_vulpkanin.dmi differ diff --git a/icons/obj/ghost_trap.dmi b/icons/obj/ghost_trap.dmi new file mode 100644 index 0000000000..70372bff3e Binary files /dev/null and b/icons/obj/ghost_trap.dmi differ diff --git a/icons/obj/guns/ghost_beam.dmi b/icons/obj/guns/ghost_beam.dmi new file mode 100644 index 0000000000..34e6865302 Binary files /dev/null and b/icons/obj/guns/ghost_beam.dmi differ diff --git a/vorestation.dme b/vorestation.dme index 9cb0798cf9..80b2619b9d 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -1735,6 +1735,11 @@ #include "code\game\objects\items\devices\scanners\reagents.dm" #include "code\game\objects\items\devices\scanners\sleevemate.dm" #include "code\game\objects\items\devices\scanners\slime.dm" +#include "code\game\objects\items\ghost_hunting\ammo.dm" +#include "code\game\objects\items\ghost_hunting\equipment.dm" +#include "code\game\objects\items\ghost_hunting\goggles.dm" +#include "code\game\objects\items\ghost_hunting\trap.dm" +#include "code\game\objects\items\ghost_hunting\weapons.dm" #include "code\game\objects\items\robot\robot_items.dm" #include "code\game\objects\items\robot\robot_parts.dm" #include "code\game\objects\items\robot\robot_upgrades.dm" @@ -4560,6 +4565,7 @@ #include "code\modules\research\tg\experisci\experiment\instances\ordinance.dm" #include "code\modules\research\tg\experisci\experiment\instances\people.dm" #include "code\modules\research\tg\experisci\experiment\instances\physical.dm" +#include "code\modules\research\tg\experisci\experiment\instances\spectral.dm" #include "code\modules\research\tg\experisci\experiment\instances\teleporting.dm" #include "code\modules\research\tg\experisci\experiment\types\experiment.dm" #include "code\modules\research\tg\experisci\experiment\types\ordinance.dm" @@ -4874,7 +4880,6 @@ #include "code\modules\vore\eating\mob_vr.dm" #include "code\modules\vore\eating\silicon_vr.dm" #include "code\modules\vore\eating\simple_animal_vr.dm" -#include "code\modules\vore\eating\slipvore_vr.dm" #include "code\modules\vore\eating\soulcatcher.dm" #include "code\modules\vore\eating\soulcatcher_import.dm" #include "code\modules\vore\eating\soulcatcher_mob.dm"