diff --git a/code/__DEFINES/alerts.dm b/code/__DEFINES/alerts.dm index 662711610ee..ea12830d858 100644 --- a/code/__DEFINES/alerts.dm +++ b/code/__DEFINES/alerts.dm @@ -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 */ diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm index fe2071df0b2..67d69dc5b05 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm @@ -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" diff --git a/code/__DEFINES/diseases.dm b/code/__DEFINES/diseases.dm index fe05a30e0fd..379f9a37d29 100644 --- a/code/__DEFINES/diseases.dm +++ b/code/__DEFINES/diseases.dm @@ -16,6 +16,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) diff --git a/code/__DEFINES/reagents.dm b/code/__DEFINES/reagents.dm index d2f5528bc49..58db74fa044 100644 --- a/code/__DEFINES/reagents.dm +++ b/code/__DEFINES/reagents.dm @@ -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 diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm index ec7d26d4da0..c10bc5922e2 100644 --- a/code/_onclick/hud/alert.dm +++ b/code/_onclick/hud/alert.dm @@ -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..." diff --git a/code/datums/diseases/_disease.dm b/code/datums/diseases/_disease.dm index e19c19151e3..d3c8c0f7dc0 100644 --- a/code/datums/diseases/_disease.dm +++ b/code/datums/diseases/_disease.dm @@ -117,11 +117,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 @@ -233,6 +235,10 @@ stage = new_stage 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))) diff --git a/code/datums/diseases/asthma_attack.dm b/code/datums/diseases/asthma_attack.dm new file mode 100644 index 00000000000..7adefd2f746 --- /dev/null +++ b/code/datums/diseases/asthma_attack.dm @@ -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 += " The attack has entered remission. It will slowly decrease in intensity before vanishing." + 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++ diff --git a/code/datums/quirks/negative_quirks/allergic.dm b/code/datums/quirks/negative_quirks/allergic.dm index a2442adab10..25621f7970e 100644 --- a/code/datums/quirks/negative_quirks/allergic.dm +++ b/code/datums/quirks/negative_quirks/allergic.dm @@ -22,6 +22,7 @@ /datum/reagent/medicine/diphenhydramine, /datum/reagent/medicine/sansufentanyl, /datum/reagent/medicine/salglu_solution, + /datum/reagent/medicine/albuterol, ) var/allergy_string diff --git a/code/datums/quirks/negative_quirks/asthma.dm b/code/datums/quirks/negative_quirks/asthma.dm new file mode 100644 index 00000000000..6683869a646 --- /dev/null +++ b/code/datums/quirks/negative_quirks/asthma.dm @@ -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) diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm index c9e68ce6ede..6d71edad180 100644 --- a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm +++ b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm @@ -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 /** diff --git a/code/game/objects/items/devices/scanners/health_analyzer.dm b/code/game/objects/items/devices/scanners/health_analyzer.dm index 0da89a89252..d8033149045 100644 --- a/code/game/objects/items/devices/scanners/health_analyzer.dm +++ b/code/game/objects/items/devices/scanners/health_analyzer.dm @@ -432,6 +432,31 @@ Possible Cure: [disease.cure_text]\ " + + // 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("[beginning_text]") + 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("[beginning_text]") + 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 + // Time of death if(target.station_timestamp_timeofdeath && !target.appears_alive()) render_list += "
" diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm index 1e9404f334d..8f1fd0b431d 100644 --- a/code/modules/mob/living/emote.dm +++ b/code/modules/mob/living/emote.dm @@ -398,6 +398,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" diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm index 7f69044234d..53cc9d8bf2b 100644 --- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm @@ -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." diff --git a/code/modules/reagents/chemistry/recipes/cat2_medicines.dm b/code/modules/reagents/chemistry/recipes/cat2_medicines.dm index 802d5a820e2..8600f1ecdb7 100644 --- a/code/modules/reagents/chemistry/recipes/cat2_medicines.dm +++ b/code/modules/reagents/chemistry/recipes/cat2_medicines.dm @@ -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) diff --git a/code/modules/reagents/chemistry/recipes/medicine.dm b/code/modules/reagents/chemistry/recipes/medicine.dm index 07d81164906..40639b2ec30 100644 --- a/code/modules/reagents/chemistry/recipes/medicine.dm +++ b/code/modules/reagents/chemistry/recipes/medicine.dm @@ -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) diff --git a/code/modules/reagents/reagent_containers/inhaler.dm b/code/modules/reagents/reagent_containers/inhaler.dm new file mode 100644 index 00000000000..5600625df6c --- /dev/null +++ b/code/modules/reagents/reagent_containers/inhaler.dm @@ -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 [canister] 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 diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm index 9cfab3b3f8a..e4a8b78a6e7 100644 --- a/code/modules/research/designs/medical_designs.dm +++ b/code/modules/research/designs/medical_designs.dm @@ -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." diff --git a/code/modules/research/techweb/nodes/medbay_nodes.dm b/code/modules/research/techweb/nodes/medbay_nodes.dm index 0e16f9cacbd..49d4aad71f7 100644 --- a/code/modules/research/techweb/nodes/medbay_nodes.dm +++ b/code/modules/research/techweb/nodes/medbay_nodes.dm @@ -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", diff --git a/code/modules/surgery/asthmatic_bypass.dm b/code/modules/surgery/asthmatic_bypass.dm new file mode 100644 index 00000000000..7e69c99a7bd --- /dev/null +++ b/code/modules/surgery/asthmatic_bypass.dm @@ -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 diff --git a/code/modules/surgery/organs/_organ.dm b/code/modules/surgery/organs/_organ.dm index ac602413eee..54de85f5467 100644 --- a/code/modules/surgery/organs/_organ.dm +++ b/code/modules/surgery/organs/_organ.dm @@ -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) diff --git a/code/modules/surgery/organs/internal/lungs/_lungs.dm b/code/modules/surgery/organs/internal/lungs/_lungs.dm index 27a88a6d94e..c15dd74a263 100644 --- a/code/modules/surgery/organs/internal/lungs/_lungs.dm +++ b/code/modules/surgery/organs/internal/lungs/_lungs.dm @@ -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) . = ..() @@ -646,7 +650,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) @@ -1044,6 +1048,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." diff --git a/code/modules/vending/medical.dm b/code/modules/vending/medical.dm index 21cabf95dd3..c94bc440663 100644 --- a/code/modules/vending/medical.dm +++ b/code/modules/vending/medical.dm @@ -103,6 +103,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 diff --git a/icons/hud/screen_alert.dmi b/icons/hud/screen_alert.dmi index d308aea2e2f..58aa910cb22 100644 Binary files a/icons/hud/screen_alert.dmi and b/icons/hud/screen_alert.dmi differ diff --git a/icons/obj/medical/chemical.dmi b/icons/obj/medical/chemical.dmi index 9b785b3ed60..a7e9cfa48c2 100644 Binary files a/icons/obj/medical/chemical.dmi and b/icons/obj/medical/chemical.dmi differ diff --git a/tgstation.dme b/tgstation.dme index a173c28cbe0..6b0618f228a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1402,6 +1402,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" @@ -1818,6 +1819,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" @@ -5973,6 +5975,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" @@ -6260,6 +6263,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"