GameAudio.cs
namespace NoChillquarium;

/// <summary>
/// UI SFX + music beds.
/// Rule of thumb: <b>chill</b> when the tank is calm; <b>no-chill / metal</b>
/// when things get absurd or dark (chaos, sharks, meth, M80, death, battles…).
/// Priority: detonation sting &gt; death ramp &gt; absurd/no-chill &gt; chill.
/// </summary>
public static class GameAudio
{
	public const string Pop = "sounds/ui/pop";
	public const string Notice = "sounds/ui/notice";
	public const string Close = "sounds/ui/close_window";
	public const string Error = "sounds/ui/error";
	public const string Confirm = "sounds/ui/confirm";
	public const string AlertWarning = "sounds/ui/alert_warning";
	/// <summary>Dog bark — Dogecoin jackpots / big wallet wins only. Not for UI chrome.</summary>
	public const string Doge = "sounds/ui/doge";
	/// <summary>Short coin / trade sting — Dogecoin spend/earn feedback (not bark).</summary>
	public const string Coin = "sounds/ui/trade";
	public const string Feed = "sounds/ui/feed";
	public const string Upgrade = "sounds/ui/upgrade";
	public const string Trade = "sounds/ui/trade";
	public const string Tape = "sounds/ui/tape";
	public const string M80 = "sounds/ui/m80";
	public const string Inject = "sounds/ui/inject";
	public const string Achievement = "sounds/ui/achievement";
	/// <summary>Golden stamp — short "favorite" ding.</summary>
	public const string Favorite = "sounds/ui/favorite";
	/// <summary>Egg hatch chirp.</summary>
	public const string Hatch = "sounds/ui/hatch";
	/// <summary>Abomination birth growl (trimmed).</summary>
	public const string Growl = "sounds/ui/growl";
	/// <summary>Salt tank / ocean sting.</summary>
	public const string Dolphin = "sounds/ui/dolphin";
	/// <summary>Blue whale paperwork sting.</summary>
	public const string Whale = "sounds/ui/whale";

	public const string MenuMusic = "sounds/music/bitten_crown";
	public const string ChillTankMusic = "sounds/music/moonberry";

	// Escalation ladder when a fish is dying (especially from chill).
	// 1 unease → 2 metal → 3 heavy → 4 death peak
	const string DeathStage1 = "sounds/music/gloom_cartridge";      // uneasy / dark
	const string DeathStage2 = "sounds/music/game_boy_hex";         // gear up
	const string DeathStage3 = "sounds/music/pixel_horns";          // heavy
	const string DeathStage4 = "sounds/music/moonbyte_blasphemy";   // death metal peak

	/// <summary>All no-chill beds — rotate so monsters/meth don't lock one track forever.</summary>
	static readonly string[] MetalPlaylist =
	{
		"sounds/music/bitten_crown",
		"sounds/music/game_boy_hex",
		"sounds/music/pixel_horns",
		"sounds/music/pixel_hex_rite",
		"sounds/music/gloom_cartridge",
		"sounds/music/moonbyte_blasphemy"
	};

	/// <summary>Seconds on one metal bed before auto-advancing (while still no-chill).</summary>
	const float MetalRotateSeconds = 110f;

	static SoundHandle _music;
	static string _musicEvent;
	static int _metalIndex;
	static int _lastDeathStage = -1;
	static bool? _lastNoChillBed;
	static float _detonationStingLeft;
	static string _lastAbsurdReason = "";
	static float _musicRetryCooldown;
	static string _lastMissingMusic;
	static float _metalBedTimer;
	static string _activeMetalPick;
	static readonly Random _musicRng = new();
	/// <summary>Short music volume kick on big moments (sell, boom, stage, fight).</summary>
	static float _pulseBoost;
	static float _pulseTimer;

	public static bool DetonationStingActive => _detonationStingLeft > 0f;

