diff --git a/code/__DEFINES/dcs/signals/signals_action.dm b/code/__DEFINES/dcs/signals/signals_action.dm index 27206e483c5..6fbf5372acd 100644 --- a/code/__DEFINES/dcs/signals/signals_action.dm +++ b/code/__DEFINES/dcs/signals/signals_action.dm @@ -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" diff --git a/code/datums/components/manual_heart.dm b/code/datums/components/manual_heart.dm new file mode 100644 index 00000000000..09448acfc67 --- /dev/null +++ b/code/datums/components/manual_heart.dm @@ -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 diff --git a/code/modules/antagonists/wizard/equipment/artefact.dm b/code/modules/antagonists/wizard/equipment/artefact.dm index 3e4cf0766f5..4783daffb8b 100644 --- a/code/modules/antagonists/wizard/equipment/artefact.dm +++ b/code/modules/antagonists/wizard/equipment/artefact.dm @@ -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 diff --git a/code/modules/client/client_colour.dm b/code/modules/client/client_colour.dm index 88573c78e23..9ac64851a68 100644 --- a/code/modules/client/client_colour.dm +++ b/code/modules/client/client_colour.dm @@ -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 diff --git a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm index 85cdc4c3922..e2b703a9c13 100644 --- a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm @@ -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 @@ -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) + var/obj/item/organ/internal/heart/affected_heart = carbon_mob.get_organ_slot(ORGAN_SLOT_HEART) + if(isnull(affected_heart)) return - original_heart_ref = WEAKREF(original_heart) + carbon_mob.AddComponent(/datum/component/manual_heart) + return ..() - 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) - 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" diff --git a/code/modules/surgery/organs/internal/heart/_heart.dm b/code/modules/surgery/organs/internal/heart/_heart.dm index 307d3e478a5..fb97ca7eda0 100644 --- a/code/modules/surgery/organs/internal/heart/_heart.dm +++ b/code/modules/surgery/organs/internal/heart/_heart.dm @@ -104,20 +104,12 @@ icon_state = "cursedheart-off" base_icon_state = "cursedheart" decay_factor = 0 - actions_types = list(/datum/action/item_action/organ_action/cursed_heart) - var/last_pump = 0 - var/add_colour = TRUE //So we're not constantly recreating colour datums - /// 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 / 5) // 20% of normal volume, missing five pumps is instant death - - //How much to heal per pump, negative numbers would HURT the player + var/blood_loss = BLOOD_VOLUME_NORMAL * 0.2 var/heal_brute = 0 var/heal_burn = 0 var/heal_oxy = 0 - /obj/item/organ/internal/heart/cursed/attack(mob/living/carbon/human/accursed, mob/living/carbon/human/user, obj/target) if(accursed == user && istype(accursed)) playsound(user,'sound/effects/singlebeat.ogg',40,TRUE) @@ -126,75 +118,13 @@ else return ..() -/// Worker proc that checks logic for if a pump can happen, and applies effects/notifications from doing so -/obj/item/organ/internal/heart/cursed/proc/on_pump(mob/owner) - var/next_pump = last_pump + pump_delay - (1 SECONDS) // pump a second early - if(world.time < next_pump) - to_chat(owner, span_userdanger("Too soon!")) - return - - last_pump = world.time - playsound(owner,'sound/effects/singlebeat.ogg', 40, TRUE) - to_chat(owner, span_notice("Your heart beats.")) - - if(!ishuman(owner)) - return - var/mob/living/carbon/human/accursed = owner - - if(HAS_TRAIT(accursed, TRAIT_NOBLOOD) || !accursed.dna) - return - accursed.blood_volume = min(accursed.blood_volume + (blood_loss * 0.5), BLOOD_VOLUME_MAXIMUM) - accursed.remove_client_colour(/datum/client_colour/cursed_heart_blood) - add_colour = TRUE - accursed.adjustBruteLoss(-heal_brute) - accursed.adjustFireLoss(-heal_burn) - accursed.adjustOxyLoss(-heal_oxy) - -/obj/item/organ/internal/heart/cursed/on_life(seconds_per_tick, times_fired) - if(!owner.client || !ishuman(owner)) // Let's be fair, if you're not here to pump, you're not here to suffer. - last_pump = world.time - return - - if(world.time <= (last_pump + pump_delay)) - return - - var/mob/living/carbon/human/accursed = owner - if(HAS_TRAIT(accursed, TRAIT_NOBLOOD) || !accursed.dna) - return - - accursed.blood_volume = max(accursed.blood_volume - blood_loss, 0) - to_chat(accursed, span_userdanger("You have to keep pumping your blood!")) - if(add_colour) - accursed.add_client_colour(/datum/client_colour/cursed_heart_blood) //bloody screen so real - add_colour = FALSE - /obj/item/organ/internal/heart/cursed/on_insert(mob/living/carbon/accursed) . = ..() - last_pump = world.time // give them time to react - to_chat(accursed, span_userdanger("Your heart has been replaced with a cursed one, you have to pump this one manually otherwise you'll die!")) + accursed.AddComponent(/datum/component/manual_heart, pump_delay = pump_delay, blood_loss = blood_loss, heal_brute = heal_brute, heal_burn = heal_burn, heal_oxy = heal_oxy) /obj/item/organ/internal/heart/cursed/Remove(mob/living/carbon/accursed, special = FALSE) . = ..() - accursed.remove_client_colour(/datum/client_colour/cursed_heart_blood) - -/datum/action/item_action/organ_action/cursed_heart - name = "Pump your blood" - check_flags = NONE - -//You are now brea- pumping blood manually -/datum/action/item_action/organ_action/cursed_heart/Trigger(trigger_flags) - . = ..() - if(!.) - return - - var/obj/item/organ/internal/heart/cursed/cursed_heart = target - if(!istype(cursed_heart)) - CRASH("Cursed heart pump action created on non-cursed heart!") - cursed_heart.on_pump(owner) - -/datum/client_colour/cursed_heart_blood - priority = 100 //it's an indicator you're dying, so it's very high priority - colour = "#FF0000" + qdel(accursed.GetComponent(/datum/component/manual_heart)) /obj/item/organ/internal/heart/cybernetic name = "basic cybernetic heart" diff --git a/tgstation.dme b/tgstation.dme index e3a512be234..e6f65e3dd38 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1023,6 +1023,7 @@ #include "code\datums\components\magnet.dm" #include "code\datums\components\manual_blinking.dm" #include "code\datums\components\manual_breathing.dm" +#include "code\datums\components\manual_heart.dm" #include "code\datums\components\mind_linker.dm" #include "code\datums\components\mirv.dm" #include "code\datums\components\mob_harvest.dm"