Skip to content

Commit

Permalink
Converts cursed heart effect into a component. (tgstation#78554)
Browse files Browse the repository at this point in the history
## About The Pull Request

Fixes tgstation#58401 
Fixes tgstation#58799
Fixes tgstation#58800

Converts the manual heart-beating effect of the cursed heart into a
component.

Behavior has mostly been maintained, but polished a bit as compared to
the original cursed heart. Most notably, the action for beating your
heart is now a cooldown action - providing a visual indicator of when
you can beat it again, rather than leaving you guessing. Some better
checks have also been put in place for edge cases such as having your
species changed.

Implementation inspired by the existing "manual blinking" and "manual
breathing" components. Currently only used by the cursed heart and the
(now majorly simplified) effect of corazargh.

My first component, so hopefully I didn't miss anything.
## Why It's Good For The Game

The cursed heart was kind of unusably bad - which may have been part of
the intent, but having to count in your head or spam-click the button is
just annoying. With a visual indicator of when you should beat your
heart, hopefully it will be slightly less awful for the cursed.

The real motivation here was that corazargh's implementation was kind of
a travesty - summoning a cursed heart inside of your body while it was
in your system, then restoring your old heart afterward. This was
error-prone as well as just being ridiculous. Making this effect a
component gets rid of these problems, and leaves space open for new uses
of manual heart beating if anyone feels like being cruel.
## Changelog
:cl:
fix: Your heart will no longer be deleted if an admin heals you while
you have corazargh in your system.
refactor: The cursed heart has been streamlined a bit, and now gives you
a visual cooldown for when you can beat your heart again.
/:cl:
  • Loading branch information
lizardqueenlexi authored Oct 3, 2023
1 parent 5cc8832 commit 6bdf052
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 144 deletions.
5 changes: 4 additions & 1 deletion code/__DEFINES/dcs/signals/signals_action.dm
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@
/// From base of /datum/action/cooldown/mob_cooldown/lava_swoop/proc/swoop_attack(): ()
#define COMSIG_LAVA_ARENA_FAILED "mob_lava_arena_failed"

///From /datum/action/vehicle/sealed/mecha/mech_toggle_safeties/proc/update_action_icon(): ()
/// From /datum/action/vehicle/sealed/mecha/mech_toggle_safeties/proc/update_action_icon(): ()
#define COMSIG_MECH_SAFETIES_TOGGLE "mech_safeties_toggle"

/// From /datum/action/cooldown/mob_cooldown/assume_form/proc/assume_appearances(), sent to the action owner: (atom/movable/target)
#define COMSIG_ACTION_DISGUISED_APPEARANCE "mob_ability_disguise_appearance"

/// From /datum/action/cooldown/manual_heart/Activate(): ()
#define COMSIG_HEART_MANUAL_PULSE "heart_manual_pulse"
179 changes: 179 additions & 0 deletions code/datums/components/manual_heart.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/// If a beat is missed, how long to give before the next is missed
#define MANUAL_HEART_GRACE_PERIOD 2 SECONDS

/**
* Manual heart pumping component. Requires the holder to pump their heart manually every
* so often or die.
*
* Mainly used by the cursed heart.
*/
/datum/component/manual_heart
/// The action for pumping your heart
var/datum/action/cooldown/manual_heart/pump_action
/// Cooldown before harm is caused to the owner
COOLDOWN_DECLARE(heart_timer)
/// If true, add a screen tint on the next process
var/add_colour = TRUE
/// How long between needed pumps; you can pump one second early
var/pump_delay = 3 SECONDS
/// How much blood volume you lose every missed pump, this is a flat amount not a percentage!
var/blood_loss = BLOOD_VOLUME_NORMAL * 0.2 // 20% of normal volume, missing five pumps is instant death

//How much to heal per pump - negative numbers harm the owner instead
/// The amount of brute damage to heal per pump
var/heal_brute = 0
/// The amount of burn damage to heal per pump
var/heal_burn = 0
/// The amount of oxygen damage to heal per pump
var/heal_oxy = 0

/datum/component/manual_heart/Initialize(pump_delay = 3 SECONDS, blood_loss = BLOOD_VOLUME_NORMAL * 0.2, heal_brute = 0, heal_burn = 0, heal_oxy = 0)
//Non-Carbon mobs can't have hearts, and should never receive this component.
if (!iscarbon(parent))
stack_trace("Manual Heart component added to [parent] ([parent?.type]) which is not a /mob/living/carbon subtype.")
return COMPONENT_INCOMPATIBLE

src.pump_delay = pump_delay
src.blood_loss = blood_loss
src.heal_brute = heal_brute
src.heal_burn = heal_burn
src.heal_oxy = heal_oxy

pump_action = new(src)

/datum/component/manual_heart/Destroy()
QDEL_NULL(pump_action)
return ..()

/datum/component/manual_heart/RegisterWithParent()
RegisterSignal(parent, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(check_removed_organ))
RegisterSignal(parent, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(check_added_organ))
RegisterSignal(parent, COMSIG_HEART_MANUAL_PULSE, PROC_REF(on_pump))
RegisterSignals(parent, list(COMSIG_LIVING_DEATH, SIGNAL_ADDTRAIT(TRAIT_NOBLOOD)), PROC_REF(pause))
RegisterSignals(parent, list(COMSIG_LIVING_REVIVE, SIGNAL_REMOVETRAIT(TRAIT_NOBLOOD)), PROC_REF(restart))

pump_action.cooldown_time = pump_delay - (1 SECONDS) //you can pump up to a second early
pump_action.Grant(parent)

var/mob/living/carbon/carbon_parent = parent
var/obj/item/organ/internal/heart/parent_heart = carbon_parent.get_organ_slot(ORGAN_SLOT_HEART)
if(parent_heart && !HAS_TRAIT(carbon_parent, TRAIT_NOBLOOD) && carbon_parent.stat != DEAD)
START_PROCESSING(SSdcs, src)
COOLDOWN_START(src, heart_timer, pump_delay)

to_chat(parent, span_userdanger("Your heart no longer beats automatically! You have to pump it manually - otherwise you'll die!"))

/datum/component/manual_heart/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_CARBON_GAIN_ORGAN, COMSIG_CARBON_LOSE_ORGAN, COMSIG_HEART_MANUAL_PULSE, COMSIG_LIVING_REVIVE, COMSIG_LIVING_DEATH, SIGNAL_ADDTRAIT(TRAIT_NOBLOOD), SIGNAL_REMOVETRAIT(TRAIT_NOBLOOD)))

