Files
Bubberstation/code/datums/components/manual_heart.dm
SmArtKar 845ed7459e Bravo Six, Going Dark: Refactors client colors to fix dupe issues and support filters (#89843)
## About The Pull Request

Completely refactored how client colors are handled. Now they're similar
to traits, having a source associated with them. Instead of adding and
removing by strict type (which makes client colors prone to getting
duplicated and not cleaned up) you remove a filter associated with a
specific source. Adding another client color with the same source as an
already existing one will replace the existing one if its of a different
type, or do nothing if they're the same (unless force is set to TRUE).
Client colors can also force filter splitting, putting all colors that
come before them, themselves, and all colors after them into separate
filters - this is useful to prevent mixing in filters which are supposed
to remove a certain color.

<details>

<summary>Example of how Perceptomatrix and nightmare vision goggles
combined before this PR:</summary>


![dreamseeker_fBOse2i0jq](https://github.com/user-attachments/assets/fb164e1f-ce8b-4a8c-8820-5db25e710056)

And this is after, as you can see nightmare vision effect's red is only
slightly tinted by perceptomatix instead of being literally halved.


![dreamseeker_U6A24fetK8](https://github.com/user-attachments/assets/8d8b3eb5-9f53-4646-bc56-0f5897013c6f)

</details>

Additionally, added support for custom filters (and not just colors) to
client color code to allow us to work with more colorspaces.

Also fixed weird blindness behavior, so this also 
Closes #89787

## Why It's Good For The Game

Makes code less ass to work with, fixes weird color mixing, etc.

## Changelog
🆑
fix: Fixed perceptomatix helmet allowing you to see even when
unconscious
refactor: Refactored how client colors are handled, ensuring that
certain effects like nightmare goggles don't disappear when another
vision-affecting piece of clothing is worn.
/🆑
2025-03-17 11:35:11 +00:00

180 lines
7.2 KiB
Plaintext

/// 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/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(REF(src))
/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(REF(src)) //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(REF(src))
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, REF(src))
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/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(REF(src)) //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/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/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/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