Skip to content
Draft
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
963e8d4
Tidy
Jan 28, 2026
73dbb4f
tedious shite
Feb 1, 2026
ced01d3
Merge remote-tracking branch 'upstream/main' into forest/scrying
Feb 5, 2026
8f93cb6
functional again
Feb 5, 2026
c89a254
Merge remote-tracking branch 'upstream/main' into forest/scrying
Feb 10, 2026
92f970e
Merge remote-tracking branch 'upstream/main' into forest/scrying
Feb 24, 2026
b71d149
Merge remote-tracking branch 'upstream/main' into forest/scrying
Feb 24, 2026
e268e5f
Merge remote-tracking branch 'upstream/main' into forest/scrying
Feb 25, 2026
5e23c89
Why do so many different things have their own version of something
Feb 25, 2026
093f310
Merge remote-tracking branch 'upstream/main' into forest/scrying
Mar 1, 2026
b8937bb
disable eye of night
Mar 5, 2026
31b5833
Merge remote-tracking branch 'upstream/main' into forest/scrying
Mar 5, 2026
6ac1ffb
black mirror
Mar 5, 2026
8b3f50f
Remove debug logs
Mar 5, 2026
b5951e9
temp logs
Mar 6, 2026
154affb
Merge branch 'main' into forest/scrying
Mar 6, 2026
f07b793
Merge remote-tracking branch 'upstream/main' into forest/scrying
Mar 6, 2026
8e30746
conflicts
Mar 6, 2026
9bed85c
more conflicts
Mar 6, 2026
22de503
even more
Mar 6, 2026
0af6497
Merge remote-tracking branch 'upstream/main' into forest/scrying
Mar 7, 2026
d9103c3
x
Mar 7, 2026
215126e
Merge remote-tracking branch 'upstream/main' into forest/scrying
Mar 15, 2026
037ad9b
start fixing
Mar 15, 2026
114e6f2
Merge branch 'main' into forest/scrying
Jul 15, 2026
1b3cabb
Merge remote-tracking branch 'upstream/main' into forest/scrying
Jul 19, 2026
49ef3f5
Merge remote-tracking branch 'upstream/main' into forest/scrying
Jul 23, 2026
26cc5e4
move code
Jul 23, 2026
e58740b
ghost fix
Jul 23, 2026
e2a9b96
activation
Jul 23, 2026
57f024c
vamp bit
Jul 23, 2026
20fa05f
scrying orb lock
Jul 23, 2026
2735b35
op
Jul 23, 2026
1559659
vamp cooldown
Jul 23, 2026
7a4b716
clean up and black mirror lookback
Jul 23, 2026
41246fe
interact signals
Jul 23, 2026
84e46ab
getting there
Jul 23, 2026
86181d2
del fix
Jul 23, 2026
1120e73
Apply suggestions from code review
realforest2001 Jul 24, 2026
f921e69
adjustments
Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions code/__DEFINES/is_helpers.dm
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list(

#define isrogueobserver(A) (istype(A, /mob/dead/observer/rogue))

#define is_scryeye(A) (istype(A, /mob/scry_eye))
Comment thread
realforest2001 marked this conversation as resolved.
Outdated

#define isdead(A) (istype(A, /mob/dead))

#define isnewplayer(A) (istype(A, /mob/dead/new_player))
Expand Down
3 changes: 0 additions & 3 deletions code/__DEFINES/traits/definitions.dm
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai

/// Given to a mob that can throw to make them not able to throw
#define TRAIT_NO_THROWING "no_throwing"

/// Hides the SSD indicator. Used with scrying.
#define TRAIT_NOSSDINDICATOR "nossdindicator"
/// Instant grabs on someone else.
#define TRAIT_NOSTRUGGLE "nostruggle"
/// Black-bagged. More snowflaking.
Expand Down
3 changes: 0 additions & 3 deletions code/_onclick/hud/ghost.dm
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,6 @@
if(!istype(ghost))
return

if(istype(ghost, /mob/dead/observer/rogue/arcaneeye))
return

if(ghost.isinhell)
return

Expand Down
3 changes: 0 additions & 3 deletions code/_onclick/observer.dm
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
/mob/dead/observer/profane/DblClickOn(atom/clicked_atom, params) // Souls trapped by the dagger should not be jumping around.
return

/mob/dead/observer/rogue/arcaneeye/DblClickOn(atom/clicked_atom, params)
return

/mob/dead/observer/ClickOn(atom/clicked_atom, params)
var/list/modifiers = params2list(params)

Expand Down
3 changes: 1 addition & 2 deletions code/datums/components/orbiter.dm
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@
orbiter.glide_size = movable_parent.glide_size

orbiter.abstract_move(get_turf(parent))
if(!istype(orbiter, /mob/dead/observer/screye))
to_chat(orbiter, span_notice("Now orbiting [parent]."))
to_chat(orbiter, span_notice("Now orbiting [parent]."))

/datum/component/orbiter/proc/end_orbit(atom/movable/orbiter, refreshing=FALSE)
if(!orbiter_list[orbiter])
Expand Down
276 changes: 276 additions & 0 deletions code/datums/components/scrying.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
/datum/component/scrying
var/name = "scrying component"

var/text_cooldown_fail = "I look into NAME_HERE but only see inky smoke. Maybe I should wait."

var/vision_duration = 8 SECONDS
var/cooldown_duration = 30 SECONDS

/// Whether or not the user of the scrying device needs to personally know the identity of their target.
var/needs_to_know = TRUE
/// Whether or not the target needs to be alive
var/needs_to_live = TRUE
Comment on lines +6 to +12

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be your init args


var/mob/scry_eye/scrying_eye
var/mob/living/carbon/held_user

var/interact_method = "attack_self"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a numeric define

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe you don't even need it and just change it depending on structure / item

COOLDOWN_DECLARE(scry_cooldown)

/datum/component/scrying/Initialize(obj/item/scrying/parent)
. = ..()
text_cooldown_fail = replacetext(text_cooldown_fail, "NAME_HERE", "\the [name]")
if(!parent)
return
Comment thread
realforest2001 marked this conversation as resolved.
Outdated

/datum/component/scrying/RegisterWithParent()
switch(interact_method)
if("attack_self")
RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, PROC_REF(activate))
if("attack_hand")
RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(activate))

