Files
Bubberstation/code/datums/components/shielded.dm
necromanceranne 8dadc1e9ed Fixes unfathomable curios not blocking when equipped. Fixes the shielded component still blocking despite not having a 'wearer'. (#82073)
<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may
not be viewable. -->
<!-- You can view Contributing.MD for a detailed description of the pull
request process. -->

## About The Pull Request

The unfathomable curio now only blocks when equipped to the belt slot,
and not when equipped in the hand.

The shielded component now no longer blocks despite not having a wearer,
and does not block if the wearer is not the same as the owner in the
``hit_reaction()`` proc.

Fixes https://github.com/tgstation/tgstation/issues/82068

## Why It's Good For The Game

The curio was only blocking while in-hand, rather than how it should be;
in the belt slot. Now it does what it is supposed to.

On top of that, this fixes what I think has been a bug for quite some
time with shield_inhand just not being respected whatsoever with regards
to whether or not the component worked. It only really determined
whether or not you got the sprites added/removed, but didn't factor into
the blocking ability whatsoever.

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Be sure to properly
mark your PRs to prevent unnecessary GBP loss. You can read up on GBP
and it's effects on PRs in the tgstation guides for contributors. Please
note that maintainers freely reserve the right to remove and add tags
should they deem it appropriate. You can attempt to finagle the system
all you want, but it's best to shoot for clear communication right off
the bat. -->

🆑
fix: Unfathomable Curios now properly block as expected.
fix: The shielded component actually respects the shield_inhand when
determining blocking potential.
fix: Ensures that the shielded component has a wearer before attempting
to block, and that this wearer is the same as the owner of the item.
/🆑

<!-- Both 🆑's are required for the changelog to work! You can put
your name to the right of the first 🆑 if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->

---------

Co-authored-by: san7890 <the@san7890.com>
2024-03-28 21:53:03 +00:00

205 lines
8.2 KiB
Plaintext

/**
* The shielded component causes the parent item to nullify a certain number of attacks against the wearer, see: shielded vests.
*/
/datum/component/shielded
dupe_mode = COMPONENT_DUPE_UNIQUE
/// The person currently wearing us
var/mob/living/wearer
/// How many charges we can have max, and how many we start with
var/max_charges
/// How many charges we currently have
var/current_charges
/// How long we have to avoid being hit to replenish charges. If set to 0, we never recharge lost charges
var/recharge_start_delay = 20 SECONDS
/// Once we go unhit long enough to recharge, we replenish charges this often. The floor is effectively 1 second, AKA how often SSdcs processes
var/charge_increment_delay = 1 SECONDS
/// How many charges we recover on each charge increment
var/charge_recovery = 1
/// What .dmi we're pulling the shield icon from
var/shield_icon_file = 'icons/effects/effects.dmi'
/// What icon is used when someone has a functional shield up
var/shield_icon = "shield-old"
/// Do we still shield if we're being held in-hand? If FALSE, it needs to be equipped to a slot to work
var/shield_inhand = FALSE
/// Should the shield lose charges equal to the damage dealt by a hit?
var/lose_multiple_charges = FALSE
/// Should the shield's alpha change to show its remaining charge
var/show_charge_as_alpha = FALSE
/// The item we use for recharging
var/recharge_path
/// The cooldown tracking when we were last hit
COOLDOWN_DECLARE(recently_hit_cd)
/// The cooldown tracking when we last replenished a charge
COOLDOWN_DECLARE(charge_add_cd)
/// A callback for the sparks/message that play when a charge is used, see [/datum/component/shielded/proc/default_run_hit_callback]
var/datum/callback/on_hit_effects
/datum/component/shielded/Initialize(max_charges = 3, recharge_start_delay = 20 SECONDS, charge_increment_delay = 1 SECONDS, charge_recovery = 1, lose_multiple_charges = FALSE, show_charge_as_alpha = FALSE, recharge_path = null, starting_charges = null, shield_icon_file = 'icons/effects/effects.dmi', shield_icon = "shield-old", shield_inhand = FALSE, run_hit_callback)
if(!isitem(parent) || max_charges <= 0)
return COMPONENT_INCOMPATIBLE
src.max_charges = max_charges
src.recharge_start_delay = recharge_start_delay
src.charge_increment_delay = charge_increment_delay
src.charge_recovery = charge_recovery
src.lose_multiple_charges = lose_multiple_charges
src.show_charge_as_alpha = show_charge_as_alpha
src.recharge_path = recharge_path
src.shield_icon_file = shield_icon_file
src.shield_icon = shield_icon
src.shield_inhand = shield_inhand
src.on_hit_effects = run_hit_callback || CALLBACK(src, PROC_REF(default_run_hit_callback))
if(isnull(starting_charges))
current_charges = max_charges
else
current_charges = starting_charges
if(recharge_start_delay)
START_PROCESSING(SSdcs, src)
/datum/component/shielded/Destroy(force)
if(wearer)
shield_icon = "broken"
UnregisterSignal(wearer, COMSIG_ATOM_UPDATE_OVERLAYS)
wearer.update_appearance(UPDATE_ICON)
wearer = null
on_hit_effects = null
return ..()
/datum/component/shielded/RegisterWithParent()
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equipped))
RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(lost_wearer))
RegisterSignal(parent, COMSIG_ITEM_HIT_REACT, PROC_REF(on_hit_react))
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(check_recharge_rune))
var/atom/shield = parent
if(ismob(shield.loc))
var/mob/holder = shield.loc
if(holder.is_holding(parent) && !shield_inhand)
return
set_wearer(holder)
/datum/component/shielded/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED, COMSIG_ITEM_HIT_REACT, COMSIG_ATOM_ATTACKBY))
var/atom/shield = parent
if(shield.loc == wearer)
lost_wearer(src, wearer)
// Handle recharging, if we want to
/datum/component/shielded/process(seconds_per_tick)
if(current_charges >= max_charges)
STOP_PROCESSING(SSdcs, src)
return
if(!COOLDOWN_FINISHED(src, recently_hit_cd))
return
if(!COOLDOWN_FINISHED(src, charge_add_cd))
return
var/obj/item/item_parent = parent
COOLDOWN_START(src, charge_add_cd, charge_increment_delay)
adjust_charge(charge_recovery) // set the number of charges to current + recovery per increment, clamped from zero to max_charges
playsound(item_parent, 'sound/magic/charge.ogg', 50, TRUE)
if(current_charges == max_charges)
playsound(item_parent, 'sound/machines/ding.ogg', 50, TRUE)
/datum/component/shielded/proc/adjust_charge(change)
current_charges = clamp(current_charges + change, 0, max_charges)
if(wearer)
wearer.update_appearance(UPDATE_ICON)
/// Check if we've been equipped to a valid slot to shield
/datum/component/shielded/proc/on_equipped(datum/source, mob/user, slot)
SIGNAL_HANDLER
if(user.is_holding(parent) && !shield_inhand)
lost_wearer(source, user)
return
set_wearer(user)
/// Either we've been dropped or our wearer has been QDEL'd. Either way, they're no longer our problem
/datum/component/shielded/proc/lost_wearer(datum/source, mob/user)
SIGNAL_HANDLER
if(wearer)
UnregisterSignal(wearer, list(COMSIG_ATOM_UPDATE_OVERLAYS, COMSIG_QDELETING))
wearer.update_appearance(UPDATE_ICON)
wearer = null
/datum/component/shielded/proc/set_wearer(mob/user)
if(wearer == user)
return
if(!isnull(wearer))
CRASH("[type] called set_wearer with [user] but [wearer] was already the wearer!")
wearer = user
RegisterSignal(wearer, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_update_overlays))
RegisterSignal(wearer, COMSIG_QDELETING, PROC_REF(lost_wearer))
if(current_charges)
wearer.update_appearance(UPDATE_ICON)
/// Used to draw the shield overlay on the wearer
/datum/component/shielded/proc/on_update_overlays(atom/parent_atom, list/overlays)
SIGNAL_HANDLER
var/mutable_appearance/shield_appearance = mutable_appearance(shield_icon_file, (current_charges > 0 ? shield_icon : "broken"), MOB_SHIELD_LAYER)
if(show_charge_as_alpha)
shield_appearance.alpha = (current_charges/max_charges)*255
overlays += shield_appearance
/**
* This proc fires when we're hit, and is responsible for checking if we're charged, then deducting one + returning that we're blocking if so.
* It then runs the callback in [/datum/component/shielded/var/on_hit_effects] which handles the messages/sparks (so the visuals)
*/
/datum/component/shielded/proc/on_hit_react(datum/source, mob/living/carbon/human/owner, atom/movable/hitby, attack_text, final_block_chance, damage, attack_type, damage_type)
SIGNAL_HANDLER
COOLDOWN_START(src, recently_hit_cd, recharge_start_delay)
//No wearer? No block.
if(isnull(wearer))
return
//if our wearer isn't the owner of the block, don't block
if(owner != wearer)
return
if(current_charges <= 0)
return
. = COMPONENT_HIT_REACTION_BLOCK
var/charge_loss = 1 // how many charges do we lose
if(lose_multiple_charges) // if the shield has health like damage we'll lose charges equal to the damage of the hit
charge_loss = damage
adjust_charge(-charge_loss)
INVOKE_ASYNC(src, PROC_REF(actually_run_hit_callback), owner, attack_text, current_charges)
if(!recharge_start_delay) // if recharge_start_delay is 0, we don't recharge
return
START_PROCESSING(SSdcs, src) // if we DO recharge, start processing so we can do that
/// The wrapper to invoke the on_hit callback, so we don't have to worry about blocking in the signal handler
/datum/component/shielded/proc/actually_run_hit_callback(mob/living/owner, attack_text, current_charges)
on_hit_effects.Invoke(owner, attack_text, current_charges)
/// Default on_hit proc, since cult robes are stupid and have different descriptions/sparks
/datum/component/shielded/proc/default_run_hit_callback(mob/living/owner, attack_text, current_charges)
do_sparks(2, TRUE, owner)
owner.visible_message(span_danger("[owner]'s shields deflect [attack_text] in a shower of sparks!"))
if(current_charges <= 0)
owner.visible_message(span_warning("[owner]'s shield overloads!"))
/datum/component/shielded/proc/check_recharge_rune(datum/source, obj/item/recharge_rune, mob/living/user)
SIGNAL_HANDLER
if(!istype(recharge_rune, recharge_path))
return
. = COMPONENT_NO_AFTERATTACK
adjust_charge(charge_recovery)
to_chat(user, span_notice("You charge \the [parent]. It can now absorb [current_charges] hits."))
qdel(recharge_rune)