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