to_chat(parent, span_userdanger("You feel your heart start beating normally again!"))
var/mob/living/carbon/carbon_parent = parent
if(istype(carbon_parent))
carbon_parent.remove_client_colour(/datum/client_colour/manual_heart_blood)

/datum/component/manual_heart/proc/restart()
SIGNAL_HANDLER

if(!check_valid())
return
COOLDOWN_START(src, heart_timer, pump_delay)
pump_action.build_all_button_icons(UPDATE_BUTTON_STATUS) //make sure the action button always shows as available when it is
START_PROCESSING(SSdcs, src)

/datum/component/manual_heart/proc/pause()
SIGNAL_HANDLER
pump_action.build_all_button_icons(UPDATE_BUTTON_STATUS)
var/mob/living/carbon/carbon_parent = parent
if(istype(carbon_parent))
carbon_parent.remove_client_colour(/datum/client_colour/manual_heart_blood) //prevents red overlay from getting stuck
STOP_PROCESSING(SSdcs, src)

/// Worker proc that checks logic for if a pump can happen, and applies effects from doing so
/datum/component/manual_heart/proc/on_pump(mob/owner)
COOLDOWN_START(src, heart_timer, pump_delay)
playsound(owner,'sound/effects/singlebeat.ogg', 40, TRUE)

var/mob/living/carbon/carbon_owner = owner

if(HAS_TRAIT(carbon_owner, TRAIT_NOBLOOD))
return
carbon_owner.blood_volume = min(carbon_owner.blood_volume + (blood_loss * 0.5), BLOOD_VOLUME_MAXIMUM)
carbon_owner.remove_client_colour(/datum/client_colour/manual_heart_blood)
add_colour = TRUE
carbon_owner.adjustBruteLoss(-heal_brute)
carbon_owner.adjustFireLoss(-heal_burn)
carbon_owner.adjustOxyLoss(-heal_oxy)

/datum/component/manual_heart/process()
var/mob/living/carbon/carbon_parent = parent

//If they aren't connected, don't kill them.
if(!carbon_parent.client)
COOLDOWN_START(src, heart_timer, pump_delay)
return

if(!COOLDOWN_FINISHED(src, heart_timer))
return

carbon_parent.blood_volume = max(carbon_parent.blood_volume - blood_loss, 0)
to_chat(carbon_parent, span_userdanger("You have to keep pumping your blood!"))
COOLDOWN_START(src, heart_timer, MANUAL_HEART_GRACE_PERIOD) //give two full seconds before losing more blood
if(add_colour)
carbon_parent.add_client_colour(/datum/client_colour/manual_heart_blood)
add_colour = FALSE

