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