mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-24 13:38:41 +01:00
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>
This commit is contained in:
co-authored by
SyncIt21
MrMelbert
parent
5e915f7313
commit
fadbf16e1b
@@ -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
|
||||
Reference in New Issue
Block a user