Improve, cleanup, and expand wound code (#20757)

This PR includes several changes to bring our wound code up to date with
baystation, nebula, and modern standards. Highlights include:
- The usage of integers between 0 and 100 for ratios (expressed as a
percentage), instead of 0.0 to 1.0. Small decimal values are prone to
floating point precision errors, but by moving to a percentage instead,
we can reduce the impact of this.
- The untangling of damage flags and injury flags, which were used
interchangably in the code. This could have caused issues should we
expand any of these.
- Larger embedded objects now halt bleeding in wounds, until they are
pulled out. In exchange however, embedded objects will halt healing in
wounds (it is difficult to close an open wound that currently has a rod
stuck in it)
- A lot of data, such as implanted objects and organs, are now stored in
amputated limbs.
This commit is contained in:
Cody Brittain
2025-12-19 20:22:29 +00:00
committed by GitHub
parent 034d0e4cb1
commit 96a2951608
23 changed files with 861 additions and 575 deletions
+254
View File
@@ -0,0 +1,254 @@
/****************************************************
WOUNDS
****************************************************/
/datum/wound
/// number representing the current stage
var/current_stage = 0
/// description of the wound
var/desc = "wound" //default in case something borks
/// amount of damage this wound causes
var/damage = 0
/// how many times we've tried to lower the bleed timer, used to determine whether hemophilia should let bleed timer lower
var/bleed_timer_increment = 0
/// ticks of bleeding left.
var/bleed_timer = 0
/// Above this amount wounds you will need to treat the wound to stop bleeding, regardless of bleed_timer
var/bleed_threshold = 30
/// amount of damage the current wound type requires(less means we need to apply the next healing stage)
var/min_damage = 0
/// is the wound bandaged?
var/bandaged = FALSE
/// Similar to bandaged, but works differently
var/clamped = FALSE
/// is the wound salved?
var/salved = FALSE
/// is the wound disinfected?
var/disinfected = FALSE
var/created = 0
/// number of wounds of this type
var/amount = 1
/// amount of germs in the wound
var/germ_level = 0
/// the organ the wound is on, if on an organ
var/obj/item/organ/external/parent_organ
/* These are defined by the wound type and should not be changed */
/// stages such as "cut", "deep cut", etc.
var/list/stages
/// maximum stage at which bleeding should still happen. Beyond this stage bleeding is prevented.
var/max_bleeding_stage = 0
/// String (One of `DAMAGE_TYPE_*`). The wound's injury type.
var/damage_type = INJURY_TYPE_CUT
/// whether this wound needs a bandage/salve to heal at all
/// the maximum amount of damage that this wound can have and still autoheal
var/autoheal_cutoff = 15
// helper lists
var/list/embedded_objects
var/tmp/list/desc_list = list()
var/tmp/list/damage_list = list()
/datum/wound/New(damage, obj/item/organ/external/organ = null)
created = world.time
// reading from a list("stage" = damage) is pretty difficult, so build two separate
// lists from them instead
for(var/V in stages)
desc_list += V
damage_list += stages[V]
src.damage = damage
// initialize with the appropriate stage
src.init_stage(damage)
bleed_timer += damage
if(istype(organ))
parent_organ = organ
/datum/wound/Destroy()
if(parent_organ)
LAZYREMOVE(parent_organ.wounds, src)
parent_organ = null
LAZYCLEARLIST(embedded_objects)
. = ..()
///Returns TRUE if there's a next stage, FALSE otherwise
/datum/wound/proc/init_stage(initial_damage)
current_stage = length(stages)
while(src.current_stage > 1 && src.damage_list[current_stage-1] <= initial_damage / src.amount)
src.current_stage--
src.min_damage = damage_list[current_stage]
src.desc = desc_list[current_stage]
///The amount of damage per wound
/datum/wound/proc/wound_damage()
return src.damage / src.amount
/datum/wound/proc/can_autoheal()
if(LAZYLEN(embedded_objects))
return FALSE
return (wound_damage() <= autoheal_cutoff) ? TRUE : is_treated()
/// Checks whether the wound has been appropriately treated
/datum/wound/proc/is_treated()
if(!LAZYLEN(embedded_objects))
switch(damage_type)
if(INJURY_TYPE_BRUISE, INJURY_TYPE_CUT, INJURY_TYPE_PIERCE)
return bandaged
if(INJURY_TYPE_BURN)
return salved
/// Checks whether other other can be merged into src.
/datum/wound/proc/can_merge(datum/wound/other)
if (other.type != src.type) return FALSE
if (other.current_stage != src.current_stage) return FALSE
if (other.damage_type != src.damage_type) return FALSE
if (!(other.can_autoheal()) != !(src.can_autoheal())) return FALSE
if (!(other.bandaged) != !(src.bandaged)) return FALSE
if (!(other.clamped) != !(src.clamped)) return FALSE
if (!(other.salved) != !(src.salved)) return FALSE
if (!(other.disinfected) != !(src.disinfected)) return FALSE
if (other.parent_organ != parent_organ) return FALSE
return TRUE
/datum/wound/proc/merge_wound(datum/wound/other)
if(LAZYLEN(other.embedded_objects))
LAZYDISTINCTADD(src.embedded_objects, other.embedded_objects)
src.damage += other.damage
src.amount += other.amount
src.bleed_timer += other.bleed_timer
src.bleed_timer_increment += other.bleed_timer_increment
src.germ_level = max(src.germ_level, other.germ_level)
src.created = max(src.created, other.created) //take the newer created time
qdel(other)
/// checks if wound is considered open for external infections
/// untreated cuts (and bleeding bruises) and burns are possibly infectable, chance higher if wound is bigger
/datum/wound/proc/infection_check()
if (damage < 10) //small cuts, tiny bruises, and moderate burns shouldn't be infectable.
return FALSE
if (is_treated() && damage < 25) //anything less than a flesh wound (or equivalent) isn't infectable if treated properly
return FALSE
if (disinfected)
germ_level = 0 //reset this, just in case
return FALSE
if (damage_type == INJURY_TYPE_BRUISE && !bleeding()) //bruises only infectable if bleeding
return FALSE
var/dam_coef = round(damage/10)
switch (damage_type)
if (INJURY_TYPE_BRUISE)
return prob(dam_coef*5)
if (INJURY_TYPE_BURN)
return prob(dam_coef*10)
if (INJURY_TYPE_CUT)
return prob(dam_coef*20)
return FALSE
/datum/wound/proc/bandage()
bandaged = TRUE
/datum/wound/proc/salve()
salved = TRUE
/datum/wound/proc/disinfect()
disinfected = TRUE
/// Heal the given amount of damage, and if the given amount of damage was more than what esd needed to be healed, return how much heal was left
/datum/wound/proc/heal_damage(amount)
if(LAZYLEN(embedded_objects))
return amount // heal nothing
if(parent_organ)
if (damage_type == INJURY_TYPE_BURN && !(parent_organ.burn_ratio < 100))
return amount // We don't want to heal wounds on irreparable organs
else if(!(parent_organ.brute_ratio < 100))
return amount
var/healed_damage = min(src.damage, amount)
amount -= healed_damage
src.damage -= healed_damage
while(src.wound_damage() < damage_list[current_stage] && current_stage < length(src.desc_list))
current_stage++
desc = desc_list[current_stage]
src.min_damage = damage_list[current_stage]
// return amount of healing still leftover, can be used for other wounds
return amount
/// Opens the wound again
/datum/wound/proc/open_wound(damage)
src.damage += damage
bleed_timer += damage
while(src.current_stage > 1 && src.damage_list[current_stage-1] <= src.damage / src.amount)
src.current_stage--
src.desc = desc_list[current_stage]
src.min_damage = damage_list[current_stage]
src.bandaged = FALSE
///returns whether this wound can absorb the given amount of damage.
///this will prevent large amounts of damage being trapped in less severe wound types
/datum/wound/proc/can_worsen(damage_type, damage)
if (src.damage_type != damage_type)
return FALSE //incompatible damage types
if (src.amount > 1)
return FALSE //merged wounds cannot be worsened.
//with 1.5*, a shallow cut will be able to carry at most 30 damage,
//37.5 for a deep cut
//52.5 for a flesh wound, etc.
var/max_wound_damage = 1.5*src.damage_list[1]
if (src.damage + damage > max_wound_damage)
return FALSE
return TRUE
/datum/wound/proc/bleeding()
for(var/obj/item/thing in embedded_objects)
if(thing.w_class > WEIGHT_CLASS_SMALL)
return FALSE
if (bandaged||clamped)
return FALSE
return ((bleed_timer > 0 || wound_damage() > bleed_threshold) && current_stage <= max_bleeding_stage)
/// Called in organ_external.dm update_damages, this will update the limb's status to bleeding, and lowers the bleed_timer if applicable
/datum/wound/proc/handle_bleeding(var/mob/victim, var/obj/item/organ/external/limb)
limb.status |= ORGAN_BLEEDING
var/can_lower_bleed = TRUE
if(HAS_TRAIT(victim, TRAIT_DISABILITY_HEMOPHILIA_MAJOR)) // major will NEVER lower
can_lower_bleed = FALSE
else if(HAS_TRAIT(victim, TRAIT_DISABILITY_HEMOPHILIA) && bleed_timer_increment % 2 != 0) // basic will lower half as fast
can_lower_bleed = FALSE
if(can_lower_bleed)
bleed_timer--
bleed_timer_increment++
/datum/wound/proc/close()
return
+291
View File
@@ -0,0 +1,291 @@
/** WOUND DEFINITIONS **/
//Note that the MINIMUM damage before a wound can be applied should correspond to
//the damage amount for the stage with the same name as the wound.
//e.g. /datum/wound/cut/deep should only be applied for 15 damage and up,
//because in it's stages list, "deep cut" = 15.
/proc/get_wound_type(type = INJURY_TYPE_CUT, damage)
switch(type)
if(INJURY_TYPE_CUT)
switch(damage)
if(70 to INFINITY)
return /datum/wound/cut/massive
if(60 to 70)
return /datum/wound/cut/gaping_big
if(50 to 60)
return /datum/wound/cut/gaping
if(25 to 50)
return /datum/wound/cut/flesh
if(15 to 25)
return /datum/wound/cut/deep
if(0 to 15)
return /datum/wound/cut/small
if(INJURY_TYPE_PIERCE)
switch(damage)
if(60 to INFINITY)
return /datum/wound/puncture/massive
if(50 to 60)
return /datum/wound/puncture/gaping_big
if(30 to 50)
return /datum/wound/puncture/gaping
if(15 to 30)
return /datum/wound/puncture/flesh
if(0 to 15)
return /datum/wound/puncture/small
if(INJURY_TYPE_BRUISE)
return /datum/wound/bruise
if(INJURY_TYPE_BURN, INJURY_TYPE_LASER)
switch(damage)
if(50 to INFINITY)
return /datum/wound/burn/carbonised
if(40 to 50)
return /datum/wound/burn/deep
if(30 to 40)
return /datum/wound/burn/severe
if(15 to 30)
return /datum/wound/burn/large
if(0 to 15)
return /datum/wound/burn/moderate
return null //no wound
/** CUTS **/
/datum/wound/cut
bleed_threshold = 5
damage_type = INJURY_TYPE_CUT
/datum/wound/cut/bandage()
..()
if(!autoheal_cutoff)
autoheal_cutoff = initial(autoheal_cutoff)
/datum/wound/cut/close()
current_stage = max_bleeding_stage + 1
desc = desc_list[current_stage]
min_damage = damage_list[current_stage]
if(damage > min_damage)
heal_damage(damage-min_damage)
/datum/wound/cut/small
// link wound descriptions to amounts of damage
// Minor cuts have max_bleeding_stage set to the stage that bears the wound type's name.
// The major cut types have the max_bleeding_stage set to the clot stage (which is accordingly given the "blood soaked" descriptor).
max_bleeding_stage = 3
stages = list(
"ugly ripped cut" = 20,
"ripped cut" = 10,
"cut" = 5,
"healing cut" = 2,
"small scab" = 0
)
/datum/wound/cut/deep
max_bleeding_stage = 3
stages = list(
"ugly deep ripped cut" = 25,
"deep ripped cut" = 20,
"deep cut" = 15,
"clotted cut" = 8,
"scab" = 2,
"fresh skin" = 0
)
/datum/wound/cut/flesh
max_bleeding_stage = 4
stages = list(
"ugly ripped flesh wound" = 35,
"ugly flesh wound" = 30,
"flesh wound" = 25,
"blood soaked clot" = 15,
"large scab" = 5,
"fresh skin" = 0
)
/datum/wound/cut/gaping
max_bleeding_stage = 3
stages = list(
"gaping wound" = 50,
"large blood soaked clot" = 25,
"blood soaked clot" = 15,
"small angry scar" = 5,
"small straight scar" = 0
)
/datum/wound/cut/gaping_big
max_bleeding_stage = 3
stages = list(
"big gaping wound" = 60,
"healing gaping wound" = 40,
"large blood soaked clot" = 25,
"large angry scar" = 10,
"large straight scar" = 0
)
/datum/wound/cut/massive
max_bleeding_stage = 3
stages = list(
"massive wound" = 70,
"massive healing wound" = 50,
"massive blood soaked clot" = 25,
"massive angry scar" = 10,
"massive jagged scar" = 0
)
/** PUNCTURES **/
/datum/wound/puncture
bleed_threshold = 10
damage_type = INJURY_TYPE_PIERCE
/datum/wound/puncture/can_worsen(damage_type, damage)
return FALSE //puncture wounds cannot be enlargened
/datum/wound/puncture/small
max_bleeding_stage = 2
stages = list(
"puncture" = 5,
"healing puncture" = 2,
"small scab" = 0
)
/datum/wound/puncture/flesh
max_bleeding_stage = 2
stages = list(
"puncture wound" = 15,
"blood soaked clot" = 5,
"large scab" = 2,
"small round scar" = 0
)
/datum/wound/puncture/gaping
max_bleeding_stage = 3
stages = list(
"gaping hole" = 30,
"large blood soaked clot" = 15,
"blood soaked clot" = 10,
"small angry scar" = 5,
"small round scar" = 0
)
/datum/wound/puncture/gaping_big
max_bleeding_stage = 3
stages = list(
"big gaping hole" = 50,
"healing gaping hole" = 20,
"large blood soaked clot" = 15,
"large angry scar" = 10,
"large round scar" = 0
)
/datum/wound/puncture/massive
max_bleeding_stage = 3
stages = list(
"massive wound" = 60,
"massive healing wound" = 30,
"massive blood soaked clot" = 25,
"massive angry scar" = 10,
"massive jagged scar" = 0
)
/** BRUISES **/
/datum/wound/bruise
stages = list(
"monumental bruise" = 80,
"huge bruise" = 50,
"large bruise" = 30,
"moderate bruise" = 20,
"small bruise" = 10,
"tiny bruise" = 5
)
bleed_threshold = 20
max_bleeding_stage = 3 //only large bruise and above can bleed.
autoheal_cutoff = 30
damage_type = INJURY_TYPE_BRUISE
/** BURNS **/
/datum/wound/burn
damage_type = INJURY_TYPE_BURN
max_bleeding_stage = 0
/datum/wound/burn/bleeding()
return FALSE
/datum/wound/burn/moderate
stages = list(
"ripped burn" = 10,
"moderate burn" = 5,
"healing moderate burn" = 2,
"fresh skin" = 0
)
/datum/wound/burn/large
stages = list(
"ripped large burn" = 20,
"large burn" = 15,
"healing large burn" = 5,
"fresh skin" = 0
)
/datum/wound/burn/severe
stages = list(
"ripped severe burn" = 35,
"severe burn" = 30,
"healing severe burn" = 10,
"burn scar" = 0
)
/datum/wound/burn/deep
stages = list(
"ripped deep burn" = 45,
"deep burn" = 40,
"healing deep burn" = 15,
"large burn scar" = 0
)
/datum/wound/burn/carbonised
stages = list(
"carbonised area" = 50,
"healing carbonised area" = 20,
"massive burn scar" = 0
)
/** EXTERNAL ORGAN LOSS **/
/datum/wound/lost_limb
var/limb_tag
/datum/wound/lost_limb/New(obj/item/organ/external/lost_limb, losstype, clean)
var/damage_amt = lost_limb.max_damage
if(clean) damage_amt /= 2
limb_tag = lost_limb.organ_tag
switch(losstype)
if(DROPLIMB_EDGE, DROPLIMB_BLUNT)
damage_type = INJURY_TYPE_CUT
if(BP_IS_ROBOTIC(lost_limb))
max_bleeding_stage = -1 //Robotic stumps do not bleed
bleed_threshold = INFINITY
stages = list("mangled robotic socket" = 0)
else
max_bleeding_stage = 3 //clotted stump and above can bleed.
stages = list(
"ripped stump" = damage_amt*1.3,
"bloody stump" = damage_amt,
"clotted stump" = damage_amt*0.5,
"scarred stump" = 0
)
if(DROPLIMB_BURN)
damage_type = INJURY_TYPE_BURN
stages = list(
"ripped charred stump" = damage_amt*1.3,
"charred stump" = damage_amt,
"scarred stump" = damage_amt*0.5,
"scarred stump" = 0
)
..(damage_amt)
/datum/wound/lost_limb/can_merge(var/datum/wound/other)
return 0 //cannot be merged