Files
MrMelbertandGitHub e4f533111f Deafness is now solely tracked by trait (#95029)
## About The Pull Request

Deletes `can_hear`, replaces it with trait-checking deafness.

The only two non-trait sources of deafness (hardcrit and lacking ears)
were refactored into using the trait.

## Why It's Good For The Game

Many places inconsistently check for the deaf trait rather than use
can_hear which meant behavior was not consistent.
Some code would treat "do we lack ears?" as being deaf, some would not. 

This unifies all the behavior so being deaf means you're deaf
everywhere.

It also means we can now easily react to gaining and losing deafness via
signal, where before we could not react to it without hooking the trait,
organ remove, AND stat change. Which no one did, of course, because who
would ever think to do that?

## Changelog

🆑 Melbert
refactor: Refactored how deafness is tracked. Please report any weird
interactions with sounds, like messages or sfx being missing.
fix: Lacking ears and being in hard crit now consistently treats you as
"being deaf". This affects a few minor interactions like empath, the
jukebox, and sleeping.
/🆑
2026-02-05 20:19:56 -05:00

145 lines
5.7 KiB
Plaintext

/// The subsystem used to play ambience to users every now and then, makes them real excited.
SUBSYSTEM_DEF(ambience)
name = "Ambience"
flags = SS_BACKGROUND|SS_NO_INIT
priority = FIRE_PRIORITY_AMBIENCE
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
wait = 1 SECONDS
///Assoc list of listening client - next ambience time
var/list/ambience_listening_clients = list()
var/list/client_old_areas = list()
///Cache for sanic speed :D
var/list/currentrun = list()
/datum/controller/subsystem/ambience/fire(resumed)
if(!resumed)
currentrun = ambience_listening_clients.Copy()
var/list/cached_clients = currentrun
while(cached_clients.len)
var/client/client_iterator = cached_clients[cached_clients.len]
cached_clients.len--
//Check to see if the client exists and isn't held by a new player
var/mob/client_mob = client_iterator?.mob
if(isnull(client_iterator) || !client_mob || isnewplayer(client_mob))
ambience_listening_clients -= client_iterator
client_old_areas -= client_iterator
continue
if(HAS_TRAIT(client_mob, TRAIT_DEAF)) //WHAT? I CAN'T HEAR YOU
continue
//Check to see if the client-mob is in a valid area
var/area/current_area = get_area(client_mob)
if(!current_area) //Something's gone horribly wrong
stack_trace("[key_name(client_mob)] has somehow ended up in nullspace. WTF did you do")
remove_ambience_client(client_iterator)
continue
if(ambience_listening_clients[client_iterator] > world.time)
if(!(current_area.forced_ambience && (client_old_areas?[client_iterator] != current_area) && prob(5)))
continue
//Run play_ambience() on the client-mob and set a cooldown
ambience_listening_clients[client_iterator] = world.time + current_area.play_ambience(client_mob)
//We REALLY don't want runtimes in SSambience
if(client_iterator)
client_old_areas[client_iterator] = current_area
if(MC_TICK_CHECK)
return
///Attempts to play an ambient sound to a mob, returning the cooldown in deciseconds
/area/proc/play_ambience(mob/M, sound/override_sound, volume = 27)
var/sound/new_sound = override_sound || pick(ambientsounds)
if(!new_sound) // Dont try to play a sound if we dont have any.
return 1 MINUTES
/// volume modifier for ambience as set by the player in preferences.
var/volume_modifier = (M.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_ambience_volume))/100
new_sound = sound(new_sound, repeat = 0, wait = 0, volume = volume*volume_modifier, channel = CHANNEL_AMBIENCE)
SEND_SOUND(M, new_sound)
var/sound_length = SSsounds.get_sound_length(new_sound.file)
if(!sound_length)
// This will cause sounds to cut into eachother if the sound is longer then the min_ambience_cooldown
stack_trace("play_ambience failed to get soundlength from [new_sound] with a file of [new_sound.file].")
return sound_length + rand(min_ambience_cooldown, max_ambience_cooldown)
/datum/controller/subsystem/ambience/proc/remove_ambience_client(client/to_remove)
ambience_listening_clients -= to_remove
client_old_areas -= to_remove
currentrun -= to_remove
/area/station/maintenance
///A list of rare sound effects to fuck with players. No, it does not contain actual minecraft sounds anymore.
var/static/list/minecraft_cave_noises = list(
'sound/machines/airlock/airlock.ogg',
'sound/effects/snap.ogg',
'sound/effects/footstep/clownstep1.ogg',
'sound/effects/footstep/clownstep2.ogg',
'sound/items/tools/welder.ogg',
'sound/items/tools/welder2.ogg',
'sound/items/tools/crowbar.ogg',
'sound/items/deconstruct.ogg',
'sound/ambience/misc/source_holehit3.ogg',
'sound/ambience/misc/cavesound3.ogg',
)
/area/station/maintenance/play_ambience(mob/M, sound/override_sound, volume)
if(!M.has_light_nearby() && prob(0.5))
return ..(M, pick(minecraft_cave_noises))
return ..()
/**
* Ambience buzz handling called by either area/Enter() or refresh_looping_ambience()
*/
/mob/proc/update_ambience_area(area/new_area)
var/old_tracked_area = ambience_tracked_area
if(old_tracked_area)
UnregisterSignal(old_tracked_area, COMSIG_AREA_POWER_CHANGE)
ambience_tracked_area = null
if(!client)
return
if(new_area)
ambience_tracked_area = new_area
RegisterSignal(ambience_tracked_area, COMSIG_AREA_POWER_CHANGE, PROC_REF(refresh_looping_ambience), TRUE)
refresh_looping_ambience()
/mob/proc/refresh_looping_ambience()
SIGNAL_HANDLER
if(!client || isobserver(client.mob)) // If a tree falls in the woods. sadboysuss: Don't refresh for ghosts, it sounds bad
return
var/area/my_area = get_area(src)
var/sound_to_use = my_area.ambient_buzz
var/volume_modifier = client.prefs.read_preference(/datum/preference/numeric/volume/sound_ship_ambience_volume)
if(!sound_to_use || !(client.prefs.read_preference(/datum/preference/numeric/volume/sound_ship_ambience_volume)))
SEND_SOUND(src, sound(null, repeat = 0, wait = 0, channel = CHANNEL_BUZZ))
client.current_ambient_sound = null
return
if(HAS_TRAIT(src, TRAIT_DEAF)) // Can the mob hear?
SEND_SOUND(src, sound(null, repeat = 0, wait = 0, channel = CHANNEL_BUZZ))
client.current_ambient_sound = null
return
//Station ambience is dependent on a functioning and charged APC with environment power enabled.
if(!is_mining_level(my_area.z) && ((!my_area.apc || !my_area.apc.operating || !my_area.apc.cell?.charge && my_area.requires_power || !my_area.power_environ)))
SEND_SOUND(src, sound(null, repeat = 0, wait = 0, channel = CHANNEL_BUZZ))
client.current_ambient_sound = null
return
else
if(sound_to_use == client.current_ambient_sound) // Don't reset current loops
return
client.current_ambient_sound = sound_to_use
SEND_SOUND(src, sound(my_area.ambient_buzz, repeat = 1, wait = 0, volume = my_area.ambient_buzz_vol * (volume_modifier / 100), channel = CHANNEL_BUZZ))