Files
RikuTheKillerandGitHub c7cb0674cc Preliminary blood refactor (#93854)
## About The Pull Request

Moves all blood handling into procs and adds ways to easily hook into
basically every basic blood behavior.

This PR is not meant to fix every single case of janky blood logic in
the game. The main point and motivation of this PR is to add hooks for
blood behaviors. This allows for way more flexibility with blood code.

I am not going to fix our 3000 instances of single-letter vars, wacky
blood transfers, etc. This is just the groundwork for future PRs to
build off of, and by itself, should do very little to change blood
behavior.

I also added a rigorous set of unit tests for verifying that all of the
basic blood volume procs work correctly.

## Why It's Good For The Game

Previously, blood was handled via directly reading/writing
[var/blood_volume]. This was INCREDIBLY inconsistent and there was no
way to hook into it. This PR makes blood handling way more consistent,
which is great for all sorts of features.
2025-11-13 11:45:36 -06:00

143 lines
6.1 KiB
Plaintext

// Unique damage types we can apply for blacklisting, so we dont remove all organs twice etc
#define CORPSE_DAMAGE_ORGAN_DECAY "organ decay"
#define CORPSE_DAMAGE_ORGAN_LOSS "organ loss"
#define CORPSE_DAMAGE_LIMB_LOSS "limb loss"
/// The main style controller for new dead bodies! Determines the character, lore, possible causes of death, decay and every other modifier!
/datum/corpse_damage_class
/// Message sent to the recovered crew, constructed from the different death lores
var/list/death_lore = list()
/// Lore we give on revival, this is the first line
var/area_lore = "I was doing something"
/// Weight given to this class. Setting this is all that's needed for it to be rollable
var/weight
/// Different character archetypes we can spawn
var/list/possible_character_types
/// Assignments that can be given to the corpse
var/list/possible_character_assignments
/// Possible flavors that can be applied to the corpse
var/list/possible_flavor_types
/// Whatever killed us
var/list/possible_causes_of_death
/// Goddamn space vultures stealing my organs
var/list/post_mortem_effects
/// A random decay we apply. Defined here so we can vary it (i.e. a spaced body has less decay than one in a warm jungle)
var/list/decays = list(/datum/corpse_damage/post_mortem/organ_decay)
/// When healthscanned, this is the minimum time that shows
var/lore_death_time_min = 1 DAYS
/// When healthscanned, this is the maximum time that shows
var/lore_death_time_max = 5 YEARS
/// Generate and apply a possible character (species etc)
/datum/corpse_damage_class/proc/apply_character(mob/living/carbon/human/fashion_corpse, list/protected_objects, list/recovered_items, list/datum/callback/on_revive_and_player_occupancy, list/body_data)
var/datum/corpse_character/character = pick_weight(possible_character_types)
character = new character()
character.apply_character(fashion_corpse, protected_objects, recovered_items, on_revive_and_player_occupancy)
var/datum/corpse_assignment/assignment = pick_weight(possible_character_assignments)
if(ispath(assignment))
assignment = new assignment()
assignment.apply_assignment(fashion_corpse, protected_objects, recovered_items, on_revive_and_player_occupancy)
body_data += assignment.type
var/datum/corpse_flavor/flavor = pick_weight(possible_flavor_types)
if(ispath(flavor))
flavor = new flavor()
flavor?.apply_flavor(fashion_corpse, protected_objects, on_revive_and_player_occupancy)
body_data += flavor.type
death_lore += assignment?.job_lore
body_data += character.type
/// Set up injuries
/datum/corpse_damage_class/proc/apply_injuries(mob/living/carbon/human/victim, list/saved_objects, list/datum/callback/on_revive_and_player_occupancy, list/body_data)
var/bonus_roll = prob(80) //do an extra turn for cause of death or post mortum effect
var/bonus_roll_used = FALSE
var/list/used_damage_types = list()
var/datum/corpse_damage/cause_of_death/cause_of_death
for(var/i in 0 to 2)
cause_of_death = pick_damage_type(possible_causes_of_death, used_damage_types, bonus_roll_used)
if(!cause_of_death) //we ran out of causes of death (impressive!)
break
cause_of_death.apply_to_body(victim, rand(), saved_objects, on_revive_and_player_occupancy)
body_data += cause_of_death.type
if(cause_of_death.no_bonus_roll || !bonus_roll || prob(70)) //70% chance to do the bonus roll, otherwise pass the bonus to post mortem
break
bonus_roll = FALSE
bonus_roll_used = TRUE
for(var/i in 0 to 2)
var/datum/corpse_damage/post_mortem = pick_damage_type(post_mortem_effects, used_damage_types)
if(post_mortem)
post_mortem.apply_to_body(victim, rand(), saved_objects, on_revive_and_player_occupancy)
body_data += post_mortem.type
if(!bonus_roll)
break
bonus_roll = FALSE
var/datum/corpse_damage/decay = pick_damage_type(decays, used_damage_types)
decay?.apply_to_body(victim, rand(), saved_objects, on_revive_and_player_occupancy)
body_data += decay.type
// Simulate bloodloss by dragging/moving
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
/// Wrapped pickweight so we can have a bit more controle over how we pick our rules
/datum/corpse_damage_class/proc/pick_damage_type(list/damages, list/used_damage_types, bonus_roll_used)
var/list/possible_damages = list()
for(var/datum/corpse_damage/damage as anything in damages)
if(ispath(damage) && initial(damage.damage_type) && (initial(damage.damage_type) in used_damage_types) || bonus_roll_used && initial(damage.no_bonus_roll))
continue
possible_damages[damage] = damages[damage]
var/datum/corpse_damage/chosen = pick_weight(possible_damages)
if(!ispath(chosen)) //can also be null for some variants
return null
chosen = new chosen()
if(chosen.damage_type)
used_damage_types += chosen.damage_type
return chosen
/// Soulfully give a date of death for health analyzers
/datum/corpse_damage_class/proc/set_death_date(mob/living/carbon/body)
var/died_how_long_ago = rand(lore_death_time_min, lore_death_time_max)
body.timeofdeath = world.time - died_how_long_ago
var/death_real_time = world.realtime - died_how_long_ago
var/current_date = time2text(death_real_time, "DD Month", 0)
var/current_year = text2num(time2text(death_real_time, "YYYY", NO_TIMEZONE)) + STATION_YEAR_OFFSET
body.station_timestamp_timeofdeath = "[current_date] [current_year]"
/// Main corpse damage type that's used to apply damages to a body
/datum/corpse_damage
/// When given, automatically blacklist corpse_damages with the same damage_type flag to avoid stuff like being delimbed twice (dragon ate me AND I got space vultures???)
var/damage_type
/// if TRUE this can only be the only cause of death, and not multiple at once
var/no_bonus_roll = FALSE
/// Tear IT UPPP!!! Apply any damages to the body that we need to
/datum/corpse_damage/proc/apply_to_body(mob/living/carbon/human/body, severity, list/storage)
return
/// This is the reason we died
/datum/corpse_damage/cause_of_death
/// I was in x, [when I ....]
var/cause_of_death = "when I tripped and died."
/// Some post mortem damages from space vultures
/datum/corpse_damage/post_mortem