///If a new heart is added, start processing.
/datum/component/manual_heart/proc/check_added_organ(mob/organ_owner, obj/item/organ/new_organ)
SIGNAL_HANDLER

var/obj/item/organ/internal/heart/new_heart = new_organ

if(!istype(new_heart) || !check_valid())
return
COOLDOWN_START(src, heart_timer, pump_delay)
pump_action.build_all_button_icons(UPDATE_BUTTON_STATUS)
var/mob/living/carbon/carbon_parent = parent
if(istype(carbon_parent))
carbon_parent.remove_client_colour(/datum/client_colour/manual_heart_blood) //prevents red overlay from getting stuck
START_PROCESSING(SSdcs, src)

///If the heart is removed, stop processing.
/datum/component/manual_heart/proc/check_removed_organ(mob/organ_owner, obj/item/organ/removed_organ)
SIGNAL_HANDLER

var/obj/item/organ/internal/heart/removed_heart = removed_organ

if(istype(removed_heart))
pump_action.build_all_button_icons(UPDATE_BUTTON_STATUS)
STOP_PROCESSING(SSdcs, src)

///Helper proc to check if processing can be restarted.
/datum/component/manual_heart/proc/check_valid()
var/mob/living/carbon/carbon_parent = parent
var/obj/item/organ/internal/heart/parent_heart = carbon_parent.get_organ_slot(ORGAN_SLOT_HEART)
return !isnull(parent_heart) && !HAS_TRAIT(carbon_parent, TRAIT_NOBLOOD) && carbon_parent.stat != DEAD

///Action to pump your heart. Cooldown will always be set to 1 second less than the pump delay.
/datum/action/cooldown/manual_heart
name = "Pump your blood"
cooldown_time = 2 SECONDS
check_flags = NONE
button_icon = 'icons/obj/medical/organs/organs.dmi'
button_icon_state = "cursedheart-off"

/datum/action/cooldown/manual_heart/Activate(atom/atom_target)
. = ..()

SEND_SIGNAL(owner, COMSIG_HEART_MANUAL_PULSE)

///The action button is only available when you're a living carbon with blood and a heart.
/datum/action/cooldown/manual_heart/IsAvailable(feedback = FALSE)
var/mob/living/carbon/heart_haver = owner
if(!istype(heart_haver) || HAS_TRAIT(heart_haver, TRAIT_NOBLOOD) || heart_haver.stat == DEAD)
return FALSE
var/obj/item/organ/internal/heart/heart_havers_heart = heart_haver.get_organ_slot(ORGAN_SLOT_HEART)
if(isnull(heart_havers_heart))
return FALSE
return ..()

#undef MANUAL_HEART_GRACE_PERIOD
2 changes: 1 addition & 1 deletion code/modules/antagonists/wizard/equipment/artefact.dm
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@

//Provides a decent heal, need to pump every 6 seconds
/obj/item/organ/internal/heart/cursed/wizard
pump_delay = 60
pump_delay = 6 SECONDS
heal_brute = 25
heal_burn = 25
heal_oxy = 25
Expand Down
4 changes: 4 additions & 0 deletions code/modules/client/client_colour.dm
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@
override = TRUE
colour = list(0.8,0,0,0, 0,0,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0)

/datum/client_colour/manual_heart_blood
priority = PRIORITY_ABSOLUTE
colour = COLOR_RED

#undef PRIORITY_ABSOLUTE
#undef PRIORITY_HIGH
#undef PRIORITY_NORMAL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ Basically, we fill the time between now and 2s from now with hands based off the
affected_mob.cure_trauma_type(temp_trauma, resilience = TRAUMA_RESILIENCE_MAGIC)

/datum/reagent/inverse/corazargh
name = "Corazargh" //It's what you yell! Though, if you've a better name feel free. Also an omage to an older chem
name = "Corazargh" //It's what you yell! Though, if you've a better name feel free. Also an homage to an older chem
description = "Interferes with the body's natural pacemaker, forcing the patient to manually beat their heart."
color = "#5F5F5F"
self_consuming = TRUE
Expand All @@ -695,83 +695,23 @@ Basically, we fill the time between now and 2s from now with hands based off the
metabolization_rate = REM
chemical_flags = REAGENT_DEAD_PROCESS
tox_damage = 0
///Weakref to the old heart we're swapping for
var/datum/weakref/original_heart_ref
///Weakref to the new heart that's temp added
var/datum/weakref/manual_heart_ref