	/// <summary>Kick music intensity for a beat so the bed reacts to gameplay, not only mood swaps.</summary>
	public static void PulseMusic( float amount = 0.14f, float seconds = 0.9f )
	{
		amount = Math.Clamp( amount, 0.04f, 0.35f );
		seconds = Math.Clamp( seconds, 0.25f, 2.5f );
		_pulseBoost = Math.Max( _pulseBoost, amount );
		_pulseTimer = Math.Max( _pulseTimer, seconds );
	}

	/// <summary>
	/// True when gameplay should feel NO-CHILL (music + chrome), not only high chaos.
	/// </summary>
	public static bool WantsNoChillMusic => EvaluateAbsurd( out _ );

	public static string AbsurdReason
	{
		get
		{
			EvaluateAbsurd( out var reason );
			return reason ?? "";
		}
	}

	public static string CurrentBedLabel
	{
		get
		{
			if ( string.IsNullOrEmpty( _musicEvent ) )
				return "—";
			if ( DetonationStingActive )
				return "DEATH METAL";
			if ( _musicEvent.Contains( "moonberry" ) )
				return "CHILL BED";
			if ( _musicEvent.Contains( "gloom" ) )
				return "UNEASE BED";
			if ( _musicEvent.Contains( "moonbyte" ) || _musicEvent.Contains( "pixel_hex" ) )
				return "DEATH METAL";
			if ( _musicEvent.Contains( "pixel_horns" ) )
				return "HEAVY METAL";
			return "NO-CHILL BED";
		}
	}

	/// <summary>
	/// Absurd / dark moments that pull the bed off chill even if chaos is still low.
	/// Calm tank + low chaos + no hazards → chill.
	/// </summary>
	public static bool EvaluateAbsurd( out string reason )
	{
		reason = "";

		if ( DetonationStingActive )
		{
			reason = "detonation";
			return true;
		}

		if ( BoomFeel.IsActive )
		{
			reason = "boom";
			return true;
		}

		if ( DeathFeel.PeakStage > 0 )
		{
			reason = "dying";
			return true;
		}

		if ( SharkSystem.HasSharks )
		{
			reason = "sharks";
			return true;
		}

		if ( BattleSystem.Phase == BattlePhase.Fighting )
		{
			reason = "battle";
			return true;
		}

		if ( BattleSystem.IsOpen )
		{
			reason = "arena";
			return true;
		}

		if ( LotRunSystem.IsOpen && !LotRunSystem.IsFlushing )
		{
			reason = "adventure";
			return true;
		}

		if ( MethRush.Open )
		{
			reason = "meth-rush";
			return true;
		}

		if ( TrainingSystem.InjectOpen )
		{
			reason = "inject";
			return true;
		}

		if ( ExplodeSystem.ModeActive && ExplodeSystem.SelectedCount > 0 )
		{
			reason = "m80-armed";
			return true;
		}

		// Fusion abominations force NO-CHILL bed (any unlocked tank).
		if ( TankSim.HasAnyMonster )
		{
			reason = "monsters";
			return true;
		}

		// Living absurdity in the active tank
		if ( TankSim.IsActive )
		{
			var meth = 0;
			var jacked = 0;
			var wd = 0;
			foreach ( var f in TankSim.Fish )
			{
				if ( f.OnMeth )
					meth++;
				if ( f.Jacked )
					jacked++;
				if ( f.InWithdrawal )
					wd++;
			}

			if ( meth > 0 )
			{
				reason = "meth";
				return true;
			}

			if ( wd >= 2 )
			{
				reason = "withdrawal";
				return true;
			}

			if ( jacked >= 3 )
			{
				reason = "jacked-pack";
				return true;
			}
		}

		// High chaos meter = classic no-chill band
		if ( Chaos.IsNoChill )
		{
			reason = "chaos";
			return true;
		}

		// Ocean alone is tense but still "chill bed" until sharks or chaos — keep calm water chill.
		return false;
	}

