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.")