Files
CabinetOnFire 5a68cb5ff3 Implements "Sound Tokens", a way to have spatial audio. And implements it on looping sounds. (And also some tweaks to fall-off) (#96018)
## About The Pull Request

This pull request implements sound tokens, based on the implementation
by Kapu at https://github.com/DaedalusDock/daedalusdock/pull/1379

I've taken that implementation and extended it to looping_sounds,
allowing us to have spatial audio that updates much more nicely when
players walk closer. This system also lets players that walk into range
after the sound has started playing to also hear the sound, before if
you missed the sound, you would never hear it. This is particularly
noticeable with long sounds (Which is lame!)

**VIDEOS**:
griddle:
https://streamable.com/v7ayqm

engineering / SM:
https://streamable.com/5lr1oj


The system essentially sends the sound to every client, and changes the
volume based on distance to the sound. There are some performance
concerns here, so we will need to testmerge this and see how impactful
it is. It is in a subsystem though, so there is some limit to the amount
of cpu it will use. Do keep this in mind when watching the videos; it
will probably not be AS responsive as in there.

I've also changed how intense our fall-off is, making sounds lose their
volume slower. Before taking one step away from a 50 volume sound would
make it 18 volume, this is way too big of a drop and it became
particularly jarring with the changes in this PR.

Some sounds might be too long range now, but I think we should just
reduce the max range of those sounds as we were basically playing them
at <5 volume after 10 tiles, at which point we should maybe just reduce
the range a bit since at those volumes theyre really not audible.

**OLD FALLOFF;** note the MASSIVE drop from 1 tile to 2 tiles
<img width="1416" height="897" alt="image"
src="https://github.com/user-attachments/assets/805b270e-6aa5-4e84-868e-39585966f0a4"
/>

**NEW FALLOFF:**
<img width="1418" height="905" alt="image"
src="https://github.com/user-attachments/assets/eabbd592-ae9c-4690-8945-fbd0b4f6614f"
/>

Also tweaked a few looping sounds (deep-fryer in particular got some
clean-up, as its mid-length was way too short for the sounds and it had
a double-definition)


## Why It's Good For The Game

This system allows us to have a richer audio experience, I really don't
like how choppy our looping sounds are, especially in some areas where
you hear them a lot (kitchen and engineering)



## Changelog

🆑 CabinetOnFire, Kapu
sound: Changed our falloff to be less intense, as it was punching volume
down way too fast.
refactor: Implements a system for spatial audio to improve our looping
sounds
/🆑

---------

Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com>
Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
2026-06-05 16:24:25 -07:00

388 lines
16 KiB
Plaintext

// the different states of the mystery box
/// Closed, can't interact
#define MYSTERY_BOX_COOLING_DOWN 0
/// Closed, ready to be interacted with
#define MYSTERY_BOX_STANDBY 1
/// The box is choosing the prize
#define MYSTERY_BOX_CHOOSING 2
/// The box is presenting the prize, for someone to claim it
#define MYSTERY_BOX_PRESENTING 3
// delays for the different stages of the box's state, the visuals, and the audio
/// How long the box takes to decide what the prize is
#define MBOX_DURATION_CHOOSING (5 SECONDS)
/// How long the box takes to start expiring the offer, though it's still valid until MBOX_DURATION_EXPIRING finishes. Timed to the sound clips
#define MBOX_DURATION_PRESENTING (3.5 SECONDS)
/// How long the box takes to start lowering the prize back into itself. When this finishes, the prize is gone
#define MBOX_DURATION_EXPIRING (4.5 SECONDS)
/// How long after the box closes until it can go again
#define MBOX_DURATION_STANDBY (2.7 SECONDS)
GLOBAL_LIST_INIT(mystery_box_guns, list(
/obj/item/gun/energy/recharge/ebow/large,
/obj/item/gun/energy/e_gun,
/obj/item/gun/energy/e_gun/nuclear,
/obj/item/gun/energy/laser,
/obj/item/gun/energy/laser/soul,
/obj/item/gun/energy/laser/pistol,
/obj/item/gun/energy/laser/assault,
/obj/item/gun/energy/laser/hellgun,
/obj/item/gun/energy/laser/captain,
/obj/item/gun/energy/laser/scatter,
/obj/item/gun/energy/temperature,
/obj/item/gun/ballistic/revolver/c38/detective,
/obj/item/gun/ballistic/revolver/mateba,
/obj/item/gun/ballistic/automatic/pistol/deagle/camo,
/obj/item/gun/ballistic/automatic/pistol/suppressed,
/obj/item/gun/energy/pulse/carbine/taserless,
/obj/item/gun/energy/pulse/pistol/taserless,
/obj/item/gun/ballistic/shotgun/lethal,
/obj/item/gun/ballistic/shotgun/automatic/combat,
/obj/item/gun/ballistic/shotgun/bulldog/unrestricted,
/obj/item/gun/ballistic/rifle/boltaction,
/obj/item/gun/ballistic/automatic/ar,
/obj/item/gun/ballistic/automatic/proto/unrestricted,
/obj/item/gun/ballistic/automatic/c20r/unrestricted,
/obj/item/gun/ballistic/automatic/l6_saw/unrestricted,
/obj/item/gun/ballistic/automatic/m90/unrestricted,
/obj/item/gun/ballistic/automatic/tommygun,
/obj/item/gun/ballistic/automatic/wt550,
/obj/item/gun/ballistic/automatic/smartgun,
/obj/item/gun/ballistic/rifle/sniper_rifle,
/obj/item/gun/ballistic/rifle/boltaction,
))
GLOBAL_LIST_INIT(mystery_box_extended, list(
/obj/item/clothing/gloves/tackler/combat,
/obj/item/clothing/gloves/race,
/obj/item/clothing/gloves/rapid,
/obj/item/shield/riot/flash,
/obj/item/grenade/stingbang/mega,
/obj/item/storage/belt/sheath/sabre,
/obj/item/knife/combat,
/obj/item/melee/baton/security/loaded,
/obj/item/reagent_containers/hypospray/combat,
/obj/item/defibrillator/compact/combat/loaded/nanotrasen,
/obj/item/melee/energy/sword/saber,
/obj/item/spear,
/obj/item/circular_saw,
))
GLOBAL_LIST_INIT(mystery_magic, list(
/obj/item/gun/magic/wand/arcane_barrage,
/obj/item/gun/magic/wand/arcane_barrage/blood,
/obj/item/gun/magic/wand/fireball,
/obj/item/gun/magic/wand/resurrection,
/obj/item/gun/magic/wand/teleport,
/obj/item/gun/magic/wand/door,
/obj/item/gun/magic/wand/nothing,
/obj/item/storage/belt/wands/full,
/obj/item/storage/belt/wands/full/discount,
/obj/item/gun/magic/staff/healing,
/obj/item/gun/magic/staff/chaos,
/obj/item/gun/magic/staff/door,
/obj/item/gun/magic/staff/honk,
/obj/item/gun/magic/staff/spellblade,
/obj/item/gun/magic/staff/flying,
/obj/item/gun/magic/staff/babel,
/obj/item/singularityhammer,
/obj/item/runic_vendor_scepter,
))
GLOBAL_LIST_INIT(mystery_fishing, list(
/obj/item/storage/toolbox/fishing/master,
/obj/item/storage/box/fish_revival_kit,
/obj/item/fishing_rod/telescopic/master,
/obj/item/bait_can/super_baits,
/obj/item/storage/fish_case/tiziran,
/obj/item/storage/fish_case/syndicate,
/obj/item/claymore/cutlass/old,
/obj/item/gun/energy/laser/retro/old,
/obj/item/gun/energy/laser/musket,
/obj/item/gun/energy/disabler/smoothbore,
/obj/item/gun/ballistic/rifle/boltaction/surplus,
/obj/item/food/rationpack,
/obj/item/food/canned/squid_ink,
/obj/item/reagent_containers/cup/glass/bottle/rum/aged,
/obj/item/storage/bag/money/dutchmen,
/obj/item/language_manual/piratespeak,
/obj/item/clothing/head/costume/pirate/armored,
/obj/item/clothing/suit/costume/pirate/armored,
/obj/structure/cannon/mystery_box,
/obj/item/stack/cannonball/trashball/four,
/obj/item/stack/cannonball/four,
))
/obj/structure/mystery_box
name = "mystery box"
desc = "A wooden crate that seems equally magical and mysterious, capable of granting the user all kinds of different pieces of gear."
icon = 'icons/obj/storage/crates.dmi'
icon_state = "wooden"
pixel_y = -4
anchored = TRUE
density = TRUE
max_integrity = 99999
damage_deflection = 100
var/crate_open_sound = 'sound/machines/crate/crate_open.ogg'
var/crate_close_sound = 'sound/machines/crate/crate_close.ogg'
var/open_sound = 'sound/effects/mysterybox/mbox_full.ogg'
var/grant_sound = 'sound/effects/mysterybox/mbox_end.ogg'
/// The box's current state, and whether it can be interacted with in different ways
var/box_state = MYSTERY_BOX_STANDBY
/// The object that represents the rapidly changing item that will be granted upon being claimed. Is not, itself, an item.
var/obj/effect/abstract/mystery_box_item/presented_item
/// A timer for how long it takes for the box to start its expire animation
var/box_expire_timer
/// A timer for how long it takes for the box to close itself
var/box_close_timer
/// Every type that's a child of this that has an icon, icon_state, and isn't ABSTRACT is fair game. More granularity to come
var/selectable_base_type = /obj/item
/// The instantiated list that contains all of the valid items that can be chosen from. Generated in [/obj/structure/mystery_box/proc/generate_valid_types]
var/list/valid_types
/// If the prize is a ballistic gun with an external magazine, should we grant the user a spare mag?
var/grant_extra_mag = TRUE
/// Stores the current sound channel we're using so we can cut off our own sounds as needed. Randomized after each roll
var/current_sound_channel
/// How many time can it still be used?
var/uses_left = INFINITY
/// A list of weakrefs to mind datums of people that opened it and how many times.
var/list/datum/weakref/minds_that_opened_us
/obj/structure/mystery_box/Initialize(mapload)
. = ..()
generate_valid_types()
/obj/structure/mystery_box/Destroy()
QDEL_NULL(presented_item)
if(current_sound_channel)
SSsounds.free_sound_channel(current_sound_channel)
minds_that_opened_us = null
return ..()
/obj/structure/mystery_box/attack_hand(mob/living/user, list/modifiers)
. = ..()
switch(box_state)
if(MYSTERY_BOX_STANDBY)
activate(user)
if(MYSTERY_BOX_PRESENTING)
if(presented_item.claimable)
grant_weapon(user)
/obj/structure/mystery_box/update_icon_state()
icon_state = "[initial(icon_state)][box_state > MYSTERY_BOX_STANDBY ? "open" : ""]"
return ..()
/// This proc is used to define what item types valid_types is filled with
/obj/structure/mystery_box/proc/generate_valid_types()
valid_types = get_sane_item_types(selectable_base_type)
/obj/structure/mystery_box/IsContainedAtomAccessible(atom/contained, atom/movable/user)
return TRUE
/// The box has been activated, play the sound and spawn the prop item
/obj/structure/mystery_box/proc/activate(mob/living/user)
box_state = MYSTERY_BOX_CHOOSING
update_icon_state()
presented_item = new(src)
presented_item.vis_flags = VIS_INHERIT_PLANE
vis_contents += presented_item
presented_item.start_animation(src)
current_sound_channel = SSsounds.reserve_sound_channel_for_datum(src)
playsound(src, open_sound, 70, FALSE, channel = current_sound_channel, falloff_exponent = 10)
playsound(src, crate_open_sound, 80)
if(user.mind)
LAZYINITLIST(minds_that_opened_us)
var/datum/weakref/ref = WEAKREF(user.mind)
minds_that_opened_us[ref] += 1
uses_left--
/// The box has finished choosing, mark it as available for grabbing
/obj/structure/mystery_box/proc/present_weapon()
visible_message(span_notice("[src] presents [presented_item]!"), vision_distance = COMBAT_MESSAGE_RANGE)
box_state = MYSTERY_BOX_PRESENTING
box_expire_timer = addtimer(CALLBACK(src, PROC_REF(start_expire_offer)), MBOX_DURATION_PRESENTING, TIMER_STOPPABLE)
/// The prize is still claimable, but the animation will show it start to recede back into the box
/obj/structure/mystery_box/proc/start_expire_offer()
presented_item.expire_animation()
box_close_timer = addtimer(CALLBACK(src, PROC_REF(close_box)), MBOX_DURATION_EXPIRING, TIMER_STOPPABLE)
/// The box is closed, whether because the prize fully expired, or it was claimed. Start resetting all of the state stuff
/obj/structure/mystery_box/proc/close_box()
box_state = MYSTERY_BOX_COOLING_DOWN
update_icon_state()
QDEL_NULL(presented_item)
deltimer(box_close_timer)
deltimer(box_expire_timer)
playsound(src, crate_close_sound, 100)
box_close_timer = null
box_expire_timer = null
addtimer(CALLBACK(src, PROC_REF(ready_again)), MBOX_DURATION_STANDBY)
if(uses_left <= 0)
visible_message("[src] breaks down.")
deconstruct(disassembled = FALSE)
/// The cooldown between activations has finished, shake to show that
/obj/structure/mystery_box/proc/ready_again()
SSsounds.free_sound_channel(current_sound_channel)
current_sound_channel = null
box_state = MYSTERY_BOX_STANDBY
Shake(3, 0, 0.5 SECONDS)
/// Someone attacked the box with an empty hand, spawn the shown prize and give it to them, then close the box
/obj/structure/mystery_box/proc/grant_weapon(mob/living/user)
var/atom/movable/instantiated_weapon = new presented_item.selected_path(loc)
user.visible_message(span_notice("[user] takes [presented_item] from [src]."), span_notice("You take [presented_item] from [src]."), vision_distance = COMBAT_MESSAGE_RANGE)
playsound(src, grant_sound, 70, FALSE, channel = current_sound_channel, falloff_exponent = 10)
close_box()
if(!isitem(instantiated_weapon))
return
user.put_in_hands(instantiated_weapon)
if(!isgun(instantiated_weapon))
return
// handle pins + possibly extra ammo
var/obj/item/gun/instantiated_gun = instantiated_weapon
instantiated_gun.unlock()
if(!grant_extra_mag || !istype(instantiated_gun, /obj/item/gun/ballistic))
return
var/obj/item/gun/ballistic/instantiated_ballistic = instantiated_gun
if(!instantiated_ballistic.internal_magazine)
var/obj/item/ammo_box/magazine/extra_mag = new instantiated_ballistic.spawn_magazine_type(loc)
user.put_in_hands(extra_mag)
/obj/structure/mystery_box/guns
desc = "A wooden crate that seems equally magical and mysterious, capable of granting the user all kinds of different pieces of gear. This one seems focused on firearms."
/obj/structure/mystery_box/guns/generate_valid_types()
valid_types = GLOB.summoned_guns
/obj/structure/mystery_box/tdome
desc = "A wooden crate that seems equally magical and mysterious, capable of granting the user all kinds of different pieces of gear. This one has an extended array of weaponry."
/obj/structure/mystery_box/tdome/generate_valid_types()
valid_types = GLOB.mystery_box_guns + GLOB.mystery_box_extended
/obj/structure/mystery_box/wands
desc = "A wooden crate that seems equally magical and mysterious, capable of granting the user all kinds of different magical items."
/obj/structure/mystery_box/wands/generate_valid_types()
valid_types = GLOB.mystery_magic
/obj/structure/mystery_box/wildcard
desc = "A wooden crate that seems equally magical and mysterious, capable of granting the user all kinds of different pieces of gear. This one has an EXTREAMLY extended array of weaponry."
/obj/structure/mystery_box/wildcard/generate_valid_types()
valid_types = GLOB.summoned_all_guns
///A fishing and pirate-themed mystery box, rarely found by fishing in the ocean, then another cannot be caught for the next 30 minutes.
/obj/structure/mystery_box/fishing
name = "treasure chest"
desc = "A piratey coffer equally magical and mysterious, capable of granting different pieces of gear to whoever opens it."
icon_state = "treasure"
uses_left = 18
max_integrity = 100
damage_deflection = 30
grant_extra_mag = FALSE
anchored = FALSE
/obj/structure/mystery_box/handle_deconstruct(disassembled)
new /obj/item/stack/sheet/mineral/wood(drop_location(), 2)
return ..()
/obj/structure/mystery_box/fishing/generate_valid_types()
valid_types = GLOB.mystery_fishing
/obj/structure/mystery_box/fishing/activate(mob/living/user)
if(user.mind && minds_that_opened_us?[WEAKREF(user.mind)] >= 3)
to_chat(user, span_warning("[src] refuses to open to you anymore. Perhaps you should present it to someone else..."))
return
return ..()
/// This represents the item that comes out of the box and is constantly changing before the box finishes deciding. Can probably be just an /atom or /movable.
/obj/effect/abstract/mystery_box_item
name = "???"
desc = "Who knows what it'll be??"
icon = 'icons/obj/weapons/guns/ballistic.dmi'
icon_state = "revolver"
pixel_z = -4
uses_integrity = FALSE
/// The currently selected item. Constantly changes while choosing, determines what is spawned if the prize is claimed, and its current icon
var/selected_path = /obj/item/gun/ballistic/revolver/c38/detective
/// The box that spawned this
var/obj/structure/mystery_box/parent_box
/// Whether this prize is currently claimable
var/claimable = FALSE
/obj/effect/abstract/mystery_box_item/Initialize(mapload)
. = ..()
var/matrix/starting = matrix()
starting.Scale(0.5,0.5)
transform = starting
add_filter("weapon_rays", 3, list("type" = "rays", "size" = 28, "color" = COLOR_VIVID_YELLOW))
/obj/effect/abstract/mystery_box_item/Destroy(force)
parent_box = null
return ..()
// this way, clicking on the prize will work the same as clicking on the box
/obj/effect/abstract/mystery_box_item/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(claimable)
parent_box.grant_weapon(user)
/// Start pushing the prize up
/obj/effect/abstract/mystery_box_item/proc/start_animation(atom/parent)
parent_box = parent
loop_icon_changes()
/// Keep changing the icon and selected path
/obj/effect/abstract/mystery_box_item/proc/loop_icon_changes()
var/change_delay = 1 // the running count of the delay
var/change_delay_delta = 1 // How much to increment the delay per step so the changing slows down
var/change_counter = 0 // The running count of the running count
var/matrix/starting = matrix()
animate(src, pixel_z = 10, transform = starting, time = MBOX_DURATION_CHOOSING, easing = QUAD_EASING | EASE_OUT)
while((change_counter + change_delay_delta + change_delay) < MBOX_DURATION_CHOOSING)
change_delay += change_delay_delta
change_counter += change_delay
selected_path = pick(parent_box.valid_types)
addtimer(CALLBACK(src, PROC_REF(update_random_icon), selected_path), change_counter)
addtimer(CALLBACK(src, PROC_REF(present_item)), MBOX_DURATION_CHOOSING)
/// animate() isn't up to the task for queueing up icon changes, so this is the proc we call with timers to update our icon
/obj/effect/abstract/mystery_box_item/proc/update_random_icon(new_item_type)
var/atom/movable/new_item = new_item_type
icon = new_item::icon
icon_state = new_item::icon_state
/obj/effect/abstract/mystery_box_item/proc/present_item()
var/atom/movable/selected_item = selected_path
add_filter("ready_outline", 2, list("type" = "outline", "color" = COLOR_VIVID_YELLOW, "size" = 0.2))
name = selected_item::name
parent_box.present_weapon()
claimable = TRUE
/// Sink back into the box
/obj/effect/abstract/mystery_box_item/proc/expire_animation()
var/matrix/shrink_back = matrix()
shrink_back.Scale(0.5,0.5)
animate(src, pixel_z = -4, transform = shrink_back, time = MBOX_DURATION_EXPIRING)
#undef MYSTERY_BOX_COOLING_DOWN
#undef MYSTERY_BOX_STANDBY
#undef MYSTERY_BOX_CHOOSING
#undef MYSTERY_BOX_PRESENTING
#undef MBOX_DURATION_CHOOSING
#undef MBOX_DURATION_PRESENTING
#undef MBOX_DURATION_EXPIRING
#undef MBOX_DURATION_STANDBY