/datum/component/scrying/UnregisterFromParent()
switch(interact_method)
if("attack_self")
UnregisterSignal(parent, COMSIG_ITEM_ATTACK_SELF)
if("attack_hand")
UnregisterSignal(parent, COMSIG_ATOM_ATTACK_HAND)

/datum/component/scrying/Destroy(force)
QDEL_NULL(scrying_eye)
held_user = null
. = ..()
Comment thread
realforest2001 marked this conversation as resolved.
Outdated

/datum/component/scrying/proc/activate(datum/source, mob/user)
if(!pass_extra_checks(user))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a callback

return FALSE

if(!COOLDOWN_FINISHED(src, scry_cooldown))
to_chat(user, span_warning(text_cooldown_fail))
return FALSE

var/search_name = tgui_input_text(user, "Who are you looking for?", name)
if(!search_name)
return FALSE

if(!user.mind || (needs_to_know && !user.mind.do_i_know(name = search_name)))
to_chat(user, span_warning("I don't know anyone by that name."))
return FALSE

//check is applied twice to prevent someone from bypassing the cooldown
if(!COOLDOWN_FINISHED(src, scry_cooldown))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this above the above check

to_chat(user, span_warning(text_cooldown_fail))
return FALSE

var/mob/living/carbon/human/found_target
for(var/mob/living/carbon/human/human_target as anything in GLOB.human_list)
if(lowertext(human_target.real_name) == lowertext(search_name))
var/turf/target_turf = get_turf(human_target)
if(!target_turf)
continue
found_target = human_target
break

held_user = user
Comment thread
realforest2001 marked this conversation as resolved.
Outdated
if(HAS_TRAIT(found_target, TRAIT_ANTISCRYING))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this above the input