	/// <summary>Hard cut to peak death metal for a few seconds after M80 detonation.</summary>
	public static void TriggerDetonationSting( int victimCount )
	{
		if ( GameFlow.Screen != GameScreen.Playing )
			return;

		var n = Math.Max( 1, victimCount );
		_detonationStingLeft = 4.2f + n * 0.55f;
		// Peak blasphemy + louder.
		StartMusic( DeathStage4, force: true, volumeMult: 1.28f );
	}

	public static void PlayUi( string soundEvent )
	{
		if ( string.IsNullOrWhiteSpace( soundEvent ) )
			return;

		// Never let the bark leak through a mis-wire — only PlayDogeBark uses it.
		if ( string.Equals( soundEvent, Doge, StringComparison.OrdinalIgnoreCase ) )
		{
			PlayDogeBark();
			return;
		}

		var handle = PlayLocal( soundEvent, GameSettings.SfxVolume );
		// One soft fail is fine — avoid console spam for SFX.
		_ = handle;
	}

	/// <summary>
	/// Pack-open ticks. Step 0 deal · 1 unfold · 2 fish · 3 rarity stamp.
	/// Pitch climbs so the unfold feels like a slot stopping.
	/// </summary>
	public static void PlayBuyTick( int step, FishRarity rarity )
	{
		var vol = Math.Clamp( GameSettings.SfxVolume, 0f, 1f );
		step = Math.Clamp( step, 0, 3 );
		var pitch = 0.92f + step * 0.08f;

		if ( step == 0 )
		{
			PlayPitched( Pop, vol * 0.88f, pitch );
			return;
		}

		if ( step == 1 )
		{
			PlayPitched( Pop, vol * 0.72f, pitch + 0.04f );
			return;
		}

		if ( step == 2 )
		{
			PlayPitched( Trade, vol * 0.95f, 1.02f );
			return;
		}

		// Stamp — commons thud, jackpots explode.
		if ( rarity >= FishRarity.Mythical )
		{
			PlayBigWin( bark: true );
			PlayPitched( Favorite, vol * 0.85f, 1.12f );
			return;
		}

		if ( rarity >= FishRarity.Legendary )
		{
			PlayBigWin( bark: false );
			PlayPitched( Favorite, vol * 0.95f, 1.05f );
			return;
		}

		if ( rarity >= FishRarity.PaintedRare )
		{
			PlayLayered( Achievement, Confirm, 0.55f );
			PulseMusic( 0.14f, 0.7f );
			return;
		}

		if ( rarity >= FishRarity.Rare )
		{
			PlayLayered( Upgrade, Confirm, 0.5f );
			PulseMusic( 0.1f, 0.55f );
			return;
		}

		if ( rarity >= FishRarity.Uncommon )
		{
			PlayPitched( Confirm, vol, 1.1f );
			return;
		}

		// Common: quiet pop so the next click is the thought.
		PlayPitched( Pop, vol * 0.7f, 0.88f );
	}

	static void PlayPitched( string soundEvent, float volume, float pitch )
	{
		_ = PlayLocal( soundEvent, volume, pitch );
	}

	/// <summary>
	/// Dogecoin jackpot bark. Call only for meaningful Ð wins (fight prize, shark loot, big milestone).
	/// </summary>
	public static void PlayDogeBark()
	{
		var handle = PlayLocal( Doge, GameSettings.SfxVolume );
		_ = handle;
	}

	/// <summary>Soft coin / trade ping for Dogecoin earn or spend feedback.</summary>
	public static void PlayCoin()
	{
		var handle = PlayLocal( Coin, GameSettings.SfxVolume * 0.9f );
		_ = handle;
	}

	/// <summary>
	/// Layer two SFX so big moments land even if one event fails to load.
	/// Primary at full SFX vol; secondary quieter and slightly delayed feel via volume only.
	/// </summary>
	public static void PlayLayered( string primary, string secondary, float secondaryMul = 0.55f )
	{
		var vol = Math.Clamp( GameSettings.SfxVolume, 0f, 1f );
		if ( !string.IsNullOrWhiteSpace( primary ) )
			_ = PlayLocal( primary, vol );
		if ( !string.IsNullOrWhiteSpace( secondary ) && secondaryMul > 0.01f )
			_ = PlayLocal( secondary, vol * Math.Clamp( secondaryMul, 0.1f, 1f ) );
	}

