Mirrors can trap dormant revenants (#95942)

## About The Pull Request
If glimmering residue (revenant ectoplasm) is scattered in the vicinity
of a mirror, the revenant will be trapped inside it instead.
-When mirrors are cursed in this way, the reflections are shifty and
distorted, like the revenant's reflection when it's alive.
-In this state, the revenant may only communicate via telepathy with
people reflected in the mirror (it's more like "standing close to it",
but that's probably ok).
-If the mirror is broken, the revenant will reform. If it's catatonic, a
new candidate will be pulled, much like with ectoplasm reforming.
-If the mirror is destroyed in another way, the revenant will be
destroyed as well.
-There is a 1/500 chance that a mirror will be haunted roundstart
## Why It's Good For The Game
You can now torment the revenant by eternally trapping it if it really
pissed you off

## Changelog
🆑
add: you can now trap dormant revenants in mirrors
add: haunted mirrors may very rarely appear roundstart
/🆑

---------

Co-authored-by: l0 <-->
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
This commit is contained in:
levels0
2026-05-24 12:10:35 +02:00
committed by GitHub
co-authored by l0 <--> SmArtKar
parent 259a28b98b
commit 7cf1670987
9 changed files with 162 additions and 61 deletions
@@ -62,6 +62,8 @@
var/draining = FALSE
/// Have we already given this revenant abilities?
var/generated_objectives_and_spells = FALSE
/// ckey of the player who controlled this mob when it was killed
var/old_ckey = ""
/// Lazylist of drained mobs to ensure that we don't steal a soul from someone twice
var/list/drained_mobs = null
@@ -343,10 +345,7 @@
visible_message(span_danger("[src]'s body breaks apart into a fine pile of blue dust."))
var/obj/item/ectoplasm/revenant/goop = new(get_turf(src)) // the ectoplasm will handle moving us out of dormancy
goop.old_ckey = client.ckey
goop.revenant = src
forceMove(goop)
new /obj/item/ectoplasm/revenant(get_turf(src), src) // the ectoplasm will handle moving us out of dormancy
/mob/living/basic/revenant/proc/on_move(datum/source, atom/entering_loc)
SIGNAL_HANDLER
@@ -519,4 +518,30 @@
if(!HAS_TRAIT(src, TRAIT_REVENANT_REVEALED) && !istype(reflecting_in, /obj/structure/mirror/magic))
apply_wibbly_filters(reflection)
/mob/living/basic/revenant/proc/get_new_user()
message_admins("A poll for the reforming revenant was created.")
var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to be [span_notice(name)] (reforming)?", check_jobban = ROLE_REVENANT, role = ROLE_REVENANT, poll_time = 5 SECONDS, checked_target = src, alert_pic = src, role_name_text = "reforming revenant", chat_text_border_icon = src)
if(!chosen_one)
message_admins("No candidates were found for the new revenant.")
visible_message(span_revenwarning("A blue dust appears from thin air and settles down."))
new /obj/item/ectoplasm/revenant(get_turf(src)) // inert
qdel(src)
return
PossessByPlayer(chosen_one.key)
message_admins("[chosen_one.key] has been made into the reformed revenant via poll.")
qdel(chosen_one)
/mob/living/basic/revenant/proc/reform(cause)
if(QDELETED(src))
return FALSE
death_reset()
if(isnull(client))
INVOKE_ASYNC(src, PROC_REF(get_new_user))
return TRUE
message_admins("[client.ckey] has been remade into a revenant.")
log_message("was remade as a revenant.", LOG_GAME)
return TRUE
#undef REVENANT_STUNNED_TRAIT
@@ -1,7 +1,7 @@
#define REVENANT_DEFILE_MIN_DAMAGE 30
#define REVENANT_DEFILE_MAX_DAMAGE 50
//Transmit: the revemant's only direct way to communicate. Sends a single message silently to a single mob
//Transmit: the revenant's only direct way to communicate. Sends a single message silently to a single mob
/datum/action/cooldown/spell/list_target/telepathy/revenant
name = "Revenant Transmit"
background_icon_state = "bg_revenant"
@@ -12,6 +12,14 @@
antimagic_flags = MAGIC_RESISTANCE_HOLY|MAGIC_RESISTANCE_MIND
/datum/action/cooldown/spell/list_target/telepathy/revenant/get_list_targets(atom/center, target_radius = 7)
if(!istype(center, /mob/living/basic/revenant))
return ..()
var/mob/living/basic/revenant/revenant = center
if(!revenant.dormant)
return ..()
return ..(get_turf(revenant), 2)
/datum/action/cooldown/spell/aoe/revenant
background_icon_state = "bg_revenant"
overlay_icon_state = "bg_revenant_border"
@@ -5,31 +5,36 @@
icon = 'icons/effects/effects.dmi'
icon_state = "revenantEctoplasm"
w_class = WEIGHT_CLASS_SMALL
/// Are we currently reforming?
var/reforming = TRUE
/// Are we inert (aka distorted such that we can't reform)?
// Can the revenant reform?
var/inert = FALSE
/// The key of the revenant that we started the reform as
var/old_ckey
/// The revenant we're currently storing
var/mob/living/basic/revenant/revenant
/obj/item/ectoplasm/revenant/Initialize(mapload)
/obj/item/ectoplasm/revenant/Initialize(mapload, revenant)
. = ..()
addtimer(CALLBACK(src, PROC_REF(try_reform)), 1 MINUTES)
inert = !revenant
if(revenant)
AddComponent(/datum/component/revenant_prison, revenant = revenant)
addtimer(CALLBACK(src, PROC_REF(reform)), 1 MINUTES)
/obj/item/ectoplasm/revenant/Destroy()
if(!QDELETED(revenant))
qdel(revenant)
return ..()
/obj/item/ectoplasm/revenant/proc/check_for_mirrors(turf/location, radius)
PRIVATE_PROC(TRUE)
for(var/obj/structure/mirror/mirror in view(radius, location))
if(mirror.cursable && !mirror.GetComponent(/datum/component/revenant_prison))
return mirror
return null
/obj/item/ectoplasm/revenant/attack_self(mob/user)
if(!reforming || inert)
if(inert)
return ..()
user.visible_message(
span_notice("[user] scatters [src] in all directions."),
span_notice("You scatter [src] across the area. The particles slowly fade away."),
span_notice("You scatter [src] across the area."),
)
var/obj/structure/mirror/nearby_mirror = check_for_mirrors(drop_location(), 5)
if(nearby_mirror)
transfer_to_mirror(nearby_mirror)
user.dropItemToGround(src)
qdel(src)
@@ -37,14 +42,25 @@
. = ..()
if(inert)
return
visible_message(span_notice("[src] breaks into particles upon impact, which fade away to nothingness."))
var/obj/structure/mirror/nearby_mirror = check_for_mirrors(get_turf(hit_atom), 3)
if(!nearby_mirror)
visible_message(span_notice("[src] breaks into particles upon impact, which fade away to nothingness."))
else
transfer_to_mirror(nearby_mirror)
qdel(src)
/obj/item/ectoplasm/revenant/proc/transfer_to_mirror(obj/structure/mirror/nearby_mirror)
PRIVATE_PROC(TRUE)
nearby_mirror.TakeComponent(GetComponent(/datum/component/revenant_prison))
nearby_mirror.visible_message(span_revenwarning("A dismal moan echoes as particles of [src] fall onto [nearby_mirror]!"))
log_game("A revenant was trapped inside [nearby_mirror]")
message_admins("A revenant was trapped inside [nearby_mirror] [ADMIN_JMP(nearby_mirror)]")
/obj/item/ectoplasm/revenant/examine(mob/user)
. = ..()
if(inert)
. += span_revennotice("It seems inert.")
else if(reforming)
else
. += span_revenwarning("It is shifting and distorted. It would be wise to destroy this.")
/obj/item/ectoplasm/revenant/suicide_act(mob/living/user)
@@ -52,45 +68,13 @@
qdel(src)
return OXYLOSS
/obj/item/ectoplasm/revenant/proc/try_reform()
if(reforming)
reforming = FALSE
reform()
else
inert = TRUE
visible_message(span_warning("[src] settles down and seems lifeless."))
/// Actually moves the revenant out of ourself
/obj/item/ectoplasm/revenant/proc/reform()
if(QDELETED(src) || QDELETED(revenant) || inert)
if(QDELETED(src) || inert)
return
if(!GetComponent(/datum/component/revenant_prison))
return
message_admins("Revenant ectoplasm was left undestroyed for 1 minute and is reforming into a new revenant.")
forceMove(drop_location()) //In case it's in a backpack or someone's hand
var/user_name = old_ckey
if(isnull(revenant.client))
var/mob/potential_user = get_new_user()
revenant.PossessByPlayer(potential_user.key)
user_name = potential_user.ckey
qdel(potential_user)
message_admins("[user_name] has been [old_ckey == user_name ? "re":""]made into a revenant by reforming ectoplasm.")
revenant.log_message("was [old_ckey == user_name ? "re":""]made as a revenant by reforming ectoplasm.", LOG_GAME)
SEND_SIGNAL(src, COMSIG_REVENANT_RELEASE, cause = "ectoplasm reforming")
visible_message(span_revenboldnotice("[src] suddenly rises into the air before fading away."))
revenant.death_reset()
revenant = null
qdel(src)
/// Handles giving the revenant a new client to control it
/obj/item/ectoplasm/revenant/proc/get_new_user()
message_admins("The new revenant's old client either could not be found or is in a new, living mob - grabbing a random candidate instead...")
var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to be [span_notice(revenant.name)] (reforming)?", check_jobban = ROLE_REVENANT, role = ROLE_REVENANT, poll_time = 5 SECONDS, checked_target = revenant, alert_pic = revenant, role_name_text = "reforming revenant", chat_text_border_icon = revenant)
if(isnull(chosen_one))
message_admins("No candidates were found for the new revenant.")
inert = TRUE
visible_message(span_revenwarning("[src] settles down and seems lifeless."))
qdel(revenant)
return null
return chosen_one