// Assets/_Project/Scripts/Audio/AudioVolumeSettings.cs
using UnityEngine;
namespace TD.Audio
{
///
/// Player-facing volume mix, persisted in and shared by
/// every audio consumer in the game. Two buses:
///
/// - Music — the looping scene track played by .
/// - SFX — everything routed through
/// (spells, tower fire, build/land, enemy death, sell, UI clicks).
///
///
///
/// Why not named AudioSettings. UnityEngine.AudioSettings already
/// exists, so a TD.Audio.AudioSettings would be an ambiguous reference in any
/// file that has both using UnityEngine; and using TD.Audio;.
///
/// Units. Public values are ints 0..100 — the numbers the settings slider
/// shows. Consumers multiply by / ,
/// which apply a squared taper so the slider's lower half isn't perceptually dead
/// (linear amplitude drops off much faster than perceived loudness). At 100 the
/// scalar is exactly 1, so the default mix is byte-identical to the pre-settings
/// behaviour.
///
/// Live updates. fires on every mutation.
/// Continuous sources (music) subscribe and re-apply; one-shots read the scalar at
/// play time, so a mid-flight SFX keeps the volume it started with. That's fine —
/// they're all short.
///
public static class AudioVolumeSettings
{
public const int MinVolume = 0;
public const int MaxVolume = 100;
private const int DefaultVolume = 100;
private const string MusicKey = "td.audio.music";
private const string SfxKey = "td.audio.sfx";
/// Raised whenever music or SFX volume changes.
public static event System.Action OnChanged;
// -1 = not yet read from PlayerPrefs. Statics are cleared on domain reload
// (entering play mode in the editor), so the lazy load re-runs each session.
private static int music = -1;
private static int sfx = -1;
public static int MusicVolume
{
get
{
if (music < 0) music = PlayerPrefs.GetInt(MusicKey, DefaultVolume);
return music;
}
set => Set(ref music, MusicKey, value);
}
public static int SfxVolume
{
get
{
if (sfx < 0) sfx = PlayerPrefs.GetInt(SfxKey, DefaultVolume);
return sfx;
}
set => Set(ref sfx, SfxKey, value);
}
/// Amplitude multiplier (0..1) for the music bus.
public static float MusicScalar => ToScalar(MusicVolume);
/// Amplitude multiplier (0..1) for the sound-effects bus.
public static float SfxScalar => ToScalar(SfxVolume);
///
/// Flushes PlayerPrefs to disk. Setters only write the in-memory pref (cheap
/// enough to call on every frame of a slider drag); call this once when the
/// settings UI closes so the choice survives a crash.
///
public static void Flush() => PlayerPrefs.Save();
private static void Set(ref int field, string key, int value)
{
int clamped = Mathf.Clamp(value, MinVolume, MaxVolume);
if (field == clamped) return;
field = clamped;
PlayerPrefs.SetInt(key, clamped);
OnChanged?.Invoke();
}
private static float ToScalar(int volume)
{
float linear = Mathf.Clamp01(volume / (float)MaxVolume);
return linear * linear;
}
}
}