	/// <summary>Sell / stage / fight win — coin + upgrade so the wallet *and* the room react.</summary>
	public static void PlayBigCash()
	{
		PlayLayered( Coin, Upgrade, 0.5f );
		PulseMusic( 0.12f, 0.7f );
	}

	/// <summary>Combine birth / championship / shark kill — bark-tier without always barking.</summary>
	public static void PlayBigWin( bool bark = false )
	{
		if ( bark )
		{
			PlayDogeBark();
			_ = PlayLocal( Upgrade, Math.Clamp( GameSettings.SfxVolume * 0.7f, 0f, 1f ) );
			PulseMusic( 0.22f, 1.2f );
			return;
		}
		PlayLayered( Confirm, Coin, 0.65f );
		PulseMusic( 0.16f, 0.9f );
	}

	/// <summary>Egg hatch — chirp, hotter if the guppy is spicy.</summary>
	public static void PlayHatch( FishRarity rarity )
	{
		var vol = Math.Clamp( GameSettings.SfxVolume, 0f, 1f );
		PlayPitched( Hatch, vol, rarity >= FishRarity.Rare ? 1.12f : 1f );
		if ( rarity >= FishRarity.Legendary )
			PlayPitched( Favorite, vol * 0.7f, 1.04f );
	}

	/// <summary>Combine birth — growl over the upgrade sting.</summary>
	public static void PlayFusion()
	{
		PlayLayered( Growl, Upgrade, 0.55f );
		PulseMusic( 0.18f, 0.9f );
	}

	/// <summary>Salt tank online — short dolphin.</summary>
	public static void PlaySaltUnlock()
	{
		PlayLayered( Dolphin, Upgrade, 0.5f );
		PulseMusic( 0.12f, 0.7f );
	}

	/// <summary>Blue whale bought.</summary>
	public static void PlayWhale()
	{
		PlayLayered( Whale, Coin, 0.4f );
		PulseMusic( 0.16f, 1.0f );
	}

	/// <summary>Shark enters — alert + notice so it never feels silent.</summary>
	public static void PlaySharkAlert()
	{
		PlayLayered( AlertWarning, Notice, 0.45f );
	}

	/// <summary>Training minigame — clear hit / miss feedback (slightly hotter than chrome UI).</summary>
	public static void PlayTrainRep( bool hit, bool perfect = false )
	{
		var vol = Math.Clamp( GameSettings.SfxVolume * 1.2f, 0f, 1f );
		if ( !hit )
		{
			_ = PlayLocal( Error, vol );
			return;
		}

		// Perfect = brighter confirm; solid hit = upgrade sting
		_ = PlayLocal( perfect ? Confirm : Upgrade, vol );
		// Soft second layer so a hit always "lands" even if one event fails
		if ( perfect )
			_ = PlayLocal( Notice, vol * 0.55f );
	}

	/// <summary>End of a 3-rep training set.</summary>
	public static void PlayTrainSessionDone( bool anyHits )
	{
		var vol = Math.Clamp( GameSettings.SfxVolume * 1.15f, 0f, 1f );
		_ = PlayLocal( anyHits ? Upgrade : Close, vol );
	}

	public static void StartMenuMusic()
	{
		_lastDeathStage = -1;
		_lastNoChillBed = null;
		_detonationStingLeft = 0f;
		_lastAbsurdReason = "";
		_metalBedTimer = 0f;
		_activeMetalPick = null;
		StartMusic( MenuMusic, force: true );
	}

	public static void StartTankMusic()
	{
		_lastNoChillBed = null;
		_lastDeathStage = -1;
		_detonationStingLeft = 0f;
		_lastAbsurdReason = "";
		_metalBedTimer = 0f;
		_activeMetalPick = null;
		RefreshPlayingMusic( force: true );
	}

