Files
Bubberstation/code/modules/reagents/reagent_containers/inhaler.dm
paganiy fb97113b20 Prevents sealed-looking containers from spilling. (#93649)
## About The Pull Request
Fixes a logical inconsistency with several types of containers that are
**functionally open** for game mechanics (allowing refilling, transfer,
etc.) but are **perceived as closed** from a user perspective.
2025-10-28 12:49:48 -05:00

315 lines
12 KiB
Plaintext

/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"
initial_reagent_flags = SEALED_CONTAINER | DRAINABLE | REFILLABLE | NO_SPLASH
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