to_chat(user, span_warning("I peer into \the [name], but an impenetrable fog shrouds [search_name]."))
to_chat(found_target, span_warning("My magical shrouding reacted to something."))
held_user = null
return

create_eye()
if(!scrying_eye)
remove_eye(TRUE)
return

if(needs_to_live && found_target.stat)
Comment thread
realforest2001 marked this conversation as resolved.
Outdated
to_chat(user, span_warning("I peer into \the [name], but can't find [search_name]."))
remove_eye(TRUE)
return FALSE

log_game("SCRYING: [user.real_name] ([user.ckey]) has used the [name] to scry [found_target.real_name] ([found_target.ckey])")

var/real_cooldown = cooldown_duration + vision_duration
COOLDOWN_START(src, scry_cooldown, real_cooldown)
user.visible_message(span_danger("[user] stares into \the [name], [user.p_their()] eyes rolling back into [user.p_their()] head."), span_warning("My eyes roll into the back of my head as I'm lost in the depths of the orb."))
scrying_eye.orbit(found_target)
var/target_perception = GET_MOB_ATTRIBUTE_VALUE(found_target, STAT_PERCEPTION)
if(target_perception >= 15)
if(found_target.mind)
if(found_target.mind.do_i_know(name = user.real_name))
to_chat(found_target, span_warning("I can clearly see the face of [user.real_name] staring at me!"))
to_chat(user, span_warning("[found_target.real_name] stares back at me!"))
return TRUE
to_chat(found_target, span_warning("I can clearly see the face of an unknown [user.gender == FEMALE ? "woman" : "man"] staring at me!"))
return TRUE
if(target_perception >= 11)
to_chat(found_target, span_warning("I feel a pair of unknown eyes on me."))
return TRUE

/datum/component/scrying/proc/create_eye()
if(!held_user)
return FALSE
scrying_eye = new
scrying_eye.user_mob = held_user
held_user.reset_perspective(scrying_eye)
held_user.Immobilize(vision_duration)
held_user.overlay_fullscreen("scrying", /atom/movable/screen/backhudl/obscured)
addtimer(CALLBACK(src, PROC_REF(remove_eye)), vision_duration)

/datum/component/scrying/proc/remove_eye(early = FALSE)
if(!held_user)
return FALSE
held_user.reset_perspective(held_user)
held_user.clear_fullscreen("scrying")
if(early)
held_user.SetImmobilized(2 SECONDS)
QDEL_NULL(scrying_eye)
held_user = null


/datum/component/scrying/proc/pass_extra_checks(mob/living/user)
return TRUE

/datum/component/scrying/orb
name = "Scrying Orb"

/datum/component/scrying/orb/pass_extra_checks(mob/living/user)
if(GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) < 1)
to_chat(user, span_warning("I do not know what to do with this..."))
return FALSE
return TRUE

/datum/component/scrying/eye
name = "Accursed Eye"
cooldown_duration = 5 MINUTES

/datum/component/scrying/vampire
name = "Night's Eye"
needs_to_know = FALSE
needs_to_live = FALSE
vision_duration = 12 SECONDS
cooldown_duration = 3 SECONDS

/datum/component/scrying/vampire/pass_extra_checks(mob/living/user)
if(!user?.mind.has_antag_datum(/datum/antagonist/vampire/lord))
to_chat(user, span_warning("I don't have the power to use this!"))
return FALSE
return TRUE

/datum/component/scrying/telescope
name = "NOC Device"
text_cooldown_fail = "I peer into the sky but cannot focus the lens on the face of Noc. Maybe I should wait."
interact_method = "attack_hand"

/datum/component/scrying/telescope/pass_extra_checks(mob/living/user)
var/mob/living/carbon/human/human_user = user
if(!ishuman(human_user) || !HAS_TRAIT(human_user, TRAIT_VIRGIN))
to_chat(human_user, span_warning("Noc looks angry with me..."))
return FALSE
return TRUE

/datum/component/scrying/mirror

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one can remain a subtype for now