	/// <summary>
	/// Next metal bed, never the same as the one just playing when possible.
	/// </summary>
	static string NextMetalTrack()
	{
		if ( MetalPlaylist.Length == 0 )
			return MenuMusic;

		// Prefer a random different track so long monster sessions don't cycle 1-2-3-1…
		if ( MetalPlaylist.Length > 1 )
		{
			var avoid = _activeMetalPick ?? _musicEvent;
			for ( var attempt = 0; attempt < 8; attempt++ )
			{
				var i = _musicRng.Next( MetalPlaylist.Length );
				var pick = MetalPlaylist[i];
				if ( !string.Equals( pick, avoid, StringComparison.OrdinalIgnoreCase ) )
				{
					_metalIndex = i;
					_activeMetalPick = pick;
					_metalBedTimer = 0f;
					return pick;
				}
			}
		}

		_metalIndex = (_metalIndex + 1) % MetalPlaylist.Length;
		_activeMetalPick = MetalPlaylist[_metalIndex];
		_metalBedTimer = 0f;
		return _activeMetalPick;
	}

	static string CurrentMetalTrack()
	{
		if ( !string.IsNullOrEmpty( _activeMetalPick ) )
			return _activeMetalPick;
		_activeMetalPick = MetalPlaylist[_metalIndex % MetalPlaylist.Length];
		return _activeMetalPick;
	}

	/// <summary>Good-boy credits — pure chill bed.</summary>
	public static void StartMusicChillEnding()
	{
		_detonationStingLeft = 0f;
		_lastDeathStage = -1;
		_lastNoChillBed = false;
		StartMusic( ChillTankMusic, force: true, volumeMult: 1f );
	}

	/// <summary>Bad/mixed credits — metal bed (bad = death metal peak).</summary>
	public static void StartMusicMetalEnding( bool peakDeathMetal )
	{
		_detonationStingLeft = 0f;
		_lastDeathStage = -1;
		_lastNoChillBed = true;
		if ( peakDeathMetal )
			StartMusic( DeathStage4, force: true, volumeMult: 1.2f );
		else
			StartMusic( MetalPlaylist[_metalIndex % MetalPlaylist.Length], force: true, volumeMult: 1.1f );
	}

	/// <summary>Called when chaos crosses chill/no-chill threshold or other systems request a recheck.</summary>
	public static void OnMoodMaybeChanged( bool force = false )
	{
		if ( GameFlow.Screen != GameScreen.Playing )
			return;

		RefreshPlayingMusic( force: force );
	}

	/// <summary>Called from DeathFeel when sickness stage changes.</summary>
	public static void OnDeathStageChanged( int stage )
	{
		if ( GameFlow.Screen != GameScreen.Playing )
			return;

		if ( stage == _lastDeathStage )
			return;

		_lastDeathStage = stage;
		RefreshPlayingMusic( force: true );
	}

	/// <summary>
	/// Pick the right bed for playing: dark moments beat chill.
	/// Metal beds rotate on enter / reason change / track end / timed swap.
	/// </summary>
	/// <param name="force">Re-resolve mood even if already playing.</param>
	/// <param name="rotateMetal">Advance to a different metal track while staying no-chill.</param>
	public static void RefreshPlayingMusic( bool force = false, bool rotateMetal = false )
	{
		if ( GameFlow.Screen != GameScreen.Playing )
			return;

		var want = ResolvePlayingBed( out var volumeMult, out var deathStage, out var noChillBed, out var reason );
		if ( want is null )
			return;

		var deathChanged = deathStage != _lastDeathStage;
		var moodChanged = _lastNoChillBed != noChillBed;
		var reasonChanged = reason != _lastAbsurdReason;
		var enteringMetal = noChillBed && deathStage <= 0 && !DetonationStingActive
			&& (moodChanged || _lastNoChillBed != true);
		var metalPool = noChillBed && deathStage <= 0 && !DetonationStingActive
			&& reason is not "dying" and not "detonation";

		// Pick / rotate metal playlist tracks (not the fixed death-ramp beds).
		if ( metalPool )
		{
			if ( rotateMetal || enteringMetal || reasonChanged || string.IsNullOrEmpty( _activeMetalPick ) )
				want = NextMetalTrack();
			else
				want = CurrentMetalTrack();
		}
		else if ( !noChillBed )
		{
			_activeMetalPick = null;
			_metalBedTimer = 0f;
		}

		if ( !force
			&& !deathChanged
			&& !moodChanged
			&& !reasonChanged
			&& !rotateMetal
			&& _music.IsValid()
			&& _music.IsPlaying
			&& _musicEvent == want )
		{
			ApplyMusicVolume( volumeMult );
			return;
		}

		_lastDeathStage = deathStage;
		_lastNoChillBed = noChillBed;
		_lastAbsurdReason = reason ?? "";
		StartMusic( want, force: true, volumeMult: volumeMult );
	}

