From 4ee7995b9cf213ccca5438aadc2ace056debb0ce Mon Sep 17 00:00:00 2001 From: Zephyr <12817816+ZephyrTFA@users.noreply.github.com> Date: Thu, 24 Nov 2022 14:30:15 -0500 Subject: [PATCH] Strange Reagent correctly revives station pets, (and humans), correctly again. (#71430) Also makes the healing factor non hardcoded ## About The Pull Request Strange Reagent had a callback to call /mob/living/revive; however the arguments for that proc were changed but the callback arguments were not changed. Also makes it so that the healing per unit is no longer hardcoded, although its still kept at the stock 5 healing per unit. Also, the max health cutoff was also hardcoded, even though we have a getter for a mob's max health? ## Why It's Good For The Game resolves https://github.com/tgstation/tgstation/issues/71417 ## Changelog :cl: fix: Strange Reagent is once again Strange! (it works at all) /:cl: --- .../mob/living/carbon/carbon_update_icons.dm | 4 +- code/modules/mob/living/living.dm | 19 +++ .../chemistry/reagents/medicine_reagents.dm | 76 ++++++++- code/modules/unit_tests/_unit_tests.dm | 1 + code/modules/unit_tests/strange_reagent.dm | 149 ++++++++++++++++++ 5 files changed, 239 insertions(+), 10 deletions(-) create mode 100644 code/modules/unit_tests/strange_reagent.dm diff --git a/code/modules/mob/living/carbon/carbon_update_icons.dm b/code/modules/mob/living/carbon/carbon_update_icons.dm index b150a8b3676..9001e51d98c 100644 --- a/code/modules/mob/living/carbon/carbon_update_icons.dm +++ b/code/modules/mob/living/carbon/carbon_update_icons.dm @@ -73,11 +73,11 @@ //used when putting/removing clothes that hide certain mutant body parts to just update those and not update the whole body. /mob/living/carbon/human/proc/update_mutant_bodyparts() - dna.species.handle_mutant_bodyparts(src) + dna?.species.handle_mutant_bodyparts(src) update_body_parts() /mob/living/carbon/update_body(is_creating = FALSE) - dna.species.handle_body(src) //This calls `handle_mutant_bodyparts` which calls `update_mutant_bodyparts()`. Don't double call! + dna?.species.handle_body(src) //This calls `handle_mutant_bodyparts` which calls `update_mutant_bodyparts()`. Don't double call! update_body_parts(is_creating) /mob/living/carbon/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index fd9f4b56fca..6f8c2b6d25c 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -869,6 +869,25 @@ stop_sound_channel(CHANNEL_HEARTBEAT) SEND_SIGNAL(src, COMSIG_LIVING_POST_FULLY_HEAL, heal_flags) +/** + * Called by strange_reagent, with the amount of healing the strange reagent is doing + * It uses the healing amount on brute/fire damage, and then uses the excess healing for revive + */ +/mob/living/proc/do_strange_reagent_revival(healing_amount) + var/brute_loss = getBruteLoss() + if(brute_loss) + var/brute_healing = min(healing_amount * 0.5, brute_loss) // 50% of the healing goes to brute + setBruteLoss(round(brute_loss - brute_healing, DAMAGE_PRECISION), updating_health=FALSE, forced=TRUE) + healing_amount = max(0, healing_amount - brute_healing) + + var/fire_loss = getFireLoss() + if(fire_loss && healing_amount) + var/fire_healing = min(healing_amount, fire_loss) // rest of the healing goes to fire + setFireLoss(round(fire_loss - fire_healing, DAMAGE_PRECISION), updating_health=TRUE, forced=TRUE) + healing_amount = max(0, healing_amount - fire_healing) + + revive(NONE, excess_healing=max(healing_amount, 0), force_grab_ghost=FALSE) // and any excess healing is passed along + /// Checks if we are actually able to ressuscitate this mob. /// (We don't want to revive then to have them instantly die again) /mob/living/proc/can_be_revived() diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm index 4acc0b8b5d9..113fc32325a 100644 --- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm @@ -802,7 +802,7 @@ /datum/reagent/medicine/strange_reagent name = "Strange Reagent" - description = "A miracle drug capable of bringing the dead back to life. Works topically unless anotamically complex, in which case works orally. Only works if the target has less than 200 total brute and burn damage and hasn't been husked and requires more reagent depending on damage inflicted. Causes damage to the living." + description = "A miracle drug capable of bringing the dead back to life. Works topically unless anotamically complex, in which case works orally. Cannot revive targets under -%MAXHEALTHRATIO% health." reagent_state = LIQUID color = "#A0E85E" metabolization_rate = 1.25 * REAGENTS_METABOLISM @@ -810,7 +810,24 @@ harmful = TRUE ph = 0.5 chemical_flags = REAGENT_CAN_BE_SYNTHESIZED + /// The amount of damage a single unit of this will heal + var/healing_per_reagent_unit = 5 + /// The ratio of the excess reagent used to contribute to excess healing + var/excess_healing_ratio = 0.8 + /// Do we instantly revive + var/instant = FALSE + /// The maximum amount of damage we can revive from, as a ratio of max health + var/max_revive_damage_ratio = 2 +/datum/reagent/medicine/strange_reagent/instant + name = "Stranger Reagent" + instant = TRUE + +/datum/reagent/medicine/strange_reagent/New() + . = ..() + description = replacetext(description, "%MAXHEALTHRATIO%", "[max_revive_damage_ratio * 100]%") + if(instant) + description += " It appears to be pulsing with a warm pink light." // FEED ME SEYMOUR /datum/reagent/medicine/strange_reagent/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user) @@ -818,26 +835,69 @@ if(chems.has_reagent(type, 1)) mytray.spawnplant() +/// Calculates the amount of reagent to at a bare minimum make the target not dead +/datum/reagent/medicine/strange_reagent/proc/calculate_amount_needed_to_revive(mob/living/benefactor) + var/their_health = benefactor.getMaxHealth() - (benefactor.getBruteLoss() + benefactor.getFireLoss()) + if(their_health > 0) + return 1 + + return round(-their_health / healing_per_reagent_unit, DAMAGE_PRECISION) + +/// Calculates the amount of reagent that will be needed to both revive and full heal the target. Looks at healing_per_reagent_unit and excess_healing_ratio +/datum/reagent/medicine/strange_reagent/proc/calculate_amount_needed_to_full_heal(mob/living/benefactor) + var/their_health = benefactor.getBruteLoss() + benefactor.getFireLoss() + var/max_health = benefactor.getMaxHealth() + if(their_health >= max_health) + return 1 + + var/amount_needed_to_revive = calculate_amount_needed_to_revive(benefactor) + var/expected_amount_to_full_heal = round(max_health / healing_per_reagent_unit, DAMAGE_PRECISION) / excess_healing_ratio + return amount_needed_to_revive + expected_amount_to_full_heal + /datum/reagent/medicine/strange_reagent/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume) - if(exposed_mob.stat != DEAD) + if(exposed_mob.stat != DEAD || !(exposed_mob.mob_biotypes & MOB_ORGANIC)) return ..() + if(exposed_mob.suiciding) //they are never coming back exposed_mob.visible_message(span_warning("[exposed_mob]'s body does not react...")) return + if(iscarbon(exposed_mob) && !(methods & INGEST)) //simplemobs can still be splashed return ..() - var/amount_to_revive = round((exposed_mob.getBruteLoss() + exposed_mob.getFireLoss()) / 20) - if(exposed_mob.getBruteLoss() + exposed_mob.getFireLoss() >= 200 || HAS_TRAIT(exposed_mob, TRAIT_HUSK) || reac_volume < amount_to_revive) //body will die from brute+burn on revive or you haven't provided enough to revive. + + if(HAS_TRAIT(exposed_mob, TRAIT_HUSK)) + exposed_mob.visible_message(span_warning("[exposed_mob]'s body lets off a puff of smoke...")) + return + + if((exposed_mob.getBruteLoss() + exposed_mob.getFireLoss()) > (exposed_mob.getMaxHealth() * max_revive_damage_ratio)) + exposed_mob.visible_message(span_warning("[exposed_mob]'s body convulses violently, before falling still...")) + return + + var/needed_to_revive = calculate_amount_needed_to_revive(exposed_mob) + if(reac_volume < needed_to_revive) exposed_mob.visible_message(span_warning("[exposed_mob]'s body convulses a bit, and then falls still once more.")) exposed_mob.do_jitter_animation(10) return + exposed_mob.visible_message(span_warning("[exposed_mob]'s body starts convulsing!")) exposed_mob.notify_ghost_cloning("Your body is being revived with Strange Reagent!") exposed_mob.do_jitter_animation(10) - var/excess_healing = 5 * (reac_volume - amount_to_revive) //excess reagent will heal blood and organs across the board - addtimer(CALLBACK(exposed_mob, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 10), 40) //jitter immediately, then again after 4 and 8 seconds - addtimer(CALLBACK(exposed_mob, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 10), 80) - addtimer(CALLBACK(exposed_mob, TYPE_PROC_REF(/mob/living, revive), FALSE, FALSE, excess_healing), 79) + + // we factor in healing needed when determing if we do anything + var/healing = needed_to_revive * healing_per_reagent_unit + // but excessive healing is penalized, to reward doctors who use the perfect amount + reac_volume -= needed_to_revive + healing += (reac_volume * healing_per_reagent_unit) * excess_healing_ratio + + // during unit tests, we want it to happen immediately + if(instant) + exposed_mob.do_strange_reagent_revival(healing) + else + // jitter immediately, after four seconds, and after eight seconds + addtimer(CALLBACK(exposed_mob, TYPE_PROC_REF(/mob/living, do_jitter_animation), 1 SECONDS), 4 SECONDS) + addtimer(CALLBACK(exposed_mob, TYPE_PROC_REF(/mob/living, do_strange_reagent_revival), healing), 7 SECONDS) + addtimer(CALLBACK(exposed_mob, TYPE_PROC_REF(/mob/living, do_jitter_animation), 1 SECONDS), 8 SECONDS) + return ..() /datum/reagent/medicine/strange_reagent/on_mob_life(mob/living/carbon/M, delta_time, times_fired) diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 8de44cce602..d6fdc13c7e9 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -189,6 +189,7 @@ #include "stack_singular_name.dm" #include "station_trait_tests.dm" #include "stomach.dm" +#include "strange_reagent.dm" #include "strippable.dm" #include "subsystem_init.dm" #include "suit_storage_icons.dm" diff --git a/code/modules/unit_tests/strange_reagent.dm b/code/modules/unit_tests/strange_reagent.dm new file mode 100644 index 00000000000..e84a8f40c1b --- /dev/null +++ b/code/modules/unit_tests/strange_reagent.dm @@ -0,0 +1,149 @@ +/// Checks to ensure strange reagent works as expected +/datum/unit_test/strange_reagent + priority = TEST_LONGER + + var/datum/reagent/medicine/strange_reagent/instant/strange_reagent + var/target_max_health + var/amount_needed_to_full_heal + var/amount_needed_to_revive + +/datum/unit_test/strange_reagent/Run() + strange_reagent = new + + var/list/types_to_check = typecacheof(list( + /mob/living/carbon/human, + /mob/living/simple_animal, + /mob/living/basic, + )) + types_to_check -= /mob/living/simple_animal/pet/gondola/gondolapod // need a pod, which we don't have + types_to_check -= typesof(/mob/living/simple_animal/hostile/megafauna) // no + types_to_check -= typesof(/mob/living/basic/mouse) // qdel themselves on death; why dont they use DEL_ON_DEATH you might ask. I-unno + types_to_check -= typesof(/mob/living/simple_animal/slime) // if they roll the 50% chance to spawn as an adult, they can just at random split and qdel themselves + + for(var/mob/living/type as anything in types_to_check) + var/mob/living/target = allocate_new_target(type) + var/is_basic = istype(target, /mob/living/basic) + var/is_simple = istype(target, /mob/living/simple_animal) + // check some basic stuff + if(target.status_flags & GODMODE) + continue + if(!(target.mob_biotypes & MOB_ORGANIC)) + continue + + if(is_simple) + var/mob/living/simple_animal/simple_animal = target + if(simple_animal.del_on_death) + continue + simple_animal.loot?.Cut() + + if(is_basic) + var/mob/living/basic/basic = target + if(basic.basic_mob_flags & DEL_ON_DEATH) + continue + + test_damage_but_no_death(type) + test_death_no_damage(type) + test_death_with_damage(type) + test_death_with_damage_but_not_enough_reagent(type) + if(!is_basic && !is_simple) // simple/basic mobs cannot have negative health + test_death_with_full_heal(type) + test_death_from_damage(type) + test_death_from_too_much_damage(type) + // cleanup our vars + QDEL_NULL(strange_reagent) + allocate_new_target(null) + +/datum/unit_test/strange_reagent/proc/allocate_new_target(type) + // cache the last one created so that we don't create N instances of the exact same mob + var/static/mob/living/pre_allocated + if(!type) + pre_allocated = null + return + + if(pre_allocated?.type == type) + pre_allocated.revive(ALL) // for some reason HEAL_ADMIN doesn't work? + return pre_allocated + + pre_allocated = allocate(type) + target_max_health = pre_allocated.getMaxHealth() + update_amounts(pre_allocated) + return pre_allocated + +/datum/unit_test/strange_reagent/proc/update_amounts(mob/living/target) + amount_needed_to_full_heal = strange_reagent.calculate_amount_needed_to_full_heal(target) + amount_needed_to_revive = strange_reagent.calculate_amount_needed_to_revive(target) + +/datum/unit_test/strange_reagent/proc/damage_target_to_percentage(mob/living/target, percent) + var/damage = target_max_health * percent * 0.5 + target.setBruteLoss(damage, updating_health=FALSE) // no point running health update logic here + target.setFireLoss(damage, updating_health=TRUE) // since we do it here + update_amounts(target) + if(percent >= 1) + target.death() + return TRUE + +/datum/unit_test/strange_reagent/proc/get_target_organic_health_manual(mob/living/target) + return target.getMaxHealth() - (target.getBruteLoss() + target.getFireLoss()) + +/datum/unit_test/strange_reagent/proc/test_damage_but_no_death(target_type) + var/mob/living/target = allocate_new_target(target_type) + if(!damage_target_to_percentage(target, 0.8)) + return + + var/health = get_target_organic_health_manual(target) + strange_reagent.expose_mob(target, INGEST, 20) + TEST_ASSERT_EQUAL(health, get_target_organic_health_manual(target), "Strange Reagent healed a target type [target.type] that was not dead.") + +/datum/unit_test/strange_reagent/proc/test_death_no_damage(target_type) + var/mob/living/target = allocate_new_target(target_type) + target.death() + update_amounts(target) + strange_reagent.expose_mob(target, INGEST, amount_needed_to_revive) + TEST_ASSERT_NOTEQUAL(target.stat, DEAD, "Strange Reagent did not revive a dead target type [target.type].") + +/datum/unit_test/strange_reagent/proc/test_death_with_damage(target_type) + var/mob/living/target = allocate_new_target(target_type) + if(!damage_target_to_percentage(target, 0.8)) + return + + target.death() + update_amounts(target) + strange_reagent.expose_mob(target, INGEST, amount_needed_to_revive) + TEST_ASSERT_NOTEQUAL(target.stat, DEAD, "Strange Reagent did not revive a dead target type [target.type].") + +/datum/unit_test/strange_reagent/proc/test_death_with_damage_but_not_enough_reagent(target_type) + var/mob/living/target = allocate_new_target(target_type) + if(!damage_target_to_percentage(target, 1.2)) + return + + update_amounts(target) + strange_reagent.expose_mob(target, INGEST, amount_needed_to_revive - 1) + TEST_ASSERT_EQUAL(target.stat, DEAD, "Strange Reagent revived a dead target type [target.type] without enough reagent.") + +/datum/unit_test/strange_reagent/proc/test_death_with_full_heal(target_type) + var/mob/living/target = allocate_new_target(target_type) + if(!damage_target_to_percentage(target, 0.8)) + return + + target.death() + update_amounts(target) + strange_reagent.expose_mob(target, INGEST, amount_needed_to_full_heal) + TEST_ASSERT_EQUAL(target_max_health, get_target_organic_health_manual(target), "Strange Reagent did not fully heal a dead target type [target.type] with the expected amount.") + +/datum/unit_test/strange_reagent/proc/test_death_from_damage(target_type) + var/mob/living/target = allocate_new_target(target_type) + if(!damage_target_to_percentage(target, strange_reagent.max_revive_damage_ratio * 0.9)) // 10% under the damage cap + return + + update_amounts(target) + strange_reagent.expose_mob(target, INGEST, amount_needed_to_revive) + TEST_ASSERT_NOTEQUAL(target.stat, DEAD, "Strange Reagent did not revive a target type [target.type] who died from damage.") + +/datum/unit_test/strange_reagent/proc/test_death_from_too_much_damage(target_type) + var/mob/living/target = allocate_new_target(target_type) + if(!damage_target_to_percentage(target, strange_reagent.max_revive_damage_ratio * 1.1)) // 10% over the damage cap + return + + update_amounts(target) + strange_reagent.expose_mob(target, INGEST, amount_needed_to_revive) + TEST_ASSERT_EQUAL(target.stat, DEAD, "Strange Reagent revived a target type [target.type] with more than double their max health in damage.")