///Creates a new cursed heart and puts the old inside of it, then replaces the position of the old
///Give the victim the manual heart beating component.
/datum/reagent/inverse/corazargh/on_mob_metabolize(mob/living/affected_mob)
. = ..()
if(!iscarbon(affected_mob))
return
var/mob/living/carbon/carbon_mob = affected_mob
var/obj/item/organ/internal/heart/original_heart = affected_mob.get_organ_slot(ORGAN_SLOT_HEART)
if(!original_heart)
return
original_heart_ref = WEAKREF(original_heart)

var/obj/item/organ/internal/heart/cursed/manual_heart = new(null, src)
manual_heart_ref = WEAKREF(manual_heart)
original_heart.Remove(carbon_mob, special = TRUE) //So we don't suddenly die
original_heart.forceMove(manual_heart)
original_heart.organ_flags |= ORGAN_FROZEN //Not actually frozen, but we want to pause decay
manual_heart.Insert(carbon_mob, special = TRUE)
//these last so instert doesn't call them
RegisterSignal(carbon_mob, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(on_gained_organ))
RegisterSignal(carbon_mob, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(on_removed_organ))
to_chat(affected_mob, span_userdanger("You feel your heart suddenly stop beating on it's own - you'll have to manually beat it!"))

///Intercepts the new heart and creates a new cursed heart - putting the old inside of it
/datum/reagent/inverse/corazargh/proc/on_gained_organ(mob/affected_mob, obj/item/organ/organ)
SIGNAL_HANDLER
if(!istype(organ, /obj/item/organ/internal/heart))
return
// DO NOT REACT TO YOUR OWN HEART ADDITION I SWEAR TO CHRIST
var/obj/item/organ/internal/heart/cursed/manual_heart = manual_heart_ref?.resolve()
if(organ == manual_heart)
return

var/mob/living/carbon/affected_carbon = affected_mob
var/obj/item/organ/internal/heart/original_heart = organ
original_heart_ref = WEAKREF(original_heart)
original_heart.Remove(affected_carbon, special = TRUE)
if(!manual_heart)
manual_heart = new(null, src)
manual_heart_ref = WEAKREF(manual_heart)
original_heart.forceMove(manual_heart)
original_heart.organ_flags |= ORGAN_FROZEN //Not actually frozen, but we want to pause decay
manual_heart.Insert(affected_carbon, special = TRUE)

///If we're ejecting out the organ - replace it with the original
/datum/reagent/inverse/corazargh/proc/on_removed_organ(mob/prev_owner, obj/item/organ/organ)
SIGNAL_HANDLER
var/obj/item/organ/internal/heart/cursed/manual_heart = manual_heart_ref?.resolve()
if(organ != manual_heart)
return
var/obj/item/organ/internal/heart/original_heart = original_heart_ref?.resolve()
if(!original_heart)
var/obj/item/organ/internal/heart/affected_heart = carbon_mob.get_organ_slot(ORGAN_SLOT_HEART)
if(isnull(affected_heart))
return
carbon_mob.AddComponent(/datum/component/manual_heart)
return ..()

original_heart.forceMove(manual_heart.loc)
original_heart.organ_flags &= ~ORGAN_FROZEN //enable decay again
QDEL_NULL(manual_heart_ref)

///We're done - remove the curse and restore the old one
///We're done - remove the curse
/datum/reagent/inverse/corazargh/on_mob_end_metabolize(mob/living/affected_mob)
. = ..()
//Do these first so Insert doesn't call them
UnregisterSignal(affected_mob, COMSIG_CARBON_LOSE_ORGAN)
UnregisterSignal(affected_mob, COMSIG_CARBON_GAIN_ORGAN)
if(!iscarbon(affected_mob))
return
var/mob/living/carbon/affected_carbon = affected_mob
var/obj/item/organ/internal/heart/original_heart = original_heart_ref?.resolve()
if(original_heart) //Mostly a just in case
original_heart.organ_flags &= ~ORGAN_FROZEN //enable decay again
original_heart.Insert(affected_carbon, special = TRUE)
QDEL_NULL(manual_heart_ref)
to_chat(affected_mob, span_userdanger("You feel your heart start beating normally again!"))
qdel(affected_mob.GetComponent(/datum/component/manual_heart))
..()

/datum/reagent/inverse/antihol
name = "Prohol"
Expand Down
Loading

0 comments on commit 6bdf052

Please sign in to comment.