Lung cleanup ft. minor suffocation tweak (#96659)

## About The Pull Request

### Notable changes

Before: When taking suffocation damage, 3 oxyloss damage is dealt, or
0.66 damage if below 0 health (soft crit threshold). If below 0 hp, and
you have `NOCRITDAMAGE`, gets nullified.
After: When taking suffocation damage, 3 oxyloss damage is dealt,
reduced if you had a partially successful breath (i.e. 75% of a breath =
0.25x damage) or are in soft crit or hard crit (0.22x less damage). If
in crit, and you have `NOCRITDAMAGE`, gets nullified.

Before: You skip 1 in every 4 breaths when your health is below 0 (soft
crit threshold), and skip every breath when your health is below -30
(hard crit threshold)
After: You skip 1 in every 4 breaths when in soft crit, and skip every
breath when in hard crit

Before: You don't heal from successful breaths when below 0 health (soft
crit threshold)
After: You don't heal from successful breaths when in soft or hard crit

### Other changes

- Removed `gasp` from losebreath processing, ie removed chance to gasp
twice in one tick when choking.
- Deletes `respiration_type`s
- Deletes `crit_stabilizing_reagent`
- Deletes species `breathid`, replaces it with `get_breath_type` that
checks the species' lungs

## Why It's Good For The Game

- There was a strange inconsistency where being in crit would
drastically reduce the amount of suffocation damage you were dealt, but
only if you were not breathing. If you were somehow breathing, you took
the full damage. I thought this was a little obtuse, so I wanted to
change it to be consistent: Being in crit is just a multiplier.

- Making them check for `stat` instead of `health` means they affect
people with `NO_SOFTCRIT` or `NO_HARDCRIT` nicer.

- See above.

- Really just to be cleaner, handle gasping in one place.

- I'm not sure why we have `respiration_type`. The only time they're
(practically) referred to is in reagent handling, but there are no
reagents that set it anything but `ALL`. All it does is make our oxyloss
handling made more complicated and harder to read for no purpose. So I
decided to axe it - I think anything special handing that should arise
should just have some bespoke checks instead.

- Nothing set this value to anything else. And guess what, if you
changed it, epinephrine still worked because it applies `NO_CRITDAMAGE`.

- Species de-hardcoding. Makes it easier to add new breath types. 


## Changelog

🆑 Melbert
balance: Suffocation damage is slightly more consistent now: You take 3
oxyloss per suffocation tick, reduced for partially successful breaths
and or if you are in crit. Alternatively, if you are in crit and you
have epinephrine/atropine in your system, it gets nullified entirely.
code: Cleaned up lungs a bit, particularly relating to abnormal lungs
like plasmamen or fish, report any oddities with them.
del: You no longer have a chance to gasp twice in one suffocation tick.
/🆑

---------

Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
Co-authored-by: san7890 <the@san7890.com>
This commit is contained in:
MrMelbert
2026-07-02 01:59:27 +00:00
committed by GitHub
co-authored by Ghom san7890
parent 94ecd1d5a6
commit cb87e54d93
24 changed files with 119 additions and 178 deletions
@@ -7,8 +7,6 @@
gender = PLURAL
w_class = WEIGHT_CLASS_SMALL
var/respiration_type = NONE // The type(s) of gas this lung needs for respiration
healing_factor = STANDARD_ORGAN_HEALING
decay_factor = STANDARD_ORGAN_DECAY * 0.9 // fails around 16.5 minutes, lungs are one of the last organs to die (of the ones we have)
@@ -111,22 +109,12 @@
var/heat_level_3_damage = HEAT_GAS_DAMAGE_LEVEL_3
var/heat_damage_type = BURN
var/crit_stabilizing_reagent = /datum/reagent/medicine/epinephrine
var/breath_noise = "steady in- and exhalation"
// assign the respiration_type
/obj/item/organ/lungs/Initialize(mapload)
. = ..()
breath_out = new(BREATH_VOLUME)
if(safe_nitro_min)
respiration_type |= RESPIRATION_N2
if(safe_oxygen_min)
respiration_type |= RESPIRATION_OXYGEN
if(safe_plasma_min)
respiration_type |= RESPIRATION_PLASMA
// Sets up what gases we want to react to, and in what way
// always is always processed, while_present is called when the gas is in the breath, and on_loss is called right after a gas is lost
// The naming convention goes like this
@@ -264,7 +252,7 @@
breathe_gas_volume(breath, /datum/gas/oxygen, /datum/gas/carbon_dioxide)
// Heal mob if not in crit.
if(breather.health >= breather.crit_threshold && breather.oxyloss)
if(breather.stat != SOFT_CRIT && breather.stat != HARD_CRIT && breather.get_oxy_loss())
breather.adjust_oxy_loss(-5)
/// Maximum Oxygen effects. "Too much O2!"
@@ -736,32 +724,28 @@
/obj/item/organ/lungs/proc/handle_suffocation(mob/living/carbon/human/suffocator = null, breath_pp = 0, safe_breath_min = 0, mole_count = 0)
. = 0
// Can't suffocate without a Human, or without minimum breath pressure.
if(!suffocator || !safe_breath_min)
if(isnull(suffocator) || safe_breath_min <= 0)
return
// Mob is suffocating.
suffocator.failed_last_breath = TRUE
// Give them a chance to notice something is wrong.
if(prob(20))
if(prob(25))
suffocator.emote("gasp")
// If mob is at critical health, check if they can be damaged further.
if(suffocator.health < suffocator.crit_threshold)
// Mob is immune to damage at critical health.
if(HAS_TRAIT(suffocator, TRAIT_NOCRITDAMAGE))
return
// Reagents like Epinephrine stop suffocation at critical health.
if(suffocator.reagents.has_reagent(crit_stabilizing_reagent, needs_metabolizing = TRUE))
return
// Low pressure.
if(breath_pp)
var/ratio = safe_breath_min / breath_pp
suffocator.apply_damage(min(5 * ratio, HUMAN_MAX_OXYLOSS), OXY)
return mole_count * ratio / 6
// Zero pressure.
if(suffocator.health >= suffocator.crit_threshold)
suffocator.apply_damage(HUMAN_MAX_OXYLOSS, OXY)
else
suffocator.apply_damage(HUMAN_CRIT_MAX_OXYLOSS, OXY)
// note: this is where crit damage is handled for mobs that breathe
var/oxy_damage_dealt = SUFFOCATION_OXYLOSS
if(breath_pp > 0)
// we got a partial breath, scale the damage based on how much we got
oxy_damage_dealt *= ((safe_breath_min - breath_pp) / safe_breath_min)
. = mole_count
// in hard crit, suffocation damage is reduced significantly (or to zero if the relevant trait is present)
if(suffocator.stat == SOFT_CRIT || suffocator.stat == HARD_CRIT)
oxy_damage_dealt *= (HAS_TRAIT(suffocator, TRAIT_NOCRITDAMAGE) ? 0 : SUFFOCATION_OXYLOSS_CRIT_MODIFIER)
if(oxy_damage_dealt > 0)
suffocator.apply_damage(oxy_damage_dealt, OXY)
return .
/obj/item/organ/lungs/proc/handle_breath_temperature(datum/gas_mixture/breath, mob/living/carbon/human/breather) // called by human/life, handles temperatures
var/breath_temperature = breath.temperature