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

199 lines
6.5 KiB
Plaintext

#define DREAMING_SOURCE "dreaming_source"
/**
* Begins the dreaming process on a sleeping carbon.
*
* Checks a 10% chance and whether or not the carbon this is called on is already dreaming. If
* the prob() passes and there are no dream images left to display, a new dream is constructed.
*/
/mob/living/carbon/proc/handle_dreams()
if(!HAS_TRAIT(src, TRAIT_DREAMING) && prob(10))
dream()
/**
* Generates a dream sequence to be displayed to the sleeper.
*
* Generates the "dream" to display to the sleeper. A dream consists of a subject, a verb, and (most of the time) an object, displayed in sequence to the sleeper.
* Dreams are generated as a list of strings stored inside dream_fragments, which is passed to and displayed in dream_sequence().
* Bedsheets on the sleeper will provide a custom subject for the dream, pulled from the dream_messages on each bedsheet.
*/
/mob/living/carbon/proc/dream()
set waitfor = FALSE
var/list/dream_pool = list()
SEND_SIGNAL(src, COMSIG_PRE_DREAMING, dream_pool)
if(!length(dream_pool))
dream_pool = GLOB.dreams
var/datum/dream/chosen_dream = pick_weight(dream_pool)
ADD_TRAIT(src, TRAIT_DREAMING, DREAMING_SOURCE)
SEND_SIGNAL(src, COMSIG_START_DREAMING, chosen_dream)
dream_sequence(chosen_dream.GenerateDream(src), chosen_dream)
/**
* Displays the passed list of dream fragments to a sleeping carbon.
*
* Displays the first string of the passed dream fragments, then either ends the dream sequence
* or performs a callback on itself depending on if there are any remaining dream fragments to display.
*
* Arguments:
* * dream_fragments - A list of strings, in the order they will be displayed.
* * current_dream - The dream datum used for the current dream
*/
/mob/living/carbon/proc/dream_sequence(list/dream_fragments, datum/dream/current_dream)
if(stat != UNCONSCIOUS || HAS_TRAIT(src, TRAIT_CRITICAL_CONDITION))
REMOVE_TRAIT(src, TRAIT_DREAMING, DREAMING_SOURCE)
current_dream.OnDreamEnd(src)
SEND_SIGNAL(src, COMSIG_END_DREAMING, current_dream)
return
var/next_message = dream_fragments[1]
dream_fragments.Cut(1,2)
if(istype(next_message, /datum/callback))
var/datum/callback/something_happens = next_message
next_message = something_happens.InvokeAsync(src)
to_chat(src, span_notice("<i>... [next_message] ...</i>"))
if(LAZYLEN(dream_fragments))
var/next_wait = rand(10, 30)
if(current_dream.sleep_until_finished)
AdjustSleeping(next_wait)
addtimer(CALLBACK(src, PROC_REF(dream_sequence), dream_fragments, current_dream), next_wait)
else
REMOVE_TRAIT(src, TRAIT_DREAMING, DREAMING_SOURCE)
current_dream.OnDreamEnd(src)
SEND_SIGNAL(src, COMSIG_END_DREAMING, current_dream)
//-------------------------
// DREAM DATUMS
GLOBAL_LIST_INIT(dreams, populate_dream_list())
/proc/populate_dream_list()
var/list/output = list()
for(var/datum/dream/dream_type as anything in subtypesof(/datum/dream))
output[new dream_type] = initial(dream_type.weight)
return output
/**
* Contains all the behavior needed to play a kind of dream.
* All dream types get randomly selected from based on weight when an appropriate mobs dreams.
*/
/datum/dream
/// The relative chance this dream will be randomly selected
var/weight = 0
/// Causes the mob to sleep long enough for the dream to finish if begun
var/sleep_until_finished = FALSE
/**
* Called when beginning a new dream for the dreamer.
* Gives back a list of dream events. Events can be text or callbacks that return text.
*/
/datum/dream/proc/GenerateDream(mob/living/carbon/dreamer)
return list()
/**
* Called when the dream ends or is interrupted.
*/
/datum/dream/proc/OnDreamEnd(mob/living/carbon/dreamer)
return
/// The classic random dream of various words that might form a cohesive narrative, but usually wont
/datum/dream/random
weight = 1000
/datum/dream/random/GenerateDream(mob/living/carbon/dreamer)
var/list/custom_dream_nouns = get_dream_nouns(dreamer) || list()
var/fragment = ""
. = list()
. += "you see"
//Subject
if(custom_dream_nouns.len && prob(90))
fragment += pick(custom_dream_nouns)
else
fragment += pick(GLOB.dream_strings)
if(prob(50)) //Replace the adjective space with an adjective, or just get rid of it
fragment = replacetext(fragment, "%ADJECTIVE%", pick(GLOB.adjectives))
else
fragment = replacetext(fragment, "%ADJECTIVE% ", "")
if(findtext(fragment, "%A% "))
fragment = "\a [replacetext(fragment, "%A% ", "")]"
. += fragment
//Verb
fragment = ""
if(prob(50))
if(prob(35))
fragment += "[pick(GLOB.adverbs)] "
fragment += pick(GLOB.ing_verbs)
else
fragment += "will "
fragment += pick(GLOB.verbs)
. += fragment
if(prob(25))
return
//Object
fragment = ""
fragment += pick(GLOB.dream_strings)
if(prob(50))
fragment = replacetext(fragment, "%ADJECTIVE%", pick(GLOB.adjectives))
else
fragment = replacetext(fragment, "%ADJECTIVE% ", "")
if(findtext(fragment, "%A% "))
fragment = "\a [replacetext(fragment, "%A% ", "")]"
. += fragment
/datum/dream/random/proc/get_dream_nouns(mob/living/carbon/dreamer)
var/list/custom_dream_nouns = list()
for(var/obj/item/bedsheet/sheet in dreamer.loc)
custom_dream_nouns += sheet.dream_messages
return custom_dream_nouns
/// Dream plays a random sound at you, chosen from all sounds in the folder
/datum/dream/hear_something
weight = 500
var/reserved_sound_channel
/datum/dream/hear_something/New()
. = ..()
RegisterSignal(SSsounds, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(ReserveSoundChannel))
/datum/dream/hear_something/GenerateDream(mob/living/carbon/dreamer)
. = ..()
. += pick("you wind up a toy", "you hear something strange", "you pick out a record to play", "you hit shuffle on your music player")
. += CALLBACK(src, PROC_REF(PlayRandomSound))
. += "it reminds you of something"
/datum/dream/hear_something/OnDreamEnd(mob/living/carbon/dreamer)
. = ..()
// In case we play some long ass music track
addtimer(CALLBACK(src, PROC_REF(StopSound), dreamer), 5 SECONDS)
/datum/dream/hear_something/proc/ReserveSoundChannel()
reserved_sound_channel = SSsounds.reserve_sound_channel_for_datum(src)
UnregisterSignal(SSsounds, COMSIG_SUBSYSTEM_POST_INITIALIZE)
/datum/dream/hear_something/proc/PlayRandomSound(mob/living/carbon/dreamer)
var/sound/random_sound = sound(pick(SSsounds.all_sounds), channel=reserved_sound_channel)
random_sound.status = SOUND_STREAM
SEND_SOUND(dreamer, random_sound)
return "you hear something you weren't expecting!"
/datum/dream/hear_something/proc/StopSound(mob/living/carbon/dreamer)
SEND_SOUND(dreamer, sound(channel=reserved_sound_channel))
#undef DREAMING_SOURCE