From 0a61cce996d3087e540838564e0534a3b330b4c3 Mon Sep 17 00:00:00 2001
From: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Date: Sun, 15 Mar 2026 15:24:20 -0500
Subject: [PATCH] Refactor revenant abilities / they now indicate if they are
unlocked (#95380)
## About The Pull Request
Refactors Revenant abilities into a component
Revenant abilities now indicate if they are locked
They also more accurately update if they are un/usable (ie, if you enter
a wall, they turn red.)
Also, Revenants are no longer affected by gravity. They already weren't
affected by gravity (as far as I could tell) this just prevents them
from getting the no-grav alert.
## Why It's Good For The Game
- Not all future Revenant abilities need to be an AOE spell
- It makes them easier to parse at a glance (what powers you have /
don't have / can use)
## Changelog
:cl: Melbert
qol: Revenant abilities indicate if they are locked, and better indicate
if they are currently usable
qol: Revenants are no longer alerted that they have no gravity (they
always have gravity)
refactor: Refactored Revenant abilities, report any oddities with them.
/:cl:
---
code/__DEFINES/dcs/signals/signals_spell.dm | 4 +
.../antagonists/revenant/revenant_skill.dm | 124 ++++++++++++++++++
.../basic/space_fauna/revenant/_revenant.dm | 39 +++++-
.../revenant/revenant_abilities.dm | 72 +++-------
code/modules/spells/spell.dm | 3 +
icons/mob/actions/actions_revenant.dmi | Bin 10664 -> 10942 bytes
tgstation.dme | 1 +
7 files changed, 181 insertions(+), 62 deletions(-)
create mode 100644 code/modules/antagonists/revenant/revenant_skill.dm
diff --git a/code/__DEFINES/dcs/signals/signals_spell.dm b/code/__DEFINES/dcs/signals/signals_spell.dm
index ebbdcc4e2be..1154eba338f 100644
--- a/code/__DEFINES/dcs/signals/signals_spell.dm
+++ b/code/__DEFINES/dcs/signals/signals_spell.dm
@@ -14,6 +14,10 @@
/// Return from before cast signals to prevent the spell from going on cooldown before aftercast.
#define SPELL_NO_IMMEDIATE_COOLDOWN (1 << 2)
+/// Sent from /datum/action/cooldown/spell/can_cast_check() to the spell: (feedback)
+#define COMSIG_SPELL_CAN_CAST_CHECK "can_cast_spell"
+ // Return SPELL_CANCEL_CAST to prevent the spell from being cast
+
/// Sent to an mob when a [/datum/action/cooldown/spell] calls try_invoke() to the caster: (datum/action/cooldown/spell/spell, feedback)
#define COMSIG_MOB_TRY_INVOKE_SPELL "try_invoke_spell"
/// The spell gets canceled
diff --git a/code/modules/antagonists/revenant/revenant_skill.dm b/code/modules/antagonists/revenant/revenant_skill.dm
new file mode 100644
index 00000000000..e3259ba9143
--- /dev/null
+++ b/code/modules/antagonists/revenant/revenant_skill.dm
@@ -0,0 +1,124 @@
+/// Attach to revenant spells to make them cost essence to cast
+/datum/component/revenant_ability
+ /// If it's locked, and needs to be unlocked before use
+ VAR_FINAL/locked = TRUE
+ /// How much essence it costs to unlock
+ var/unlock_amount = 100
+ /// How much essence it costs to use
+ var/cast_amount = 50
+
+ /// How long it reveals the revenant
+ var/reveal_duration = 8 SECONDS
+ // How long it stuns the revenant
+ var/stun_duration = 2 SECONDS
+
+ VAR_FINAL/image/locked_overlay
+
+/datum/component/revenant_ability/Initialize(
+ unlock_amount = 100,
+ cast_amount = 50,
+ reveal_duration = 8 SECONDS,
+ stun_duration = 2 SECONDS,
+)
+
+ if(!istype(parent, /datum/action/cooldown/spell))
+ return COMPONENT_INCOMPATIBLE
+
+ set_unlock_amount(unlock_amount)
+ set_cast_amount(cast_amount)
+ set_durations(reveal_duration, stun_duration)
+
+ RegisterSignal(parent, COMSIG_SPELL_CAN_CAST_CHECK, PROC_REF(can_cast))
+ RegisterSignal(parent, COMSIG_SPELL_BEFORE_CAST, PROC_REF(before_cast))
+ RegisterSignal(parent, COMSIG_SPELL_AFTER_CAST, PROC_REF(after_cast))
+ RegisterSignal(parent, COMSIG_ACTION_OVERLAY_APPLY, PROC_REF(add_locked_overlay))
+
+ locked_overlay = image('icons/mob/actions/actions_revenant.dmi', "locked")
+
+/datum/component/revenant_ability/vv_edit_var(var_name, var_value)
+ . = ..()
+ switch(var_name)
+ if(NAMEOF(src, unlock_amount))
+ set_unlock_amount(var_value)
+ if(NAMEOF(src, cast_amount))
+ set_cast_amount(var_value)
+ if(NAMEOF(src, reveal_duration), NAMEOF(src, stun_duration))
+ set_durations(reveal_duration, stun_duration)
+ if(NAMEOF(src, locked))
+ update_spell_name()
+
+/datum/component/revenant_ability/proc/update_spell_name()
+ var/datum/action/cooldown/spell/spell = parent
+ if(locked)
+ spell.name = "[initial(spell.name)] ([unlock_amount]SE)"
+ else
+ spell.name = "[initial(spell.name)] ([cast_amount]E)"
+ spell.build_all_button_icons()
+
+/datum/component/revenant_ability/proc/set_unlock_amount(new_value)
+ unlock_amount = new_value
+ update_spell_name()
+
+/datum/component/revenant_ability/proc/set_cast_amount(new_value)
+ cast_amount = new_value
+ update_spell_name()
+
+/datum/component/revenant_ability/proc/set_durations(new_reveal_duration, new_stun_duration)
+ reveal_duration = new_reveal_duration
+ stun_duration = new_stun_duration
+
+/datum/component/revenant_ability/proc/can_cast(datum/action/cooldown/spell/source, feedback)
+ SIGNAL_HANDLER
+
+ var/mob/living/basic/revenant/ghost = source.owner
+ if(!istype(ghost))
+ return NONE // just allow it anyways
+
+ if(locked)
+ if(ghost.essence_excess >= unlock_amount)
+ return NONE
+ if(feedback)
+ to_chat(ghost, span_revenwarning("You don't have enough essence to unlock [initial(source.name)]!"))
+ return SPELL_CANCEL_CAST
+
+ if(!ghost.cast_check(cast_amount, deduct_essence = FALSE, silent = !feedback))
+ return SPELL_CANCEL_CAST
+
+ return NONE
+
+/datum/component/revenant_ability/proc/before_cast(datum/action/cooldown/spell/source, atom/cast_on)
+ SIGNAL_HANDLER
+
+ var/mob/living/basic/revenant/ghost = source.owner
+ if(!istype(ghost))
+ return NONE // just allow it anyways
+
+ if(locked)
+ if(ghost.unlock(unlock_amount))
+ to_chat(ghost, span_revennotice("You have unlocked [initial(source.name)]!"))
+ locked = FALSE
+ update_spell_name()
+ else
+ to_chat(ghost, span_revenwarning("You don't have enough essence to unlock [initial(source.name)]!"))
+ return SPELL_CANCEL_CAST
+
+ if(!ghost.cast_check(cast_amount, deduct_essence = TRUE, silent = FALSE))
+ return SPELL_CANCEL_CAST
+
+ return NONE
+
+/datum/component/revenant_ability/proc/after_cast(datum/action/cooldown/spell/source, atom/cast_on)
+ SIGNAL_HANDLER
+
+ var/mob/living/caster = source.owner
+ if(reveal_duration > 0 SECONDS)
+ caster.apply_status_effect(/datum/status_effect/revenant/revealed, reveal_duration)
+ if(stun_duration > 0 SECONDS)
+ caster.apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, stun_duration)
+
+/datum/component/revenant_ability/proc/add_locked_overlay(datum/action/cooldown/spell/source, atom/movable/screen/movable/action_button/current_button, ...)
+ SIGNAL_HANDLER
+
+ current_button.cut_overlay(locked_overlay)
+ if(locked)
+ current_button.add_overlay(locked_overlay)
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
index 4aae0ffa2d7..436f8e27c89 100644
--- a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
@@ -160,6 +160,7 @@
/mob/living/basic/revenant/proc/update_revenant_appearance()
SIGNAL_HANDLER
update_appearance(UPDATE_ICON)
+ update_mob_action_buttons()
/mob/living/basic/revenant/AltClickOn(atom/target)
if(CAN_I_SEE(target))
@@ -321,6 +322,7 @@
return
ADD_TRAIT(src, TRAIT_NO_TRANSFORM, REVENANT_STUNNED_TRAIT)
dormant = TRUE
+ update_mob_action_buttons()
visible_message(
span_warning("[src] lets out a waning screech as violet mist swirls around its dissolving body!"),
@@ -410,28 +412,39 @@
return TRUE
-/mob/living/basic/revenant/proc/cast_check(essence_cost)
+/mob/living/basic/revenant/proc/cast_check(essence_cost, deduct_essence = TRUE, silent = FALSE)
if(QDELETED(src))
return
var/turf/current = get_turf(src)
if(isclosedturf(current))
- to_chat(src, span_revenwarning("You cannot use abilities from inside of a wall."))
+ if(!silent)
+ to_chat(src, span_revenwarning("You cannot use abilities from inside of a wall."))
return FALSE
for(var/obj/thing in current)
if(!thing.density || thing.CanPass(src, get_dir(current, src)))
continue
- to_chat(src, span_revenwarning("You cannot use abilities inside of a dense object."))
+ if(!silent)
+ to_chat(src, span_revenwarning("You cannot use abilities inside of a dense object."))
return FALSE
+ if(dormant)
+ if(!silent)
+ to_chat(src, span_revenwarning("Your powers lie dormant right now!"))
+ return SPELL_CANCEL_CAST
+
if(HAS_TRAIT(src, TRAIT_REVENANT_INHIBITED))
- to_chat(src, span_revenwarning("Your powers have been suppressed by a nullifying energy!"))
+ if(!silent)
+ to_chat(src, span_revenwarning("Your powers have been suppressed by a nullifying energy!"))
return FALSE
- if(!change_essence_amount(essence_cost, TRUE))
- to_chat(src, span_revenwarning("You lack the essence to use that ability."))
+ essence_cost = abs(essence_cost) * -1
+ var/has_essence = deduct_essence ? change_essence_amount(essence_cost, silent = TRUE) : (essence + essence_cost >= 0)
+ if(!has_essence)
+ if(!silent)
+ to_chat(src, span_revenwarning("You lack the essence to use that ability!"))
return FALSE
return TRUE
@@ -455,6 +468,7 @@
incorporeal_move = INCORPOREAL_MOVE_JAUNT
RemoveInvisibility(type)
alpha = 255
+ update_mob_action_buttons()
/mob/living/basic/revenant/proc/change_essence_amount(essence_to_change_by, silent = FALSE, source = null)
if(QDELETED(src))
@@ -478,6 +492,19 @@
to_chat(src, span_revenminor("Lost [essence_to_change_by]E [source ? "from [source]":""]."))
return TRUE
+/mob/living/basic/revenant/mob_negates_gravity()
+ return TRUE // i don't gotta explain shit
+
+/mob/living/basic/revenant/vv_edit_var(vname, vval)
+ . = ..()
+ if(vname == NAMEOF(src, essence) || vname == NAMEOF(src, max_essence) || vname == NAMEOF(src, essence_excess))
+ update_health_hud()
+ update_mob_action_buttons()
+
+/mob/living/basic/revenant/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ update_mob_action_buttons()
+
/mob/living/basic/revenant/proc/on_reflect(datum/source, atom/movable/reflecting_in, obj/effect/abstract/reflection)
SIGNAL_HANDLER
// powers are inhibited and we're not revealed so we can't project a reflect
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
index d456f104218..2030315410c 100644
--- a/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
@@ -20,13 +20,10 @@
antimagic_flags = MAGIC_RESISTANCE_HOLY
spell_requirements = NONE
- /// If it's locked, and needs to be unlocked before use
- var/locked = TRUE
/// How much essence it costs to unlock
var/unlock_amount = 100
/// How much essence it costs to use
var/cast_amount = 50
-
/// How long it reveals the revenant
var/reveal_duration = 8 SECONDS
// How long it stuns the revenant
@@ -34,64 +31,27 @@
/datum/action/cooldown/spell/aoe/revenant/New(Target)
. = ..()
- if(!isrevenant(target))
- stack_trace("[type] was given to a non-revenant mob, please don't.")
- qdel(src)
- return
-
- if(locked)
- name = "[initial(name)] ([unlock_amount]SE)"
- else
- name = "[initial(name)] ([cast_amount]E)"
-
-/datum/action/cooldown/spell/aoe/revenant/can_cast_spell(feedback = TRUE)
- . = ..()
- if(!.)
- return FALSE
- if(!isrevenant(owner))
- stack_trace("[type] was owned by a non-revenant mob, please don't.")
- return FALSE
-
- var/mob/living/basic/revenant/ghost = owner
- if(ghost.dormant || HAS_TRAIT(ghost, TRAIT_REVENANT_INHIBITED))
- return FALSE
- if(locked && ghost.essence_excess <= unlock_amount)
- return FALSE
- if(ghost.essence <= cast_amount)
- return FALSE
-
- return TRUE
+ AddComponent(/datum/component/revenant_ability, \
+ unlock_amount = unlock_amount, \
+ cast_amount = cast_amount, \
+ reveal_duration = reveal_duration, \
+ stun_duration = stun_duration, \
+ )
/datum/action/cooldown/spell/aoe/revenant/get_things_to_cast_on(atom/center)
return RANGE_TURFS(aoe_radius, center)
-/datum/action/cooldown/spell/aoe/revenant/before_cast(mob/living/basic/revenant/cast_on)
+/datum/action/cooldown/spell/aoe/revenant/vv_edit_var(var_name, var_value)
. = ..()
- if(. & SPELL_CANCEL_CAST)
- return
-
- if(locked)
- if(!cast_on.unlock(unlock_amount))
- to_chat(cast_on, span_revenwarning("You don't have enough essence to unlock [initial(name)]!"))
- reset_spell_cooldown()
- return . | SPELL_CANCEL_CAST
-
- name = "[initial(name)] ([cast_amount]E)"
- to_chat(cast_on, span_revennotice("You have unlocked [initial(name)]!"))
- locked = FALSE
- reset_spell_cooldown()
- return . | SPELL_CANCEL_CAST
-
- if(!cast_on.cast_check(-cast_amount))
- reset_spell_cooldown()
- return . | SPELL_CANCEL_CAST
-
-/datum/action/cooldown/spell/aoe/revenant/after_cast(mob/living/basic/revenant/cast_on)
- . = ..()
- if(reveal_duration > 0 SECONDS)
- cast_on.apply_status_effect(/datum/status_effect/revenant/revealed, reveal_duration)
- if(stun_duration > 0 SECONDS)
- cast_on.apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, stun_duration)
+ // gross getcomp, but this is solely to make life easier for badmins/debug. sue me
+ var/datum/component/revenant_ability/rev_comp = GetComponent(/datum/component/revenant_ability)
+ switch(var_name)
+ if(NAMEOF(src, unlock_amount))
+ rev_comp.set_unlock_amount(var_value)
+ if(NAMEOF(src, cast_amount))
+ rev_comp.set_cast_amount(var_value)
+ if(NAMEOF(src, reveal_duration), NAMEOF(src, stun_duration))
+ rev_comp.set_durations(reveal_duration, stun_duration)
//Overload Light: Breaks a light that's online and sends out lightning bolts to all nearby people.
/datum/action/cooldown/spell/aoe/revenant/overload
diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm
index 5bdf8014769..ccbf0097cda 100644
--- a/code/modules/spells/spell.dm
+++ b/code/modules/spells/spell.dm
@@ -153,6 +153,9 @@
if(!owner)
CRASH("[type] - can_cast_spell called on a spell without an owner!")
+ if(SEND_SIGNAL(src, COMSIG_SPELL_CAN_CAST_CHECK, feedback) & SPELL_CANCEL_CAST)
+ return FALSE
+
// Certain spells are not allowed on the centcom zlevel
var/turf/caster_turf = get_turf(owner)
// Spells which require being on the station
diff --git a/icons/mob/actions/actions_revenant.dmi b/icons/mob/actions/actions_revenant.dmi
index d657e8a3daebb3160ff2b85791e5e55b8db5b305..3b75c4c1d30c6f9d998b8accd53201e455378e7f 100644
GIT binary patch
literal 10942
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