	static string CurrentBedLabelFor( string evt )
	{
		if ( string.IsNullOrEmpty( evt ) )
			return "—";
		if ( evt.Contains( "moonberry" ) )
			return "CHILL";
		if ( evt.Contains( "gloom" ) )
			return "UNEASE";
		if ( evt.Contains( "moonbyte" ) )
			return "DEATH METAL";
		return "METAL";
	}

	static string ResolvePlayingBed( out float volumeMult, out int deathStage, out bool noChillBed, out string reason )
	{
		deathStage = DeathFeel.PeakStage;
		volumeMult = 1f;
		noChillBed = false;
		reason = "";

		// --- Detonation sting outranks everything for a few seconds ---
		if ( _detonationStingLeft > 0f )
		{
			volumeMult = 1.28f;
			noChillBed = true;
			reason = "detonation";
			return DeathStage4;
		}

		// --- Death ramp (from chill, this is the big gear shift) ---
		if ( deathStage >= 4 )
		{
			volumeMult = 1.18f;
			noChillBed = true;
			reason = "dying";
			return DeathStage4;
		}

		if ( deathStage >= 3 )
		{
			volumeMult = 1.12f;
			noChillBed = true;
			reason = "dying";
			return DeathStage3;
		}

		if ( deathStage >= 2 )
		{
			volumeMult = 1.08f;
			noChillBed = true;
			reason = "dying";
			return DeathStage2;
		}

		if ( deathStage >= 1 )
		{
			// Leave chill: dark unease first, not full blast.
			volumeMult = 1.05f;
			noChillBed = true;
			reason = "dying";
			return DeathStage1;
		}

		// --- Absurd / dark moments → metal no-chill beds (track chosen in RefreshPlayingMusic) ---
		if ( EvaluateAbsurd( out reason ) )
		{
			noChillBed = true;
			volumeMult = reason switch
			{
				"sharks" => 1.16f,
				"monsters" => 1.14f,
				"meth" => 1.12f,
				"meth-rush" => 1.14f,
				"battle" => 1.12f,
				"arena" => 1.08f,
				"adventure" => 1.1f,
				"m80-armed" => 1.1f,
				"jacked-pack" => 1.12f,
				"boom" => 1.18f,
				"inject" => 1.08f,
				_ => 1.06f
			};

			// Placeholder — RefreshPlayingMusic swaps via NextMetalTrack / CurrentMetalTrack.
			return CurrentMetalTrack();
		}

		// --- Calm tank → chill bed ---
		noChillBed = false;
		reason = "calm";
		volumeMult = 1f;
		return ChillTankMusic;
	}