name = "Black Mirror"
vision_duration = 6 SECONDS
needs_to_know = FALSE
needs_to_live = FALSE
var/obj/item/inqarticles/bmirror/parent_mirror
var/mob/stored_target
var/atom/movable/screen/alert/blackmirror/effect

/datum/component/scrying/mirror/Initialize(obj/item/scrying/parent)
. = ..()
parent_mirror = parent
if(!istype(parent_mirror))
return INITIALIZE_HINT_QDEL

/datum/component/scrying/mirror/Destroy(force)
parent_mirror = null
stored_target = null
QDEL_NULL(effect)
. = ..()

/datum/component/scrying/mirror/activate(mob/living/user)
if(!pass_extra_checks(user))
message_admins("SCRY DEBUG: EXTRA CHECKS FAIL")
return FALSE

if(!COOLDOWN_FINISHED(src, scry_cooldown))
to_chat(user, span_warning(text_cooldown_fail))
return FALSE

var/search_name = stripped_input(user, "Who are you looking for?", name)
if(!search_name)
return FALSE

if(!user.mind)
to_chat(user, span_warning("I don't know of anyone by that name."))
return FALSE

//check is applied twice to prevent someone from bypassing the cooldown
if(!COOLDOWN_FINISHED(src, scry_cooldown))
to_chat(user, span_warning(text_cooldown_fail))
return FALSE

for(var/mob/living/carbon/human/human_target in GLOB.human_list)
if(human_target.real_name == search_name)
var/turf/target_turf = get_turf(human_target)
if(!target_turf)
continue
stored_target = human_target
break

held_user = user
if(HAS_TRAIT(stored_target, TRAIT_ANTISCRYING))
to_chat(user, span_warning("I peer into \the [name], but an impenetrable fog shrouds [search_name]."))
to_chat(stored_target, span_warning("My magical shrouding reacted to something."))
held_user = null
return

create_eye()
if(!scrying_eye)
remove_eye(TRUE)
return

log_game("SCRYING: [user.real_name] ([user.ckey]) has used the [name] to leer at [stored_target.real_name] ([stored_target.ckey])")

var/real_cooldown = cooldown_duration + vision_duration
COOLDOWN_START(src, scry_cooldown, real_cooldown)
user.visible_message(span_danger("[user] stares into \the [name], [user.p_their()] eyes rolling back into [user.p_their()] head."), span_warning("My eyes roll into the back of my head as I'm lost in the depths of the orb."))
apply_black_eye()
return TRUE

/datum/component/scrying/mirror/create_eye()
if(!held_user)
return FALSE
scrying_eye = new
scrying_eye.user_mob = held_user
held_user.reset_perspective(scrying_eye)
held_user.Immobilize(vision_duration)
held_user.overlay_fullscreen("scrying", /atom/movable/screen/backhudl/obscured)
playsound(held_user, 'sound/items/blackmirror_use.ogg', 100, FALSE)
addtimer(CALLBACK(src, PROC_REF(remove_eye)), vision_duration)

/datum/component/scrying/mirror/remove_eye(early = FALSE)
if(!held_user)
return FALSE
held_user.reset_perspective(held_user)
held_user.clear_fullscreen("scrying")
playsound(held_user, 'sound/items/blackeye.ogg', 100, FALSE)
if(early)
held_user.SetImmobilized(2 SECONDS)
QDEL_NULL(scrying_eye)
held_user = null
if(stored_target)
stored_target.clear_alert("blackmirror", TRUE)
stored_target.playsound_local(src, 'sound/items/blackeye.ogg', 40, FALSE)
stored_target = null
parent_mirror.donefixating()
effect = null

/datum/component/scrying/mirror/proc/apply_black_eye()
scrying_eye.orbit(stored_target)
effect = stored_target.throw_alert("blackmirror", /atom/movable/screen/alert/blackmirror, override = TRUE)
effect.source = parent_mirror
playsound(stored_target, 'sound/items/blackeye_warn.ogg', 100, FALSE)
Loading
Loading