Files
SmArtKarandGitHub f58b8511f0 Refactors effect_system (#94999)
## About The Pull Request

This PR refactors ``effect_system``s to be a bit easier to use by
getting rid of ``set_up``, allowing ``attach()`` to be chained into
``start()`` and refactoring most direct system usages in our code to use
helper procs.

``set_up`` was unnecessary and only existed to allow ``New``'s behavior
to be fully overriden, which is not required if we split
sparks/lightning/steam into a new ``/datum/effect_system/basic`` subtype
which houses the effect spreading behavior. This allows us to roll all
logic from ``set_up`` into ``New`` and cut down on code complexity.
Chaining setup as ``system.attach(src).start()`` also helps a bit in
case no helper method exists

I've added ``do_chem_smoke`` and ``do_foam`` helpers, which respectively
allow chemical smoke or foam to be spawned easily without having to
manually create effect datums and reagent holders.

Also turns out we've had some nonfunctional effect systems which either
never set themselves up, or never started, so I fixed those while I was
at it (mostly by moving them to aforementioned helper procs)

## Why It's Good For The Game

Cleaner code, makes it significantly easier for users to work with. Also
most of our effect system usage was copypasta which was passing booleans
as numbers, while perfectly fine helper procs existed in our code.

## Changelog
🆑
refactor: Refactored sparks, foam, smoke, and other miscellaneous effect
systems.
refactor: Vapes now have consistent rigging with cigs using the new
system.
fix: Fixed some effects never working.
/🆑
2026-02-03 22:23:09 -05:00

306 lines
11 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 ITEM_INTERACT_BLOCKING
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, target_mob))
return ITEM_INTERACT_BLOCKING
if (!can_puff(target_mob, user)) // sanity
return ITEM_INTERACT_BLOCKING
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)
return ITEM_INTERACT_SUCCESS
/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)
visible_message(span_warning("[src] breaks open - but is empty!"))
return ..()
do_chem_smoke(1, src, get_turf(src), carry = reagents, log = TRUE)
visible_message(span_warning("[src] breaks open and sprays its aerosilized contents everywhere!"))
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