	static void StartMusic( string soundEvent, bool force = false, float volumeMult = 1f )
	{
		if ( string.IsNullOrWhiteSpace( soundEvent ) )
			return;

		if ( !force && _music.IsValid() && _music.IsPlaying && _musicEvent == soundEvent )
		{
			ApplyMusicVolume( volumeMult );
			return;
		}

		// Don't thrash the mixer every frame if a track failed to resolve.
		if ( !force && _musicRetryCooldown > 0f && _musicEvent == soundEvent && !_music.IsValid() )
			return;

		StopMusic();

		var vol = GameSettings.MusicVolume * volumeMult;
		_music = PlayLocal( soundEvent, vol );
		_musicEvent = soundEvent;

		if ( !_music.IsValid() )
		{
			_musicRetryCooldown = 2.5f;
			if ( _lastMissingMusic != soundEvent )
			{
				_lastMissingMusic = soundEvent;
				Log.Warning( $"[NO-CHILLquarium] Music missing: {soundEvent} (recompile Assets/sounds/music if needed)" );
			}
			return;
		}

		_musicRetryCooldown = 0f;
		_lastMissingMusic = null;
	}

	/// <summary>
	/// Play a project .sound event as local 2D audio.
	/// Paths are under Assets/ (e.g. sounds/music/moonberry).
	/// Tries several path forms; failures are quiet (no resource-spam).
	/// </summary>
	static SoundHandle PlayLocal( string path, float volume, float pitch = 1f )
	{
		if ( string.IsNullOrWhiteSpace( path ) )
			return default;

		var clean = path.Trim().Replace( '\\', '/' );
		if ( clean.EndsWith( ".sound", StringComparison.OrdinalIgnoreCase ) )
			clean = clean[..^6];
		if ( clean.EndsWith( ".vsnd", StringComparison.OrdinalIgnoreCase ) )
			clean = clean[..^5];

		// Path forms S&box commonly accepts for SoundEvents.
		var candidates = new[]
		{
			clean + ".sound",
			clean,
			"sounds/" + clean.TrimStart( '/' ).Replace( "sounds/", "" ) + ".sound",
			clean + ".vsnd",
		};

		foreach ( var candidate in candidates )
		{
			try
			{
				if ( ResourceLibrary.Get<SoundEvent>( candidate ) is SoundEvent evt )
				{
					var fromEvt = Sound.Play( evt, Vector3.Zero );
					if ( fromEvt.IsValid() )
					{
						fromEvt.ListenLocal = true;
						fromEvt.SpacialBlend = 0f;
						fromEvt.Volume = volume;
						if ( pitch > 0.01f && MathF.Abs( pitch - 1f ) > 0.01f )
							fromEvt.Pitch = pitch;
						return fromEvt;
					}
				}
			}
			catch
			{
				// next form
			}

			try
			{
				var h = Sound.Play( candidate, Vector3.Zero );
				if ( !h.IsValid() )
					continue;
				h.ListenLocal = true;
				h.SpacialBlend = 0f;
				h.Volume = volume;
				if ( pitch > 0.01f && MathF.Abs( pitch - 1f ) > 0.01f )
					h.Pitch = pitch;
				return h;
			}
			catch
			{
				// next form
			}
		}

		return default;
	}

	public static void StopMusic()
	{
		if ( _music.IsValid() )
			_music.Stop();

		_music = default;
		// Keep _musicEvent so retry cooldown can match the failed track.
	}

	public static void ApplyMusicVolume()
	{
		ResolvePlayingBed( out var mult, out _, out _, out _ );
		ApplyMusicVolume( mult );
	}

	static void ApplyMusicVolume( float mult )
	{
		if ( !_music.IsValid() )
			return;

		_music.Volume = GameSettings.MusicVolume * mult;
	}

