From c7cb0674cc7d61843062321edeb0a0db9d677919 Mon Sep 17 00:00:00 2001 From: RikuTheKiller <88713943+RikuTheKiller@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:45:36 +0200 Subject: [PATCH] Preliminary blood refactor (#93854) ## About The Pull Request Moves all blood handling into procs and adds ways to easily hook into basically every basic blood behavior. This PR is not meant to fix every single case of janky blood logic in the game. The main point and motivation of this PR is to add hooks for blood behaviors. This allows for way more flexibility with blood code. I am not going to fix our 3000 instances of single-letter vars, wacky blood transfers, etc. This is just the groundwork for future PRs to build off of, and by itself, should do very little to change blood behavior. I also added a rigorous set of unit tests for verifying that all of the basic blood volume procs work correctly. ## Why It's Good For The Game Previously, blood was handled via directly reading/writing [var/blood_volume]. This was INCREDIBLY inconsistent and there was no way to hook into it. This PR makes blood handling way more consistent, which is great for all sorts of features. --- .../signals/signals_mob/signals_mob_living.dm | 3 + code/__DEFINES/living.dm | 6 + code/__DEFINES/traits/declarations.dm | 2 +- code/_onclick/hud/screen_objects.dm | 2 +- code/datums/components/aura_healing.dm | 3 +- code/datums/components/cult_ritual_item.dm | 10 +- code/datums/components/damage_aura.dm | 8 +- code/datums/components/manual_heart.dm | 25 +- code/datums/components/splattercasting.dm | 2 +- .../diseases/advance/symptoms/oxygen.dm | 4 +- code/datums/elements/leeching_walk.dm | 3 +- code/datums/mutations/touch.dm | 28 +- .../negative_quirks/blood_deficiency.dm | 6 +- .../negative_quirks/prosthetic_organ.dm | 2 +- code/datums/quirks/negative_quirks/tin_man.dm | 2 +- code/datums/status_effects/debuffs/debuffs.dm | 3 +- code/datums/wounds/bones.dm | 4 +- code/datums/wounds/loss.dm | 2 +- code/datums/wounds/pierce.dm | 4 +- code/datums/wounds/slash.dm | 2 +- code/game/machinery/iv_drip.dm | 2 +- code/game/machinery/medical_kiosk.dm | 2 +- code/game/machinery/pipe/construction.dm | 2 +- code/game/machinery/wall_healer.dm | 7 +- .../objects/items/devices/blood_scanner.dm | 2 +- code/game/objects/items/devices/flashlight.dm | 6 +- .../items/devices/scanners/autopsy_scanner.dm | 39 +-- .../items/devices/scanners/health_analyzer.dm | 15 +- code/game/objects/items/soulscythe.dm | 10 +- .../abductor/equipment/glands/heal.dm | 8 +- code/modules/antagonists/cult/blood_magic.dm | 15 +- code/modules/antagonists/cult/runes.dm | 4 +- .../heretic/knowledge/rust_lore.dm | 5 +- .../sacrifice_knowledge/sacrifice_buff.dm | 9 +- .../heretic/magic/apetravulnera.dm | 2 +- .../antagonists/heretic/magic/blood_cleave.dm | 2 +- .../antagonists/heretic/magic/blood_siphon.dm | 7 +- .../heretic/magic/crimson_cleave.dm | 5 +- .../heretic/status_effects/buffs.dm | 2 +- .../heretic/status_effects/heretic_passive.dm | 6 +- .../bitrunning/components/netpod_healing.dm | 3 +- code/modules/clothing/neck/_neck.dm | 8 +- code/modules/hallucination/blood_flow.dm | 39 ++- code/modules/lost_crew/damages/_damages.dm | 2 +- code/modules/mob/living/basic/alien/_alien.dm | 2 +- code/modules/mob/living/basic/clown/clown.dm | 2 +- .../mob/living/basic/cytology/vatbeast.dm | 2 +- .../mob/living/basic/farm_animals/cow/_cow.dm | 2 +- .../living/basic/farm_animals/deer/deer.dm | 2 +- .../living/basic/farm_animals/goat/_goat.dm | 2 +- .../mob/living/basic/farm_animals/pig.dm | 2 +- .../mob/living/basic/farm_animals/pony.dm | 2 +- .../mob/living/basic/farm_animals/sheep.dm | 2 +- code/modules/mob/living/basic/pets/pet.dm | 2 +- code/modules/mob/living/blood.dm | 290 ++++++++++++------ code/modules/mob/living/carbon/carbon.dm | 8 +- .../mob/living/carbon/carbon_defines.dm | 2 +- code/modules/mob/living/carbon/examine.dm | 2 +- .../mob/living/carbon/human/_species.dm | 3 +- code/modules/mob/living/carbon/human/death.dm | 4 +- code/modules/mob/living/carbon/human/human.dm | 12 +- .../mob/living/carbon/human/human_defense.dm | 3 +- .../carbon/human/species_types/jellypeople.dm | 53 ++-- .../carbon/human/species_types/vampire.dm | 24 +- .../modules/mob/living/carbon/init_signals.dm | 22 ++ code/modules/mob/living/carbon/life.dm | 13 +- code/modules/mob/living/damage_procs.dm | 9 +- code/modules/mob/living/living.dm | 2 + code/modules/mob/living/living_defense.dm | 2 +- code/modules/mob/living/living_defines.dm | 7 +- .../hostile/megafauna/blood_drunk_miner.dm | 2 +- .../hostile/megafauna/bubblegum.dm | 2 +- .../hostile/megafauna/demonic_frost_miner.dm | 2 +- .../hostile/megafauna/wendigo.dm | 2 +- .../reagents/drinks/alcohol_reagents.dm | 3 +- .../chemistry/reagents/food_reagents.dm | 2 +- .../impure_medicine_reagents.dm | 3 +- .../chemistry/reagents/medicine_reagents.dm | 36 ++- .../chemistry/reagents/other_reagents.dm | 12 +- code/modules/religion/religion_sects.dm | 14 +- .../xenobiology/crossbreeding/_weapons.dm | 8 +- .../xenobiology/crossbreeding/consuming.dm | 2 +- .../research/xenobiology/xenobiology.dm | 2 +- .../spell_types/self/sanguine_strike.dm | 8 +- .../spell_types/self/splattercasting_spell.dm | 2 +- .../spell_types/shapeshift/_shape_status.dm | 8 +- .../surgery/organs/internal/heart/_heart.dm | 7 +- .../organs/internal/heart/heart_anomalock.dm | 3 +- .../surgery/organs/internal/liver/_liver.dm | 3 +- .../surgery/organs/internal/lungs/_lungs.dm | 2 +- code/modules/unit_tests/_unit_tests.dm | 1 + code/modules/unit_tests/blood_volume_procs.dm | 115 +++++++ 92 files changed, 660 insertions(+), 378 deletions(-) create mode 100644 code/modules/unit_tests/blood_volume_procs.dm diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm index c3b84b4e6c3..a78a1060fce 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm @@ -374,3 +374,6 @@ /// From /mob/living/proc/mob_pickup() : (mob/living/user, obj/item/mob_holder/holder) #define COMSIG_LIVING_SCOOPED_UP "living_scooped_up" + +/// From /mob/living/proc/update_blood_status(), sent when the return value of /mob/living/proc/can_have_blood() changes : (had_blood, has_blood, old_blood_volume, new_blood_volume) +#define COMSIG_LIVING_UPDATE_BLOOD_STATUS "living_update_blood_status" diff --git a/code/__DEFINES/living.dm b/code/__DEFINES/living.dm index 920b8054445..ac2cb64efae 100644 --- a/code/__DEFINES/living.dm +++ b/code/__DEFINES/living.dm @@ -12,9 +12,15 @@ #define STOP_OVERLAY_UPDATE_BODY_PARTS (1<<2) /// Nutrition changed last life tick, so we should bulk update this tick #define QUEUE_NUTRITION_UPDATE (1<<3) +/// Blood volume has changed since the last [proc/update_blood_effects] call +#define QUEUE_BLOOD_UPDATE (1<<4) +/// This mob can have blood, cached value of [proc/can_have_blood] +#define LIVING_CAN_HAVE_BLOOD (1<<5) /// Getter for a mob/living's lying angle, otherwise protected #define GET_LYING_ANGLE(mob) (UNLINT(mob.lying_angle)) +/// Checks if the mob can have blood +#define CAN_HAVE_BLOOD(mob) (mob.living_flags & LIVING_CAN_HAVE_BLOOD) // Used in living mob offset list for determining pixel offsets #define PIXEL_W_OFFSET "w" diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index a226fedfc22..13466757108 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -222,7 +222,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_NO_AUGMENTS "no_augments" /// This carbon doesn't get hungry #define TRAIT_NOHUNGER "no_hunger" -/// This carbon doesn't bleed +/// This carbon doesn't have blood #define TRAIT_NOBLOOD "noblood" /// This just means that the carbon will always have functional liverless metabolism #define TRAIT_LIVERLESS_METABOLISM "liverless_metabolism" diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index f54a5bcd15d..77f1316d4a4 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -1174,7 +1174,7 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/splash) if(!isliving(source)) return - maptext = FORMAT_BLOOD_LEVEL_HUD_MAPTEXT(source.blood_volume) + maptext = FORMAT_BLOOD_LEVEL_HUD_MAPTEXT(source.get_blood_volume()) #undef FORMAT_BLOOD_LEVEL_HUD_MAPTEXT diff --git a/code/datums/components/aura_healing.dm b/code/datums/components/aura_healing.dm index 416ab713b44..6d1aa93923d 100644 --- a/code/datums/components/aura_healing.dm +++ b/code/datums/components/aura_healing.dm @@ -151,8 +151,7 @@ var/mob/living/basic/basic_candidate = candidate basic_candidate.adjust_health(-simple_heal * seconds_per_tick, updating_health = FALSE) - if (candidate.blood_volume < BLOOD_VOLUME_NORMAL) - candidate.blood_volume += blood_heal * seconds_per_tick + candidate.adjust_blood_volume(blood_heal * seconds_per_tick, maximum = BLOOD_VOLUME_NORMAL) candidate.updatehealth() diff --git a/code/datums/components/cult_ritual_item.dm b/code/datums/components/cult_ritual_item.dm index 1e4e4ab76bc..f9eb301392f 100644 --- a/code/datums/components/cult_ritual_item.dm +++ b/code/datums/components/cult_ritual_item.dm @@ -300,12 +300,14 @@ return our_turf = get_turf(cultist) //we may have moved. adjust as needed... + var/can_have_blood = CAN_HAVE_BLOOD(cultist) + cultist.visible_message( - span_warning("[cultist] [cultist.blood_volume ? "cuts open [cultist.p_their()] arm and begins writing in [cultist.p_their()] own blood":"begins sketching out a strange design"]!"), - span_cult("You [cultist.blood_volume ? "slice open your arm and ":""]begin drawing a sigil of the Geometer.") + span_warning("[cultist] [can_have_blood ? "cuts open [cultist.p_their()] arm and begins writing in [cultist.p_their()] own blood":"begins sketching out a strange design"]!"), + span_cult("You [can_have_blood ? "slice open your arm and ":""]begin drawing a sigil of the Geometer.") ) - if(cultist.blood_volume) + if(can_have_blood) cultist.apply_damage(initial(rune_to_scribe.scribe_damage), BRUTE, pick(GLOB.arm_zones), wound_bonus = CANT_WOUND) // *cuts arm* *bone explodes* ever have one of those days? var/scribe_mod = initial(rune_to_scribe.scribe_delay) @@ -332,7 +334,7 @@ return FALSE cultist.visible_message( - span_warning("[cultist] creates a strange circle[cultist.blood_volume ? " in [cultist.p_their()] own blood":""]."), + span_warning("[cultist] creates a strange circle[can_have_blood ? " in [cultist.p_their()] own blood":""]."), span_cult("You finish drawing the arcane markings of the Geometer.") ) diff --git a/code/datums/components/damage_aura.dm b/code/datums/components/damage_aura.dm index 9c4e996113b..1c95afd9567 100644 --- a/code/datums/components/damage_aura.dm +++ b/code/datums/components/damage_aura.dm @@ -102,8 +102,9 @@ need_mob_update += owner_mob.adjustFireLoss(-1 * seconds_per_tick, updating_health = FALSE) need_mob_update += owner_mob.adjustToxLoss(-1 * seconds_per_tick, updating_health = FALSE, forced = TRUE) need_mob_update += owner_mob.adjustOxyLoss(-1 * seconds_per_tick, updating_health = FALSE) - if (owner_mob.blood_volume < BLOOD_VOLUME_NORMAL) - owner_mob.blood_volume += 2 * seconds_per_tick + + owner_mob.adjust_blood_volume(2 * seconds_per_tick, maximum = BLOOD_VOLUME_NORMAL) + if(need_mob_update) owner_mob.updatehealth() @@ -151,8 +152,7 @@ var/mob/living/basic/basic_candidate = candidate basic_candidate.adjust_health(simple_damage * seconds_per_tick, updating_health = FALSE) - if (candidate.blood_volume > BLOOD_VOLUME_SURVIVE) - candidate.blood_volume -= blood_damage * seconds_per_tick + candidate.adjust_blood_volume(blood_damage * seconds_per_tick, minimum = BLOOD_VOLUME_SURVIVE) candidate.updatehealth() diff --git a/code/datums/components/manual_heart.dm b/code/datums/components/manual_heart.dm index fb815cdfdbb..83fb87124a3 100644 --- a/code/datums/components/manual_heart.dm +++ b/code/datums/components/manual_heart.dm @@ -49,22 +49,21 @@ 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)) + RegisterSignal(parent, COMSIG_LIVING_UPDATE_BLOOD_STATUS, PROC_REF(on_update_blood_status)) 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) + if(parent_heart && CAN_HAVE_BLOOD(carbon_parent) && 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))) + UnregisterSignal(parent, list(COMSIG_CARBON_GAIN_ORGAN, COMSIG_CARBON_LOSE_ORGAN, COMSIG_HEART_MANUAL_PULSE, COMSIG_LIVING_REVIVE, COMSIG_LIVING_DEATH, COMSIG_LIVING_UPDATE_BLOOD_STATUS)) to_chat(parent, span_userdanger("You feel your heart start beating normally again!")) var/mob/living/carbon/carbon_parent = parent @@ -95,9 +94,9 @@ var/mob/living/carbon/carbon_owner = owner - if(HAS_TRAIT(carbon_owner, TRAIT_NOBLOOD)) + if(!CAN_HAVE_BLOOD(carbon_owner)) return - carbon_owner.blood_volume = min(carbon_owner.blood_volume + (blood_loss * 0.5), BLOOD_VOLUME_MAXIMUM) + carbon_owner.adjust_blood_volume(blood_loss * 0.5) carbon_owner.remove_client_colour(REF(src)) add_colour = TRUE carbon_owner.adjustBruteLoss(-heal_brute) @@ -115,13 +114,21 @@ if(!COOLDOWN_FINISHED(src, heart_timer)) return - carbon_parent.blood_volume = max(carbon_parent.blood_volume - blood_loss, 0) + carbon_parent.adjust_blood_volume(-blood_loss) 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 +/datum/component/manual_heart/proc/on_update_blood_status(datum/source, had_blood, has_blood, new_blood_volume, old_blood_volume) + SIGNAL_HANDLER + + if (has_blood) + restart() + else + pause() + ///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 @@ -151,7 +158,7 @@ /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 + return !isnull(parent_heart) && CAN_HAVE_BLOOD(carbon_parent) && 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 @@ -169,7 +176,7 @@ ///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) + if(!istype(heart_haver) || !CAN_HAVE_BLOOD(heart_haver) || 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)) diff --git a/code/datums/components/splattercasting.dm b/code/datums/components/splattercasting.dm index cc11afbf789..bac08cdbbec 100644 --- a/code/datums/components/splattercasting.dm +++ b/code/datums/components/splattercasting.dm @@ -82,7 +82,7 @@ var/blood_cost = (cooldown_remaining - new_cooldown ) * COOLDOWN_TO_BLOOD_RATIO spell.StartCooldown(new_cooldown) - source.blood_volume -= blood_cost + source.adjust_blood_volume(-blood_cost) var/cost_desc diff --git a/code/datums/diseases/advance/symptoms/oxygen.dm b/code/datums/diseases/advance/symptoms/oxygen.dm index 79f0646a5c4..9966da2f76b 100644 --- a/code/datums/diseases/advance/symptoms/oxygen.dm +++ b/code/datums/diseases/advance/symptoms/oxygen.dm @@ -42,8 +42,8 @@ infected_mob.adjustOxyLoss(-7) if(prob(base_message_chance)) to_chat(infected_mob, span_notice("You realize you haven't been breathing.")) - if(regenerate_blood && infected_mob.blood_volume < BLOOD_VOLUME_NORMAL) - infected_mob.blood_volume += 1 + if(regenerate_blood) + infected_mob.adjust_blood_volume(1, maximum = BLOOD_VOLUME_NORMAL) else if(prob(base_message_chance)) to_chat(infected_mob, span_notice("Your lungs feel great.")) diff --git a/code/datums/elements/leeching_walk.dm b/code/datums/elements/leeching_walk.dm index a0d9e7a2706..6948c1b1df7 100644 --- a/code/datums/elements/leeching_walk.dm +++ b/code/datums/elements/leeching_walk.dm @@ -55,8 +55,7 @@ // Reduces duration of stuns/etc source.AdjustAllImmobility((-0.5 SECONDS) * delta_time) // Heals blood loss - if(source.blood_volume < BLOOD_VOLUME_NORMAL) - source.blood_volume += 2.5 * delta_time + source.adjust_blood_volume(2.5 * delta_time, maximum = BLOOD_VOLUME_NORMAL) // Slowly regulates your body temp source.adjust_bodytemperature((source.get_body_temp_normal() - source.bodytemperature) / 5) diff --git a/code/datums/mutations/touch.dm b/code/datums/mutations/touch.dm index 3020715d257..5325c709959 100644 --- a/code/datums/mutations/touch.dm +++ b/code/datums/mutations/touch.dm @@ -306,49 +306,43 @@ iter_wound.remove_wound() iter_wound.apply_wound(mendicant_transfer_limb) - if(HAS_TRAIT(mendicant, TRAIT_NOBLOOD)) + if(!CAN_HAVE_BLOOD(mendicant) || !CAN_HAVE_BLOOD(hurtguy)) return . // 10% base var/max_blood_transfer = (BLOOD_VOLUME_NORMAL * 0.10) * heal_multiplier // Too little blood - if(hurtguy.blood_volume < BLOOD_VOLUME_NORMAL) - var/max_blood_to_hurtguy = min(mendicant.blood_volume, BLOOD_VOLUME_NORMAL - hurtguy.blood_volume) - var/blood_to_hurtguy = min(max_blood_transfer, max_blood_to_hurtguy) - if(!blood_to_hurtguy) - return . - + if(hurtguy.get_blood_volume() < BLOOD_VOLUME_NORMAL) // We ignore incompatibility here. - if(!mendicant.transfer_blood_to(hurtguy, blood_to_hurtguy, forced = TRUE, ignore_incompatibility = TRUE)) + var/blood_transferred = mendicant.transfer_blood_to(hurtguy, max_blood_transfer, ignore_low_blood = TRUE, ignore_incompatibility = TRUE) + + if(!blood_transferred) return to_chat(mendicant, span_notice("Your veins (and brain) feel a bit lighter.")) . = TRUE // Because we do our own spin on it! if(hurtguy.get_blood_compatibility(mendicant) == FALSE) - hurtguy.adjustToxLoss((blood_to_hurtguy * 0.1) * pain_multiplier) // 1 dmg per 10 blood + hurtguy.adjustToxLoss((blood_transferred * 0.1) * pain_multiplier) // 1 dmg per 10 blood to_chat(hurtguy, span_notice("Your veins feel thicker, but they itch a bit.")) else to_chat(hurtguy, span_notice("Your veins feel thicker!")) return - if(hurtguy.blood_volume < BLOOD_VOLUME_MAXIMUM) + if(hurtguy.get_blood_volume() < BLOOD_VOLUME_EXCESS) return - // Too MUCH blood - var/max_blood_to_mendicant = BLOOD_VOLUME_EXCESS - hurtguy.blood_volume - var/blood_to_mendicant = min(max_blood_transfer, max_blood_to_mendicant) - // mender always gonna have blood - // We ignore incompatibility here. - if(!hurtguy.transfer_blood_to(mendicant, hurtguy.blood_volume - BLOOD_VOLUME_EXCESS, forced = TRUE, ignore_incompatibility = TRUE)) + var/blood_received = hurtguy.transfer_blood_to(mendicant, hurtguy.get_blood_volume() - BLOOD_VOLUME_EXCESS, ignore_incompatibility = TRUE) + + if(!blood_received) return to_chat(hurtguy, span_notice("Your veins don't feel quite so swollen anymore.")) . = TRUE // Because we do our own spin on it! if(mendicant.get_blood_compatibility(hurtguy) == FALSE) - mendicant.adjustToxLoss((blood_to_mendicant * 0.1) * pain_multiplier) // 1 dmg per 10 blood + mendicant.adjustToxLoss((blood_received * 0.1) * pain_multiplier) // 1 dmg per 10 blood to_chat(mendicant, span_notice("Your veins swell and itch!")) else to_chat(mendicant, span_notice("Your veins swell!")) diff --git a/code/datums/quirks/negative_quirks/blood_deficiency.dm b/code/datums/quirks/negative_quirks/blood_deficiency.dm index bacdcff80ea..7dfd01131dd 100644 --- a/code/datums/quirks/negative_quirks/blood_deficiency.dm +++ b/code/datums/quirks/negative_quirks/blood_deficiency.dm @@ -35,11 +35,11 @@ SIGNAL_HANDLER var/mob/living/carbon/human/human_holder = quirk_holder - if(human_holder.stat == DEAD || human_holder.blood_volume <= min_blood) + + if(human_holder.stat == DEAD) return - if(!HAS_TRAIT(quirk_holder, TRAIT_NOBLOOD)) - human_holder.blood_volume = max(min_blood, human_holder.blood_volume - human_holder.dna.species.blood_deficiency_drain_rate * seconds_per_tick) + human_holder.adjust_blood_volume(-human_holder.dna.species.blood_deficiency_drain_rate * seconds_per_tick, minimum = min_blood) /// Try to update the mail goodies to match the quirk holder's blood type. If we fail for whatever reason then it will just default to the initial O- blood pack that we start with. /datum/quirk/blooddeficiency/proc/update_mail(mob/living/carbon/human/human_quirk_holder, datum/blood_type/new_blood_type, update_cached_blood_dna_info) diff --git a/code/datums/quirks/negative_quirks/prosthetic_organ.dm b/code/datums/quirks/negative_quirks/prosthetic_organ.dm index 7877e177b39..dddba8a28e2 100644 --- a/code/datums/quirks/negative_quirks/prosthetic_organ.dm +++ b/code/datums/quirks/negative_quirks/prosthetic_organ.dm @@ -29,7 +29,7 @@ preferred_organ = GLOB.organ_choice[pick(GLOB.organ_choice)] var/list/possible_organ_slots = organ_slots.Copy() - if(HAS_TRAIT(human_holder, TRAIT_NOBLOOD)) + if(!CAN_HAVE_BLOOD(human_holder)) possible_organ_slots -= ORGAN_SLOT_HEART if(HAS_TRAIT(human_holder, TRAIT_NOBREATH)) possible_organ_slots -= ORGAN_SLOT_LUNGS diff --git a/code/datums/quirks/negative_quirks/tin_man.dm b/code/datums/quirks/negative_quirks/tin_man.dm index a75cbbedd76..054a5eecf06 100644 --- a/code/datums/quirks/negative_quirks/tin_man.dm +++ b/code/datums/quirks/negative_quirks/tin_man.dm @@ -17,7 +17,7 @@ ORGAN_SLOT_STOMACH = /obj/item/organ/stomach/cybernetic/surplus, ) var/list/possible_organ_slots = organ_slots.Copy() - if(HAS_TRAIT(human_holder, TRAIT_NOBLOOD)) + if(!CAN_HAVE_BLOOD(human_holder)) possible_organ_slots -= ORGAN_SLOT_HEART if(HAS_TRAIT(human_holder, TRAIT_NOBREATH)) possible_organ_slots -= ORGAN_SLOT_LUNGS diff --git a/code/datums/status_effects/debuffs/debuffs.dm b/code/datums/status_effects/debuffs/debuffs.dm index f2dd086188c..cf7611b8ca6 100644 --- a/code/datums/status_effects/debuffs/debuffs.dm +++ b/code/datums/status_effects/debuffs/debuffs.dm @@ -1069,8 +1069,7 @@ /datum/status_effect/midas_blight/tick(seconds_between_ticks) var/mob/living/carbon/human/victim = owner // We're transmuting blood, time to lose some. - if(victim.blood_volume > BLOOD_VOLUME_SURVIVE + 50 && !HAS_TRAIT(victim, TRAIT_NOBLOOD)) - victim.blood_volume -= 5 * seconds_between_ticks + victim.adjust_blood_volume(-5 * seconds_between_ticks, minimum = BLOOD_VOLUME_SURVIVE + 50) // This has been hell to try and balance so that you'll actually get anything out of it victim.reagents.add_reagent(/datum/reagent/gold/cursed, amount = seconds_between_ticks * goldscale, no_react = TRUE) var/current_gold_amount = victim.reagents.get_reagent_amount(/datum/reagent/gold, type_check = REAGENT_SUB_TYPE) diff --git a/code/datums/wounds/bones.dm b/code/datums/wounds/bones.dm index d69a86f03c9..f5493fd40d0 100644 --- a/code/datums/wounds/bones.dm +++ b/code/datums/wounds/bones.dm @@ -169,7 +169,7 @@ if(!victim || wounding_dmg < WOUND_MINIMUM_DAMAGE || !victim.can_bleed()) return - if(limb.body_zone == BODY_ZONE_CHEST && victim.blood_volume && prob(internal_bleeding_chance + wounding_dmg)) + if(limb.body_zone == BODY_ZONE_CHEST && victim.get_blood_volume() && prob(internal_bleeding_chance + wounding_dmg)) var/blood_bled = rand(1, wounding_dmg * (severity == WOUND_SEVERITY_CRITICAL ? 2 : 1.5)) // 12 brute toolbox can cause up to 18/24 bleeding with a severe/critical chest wound switch(blood_bled) if(1 to 6) @@ -559,6 +559,6 @@ if(limb.body_zone == BODY_ZONE_HEAD) . += "Cranial Trauma Detected: Patient will suffer random bouts of [severity == WOUND_SEVERITY_SEVERE ? "mild" : "severe"] brain traumas until bone is repaired." - else if(limb.body_zone == BODY_ZONE_CHEST && victim.blood_volume) + else if(limb.body_zone == BODY_ZONE_CHEST && CAN_HAVE_BLOOD(victim)) . += "Ribcage Trauma Detected: Further trauma to chest is likely to worsen internal bleeding until bone is repaired." . += "" diff --git a/code/datums/wounds/loss.dm b/code/datums/wounds/loss.dm index 779c181474c..ea6eefca691 100644 --- a/code/datums/wounds/loss.dm +++ b/code/datums/wounds/loss.dm @@ -49,7 +49,7 @@ set_limb(dismembered_part) second_wind() log_wound(victim, src) - if(dismembered_part.can_bleed() && wounding_type != WOUND_BURN && victim.blood_volume) + if(dismembered_part.can_bleed() && wounding_type != WOUND_BURN && victim.get_blood_volume()) victim.spray_blood(attack_direction, severity) dismembered_part.dismember(wounding_type == WOUND_BURN ? BURN : BRUTE, wounding_type = wounding_type) qdel(src) diff --git a/code/datums/wounds/pierce.dm b/code/datums/wounds/pierce.dm index 0d23a40c977..9e69454d608 100644 --- a/code/datums/wounds/pierce.dm +++ b/code/datums/wounds/pierce.dm @@ -44,13 +44,13 @@ /datum/wound/pierce/bleed/wound_injury(datum/wound/old_wound = null, attack_direction = null) set_blood_flow(initial_flow) - if(limb.can_bleed() && attack_direction && victim.blood_volume > BLOOD_VOLUME_OKAY) + if(limb.can_bleed() && attack_direction && victim.get_blood_volume() > BLOOD_VOLUME_OKAY) victim.spray_blood(attack_direction, severity) return ..() /datum/wound/pierce/bleed/receive_damage(wounding_type, wounding_dmg, wound_bonus) - if(victim.stat == DEAD || (wounding_dmg < 5) || !limb.can_bleed() || !victim.blood_volume || !prob(internal_bleeding_chance + wounding_dmg)) + if(victim.stat == DEAD || (wounding_dmg < 5) || !limb.can_bleed() || !victim.get_blood_volume() || !prob(internal_bleeding_chance + wounding_dmg)) return if(limb.current_gauze?.splint_factor) wounding_dmg *= (1 - limb.current_gauze.splint_factor) diff --git a/code/datums/wounds/slash.dm b/code/datums/wounds/slash.dm index 22318912bec..2111fa9c5a1 100644 --- a/code/datums/wounds/slash.dm +++ b/code/datums/wounds/slash.dm @@ -66,7 +66,7 @@ old_wound.clear_highest_scar() else set_blood_flow(initial_flow) - if(limb.can_bleed() && attack_direction && victim.blood_volume > BLOOD_VOLUME_OKAY) + if(limb.can_bleed() && attack_direction && victim.get_blood_volume() > BLOOD_VOLUME_OKAY) victim.spray_blood(attack_direction, severity) if(!highest_scar) diff --git a/code/game/machinery/iv_drip.dm b/code/game/machinery/iv_drip.dm index 15ffadec654..b4d4da9169f 100644 --- a/code/game/machinery/iv_drip.dm +++ b/code/game/machinery/iv_drip.dm @@ -264,7 +264,7 @@ return // If the human is losing too much blood, beep. - if(attached_mob.blood_volume < BLOOD_VOLUME_SAFE && prob(5)) + if(attached_mob.get_blood_volume(apply_modifiers = TRUE) < BLOOD_VOLUME_SAFE && prob(5)) audible_message(span_hear("[src] beeps loudly.")) playsound(loc, 'sound/machines/beep/twobeep_high.ogg', 50, TRUE) var/atom/movable/target = use_internal_storage ? src : reagent_container diff --git a/code/game/machinery/medical_kiosk.dm b/code/game/machinery/medical_kiosk.dm index 65052a2e833..afe4610f7cd 100644 --- a/code/game/machinery/medical_kiosk.dm +++ b/code/game/machinery/medical_kiosk.dm @@ -236,7 +236,7 @@ var/bleed_status = "Patient is not currently bleeding." var/blood_status = " Patient either has no blood, or does not require it to function." - var/blood_percent = round((patient.blood_volume / BLOOD_VOLUME_NORMAL) * 100) + var/blood_percent = round((patient.get_blood_volume(apply_modifiers = TRUE) / BLOOD_VOLUME_NORMAL) * 100) var/datum/blood_type/blood_type = patient.get_bloodtype() var/blood_name = "error" var/blood_warning = " " diff --git a/code/game/machinery/pipe/construction.dm b/code/game/machinery/pipe/construction.dm index 7f1f8de2c5b..786cac052a4 100644 --- a/code/game/machinery/pipe/construction.dm +++ b/code/game/machinery/pipe/construction.dm @@ -393,7 +393,7 @@ Buildable meters if(prob(20)) C.spew_organ() sleep(0.5 SECONDS) - C.blood_volume = 0 + C.set_blood_volume(0) return(OXYLOSS|BRUTELOSS) /obj/item/pipe/examine(mob/user) diff --git a/code/game/machinery/wall_healer.dm b/code/game/machinery/wall_healer.dm index cde06ebb98a..1d816405faf 100644 --- a/code/game/machinery/wall_healer.dm +++ b/code/game/machinery/wall_healer.dm @@ -490,7 +490,7 @@ var/brute_healing_now = round(min(initial(brute_healing) * 0.1, brute_healing, current_user.getBruteLoss()), DAMAGE_PRECISION) var/burn_healing_now = round(min(initial(burn_healing) * 0.1, burn_healing, current_user.getFireLoss()), DAMAGE_PRECISION) var/tox_healing_now = round(min(initial(tox_healing) * 0.1, tox_healing, current_user.getToxLoss()), DAMAGE_PRECISION) - var/blood_healing_now = HAS_TRAIT(current_user, TRAIT_NOBLOOD) ? 0 : round(min(initial(blood_healing) * 0.1, blood_healing, max(BLOOD_VOLUME_OKAY - current_user.blood_volume, 0)), 0.1) + var/blood_healing_now = round(min(initial(blood_healing) * 0.1, blood_healing, max(0, BLOOD_VOLUME_OKAY - current_user.get_blood_volume())), 0.1) var/cost = round(per_heal_cost * (brute_healing_now + burn_healing_now + tox_healing_now + blood_healing_now), 1) if(attempt_charge(src, current_user, extra_fees = cost) & COMPONENT_OBJ_CANCEL_CHARGE) @@ -512,8 +512,7 @@ amount_healed += current_user.adjustToxLoss(-tox_healing_now, required_biotype = MOB_ORGANIC) tox_healing -= tox_healing_now if(blood_healing_now) - current_user.blood_volume += blood_healing_now - amount_healed += blood_healing_now + amount_healed += current_user.adjust_blood_volume(blood_healing_now, maximum = BLOOD_VOLUME_OKAY) blood_healing -= blood_healing_now add_mob_blood(current_user) @@ -531,7 +530,7 @@ var/missed_brute_healing = brute_healing_now > 0 && !current_user.getBruteLoss() var/missed_burn_healing = burn_healing_now > 0 && !current_user.getFireLoss() var/missed_tox_healing = tox_healing_now > 0 && !current_user.getToxLoss() - var/missed_blood_healing = blood_healing_now > 0 && current_user.blood_volume >= BLOOD_VOLUME_OKAY + var/missed_blood_healing = blood_healing_now > 0 && current_user.get_blood_volume() >= BLOOD_VOLUME_OKAY if(missed_brute_healing || missed_burn_healing || missed_tox_healing || missed_blood_healing) to_chat(current_user, span_notice("Nothing happens. Seems like [src] needs to recharge.")) return diff --git a/code/game/objects/items/devices/blood_scanner.dm b/code/game/objects/items/devices/blood_scanner.dm index 5add88050f5..5292da18148 100644 --- a/code/game/objects/items/devices/blood_scanner.dm +++ b/code/game/objects/items/devices/blood_scanner.dm @@ -62,7 +62,7 @@ render_list += "Blood Type: [scanned_person?.dna?.blood_type]\n" if(oxy_loss > 50)//if they have knockout levels of suffocation damage render_list += "Warning: Hypoxic blood oxygen levels.\n" - if(scanned_person.blood_volume <= BLOOD_VOLUME_SAFE) + if(scanned_person.get_blood_volume(apply_modifiers = TRUE) <= BLOOD_VOLUME_SAFE) render_list += "Warning: Dangerously low blood flow.\n" if(tox_loss > 10) render_list += "Warning: Toxic buildup detected in bloodstream.\n" diff --git a/code/game/objects/items/devices/flashlight.dm b/code/game/objects/items/devices/flashlight.dm index f38aa9147cf..6b58419e50a 100644 --- a/code/game/objects/items/devices/flashlight.dm +++ b/code/game/objects/items/devices/flashlight.dm @@ -250,9 +250,11 @@ else . += span_info_ml("You press a finger to [patient.p_their()] gums:\n") - if(patient.blood_volume <= BLOOD_VOLUME_SAFE && patient.blood_volume > BLOOD_VOLUME_OKAY) + var/cached_blood_volume = patient.get_blood_volume(apply_modifiers = TRUE) + + if(cached_blood_volume <= BLOOD_VOLUME_SAFE && cached_blood_volume > BLOOD_VOLUME_OKAY) . += span_danger_ml("Color returns slowly!\n")//low blood - else if(patient.blood_volume <= BLOOD_VOLUME_OKAY) + else if(cached_blood_volume <= BLOOD_VOLUME_OKAY) . += span_danger_ml("Color does not return!\n")//critical blood else . += span_notice_ml("Color returns quickly.\n")//they're okay :D diff --git a/code/game/objects/items/devices/scanners/autopsy_scanner.dm b/code/game/objects/items/devices/scanners/autopsy_scanner.dm index d407ce6b8e3..863e4541636 100644 --- a/code/game/objects/items/devices/scanners/autopsy_scanner.dm +++ b/code/game/objects/items/devices/scanners/autopsy_scanner.dm @@ -187,31 +187,32 @@ // Blood Info if(HAS_TRAIT(scanned, TRAIT_HUSK)) - autopsy_information += "Blood can't be found, subject is husked by: " + autopsy_information += "Subject is husked by: " if(HAS_TRAIT_FROM(scanned, TRAIT_HUSK, BURN)) autopsy_information += "Severe burns.
" else if (HAS_TRAIT_FROM(scanned, TRAIT_HUSK, CHANGELING_DRAIN)) autopsy_information += "Desiccation, commonly caused by Changelings.
" else autopsy_information += "Unknown causes.
" - else - var/datum/blood_type/blood_type = scanned.get_bloodtype() - if(blood_type) - var/blood_percent = round((scanned.blood_volume / BLOOD_VOLUME_NORMAL) * 100) - var/blood_type_format - var/level_format - if(scanned.blood_volume <= BLOOD_VOLUME_SAFE && scanned.blood_volume > BLOOD_VOLUME_OKAY) - level_format = "LOW [blood_percent]%, [scanned.blood_volume] cl" - else if(scanned.blood_volume <= BLOOD_VOLUME_OKAY) - level_format = "CRITICAL [blood_percent]%, [scanned.blood_volume] cl" - else - level_format = "[blood_percent]%, [scanned.blood_volume] cl" - if(blood_type.get_type()) - blood_type_format = "type: [blood_type.get_type()]" - autopsy_information += "[blood_type.get_blood_name()] level: [level_format], [blood_type_format]
" - var/blood_alcohol_content = scanned.get_blood_alcohol_content() - if(blood_alcohol_content > 0) - autopsy_information += "↳ [blood_type?.get_blood_name() || "Blood"] alcohol content: [blood_alcohol_content]%
" + + var/datum/blood_type/blood_type = scanned.get_bloodtype() + if(blood_type) + var/cached_blood_volume = scanned.get_blood_volume(apply_modifiers = TRUE) + var/blood_percent = round((cached_blood_volume / BLOOD_VOLUME_NORMAL) * 100) + var/blood_type_format + var/level_format + if(cached_blood_volume <= BLOOD_VOLUME_SAFE && cached_blood_volume > BLOOD_VOLUME_OKAY) + level_format = "LOW [blood_percent]%, [cached_blood_volume] cl" + else if(cached_blood_volume <= BLOOD_VOLUME_OKAY) + level_format = "CRITICAL [blood_percent]%, [cached_blood_volume] cl" + else + level_format = "[blood_percent]%, [cached_blood_volume] cl" + if(blood_type.get_type()) + blood_type_format = "type: [blood_type.get_type()]" + autopsy_information += "[blood_type.get_blood_name()] level: [level_format], [blood_type_format]
" + var/blood_alcohol_content = scanned.get_blood_alcohol_content() + if(blood_alcohol_content > 0) + autopsy_information += "↳ [blood_type?.get_blood_name() || "Blood"] alcohol content: [blood_alcohol_content]%
" autopsy_information += "
" autopsy_information += "Chemical Data:
" diff --git a/code/game/objects/items/devices/scanners/health_analyzer.dm b/code/game/objects/items/devices/scanners/health_analyzer.dm index bb0f7fdde8d..62161578987 100644 --- a/code/game/objects/items/devices/scanners/health_analyzer.dm +++ b/code/game/objects/items/devices/scanners/health_analyzer.dm @@ -371,15 +371,16 @@ // Blood Level var/datum/blood_type/blood_type = target.get_bloodtype() if(blood_type) - var/blood_percent = round((target.blood_volume / BLOOD_VOLUME_NORMAL) * 100) + var/cached_blood_volume = target.get_blood_volume(apply_modifiers = TRUE) + var/blood_percent = round((cached_blood_volume / BLOOD_VOLUME_NORMAL) * 100) var/blood_type_format var/level_format - if(target.blood_volume <= BLOOD_VOLUME_SAFE && target.blood_volume > BLOOD_VOLUME_OKAY) - level_format = "LOW [blood_percent]%, [target.blood_volume] cl" + if(cached_blood_volume <= BLOOD_VOLUME_SAFE && cached_blood_volume > BLOOD_VOLUME_OKAY) + level_format = "LOW [blood_percent]%, [cached_blood_volume] cl" if (blood_type.restoration_chem) level_format = conditional_tooltip(level_format, "Recommendation: [blood_type.restoration_chem::name] supplement.", tochat) - else if(target.blood_volume <= BLOOD_VOLUME_OKAY) - level_format = "CRITICAL [blood_percent]%, [target.blood_volume] cl" + else if(cached_blood_volume <= BLOOD_VOLUME_OKAY) + level_format = "CRITICAL [blood_percent]%, [cached_blood_volume] cl" var/recommendation = list() if (blood_type.restoration_chem) recommendation += "[blood_type.restoration_chem::name] supplement" @@ -391,7 +392,7 @@ recommendation += "immediate [blood_type.get_blood_name()] transufion" level_format = conditional_tooltip(level_format, "Recommendation: [english_list(recommendation, and_text = " or ")].", tochat) else - level_format = "[blood_percent]%, [target.blood_volume] cl" + level_format = "[blood_percent]%, [cached_blood_volume] cl" if (blood_type.get_type()) blood_type_format = "type: [blood_type.get_type()]" @@ -401,7 +402,7 @@ compatible_types_readable |= initial(comp_blood_type.name) blood_type_format = span_tooltip("Can receive from types [english_list(compatible_types_readable)].", blood_type_format) - render_list += "[blood_type.get_blood_name()] level: [level_format], [blood_type_format]
" + render_list += "[blood_type.get_blood_name()] level: [level_format], [blood_type_format]
" var/blood_alcohol_content = target.get_blood_alcohol_content() if(blood_alcohol_content > 0) diff --git a/code/game/objects/items/soulscythe.dm b/code/game/objects/items/soulscythe.dm index b4699dd3fa5..1335771bca1 100644 --- a/code/game/objects/items/soulscythe.dm +++ b/code/game/objects/items/soulscythe.dm @@ -138,15 +138,15 @@ return TRUE /obj/item/soulscythe/proc/use_blood(amount = 0, message = TRUE) - if(amount > soul.blood_volume) + if(amount > soul.get_blood_volume()) if(message) to_chat(soul, span_warning("Not enough blood!")) return FALSE - soul.blood_volume -= amount + soul.adjust_blood_volume(-amount) return TRUE /obj/item/soulscythe/proc/give_blood(amount) - soul.blood_volume = min(MAX_BLOOD_LEVEL, soul.blood_volume + amount) + soul.adjust_blood_volume(amount, maximum = MAX_BLOOD_LEVEL) /obj/item/soulscythe/proc/on_resist(mob/living/user) SIGNAL_HANDLER @@ -256,7 +256,7 @@ gender = NEUTER mob_biotypes = MOB_SPIRIT faction = list() - blood_volume = MAX_BLOOD_LEVEL + default_blood_volume = MAX_BLOOD_LEVEL hud_type = /datum/hud/soulscythe /mob/living/basic/soulscythe/Initialize(mapload) @@ -266,7 +266,7 @@ /mob/living/basic/soulscythe/proc/on_life(datum/source, seconds_per_tick, times_fired) // done like this because there's no need to go through all of life since the item does the work anyways if(stat == CONSCIOUS) - blood_volume = min(MAX_BLOOD_LEVEL, blood_volume + round(1 * seconds_per_tick)) + adjust_blood_volume(round(1 * seconds_per_tick), maximum = MAX_BLOOD_LEVEL) return COMPONENT_LIVING_CANCEL_LIFE_PROCESSING /// Special projectile for the soulscythe. diff --git a/code/modules/antagonists/abductor/equipment/glands/heal.dm b/code/modules/antagonists/abductor/equipment/glands/heal.dm index ae3be2d40b4..c4e29565fa0 100644 --- a/code/modules/antagonists/abductor/equipment/glands/heal.dm +++ b/code/modules/antagonists/abductor/equipment/glands/heal.dm @@ -68,8 +68,8 @@ if(tox_amount > 10) replace_blood() return - if(owner.blood_volume < BLOOD_VOLUME_OKAY) - owner.blood_volume = BLOOD_VOLUME_NORMAL + if(owner.get_blood_volume() < BLOOD_VOLUME_OKAY) + owner.set_blood_volume(BLOOD_VOLUME_NORMAL) to_chat(owner, span_warning("You feel your blood pulsing within you.")) return @@ -196,8 +196,8 @@ owner.Stun(15) owner.adjustToxLoss(-15, forced = TRUE) - owner.blood_volume = min(BLOOD_VOLUME_NORMAL, owner.blood_volume + 20) - if(owner.blood_volume < BLOOD_VOLUME_NORMAL) + owner.adjust_blood_volume(20, maximum = BLOOD_VOLUME_NORMAL) + if(owner.get_blood_volume() < BLOOD_VOLUME_NORMAL) keep_going = TRUE if(owner.getToxLoss()) diff --git a/code/modules/antagonists/cult/blood_magic.dm b/code/modules/antagonists/cult/blood_magic.dm index 504b4519aba..960be7f9070 100644 --- a/code/modules/antagonists/cult/blood_magic.dm +++ b/code/modules/antagonists/cult/blood_magic.dm @@ -752,7 +752,7 @@ if(!ishuman(target)) return var/mob/living/carbon/human/human_bloodbag = target - if(HAS_TRAIT(human_bloodbag, TRAIT_NOBLOOD)) + if(!CAN_HAVE_BLOOD(human_bloodbag)) human_bloodbag.balloon_alert(user, "no blood!") return if(human_bloodbag.stat == DEAD) @@ -805,17 +805,18 @@ /// used to ensure the proc returns TRUE if we completely restore an undamaged persons blood var/blood_donor = FALSE - if(human_bloodbag.blood_volume < BLOOD_VOLUME_SAFE) - var/blood_needed = BLOOD_VOLUME_SAFE - human_bloodbag.blood_volume + var/cached_blood_volume = human_bloodbag.get_blood_volume() + if(cached_blood_volume < BLOOD_VOLUME_SAFE) + var/blood_needed = BLOOD_VOLUME_SAFE - cached_blood_volume /// how much blood we are capable of restoring, based on spell charges var/blood_bank = USES_TO_BLOOD * uses if(blood_bank < blood_needed) - human_bloodbag.blood_volume += blood_bank + human_bloodbag.adjust_blood_volume(blood_bank) to_chat(user,span_danger("You use the last of your blood rites to restore what blood you could!")) uses = 0 return TRUE blood_donor = TRUE - human_bloodbag.blood_volume = BLOOD_VOLUME_SAFE + human_bloodbag.set_blood_volume(BLOOD_VOLUME_SAFE) uses -= round(blood_needed / USES_TO_BLOOD) to_chat(user,span_warning("Your blood rites have restored [human_bloodbag == user ? "your" : "[human_bloodbag.p_their()]"] blood to safe levels!")) @@ -858,10 +859,10 @@ if(human_bloodbag.has_status_effect(/datum/status_effect/speech/slurring/cult)) to_chat(user,span_danger("[human_bloodbag.p_Their()] blood has been tainted by an even stronger form of blood magic, it's no use to us like this!")) return FALSE - if(human_bloodbag.blood_volume <= BLOOD_VOLUME_SAFE) + if(human_bloodbag.get_blood_volume() <= BLOOD_VOLUME_SAFE) to_chat(user,span_warning("[human_bloodbag.p_Theyre()] missing too much blood - you cannot drain [human_bloodbag.p_them()] further!")) return FALSE - human_bloodbag.blood_volume -= BLOOD_DRAIN_GAIN * USES_TO_BLOOD + human_bloodbag.adjust_blood_volume(-BLOOD_DRAIN_GAIN * USES_TO_BLOOD) uses += BLOOD_DRAIN_GAIN user.Beam(human_bloodbag, icon_state="drainbeam", time = 1 SECONDS) playsound(get_turf(human_bloodbag), 'sound/effects/magic/enter_blood.ogg', 50) diff --git a/code/modules/antagonists/cult/runes.dm b/code/modules/antagonists/cult/runes.dm index aa5b3ef3885..d4a2b299e02 100644 --- a/code/modules/antagonists/cult/runes.dm +++ b/code/modules/antagonists/cult/runes.dm @@ -930,7 +930,7 @@ GLOBAL_VAR_INIT(narsie_summon_count, 0) color = "#FC9B54" set_light(6, 1, color) for(var/mob/living/target in viewers(T)) - if(!IS_CULTIST(target) && target.blood_volume) + if(!IS_CULTIST(target) && CAN_HAVE_BLOOD(target)) if(target.can_block_magic(charge_cost = 0)) continue to_chat(target, span_cult_large("Your blood boils in your veins!")) @@ -955,7 +955,7 @@ GLOBAL_VAR_INIT(narsie_summon_count, 0) /obj/effect/rune/blood_boil/proc/do_area_burn(turf/T, multiplier) set_light(6, 1, color) for(var/mob/living/target in viewers(T)) - if(!IS_CULTIST(target) && target.blood_volume) + if(!IS_CULTIST(target) && target.get_blood_volume()) if(target.can_block_magic(charge_cost = 0)) continue target.take_overall_damage(tick_damage*multiplier, tick_damage*multiplier) diff --git a/code/modules/antagonists/heretic/knowledge/rust_lore.dm b/code/modules/antagonists/heretic/knowledge/rust_lore.dm index 7db964c5723..44b26bb5a45 100644 --- a/code/modules/antagonists/heretic/knowledge/rust_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/rust_lore.dm @@ -307,7 +307,8 @@ need_mob_update += source.adjustToxLoss(-base_heal_amt, updating_health = FALSE, forced = TRUE) need_mob_update += source.adjustOxyLoss(-base_heal_amt, updating_health = FALSE) need_mob_update += source.adjustStaminaLoss(-base_heal_amt * 4, updating_stamina = FALSE) - if(source.blood_volume < BLOOD_VOLUME_NORMAL) - source.blood_volume += base_heal_amt + + source.adjust_blood_volume(base_heal_amt, maximum = BLOOD_VOLUME_NORMAL) + if(need_mob_update) source.updatehealth() diff --git a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_buff.dm b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_buff.dm index eb51b632dd9..23423ab86ad 100644 --- a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_buff.dm +++ b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_buff.dm @@ -23,8 +23,8 @@ /datum/status_effect/unholy_determination/on_apply() owner.add_traits(list(TRAIT_COAGULATING, TRAIT_NOCRITDAMAGE, TRAIT_NOSOFTCRIT), TRAIT_STATUS_EFFECT(id)) - if(owner.blood_volume < BLOOD_VOLUME_OKAY) - owner.blood_volume = BLOOD_VOLUME_OKAY + if(owner.get_blood_volume() < BLOOD_VOLUME_OKAY) + owner.set_blood_volume(BLOOD_VOLUME_OKAY) return TRUE /datum/status_effect/unholy_determination/on_remove() @@ -94,11 +94,10 @@ * Slow and stop any blood loss the owner's experiencing. */ /datum/status_effect/unholy_determination/proc/adjust_bleed_wounds(seconds_between_ticks) - if(!iscarbon(owner) || !owner.blood_volume) + if(!iscarbon(owner) || !CAN_HAVE_BLOOD(owner)) return - if(owner.blood_volume < BLOOD_VOLUME_NORMAL) - owner.blood_volume = owner.blood_volume + (2 * seconds_between_ticks) + owner.adjust_blood_volume(2 * seconds_between_ticks, maximum = BLOOD_VOLUME_NORMAL) var/mob/living/carbon/carbon_owner = owner var/datum/wound/bloodiest_wound diff --git a/code/modules/antagonists/heretic/magic/apetravulnera.dm b/code/modules/antagonists/heretic/magic/apetravulnera.dm index e80d0891184..c0398b2b745 100644 --- a/code/modules/antagonists/heretic/magic/apetravulnera.dm +++ b/code/modules/antagonists/heretic/magic/apetravulnera.dm @@ -27,7 +27,7 @@ if(IS_HERETIC_OR_MONSTER(cast_on)) return FALSE - if(!cast_on.blood_volume) + if(!CAN_HAVE_BLOOD(cast_on)) return FALSE if(cast_on.can_block_magic(antimagic_flags)) diff --git a/code/modules/antagonists/heretic/magic/blood_cleave.dm b/code/modules/antagonists/heretic/magic/blood_cleave.dm index d5317f23e34..065286f490e 100644 --- a/code/modules/antagonists/heretic/magic/blood_cleave.dm +++ b/code/modules/antagonists/heretic/magic/blood_cleave.dm @@ -36,7 +36,7 @@ ) continue - if(!victim.blood_volume) + if(!CAN_HAVE_BLOOD(victim)) continue victim.visible_message( diff --git a/code/modules/antagonists/heretic/magic/blood_siphon.dm b/code/modules/antagonists/heretic/magic/blood_siphon.dm index c63b17a703f..42a1e206088 100644 --- a/code/modules/antagonists/heretic/magic/blood_siphon.dm +++ b/code/modules/antagonists/heretic/magic/blood_siphon.dm @@ -43,12 +43,7 @@ cast_on.adjustBruteLoss(20) living_owner.adjustBruteLoss(-20) - if(!cast_on.blood_volume || !living_owner.blood_volume) - return TRUE - - cast_on.blood_volume -= 20 - if(living_owner.blood_volume < BLOOD_VOLUME_MAXIMUM) // we dont want to explode from casting - living_owner.blood_volume += 20 + cast_on.transfer_blood_to(living_owner, 20, ignore_low_blood = TRUE, ignore_incompatibility = TRUE, transfer_viruses = FALSE) if(!iscarbon(cast_on) || !iscarbon(owner)) return TRUE diff --git a/code/modules/antagonists/heretic/magic/crimson_cleave.dm b/code/modules/antagonists/heretic/magic/crimson_cleave.dm index 411409ee933..8a1e7bd3732 100644 --- a/code/modules/antagonists/heretic/magic/crimson_cleave.dm +++ b/code/modules/antagonists/heretic/magic/crimson_cleave.dm @@ -55,10 +55,7 @@ victim.apply_damage(15, BRUTE, wound_bonus = CANT_WOUND) living_owner.adjustBruteLoss(-15) - if(victim.blood_volume) - victim.blood_volume -= 15 - if(living_owner.blood_volume && living_owner.blood_volume < (BLOOD_VOLUME_MAXIMUM - 50)) - living_owner.blood_volume += 15 + victim.transfer_blood_to(living_owner, 15, ignore_low_blood = TRUE, ignore_incompatibility = TRUE, transfer_viruses = FALSE) new /obj/effect/temp_visual/cleave(get_turf(victim)) diff --git a/code/modules/antagonists/heretic/status_effects/buffs.dm b/code/modules/antagonists/heretic/status_effects/buffs.dm index bc16a2b0ec3..2a3043c318a 100644 --- a/code/modules/antagonists/heretic/status_effects/buffs.dm +++ b/code/modules/antagonists/heretic/status_effects/buffs.dm @@ -120,7 +120,7 @@ carbie.adjustFireLoss(-heal_amt) else carbie.adjustBruteLoss(-heal_amt) - carbie.blood_volume += carbie.blood_volume >= BLOOD_VOLUME_NORMAL ? 0 : heal_amt*3 + carbie.adjust_blood_volume(heal_amt * 3, maximum = BLOOD_VOLUME_NORMAL) /atom/movable/screen/alert/status_effect/crucible_soul diff --git a/code/modules/antagonists/heretic/status_effects/heretic_passive.dm b/code/modules/antagonists/heretic/status_effects/heretic_passive.dm index 3fe6f5c89f7..0a7a0f8b978 100644 --- a/code/modules/antagonists/heretic/status_effects/heretic_passive.dm +++ b/code/modules/antagonists/heretic/status_effects/heretic_passive.dm @@ -303,8 +303,7 @@ var/healed_amount = owner.heal_overall_damage(2, 2, updating_health = FALSE) healed_amount += owner.adjustOxyLoss(-2, FALSE) healed_amount += owner.adjustToxLoss(-2, FALSE, TRUE) - if(!HAS_TRAIT(owner, TRAIT_NOBLOOD)) - owner.blood_volume += 2.5 + owner.adjust_blood_volume(2.5) if(!iscarbon(owner)) return var/mob/living/carbon/carbon_eater = owner @@ -536,8 +535,7 @@ var/stun_reduction = 0.5 * passive_level * delta_time source.AdjustAllImmobility(-stun_reduction) // Heals blood loss - if(source.blood_volume < BLOOD_VOLUME_NORMAL) - source.blood_volume += 2.5 * delta_time + source.adjust_blood_volume(2.5 * delta_time, maximum = BLOOD_VOLUME_NORMAL) for(var/datum/reagent/reagent as anything in source.reagents.reagent_list) source.reagents.remove_reagent(reagent.type, 2 * reagent.purge_multiplier * REM * seconds_per_tick) diff --git a/code/modules/bitrunning/components/netpod_healing.dm b/code/modules/bitrunning/components/netpod_healing.dm index c114777ff4c..17fd2a4859b 100644 --- a/code/modules/bitrunning/components/netpod_healing.dm +++ b/code/modules/bitrunning/components/netpod_healing.dm @@ -38,8 +38,7 @@ need_mob_update += owner.adjustFireLoss(-BASE_HEAL * seconds_per_tick, updating_health = FALSE) need_mob_update += owner.adjustToxLoss(-BASE_HEAL * seconds_per_tick, updating_health = FALSE, forced = TRUE) - if(owner.blood_volume < BLOOD_VOLUME_NORMAL) - owner.blood_volume += BASE_HEAL * seconds_per_tick + owner.adjust_blood_volume(BASE_HEAL * seconds_per_tick, maximum = BLOOD_VOLUME_NORMAL) if(need_mob_update) owner.updatehealth() diff --git a/code/modules/clothing/neck/_neck.dm b/code/modules/clothing/neck/_neck.dm index 11ec3c534e6..1800d99cd0b 100644 --- a/code/modules/clothing/neck/_neck.dm +++ b/code/modules/clothing/neck/_neck.dm @@ -313,7 +313,7 @@ heart_noises = FALSE else if(having_heart_attack) render_list += "You hear a rapid, irregular heartbeat.\n" - else if(heart.damage > 10 || carbon_patient.blood_volume <= BLOOD_VOLUME_OKAY) + else if(heart.damage > 10 || carbon_patient.get_blood_volume(apply_modifiers = TRUE) <= BLOOD_VOLUME_OKAY) render_list += "You hear a weak heartbeat.\n"//their heart is damaged, or they have critical blood else render_list += "You hear a healthy heartbeat.\n"//they're okay :D @@ -361,8 +361,10 @@ render_list += span_info("You carefully press your fingers to [carbon_patient]'s [body_part]:\n") user.visible_message(span_notice("[user] presses their fingers against [carbon_patient]'s [body_part]."), ignored_mobs = user) + var/cached_blood_volume = carbon_patient.get_blood_volume(apply_modifiers = TRUE) + //assess pulse (heart & blood level) - if(isnull(heart) || !heart.is_beating() || carbon_patient.blood_volume <= BLOOD_VOLUME_OKAY || carbon_patient.stat == DEAD) + if(isnull(heart) || !heart.is_beating() || cached_blood_volume <= BLOOD_VOLUME_OKAY || carbon_patient.stat == DEAD) render_list += "You can't find a pulse!\n"//they're dead, their heart isn't beating, or they have critical blood else if(having_heart_attack) @@ -372,7 +374,7 @@ else heart_strength = span_notice("regular")//they're okay :D - if((carbon_patient.blood_volume <= BLOOD_VOLUME_SAFE && carbon_patient.blood_volume > BLOOD_VOLUME_OKAY) || having_heart_attack) + if((cached_blood_volume <= BLOOD_VOLUME_SAFE && cached_blood_volume > BLOOD_VOLUME_OKAY) || having_heart_attack) pulse_pressure = span_danger("thready")//low blood else pulse_pressure = span_notice("strong")//they're okay :D diff --git a/code/modules/hallucination/blood_flow.dm b/code/modules/hallucination/blood_flow.dm index 0ae641d34a0..347e2da9414 100644 --- a/code/modules/hallucination/blood_flow.dm +++ b/code/modules/hallucination/blood_flow.dm @@ -3,6 +3,8 @@ hallucination_tier = HALLUCINATION_TIER_COMMON /// The bleeding hallucination's image var/image/bleeding + /// Ref to the bleeding bodypart, necessary to unregister signals + var/obj/item/bodypart/bleeding_bodypart /datum/hallucination/blood_flow/start() if(!hallucinator.client || !iscarbon(hallucinator)) @@ -22,22 +24,24 @@ if(isnull(picked)) return FALSE - feedback_details += "Bleeding: [picked]" + bleeding_bodypart = picked - RegisterSignals(picked, list(COMSIG_QDELETING, COMSIG_BODYPART_REMOVED), PROC_REF(stop_bleeding)) - RegisterSignal(hallucinator, SIGNAL_ADDTRAIT(TRAIT_NOBLOOD), PROC_REF(stop_bleeding)) + feedback_details += "Bleeding: [bleeding_bodypart]" - to_chat(hallucinator, span_warning("Your [picked.plaintext_zone] looses a spray of blood!")) + RegisterSignals(bleeding_bodypart, list(COMSIG_QDELETING, COMSIG_BODYPART_REMOVED), PROC_REF(stop_bleeding)) + RegisterSignal(hallucinator, COMSIG_LIVING_UPDATE_BLOOD_STATUS, PROC_REF(stop_bleeding)) + + to_chat(hallucinator, span_warning("Your [bleeding_bodypart.plaintext_zone] looses a spray of blood!")) var/bleed_duration = rand(16 SECONDS, 40 SECONDS) - addtimer(CALLBACK(src, PROC_REF(stop_bleeding), picked), bleed_duration) + addtimer(CALLBACK(src, PROC_REF(stop_bleeding)), bleed_duration) if(prob(25)) - addtimer(CALLBACK(src, PROC_REF(by_god), picked), bleed_duration * pick(0.5, 0.66)) + addtimer(CALLBACK(src, PROC_REF(by_god)), bleed_duration * pick(0.5, 0.66)) stamina_loop() hallucinator.playsound_local(get_turf(hallucinator), pick('sound/effects/wounds/blood1.ogg', 'sound/effects/wounds/blood2.ogg', 'sound/effects/wounds/blood3.ogg'), 50, TRUE) bleeding = image( icon = 'icons/mob/effects/bleed_overlays.dmi', - icon_state = "[picked.body_zone]_[pick(2, 3)]", + icon_state = "[bleeding_bodypart.body_zone]_[pick(2, 3)]", loc = hallucinator, ) bleeding.color = carb_hallucinator.get_bloodtype()?.get_wound_color(carb_hallucinator) || BLOOD_COLOR_RED @@ -49,18 +53,23 @@ hallucinator.client?.images -= bleeding return ..() -/datum/hallucination/blood_flow/proc/by_god(obj/item/bodypart/picked) - if(QDELETED(src) || QDELETED(hallucinator) || QDELETED(picked)) +/datum/hallucination/blood_flow/proc/by_god() + if(QDELETED(src) || QDELETED(hallucinator) || QDELETED(bleeding_bodypart)) return - to_chat(hallucinator, span_warning("The blood doesn't stop flowing, yet [picked.plaintext_zone] doesn't seem to hurt...")) + to_chat(hallucinator, span_warning("The blood doesn't stop flowing, yet [bleeding_bodypart.plaintext_zone] doesn't seem to hurt...")) -/datum/hallucination/blood_flow/proc/stop_bleeding(obj/item/bodypart/source) +/datum/hallucination/blood_flow/proc/on_update_blood_status(datum/source, had_blood, has_blood, old_blood_volume, new_blood_volume) SIGNAL_HANDLER - UnregisterSignal(source, list(COMSIG_QDELETING, COMSIG_BODYPART_REMOVED)) - UnregisterSignal(hallucinator, SIGNAL_ADDTRAIT(TRAIT_NOBLOOD)) - if(!QDELETED(source)) - to_chat(hallucinator, span_warning("Your [source.plaintext_zone] stops bleeding.")) + if (!has_blood) + stop_bleeding() + +/datum/hallucination/blood_flow/proc/stop_bleeding() + SIGNAL_HANDLER + UnregisterSignal(bleeding_bodypart, list(COMSIG_QDELETING, COMSIG_BODYPART_REMOVED)) + UnregisterSignal(hallucinator, COMSIG_LIVING_UPDATE_BLOOD_STATUS) + if(!QDELETED(bleeding_bodypart)) + to_chat(hallucinator, span_warning("Your [bleeding_bodypart.plaintext_zone] stops bleeding.")) if(!QDELETED(src)) qdel(src) diff --git a/code/modules/lost_crew/damages/_damages.dm b/code/modules/lost_crew/damages/_damages.dm index 8478e51ec71..226d1cec473 100644 --- a/code/modules/lost_crew/damages/_damages.dm +++ b/code/modules/lost_crew/damages/_damages.dm @@ -88,7 +88,7 @@ body_data += decay.type // Simulate bloodloss by dragging/moving - victim.blood_volume = max(victim.blood_volume - victim.bleed_drag_amount() * rand(20, 100), 0) + victim.adjust_blood_volume(-victim.bleed_drag_amount() * rand(20, 100)) set_death_date(victim) death_lore += area_lore + " " + cause_of_death.cause_of_death diff --git a/code/modules/mob/living/basic/alien/_alien.dm b/code/modules/mob/living/basic/alien/_alien.dm index eec5ae92ee2..af0bd349172 100644 --- a/code/modules/mob/living/basic/alien/_alien.dm +++ b/code/modules/mob/living/basic/alien/_alien.dm @@ -47,7 +47,7 @@ unsuitable_heat_damage = 20 ai_controller = /datum/ai_controller/basic_controller/alien - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL ///List of loot items to drop when deleted, if this is set then we apply DEL_ON_DEATH var/list/loot diff --git a/code/modules/mob/living/basic/clown/clown.dm b/code/modules/mob/living/basic/clown/clown.dm index c4544b8724c..65352a2de86 100644 --- a/code/modules/mob/living/basic/clown/clown.dm +++ b/code/modules/mob/living/basic/clown/clown.dm @@ -25,7 +25,7 @@ habitable_atmos = list("min_oxy" = 5, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) minimum_survivable_temperature = (T0C - 10) maximum_survivable_temperature = (T0C + 100) - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL faction = list(FACTION_CLOWN) ai_controller = /datum/ai_controller/basic_controller/clown ///list of stuff we drop on death diff --git a/code/modules/mob/living/basic/cytology/vatbeast.dm b/code/modules/mob/living/basic/cytology/vatbeast.dm index d9d1c364802..5b6460eebb2 100644 --- a/code/modules/mob/living/basic/cytology/vatbeast.dm +++ b/code/modules/mob/living/basic/cytology/vatbeast.dm @@ -27,7 +27,7 @@ lighting_cutoff_blue = 20 ai_controller = /datum/ai_controller/basic_controller/vatbeast faction = list(FACTION_HOSTILE) - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL /// What can you feed a vatbeast to tame it? var/static/list/enjoyed_food = list( /obj/item/food/carrotfries, diff --git a/code/modules/mob/living/basic/farm_animals/cow/_cow.dm b/code/modules/mob/living/basic/farm_animals/cow/_cow.dm index a07a70d0172..72d1d892e62 100644 --- a/code/modules/mob/living/basic/farm_animals/cow/_cow.dm +++ b/code/modules/mob/living/basic/farm_animals/cow/_cow.dm @@ -25,7 +25,7 @@ health = 50 maxHealth = 50 gold_core_spawnable = FRIENDLY_SPAWN - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL ai_controller = /datum/ai_controller/basic_controller/cow /// what this cow munches on, and what can be used to tame it. var/list/food_types = list(/obj/item/food/grown/wheat) diff --git a/code/modules/mob/living/basic/farm_animals/deer/deer.dm b/code/modules/mob/living/basic/farm_animals/deer/deer.dm index dc27c82dd82..b726ae65f42 100644 --- a/code/modules/mob/living/basic/farm_animals/deer/deer.dm +++ b/code/modules/mob/living/basic/farm_animals/deer/deer.dm @@ -19,7 +19,7 @@ attack_sound = 'sound/items/weapons/punch1.ogg' health = 75 maxHealth = 75 - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL ai_controller = /datum/ai_controller/basic_controller/deer /// Things that will scare us into being stationary. Vehicles are scary to deers because they might have headlights. var/static/list/stationary_scary_things = list(/obj/vehicle) diff --git a/code/modules/mob/living/basic/farm_animals/goat/_goat.dm b/code/modules/mob/living/basic/farm_animals/goat/_goat.dm index 7b177c5c17d..03582ff2575 100644 --- a/code/modules/mob/living/basic/farm_animals/goat/_goat.dm +++ b/code/modules/mob/living/basic/farm_animals/goat/_goat.dm @@ -31,7 +31,7 @@ minimum_survivable_temperature = COLD_ROOM_TEMP - 75 // enough so that they can survive the cold room spawn with plenty of room for comfort - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL ai_controller = /datum/ai_controller/basic_controller/goat /// How often will we develop an evil gleam in our eye? diff --git a/code/modules/mob/living/basic/farm_animals/pig.dm b/code/modules/mob/living/basic/farm_animals/pig.dm index 3a4ae888512..7654933c0d0 100644 --- a/code/modules/mob/living/basic/farm_animals/pig.dm +++ b/code/modules/mob/living/basic/farm_animals/pig.dm @@ -25,7 +25,7 @@ health = 50 maxHealth = 50 gold_core_spawnable = FRIENDLY_SPAWN - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL ai_controller = /datum/ai_controller/basic_controller/pig /datum/emote/pig diff --git a/code/modules/mob/living/basic/farm_animals/pony.dm b/code/modules/mob/living/basic/farm_animals/pony.dm index 09166933e8d..6598b70606c 100644 --- a/code/modules/mob/living/basic/farm_animals/pony.dm +++ b/code/modules/mob/living/basic/farm_animals/pony.dm @@ -22,7 +22,7 @@ health = 50 maxHealth = 50 gold_core_spawnable = FRIENDLY_SPAWN - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL ai_controller = /datum/ai_controller/basic_controller/pony /// Do we register a unique rider? var/unique_tamer = FALSE diff --git a/code/modules/mob/living/basic/farm_animals/sheep.dm b/code/modules/mob/living/basic/farm_animals/sheep.dm index de4999a561d..bbbf7e58875 100644 --- a/code/modules/mob/living/basic/farm_animals/sheep.dm +++ b/code/modules/mob/living/basic/farm_animals/sheep.dm @@ -23,7 +23,7 @@ health = 50 maxHealth = 50 gold_core_spawnable = FRIENDLY_SPAWN - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL ai_controller = /datum/ai_controller/basic_controller/sheep /// Were we sacrificed by cultists? diff --git a/code/modules/mob/living/basic/pets/pet.dm b/code/modules/mob/living/basic/pets/pet.dm index e4882a67ff7..b3ffa64d9ec 100644 --- a/code/modules/mob/living/basic/pets/pet.dm +++ b/code/modules/mob/living/basic/pets/pet.dm @@ -2,7 +2,7 @@ icon = 'icons/mob/simple/pets.dmi' mob_size = MOB_SIZE_SMALL mob_biotypes = MOB_ORGANIC|MOB_BEAST - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL basic_mob_flags = SENDS_DEATH_MOODLETS /// if the mob is protected from being renamed by collars. var/unique_pet = FALSE diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm index c1721fbba6e..e3d940141d4 100644 --- a/code/modules/mob/living/blood.dm +++ b/code/modules/mob/living/blood.dm @@ -6,55 +6,160 @@ BLOOD SYSTEM ****************************************************/ +/// Returns whether this mob can have blood. +/// Use the CAN_HAVE_BLOOD(mob) macro instead, this is used to update the cached value. +/mob/living/proc/can_have_blood() + return default_blood_volume > 0 + +/mob/living/carbon/can_have_blood() + return !HAS_TRAIT(src, TRAIT_NOBLOOD) + +/// Returns the blood volume of the mob. +/// Apply modifiers when reading blood volume for oxyloss damage, HUDs and analyzers. +/// Don't apply modifiers when using blood itself, like in spells and reagent transfers. +/mob/living/proc/get_blood_volume(apply_modifiers = FALSE) + return CAN_HAVE_BLOOD(src) ? blood_volume : 0 // Overriding blood setting code can cause blood_volume to be non-zero even when a mob shouldn't have blood. + +/mob/living/carbon/get_blood_volume(apply_modifiers = FALSE) + if (!CAN_HAVE_BLOOD(src)) + return 0 // Overriding blood setting code can cause blood_volume to be non-zero even when a mob shouldn't have blood. + if (!apply_modifiers) + return blood_volume // Default behavior, returns the real blood volume. + if (HAS_TRAIT(src, TRAIT_GODMODE)) + return default_blood_volume // Makes TRAIT_GODMODE grant immunity to the effects of bleeding. (oxyloss, passing out, etc.) + + var/amount = blood_volume + + // Handled here instead of in the saline reagent datum, because this way the modification order is consistent. + // E.g. if you have an effect that modifies blood volume over the dilution cap, then saline should do nothing. + var/datum/reagent/medicine/salglu_solution/saline = reagents?.has_reagent(/datum/reagent/medicine/salglu_solution) + if (saline && amount < saline.dilution_cap) + var/datum/blood_type/blood_type = get_bloodtype() + if (blood_type?.restoration_chem == saline.required_restoration_chem) + amount = min(amount + saline.volume * saline.dilution_per_unit, BLOOD_VOLUME_NORMAL) + + return amount + +/// Sets the base blood volume of the mob, returns the blood volume of the mob after. +/mob/living/proc/set_blood_volume(amount, minimum = 0, maximum = BLOOD_VOLUME_MAXIMUM, cached_blood_volume = null) + if (!isnum(cached_blood_volume)) + cached_blood_volume = get_blood_volume() + + if (!CAN_HAVE_BLOOD(src) && amount != 0) + return cached_blood_volume + + if (amount == cached_blood_volume) + return cached_blood_volume + + blood_volume = clamp(amount, minimum, maximum) + + var/updated_blood_volume = get_blood_volume() + + if (cached_blood_volume != updated_blood_volume) + living_flags |= QUEUE_BLOOD_UPDATE + + return updated_blood_volume + +/// Adjusts the base blood volume of the mob and returns the change. +/// Increases in blood volume give a positive return value and vice versa. +/// Maximum only applies on positive amounts and vice versa. +/mob/living/proc/adjust_blood_volume(amount, minimum = 0, maximum = BLOOD_VOLUME_MAXIMUM) + if (!CAN_HAVE_BLOOD(src) || amount == 0) + return 0 + + var/cached_blood_volume = get_blood_volume() + + if (amount < 0) + if (cached_blood_volume <= minimum) + // Already at or below the minimum, don't decrease further. + return 0 + // Decreases shouldn't jump the pre-existing value to the maximum. + maximum = INFINITY + else + if (cached_blood_volume >= maximum) + // Already at or above the maximum, don't increase further. + return 0 + // Increases shouldn't jump the pre-existing value to the minimum. + minimum = -INFINITY + + var/updated_blood_volume = set_blood_volume(cached_blood_volume + amount, minimum = minimum, maximum = maximum, cached_blood_volume = cached_blood_volume) + return updated_blood_volume - cached_blood_volume + +/// Updates effects that rely on blood volume, like blood HUDs. +/mob/living/proc/update_blood_effects() + living_flags &= ~QUEUE_BLOOD_UPDATE + +/// Updates effects that rely on whether the mob can have blood. +/mob/living/proc/update_blood_status() + var/had_blood = CAN_HAVE_BLOOD(src) + + living_flags = can_have_blood() ? (living_flags | LIVING_CAN_HAVE_BLOOD) : (living_flags & ~LIVING_CAN_HAVE_BLOOD) + + var/has_blood = CAN_HAVE_BLOOD(src) + + if (had_blood == has_blood) + return + + var/old_blood_volume = get_blood_volume() + + set_blood_volume(has_blood ? default_blood_volume : 0) + + var/new_blood_volume = get_blood_volume() + + SEND_SIGNAL(src, COMSIG_LIVING_UPDATE_BLOOD_STATUS, had_blood, has_blood, old_blood_volume, new_blood_volume) + // Takes care blood loss and regeneration /mob/living/carbon/human/handle_blood(seconds_per_tick, times_fired) // Under these circumstances blood handling is not necessary - if(bodytemperature < BLOOD_STOP_TEMP || HAS_TRAIT(src, TRAIT_FAKEDEATH) || HAS_TRAIT(src, TRAIT_HUSK)) + if(bodytemperature < BLOOD_STOP_TEMP || HAS_TRAIT(src, TRAIT_FAKEDEATH)) return + // Run the signal, still allowing mobs with noblood to "handle blood" in their own way var/sigreturn = SEND_SIGNAL(src, COMSIG_HUMAN_ON_HANDLE_BLOOD, seconds_per_tick, times_fired) - if((sigreturn & HANDLE_BLOOD_HANDLED) || HAS_TRAIT(src, TRAIT_NOBLOOD)) + if((sigreturn & HANDLE_BLOOD_HANDLED) || !CAN_HAVE_BLOOD(src)) return //Blood regeneration if there is some space - if(!(sigreturn & HANDLE_BLOOD_NO_NUTRITION_DRAIN)) - if(blood_volume < BLOOD_VOLUME_NORMAL && !HAS_TRAIT(src, TRAIT_NOHUNGER)) - var/nutrition_ratio = round(nutrition / NUTRITION_LEVEL_WELL_FED, 0.2) - if(satiety > 80) - nutrition_ratio *= 1.25 - adjust_nutrition(-nutrition_ratio * HUNGER_FACTOR * seconds_per_tick) - blood_volume = min(blood_volume + (BLOOD_REGEN_FACTOR * physiology.blood_regen_mod * nutrition_ratio * seconds_per_tick), BLOOD_VOLUME_NORMAL) + if(!(sigreturn & HANDLE_BLOOD_NO_NUTRITION_DRAIN) && get_blood_volume() < BLOOD_VOLUME_NORMAL && !HAS_TRAIT(src, TRAIT_NOHUNGER)) + var/nutrition_ratio = round(nutrition / NUTRITION_LEVEL_WELL_FED, 0.2) - //Bloodloss from wounds - var/temp_bleed = 0 - for(var/obj/item/bodypart/iter_part as anything in bodyparts) - temp_bleed += iter_part.cached_bleed_rate * seconds_per_tick + if(satiety > 80) + nutrition_ratio *= 1.25 - if(iter_part.generic_bleedstacks) // If you don't have any bleedstacks, don't try and heal them - iter_part.adjustBleedStacks(-1, 0) + var/blood_to_restore = BLOOD_REGEN_FACTOR * physiology.blood_regen_mod * nutrition_ratio * seconds_per_tick + var/blood_restored = adjust_blood_volume(blood_to_restore, maximum = BLOOD_VOLUME_NORMAL) - if(temp_bleed) - bleed(temp_bleed) - bleed_warn(temp_bleed) + if (blood_restored > 0) + adjust_nutrition(-nutrition_ratio * HUNGER_FACTOR * seconds_per_tick * (blood_restored / blood_to_restore)) + + var/bleed_rate = get_bleed_rate() + + if(bleed_rate) + bleed(bleed_rate * seconds_per_tick) + bleed_warn(bleed_rate) + + for (var/obj/item/bodypart/bodypart as anything in bodyparts) + if (bodypart.generic_bleedstacks) + bodypart.adjustBleedStacks(-1, 0) //Effects of bloodloss if(sigreturn & HANDLE_BLOOD_NO_OXYLOSS) return + // Takes into account modifiers like saline-glucose solution in the blood + var/modified_blood_volume = get_blood_volume(apply_modifiers = TRUE) + // Some effects are halved mid-combat. var/determined_mod = has_status_effect(/datum/status_effect/determined) ? 0.5 : 0 var/word = pick("dizzy","woozy","faint") - switch(blood_volume) + switch(modified_blood_volume) + // Way too much blood! if(BLOOD_VOLUME_EXCESS to BLOOD_VOLUME_MAX_LETHAL) if(SPT_PROB(7.5, seconds_per_tick)) to_chat(src, span_userdanger("Blood starts to tear your skin apart. You're going to burst!")) investigate_log("has been gibbed by having too much blood.", INVESTIGATE_DEATHS) inflate_gib() - // Way too much blood! - if(BLOOD_VOLUME_EXCESS to BLOOD_VOLUME_MAX_LETHAL) - if(SPT_PROB(5, seconds_per_tick)) - to_chat(src, span_warning("You feel your skin swelling.")) // Too much blood if(BLOOD_VOLUME_MAXIMUM to BLOOD_VOLUME_EXCESS) if(SPT_PROB(5, seconds_per_tick)) @@ -103,7 +208,7 @@ death() // Blood ratio! if you have 280 blood, this equals 0.5 as that's half of the current value, 560. - var/effective_blood_ratio = blood_volume / BLOOD_VOLUME_NORMAL + var/effective_blood_ratio = modified_blood_volume / BLOOD_VOLUME_NORMAL var/target_oxyloss = max((1 - effective_blood_ratio) * 100, 0) // If your ratio is less than one (you're missing any blood) and your oxyloss is under missing blood %, start getting oxy damage. @@ -111,7 +216,7 @@ // If the damage surpasses the KO threshold for oxyloss, then we'll always tick up so you die eventually if(target_oxyloss > 0 && (getOxyLoss() < target_oxyloss || (target_oxyloss >= OXYLOSS_PASSOUT_THRESHOLD && stat >= UNCONSCIOUS))) // At roughly half blood this equals to 3 oxyloss per tick. At 90% blood it's close to 0.5 - var/rounded_oxyloss = round(0.01 * (BLOOD_VOLUME_NORMAL - blood_volume), 0.25) * seconds_per_tick + var/rounded_oxyloss = round(0.01 * (BLOOD_VOLUME_NORMAL - modified_blood_volume), 0.25) * seconds_per_tick adjustOxyLoss(rounded_oxyloss, updating_health = TRUE) /// Has each bodypart update its bleed/wound overlay icon states @@ -119,19 +224,19 @@ for(var/obj/item/bodypart/iter_part as anything in bodyparts) iter_part.update_part_wound_overlay() -/// Makes a blood drop, leaking amt units of blood from the mob -/mob/living/proc/bleed(amt) +/// Bleeds amount units of blood from the mob, sometimes creating a blood splatter on the floor. +/mob/living/proc/bleed(amount) if(HAS_TRAIT(src, TRAIT_GODMODE) || !can_bleed()) return - blood_volume = max(blood_volume - amt, 0) + var/amount_bled = -adjust_blood_volume(-amount) // Blood loss still happens in locker, floor stays clean - if(isturf(loc) && prob(sqrt(amt) * BLOOD_DRIP_RATE_MOD)) - add_splatter_floor(loc, (amt <= 10)) + if(isturf(loc) && prob(sqrt(amount_bled) * BLOOD_DRIP_RATE_MOD)) + add_splatter_floor(loc, (amount_bled <= 10)) -/mob/living/carbon/human/bleed(amt) - amt *= physiology.bleed_mod +/mob/living/carbon/human/bleed(amount) + amount *= physiology.bleed_mod return ..() /// A helper to see how much blood we're losing per tick @@ -140,12 +245,11 @@ /mob/living/carbon/get_bleed_rate() if(HAS_TRAIT(src, TRAIT_GODMODE) || !can_bleed()) - return - var/bleed_amt = 0 - for(var/X in bodyparts) - var/obj/item/bodypart/iter_bodypart = X - bleed_amt += iter_bodypart.cached_bleed_rate - return bleed_amt + return 0 + + . = 0 + for(var/obj/item/bodypart/bodypart as anything in bodyparts) + . += bodypart.cached_bleed_rate /mob/living/carbon/human/get_bleed_rate() return ..() * physiology.bleed_mod @@ -154,22 +258,22 @@ * bleed_warn() is used to for carbons with an active client to occasionally receive messages warning them about their bleeding status (if applicable) * * Arguments: - * * bleed_amt- When we run this from [/mob/living/carbon/human/proc/handle_blood] we already know how much blood we're losing this tick, so we can skip tallying it again with this - * * forced- + * * bleed_rate - When we run this from [/mob/living/carbon/human/proc/handle_blood] we already know how much blood we're losing per second, so we can skip tallying it again with this. + * * skip_cooldown - Skips caring about the bleed message cooldown. */ -/mob/living/carbon/proc/bleed_warn(bleed_amt = 0, forced = FALSE) - if(!blood_volume || !client) +/mob/living/carbon/proc/bleed_warn(bleed_rate = null, skip_cooldown = FALSE) + if(!CAN_HAVE_BLOOD(src) || !client) return - if(!COOLDOWN_FINISHED(src, bleeding_message_cd) && !forced) + if(!COOLDOWN_FINISHED(src, bleeding_message_cd) && !skip_cooldown) return - if(!bleed_amt) // if we weren't provided the amount of blood we lost this tick in the args - bleed_amt = get_bleed_rate() + if(!isnum(bleed_rate)) + bleed_rate = get_bleed_rate() var/bleeding_severity = "" var/next_cooldown = BLEEDING_MESSAGE_BASE_CD - switch(bleed_amt) + switch(bleed_rate) if(-INFINITY to 0) return if(0 to 1) @@ -208,15 +312,11 @@ to_chat(src, span_warning("[bleeding_severity][rate_of_change]")) COOLDOWN_START(src, bleeding_message_cd, next_cooldown) -/mob/living/carbon/human/bleed_warn(bleed_amt = 0, forced = FALSE) - if(!HAS_TRAIT(src, TRAIT_NOBLOOD)) - return ..() - /mob/living/proc/restore_blood() - blood_volume = initial(blood_volume) + set_blood_volume(default_blood_volume) /mob/living/carbon/restore_blood() - blood_volume = BLOOD_VOLUME_NORMAL + . = ..() for(var/obj/item/bodypart/bodypart_to_restore as anything in bodyparts) bodypart_to_restore.setBleedStacks(0) @@ -224,47 +324,54 @@ BLOOD TRANSFERS ****************************************************/ -//Gets blood from mob to a container or other mob, preserving all data in it. -/mob/living/proc/transfer_blood_to(atom/movable/receiver, amount, forced, ignore_incompatibility) - if(!blood_volume || !receiver.reagents) - return FALSE +// Transfers blood from mob to a container or another mob, preserving all data in it. +// Returns how much blood was able to be transferred. +/mob/living/proc/transfer_blood_to(atom/movable/receiver, amount, ignore_low_blood = FALSE, ignore_incompatibility = FALSE, transfer_viruses = TRUE) + var/cached_blood_volume = get_blood_volume() - if(blood_volume < BLOOD_VOLUME_BAD && !forced) - return FALSE + if(!cached_blood_volume || !receiver.reagents || amount <= 0) + return 0 - if(blood_volume < amount) - amount = blood_volume + if(cached_blood_volume < BLOOD_VOLUME_BAD && !ignore_low_blood) + return 0 var/datum/blood_type/blood_type = get_bloodtype() if (!blood_type) - return FALSE + return 0 var/blood_reagent = get_blood_reagent() - - blood_volume -= amount var/list/blood_data = get_blood_data() - if (!isliving(receiver)) - receiver.reagents.add_reagent(blood_reagent, amount, blood_data, bodytemperature, creation_callback = CALLBACK(src, PROC_REF(on_blood_created), blood_type)) - return TRUE + // Caps the amount to how much blood we have. + amount = min(amount, get_blood_volume()) + + if (!ignore_low_blood) + // Caps the amount to how much we can transfer before reaching low blood. + amount = min(amount, get_blood_volume() - BLOOD_VOLUME_BAD) var/mob/living/target = receiver - if (target.get_blood_reagent() != blood_reagent) - target.reagents.add_reagent(blood_reagent, amount, blood_data, bodytemperature, creation_callback = CALLBACK(src, PROC_REF(on_blood_created), blood_type)) - return TRUE + if (!isliving(receiver) || target.get_blood_reagent() != blood_reagent) + // Further caps the amount to how much blood we were able to add to the target. + amount = receiver.reagents.add_reagent(blood_reagent, amount, blood_data, bodytemperature, creation_callback = CALLBACK(src, PROC_REF(on_blood_created), blood_type)) + adjust_blood_volume(-amount) + return amount - if(blood_data["viruses"]) + if(blood_data["viruses"] && transfer_viruses) for(var/datum/disease/blood_disease as anything in blood_data["viruses"]) if((blood_disease.spread_flags & DISEASE_SPREAD_SPECIAL) || (blood_disease.spread_flags & DISEASE_SPREAD_NON_CONTAGIOUS)) continue target.ForceContractDisease(blood_disease) if(!ignore_incompatibility && !(blood_type.type_key() in target.get_bloodtype().compatible_types)) - target.reagents.add_reagent(/datum/reagent/toxin, amount * 0.5) - return TRUE + // Yes, we cap it to the amount of toxin. This is ridiculously niche, but we do it anyway. + amount = target.reagents.add_reagent(/datum/reagent/toxin, amount * 0.5) * 2 + adjust_blood_volume(-amount) + return amount - target.blood_volume = min(target.blood_volume + round(amount, 0.1), BLOOD_VOLUME_MAX_LETHAL) - return TRUE + // And, obviously, cap it to how much blood the target can take if they're living. + amount = target.adjust_blood_volume(amount, maximum = BLOOD_VOLUME_MAX_LETHAL) + adjust_blood_volume(-amount) + return amount /// Callback that adds blood_reagent to any blood extracted from ourselves /mob/living/proc/on_blood_created(datum/blood_type/blood_type, datum/reagent/new_blood) @@ -346,7 +453,7 @@ /mob/living/proc/get_bloodtype() RETURN_TYPE(/datum/blood_type) - if (!blood_volume) + if (!CAN_HAVE_BLOOD(src)) return if (!(mob_biotypes & MOB_ORGANIC)) @@ -376,7 +483,7 @@ /// Check if a mob can bleed, and possibly if they're capable of leaving decals on turfs/mobs/items /mob/living/proc/can_bleed(bleed_flag = NONE) - if (HAS_TRAIT(src, TRAIT_HUSK) || HAS_TRAIT(src, TRAIT_NOBLOOD)) + if (!CAN_HAVE_BLOOD(src)) return BLEED_NONE if (!bleed_flag) @@ -460,7 +567,7 @@ /// Create a small visual-only blood splatter /mob/living/proc/create_splatter(splatter_dir = pick(GLOB.cardinals)) - // Check for husking and TRAIT_NOBLOOD + // Check for TRAIT_NOBLOOD if (!can_bleed()) // Even if we can't cover turfs, we still can add DNA to everything our blood hits return var/obj/effect/temp_visual/dir_setting/bloodsplatter/splatter = new(get_turf(src), splatter_dir, get_bloodtype()?.get_color()) @@ -479,7 +586,7 @@ if (!splatter_turf) return - // Check for husking and TRAIT_NOBLOOD + // Check for TRAIT_NOBLOOD switch (can_bleed(BLOOD_COVER_TURFS)) if (BLEED_NONE) return @@ -505,24 +612,34 @@ var/turf/targ = get_ranged_target_turf(src, splatter_direction, splatter_strength) our_splatter.fly_towards(targ, splatter_strength) +// FIXME: This duplicates blood like crazy. The amount you bleed is way less than the splatter. +// To summarize, you can literally dupe blood and bypass bloodloss with a syringe and a beaker. +// I'm writing this here because it's out of scope for my PR, but was discovered because of it. /mob/living/proc/make_blood_trail(turf/target_turf, turf/start, was_facing, movement_direction) - if(!has_gravity() || !isturf(start)) + if(!has_gravity() || !isturf(start) || !can_bleed()) + return + + var/cached_blood_volume = get_blood_volume() + + if(!cached_blood_volume) return var/base_bleed_rate = get_bleed_rate() var/base_brute = getBruteLoss() var/brute_ratio = round(base_brute / (maxHealth * 4), 0.1) - var/bleeding_rate = round(base_bleed_rate / 4, 0.1) + var/bleeding_rate = round(base_bleed_rate / 4, 0.1) + // We only leave a trail if we're below a certain blood threshold // The more brute damage we have, or the more we're bleeding, the less blood we need to leave a trail - if(blood_volume < max(BLOOD_VOLUME_NORMAL * (1 - max(bleeding_rate, brute_ratio)), 0)) + if(cached_blood_volume < max(BLOOD_VOLUME_NORMAL * (1 - max(bleeding_rate, brute_ratio)), 0)) return var/blood_to_add = BLOOD_AMOUNT_PER_DECAL * 0.1 + if(body_position == LYING_DOWN) blood_to_add += bleed_drag_amount() - blood_volume = max(blood_volume - blood_to_add, 0) + adjust_blood_volume(-blood_to_add) else blood_to_add += base_bleed_rate @@ -530,10 +647,11 @@ if(base_brute >= 300 || base_bleed_rate >= 7) blood_to_add *= 2 - switch (can_bleed(BLOOD_COVER_TURFS)) - if (BLEED_NONE) + // Checks if we can add visual blood effects on turfs. + switch(can_bleed(BLOOD_COVER_TURFS)) + if(BLEED_NONE) return - if (BLEED_ADD_DNA) + if(BLEED_ADD_DNA) return start.add_mob_blood(src) var/trail_dir = REVERSE_DIR(movement_direction) @@ -565,7 +683,7 @@ trail_dir &= ~(was_facing & (EAST|WEST)) break - if (continuing_trail || (trail_dir in GLOB.diagonals)) + if(continuing_trail || (trail_dir in GLOB.diagonals)) create_blood_trail_component(start, trail_dir, blood_to_add * 0.67, FALSE) create_blood_trail_component(target_turf, get_dir(start, target_turf), blood_to_add * 0.33, TRUE) return diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index c91cd3ec879..c78f6725f44 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -735,8 +735,7 @@ /mob/living/carbon/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE) if(excess_healing) - if(dna && !HAS_TRAIT(src, TRAIT_NOBLOOD)) - blood_volume += (excess_healing * 2) //1 excess = 10 blood + adjust_blood_volume(excess_healing * 2) for(var/obj/item/organ/target_organ as anything in organs) if(!target_organ.damage) @@ -1101,12 +1100,17 @@ /// if any of our bodyparts are bleeding /mob/living/carbon/proc/is_bleeding() + if(!CAN_HAVE_BLOOD(src)) + return FALSE for(var/obj/item/bodypart/part as anything in bodyparts) if(part.cached_bleed_rate) return TRUE /// get our total bleedrate /mob/living/carbon/proc/get_total_bleed_rate() + if(!CAN_HAVE_BLOOD(src)) + return FALSE + var/total_bleed_rate = 0 for(var/obj/item/bodypart/part as anything in bodyparts) total_bleed_rate += part.cached_bleed_rate diff --git a/code/modules/mob/living/carbon/carbon_defines.dm b/code/modules/mob/living/carbon/carbon_defines.dm index 90896042dbe..fabccde2129 100644 --- a/code/modules/mob/living/carbon/carbon_defines.dm +++ b/code/modules/mob/living/carbon/carbon_defines.dm @@ -1,6 +1,6 @@ /mob/living/carbon abstract_type = /mob/living/carbon - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL gender = MALE pressure_resistance = 15 hud_possible = list(HEALTH_HUD,STATUS_HUD,ANTAG_HUD,GLAND_HUD) diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm index 82f06048ffa..b8da1781832 100644 --- a/code/modules/mob/living/carbon/examine.dm +++ b/code/modules/mob/living/carbon/examine.dm @@ -143,7 +143,7 @@ if(DISGUST_LEVEL_DISGUSTED to INFINITY) . += "[t_He] look[p_s()] extremely disgusted." - var/apparent_blood_volume = blood_volume + var/apparent_blood_volume = CAN_HAVE_BLOOD(src) ? get_blood_volume(apply_modifiers = TRUE) : BLOOD_VOLUME_NORMAL if(HAS_TRAIT(src, TRAIT_USES_SKINTONES) && ishuman(src)) var/mob/living/carbon/human/husrc = src // gross istypesrc but easier than refactoring even further for now if(husrc.skin_tone == "albino") diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm index b1db6e44f2d..5a312eecbb8 100644 --- a/code/modules/mob/living/carbon/human/_species.dm +++ b/code/modules/mob/living/carbon/human/_species.dm @@ -354,8 +354,7 @@ GLOBAL_LIST_EMPTY(features_by_species) * Normalizes blood in a human if it is excessive. If it is above BLOOD_VOLUME_NORMAL, this will clamp it to that value. It will not give the human more blodo than they have less than this value. */ /datum/species/proc/normalize_blood(mob/living/carbon/human/blood_possessing_human) - var/normalized_blood_values = max(blood_possessing_human.blood_volume, 0, BLOOD_VOLUME_NORMAL) - blood_possessing_human.blood_volume = normalized_blood_values + blood_possessing_human.set_blood_volume(min(blood_possessing_human.get_blood_volume(), BLOOD_VOLUME_NORMAL)) /** * Proc called when a carbon becomes this species. diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm index c05682c513b..3ac68f365e5 100644 --- a/code/modules/mob/living/carbon/human/death.dm +++ b/code/modules/mob/living/carbon/human/death.dm @@ -34,7 +34,7 @@ GLOBAL_LIST_EMPTY(dead_players_during_shift) investigate_log("has died at [loc_name(src)].
\ BRUTE: [src.getBruteLoss()] BURN: [src.getFireLoss()] TOX: [src.getToxLoss()] OXY: [src.getOxyLoss()] STAM: [src.getStaminaLoss()]
\ Brain damage: [src.get_organ_loss(ORGAN_SLOT_BRAIN) || "0"]
\ - [get_bloodtype()?.get_blood_name() || "Blood"] volume: [src.blood_volume]cl ([round((src.blood_volume / BLOOD_VOLUME_NORMAL) * 100, 0.1)]%)
\ + [get_bloodtype()?.get_blood_name() || "Blood"] volume: [src.get_blood_volume(apply_modifiers = TRUE)]cl ([round((src.get_blood_volume(apply_modifiers = TRUE) / BLOOD_VOLUME_NORMAL) * 100, 0.1)]%)
\ Reagents:
[reagents_readout()]", INVESTIGATE_DEATHS) to_chat(src, span_warning("You have died. Barring complete bodyloss, you can in most cases be revived by other players. \ If you do not wish to be brought back, use the \"Do Not Resuscitate\" button at the bottom of your screen.")) @@ -60,5 +60,5 @@ GLOBAL_LIST_EMPTY(dead_players_during_shift) /mob/living/carbon/proc/Drain() become_husk(CHANGELING_DRAIN) ADD_TRAIT(src, TRAIT_BADDNA, CHANGELING_DRAIN) - blood_volume = 0 + set_blood_volume(0) return TRUE diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 5be526cfb3b..c4f78f9a8dc 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -707,7 +707,7 @@ return ..() /mob/living/carbon/human/vomit(vomit_flags = VOMIT_CATEGORY_DEFAULT, vomit_type = /obj/effect/decal/cleanable/vomit/toxic, lost_nutrition = 10, distance = 1, purge_ratio = 0.1) - if(!((vomit_flags & MOB_VOMIT_BLOOD) && HAS_TRAIT(src, TRAIT_NOBLOOD) && !HAS_TRAIT(src, TRAIT_TOXINLOVER))) + if(!((vomit_flags & MOB_VOMIT_BLOOD) && !CAN_HAVE_BLOOD(src) && !HAS_TRAIT(src, TRAIT_TOXINLOVER))) return ..() if(vomit_flags & MOB_VOMIT_MESSAGE) @@ -970,16 +970,6 @@ else remove_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown) -/mob/living/carbon/human/is_bleeding() - if(HAS_TRAIT(src, TRAIT_NOBLOOD)) - return FALSE - return ..() - -/mob/living/carbon/human/get_total_bleed_rate() - if(HAS_TRAIT(src, TRAIT_NOBLOOD)) - return FALSE - return ..() - /mob/living/carbon/human/get_exp_list(minutes) . = ..() if(mind.assigned_role.title in SSjob.name_occupations) diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index 5cde929b6cb..90c4ef2db38 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -580,7 +580,8 @@ if(40 to INFINITY) combined_msg += span_danger("You feel very unwell!") - var/oxy = getOxyLoss() + (losebreath * 4) + (blood_volume < BLOOD_VOLUME_NORMAL ? ((BLOOD_VOLUME_NORMAL - blood_volume) * 0.1) : 0) + (HAS_TRAIT(src, TRAIT_SELF_AWARE) ? 0 : (rand(-3, 0) * 5)) + var/cached_blood_volume = get_blood_volume(apply_modifiers = TRUE) + var/oxy = getOxyLoss() + (losebreath * 4) + (cached_blood_volume < BLOOD_VOLUME_NORMAL ? ((BLOOD_VOLUME_NORMAL - cached_blood_volume) * 0.1) : 0) + (HAS_TRAIT(src, TRAIT_SELF_AWARE) ? 0 : (rand(-3, 0) * 5)) switch(oxy) if(10 to 20) combined_msg += span_danger("You feel lightheaded.") diff --git a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm index 0cff7a90fdc..89462d9730f 100644 --- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm @@ -63,22 +63,29 @@ if(slime.stat == DEAD) return HANDLE_BLOOD_HANDLED - if(slime.blood_volume <= 0) - slime.blood_volume += JELLY_REGEN_RATE_EMPTY * slime.physiology.blood_regen_mod * seconds_per_tick + // In the following code, do not cache blood volumes. + // They are repeadetly updated and caching can introduce bugs. + + // Blood regen thresholds use your real amount of blood. + if(slime.get_blood_volume() <= 0) + slime.adjust_blood_volume(JELLY_REGEN_RATE_EMPTY * slime.physiology.blood_regen_mod * seconds_per_tick) slime.adjustBruteLoss(2.5 * seconds_per_tick) to_chat(slime, span_danger("You feel empty!")) - if(slime.blood_volume < BLOOD_VOLUME_NORMAL) + // Same logic applies here. + if(slime.get_blood_volume() < BLOOD_VOLUME_NORMAL) if(slime.nutrition >= NUTRITION_LEVEL_STARVING) - slime.blood_volume += JELLY_REGEN_RATE * slime.physiology.blood_regen_mod * seconds_per_tick - if(slime.blood_volume <= BLOOD_VOLUME_LOSE_NUTRITION) // don't lose nutrition if we are above a certain threshold, otherwise slimes on IV drips will still lose nutrition + slime.adjust_blood_volume(JELLY_REGEN_RATE * slime.physiology.blood_regen_mod * seconds_per_tick) + if(slime.get_blood_volume() <= BLOOD_VOLUME_LOSE_NUTRITION) // don't lose nutrition if we are above a certain threshold, otherwise slimes on IV drips will still lose nutrition slime.adjust_nutrition(-1.25 * seconds_per_tick) - if(slime.blood_volume < BLOOD_VOLUME_OKAY) + // If you're on saline, you don't feel the effects of bloodloss. + if(slime.get_blood_volume(apply_modifiers = TRUE) < BLOOD_VOLUME_OKAY) if(SPT_PROB(2.5, seconds_per_tick)) to_chat(slime, span_danger("You feel drained!")) - if(slime.blood_volume < BLOOD_VOLUME_BAD) + // Saline can prevent you from cannibalizing yourself. + if(slime.get_blood_volume(apply_modifiers = TRUE) < BLOOD_VOLUME_BAD) Cannibalize_Body(slime) regenerate_limbs?.build_all_button_icons(UPDATE_BUTTON_STATUS) @@ -96,7 +103,7 @@ consumed_limb.drop_limb() to_chat(H, span_userdanger("Your [consumed_limb] is drawn back into your body, unable to maintain its shape!")) qdel(consumed_limb) - H.blood_volume += 20 * H.physiology.blood_regen_mod + H.adjust_blood_volume(20 * H.physiology.blood_regen_mod) /datum/species/jelly/get_species_description() return "Jellypeople are a strange and alien species with three eyes, made entirely out of gel." @@ -136,6 +143,8 @@ background_icon_state = "bg_alien" overlay_icon_state = "bg_alien_border" + var/blood_per_limb = 40 + /datum/action/innate/regenerate_limbs/IsAvailable(feedback = FALSE) . = ..() if(!.) @@ -144,7 +153,7 @@ var/list/limbs_to_heal = H.get_missing_limbs() if(!length(limbs_to_heal)) return FALSE - if(H.blood_volume >= BLOOD_VOLUME_OKAY+40) + if(H.get_blood_volume() >= BLOOD_VOLUME_OKAY + blood_per_limb) return TRUE /datum/action/innate/regenerate_limbs/Activate() @@ -154,17 +163,17 @@ to_chat(H, span_notice("You feel intact enough as it is.")) return to_chat(H, span_notice("You focus intently on your missing [length(limbs_to_heal) >= 2 ? "limbs" : "limb"]...")) - if(H.blood_volume >= 40*length(limbs_to_heal)+BLOOD_VOLUME_OKAY) + if(H.get_blood_volume() >= blood_per_limb * length(limbs_to_heal) + BLOOD_VOLUME_OKAY) H.regenerate_limbs() - H.blood_volume -= 40*length(limbs_to_heal) + H.adjust_blood_volume(-blood_per_limb * length(limbs_to_heal)) to_chat(H, span_notice("...and after a moment you finish reforming!")) return - else if(H.blood_volume >= 40)//We can partially heal some limbs - while(H.blood_volume >= BLOOD_VOLUME_OKAY+40) + else if(H.get_blood_volume() >= blood_per_limb)//We can partially heal some limbs + while(H.get_blood_volume() >= BLOOD_VOLUME_OKAY + blood_per_limb) var/healed_limb = pick(limbs_to_heal) H.regenerate_limb(healed_limb) limbs_to_heal -= healed_limb - H.blood_volume -= 40 + H.adjust_blood_volume(-blood_per_limb) to_chat(H, span_warning("...but there is not enough of you to fix everything! You must attain more mass to heal completely!")) return to_chat(H, span_warning("...but there is not enough of you to go around! You must attain more mass to heal!")) @@ -205,7 +214,7 @@ bodies -= C // This means that the other bodies maintain a link // so if someone mindswapped into them, they'd still be shared. bodies = null - C.blood_volume = min(C.blood_volume, BLOOD_VOLUME_NORMAL) + C.set_blood_volume(C.get_blood_volume(), maximum = BLOOD_VOLUME_NORMAL) UnregisterSignal(C, COMSIG_LIVING_DEATH) ..() @@ -248,13 +257,13 @@ /datum/species/jelly/slime/spec_life(mob/living/carbon/human/H, seconds_per_tick, times_fired) . = ..() - if(H.blood_volume >= BLOOD_VOLUME_SLIME_SPLIT) + if(H.get_blood_volume() >= BLOOD_VOLUME_SLIME_SPLIT) if(SPT_PROB(2.5, seconds_per_tick)) to_chat(H, span_notice("You feel very bloated!")) else if(H.nutrition >= NUTRITION_LEVEL_WELL_FED) - H.blood_volume += 1.5 * seconds_per_tick - if(H.blood_volume <= BLOOD_VOLUME_LOSE_NUTRITION) + H.adjust_blood_volume(1.5 * seconds_per_tick) + if(H.get_blood_volume() <= BLOOD_VOLUME_LOSE_NUTRITION) H.adjust_nutrition(-1.25 * seconds_per_tick) /datum/action/innate/split_body @@ -270,7 +279,7 @@ if(!.) return var/mob/living/carbon/human/H = owner - if(H.blood_volume >= BLOOD_VOLUME_SLIME_SPLIT) + if(H.get_blood_volume() >= BLOOD_VOLUME_SLIME_SPLIT) return TRUE return FALSE @@ -287,7 +296,7 @@ ADD_TRAIT(src, TRAIT_NO_TRANSFORM, REF(src)) if(do_after(owner, delay = 6 SECONDS, target = owner, timed_action_flags = IGNORE_HELD_ITEM)) - if(H.blood_volume >= BLOOD_VOLUME_SLIME_SPLIT) + if(H.get_blood_volume() >= BLOOD_VOLUME_SLIME_SPLIT) make_dupe() else to_chat(H, span_warning("...but there is not enough of you to go around! You must attain more mass to split!")) @@ -311,7 +320,7 @@ spare.updateappearance(mutcolor_update=1) spare.Move(get_step(H.loc, pick(NORTH,SOUTH,EAST,WEST))) - H.blood_volume *= 0.45 + H.set_blood_volume(H.get_blood_volume() * 0.45) REMOVE_TRAIT(H, TRAIT_NO_TRANSFORM, REF(src)) var/datum/species/jelly/slime/origin_datum = H.dna.species @@ -390,7 +399,7 @@ occupied = "available" L["status"] = stat - L["exoticblood"] = body.blood_volume + L["exoticblood"] = body.get_blood_volume() L["name"] = body.name L["ref"] = "[REF(body)]" L["occupied"] = occupied diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm index 7fbc95d62ec..0a9861d1398 100644 --- a/code/modules/mob/living/carbon/human/species_types/vampire.dm +++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm @@ -57,8 +57,8 @@ if(need_mob_update) vampire.updatehealth() return - vampire.blood_volume -= 0.125 * seconds_per_tick - if(vampire.blood_volume <= BLOOD_VOLUME_SURVIVE) + vampire.adjust_blood_volume(-0.125 * seconds_per_tick) + if(vampire.get_blood_volume(apply_modifiers = TRUE) <= BLOOD_VOLUME_SURVIVE) to_chat(vampire, span_danger("You ran out of blood!")) vampire.investigate_log("has been dusted by a lack of blood (vampire).", INVESTIGATE_DEATHS) vampire.dust() @@ -231,14 +231,14 @@ return FALSE var/mob/living/carbon/victim = user.pulling - if(user.blood_volume >= BLOOD_VOLUME_MAXIMUM) + if(user.get_blood_volume() >= BLOOD_VOLUME_MAXIMUM) to_chat(user, span_warning("You're already full!")) return FALSE if(victim.stat == DEAD) to_chat(user, span_warning("You need a living victim!")) return FALSE var/blood_name = LOWER_TEXT(user.get_bloodtype()?.get_blood_name()) - if(!victim.blood_volume || victim.get_blood_reagent() != user.get_blood_reagent()) + if(!victim.get_blood_volume() || victim.get_blood_reagent() != user.get_blood_reagent()) if (blood_name) to_chat(user, span_warning("[victim] doesn't have [blood_name]!")) else @@ -255,14 +255,20 @@ return FALSE if(!do_after(user, 3 SECONDS, target = victim, hidden = TRUE)) return FALSE - var/blood_volume_difference = BLOOD_VOLUME_MAXIMUM - user.blood_volume //How much capacity we have left to absorb blood - var/drained_blood = min(victim.blood_volume, VAMP_DRAIN_AMOUNT, blood_volume_difference) + victim.show_message(span_danger("[user] is draining your blood!")) to_chat(user, span_notice("You drain some blood!")) playsound(user, 'sound/items/drink.ogg', 30, TRUE, -2) - victim.blood_volume = clamp(victim.blood_volume - drained_blood, 0, BLOOD_VOLUME_MAXIMUM) - user.blood_volume = clamp(user.blood_volume + drained_blood, 0, BLOOD_VOLUME_MAXIMUM) - if(!victim.blood_volume) + + // Since we adjust the user first, we need to take the victim's blood volume into account. + var/amount_drained = min(VAMP_DRAIN_AMOUNT, victim.get_blood_volume()) + + // Takes into account how much blood the vampire can take. + amount_drained = user.adjust_blood_volume(amount_drained) + + victim.adjust_blood_volume(-amount_drained) + + if(!victim.get_blood_volume()) to_chat(user, span_notice("You finish off [victim]'s [blood_name] supply.")) return TRUE diff --git a/code/modules/mob/living/carbon/init_signals.dm b/code/modules/mob/living/carbon/init_signals.dm index 1f9f30093a5..25fbca2790e 100644 --- a/code/modules/mob/living/carbon/init_signals.dm +++ b/code/modules/mob/living/carbon/init_signals.dm @@ -5,6 +5,8 @@ //Traits that register add and remove RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_AGENDER), PROC_REF(on_agender_trait_gain)) RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_AGENDER), PROC_REF(on_agender_trait_loss)) + RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_NOBLOOD), PROC_REF(on_noblood_trait_gain)) + RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_NOBLOOD), PROC_REF(on_noblood_trait_loss)) //Traits that register add only RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_NOBREATH), PROC_REF(on_nobreath_trait_gain)) @@ -34,6 +36,26 @@ var/datum/dna_block/identity/gender/to_update = GLOB.dna_identity_blocks[/datum/dna_block/identity/gender] to_update.apply_to_mob(src, src.dna.unique_identity) +/** + * On gain of TRAIT_NOBLOOD + * + * This will make the mob update its blood state. + */ +/mob/living/carbon/proc/on_noblood_trait_gain(datum/source) + SIGNAL_HANDLER + + update_blood_status() + +/** + * On removal of TRAIT_NOBLOOD + * + * This will make the mob update its blood state. + */ +/mob/living/carbon/proc/on_noblood_trait_loss(datum/source) + SIGNAL_HANDLER + + update_blood_status() + /** * On gain of TRAIT_NOBREATH * diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index 02e67d04d16..ae0961dc1b3 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -467,8 +467,9 @@ if(!blood_type) return - if(chem.type == blood_type?.restoration_chem && blood_volume < BLOOD_VOLUME_NORMAL) - blood_volume += BLOOD_REGEN_FACTOR * seconds_per_tick + if(chem.type == blood_type?.restoration_chem && get_blood_volume() < BLOOD_VOLUME_NORMAL) + // Don't clamp this to BLOOD_VOLUME_NORMAL. Reagents have quantization, making an clamped threshold janky. + adjust_blood_volume(BLOOD_REGEN_FACTOR * seconds_per_tick) reagents.remove_reagent(chem.type, chem.metabolization_rate * seconds_per_tick) return COMSIG_MOB_STOP_REAGENT_TICK @@ -485,10 +486,8 @@ if(blood_type.reagent_type != chem.type) return - var/blood_stream_volume = min(round(reac_volume, CHEMICAL_VOLUME_ROUNDING), BLOOD_VOLUME_MAXIMUM - blood_volume) - if(blood_stream_volume > 0) //remove reagents from mob that has now entered the bloodstream - reagents.remove_reagent(chem.type, blood_stream_volume) - blood_volume += blood_stream_volume + var/blood_added = adjust_blood_volume(round(reac_volume, CHEMICAL_VOLUME_ROUNDING)) + reagents.remove_reagent(chem.type, blood_added) if(chem.data?["blood_type"]) var/datum/blood_type/donor_type = chem.data["blood_type"] @@ -803,7 +802,7 @@ /mob/living/carbon/proc/needs_heart() if(HAS_TRAIT(src, TRAIT_STABLEHEART)) return FALSE - if(dna && dna.species && (HAS_TRAIT(src, TRAIT_NOBLOOD) || isnull(dna.species.mutantheart))) //not all carbons have species! + if(dna && dna.species && (!CAN_HAVE_BLOOD(src) || isnull(dna.species.mutantheart))) //not all carbons have species! return FALSE return TRUE diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm index fe2b3b92e96..6a38597a78a 100644 --- a/code/modules/mob/living/damage_procs.dm +++ b/code/modules/mob/living/damage_procs.dm @@ -368,11 +368,10 @@ amount = -amount if(HAS_TRAIT(src, TRAIT_TOXIMMUNE)) //Prevents toxin damage, but not healing amount = min(amount, 0) - if(blood_volume) - if(amount > 0) - blood_volume = max(blood_volume - (5 * amount), 0) - else - blood_volume = max(blood_volume - amount, 0) + if(amount > 0) + adjust_blood_volume(-5 * amount) + else + adjust_blood_volume(-amount) else if(!forced && HAS_TRAIT(src, TRAIT_TOXIMMUNE)) //Prevents toxin damage, but not healing amount = min(amount, 0) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 4581bdc6132..6ce766e4a15 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -6,6 +6,8 @@ register_init_signals() if(unique_name) set_name() + update_blood_status() + update_blood_effects() var/datum/atom_hud/data/human/medical/advanced/medhud = GLOB.huds[DATA_HUD_MEDICAL_ADVANCED] medhud.add_atom_to_hud(src) var/datum/atom_hud/data/diagnostic/diag_hud = GLOB.huds[DATA_HUD_DIAGNOSTIC] diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index e1cb74d5095..2934bdf5725 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -166,7 +166,7 @@ return var/obj/item/bodypart/hit_bodypart = get_bodypart(check_hit_limb_zone_name(def_zone)) - if (blood_volume && (isnull(hit_bodypart) || hit_bodypart.can_bleed())) + if (get_blood_volume() && (isnull(hit_bodypart) || hit_bodypart.can_bleed())) create_splatter(angle2dir(proj.angle)) if(prob(33)) add_splatter_floor(get_turf(src)) diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index 8b85214bbdd..1dcd959e13f 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -164,8 +164,13 @@ ///effectiveness prob. is modified negatively by this amount; positive numbers make it more difficult, negative ones make it easier var/butcher_difficulty = 0 - ///how much blood the mob has + /// How much blood the mob currently has. + /// Don't read directly, use get_blood_volume() and get_blood_volume(apply_modifiers = TRUE). + /// Don't write directly either, use set_blood_volume() and adjust_blood_volume(). + /// Also don't initialize this. Initialize default_blood_volume instead. var/blood_volume = 0 + /// The default blood volume of the mob. Used primarily for healing bloodloss. + var/default_blood_volume = 0 ///a list of all status effects the mob has var/list/status_effects diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm index ab8aa9aa31e..ce63bb86ab6 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm @@ -41,7 +41,7 @@ Difficulty: Medium loot = list(/obj/item/melee/cleaving_saw, /obj/item/gun/energy/recharge/kinetic_accelerator) wander = FALSE del_on_death = TRUE - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL gps_name = "Resonant Signal" achievement_type = /datum/award/achievement/boss/blood_miner_kill crusher_achievement_type = /datum/award/achievement/boss/blood_miner_crusher diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm index ad03aeef9a3..29435d817d4 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm @@ -61,7 +61,7 @@ Difficulty: Hard loot = list(/obj/structure/closet/crate/necropolis/bubblegum) crusher_loot = /obj/structure/closet/crate/necropolis/bubblegum/crusher replace_crusher_drop = TRUE - blood_volume = BLOOD_VOLUME_MAXIMUM //BLEED FOR ME + default_blood_volume = BLOOD_VOLUME_MAXIMUM //BLEED FOR ME gps_name = "Bloody Signal" achievement_type = /datum/award/achievement/boss/bubblegum_kill crusher_achievement_type = /datum/award/achievement/boss/bubblegum_crusher diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm index a29e003dcca..29f4caa8a46 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm @@ -36,7 +36,7 @@ Difficulty: Extremely Hard loot = list(/obj/effect/decal/remains/plasma, /obj/item/ice_energy_crystal) wander = FALSE del_on_death = TRUE - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL achievement_type = /datum/award/achievement/boss/demonic_miner_kill crusher_achievement_type = /datum/award/achievement/boss/demonic_miner_crusher score_achievement_type = /datum/award/score/demonic_miner_score diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm index f0f11b583c8..f6db48717b9 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm @@ -38,7 +38,7 @@ Difficulty: Hard crusher_loot = /obj/item/crusher_trophy/wendigo_horn wander = FALSE del_on_death = FALSE - blood_volume = BLOOD_VOLUME_NORMAL + default_blood_volume = BLOOD_VOLUME_NORMAL achievement_type = /datum/award/achievement/boss/wendigo_kill crusher_achievement_type = /datum/award/achievement/boss/wendigo_crusher score_achievement_type = /datum/award/score/wendigo_score diff --git a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm index 945284154fb..fb0837755d3 100644 --- a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm @@ -673,8 +673,7 @@ /datum/reagent/consumable/ethanol/bloody_mary/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired) . = ..() - if(drinker.blood_volume < BLOOD_VOLUME_NORMAL) - drinker.blood_volume = min(drinker.blood_volume + (3 * REM * seconds_per_tick), BLOOD_VOLUME_NORMAL) //Bloody Mary quickly restores blood loss. + drinker.adjust_blood_volume(3 * REM * seconds_per_tick, maximum = BLOOD_VOLUME_NORMAL) // Bloody Mary quickly restores blood loss. /datum/reagent/consumable/ethanol/brave_bull name = "Brave Bull" diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm index 555a8c5c47f..0c2e021afd8 100644 --- a/code/modules/reagents/chemistry/reagents/food_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm @@ -985,7 +985,7 @@ /datum/reagent/consumable/liquidelectricity/enriched/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) . = ..() if(isethereal(affected_mob)) - affected_mob.blood_volume += 1 * seconds_per_tick + affected_mob.adjust_blood_volume(1 * seconds_per_tick) else if(SPT_PROB(10, seconds_per_tick)) //lmao at the newbs who eat energy bars affected_mob.electrocute_act(rand(5,10), "Liquid Electricity in their body", 1, SHOCK_NOGLOVES) //the shock is coming from inside the house playsound(affected_mob, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) 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 8ff3825428c..a188022c935 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 @@ -1042,8 +1042,7 @@ Basically, we fill the time between now and 2s from now with hands based off the else holder.add_reagent(/datum/reagent/medicine/coagulant, 0.2 * REM * seconds_per_tick) - if(affected_mob.blood_volume < BLOOD_VOLUME_NORMAL) - need_mob_update += affected_mob.blood_volume += min(3 * seconds_per_tick, BLOOD_VOLUME_NORMAL) + affected_mob.adjust_blood_volume(3 * seconds_per_tick, maximum = BLOOD_VOLUME_NORMAL) switch(current_cycle) if(10) diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm index 9dda07ae412..f2307304e73 100644 --- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm @@ -291,30 +291,34 @@ metabolization_rate = 0.5 * REAGENTS_METABOLISM overdose_threshold = 60 taste_description = "sweetness and salt" - var/last_added = 0 - var/maximum_reachable = BLOOD_VOLUME_NORMAL - 10 //So that normal blood regeneration can continue with salglu active - var/extra_regen = 0.25 // in addition to acting as temporary blood, also add about half this much to their actual blood per second ph = 5.5 chemical_flags = REAGENT_CAN_BE_SYNTHESIZED + /// Add about half this much extra blood regen per second. + var/extra_regen = 0.25 + + /// Add many extra units of blood per unit of saline. + var/dilution_per_unit = 5 + + /// Doesn't dilute blood beyond this point. + var/dilution_cap = BLOOD_VOLUME_NORMAL + + /// Only supplements blood types that use this restoration chem. + var/required_restoration_chem = /datum/reagent/iron + /datum/reagent/medicine/salglu_solution/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) . = ..() var/need_mob_update = FALSE + if(SPT_PROB(18, seconds_per_tick)) need_mob_update = affected_mob.adjustBruteLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_biotype) need_mob_update += affected_mob.adjustFireLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_biotype) + + // Regen is handled here, dilution is handled in [living/proc/get_blood_volume] var/datum/blood_type/blood_type = affected_mob.get_bloodtype() - // Only suppliments base blood types - if(blood_type?.restoration_chem != /datum/reagent/iron) - return need_mob_update ? UPDATE_MOB_HEALTH : null - if(last_added) - affected_mob.blood_volume -= last_added - last_added = 0 - if(affected_mob.blood_volume < maximum_reachable) //Can only up to double your effective blood level. - var/amount_to_add = min(affected_mob.blood_volume, 5*volume) - var/new_blood_level = min(affected_mob.blood_volume + amount_to_add, maximum_reachable) - last_added = new_blood_level - affected_mob.blood_volume - affected_mob.blood_volume = new_blood_level + (extra_regen * REM * seconds_per_tick) + if(blood_type?.restoration_chem == required_restoration_chem) + affected_mob.adjust_blood_volume(extra_regen * REM * seconds_per_tick) + if(need_mob_update) return UPDATE_MOB_HEALTH @@ -1832,7 +1836,7 @@ /datum/reagent/medicine/coagulant/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) . = ..() - if(!affected_mob.blood_volume || !affected_mob.all_wounds) + if(!CAN_HAVE_BLOOD(affected_mob) || !affected_mob.all_wounds) return var/datum/wound/bloodiest_wound @@ -1853,7 +1857,7 @@ /datum/reagent/medicine/coagulant/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired) . = ..() - if(!affected_mob.blood_volume) + if(!CAN_HAVE_BLOOD(affected_mob)) return if(SPT_PROB(7.5, seconds_per_tick)) diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm index 3d8a32c43e8..ae407217bd4 100644 --- a/code/modules/reagents/chemistry/reagents/other_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm @@ -226,9 +226,8 @@ liver.apply_organ_damage(-healing_bonus * REM * seconds_per_tick) var/water_adaptation = HAS_TRAIT(affected_mob, TRAIT_WATER_ADAPTATION) - if(affected_mob.blood_volume) - var/blood_restored = water_adaptation ? 0.3 : 0.1 - affected_mob.blood_volume += blood_restored * REM * seconds_per_tick // water is good for you! + var/blood_restored = water_adaptation ? 0.3 : 0.1 + affected_mob.adjust_blood_volume(blood_restored * REM * seconds_per_tick) // water is good for you! var/drunkness_restored = water_adaptation ? -0.5 : -0.25 affected_mob.adjust_drunk_effect(drunkness_restored * REM * seconds_per_tick) // and even sobers you up slowly!! if(water_adaptation) @@ -465,8 +464,8 @@ need_mob_update += affected_mob.adjustBruteLoss(-2 * REM * seconds_per_tick, updating_health = FALSE) need_mob_update += affected_mob.adjustFireLoss(-2 * REM * seconds_per_tick, updating_health = FALSE) need_mob_update = TRUE - if(ishuman(affected_mob) && affected_mob.blood_volume < BLOOD_VOLUME_NORMAL) - affected_mob.blood_volume += 3 * REM * seconds_per_tick + if(ishuman(affected_mob)) + affected_mob.adjust_blood_volume(3 * REM * seconds_per_tick, maximum = BLOOD_VOLUME_NORMAL) var/datum/wound/bloodiest_wound @@ -2915,8 +2914,7 @@ need_mob_update += drinker.adjustOxyLoss(-2 * REM * seconds_per_tick, updating_health = FALSE) need_mob_update += drinker.adjustBruteLoss(-2 * REM * seconds_per_tick, updating_health = FALSE) need_mob_update += drinker.adjustFireLoss(-2 * REM * seconds_per_tick, updating_health = FALSE) - if(drinker.blood_volume < BLOOD_VOLUME_NORMAL) - drinker.blood_volume += 3 * REM * seconds_per_tick + drinker.adjust_blood_volume(3 * REM * seconds_per_tick, maximum = BLOOD_VOLUME_NORMAL) // Slowly regulates your body temp drinker.adjust_bodytemperature((drinker.get_body_temp_normal() - drinker.bodytemperature) / 5) for(var/datum/reagent/reagent as anything in drinker.reagents.reagent_list) diff --git a/code/modules/religion/religion_sects.dm b/code/modules/religion/religion_sects.dm index b8813f4396d..b537fac4aec 100644 --- a/code/modules/religion/religion_sects.dm +++ b/code/modules/religion/religion_sects.dm @@ -383,13 +383,13 @@ target.adjustOxyLoss(-suffocation_damage) chaplain.adjustOxyLoss(suffocation_damage * burden_modifier, forced = TRUE) - if(!HAS_TRAIT(chaplain, TRAIT_NOBLOOD)) - if(target.blood_volume < BLOOD_VOLUME_SAFE) - var/transferred_blood_amount = min(chaplain.blood_volume, BLOOD_VOLUME_SAFE - target.blood_volume) - if(transferred_blood_amount && target.get_blood_compatibility(chaplain)) - transferred = chaplain.transfer_blood_to(target, transferred_blood_amount, forced = TRUE) - else if(target.blood_volume > BLOOD_VOLUME_EXCESS) - transferred = target.transfer_blood_to(chaplain, target.blood_volume - BLOOD_VOLUME_EXCESS, forced = TRUE) + var/cached_blood_volume = target.get_blood_volume() + if (cached_blood_volume < BLOOD_VOLUME_SAFE) + if (target.get_blood_compatibility(chaplain)) + var/amount_to_transfer = BLOOD_VOLUME_SAFE - cached_blood_volume + transferred |= chaplain.transfer_blood_to(target, amount_to_transfer, ignore_low_blood = TRUE) + else if (cached_blood_volume > BLOOD_VOLUME_EXCESS) + transferred |= target.transfer_blood_to(chaplain, cached_blood_volume - BLOOD_VOLUME_EXCESS) target.update_damage_overlays() chaplain.update_damage_overlays() diff --git a/code/modules/research/xenobiology/crossbreeding/_weapons.dm b/code/modules/research/xenobiology/crossbreeding/_weapons.dm index c34e07ca4f1..b2fa0594772 100644 --- a/code/modules/research/xenobiology/crossbreeding/_weapons.dm +++ b/code/modules/research/xenobiology/crossbreeding/_weapons.dm @@ -110,13 +110,13 @@ Slimecrossing Weapons return FALSE charge_timer = 0 var/mob/living/M = loc - if(istype(M) && HAS_TRAIT(M, TRAIT_NOBLOOD) && M.stat == CONSCIOUS) - charges ++ + if(istype(M) && !CAN_HAVE_BLOOD(M) && M.stat == CONSCIOUS) + charges++ M.apply_damage(5, BRUTE) else - if(istype(M) && M.blood_volume >= 20) + if(istype(M) && M.get_blood_volume() >= 20) charges++ - M.blood_volume -= 20 + M.adjust_blood_volume(-20) if(charges == 1) recharge_newshot() return TRUE diff --git a/code/modules/research/xenobiology/crossbreeding/consuming.dm b/code/modules/research/xenobiology/crossbreeding/consuming.dm index 492e0e2fdc0..7dbfcd3e30c 100644 --- a/code/modules/research/xenobiology/crossbreeding/consuming.dm +++ b/code/modules/research/xenobiology/crossbreeding/consuming.dm @@ -346,7 +346,7 @@ Consuming extracts: playsound(get_turf(M), 'sound/effects/splat.ogg', 10, TRUE) if(iscarbon(M)) var/mob/living/carbon/C = M - C.blood_volume += 25 //Half a vampire drain. + C.adjust_blood_volume(25) //Half a vampire drain. /obj/item/slimecross/consuming/green colour = SLIME_TYPE_GREEN diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm index 38ae5ae2fb9..15b473a964b 100644 --- a/code/modules/research/xenobiology/xenobiology.dm +++ b/code/modules/research/xenobiology/xenobiology.dm @@ -257,7 +257,7 @@ GLOBAL_LIST_INIT(slime_extract_auto_activate_reactions, init_slime_auto_activate switch(activation_type) if(SLIME_ACTIVATE_MINOR) user.adjust_nutrition(50) - user.blood_volume += 50 + user.adjust_blood_volume(50) to_chat(user, span_notice("You activate [src], and your body is refilled with fresh slime jelly!")) return 150 diff --git a/code/modules/spells/spell_types/self/sanguine_strike.dm b/code/modules/spells/spell_types/self/sanguine_strike.dm index aa753f5076e..fd3865eac7d 100644 --- a/code/modules/spells/spell_types/self/sanguine_strike.dm +++ b/code/modules/spells/spell_types/self/sanguine_strike.dm @@ -64,7 +64,7 @@ if(!isliving(target)) return var/mob/living/living_target = target - if(living_target.blood_volume < BLOOD_VOLUME_SURVIVE) + if(living_target.get_blood_volume() < BLOOD_VOLUME_SURVIVE) return playsound(target, 'sound/effects/wounds/crackandbleed.ogg', 100) playsound(target, 'sound/effects/magic/charge.ogg', 100) @@ -72,13 +72,11 @@ if(iscarbon(living_target)) var/mob/living/carbon/carbon_target = living_target carbon_target.spray_blood(attack_direction, 3) - living_target.blood_volume -= 50 + living_target.adjust_blood_volume(-50) if(!isliving(user)) return var/mob/living/living_user = user - //if we blind-added blood volume to the caster, non-vampire wizards could easily kill themselves by using the spell enough - if(living_user.blood_volume < BLOOD_VOLUME_MAXIMUM) - living_user.blood_volume += 50 + living_user.adjust_blood_volume(50) /// signal called from dropping the enchanted item /datum/action/cooldown/spell/sanguine_strike/proc/on_dropped(obj/item/enchanted, mob/dropper) diff --git a/code/modules/spells/spell_types/self/splattercasting_spell.dm b/code/modules/spells/spell_types/self/splattercasting_spell.dm index e76f8e3c1b9..d3ed2daaf87 100644 --- a/code/modules/spells/spell_types/self/splattercasting_spell.dm +++ b/code/modules/spells/spell_types/self/splattercasting_spell.dm @@ -29,7 +29,7 @@ merely a vessel for the arcane flow. Soon, all that is left is not pain, but hunger.")) cast_on.set_species(/datum/species/human/vampire) - cast_on.blood_volume = BLOOD_VOLUME_NORMAL ///for predictable blood total amounts when the spell is first cast. + cast_on.set_blood_volume(BLOOD_VOLUME_NORMAL) ///for predictable blood total amounts when the spell is first cast. cast_on.AddComponent(/datum/component/splattercasting) diff --git a/code/modules/spells/spell_types/shapeshift/_shape_status.dm b/code/modules/spells/spell_types/shapeshift/_shape_status.dm index 03808c9d891..e854a3faa9d 100644 --- a/code/modules/spells/spell_types/shapeshift/_shape_status.dm +++ b/code/modules/spells/spell_types/shapeshift/_shape_status.dm @@ -175,8 +175,8 @@ owner.apply_damage(damage_to_apply, source_spell.convert_damage_type, forced = TRUE, spread_damage = TRUE, wound_bonus = CANT_WOUND) // Only transfer blood if both mobs are supposed to have a blood volume - if (initial(owner.blood_volume) > 0 && initial(caster_mob.blood_volume) > 0 && !HAS_TRAIT(owner, TRAIT_NOBLOOD) && !HAS_TRAIT(caster_mob, TRAIT_NOBLOOD)) - owner.blood_volume = caster_mob.blood_volume + if (CAN_HAVE_BLOOD(owner) && CAN_HAVE_BLOOD(caster_mob)) + owner.set_blood_volume(caster_mob.get_blood_volume()) for(var/datum/action/bodybound_action as anything in caster_mob.actions) if(bodybound_action.target != caster_mob) @@ -215,8 +215,8 @@ var/damage_to_apply = caster_mob.maxHealth * (owner.get_total_damage() / owner.maxHealth) caster_mob.apply_damage(damage_to_apply, source_spell.convert_damage_type, forced = TRUE, spread_damage = TRUE, wound_bonus = CANT_WOUND) // Only transfer blood if both mobs are supposed to have a blood volume - if (initial(owner.blood_volume) > 0 && initial(caster_mob.blood_volume) > 0 && !HAS_TRAIT(owner, TRAIT_NOBLOOD) && !HAS_TRAIT(caster_mob, TRAIT_NOBLOOD)) - caster_mob.blood_volume = owner.blood_volume + if (CAN_HAVE_BLOOD(owner) && CAN_HAVE_BLOOD(caster_mob)) + caster_mob.set_blood_volume(owner.get_blood_volume()) /datum/status_effect/shapechange_mob/from_spell/on_shape_death(datum/source, gibbed) var/datum/action/cooldown/spell/shapeshift/source_spell = source_weakref.resolve() diff --git a/code/modules/surgery/organs/internal/heart/_heart.dm b/code/modules/surgery/organs/internal/heart/_heart.dm index ce04a2fb66d..116a19c5c41 100644 --- a/code/modules/surgery/organs/internal/heart/_heart.dm +++ b/code/modules/surgery/organs/internal/heart/_heart.dm @@ -247,9 +247,12 @@ if(stabilization_available && owner.health <= owner.crit_threshold) stabilize_heart() - if(bleed_prevention && ishuman(owner) && owner.blood_volume < BLOOD_VOLUME_NORMAL) + // Wound healing is intentionally tied to blood volume. + if(bleed_prevention && ishuman(owner) && owner.get_blood_volume() < BLOOD_VOLUME_NORMAL) var/mob/living/carbon/human/wounded_owner = owner - wounded_owner.blood_volume += 2 * seconds_per_tick + + wounded_owner.adjust_blood_volume(2 * seconds_per_tick) + if(toxification_probability && prob(toxification_probability)) wounded_owner.adjustToxLoss(1 * seconds_per_tick, updating_health = FALSE) diff --git a/code/modules/surgery/organs/internal/heart/heart_anomalock.dm b/code/modules/surgery/organs/internal/heart/heart_anomalock.dm index c0ee87e50c0..1a002103027 100644 --- a/code/modules/surgery/organs/internal/heart/heart_anomalock.dm +++ b/code/modules/surgery/organs/internal/heart/heart_anomalock.dm @@ -98,8 +98,7 @@ if(!core) return - if(owner.blood_volume <= BLOOD_VOLUME_NORMAL) - owner.blood_volume += 5 * seconds_per_tick + owner.adjust_blood_volume(5 * seconds_per_tick, maximum = BLOOD_VOLUME_NORMAL) if(owner.health <= owner.crit_threshold) activate_survival(owner) diff --git a/code/modules/surgery/organs/internal/liver/_liver.dm b/code/modules/surgery/organs/internal/liver/_liver.dm index f6995237c69..a8b0ee54f85 100755 --- a/code/modules/surgery/organs/internal/liver/_liver.dm +++ b/code/modules/surgery/organs/internal/liver/_liver.dm @@ -357,8 +357,7 @@ /obj/item/organ/liver/bloody/on_life(seconds_per_tick, times_fired) . = ..() - if(owner.blood_volume < BLOOD_VOLUME_NORMAL) - owner.blood_volume += 4 * seconds_per_tick + owner.adjust_blood_volume(4 * seconds_per_tick, maximum = BLOOD_VOLUME_NORMAL) /// Convert all non-alcoholic drinks into alcohol /obj/item/organ/liver/distillery diff --git a/code/modules/surgery/organs/internal/lungs/_lungs.dm b/code/modules/surgery/organs/internal/lungs/_lungs.dm index 9d725440220..50c5eadaddb 100644 --- a/code/modules/surgery/organs/internal/lungs/_lungs.dm +++ b/code/modules/surgery/organs/internal/lungs/_lungs.dm @@ -911,7 +911,7 @@ . = ..() if (breath?.gases[/datum/gas/plasma]) var/plasma_pp = breath.get_breath_partial_pressure(breath.gases[/datum/gas/plasma][MOLES]) - breather_slime.blood_volume += (0.2 * plasma_pp) // 10/s when breathing literally nothing but plasma, which will suffocate you. + breather_slime.adjust_blood_volume(0.2 * plasma_pp) // 10/s when breathing literally nothing but plasma, which will suffocate you. /obj/item/organ/lungs/smoker_lungs name = "smoker lungs" diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index e90f802cc1a..78b9d0a37a0 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -109,6 +109,7 @@ #include "binary_insert.dm" #include "bitrunning.dm" #include "blindness.dm" +#include "blood_volume_procs.dm" #include "bloody_footprints.dm" #include "breath.dm" #include "buckle.dm" diff --git a/code/modules/unit_tests/blood_volume_procs.dm b/code/modules/unit_tests/blood_volume_procs.dm new file mode 100644 index 00000000000..9c532486aee --- /dev/null +++ b/code/modules/unit_tests/blood_volume_procs.dm @@ -0,0 +1,115 @@ +/datum/unit_test/blood_volume_procs + +/datum/unit_test/blood_volume_procs/Run() + var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent) + + // Test initial blood status. + TEST_ASSERT(dummy.can_have_blood(), "Initialization of blood volume status is screwed up.") + TEST_ASSERT(CAN_HAVE_BLOOD(dummy), "Caching of blood volume status is screwed up.") + + // Test initial blood volume. + TEST_ASSERT_EQUAL(dummy.default_blood_volume, BLOOD_VOLUME_NORMAL, "Default blood volume is incorrect.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), dummy.default_blood_volume, "Blood volume isn't initialized properly.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(apply_modifiers = TRUE), dummy.get_blood_volume(), "Blood volume is modified on initialization.") + + var/set_amount = 400 + + // Test setting blood volume. + TEST_ASSERT_EQUAL(dummy.set_blood_volume(set_amount), set_amount, "Set proc return value is incorrect.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), set_amount, "Final blood volume is different from what was expected.") + + dummy.set_blood_volume(BLOOD_VOLUME_NORMAL) + var/adjustment_amount = 100 + var/expected_final_volume = dummy.get_blood_volume() + adjustment_amount + + // Test increasing blood volume. + TEST_ASSERT_EQUAL(dummy.adjust_blood_volume(adjustment_amount), adjustment_amount, "Adjustment proc return value is incorrect.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), expected_final_volume, "Final blood volume is different from what was expected.") + + dummy.set_blood_volume(BLOOD_VOLUME_NORMAL) + adjustment_amount = -100 + expected_final_volume = dummy.get_blood_volume() + adjustment_amount + + // Test decreasing blood volume. + TEST_ASSERT_EQUAL(dummy.adjust_blood_volume(adjustment_amount), adjustment_amount, "Adjustment proc return value is incorrect.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), expected_final_volume, "Final blood volume is different from what was expected.") + + dummy.set_blood_volume(BLOOD_VOLUME_NORMAL) + adjustment_amount = 100 + var/expected_adjustment = 50 + expected_final_volume = dummy.get_blood_volume() + expected_adjustment + + // Test increasing blood volume, clamped to a maximum. + TEST_ASSERT_EQUAL(dummy.adjust_blood_volume(adjustment_amount, maximum = expected_final_volume), expected_adjustment, "Clamped adjustment proc return value is incorrect.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), expected_final_volume, "Clamped final blood volume is different from what was expected.") + + dummy.set_blood_volume(BLOOD_VOLUME_NORMAL) + adjustment_amount = -100 + expected_adjustment = -50 + expected_final_volume = dummy.get_blood_volume() + expected_adjustment + + // Test decreasing blood volume, clamped to a minimum. + TEST_ASSERT_EQUAL(dummy.adjust_blood_volume(adjustment_amount, minimum = expected_final_volume), expected_adjustment, "Clamped adjustment proc return value is incorrect.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), expected_final_volume, "Clamped final blood volume is different from what was expected.") + + dummy.set_blood_volume(BLOOD_VOLUME_NORMAL) + adjustment_amount = 100 + expected_final_volume = dummy.get_blood_volume() + adjustment_amount + var/minimum = BLOOD_VOLUME_NORMAL + 200 + + // Test if increasing an existing volume that is below the minimum causes it to jump to the minimum. + TEST_ASSERT_EQUAL(dummy.adjust_blood_volume(adjustment_amount, minimum = minimum), adjustment_amount, "When existing volume is below the minimum, adjustment the proc return value after trying to increase it is unexpected. (likely jumped to minimum)") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), expected_final_volume, "When existing volume is below the minimum, the final volume after trying to increase it is unexpected. (likely jumped to minimum)") + + dummy.set_blood_volume(BLOOD_VOLUME_NORMAL) + adjustment_amount = -100 + expected_final_volume = dummy.get_blood_volume() + adjustment_amount + var/maximum = BLOOD_VOLUME_NORMAL - 200 + + // Test if decreasing an existing volume that is above the maximum causes it to jump to the maximum. + TEST_ASSERT_EQUAL(dummy.adjust_blood_volume(adjustment_amount, maximum = maximum), adjustment_amount, "When existing volume is above the maximum, the adjustment proc return value after trying to decrease it is unexpected. (likely jumped to maximum)") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), expected_final_volume, "When existing volume is above the maximum, the final volume after trying to decrease it is unexpected. (likely jumped to maximum)") + + dummy.set_blood_volume(BLOOD_VOLUME_NORMAL) + adjustment_amount = BLOOD_VOLUME_MAXIMUM * 10 + expected_final_volume = dummy.get_blood_volume() + adjustment_amount + + // Test increasing blood volume beyond BLOOD_VOLUME_MAXIMUM by setting the maximum to INFINITY. This is allowed. (e.g. setting it to BLOOD_VOLUME_MAX_LETHAL) + TEST_ASSERT_EQUAL(dummy.adjust_blood_volume(adjustment_amount, maximum = INFINITY), adjustment_amount, "Setting adjustment proc maximum to INFINITY results in an unexpected adjustment proc return value.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), expected_final_volume, "Setting adjustment proc maximum to INFINITY results in an unexpected final volume.") + + dummy.set_blood_volume(BLOOD_VOLUME_NORMAL) + adjustment_amount = BLOOD_VOLUME_MAXIMUM * -10 + expected_final_volume = dummy.get_blood_volume() + adjustment_amount + + // Test decreasing blood volume below 0 by setting the minimum to -INFINITY. Shouldn't be used, but I want to verify that bypassing the default minimum works as expected. + TEST_ASSERT_EQUAL(dummy.adjust_blood_volume(adjustment_amount, minimum = -INFINITY), adjustment_amount, "Setting adjustment proc minimum to -INFINITY results in an unexpected adjustment proc return value.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), expected_final_volume, "Setting adjustment proc minimum to -INFINITY results in an unexpected final volume.") + + dummy.reagents.add_reagent(/datum/reagent/medicine/salglu_solution, 10) + var/datum/reagent/medicine/salglu_solution/saline = dummy.reagents.has_reagent(/datum/reagent/medicine/salglu_solution) + dummy.set_blood_volume(saline.dilution_cap) + + // Test if saline dilutes blood volume beyond the dilution cap. + TEST_ASSERT_EQUAL(dummy.get_blood_volume(apply_modifiers = TRUE), saline.dilution_cap, "Saline goes above or below its dilution cap.") + + dummy.set_blood_volume(BLOOD_VOLUME_BAD) + var/expected_dilution = saline.volume * saline.dilution_per_unit + expected_final_volume = dummy.get_blood_volume() + expected_dilution + + // Test if saline dilutes low blood volume properly. + TEST_ASSERT_EQUAL(dummy.get_blood_volume(apply_modifiers = TRUE), expected_final_volume, "Saline didn't dilute low blood by the expected amount.") + + ADD_TRAIT(dummy, TRAIT_NOBLOOD, TRAIT_GENERIC) + + // Test if adding TRAIT_NOBLOOD works properly. + TEST_ASSERT(!dummy.can_have_blood(), "Adding TRAIT_NOBLOOD didn't make the mob have no blood.") + TEST_ASSERT(!CAN_HAVE_BLOOD(dummy), "Caching of blood volume status is screwed up after the addition of TRAIT_NOBLOOD.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), 0, "Blood volume wasn't emptied after the addition of TRAIT_NOBLOOD.") + + REMOVE_TRAIT(dummy, TRAIT_NOBLOOD, TRAIT_GENERIC) + + // Test if removing TRAIT_NOBLOOD works properly. + TEST_ASSERT(dummy.can_have_blood(), "Removing TRAIT_NOBLOOD didn't make the mob have blood again.") + TEST_ASSERT(CAN_HAVE_BLOOD(dummy), "Caching of blood volume status is screwed up after the removal of TRAIT_NOBLOOD.") + TEST_ASSERT_EQUAL(dummy.get_blood_volume(), dummy.default_blood_volume, "Blood volume wasn't fixed after the removal of TRAIT_NOBLOOD.")