Files

835 lines
34 KiB
Plaintext

#define BLOOD_DRIP_RATE_MOD 90 //Greater number means creating blood drips more often while bleeding
// Conversion between internal drunk power and common blood alcohol content
#define DRUNK_POWER_TO_BLOOD_ALCOHOL 0.003
/****************************************************
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
// For simple multipliers, like a blood worm in a mob.
for (var/source in blood_volume_modifiers)
amount *= blood_volume_modifiers[source]
// 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
// Don't return early even if "amount == cached_blood_volume", because we don't know if minimum or maximum would change it anyway.
// Putting this here because I made that mistake and it led to a bug.
blood_volume = clamp(amount, minimum, maximum)
var/updated_blood_volume = get_blood_volume()
if (cached_blood_volume != updated_blood_volume)
QUEUE_BLOOD_UPDATE(src)
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 or status, like blood HUDs.
/mob/living/proc/update_blood_effects()
blood_hud_set_status()
/// Updates effects that rely on whether the mob can have blood.
/mob/living/proc/update_blood_status()
var/had_blood = CAN_HAVE_BLOOD(src)
var/has_blood = can_have_blood()
// Must not return early on first init for mobs that can have blood. (otherwise they will miss being added to the blood hud)
if (had_blood == has_blood)
return
var/old_blood_volume = get_blood_volume()
// Must be sent before living flags are updated.
SEND_SIGNAL(src, COMSIG_LIVING_PRE_UPDATE_BLOOD_STATUS, had_blood, has_blood, old_blood_volume)
living_flags = has_blood ? (living_flags | LIVING_CAN_HAVE_BLOOD) : (living_flags & ~LIVING_CAN_HAVE_BLOOD)
set_blood_volume(has_blood ? default_blood_volume : 0)
var/new_blood_volume = get_blood_volume()
// Must be sent after living flags are updated.
SEND_SIGNAL(src, COMSIG_LIVING_UPDATE_BLOOD_STATUS, had_blood, has_blood, old_blood_volume, new_blood_volume)
var/datum/atom_hud/data/human/blood/blood_hud = GLOB.huds[DATA_HUD_BLOOD]
if (has_blood)
blood_hud.add_atom_to_hud(src)
else
blood_hud.remove_atom_from_hud(src)
update_blood_effects()
/// Sets the blood volume multiplier for the given source to the given multiplier value.
/mob/living/proc/set_blood_volume_modifier(source, multiplier)
LAZYSET(blood_volume_modifiers, source, multiplier)
QUEUE_BLOOD_UPDATE(src)
/// Removes the blood volume multiplier for the given source.
/mob/living/proc/remove_blood_volume_modifier(source)
LAZYREMOVE(blood_volume_modifiers, source)
QUEUE_BLOOD_UPDATE(src)
/// A one-time coagulating effect of the mob's bloodiest cut/stab
/// Returns TRUE if a wound was affected, FALSE if no wound was found
/mob/living/proc/coagulant_effect(amount_to_heal = 1)
return FALSE
/mob/living/carbon/coagulant_effect(amount_to_heal = 1)
var/datum/wound/bloodiest_wound
for(var/datum/wound/iter_wound as anything in all_wounds)
if(iter_wound.blood_flow && iter_wound.blood_flow > bloodiest_wound?.blood_flow)
bloodiest_wound = iter_wound
bloodiest_wound?.adjust_blood_flow(-1 * abs(amount_to_heal))
return !!bloodiest_wound
// Takes care blood loss and regeneration
/mob/living/carbon/human/handle_blood(seconds_per_tick)
// Under these circumstances blood handling is not necessary
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)
if((sigreturn & HANDLE_BLOOD_HANDLED) || !CAN_HAVE_BLOOD(src))
return
var/heart_blood_multiplier = get_heart_blood_regeneration_multiplier()
//Blood regeneration if there is some space
if(heart_blood_multiplier && !(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)
if(satiety > 80)
nutrition_ratio *= 1.25
var/blood_to_restore = BLOOD_REGEN_FACTOR * physiology.blood_regen_mod * heart_blood_multiplier * nutrition_ratio * seconds_per_tick
var/blood_restored = adjust_blood_volume(blood_to_restore, maximum = BLOOD_VOLUME_NORMAL)
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 get_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 : 1
var/word = pick("dizzy","woozy","faint")
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()
// Too much blood
if(BLOOD_VOLUME_MAXIMUM to BLOOD_VOLUME_EXCESS)
if(SPT_PROB(5, seconds_per_tick))
to_chat(src, span_warning("You feel terribly bloated."))
// Low blood but not a big deal in the immediate
if(BLOOD_VOLUME_OKAY to BLOOD_VOLUME_SAFE)
if(SPT_PROB(2.5, seconds_per_tick))
set_eye_blur_if_lower(2 SECONDS * determined_mod)
if(prob(50))
to_chat(src, span_danger("You feel [word]. It's getting a bit hard to breathe."))
losebreath += 0.5 * determined_mod * seconds_per_tick
else if(get_stamina_loss() < 25 * determined_mod)
to_chat(src, span_danger("You feel [word]. It's getting a bit hard to focus."))
adjust_stamina_loss(5 * determined_mod * seconds_per_tick)
// Pretty low blood, getting dangerous!
if(BLOOD_VOLUME_RISKY to BLOOD_VOLUME_OKAY)
if(SPT_PROB(5, seconds_per_tick))
set_eye_blur_if_lower(2 SECONDS * determined_mod)
set_dizzy_if_lower(2 SECONDS * determined_mod)
if(prob(50))
to_chat(src, span_bolddanger("You feel very [word]. It's getting hard to breathe!"))
losebreath += 1 * determined_mod
else if(get_stamina_loss() < 40 * determined_mod)
to_chat(src, span_bolddanger("You feel very [word]. It's getting hard to stay awake!"))
adjust_stamina_loss(7.5 * determined_mod)
// Very low blood, danger!!
if(BLOOD_VOLUME_BAD to BLOOD_VOLUME_RISKY)
if(SPT_PROB(5, seconds_per_tick))
set_eye_blur_if_lower(4 SECONDS * determined_mod)
set_dizzy_if_lower(4 SECONDS * determined_mod)
if(prob(50))
to_chat(src, span_userdanger("You feel extremely [word]! It's getting very hard to breathe!"))
losebreath += 1.5 * determined_mod
else if(get_stamina_loss() < 80 * determined_mod)
to_chat(src, span_userdanger("You feel extremely [word]! It's getting very hard to stay awake!"))
adjust_stamina_loss(10 * determined_mod)
// Critically low blood, death is near! Adrenaline won't help you here.
if(BLOOD_VOLUME_SURVIVE to BLOOD_VOLUME_BAD)
if(SPT_PROB(7.5, seconds_per_tick))
Unconscious(rand(1 SECONDS, 2 SECONDS))
to_chat(src, span_userdanger("You black out for a moment!"))
// Instantly die upon this threshold
if(-INFINITY to BLOOD_VOLUME_SURVIVE)
if(!HAS_TRAIT(src, TRAIT_NODEATH))
investigate_log("has died of bloodloss.", INVESTIGATE_DEATHS)
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 = 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.
// This damage accrues faster the less blood you have.
// If the damage surpasses the KO threshold for oxyloss, then we'll always tick up so you die eventually
if(target_oxyloss > 0 && (get_oxy_loss() < 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 - modified_blood_volume), 0.25) * seconds_per_tick
adjust_oxy_loss(rounded_oxyloss, updating_health = TRUE)
/// Has each bodypart update its bleed/wound overlay icon states
/mob/living/carbon/proc/update_bodypart_bleed_overlays()
for(var/obj/item/bodypart/iter_part as anything in get_bodyparts())
iter_part.update_part_wound_overlay()
/// 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
var/amount_bled = -adjust_blood_volume(-amount)
// Blood loss still happens in locker, floor stays clean
if(isturf(loc) && prob(sqrt(amount_bled) * BLOOD_DRIP_RATE_MOD))
add_splatter_floor(loc, (amount_bled <= 10))
/mob/living/carbon/human/bleed(amount)
amount *= physiology.bleed_mod
return ..()
/// A helper to see how much blood we're losing per tick
/mob/living/proc/get_bleed_rate()
return 0
/mob/living/carbon/get_bleed_rate()
if(HAS_TRAIT(src, TRAIT_GODMODE) || !can_bleed())
return 0
. = 0
for(var/obj/item/bodypart/bodypart as anything in get_bodyparts())
. += bodypart.cached_bleed_rate
/mob/living/carbon/human/get_bleed_rate()
return ..() * physiology.bleed_mod
/**
* 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_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_rate = null, skip_cooldown = FALSE)
if(!CAN_HAVE_BLOOD(src) || !client)
return
if(!COOLDOWN_FINISHED(src, bleeding_message_cd) && !skip_cooldown)
return
if(!isnum(bleed_rate))
bleed_rate = get_bleed_rate()
var/bleeding_severity = ""
var/next_cooldown = BLEEDING_MESSAGE_BASE_CD
switch(bleed_rate)
if(-INFINITY to 0)
return
if(0 to 1)
bleeding_severity = "You feel light trickles of blood across your skin"
next_cooldown *= 2.5
if(1 to 3)
bleeding_severity = "You feel a small stream of blood running across your body"
next_cooldown *= 2
if(3 to 5)
bleeding_severity = "You skin feels clammy from the flow of blood leaving your body"
next_cooldown *= 1.7
if(5 to 7)
bleeding_severity = "Your body grows more and more numb as blood streams out"
next_cooldown *= 1.5
if(7 to INFINITY)
bleeding_severity = "Your heartbeat thrashes wildly trying to keep up with your bloodloss"
var/rate_of_change = ", but it's getting better." // if there's no wounds actively getting bloodier or maintaining the same flow, we must be getting better!
if(HAS_TRAIT(src, TRAIT_COAGULATING)) // if we have coagulant, we're getting better quick
rate_of_change = ", but it's clotting up quickly!"
else
// flick through our wounds to see if there are any bleeding ones getting worse or holding flow (maybe move this to handle_blood and cache it so we don't need to cycle through the wounds so much)
for(var/datum/wound/iter_wound as anything in all_wounds)
if(!iter_wound.blood_flow)
continue
var/iter_wound_roc = iter_wound.get_bleed_rate_of_change()
switch(iter_wound_roc)
if(BLOOD_FLOW_INCREASING) // assume the worst, if one wound is getting bloodier, we focus on that
rate_of_change = ", <b>and it's getting worse!</b>"
break
if(BLOOD_FLOW_STEADY) // our best case now is that our bleeding isn't getting worse
rate_of_change = ", and it's holding steady."
if(BLOOD_FLOW_DECREASING) // this only matters if none of the wounds fit the above two cases, included here for completeness
continue
to_chat(src, span_warning("[bleeding_severity][rate_of_change]"))
COOLDOWN_START(src, bleeding_message_cd, next_cooldown)
/mob/living/proc/restore_blood()
set_blood_volume(default_blood_volume)
/mob/living/carbon/restore_blood()
. = ..()
for(var/obj/item/bodypart/bodypart_to_restore as anything in get_bodyparts())
bodypart_to_restore.setBleedStacks(0)
/****************************************************
BLOOD TRANSFERS
****************************************************/
// 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(!cached_blood_volume || !receiver.reagents || amount <= 0)
return 0
if(cached_blood_volume < BLOOD_VOLUME_BAD && !ignore_low_blood)
return 0
var/datum/blood_type/blood_type = get_bloodtype()
if (!blood_type)
return 0
var/blood_reagent = get_blood_reagent()
var/list/blood_data = get_blood_data()
// 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 (!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"] && transfer_viruses)
for(var/datum/disease/blood_disease as anything in blood_data["viruses"])
if((blood_disease.spread_flags & (DISEASE_SPREAD_SPECIAL|DISEASE_SPREAD_NON_CONTAGIOUS)))
continue
target.ForceContractDisease(blood_disease)
if(!ignore_incompatibility && !(blood_type.type_key() in target.get_bloodtype().compatible_types))
// 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
var/cached_target_blood_volume = target.get_blood_volume()
// 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)
// Synthetic blood handling, used for capping blood worm growth from monkeys, blood bags and basic mobs. This forces blood worms to kill people to reach adulthood.
if (!IS_BLOOD_ALWAYS_SYNTHETIC(target))
var/added_synth_volume = amount * get_blood_synth_content()
var/existing_synth_volume = cached_target_blood_volume * target.get_blood_synth_content()
if (added_synth_volume != 0 || existing_synth_volume != 0)
// A simple weighted average that simplifies down to "total synth volume / total blood volume" i.e. "how much of their blood is synthetic"
target.AddComponent(/datum/component/synth_blood, (added_synth_volume + existing_synth_volume) / (amount + cached_target_blood_volume))
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)
new_blood.AddElement(/datum/element/blood_reagent, src, blood_type)
/mob/living/proc/get_blood_data()
SHOULD_CALL_PARENT(TRUE)
RETURN_TYPE(/list)
var/datum/blood_type/blood_type = get_bloodtype()
if (!blood_type || !can_bleed())
return
var/blood_data = list()
blood_data["blood_type"] = blood_type
blood_data["blood_DNA"] = blood_type.dna_string
if (reagents)
var/list/temp_chem = list()
for(var/datum/reagent/blood_reagent in reagents.reagent_list)
temp_chem[blood_reagent.type] = blood_reagent.volume
blood_data["trace_chem"] = list2params(temp_chem)
if (blood_type.blood_flags & BLOOD_TRANSFER_VIRAL_DATA)
// Viruses we possess
blood_data["viruses"] = list()
for(var/datum/disease/disease as anything in diseases)
blood_data["viruses"] += disease.Copy()
if(LAZYLEN(disease_resistances))
blood_data["resistances"] = disease_resistances.Copy()
var/synth_content = get_blood_synth_content()
if (synth_content > 0)
blood_data[BLOOD_DATA_SYNTH_CONTENT] = synth_content
// DNA, mind, facitons, etc don't get stored in stuff like oil
if (!(blood_type.blood_flags & BLOOD_ADD_DNA))
return blood_data
if(mind)
blood_data["mind"] = mind
if(ckey)
blood_data["ckey"] = ckey
else if (persistent_client?.mob?.ckey)
blood_data["ckey"] = persistent_client.mob.ckey
blood_data["factions"] = faction
return blood_data
/mob/living/carbon/get_blood_data()
var/list/blood_data = ..()
if (!blood_data)
return
var/datum/blood_type/blood_type = get_bloodtype()
if (!(blood_type.blood_flags & BLOOD_ADD_DNA))
return blood_data
// If we haven't suicided but the ghost cannot reenter, i.e. we ghosted, don't set ourselves as cloneable
var/mob/dead/observer/ghost = get_ghost(TRUE, TRUE)
if(!HAS_TRAIT(src, TRAIT_SUICIDED) && (!ghost || ghost.can_reenter_corpse))
blood_data["cloneable"] = TRUE
if (!blood_data["mind"] && last_mind)
blood_data["mind"] = last_mind
if (!blood_data["ckey"] && last_mind)
blood_data["ckey"] = ckey(last_mind.key)
blood_data["gender"] = gender
blood_data["real_name"] = real_name
if (dna)
blood_data["blood_DNA"] = dna.unique_enzymes
blood_data["features"] = dna.features
blood_data["quirks"] = list()
for(var/datum/quirk/quirk as anything in quirks)
blood_data["quirks"] += quirk.type
return blood_data
/mob/living/proc/get_bloodtype()
RETURN_TYPE(/datum/blood_type)
if (!CAN_HAVE_BLOOD(src))
return
if (!(mob_biotypes & MOB_ORGANIC))
if (mob_biotypes & MOB_ROBOTIC)
return get_blood_type(BLOOD_TYPE_OIL)
return
if (mob_biotypes & MOB_SLIME)
return get_blood_type(BLOOD_TYPE_TOX)
else if (mob_biotypes & MOB_PLANT)
return get_blood_type(BLOOD_TYPE_H2O)
else if (mob_biotypes & MOB_REPTILE)
return get_blood_type(BLOOD_TYPE_LIZARD)
else if (mob_biotypes & MOB_HUMANOID)
// O+ as to avoid mobs bleeding all human bloodtypes under the sun, and its statistically the most common one
return get_blood_type(BLOOD_TYPE_O_PLUS)
return get_blood_type(BLOOD_TYPE_ANIMAL)
/// Returns the reagent type this mob has for blood
/mob/living/proc/get_blood_reagent()
if (!can_bleed())
return
var/datum/blood_type/blood_type = get_bloodtype()
return blood_type?.reagent_type
/// 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 (!CAN_HAVE_BLOOD(src))
return BLEED_NONE
if (!bleed_flag)
return BLEED_SPLATTER
var/datum/blood_type/blood_type = get_bloodtype()
if (!blood_type)
return BLEED_NONE
if (blood_type.blood_flags & bleed_flag)
return BLEED_SPLATTER
if (blood_type.blood_flags & BLOOD_ADD_DNA)
return BLEED_ADD_DNA
return BLEED_NONE
/// Returns the blood_type datum that corresponds to the string id key in GLOB.blood_types
/proc/get_blood_type(id)
RETURN_TYPE(/datum/blood_type)
return GLOB.blood_types[id]
/// Returns the hex color string, or a color matrix, of a given blood_type datum given an assoc list of blood_DNA e.g. ("Unknown Blood Type", "*X")
/proc/get_color_from_blood_list(list/blood_DNA)
var/datum/blood_type/blood_type
if(!length(blood_DNA))
return get_blood_type(BLOOD_TYPE_O_PLUS).get_color()
else if (length(blood_DNA) == 1) // Microop for when we don't need to do color mixing
blood_type = blood_DNA[blood_DNA[length(blood_DNA)]]
return blood_type.get_color()
var/r_color = 0
var/g_color = 0
var/b_color = 0
var/valid_colors = 0
for (var/blood_key in blood_DNA)
blood_type = blood_DNA[blood_key]
if(!istype(blood_type))
continue
var/list/rgb_blood = rgb2num(blood_type.get_color())
r_color += rgb_blood[1]
g_color += rgb_blood[2]
b_color += rgb_blood[3]
valid_colors += 1
if (valid_colors == 0)
return get_blood_type(BLOOD_TYPE_O_PLUS).get_color()
r_color /= valid_colors
g_color /= valid_colors
b_color /= valid_colors
return rgb(r_color, g_color, b_color)
/// Checks if any of the passed blood types have certain blood flags
/proc/has_blood_flag(list/blood_DNA, blood_flags)
if (isnull(blood_DNA))
return FALSE
if (!islist(blood_DNA))
var/datum/blood_type/blood_type = blood_DNA
blood_DNA = list(blood_type.dna_string = blood_type)
var/matches = NONE
for (var/blood_key in blood_DNA)
var/datum/blood_type/blood_type = blood_DNA[blood_key]
matches |= (blood_type.blood_flags & blood_flags)
return matches
/**
* Returns TRUE if src is compatible with donor's blood, otherwise FALSE.
* * donor: Mob that is donating blood.
*/
/mob/living/proc/get_blood_compatibility(mob/living/donor)
if (get_blood_reagent() != donor.get_blood_reagent())
return FALSE
var/datum/blood_type/patient_blood_data = get_bloodtype()
var/datum/blood_type/donor_blood_data = donor.get_bloodtype()
return (donor_blood_data?.type_key() in patient_blood_data?.compatible_types)
/// Create a small visual-only blood splatter
/mob/living/proc/create_splatter(splatter_dir = pick(GLOB.cardinals))
// 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())
splatter.color = get_bloodtype()?.color
/*
* Create a splatter or drip of this mob's blood type
* Arguments:
* * splatter_turf - If the splatter should be made on a different turf than the mob
* * small_drip
*/
/mob/living/proc/add_splatter_floor(turf/splatter_turf, small_drip = FALSE)
if (!splatter_turf)
splatter_turf = get_turf(src)
if (!splatter_turf)
return
// Check for TRAIT_NOBLOOD
switch (can_bleed(BLOOD_COVER_TURFS))
if (BLEED_NONE)
return
if (BLEED_ADD_DNA)
return splatter_turf.add_mob_blood(src)
return get_bloodtype()?.make_blood_splatter(src, splatter_turf, small_drip)
/**
* This proc is a helper for spraying blood for things like slashing/piercing wounds and dismemberment.
*
* The strength of the splatter in the second argument determines how much it can dirty and how far it can go
*
* Arguments:
* * splatter_direction: Which direction the blood is flying
* * splatter_strength: How many tiles it can go, and how many items it can pass over and dirty
*/
/mob/living/proc/spray_blood(splatter_direction, splatter_strength = 3)
// Check if we can bleed and if our splatter can even go anywhere
if(!isturf(loc) || can_bleed(BLOOD_COVER_TURFS) != BLEED_SPLATTER)
return
var/obj/effect/decal/cleanable/blood/hitsplatter/our_splatter = new(loc, get_static_viruses(), get_blood_dna_list(), splatter_strength)
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) || !can_bleed())
return
var/cached_blood_volume = get_blood_volume()
if(!cached_blood_volume)
return
// BUBBER EDIT BEGIN - Blood pooling
for(var/obj/effect/decal/cleanable/blood/blood_effect in target_turf)
blood_effect.increase_blood_pool()
// BUBBER EDIT END
var/base_bleed_rate = get_bleed_rate()
var/base_brute = get_brute_loss()
var/brute_ratio = round(base_brute / (maxHealth * 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(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()
adjust_blood_volume(-blood_to_add)
else
blood_to_add += base_bleed_rate
// If we're very damaged or bleeding a lot, add even more blood to the trail
if(base_brute >= 300 || base_bleed_rate >= 7)
blood_to_add *= 2
// Checks if we can add visual blood effects on turfs.
switch(can_bleed(BLOOD_COVER_TURFS))
if(BLEED_NONE)
return
if(BLEED_ADD_DNA)
return start.add_mob_blood(src)
var/trail_dir = REVERSE_DIR(movement_direction)
// The mob is performing a diagonal movement so we need to make a diagonal trail
// This is not the same as a diagonal dir. Sorry.
// This is insteas denoted by a negative direction (so we don't conflict with real dirs)
if(movement_direction in GLOB.diagonals)
trail_dir = -1 * movement_direction
// Create a full trail on the tile we came from, and a start of a trail at the one we arrived to
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
var/continuing_trail = FALSE
// The mob is going a direction they were not previously facing
// We now factor in their facing direction to make a trail that looks like they're turning
// This is done by creating a diagonal dir
if(trail_dir != was_facing && trail_dir != REVERSE_DIR(was_facing))
// Look a step back to see if we should be constructing a diagonal dir
// If there's no existing trail making a curve would look weird
for(var/obj/effect/decal/cleanable/blood/trail_holder/past_trail in get_step(start, REVERSE_DIR(was_facing)))
if(past_trail.get_trail_component(was_facing, check_reverse = TRUE, check_diagonals = TRUE, check_reverse_diagonals = TRUE))
trail_dir |= was_facing
continuing_trail = TRUE
// In case we produced an invalid dir: go back on relevant axis
if((trail_dir & (NORTH|SOUTH)) == (NORTH|SOUTH))
trail_dir &= ~(was_facing & (NORTH|SOUTH))
if((trail_dir & (EAST|WEST)) == (EAST|WEST))
trail_dir &= ~(was_facing & (EAST|WEST))
break
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
// If we're still moving cardinally and didn't change our dir, there's a chance that there's a half-trail on our turf
// in which case we want to continue it instead of doing a full trail
// Only scenario in which we don't have one is if we just started the trail from our tile
for(var/obj/effect/decal/cleanable/blood/trail_holder/trail_holder in start)
var/obj/effect/decal/cleanable/blood/trail/trail = trail_holder.get_trail_component(trail_dir)
if (trail?.half_piece) // We're moving straight, so just continue the path
continuing_trail = TRUE
break
// If we've just started moving, put a half-trail on our previous turf instead of a full one
create_blood_trail_component(start, trail_dir, blood_to_add * 0.67, !continuing_trail)
create_blood_trail_component(target_turf, get_dir(start, target_turf), blood_to_add * 0.33, TRUE)
/*
* Locate or create a trail holder, and add a dir to it
* Arguments:
* * trail_turf - Turf on which to look for/spawn a trail
* * trail_dir - Direction in which the trail will be facing. Could be diagonal for a corner, or negative for a true diagonal trail
* * blood_to_add - How much bloodiness we should add to the trail
* * half_piece - Should we only create a beginning of a trail, and not a full tile trail? Does not support corners
*/
/mob/living/proc/create_blood_trail_component(turf/trail_turf, trail_dir, blood_to_add, half_piece)
var/obj/effect/decal/cleanable/blood/trail_holder/trail
var/check_reverse = TRUE
// Do not check the reverse dir if we're a diagonal corner or a half piece
if (trail_dir > 0 && !(trail_dir in GLOB.cardinals) || half_piece)
check_reverse = FALSE
// Pick any trail in the turf to add onto
for(var/obj/effect/decal/cleanable/blood/trail_holder/any_trail in trail_turf)
// If there exists a trail already, we will add onto the trial
// UNLESS that trail has the same direction component and it is already dried
//
// If that is the case we will look for another trail (or create a new one)
// (this will let fresh blood be laid over very dried blood)
var/obj/effect/decal/cleanable/blood/trail/any_trail_component = any_trail.get_trail_component(trail_dir, check_reverse = check_reverse)
if(isnull(any_trail_component) || !any_trail_component.dried)
trail = any_trail
break
if(!isnull(trail))
trail.adjust_bloodiness(blood_to_add)
return trail.add_dir_to_trail(trail_dir, src, blood_to_add, half_piece)
trail = new(trail_turf, get_static_viruses(), get_blood_dna_list())
if(QDELETED(trail))
return
trail.bloodiness = blood_to_add
return trail.add_dir_to_trail(trail_dir, src, blood_to_add, half_piece)
/mob/living/carbon/human/make_blood_trail(turf/target_turf, turf/start, direction)
if(!is_bleeding())
return
return ..()
/// Returns how much blood we're losing from being dragged a tile, from [/mob/living/proc/make_blood_trail]
/mob/living/proc/bleed_drag_amount()
var/brute_ratio = round(get_brute_loss() / maxHealth, 0.1)
return max(1, brute_ratio * 2)
/mob/living/carbon/bleed_drag_amount()
var/bleed_amount = 0
for(var/i in all_wounds)
var/datum/wound/iter_wound = i
bleed_amount += iter_wound.drag_bleed_amount()
return bleed_amount
/mob/living/proc/get_blood_alcohol_content()
var/blood_alcohol_content = 0
var/datum/status_effect/inebriated/inebriation = has_status_effect(/datum/status_effect/inebriated)
if(!isnull(inebriation))
blood_alcohol_content = round(inebriation.drunk_value * DRUNK_POWER_TO_BLOOD_ALCOHOL, 0.01)
return blood_alcohol_content
/// Returns on a 0-1 scale how synthetic this mobs blood is. Used for blood worms to cap growth from easily accessible sources of blood.
/mob/living/proc/get_blood_synth_content()
if (IS_BLOOD_ALWAYS_SYNTHETIC(src))
return 1 // Basic mobs, simple mobs, born monkeys and spawned mobs have fully synthetic blood.
// Handles accounting for synthetic blood injected into mobs that don't normally have it.
var/datum/component/synth_blood/synth_comp = GetComponent(/datum/component/synth_blood)
return synth_comp?.synth_content
#undef BLOOD_DRIP_RATE_MOD
#undef DRUNK_POWER_TO_BLOOD_ALCOHOL