	/// <summary>
	/// Keep bed looping; re-resolve every tick so calm ↔ absurd swaps cleanly.
	/// </summary>
	public static void TickMusic()
	{
		if ( _musicRetryCooldown > 0f )
			_musicRetryCooldown = MathF.Max( 0f, _musicRetryCooldown - Time.Delta );

		if ( GameFlow.Screen is GameScreen.MainMenu or GameScreen.Settings )
		{
			_detonationStingLeft = 0f;
			if ( !_music.IsValid() || !_music.IsPlaying || _musicEvent != MenuMusic )
				StartMusic( MenuMusic, force: _musicEvent != MenuMusic );
			return;
		}

		// Ending screens own their own bed (chill vs metal) — just keep it looping.
		if ( GameFlow.Screen == GameScreen.Ending )
		{
			if ( !_music.IsValid() || !_music.IsPlaying )
			{
				if ( EndingSystem.Kind == EndingKind.GoodBoy )
					StartMusicChillEnding();
				else
					StartMusicMetalEnding( EndingSystem.Kind == EndingKind.BadBoy );
			}
			else
			{
				ApplyMusicVolume( EndingSystem.Kind == EndingKind.BadBoy ? 1.15f : 1f );
			}
			return;
		}

		if ( GameFlow.Screen != GameScreen.Playing )
			return;

		// Count down detonation sting, then hand bus back.
		if ( _detonationStingLeft > 0f )
		{
			_detonationStingLeft -= Time.Delta;
			if ( _detonationStingLeft <= 0f )
			{
				_detonationStingLeft = 0f;
				RefreshPlayingMusic( force: true );
			}
		}

		var want = ResolvePlayingBed( out var mult, out var deathStage, out var noChillBed, out var reason );

		// While stuck in no-chill (e.g. monsters), rotate metal beds on a timer.
		if ( noChillBed && deathStage <= 0 && !DetonationStingActive
			&& reason is not "dying" and not "detonation" )
		{
			_metalBedTimer += Time.Delta;
			if ( _metalBedTimer >= MetalRotateSeconds )
			{
				_metalBedTimer = 0f;
				RefreshPlayingMusic( force: true, rotateMetal: true );
				return;
			}
		}
		else
		{
			_metalBedTimer = 0f;
		}

		// Pulse decay — big gameplay moments kick the bed.
		if ( _pulseTimer > 0f )
		{
			_pulseTimer = MathF.Max( 0f, _pulseTimer - Time.Delta );
			if ( _pulseTimer <= 0f )
				_pulseBoost = 0f;
			else
				_pulseBoost *= 0.985f; // soft falloff while timer runs
		}

		// Continuous volume ride as death / sting / sharks / chaos / pulse.
		if ( _music.IsValid() && _music.IsPlaying )
		{
			var progressBoost = 1f;
			if ( deathStage > 0 )
				progressBoost += DeathFeel.PeakProgress * 0.16f;
			if ( DetonationStingActive )
				progressBoost += 0.1f;
			if ( SharkSystem.HasSharks )
				progressBoost += 0.08f + Math.Min( 0.06f, SharkSystem.SharkCount * 0.02f );
			if ( TankSim.HasAnyMonster )
				progressBoost += 0.06f;
			if ( Chaos.Value > 40f )
				progressBoost += Math.Clamp( (Chaos.Value - 40f) / 200f, 0f, 0.1f );
			if ( BattleSystem.Phase == BattlePhase.Fighting )
				progressBoost += 0.07f;
			if ( _pulseBoost > 0.01f )
				progressBoost += _pulseBoost;
			// Thriving calm porch: slight chill bed lift so silence never feels empty.
			if ( !noChillBed && TankSim.FishCount >= 3 )
				progressBoost += 0.03f;
			ApplyMusicVolume( mult * progressBoost );
		}

		// Track ended while still no-chill → next metal bed (not the same one).
		if ( noChillBed && deathStage <= 0 && !DetonationStingActive
			&& reason is not "dying" and not "detonation"
			&& (!_music.IsValid() || !_music.IsPlaying) )
		{
			RefreshPlayingMusic( force: true, rotateMetal: true );
			return;
		}

		// Bed mismatch (calm ↔ absurd) or wrong track — switch immediately.
		if ( _music.IsValid() && _music.IsPlaying && _musicEvent == want
			&& _lastNoChillBed == noChillBed
			&& _lastAbsurdReason == (reason ?? "") )
			return;

		RefreshPlayingMusic( force: true );
	}
}