Asthma quirk, inhalers - Electric Boogaloo (#92747)

## About The Pull Request

Revival of https://github.com/tgstation/tgstation/pull/79691

A while back, I made this PR, but lost motivation after diving too deep
into the code soup of can_breathe and related procs. Now, I have removed
those parts, and have simplified that part of the code to the point I
think it's ready for review.

Many reviews from the previous PR have been addressed in this PR.

<hr>

<details>
  <summary>Details</summary>

Asthma is a 4 point negative quirk that emulates real life asthma. It
works by slowly decreasing the amount of pressure each breath you take
receives, until your lungs completely seal, ensuring death if you dont
get oxyloss meds/windpipe surgery.

Inflammation (the tracker for intensity) increases whenever you breathe
smoke, use a cigarette, metabolize histimine, or suffer an asthma
attack.

Asthma attacks have a low chance of happening every second, starting 10
minutes after you spawn and with a 20-30 grace period between attacks.
They are "diseases" that cant be outright cured by albuterol, only put
into remission. They increase inflammation at varying rates depending on
their severity, with extreme asthma attacks being a immediate threat to
your life while mild ones might not even cause inflammation. The
response to these is always the same - use your inhaler before you start
choking.

Asthmatics start with a rescue inhaler, a low-capacity inhaler loaded
with albuterol, which I will get to later.

Albuterol is a new medicine thats a little tricky to make but still
doable. It can be efficiently created with inverse convermol, or
transmutated from salbutamol and convermol. The opposite is true, with
albuterol able to be turned into salbutamol. Two canisters are available
in chemvends.

Upon use, it increases the virtual pressure of all breaths taken by 40%.
This allows for you to breathe in lower pressure environments, as well
as enhancing the effects of things like healium.

It's OD causes your diaphram to spasm, causing sporadic losebreath and
forced breathing.

Inhalers are a fancy new reagent application apparatus that uses the
INHALE reagent bitflag.

Inhalers themselves are rather unremarkable, they are merely the method
of using inhaler canisters (they also have a rotary display
approximating the uses left in a canister - just like real life
inhalers).

Inhaler canisters are the reagent containers, and are generally low
capacity. They can only be used in a inhaler, and contain aerosolized
chemicals.

Inhaler canisters and inhalers are unlocked from chemical synthesis, and
are printable for cheap from a medlathe.

In order to use a inhaler, one must uncover the mouth of a carbon and
wait a few seconds (its faster if its a self-application) before a small
amount of the reagents are delivered via the INHALE bitflag. This only
works on things currently breathing - if theyre dead, have no lungs, or
just, arent breathing - it will fail. This includes asthmatics with 100%
inflammation.

</details>

<img width="181" height="74"
alt="282863233-77a7cd6b-44d2-458e-9966-06d485df1521"
src="https://github.com/user-attachments/assets/293b6659-0834-4e9a-b033-cc3b0cfde18e"
/>

<img width="1465" height="202"
alt="282863346-2a247736-0c3a-43b0-a60b-7cff10ce4963"
src="https://github.com/user-attachments/assets/5d9a13dc-b7b2-4de2-adda-8fbc8276e667"
/>

Sprites are not mine; they are from swanni and I can NOT sprite for the
life of me
## Why It's Good For The Game

1. Asthma is just cool. One of my favorite features on bay was the fact
lung damage required you to turn up the pressure on your O2 tank to
survive, and this does precisely that.
2. Its always fun to add new ways to interact with atmos as a player
that arent grossly broken, and I fail to see how a 40% increase of gas
intake will really affect balance too badly.
3. Inhalers are badass.
## Changelog
🆑
add: Asthma quirk, based on IRL asthma
add: Inhalers, a new reagent administering method that uses INHALE
add: Albuterol, a new reagent that increases the amount of gas you
inhale by 40%
balance: Inverse convermol now forms once the reaction is done, not on
metabolize
/🆑

---------

Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
# Conflicts:
#	code/datums/quirks/negative_quirks/allergic.dm
#	code/game/objects/items/devices/scanners/health_analyzer.dm
This commit is contained in:
nikothedude
2025-09-07 21:31:21 -04:00
committed by nevimer
parent 6b8259c167
commit ee99d0e5b0
25 changed files with 1176 additions and 7 deletions

View File

@@ -14,6 +14,8 @@
#define ALERT_TOO_MUCH_NITRO "too_much_nitro"
#define ALERT_NOT_ENOUGH_NITRO "not_enough_nitro"
#define ALERT_BRONCHODILATION "bronchodilation"
#define ALERT_NOT_ENOUGH_WATER "not_enough_water"
/** Mob related */

View File

@@ -179,3 +179,6 @@
/// From /mob/living/carbon/proc/set_blood_type : (mob/living/carbon/user, datum/blood_type, update_cached_blood_dna_info)
#define COMSIG_CARBON_CHANGED_BLOOD_TYPE "carbon_set_blood_type"
//from base of [/obj/effect/particle_effect/fluid/smoke/proc/smoke_mob]: (seconds_per_tick)
#define COMSIG_CARBON_EXPOSED_TO_SMOKE "carbon_exposed_to_smoke"

View File

@@ -18,6 +18,8 @@ DEFINE_BITFIELD(visibility_flags, list(
#define CAN_CARRY (1<<1)
#define CAN_RESIST (1<<2)
#define CHRONIC (1<<3)
/// Instead of instantly curing the disease, cures will simply reduce the stage
#define INCREMENTAL_CURE (1<<4)
//Spread Flags
#define DISEASE_SPREAD_SPECIAL (1<<0)

View File

@@ -45,7 +45,7 @@
#define INJECT (1<<4)
/// Exclusive to just plumbing. if set we use the round robin technique else we use proportional
#define LINEAR (1<<5)
/// Used by smoke or inhaling from a source. Smoke and cigarettes.
/// Used by smoke or inhaling from a source. Smoke, cigarettes, and inhalers.
#define INHALE (1<<6)
///Smoke machines are both touch and inhaling

View File

@@ -199,6 +199,16 @@
//End gas alerts
/atom/movable/screen/alert/bronchodilated
name = "Bronchodilated"
desc = "You feel like your lungs are larger than usual! You're taking deeper breaths!"
icon_state = "bronchodilated"
/atom/movable/screen/alert/bronchoconstricted
name = "Bronchocontracted"
desc = "You feel like your lungs are smaller than usual! You might need a higher pressure environment/internals to breathe!"
icon_state = "bronchoconstricted"
/atom/movable/screen/alert/gross
name = "Grossed out."
desc = "That was kind of gross..."

View File

@@ -134,11 +134,13 @@
update_stage(1)
to_chat(affected_mob, span_notice("Your chronic illness is alleviated a little, though it can't be cured!"))
return
if(SPT_PROB(cure_mod, seconds_per_tick))
update_stage(max(stage - 1, 1))
if(disease_flags & CURABLE && SPT_PROB(cure_mod, seconds_per_tick))
cure()
return FALSE
if(disease_flags & INCREMENTAL_CURE)
if (!update_stage(stage - 1))
return FALSE
else
cure()
return FALSE
if(stage == max_stages && stage_peaked != TRUE) //mostly a sanity check in case we manually set a virus to max stages
stage_peaked = TRUE
@@ -257,6 +259,10 @@
// BUBBER EDIT ADDITION END - DISEASE OUTBREAK UPDATES
if(new_stage == max_stages && !(stage_peaked)) //once a virus has hit its peak, set it to have done so
stage_peaked = TRUE
if (stage <= 0)
cure()
return FALSE
return TRUE
/datum/disease/proc/has_cure()
if(!(disease_flags & (CURABLE | CHRONIC)))

View File

@@ -0,0 +1,262 @@
/datum/disease/asthma_attack
form = "Bronchitis"
name = "Asthma attack"
desc = "Subject is undergoing a autoimmune response which threatens to close the esophagus and halt all respiration, leading to death. \
Minor asthma attacks may disappear on their own, but all are dangerous."
cure_text = "Albuterol/Surgical intervention"
cures = list(/datum/reagent/medicine/albuterol)
agent = "Inflammatory"
viable_mobtypes = list(/mob/living/carbon/human)
disease_flags = CURABLE
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
spread_text = "Inflammatory"
visibility_flags = HIDDEN_PANDEMIC
bypasses_immunity = TRUE
disease_flags = CURABLE|INCREMENTAL_CURE
required_organ = ORGAN_SLOT_LUNGS
infectable_biotypes = MOB_ROBOTIC|MOB_ORGANIC|MOB_MINERAL|MOB_UNDEAD
/// The world.time after which we will begin remission.
var/time_to_start_remission
/// The max time, after initial infection, it will take for us to begin remission
var/max_time_til_remission
/// The min time, after initial infection, it will take for us to begin remission
var/min_time_til_remission
/// Are we in remission, where we stop progressing and instead slowly degrade in intensity until we remove ourselves?
var/in_remission = FALSE
/// The current progress to stage demotion. Resets to 0 and reduces our stage by 1 when it exceeds [progress_needed_to_demote]. Only increases when in remission.
var/progress_to_stage_demotion = 0
/// The amount of demotion progress we receive per second while in remission.
var/progress_to_demotion_per_second = 1
/// Once [progress_to_stage_demotion] exceeds or meets this, we reduce our stage.
var/progress_needed_to_demote = 10
/// Do we alert ghosts when we are applied?
var/alert_ghosts = FALSE
/// A assoc list of (severity -> string), where string will be suffixed to our name in (suffix) format.
var/static/list/severity_to_suffix = list(
DISEASE_SEVERITY_MEDIUM = "Minor",
DISEASE_SEVERITY_HARMFUL = "Moderate",
DISEASE_SEVERITY_DANGEROUS = "Severe",
DISEASE_SEVERITY_BIOHAZARD = "EXTREME",
)
/// A assoc list of (stringified number -> number), where the key is the stage and the number is how much inflammation we will cause the asthmatic per second.
var/list/stage_to_inflammation_per_second
/datum/disease/asthma_attack/New()
. = ..()
suffix_name()
time_to_start_remission = world.time + rand(min_time_til_remission, max_time_til_remission)
/datum/disease/asthma_attack/try_infect(mob/living/infectee, make_copy)
if (!get_asthma_quirk())
return FALSE
if (HAS_TRAIT(infectee, TRAIT_NOBREATH))
return FALSE
return ..()
/// Adds our suffix via [severity_to_suffix] in the format of (suffix) to our name.
/datum/disease/asthma_attack/proc/suffix_name()
name += " ([severity_to_suffix[severity]])"
/// Returns the asthma quirk of our victim. As we can only be applied to asthmatics, this should never return null.
/datum/disease/asthma_attack/proc/get_asthma_quirk(mob/living/target = affected_mob)
RETURN_TYPE(/datum/quirk/item_quirk/asthma)
return (locate(/datum/quirk/item_quirk/asthma) in target.quirks)
/datum/disease/asthma_attack/stage_act(seconds_per_tick, times_fired)
. = ..()
if (!.)
return
if (HAS_TRAIT(affected_mob, TRAIT_NOBREATH))
cure()
return FALSE
var/datum/quirk/item_quirk/asthma/asthma_quirk = get_asthma_quirk()
var/inflammation = stage_to_inflammation_per_second["[stage]"]
if (inflammation)
asthma_quirk.adjust_inflammation(inflammation * seconds_per_tick)
if (!(world.time >= time_to_start_remission))
return
if (!in_remission)
in_remission = TRUE
stage_prob = 0
name += " (Remission)"
desc += " <i>The attack has entered remission. It will slowly decrease in intensity before vanishing.</i>"
progress_to_stage_demotion += (progress_to_demotion_per_second * seconds_per_tick)
if (progress_to_stage_demotion >= progress_needed_to_demote)
progress_to_stage_demotion = 0
update_stage(stage - 1)
// TYPES OF ASTHMA ATTACK
/datum/disease/asthma_attack/minor
severity = DISEASE_SEVERITY_MEDIUM
stage_prob = 4
max_time_til_remission = 120 SECONDS
min_time_til_remission = 80 SECONDS
max_stages = 3
cure_chance = 20
stage_to_inflammation_per_second = list(
"2" = 0.3,
"3" = 0.6,
)
/datum/disease/asthma_attack/minor/stage_act(seconds_per_tick, times_fired)
. = ..()
if (!.)
return FALSE
if (SPT_PROB(5, seconds_per_tick))
to_chat(affected_mob, span_warning(pick("Mucous runs down the back of your throat.", "You swallow excess mucus.")))
/datum/disease/asthma_attack/moderate
severity = DISEASE_SEVERITY_HARMFUL
stage_prob = 5
max_time_til_remission = 120 SECONDS
min_time_til_remission = 80 SECONDS
max_stages = 4
cure_chance = 20
stage_to_inflammation_per_second = list(
"2" = 1,
"3" = 2,
"4" = 4,
)
/datum/disease/asthma_attack/moderate/stage_act(seconds_per_tick, times_fired)
. = ..()
if (!.)
return FALSE
if (SPT_PROB(15, seconds_per_tick))
to_chat(affected_mob, span_warning(pick("Mucous runs down the back of your throat.", "You swallow excess mucus.")))
if (stage < 4 || !SPT_PROB(10, seconds_per_tick))
return
to_chat(affected_mob, span_warning("You briefly choke on the mucus piling in your throat!"))
affected_mob.losebreath++
/datum/disease/asthma_attack/severe
severity = DISEASE_SEVERITY_DANGEROUS
stage_prob = 6
max_time_til_remission = 80 SECONDS
min_time_til_remission = 60 SECONDS
max_stages = 5
cure_chance = 20
stage_to_inflammation_per_second = list(
"2" = 1,
"3" = 3,
"4" = 6,
"5" = 8,
)
visibility_flags = HIDDEN_SCANNER
alert_ghosts = TRUE
/datum/disease/asthma_attack/severe/stage_act(seconds_per_tick, times_fired)
. = ..()
if (!.)
return FALSE
if (stage > 1)
visibility_flags &= ~HIDDEN_SCANNER // revealed
if (SPT_PROB(15, seconds_per_tick))
to_chat(affected_mob, span_warning(pick("Mucous runs down the back of your throat.", "You swallow excess mucus.")))
else if (SPT_PROB(20, seconds_per_tick))
affected_mob.emote("cough")
if (stage < 4 || !SPT_PROB(15, seconds_per_tick))
return
to_chat(affected_mob, span_warning("You briefly choke on the mucus piling in your throat!"))
affected_mob.losebreath++
/datum/disease/asthma_attack/critical
severity = DISEASE_SEVERITY_BIOHAZARD
stage_prob = 85
max_time_til_remission = 60 SECONDS // this kills you extremely quickly, so its fair
min_time_til_remission = 40 SECONDS
max_stages = 6
cure_chance = 30
stage_to_inflammation_per_second = list(
"1" = 5,
"2" = 6,
"3" = 7,
"4" = 10,
"5" = 20,
"6" = 500, // youre fucked frankly
)
/// Have we warned our user of the fact they are at stage 5? If no, and are at or above stage five, we send a warning and set this to true.
var/warned_user = FALSE
/// Have we ever reached our max stage? If no, and we are at our max stage, we send a ominous message warning them of their imminent demise.
var/max_stage_reached = FALSE
/datum/disease/asthma_attack/critical/stage_act(seconds_per_tick, times_fired)
. = ..()
if (!.)
return FALSE
if (stage < 5)
if (SPT_PROB(75, seconds_per_tick))
to_chat(affected_mob, span_warning(pick("Mucous runs down the back of your throat.", "You swallow excess mucus.")))
var/wheeze_chance
if (!warned_user && stage >= 5)
to_chat(affected_mob, span_userdanger("You feel like your lungs are filling with fluid! It's getting incredibly hard to breathe!"))
warned_user = TRUE
switch (stage)
if (1)
wheeze_chance = 0
if (2)
wheeze_chance = 20
if (3)
wheeze_chance = 40
if (4)
wheeze_chance = 60
if (5)
wheeze_chance = 80
if (!in_remission)
stage_prob = 10 // slow it down significantly
if (6)
if (!max_stage_reached)
max_stage_reached = TRUE
to_chat(affected_mob, span_userdanger("You feel your windpipe squeeze shut!"))
wheeze_chance = 0
if (SPT_PROB(10, seconds_per_tick))
affected_mob.emote("gag")
var/datum/quirk/item_quirk/asthma/asthma_quirk = get_asthma_quirk()
asthma_quirk.adjust_inflammation(INFINITY)
if (SPT_PROB(wheeze_chance, seconds_per_tick))
affected_mob.emote("wheeze")
if (stage < 4 || !SPT_PROB(15, seconds_per_tick))
return
to_chat(affected_mob, span_warning("You briefly choke on the mucus piling in your throat!"))
affected_mob.losebreath++

View File

@@ -22,6 +22,7 @@
/datum/reagent/medicine/diphenhydramine,
/datum/reagent/medicine/sansufentanyl,
/datum/reagent/medicine/salglu_solution,
/datum/reagent/medicine/albuterol,
/datum/reagent/medicine/coagulant //Bubber edit: adds coagulant
)
var/allergy_string

View File

@@ -0,0 +1,249 @@
/datum/quirk/item_quirk/asthma
name = "Asthma"
desc = "You suffer from asthma, a inflammatory disorder that causes your airpipe to squeeze shut! Be careful around smoke!"
icon = FA_ICON_LUNGS_VIRUS
value = -4 // trivialized by NOBREATH but still quite dangerous
gain_text = span_danger("You have a harder time breathing.")
lose_text = span_notice("You suddenly feel like your lungs just got a lot better at breathing!")
medical_record_text = "Patient suffers from asthma."
hardcore_value = 2
quirk_flags = QUIRK_HUMAN_ONLY
mail_goodies = list(/obj/item/reagent_containers/inhaler_canister/albuterol)
/// At this percentage of inflammation, our lung pressure mult reaches 0. From 0-1.
var/hit_max_mult_at_inflammation_percent = 0.9
/// Current inflammation of the lungs.
var/inflammation = 0
/// Highest possible inflammation. Interacts with [hit_max_mult_at_inflammation_percent]
var/max_inflammation = 500
/// The amount [inflammation] reduces every second while our owner is off stasis and alive.
var/passive_inflammation_reduction = 0.15
/// The amount of inflammation we will receive when our owner breathes smoke.
var/inflammation_on_smoke = 7.5
/// If our owner is metabolizing histamine, inflammation will increase by this per tick.
var/histamine_inflammation = 2
/// If our owner is ODing on histamine, inflammation will increase by this per tick.
var/histamine_OD_inflammation = 10 // allergic reactions tend to fuck people up
/// A tracker variable for how much albuterol has been inhaled.
var/inhaled_albuterol = 0
/// If [inhaled_albuterol] is above 0, we will reduce inflammation by this much per tick.
var/albuterol_inflammation_reduction = 3
/// When albuterol is inhaled, inflammation will be reduced via (inhaled_albuterol * albuterol_inflammation_reduction * albuterol_immediate_reduction_mult)
var/albuterol_immediate_reduction_mult = 4
/// The current asthma attack trying to kill our owner.
var/datum/disease/asthma_attack/current_attack
/// Can we cause an asthma attack?
COOLDOWN_DECLARE(next_attack_cooldown)
/// world.time + this is the time the first attack can happen. Used on spawn.
var/time_first_attack_can_happen = 10 MINUTES
/// After an attack ends, this is the minimum time we must wait before we attack again.
var/min_time_between_attacks = 15 MINUTES
/// After an attack ends, this is the maximum time we must wait before we attack again.
var/max_time_between_attacks = 25 MINUTES
/// Every second, an asthma attack can happen via this probability. 0-1.
var/chance_for_attack_to_happen_per_second = 0.05
/// Assoc list of (/datum/disease/asthma_attack typepath -> number). Used in pickweight for when we pick a random asthma attack to apply.
var/static/list/asthma_attack_rarities = list(
/datum/disease/asthma_attack/minor = 300,
/datum/disease/asthma_attack/moderate = 400,
/datum/disease/asthma_attack/severe = 100,
/datum/disease/asthma_attack/critical = 1, // this can quickly kill you, so its rarity is justified
)
/datum/quirk/item_quirk/asthma/add_unique(client/client_source)
. = ..()
var/obj/item/inhaler/albuterol/asthma/rescue_inhaler = new(get_turf(quirk_holder))
give_item_to_holder(rescue_inhaler, list(LOCATION_BACKPACK, LOCATION_HANDS), flavour_text = "You can use this to quickly relieve the symptoms of your asthma.")
RegisterSignal(quirk_holder, COMSIG_CARBON_EXPOSED_TO_SMOKE, PROC_REF(holder_exposed_to_smoke))
RegisterSignal(quirk_holder, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(organ_removed))
RegisterSignal(quirk_holder, COMSIG_ATOM_EXPOSE_REAGENTS, PROC_REF(exposed_to_reagents))
RegisterSignal(quirk_holder, COMSIG_LIVING_POST_FULLY_HEAL, PROC_REF(on_full_heal))
RegisterSignal(quirk_holder, COMSIG_LIVING_LIFE, PROC_REF(on_life))
COOLDOWN_START(src, next_attack_cooldown, time_first_attack_can_happen)
/datum/quirk/item_quirk/asthma/remove()
. = ..()
current_attack?.cure()
UnregisterSignal(quirk_holder, COMSIG_CARBON_EXPOSED_TO_SMOKE, COMSIG_CARBON_LOSE_ORGAN, COMSIG_ATOM_EXPOSE_REAGENTS, COMSIG_LIVING_POST_FULLY_HEAL, COMSIG_LIVING_LIFE)
/datum/quirk/item_quirk/asthma/proc/on_life(mob/living/source, seconds_per_tick, times_fired)
SIGNAL_HANDLER
if (quirk_holder.stat == DEAD)
return
if (HAS_TRAIT(quirk_holder, TRAIT_STASIS) || HAS_TRAIT(quirk_holder, TRAIT_NO_TRANSFORM))
return
var/obj/item/organ/lungs/holder_lungs = quirk_holder.get_organ_slot(ORGAN_SLOT_LUNGS)
if (isnull(holder_lungs))
return
adjust_inflammation(-passive_inflammation_reduction * seconds_per_tick)
var/datum/reagent/toxin/histamine/holder_histamine = quirk_holder.reagents.has_reagent(/datum/reagent/toxin/histamine)
if (holder_histamine)
if (holder_histamine.overdosed) // uh oh!
if (SPT_PROB(15, seconds_per_tick))
to_chat(quirk_holder, span_boldwarning("You feel your neck swelling, squeezing on your windpipe more and more!"))
adjust_inflammation(histamine_OD_inflammation * seconds_per_tick)
else
if (SPT_PROB(5, seconds_per_tick))
to_chat(quirk_holder, span_warning("You find yourself wheezing a little harder as your neck swells..."))
adjust_inflammation(histamine_inflammation * seconds_per_tick)
var/datum/reagent/medicine/albuterol/albuterol = quirk_holder.reagents.has_reagent(/datum/reagent/medicine/albuterol)
if (!albuterol) // sanity - couldve been purged. can be 0 or null which is why we just use a !
inhaled_albuterol = 0
else
inhaled_albuterol = min(albuterol.volume, inhaled_albuterol)
if (inhaled_albuterol > 0)
adjust_inflammation(-(albuterol_inflammation_reduction * seconds_per_tick))
// asthma attacks dont happen if theres no client, because they can just kill you and some need immediate response
else if (quirk_holder.client && isnull(current_attack) && COOLDOWN_FINISHED(src, next_attack_cooldown) && SPT_PROB(chance_for_attack_to_happen_per_second, seconds_per_tick))
do_asthma_attack()
/// Causes an asthma attack via infecting our owner with the attack disease. Notifies ghosts.
/datum/quirk/item_quirk/asthma/proc/do_asthma_attack()
var/datum/disease/asthma_attack/typepath = pick_weight(asthma_attack_rarities)
current_attack = new typepath
current_attack.infect(quirk_holder, make_copy = FALSE) // dont leave make_copy on TRUE. worst mistake ive ever made
RegisterSignal(current_attack, COMSIG_QDELETING, PROC_REF(attack_deleting))
if (current_attack.alert_ghosts)
notify_ghosts("[quirk_holder] is having an asthma attack: [current_attack.name]!", source = quirk_holder, notify_flags = NOTIFY_CATEGORY_NOFLASH, header = "Asthma attack!")
/// Setter proc for [inflammation]. Adjusts the amount by lung health, adjusts pressure mult, gives feedback messages if silent is FALSE.
/datum/quirk/item_quirk/asthma/proc/adjust_inflammation(amount, silent = FALSE)
var/old_inflammation = inflammation
var/obj/item/organ/lungs/holder_lungs = quirk_holder.get_organ_slot(ORGAN_SLOT_LUNGS)
var/health_mult = get_lung_health_mult(holder_lungs)
if (amount > 0) // make it worse
amount *= (2 - health_mult)
else // reduce the reduction
amount *= health_mult
var/old_pressure_mult = get_pressure_mult()
inflammation = (clamp(inflammation + amount, 0, max_inflammation))
var/difference = (old_inflammation - inflammation)
if (difference != 0)
var/new_pressure_mult = get_pressure_mult()
var/pressure_difference = new_pressure_mult - old_pressure_mult
holder_lungs?.adjust_received_pressure_mult(pressure_difference)
if (!silent)
INVOKE_ASYNC(src, PROC_REF(do_inflammation_change_feedback), difference)
/// Setter proc for [inhaled_albuterol]. Adjusts inflammation immediately.
/datum/quirk/item_quirk/asthma/proc/adjust_albuterol_levels(adjustment)
if (adjustment > 0)
var/obj/item/organ/lungs/holder_lungs = quirk_holder.get_organ_slot(ORGAN_SLOT_LUNGS)
if (isnull(holder_lungs) || holder_lungs.received_pressure_mult <= 0) // it didnt go into the lungs get fucked
return
adjust_inflammation(-(albuterol_inflammation_reduction * albuterol_immediate_reduction_mult))
inhaled_albuterol += adjustment
/// Returns the pressure mult to be applied to our lungs.
/datum/quirk/item_quirk/asthma/proc/get_pressure_mult()
var/virtual_max = (max_inflammation * hit_max_mult_at_inflammation_percent)
return (1 - (min(inflammation/virtual_max, 1)))
/// Sends feedback to our owner of which direction our asthma is intensifying/recovering.
/datum/quirk/item_quirk/asthma/proc/do_inflammation_change_feedback(difference)
var/change_mult = 1 + (difference / 300) // 300 is arbitrary
if (difference > 0) // it decreased
if (prob(1 * change_mult))
// in my experience with asthma an inhaler causes a bunch of mucous and you tend to cough it up
to_chat(quirk_holder, span_notice("The phlem in your throat forces you to cough!"))
quirk_holder.emote("cough")
else if (difference < 0)// it increased
if (prob(1 * change_mult))
quirk_holder.emote("wheeze")
if (prob(5 * change_mult))
to_chat(quirk_holder, span_warning("You feel your windpipe tightening..."))
/// Returns the % of health our lungs have, from 1-0. Used in reducing recovery and intensifying inflammation.
/datum/quirk/item_quirk/asthma/proc/get_lung_health_mult()
var/mob/living/carbon/carbon_quirk_holder = quirk_holder
var/obj/item/organ/lungs/holder_lungs = carbon_quirk_holder.get_organ_slot(ORGAN_SLOT_LUNGS)
if (isnull(holder_lungs))
return 1
if (holder_lungs.organ_flags & ORGAN_FAILING)
return 0
return (1 - (holder_lungs.damage / holder_lungs.maxHealth))
/// Signal proc for when we are exposed to smoke. Increases inflammation.
/datum/quirk/item_quirk/asthma/proc/holder_exposed_to_smoke(datum/signal_source, seconds_per_tick)
SIGNAL_HANDLER
adjust_inflammation(inflammation_on_smoke * seconds_per_tick)
/// Signal proc for when our lungs are removed. Resets all our variables.
/datum/quirk/item_quirk/asthma/proc/organ_removed(datum/signal_source, obj/item/organ/removed)
SIGNAL_HANDLER
if (istype(removed, /obj/item/organ/lungs))
reset_asthma()
/// Signal proc for when our owner receives reagents. If we receive albuterol via inhalation, we adjust inhaled albuterol by that amount. If we are smoking, we increase inflammation.
/datum/quirk/item_quirk/asthma/proc/exposed_to_reagents(atom/source, list/reagents, datum/reagents/source_reagents, methods, show_message)
SIGNAL_HANDLER
var/final_total = 0
for (var/datum/reagent/reagent as anything in reagents)
var/amount = reagents[reagent]
if (istype(reagent, /datum/reagent/medicine/albuterol))
adjust_albuterol_levels(amount)
final_total += amount
if (!(methods & INHALE))
return
if (istype(source_reagents.my_atom, /obj/item/cigarette)) // smoking is bad, kids
adjust_inflammation(inflammation_on_smoke * final_total * 5)
/// Signal proc for when our asthma attack qdels. Unsets our refs to it and resets [next_attack_cooldown].
/datum/quirk/item_quirk/asthma/proc/attack_deleting(datum/signal_source)
SIGNAL_HANDLER
UnregisterSignal(current_attack, COMSIG_QDELETING)
current_attack = null
COOLDOWN_START(src, next_attack_cooldown, rand(min_time_between_attacks, max_time_between_attacks))
/// Signal handler for COMSIG_LIVING_POST_FULLY_HEAL. Heals our asthma.
/datum/quirk/item_quirk/asthma/proc/on_full_heal(datum/signal_source, heal_flags)
SIGNAL_HANDLER
if (heal_flags & HEAL_ORGANS)
reset_asthma()
/// Resets our asthma to normal. No inflammation, no pressure mult.
/datum/quirk/item_quirk/asthma/proc/reset_asthma()
inflammation = 0
var/obj/item/organ/lungs/holder_lungs = quirk_holder.get_organ_slot(ORGAN_SLOT_LUNGS)
holder_lungs?.set_received_pressure_mult(holder_lungs::received_pressure_mult)

View File

@@ -124,6 +124,7 @@
smoker.smoke_delay = TRUE
addtimer(VARSET_CALLBACK(smoker, smoke_delay, FALSE), 1 SECONDS)
SEND_SIGNAL(smoker, COMSIG_CARBON_EXPOSED_TO_SMOKE, seconds_per_tick)
return TRUE
/**

View File

@@ -432,6 +432,30 @@
Possible Cure: [disease.cure_text]</div>\
</span>"
// Lungs
var/obj/item/organ/lungs/lungs = target.get_organ_slot(ORGAN_SLOT_LUNGS)
if (lungs)
var/initial_pressure_mult = lungs::received_pressure_mult
if (lungs.received_pressure_mult != initial_pressure_mult)
var/tooltip
var/dilation_text
var/beginning_text = "Lung Dilation: "
if (lungs.received_pressure_mult > initial_pressure_mult) // higher than usual
beginning_text = span_blue("<b>[beginning_text]</b>")
dilation_text = span_blue("[(lungs.received_pressure_mult * 100) - 100]%")
tooltip = "Subject's lungs are dilated and breathing more air than usual. Increases the effectiveness of healium and other gases."
else
beginning_text = span_danger("<b>[beginning_text]</b>")
if (lungs.received_pressure_mult <= 0) // lethal
dilation_text = span_bolddanger("[lungs.received_pressure_mult * 100]%")
tooltip = "Subject's lungs are completely shut. Subject is unable to breathe and requires emergency surgery. If asthmatic, perform asthmatic bypass surgery and adminster albuterol inhalant. Otherwise, replace lungs."
else
dilation_text = span_danger("[lungs.received_pressure_mult * 100]%")
tooltip = "Subject's lungs are partially shut. If unable to breathe, administer a high-pressure internals tank or replace lungs. If asthmatic, inhaled albuterol or bypass surgery will likely help."
var/lung_message = beginning_text + conditional_tooltip(dilation_text, tooltip, TRUE)
render_list += lung_message
// SKYRAT EDIT ADDITION - Mutant stuff and DEATH CONSEQUENCES
if(target.GetComponent(/datum/component/mutant_infection))
render_list += span_userdanger("UNKNOWN PROTO-VIRAL INFECTION DETECTED. ISOLATE IMMEDIATELY.")

View File

@@ -385,6 +385,11 @@
return
return user.dna.species.get_cough_sound(user)
/datum/emote/living/wheeze
key = "wheeze"
key_third_person = "wheezes"
message = "wheezes!"
emote_type = EMOTE_AUDIBLE
/datum/emote/living/pout
key = "pout"

View File

@@ -566,6 +566,87 @@
if(need_mob_update)
return UPDATE_MOB_HEALTH
/datum/reagent/medicine/albuterol
name = "Albuterol"
description = "A potent bronchodilator capable of increasing the amount of gas inhaled by the lungs. Is highly effective at shutting down asthma attacks, \
but only when inhaled. Overdose causes over-dilation, resulting in reduced lung function. "
taste_description = "bitter and salty air"
overdose_threshold = 30
color = "#8df5f0"
metabolization_rate = REAGENTS_METABOLISM
ph = 4
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
default_container = /obj/item/reagent_containers/inhaler_canister
/// The decrement we will apply to the received_pressure_mult of our targets lungs.
var/pressure_mult_increment = 0.4
/// After this many cycles of overdose, we activate secondary effects.
var/secondary_overdose_effect_cycle_threshold = 40
/// We stop increasing stamina damage once we reach this number.
var/maximum_od_stamina_damage = 80
/datum/reagent/medicine/albuterol/on_mob_metabolize(mob/living/affected_mob)
. = ..()
if (!iscarbon(affected_mob))
return
// has additional effects on asthma, but that's handled in the quirk
RegisterSignal(affected_mob, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(holder_lost_organ))
RegisterSignal(affected_mob, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(holder_gained_organ))
var/mob/living/carbon/carbon_mob = affected_mob
var/obj/item/organ/lungs/holder_lungs = carbon_mob.get_organ_slot(ORGAN_SLOT_LUNGS)
holder_lungs?.adjust_received_pressure_mult(pressure_mult_increment)
/datum/reagent/medicine/albuterol/on_mob_end_metabolize(mob/living/affected_mob)
. = ..()
if (!iscarbon(affected_mob))
return
UnregisterSignal(affected_mob, list(COMSIG_CARBON_LOSE_ORGAN, COMSIG_CARBON_GAIN_ORGAN))
var/mob/living/carbon/carbon_mob = affected_mob
var/obj/item/organ/lungs/holder_lungs = carbon_mob.get_organ_slot(ORGAN_SLOT_LUNGS)
holder_lungs?.adjust_received_pressure_mult(-pressure_mult_increment)
/datum/reagent/medicine/albuterol/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
. = ..()
if (!iscarbon(affected_mob))
return
var/mob/living/carbon/carbon_mob = affected_mob
if (SPT_PROB(25, seconds_per_tick))
carbon_mob.adjust_jitter_up_to(2 SECONDS, 20 SECONDS)
if (SPT_PROB(35, seconds_per_tick))
if (prob(60))
carbon_mob.losebreath += 1
to_chat(affected_mob, span_danger("Your diaphram spasms and you find yourself unable to breathe!"))
else
carbon_mob.breathe(seconds_per_tick, times_fired)
to_chat(affected_mob, span_danger("Your diaphram spasms and you unintentionally take a breath!"))
if (current_cycle > secondary_overdose_effect_cycle_threshold)
if (SPT_PROB(30, seconds_per_tick))
carbon_mob.adjust_eye_blur_up_to(6 SECONDS, 30 SECONDS)
if (carbon_mob.getStaminaLoss() < maximum_od_stamina_damage)
carbon_mob.adjustStaminaLoss(seconds_per_tick)
/datum/reagent/medicine/albuterol/proc/holder_lost_organ(datum/source, obj/item/organ/lost)
SIGNAL_HANDLER
if (istype(lost, /obj/item/organ/lungs))
var/obj/item/organ/lungs/holder_lungs = lost
holder_lungs.adjust_received_pressure_mult(-pressure_mult_increment)
/datum/reagent/medicine/albuterol/proc/holder_gained_organ(datum/source, obj/item/organ/gained)
SIGNAL_HANDLER
if (istype(gained, /obj/item/organ/lungs))
var/obj/item/organ/lungs/holder_lungs = gained
holder_lungs.adjust_received_pressure_mult(pressure_mult_increment)
/datum/reagent/medicine/ephedrine
name = "Ephedrine"
description = "Increases resistance to batons and movement speed, giving you hand cramps. Overdose deals toxin damage and inhibits breathing."

View File

@@ -179,7 +179,7 @@
H_ion_release = -1
rate_up_lim = 50
purity_min = 0.25
reaction_flags = REACTION_PH_VOL_CONSTANT
reaction_flags = REACTION_PH_VOL_CONSTANT|REACTION_CLEAR_INVERSE
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_HEALING | REACTION_TAG_OXY
/datum/chemical_reaction/medicine/convermol/reaction_step(datum/reagents/holder, datum/equilibrium/reaction, delta_t, delta_ph, step_reaction_vol)

View File

@@ -157,6 +157,51 @@
required_reagents = list(/datum/reagent/medicine/sal_acid = 1, /datum/reagent/lithium = 1, /datum/reagent/aluminium = 1, /datum/reagent/bromine = 1, /datum/reagent/ammonia = 1)
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_HEALING | REACTION_TAG_OXY
/datum/chemical_reaction/medicine/albuterol_creation
results = list(/datum/reagent/medicine/albuterol = 15)
required_reagents = list(/datum/reagent/lithium = 3, /datum/reagent/aluminium = 3, /datum/reagent/bromine = 3, /datum/reagent/inverse/healing/convermol = 1)
reaction_tags = REACTION_TAG_MODERATE | REACTION_TAG_ORGAN | REACTION_TAG_OTHER
required_temp = 400
optimal_temp = 600
overheat_temp = 900
/datum/chemical_reaction/medicine/salbutamol_to_albuterol
results = list(/datum/reagent/medicine/albuterol = 4, /datum/reagent/medicine/sal_acid = 0.5, /datum/reagent/ammonia = 0.5)
required_catalysts = list(/datum/reagent/toxin/acid = 1)
required_reagents = list(/datum/reagent/medicine/salbutamol = 5, /datum/reagent/medicine/c2/convermol = 1)
reaction_tags = REACTION_TAG_MODERATE | REACTION_TAG_ORGAN | REACTION_TAG_OTHER
required_temp = 500
optimal_temp = 610
overheat_temp = 980
thermic_constant = 75
rate_up_lim = 10
mix_message = "The solution rapidly changes colors, boiling into a pale blue."
/datum/chemical_reaction/medicine/albuterol_to_salbutamol
results = list(/datum/reagent/medicine/salbutamol = 2, /datum/reagent/ammonia = 1)
required_catalysts = list(/datum/reagent/toxin/acid = 1)
required_reagents = list(/datum/reagent/medicine/albuterol = 3, /datum/reagent/oxygen = 1)
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_ORGAN | REACTION_TAG_OTHER
required_temp = 300
optimal_temp = 500
overheat_temp = 800
mix_message = "The solution breaks apart, turning a deeper blue."
/datum/chemical_reaction/medicine/albuterol_to_inverse_convermol
results = list(/datum/reagent/inverse/healing/convermol = 1, /datum/reagent/lithium = 3, /datum/reagent/aluminium = 3, /datum/reagent/bromine = 3)
required_catalysts = list(/datum/reagent/toxin/acid/fluacid = 1)
required_reagents = list(/datum/reagent/medicine/albuterol = 5)
reaction_tags = REACTION_TAG_MODERATE | REACTION_TAG_ORGAN | REACTION_TAG_OTHER
required_temp = 900
optimal_temp = 920
overheat_temp = 990
thermic_constant = 25
mix_message = "The solution rapidly breaks apart, turning a mix of colors."
/datum/chemical_reaction/medicine/albuterol_to_inverse_convermol/overheated(datum/reagents/holder, datum/equilibrium/equilibrium, impure = FALSE)
var/bonus = impure ? 2 : 1
explode_smoke(holder, equilibrium, 7.5 * bonus, TRUE, TRUE)
/datum/chemical_reaction/medicine/ephedrine
results = list(/datum/reagent/medicine/ephedrine = 4)
required_reagents = list(/datum/reagent/consumable/sugar = 1, /datum/reagent/fuel/oil = 1, /datum/reagent/hydrogen = 1, /datum/reagent/diethylamine = 1)

View File

@@ -0,0 +1,314 @@
/obj/item/inhaler
name = "inhaler"
desc = "A small device capable of administering short bursts of aerosolized chemicals. Requires a canister to function."
w_class = WEIGHT_CLASS_SMALL
icon = 'icons/obj/medical/chemical.dmi'
icon_state = "inhaler_generic"
custom_materials = list(/datum/material/plastic = SHEET_MATERIAL_AMOUNT * 0.1)
/// The currently installed canister, from which we get our reagents. Nullable.
var/obj/item/reagent_containers/inhaler_canister/canister
/// The path for our initial canister to be generated by. If not null, we start with that canister type.
var/obj/item/reagent_containers/inhaler_canister/initial_casister_path
/// The underlay of our canister, if one is installed.
var/mutable_appearance/canister_underlay
/// The y offset to be applied to [canister_underlay].
var/canister_underlay_y_offset = -2
/// If true, we will show a rotary display with how many puffs we can be used for until the canister runs out.
var/show_puffs_left = TRUE // this is how real inhalers work
/obj/item/inhaler/Initialize(mapload)
. = ..()
if (ispath(initial_casister_path, /obj/item/reagent_containers/inhaler_canister))
set_canister(new initial_casister_path)
/obj/item/inhaler/Destroy(force)
QDEL_NULL(canister)
return ..()
/obj/item/inhaler/handle_deconstruct(disassembled)
. = ..()
canister?.forceMove(drop_location())
/obj/item/inhaler/proc/update_canister_underlay()
if (isnull(canister))
underlays -= canister_underlay
canister_underlay = null
else if (isnull(canister_underlay))
canister_underlay = mutable_appearance(canister.icon, canister.icon_state)
canister_underlay.pixel_z = canister_underlay_y_offset
underlays += canister_underlay
/obj/item/inhaler/examine(mob/user)
. = ..()
if (isnull(canister))
return
. += span_blue("It seems to have <b>[canister]</b> inserted.")
if (!show_puffs_left)
return
var/puffs_left = canister.get_puffs_left()
if (puffs_left > 0)
puffs_left = span_blue("[puffs_left]")
else
puffs_left = span_danger("[puffs_left]")
. += "Its rotary display shows its canister can be used [puffs_left] more times."
/obj/item/inhaler/Exited(atom/movable/gone, direction)
. = ..()
if (gone == canister)
set_canister(null, move_canister = FALSE)
/obj/item/inhaler/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if (!isliving(interacting_with))
return ..() // default behavior
var/mob/living/target_mob = interacting_with
if (!can_puff(target_mob, user))
return NONE
var/puff_timer = 0
var/pre_use_visible_message
var/pre_use_self_message
var/pre_use_target_message
var/post_use_visible_message
var/post_use_self_message
var/post_use_target_message
if (target_mob == user) // no need for a target message
puff_timer = canister.self_administer_delay
pre_use_visible_message = span_notice("[user] puts [src] to [user.p_their()] lips, fingers on the canister...")
pre_use_self_message = span_notice("You put [src] to your lips and put pressure on the canister...")
post_use_visible_message = span_notice("[user] takes a puff of [src]!")
post_use_self_message = span_notice("You take a puff of [src]!")
else
puff_timer = canister.other_administer_delay
pre_use_visible_message = span_warning("[user] tries to force [src] between [target_mob]'s lips...")
pre_use_self_message = span_notice("You try to put [src] to [target_mob]'s lips...")
pre_use_target_message = span_userdanger("[user] tries to force [src] between your lips!")
post_use_visible_message = span_warning("[user] forces [src] between [target_mob]'s lips and pushes the canister down!")
post_use_self_message = span_notice("You force [src] between [target_mob]'s lips and press on the canister!")
post_use_target_message = span_userdanger("[user] forces [src] between your lips and presses on the canister, filling your lungs with aerosol!")
if (puff_timer > 0)
user.visible_message(pre_use_visible_message, ignored_mobs = list(user, target_mob))
to_chat(user, pre_use_self_message)
if (pre_use_target_message)
to_chat(target_mob, pre_use_target_message)
if (!do_after(user, puff_timer, src))
return NONE
if (!can_puff(target_mob, user)) // sanity
return NONE
user.visible_message(post_use_visible_message, ignored_mobs = list(user, target_mob))
to_chat(user, post_use_self_message)
if (post_use_target_message)
to_chat(target_mob, post_use_target_message)
canister.puff(user, target_mob)
/obj/item/inhaler/attack_self(mob/user, modifiers)
try_remove_canister(user, modifiers)
return ..()
/obj/item/inhaler/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
if (istype(tool, /obj/item/reagent_containers/inhaler_canister))
return try_insert_canister(tool, user, modifiers)
return ..()
/// Tries to remove the canister, if any is inserted.
/obj/item/inhaler/proc/try_remove_canister(mob/living/user, modifiers)
if (isnull(canister))
balloon_alert(user, "no canister inserted!")
return FALSE
if (canister.removal_time > 0)
balloon_alert(user, "removing canister...")
if (!do_after(user, canister.removal_time, src))
return FALSE
balloon_alert(user, "canister removed")
playsound(src, canister.post_insert_sound, canister.post_insert_volume)
set_canister(null, user)
// Tries to insert a canister, if none is already inserted.
/obj/item/inhaler/proc/try_insert_canister(obj/item/reagent_containers/inhaler_canister/new_canister, mob/living/user, params)
if (!isnull(canister))
balloon_alert(user, "remove the existing canister!")
return FALSE
balloon_alert(user, "inserting canister...")
playsound(src, new_canister.pre_insert_sound, new_canister.pre_insert_volume)
if (!do_after(user, new_canister.insertion_time, src))
return FALSE
playsound(src, new_canister.post_insert_sound, new_canister.post_insert_volume)
balloon_alert(user, "canister inserted")
set_canister(new_canister, user)
return TRUE
/// Setter proc for [canister]. Moves the existing canister out of the inhaler, while moving a new canister inside and registering it.
/obj/item/inhaler/proc/set_canister(obj/item/reagent_containers/inhaler_canister/new_canister, mob/living/user, move_canister = TRUE)
if (move_canister && !isnull(canister))
if (iscarbon(loc))
var/mob/living/carbon/carbon_loc = loc
INVOKE_ASYNC(carbon_loc, TYPE_PROC_REF(/mob/living/carbon, put_in_hands), canister)
else if (!isnull(loc))
canister.forceMove(loc)
canister = new_canister
canister?.forceMove(src)
update_canister_underlay()
/// Determines if we can be used. Fails on no canister, empty canister, invalid targets, or non-breathing targets.
/obj/item/inhaler/proc/can_puff(mob/living/target_mob, mob/living/user, silent = FALSE)
if (isnull(canister))
if (!silent)
balloon_alert(user, "no canister!")
return FALSE
if (isnull(canister.reagents) || canister.reagents.total_volume <= 0)
if (!silent)
balloon_alert(user, "canister is empty!")
return FALSE
if (!iscarbon(target_mob)) // maybe mix this into a general has mouth check
if (!silent)
balloon_alert(user, "not breathing!")
return FALSE
var/mob/living/carbon/carbon_target = target_mob
if (carbon_target.is_mouth_covered())
if (!silent)
balloon_alert(user, "expose the mouth!")
return FALSE
if (HAS_TRAIT(carbon_target, TRAIT_NOBREATH))
if (!silent)
balloon_alert(user, "not breathing!")
return FALSE
var/obj/item/organ/lungs/lungs = carbon_target.get_organ_slot(ORGAN_SLOT_LUNGS)
if (isnull(lungs) || lungs.received_pressure_mult <= 0)
if (!silent)
balloon_alert(user, "not breathing!")
return FALSE
return TRUE
/obj/item/reagent_containers/inhaler_canister
name = "inhaler canister"
desc = "A small canister filled with aerosolized reagents for use in a inhaler."
w_class = WEIGHT_CLASS_TINY
icon = 'icons/obj/medical/chemical.dmi'
icon_state = "canister_generic"
reagent_flags = SEALED_CONTAINER|DRAINABLE|REFILLABLE
has_variable_transfer_amount = FALSE
max_integrity = 60
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 0.2)
/// The sound that plays when we are used.
var/puff_sound = 'sound/effects/spray.ogg'
/// The volume of [puff_sound]
var/puff_volume = 20
/// The sound that plays when someone TRIES to insert us.
var/pre_insert_sound = 'sound/items/taperecorder/tape_flip.ogg'
/// The sound that plays when we are removed or inserted.
var/post_insert_sound = 'sound/items/taperecorder/taperecorder_close.ogg'
/// The volume of [pre_insert_sound]
var/pre_insert_volume = 50
/// The volume of [post_insert_sound]
var/post_insert_volume = 50
/// The time it takes to insert us into a inhaler.
var/insertion_time = 2 SECONDS
/// The time it takes to remove us from a inhaler.
var/removal_time = 0.5 SECONDS
/// The time it takes for us to be used on someone else.
var/other_administer_delay = 3 SECONDS
/// The time it takes for us to be used on our owner.
var/self_administer_delay = 1 SECONDS
/// Called when a inhaler we are in is used on someone. Transfers reagents and plays the puff sound.
/obj/item/reagent_containers/inhaler_canister/proc/puff(mob/living/user, mob/living/carbon/target)
playsound(src, puff_sound, puff_volume, TRUE, -6)
reagents.trans_to(target, amount_per_transfer_from_this, transferred_by = user, methods = INHALE)
/// Returns a integer approximating how many puffs we can be used for.
/obj/item/reagent_containers/inhaler_canister/proc/get_puffs_left()
return ROUND_UP(reagents.total_volume / amount_per_transfer_from_this)
/obj/item/reagent_containers/inhaler_canister/handle_deconstruct(disassembled)
if (!reagents?.total_volume)
return ..()
var/datum/reagents/smoke_reagents = new/datum/reagents() // Lets be safe first, our own reagents may be qdelled if we get deleted
var/datum/effect_system/fluid_spread/smoke/chem/smoke_machine/smoke = new()
smoke_reagents.my_atom = src
for (var/datum/reagent/reagent as anything in reagents.reagent_list)
smoke_reagents.add_reagent(reagent.type, reagent.volume, added_purity = reagent.purity)
reagents.remove_reagent(reagent.type, reagent.volume)
if (smoke_reagents.reagent_list)
smoke.set_up(1, holder = src, location = get_turf(src), carry = smoke_reagents)
smoke.start(log = TRUE)
visible_message(span_warning("[src] breaks open and sprays its aerosilized contents everywhere!"))
else
visible_message(span_warning("[src] breaks open - but is empty!"))
return ..()
/obj/item/inhaler/medical
icon_state = "inhaler_medical"
/obj/item/inhaler/salbutamol
name = "salbutamol inhaler"
icon_state = "inhaler_medical"
initial_casister_path = /obj/item/reagent_containers/inhaler_canister/salbutamol
/obj/item/reagent_containers/inhaler_canister/salbutamol
name = "salbutamol canister"
icon_state = "canister_medical"
list_reagents = list(/datum/reagent/medicine/salbutamol = 30)
/obj/item/inhaler/albuterol
name = "albuterol inhaler"
icon_state = "inhaler_medical"
initial_casister_path = /obj/item/reagent_containers/inhaler_canister/albuterol
/obj/item/reagent_containers/inhaler_canister/albuterol
name = "albuterol canister"
desc = "A small canister filled with aerosolized reagents for use in a inhaler. This one contains albuterol, a potent bronchodilator that can stop \
asthma attacks in their tracks."
icon_state = "canister_medical"
list_reagents = list(/datum/reagent/medicine/albuterol = 30)
/obj/item/reagent_containers/inhaler_canister/albuterol/asthma
name = "low-pressure albuterol canister"
desc = "A small canister filled with aerosolized reagents for use in a inhaler. This one contains albuterol, a potent bronchodilator that can stop \
asthma attacks in their tracks. It seems to be a lower-pressure variant, and can only hold 20u."
list_reagents = list(/datum/reagent/medicine/albuterol = 20)
volume = 20
/obj/item/inhaler/albuterol/asthma
name = "rescue inhaler"
icon_state = "inhaler_generic"
initial_casister_path = /obj/item/reagent_containers/inhaler_canister/albuterol/asthma

View File

@@ -128,6 +128,30 @@
)
departmental_flags = DEPARTMENT_BITFLAG_MEDICAL | DEPARTMENT_BITFLAG_SCIENCE
/datum/design/inhaler
name = "Inhaler"
desc = "A small device capable of administering short bursts of aerosolized chemicals. Requires a canister to function."
id = "inhaler"
build_path = /obj/item/inhaler/medical
build_type = PROTOLATHE | AWAY_LATHE
materials = list(/datum/material/plastic = SHEET_MATERIAL_AMOUNT * 0.1)
category = list(
RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_MEDICAL
)
departmental_flags = DEPARTMENT_BITFLAG_MEDICAL
/datum/design/inhaler_canister
name = "Inhaler Canister"
desc = "A small canister filled with aerosolized reagents for use in a inhaler."
id = "inhaler_canister"
build_path = /obj/item/reagent_containers/inhaler_canister
build_type = PROTOLATHE | AWAY_LATHE
materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 0.2)
category = list(
RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_MEDICAL
)
departmental_flags = DEPARTMENT_BITFLAG_MEDICAL
/datum/design/bluespacebodybag
name = "Bluespace Body Bag"
desc = "A bluespace body bag, powered by experimental bluespace technology. It can hold loads of bodies and the largest of creatures."

View File

@@ -49,6 +49,8 @@
prereq_ids = list(TECHWEB_NODE_MEDBAY_EQUIP)
design_ids = list(
"med_spray_bottle",
"inhaler",
"inhaler_canister",
"medigel",
"medipen_refiller",
"soda_dispenser",

View File

@@ -0,0 +1,96 @@
/datum/surgery/asthmatic_bypass
name = "Asthmatic Bypass"
surgery_flags = SURGERY_REQUIRE_RESTING | SURGERY_REQUIRE_LIMB
requires_bodypart_type = NONE
organ_to_manipulate = ORGAN_SLOT_LUNGS
possible_locs = list(BODY_ZONE_CHEST)
steps = list(
/datum/surgery_step/incise,
/datum/surgery_step/retract_skin,
/datum/surgery_step/clamp_bleeders,
/datum/surgery_step/incise,
/datum/surgery_step/expand_windpipe,
/datum/surgery_step/close,
)
/datum/surgery/asthmatic_bypass/can_start(mob/user, mob/living/patient)
. = ..()
if (!.)
return
return (patient.has_quirk(/datum/quirk/item_quirk/asthma))
/datum/surgery_step/expand_windpipe
name = "force open windpipe (retractor)"
implements = list(
TOOL_RETRACTOR = 80,
TOOL_WIRECUTTER = 45,
)
time = 8 SECONDS
repeatable = TRUE
preop_sound = 'sound/items/handling/surgery/retractor1.ogg'
success_sound = 'sound/items/handling/surgery/retractor2.ogg'
/// The amount of inflammation a failure or success of this surgery will reduce.
var/inflammation_reduction = 75
/datum/surgery_step/expand_windpipe/preop(mob/user, mob/living/target, target_zone, obj/item/tool, datum/surgery/surgery)
display_results(
user,
target,
span_notice("You start to stretch [target]'s windpipe, trying your best to avoid nearby blood vessels..."),
span_notice("[user] begins to stretch [target]'s windpipe, taking care to avoid any nearby blood vessels."),
span_notice("[user] begins to stretch [target]'s windpipe."),
)
display_pain(target, "You feel an agonizing stretching sensation in your neck!")
/datum/surgery_step/expand_windpipe/success(mob/user, mob/living/target, target_zone, obj/item/tool, datum/surgery/surgery, default_display_results = TRUE)
if (!reduce_inflammation(user, target, tool, surgery))
return
default_display_results = FALSE
display_results(
user,
target,
span_notice("You stretch [target]'s windpipe with [tool], managing to avoid the nearby blood vessels and arteries."),
span_notice("[user] succeeds at stretching [target]'s windpipe with [tool], avoiding the nearby blood vessels and arteries."),
span_notice("[user] finishes stretching [target]'s windpipe.")
)
return ..()
/datum/surgery_step/expand_windpipe/failure(mob/user, mob/living/target, target_zone, obj/item/tool, datum/surgery/surgery, fail_prob)
if (!reduce_inflammation(user, target, tool, surgery))
return
display_results(
user,
target,
span_bolddanger("You stretch [target]'s windpipe with [tool], but accidentally clip a few arteries!"),
span_bolddanger("[user] succeeds at stretching [target]'s windpipe with [tool], but accidentally clips a few arteries!"),
span_bolddanger("[user] finishes stretching [target]'s windpipe, but screws up!")
)
target.losebreath++
if (iscarbon(target))
var/mob/living/carbon/carbon_patient = target
var/wound_bonus = tool.wound_bonus
var/obj/item/bodypart/head/patient_chest = carbon_patient.get_bodypart(BODY_ZONE_CHEST)
if (patient_chest)
if (prob(30))
carbon_patient.cause_wound_of_type_and_severity(WOUND_SLASH, patient_chest, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_CRITICAL, WOUND_PICK_LOWEST_SEVERITY, tool)
patient_chest.receive_damage(brute = 10, wound_bonus = wound_bonus, sharpness = SHARP_EDGED, damage_source = tool)
return FALSE
/// Reduces the asthmatic's inflammation by [inflammation_reduction]. Called by both success and failure.
/datum/surgery_step/expand_windpipe/proc/reduce_inflammation(mob/user, mob/living/target, obj/item/tool, datum/surgery/surgery)
var/datum/quirk/item_quirk/asthma/asthma_quirk = locate(/datum/quirk/item_quirk/asthma) in target.quirks
if (isnull(asthma_quirk))
qdel(surgery) // not really an error cause quirks can get removed during surgery?
return FALSE
asthma_quirk.adjust_inflammation(-inflammation_reduction)
return TRUE

View File

@@ -324,6 +324,7 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
lungs = new()
lungs.Insert(src)
lungs.set_organ_damage(0)
lungs.received_pressure_mult = lungs::received_pressure_mult
var/obj/item/organ/heart/heart = get_organ_slot(ORGAN_SLOT_HEART)
if(heart)

View File

@@ -69,6 +69,9 @@
var/n2o_euphoria = EUPHORIA_LAST_FLAG
var/healium_euphoria = EUPHORIA_LAST_FLAG
/// All incoming breaths will have their pressure multiplied against this. Higher values allow more air to be breathed at once,
/// while lower values can cause suffocation in low pressure environments.
var/received_pressure_mult = 1
var/oxy_breath_dam_min = MIN_TOXIC_GAS_DAMAGE
var/oxy_breath_dam_max = MAX_TOXIC_GAS_DAMAGE
@@ -168,6 +171,7 @@
receiver.clear_alert(ALERT_NOT_ENOUGH_NITRO)
receiver.clear_alert(ALERT_NOT_ENOUGH_PLASMA)
receiver.clear_alert(ALERT_NOT_ENOUGH_N2O)
update_bronchodilation_alerts()
/obj/item/organ/lungs/on_mob_remove(mob/living/carbon/organ_owner, special, movement_flags)
. = ..()
@@ -648,7 +652,7 @@
// Build out our partial pressures, for use as we go
var/list/partial_pressures = list()
for(var/gas_id in breath_gases)
partial_pressures[gas_id] = breath.get_breath_partial_pressure(breath_gases[gas_id][MOLES])
partial_pressures[gas_id] = breath.get_breath_partial_pressure(breath_gases[gas_id][MOLES] * received_pressure_mult)
// Treat gas as other types of gas
for(var/list/conversion_packet in treat_as)
@@ -1051,6 +1055,38 @@
#undef GAS_TOLERANCE
/// Adjusting proc for [received_pressure_mult]. Updates bronchodilation alerts.
/obj/item/organ/lungs/proc/adjust_received_pressure_mult(adjustment)
received_pressure_mult = max(received_pressure_mult + adjustment, 0)
update_bronchodilation_alerts()
/// Setter proc for [received_pressure_mult]. Updates bronchodilation alerts.
/obj/item/organ/lungs/proc/set_received_pressure_mult(new_value)
received_pressure_mult = max(new_value, 0)
update_bronchodilation_alerts()
#define LUNG_CAPACITY_ALERT_BUFFER 0.003
/// Depending on [received_pressure_mult], gives either a bronchocontraction or bronchoconstriction alert to our owner (if we have one), or clears the alert
/// if [received_pressure_mult] is near 1.
/obj/item/organ/lungs/proc/update_bronchodilation_alerts()
if (!owner)
return
var/initial_value = initial(received_pressure_mult)
// you wont really notice if youre only breathing a bit more or a bit less
var/dilated = (received_pressure_mult > (initial_value + LUNG_CAPACITY_ALERT_BUFFER))
var/constricted = (received_pressure_mult < (initial_value - LUNG_CAPACITY_ALERT_BUFFER))
if (dilated)
owner.throw_alert(ALERT_BRONCHODILATION, /atom/movable/screen/alert/bronchodilated)
else if (constricted)
owner.throw_alert(ALERT_BRONCHODILATION, /atom/movable/screen/alert/bronchoconstricted)
else
owner.clear_alert(ALERT_BRONCHODILATION)
#undef LUNG_CAPACITY_ALERT_BUFFER
/obj/item/organ/lungs/ethereal
name = "aeration reticulum"
desc = "These exotic lungs seem crunchier than most."

View File

@@ -102,6 +102,7 @@
/obj/item/reagent_containers/medigel/synthflesh = 2,
/obj/item/storage/pill_bottle/psicodine = 2,
/obj/item/storage/pill_bottle/sansufentanyl = 1,
/obj/item/inhaler/albuterol = 2,
)
default_price = 50
extra_price = 100

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -1543,6 +1543,7 @@
#include "code\datums\diseases\adrenal_crisis.dm"
#include "code\datums\diseases\anaphylaxis.dm"
#include "code\datums\diseases\anxiety.dm"
#include "code\datums\diseases\asthma_attack.dm"
#include "code\datums\diseases\beesease.dm"
#include "code\datums\diseases\brainrot.dm"
#include "code\datums\diseases\chronic_illness.dm"
@@ -1957,6 +1958,7 @@
#include "code\datums\quirks\negative_quirks\all_nighter.dm"
#include "code\datums\quirks\negative_quirks\allergic.dm"
#include "code\datums\quirks\negative_quirks\anosmia.dm"
#include "code\datums\quirks\negative_quirks\asthma.dm"
#include "code\datums\quirks\negative_quirks\bad_back.dm"
#include "code\datums\quirks\negative_quirks\bad_touch.dm"
#include "code\datums\quirks\negative_quirks\big_hands.dm"
@@ -6114,6 +6116,7 @@
#include "code\modules\reagents\reagent_containers\cooler_jug.dm"
#include "code\modules\reagents\reagent_containers\dropper.dm"
#include "code\modules\reagents\reagent_containers\hypospray.dm"
#include "code\modules\reagents\reagent_containers\inhaler.dm"
#include "code\modules\reagents\reagent_containers\jerrycan.dm"
#include "code\modules\reagents\reagent_containers\medigel.dm"
#include "code\modules\reagents\reagent_containers\patch.dm"
@@ -6401,6 +6404,7 @@
#include "code\modules\station_goals\station_goal.dm"
#include "code\modules\station_goals\vault_mutation.dm"
#include "code\modules\surgery\amputation.dm"
#include "code\modules\surgery\asthmatic_bypass.dm"
#include "code\modules\surgery\autopsy.dm"
#include "code\modules\surgery\blood_filter.dm"
#include "code\modules\surgery\bone_mending.dm"