Game/2D/UI/Gamehud.razor

Razor UI component for the in-game HUD and game-over overlay. Renders HUD elements (wave/time, score, lives, consumables, sidebar, popups), animates score/fragments, handles input (detonate, consumables), subscribes to game events, and runs the end-of-run reveal/sequence with sound and popup spawning.

NetworkingFile Access
@using Sandbox;
@using Sandbox.UI;
@using System.Linq;
@using System;
@inherits PanelComponent

<root>
	

		@* everything that was previously direct children of root goes here *@
		@if( Game?.GameStarted == true )
		{
			@* Background grid *@
			@if(Game?.IsRunning == true || Game?.IsGameOver == true)
			{
				<div class="game-bg-grid">
						<div class="ov-particles">
					@for(int i = 0; i < 20; i++)
					{
						float left     = (i * 5.2f) % 100f;
						float duration = 4f + (i * 0.3f) % 3f;
						float delay    = (i * 0.4f) % 5f;
						int   size     = (i % 3 == 0) ? 3 : (i % 5 == 0) ? 3 : 2;
						string color   = (i % 7 == 0) ? "rgba(251,191,36,0.8)"
									: (i % 5 == 0) ? "rgba(244,114,182,0.8)"
									: (i % 3 == 0) ? "rgba(99,102,241,0.9)"
									: "rgba(167,139,250,0.8)";
						<div class="ov-particle"
							style="left:@left.ToString("F1")%;width:@(size)px;height:@(size)px;background-color:@color;border-radius:50%;animation-duration:@duration.ToString("F1")s;animation-delay:@delay.ToString("F1")s;"></div>
					}
				</div>
					@for(int i = 0; i < 60; i++)
					{
						int col = i % 10;
						int row = i / 10;
						<div class="game-bg-cell"
							style="left:@(col * 234)px;
									top:@(row * 234)px;
									background-color:@(Game?.WaveGridColor ?? "#0d0820");">
						</div>
					}
				</div>
			}

			@if(DEV_MODE)
			{
				<div class="mm-dev-music-panel">
					<div class="mm-dev-music-label">🎵 MUSIC</div>
					@foreach(var track in _debugTracks)
					{
						<button class="mm-dev-btn @(_musicHandles.ContainsKey(track.Key) ? "mm-dev-btn-active" : "")"
								onclick=@(() => PlayDebugTrack(track.Key))>
							@(_musicHandles.ContainsKey(track.Key) ? "▶ " : "")@track.Value
						</button>
					}
					<button class="mm-dev-btn" onclick=@StopDebugMusic>⏹ Stop</button>
				</div>
			}

			@* ========================================================================== *@
			@* ================================= TOP BAR =================================*@
			@* ========================================================================== *@

			<div class="topbar">
				<div class="hud-block wave">
					@if (Game?.Mode == GameMode.Survival)
					{
						<div class="hud-lbl">TIME</div>
						<div class="hud-val">@FormatSurvivalTime(Game?.SurvivalElapsed ?? 0f)</div>
					}
					else
					{
						<div class="hud-lbl">WAVE</div>
						<div class="hud-val">@(Game?.Wave ?? 1)</div>
					}
				</div>
				<div class="hud-sep"></div>
				<div class="hud-block score">
					<div class="hud-lbl">SCORE</div>
					<div class="hud-val">@_displayScore.ToString("N0")</div>
				</div>
				<div class="hud-sep"></div>
				<div class="hud-block best">
					<div class="hud-lbl">BEST</div>
					<div class="hud-val">@((Game?.BestScore??0).ToString("N0"))</div>
				</div>
			</div>
	
	@* ------------------- Screen Shake overlay ------------------- *@
	<div class="hud-overlay @_currentShake">
		@if ( Game != null && Game.IsRunning )
		{
			@if (Game.Mode == GameMode.Wave)
			{
				float frac = Game.TurnTimeFraction.Clamp(0f,1f);
				int   pct  = (int)(frac * 100f);
				bool hurry = frac < 0.3f;
				bool panic = frac < 0.15f;
				bool mid   = frac < 0.55f && frac >= 0.3f;

				@* ----- Timer track ----- *@
				<div class="timer-track">
					<div class="timer-fill @(panic?"panic":hurry?"hurry":mid?"mid":"")" style="width:@pct%;"></div>
				</div>

				@* ----- Challenge bar ----- *@
				@if ( Game?.CurrentChallenge != null && Game.IsRunning )
				{
					var ch = Game.CurrentChallenge;
					string state = ch.Completed ? "done" : "active";
					<div class="challenge-bar @state">
						<span class="ch-icon">@(ch.Completed ? "✓" : "⚡")</span>
						<span class="ch-label">@ch.Label</span>
						<span class="ch-bonus">+@ch.BonusCoins</span>
					</div>
				}
			}
			else if (Game.Mode == GameMode.Survival)
			{
				<div class="survival-timer">
					<span class="survival-timer-icon">⏱️</span>
					<span class="survival-timer-val">@FormatSurvivalTime(Game.SurvivalElapsed)</span>
				</div>
				<div class="survival-spawn-track">
					<div class="survival-spawn-fill" style="width:@((Game.SurvivalSpawnTimerFraction * 100f).ToString("F0"))%;"></div>
				</div>
			}
		}
		

		@* ----- Chain track ----- *@
		@if(_chainSteps.Count > 0)
		{
			<div class="chain-track">
				<div class="chain-track-inner">
					@for(int i = 0; i < _chainSteps.Count; i++){
						int idx = i;
						<div class="step @(idx <= _litStep ? "lit" : "")">@_chainSteps[i]</div>
						@if(idx < _chainSteps.Count-1){ <span class="arr">→</span> }
					}
				</div>
			</div>
		}

		
		@* timer track *@
		@* challenge bar *@
		@* chain track *@

		@* --------------- Consumables strip -------------- *@
		@* When sidebar is closed, also remove right: 260px from .consumable-strip and .msg *@
		@* Always show during tutorial, or when player plays *@
		@if (Game?.IsRunning == true)
		{
			/*style="right:@(_sidebarOpen ? "260px" : "0px"*/
			<div class="consumable-strip">
				@foreach (var cons in ConsumableCatalog.All)
				{
					int count = ChainReactionGame.Save?.GetConsumableCount(cons.Id) ?? 0;
					bool canUse = count > 0 && !(Hand?.IsDetonating ?? false);

					<div class="consumable-slot @(canUse ? "available" : "empty")"
						onclick=@(() => { if(canUse) OnUseConsumable(cons.Id); })>
						<div class="consumable-icon">@cons.Icon</div>
						@if (count > 0)
						{
							<div class="consumable-count">@count</div>
						}
					</div>
				}
			</div>
		}

		@* ====================== Sidebar ====================== *@
		@* Sidebar toggle button *@
		<button class="sidebar-toggle" onclick=@(() => { _sidebarOpen = !_sidebarOpen; StateHasChanged(); })>
			@(_sidebarOpen ? "✕" : "?")
		</button>


		@* Sidebar — only when open *@
		@if(_sidebarOpen)
		{
			<div class="sidebar">
				<div class="sbl">Bomb Range Preview</div>
				<div class="legend">
					<div class="leg"><span class="dot p0"></span> Bomb 1</div>
					<div class="leg"><span class="dot p1"></span> Bomb 2</div>
					<div class="leg"><span class="dot p2"></span> Bomb 3</div>
					<div class="leg"><span class="dot red"></span> Destroyed</div>
					<div class="leg"><span class="dot chain"></span> Chain</div>
				</div>
				<div class="divider"></div>
				<div class="sbl">Controls</div>
				<div class="controls-hint">
					<div>1 / 2 / 3 → Select bomb</div>
					<div>LMB → Place / Remove</div>
					<div>RMB → Rotate sniper</div>
					<div>SPACE → Detonate</div>
				</div>
			</div>
		}

		@* ---------------------------------------------------------------*@		
		@* Show our Pet! (left side bottom) + LIVES (❤️❤️❤️) next to it *@
		@* ---------------------------------------------------------------*@
			<div class="hud-pet-wrap">

				@* Pet card image *@
				<div class="hud-pet-card"></div>

				@* TOP — modifiers + curse *@
				<div class="hud-pet-top">
					@if(ChainReactionGame.Save?.ActiveModifiers?.Count > 0)
					{
						<div class="sbl">Active Modifiers</div>
						<div class="mod-strip">
							@foreach(var modIdStr in ChainReactionGame.Save.ActiveModifiers)
							{
								var mod = ShopCatalog.All.FirstOrDefault(m => m.Id.ToString() == modIdStr);
								if(mod != null)
								{
									<div class="mod-strip-item" title="@mod.Name - @mod.Description">
										<span class="mod-strip-icon">@mod.Icon</span>
										<span class="mod-strip-name">@mod.Name</span>
									</div>
								}
							}
						</div>
					}

					@* Active cursed chest *@
					@if(Game?.Mode == GameMode.Wave && (Game?.ActiveCursedChests ?? 0) > 0)
					{
						<div class="curse-warning">
							💀 -@(Game.ActiveCursedChests * 2)s next wave
						</div>
					}
				</div>

				@* --- Min chest requirement --- *@
				@if(Game?.IsRunning == true && Game?.Mode == GameMode.Wave)
				{
					<div class="hud-min-chests">
						<span class="hud-min-chests-lbl">⚠️ Min chests</span>
						<span class="hud-min-chests-val">@(Game?.MinChests ?? 0)</span>
					</div>
				}

				@* BOTTOM — pet + hearts *@
				<div class="hud-pet-bottom">
					@{
						int level = ChainReactionGame.Save?.PetLevel ?? 1;
						bool unlocked = ChainReactionGame.Save?.PetUnlockShown ?? false;
						string bobClass = (!unlocked || level == 1) ? "pet-bob" : "";
					}
					<div class="cat-anchor">
						@if(_petReacting && !string.IsNullOrEmpty(_petBubbleText))
						{
							<div class="cat-speech-bubble">@_petBubbleText</div>
						}
						@if(GetPetImagePath() != null)
						{
							<image src=@GetPetImagePath() class="@bobClass"/>
						}
						else
						{
							<div class="@bobClass">@GetPetEmoji()</div>
						}
					</div>
					<div class="hud-pet-lives">
						@for(int i = 0; i < 3; i++)
						{
							bool full = i < (Game?.Lives ?? 1);
							<span class="hud-pet-heart @(full ? "full" : "empty")">
								@(full ? "❤️" : "🖤")
							</span>
						}
					</div>
				</div>
			</div>
		}
		
		@* ---------------------------------- BOTTOM BAR ---------------------------------- *@
		<div class="msg">@_msg</div>
	</div>
	
		@* ---------------------------------- GAME OVER ---------------------------------- *@
			@if(Game?.IsGameOver == true)
			{
				<div class="ov-bg">

					<div class="ov-bg-squares">
    @for(int i = 0; i < 60; i++)
    {
        float sz   = 25f + (i * 17f) % 70f;
        float left = (i * 137f + (i / 5) * 311f) % 1860f;
        float top  = (i * 89f  + (i / 4) * 197f) % 1020f;
        float dur  = 6f + (i * 0.7f) % 10f;
        float del  = (i * 0.5f) % 8f;
        int   rad  = 3 + i % 14;
        string col = (i % 6 == 0) ? "rgba(167,139,250,0.06)"
                   : (i % 5 == 0) ? "rgba(251,191,36,0.04)"
                   : (i % 4 == 0) ? "rgba(244,114,182,0.05)"
                   : (i % 3 == 0) ? "rgba(52,211,153,0.04)"
                   : (i % 2 == 0) ? "rgba(99,102,241,0.05)"
                   :                "rgba(96,165,250,0.04)";
        string brd = (i % 6 == 0) ? "rgba(167,139,250,0.18)"
                   : (i % 5 == 0) ? "rgba(251,191,36,0.14)"
                   : (i % 4 == 0) ? "rgba(244,114,182,0.16)"
                   : (i % 3 == 0) ? "rgba(52,211,153,0.13)"
                   : (i % 2 == 0) ? "rgba(99,102,241,0.16)"
                   :                "rgba(96,165,250,0.13)";
        <div class="ov-bg-sq @((i % 2 == 0) ? "sq-a" : "sq-b")"
             style="width:@sz.ToString("F0")px;
                    height:@sz.ToString("F0")px;
                    left:@left.ToString("F0")px;
                    top:@top.ToString("F0")px;
                    background-color:@col;
                    border-color:@brd;
                    border-radius:@(rad)px;
                    animation-duration:@dur.ToString("F1")s;
                    animation-delay:@del.ToString("F1")s;">
        </div>
    }
</div>

					<div class="ov-particles">
						@for(int i = 0; i < 20; i++)
						{
							float left     = (i * 5.2f) % 100f;
							float duration = 4f + (i * 0.3f) % 3f;
							float delay    = (i * 0.4f) % 5f;
							int   size     = (i % 3 == 0) ? 6 : (i % 5 == 0) ? 3 : 5;
							string color   = (i % 7 == 0) ? "rgba(251,191,36,0.8)"
										: (i % 5 == 0) ? "rgba(244,114,182,0.8)"
										: (i % 3 == 0) ? "rgba(99,102,241,0.9)"
										:                "rgba(167,139,250,0.8)";
							<div class="ov-particle"
								style="left:@left.ToString("F1")%;width:@(size)px;height:@(size)px;background-color:@color;border-radius:50%;animation-duration:@duration.ToString("F1")s;animation-delay:@delay.ToString("F1")s;">
							</div>
						}
					</div>

					@******************************************************************************@
					@* ------------------------------- LEFT PANEL ------------------------------- *@
					@******************************************************************************@
					<div class="ov-side-panel ov-side-left">

						@* Personal best progression bar *@
						<div class="ov-side-card ov-section-enter">
							<div class="ov-side-card-title">📈 YOUR BEST</div>
							@{
								int prevBest = Game.PrevBestScore;
								int curScore = Game.Score;
								bool beatBest = curScore > prevBest;
								float prevPct = prevBest <= 0 ? 0f : 100f;
								float curPct  = prevBest <= 0 ? 100f 
											: MathF.Min(100f, (float)curScore / (float)MathF.Max(prevBest, curScore) * 100f);
							}
							<div class="ov-pb-bar-wrap">
								<div class="ov-pb-bar-track">
									<div class="ov-pb-bar-fill" style="width:@curPct.ToString("F0")%;"></div>
									@if(prevBest > 0 && !beatBest)
									{
										<div class="ov-pb-marker" style="left:@prevPct.ToString("F0")%;"></div>
									}
								</div>
							</div>
							<div class="ov-pb-scores">
								<div class="ov-pb-score-current">@curScore.ToString("N0")</div>
								@if(prevBest > 0)
								{
									<div class="ov-pb-score-best">Best: @prevBest.ToString("N0")</div>
								}
							</div>
							@if(beatBest)
							{
								<div class="ov-pb-newbest">🏆 New Record!</div>
							}
						</div>

						@* Wave context *@
						@if (Game.Mode == GameMode.Wave)
						{
							<div class="ov-side-card journey ov-section-enter">
								<div class="ov-side-card-title">🌊 YOUR JOURNEY</div>
								<div class="ov-journey-wave">Wave @(Game?.Wave)</div>
								@if((ChainReactionGame.Save?.BestWave ?? 0) > 0)
								{
									<div class="ov-journey-sub">
										<span>Personal best Wave : </span> 
										<span>@(ChainReactionGame.Save?.BestWave)</span>
									</div>
								}
								<div class="ov-journey-sub">
									Run n° :@(ChainReactionGame.Save?.TotalRuns ?? 1)
								</div>
								@if((ChainReactionGame.Save?.TotalRuns ?? 0) > 5)
								{
									float avgWave = (ChainReactionGame.Save?.BestWave ?? 1) * 0.6f;
									bool aboveAvg = (Game?.Wave ?? 1) > avgWave;
									<div class="ov-journey-tag @(aboveAvg ? "good" : "")">
										@(aboveAvg ? "Above your average!" : "You got it my friend!")
									</div>
								}
							</div>
						}
					</div>
					
					@******************************************************************************@
					@* ----------------------------- MIDDLE PANEL ------------------------------- *@
					@******************************************************************************@					

					<div class="ov-box">

						@* Ambient background orbs *@
						<div class="ov-orb ov-orb-1"></div>
						<div class="ov-orb ov-orb-2"></div>
						<div class="ov-orb ov-orb-3"></div>

						@* ── 2. Wave + reason ── *@

						@* WAVE MODE *@
						@if (Game.Mode == GameMode.Wave)
						{
							<div class="ov-wave-big ov-reveal">Wave @(Game?.Wave)</div>
						}
						// SURVIVAL MODE // 
						else
						{
							<div class="ov-wave-big ov-reveal">@FormatSurvivalTime(Game?.SurvivalElapsed ?? 0f)</div>
						}
						@if(!string.IsNullOrEmpty(Game?.GameOverReason))
						{
							<div class="ov-reason ov-reveal" style="animation-delay:0.2s;">
								@Game.GameOverReason
							</div>
						}

						@* ── 3. Stats row ── *@
						@if(_ovShowStats)
						{
							<div class="ov-stats ov-section-enter">
								<div class="ov-stat">
									<span class="ov-stat-icon">🎁</span>
									<span class="ov-stat-val">@Game.TotalChestsCleared</span>
									<span class="ov-stat-lbl">chests cleared</span>
								</div>
								<div class="ov-stat">
									<span class="ov-stat-icon">🔗</span>
									<span class="ov-stat-val">@Game.BestChainLength</span>
									<span class="ov-stat-lbl">best chain</span>
								</div>
								<div class="ov-stat">
									<span class="ov-stat-icon">⚡</span>
									<span class="ov-stat-val">@Game.ChallengesCompleted</span>
									<span class="ov-stat-lbl">challenges</span>
								</div>
							</div>
						}

						@* ── 4. Secret chests — interactive ── *@
						@if(_ovShowSecrets && (Game?.SecretChestRewards?.Count ?? 0) > 0)
						{
							<div class="ov-secret-section ov-section-enter">
								<div class="ov-secret-eyebrow">✨ SECRET CHESTS</div>
								<div class="ov-secret-hint">
									@if(!_secretsDone)
									{
										@($"Click to open · {(Game.SecretChestRewards.Count - _revealedChests)} remaining")
									}
									else
									{
										@("All opened!")
									}
								</div>
								<div class="ov-chest-cards">
									@for(int i = 0; i < Game.SecretChestRewards.Count; i++)
									{
										var reward = Game.SecretChestRewards[i];
										bool revealed = i < _revealedChests;
										bool isNext   = i == _revealedChests && !_secretsDone;
										bool isNew    = i == _lastRevealedIdx;
										int capturedIdx = i;

										@if(!revealed)
										{
											<div class="ov-chest-card @(isNext ? "ready" : "waiting")"
												onclick=@(() => OnChestClick(capturedIdx))>
												<div class="ov-chest-card-inner">
													<div class="ov-chest-glow"></div>
													<div class="ov-chest-emoji">🎁</div>
													@if(isNext)
													{
														<div class="ov-chest-tap">TAP!</div>
													}
													else
													{
														<div class="ov-chest-tap locked">?</div>
													}
												</div>
											</div>
										}
									else
									{
										<div class="ov-chest-card revealed @(reward.IsRare ? "rare" : "") @(i == _lastRevealedIdx ? "pop-in" : "")">
											
												@* Confetti burst — locked to confetti idx, survives re-renders *@
												@if(_showConfetti && i == _confettiIdx)
												{
													@for(int p = 0; p < 14; p++)
													{
														float px  = -20f + (p * 11f);
														float dur = 0.8f + (p * 0.07f) % 0.5f;
														float del = p * 0.03f;
														string pc = (p % 5 == 0) ? "rgba(251,191,36,0.95)"
																: (p % 4 == 0) ? "rgba(167,139,250,0.95)"
																: (p % 3 == 0) ? "rgba(244,114,182,0.95)"
																: (p % 2 == 0) ? "rgba(52,211,153,0.95)"
																:                 "rgba(255,255,255,0.90)";
														<div class="ov-confetti-bit"
															style="left:@px.ToString("F0")px;
																	background-color:@pc;
																	animation-duration:@dur.ToString("F1")s;
																	animation-delay:@del.ToString("F2")s;">
														</div>
													}
												}

											@* Chest Secret Card (revealed) *@
											<div class="ov-chest-card-inner">
												<div class="ov-chest-revealed-icon">@reward.Icon</div>
												<div class="ov-chest-revealed-info">
													<div class="ov-chest-revealed-lbl">@reward.Label</div>
													@if(reward.IsRare)
													{
														<div class="ov-chest-rare-badge">✨ RARE · +@reward.Fragments 💠 bonus</div>
														@*<div class="ov-chest-rare-badge">✨ @reward.RareDescription</div>*@														}
												</div>
											</div>
										</div>
									}
								}
							</div>
						</div>
						}

							@* ── 6. Fragment breakdown ── *@
							@if(_ovShowFrags)
							{
								<div class="ov-frag-section ov-section-enter">
									<div class="ov-frag-label">💠 FRAGMENTS EARNED</div>
									<div class="ov-frag-val">+@_displayFragments</div>
									<div class="ov-frag-breakdown">
										
										@if (Game.Mode == GameMode.Wave)
										{
											<div class="ov-frag-line">
												<span class="ov-frag-line-lbl">Wave bonus</span>
												<span class="ov-frag-line-val">+@(Game.Wave / 2)</span>
											</div>
										}
										@if((ChainReactionGame.Save?.PetLevel ?? 1) >= 1 && Game.Wave >= 20)
										{
											<div class="ov-frag-line">
												<span class="ov-frag-line-lbl">🐾 Pet bonus</span>
												<span class="ov-frag-line-val">+@((Game.Wave / 20) * 5)</span>
											</div>
										}

										@* SECRET CHESTS REWARDS FRAGMENTS  *@
										@if(Game.SecretChestRewards.Any(r => r.Fragments > 0))
										{
											int secretFrags = Game.SecretChestRewards.Sum(r => r.Fragments);
											<div class="ov-frag-line">
												<span class="ov-frag-line-lbl">✨ Secret chests</span>
												<span class="ov-frag-line-val">+@secretFrags</span>
											</div>
										}

										@* BONUSN EVENTS FRAGMENTS  *@
										@if(Game.BonusEventFragments > 0)
										{
											<div class="ov-frag-line">
												<span class="ov-frag-line-lbl">⚡ Random events</span>
												<span class="ov-frag-line-val">+@Game.BonusEventFragments</span>
											</div>
										}

										@* PERFECT WAVE FRAGMENTS  *@
										@if(Game.PerfectWaveFragments > 0)
										{
											<div class="ov-frag-line">
												<span class="ov-frag-line-lbl">✨ Perfect waves</span>
												<span class="ov-frag-line-val">+@Game.PerfectWaveFragments</span>
											</div>
										}

										@* BOSS FRAGMENTS *@
										@if(Game.BossFragments > 0)
										{
											<div class="ov-frag-line">
												<span class="ov-frag-line-lbl">👑 Boss kills</span>
												<span class="ov-frag-line-val">+@Game.BossFragments</span>
											</div>
										}
									</div>
									<div class="ov-frag-total">Total: @(ChainReactionGame.Save?.TotalFragments ?? 0) 💠</div>
									<div class="ov-frag-hint">@GetFragHint()</div>
								</div>
							}

							@* ── 7. Final score ── *@
							@if(_ovShowScore)
							{
								<div class="ov-score-section ov-section-enter">
									@if(isNewBest)
									{
										<div class="ov-newbest-badge">🏆 NEW BEST</div>
									}
									<div class="ov-score-label">FINAL SCORE</div>
									<div class="ov-score @(isNewBest?"gold":"")">@_displayScore.ToString("N0")</div>
								</div>
							}

							@* ── 8. Buttons ── *@
							@if(_ovShowButtons)
							{
								<div class="ov-btns ov-section-enter">
									<button class="ov-btn-wrap" onclick=@OnRestart>
										▶ &nbsp; Play Again
									</button>
									<button class="ov-btn-menu" onclick=@OnReturnToMenu>
										⌂ Menu
									</button>
								</div>
							}

						</div>

					@******************************************************************************@
					@* ------------------------------ RIGHT PANEL ------------------------------- *@
					@******************************************************************************@
					<div class="ov-side-panel ov-side-right">

						@* Streak display *@
						@if(_perfectWavesThisRun >= 2)
						{
							<div class="ov-side-card streak ov-section-enter">
								<div class="ov-side-card-title">🔥 STREAK</div>
								<div class="ov-streak-count">×@_perfectWavesThisRun</div>
								<div class="ov-streak-label">waves cleared</div>
							</div>
						}

						@* Next unlock teaser *@
						@{
							var nextUnlock = ShopCatalog.All
								.Where(m => !(ChainReactionGame.Save?.OwnsModifier(m.Id) ?? false))
								.OrderBy(m => m.Cost)
								.FirstOrDefault();
							int totalFrags = ChainReactionGame.Save?.TotalFragments ?? 0;
						}
						@if(nextUnlock != null)
						{
							int needed = nextUnlock.Cost - totalFrags;
							bool canAfford = needed <= 0;
							<div class="ov-side-card ov-section-enter">
								<div class="ov-side-card-title">🔓 NEXT UNLOCK</div>
								<div class="ov-unlock-icon">@nextUnlock.Icon</div>
								<div class="ov-unlock-name">@nextUnlock.Name</div>
								@if(canAfford)
								{
									<div class="ov-unlock-ready">Ready to unlock!</div>
								}
								else
								{
									<div class="ov-unlock-cost">@needed 💠Fragments needed : </div>
									<div class="ov-unlock-bar-track">
										<div class="ov-unlock-bar-fill" 
											style="width:@(MathF.Min(100f, (float)totalFrags / nextUnlock.Cost * 100f).ToString("F0"))%;">
										</div>
									</div>
								}
							</div>
						}

					</div>
					}
				}
			</div>
			}
		}
		@if (!string.IsNullOrEmpty(_flashClass))
		{
			<div class="screen-flash @_flashClass">
				@if (!string.IsNullOrEmpty(_flashText))
				{
					<div class="flash-text">@_flashText</div>
				}
			</div>
		}
</root>

@code {
	ChainReactionGame Game => ChainReactionGame.Instance;
	BombHandSystem    Hand => Game?.Components.Get<BombHandSystem>();
	bool DEV_MODE = false;

	bool   _sub;
	string _msg          = "Place your bombs then Detonate!";
	string _multClass    = "";
	string _challengeMsg = "";
	float _popupX = 30f;   // tracks current X so popups cascade diagonally
	int    _litStep      = -1;
	int    _displayScore = 0;      // animated score shown on game over
	Queue<(int r, int c, int coins)> _pendingChestPopups = new(); // coords of destroyed chest this wave (for spawning pop ups coins)
	List<string> _chainSteps = new();

	// ScreenFlash
	string _flashClass  = "";
	string _flashText   = "";
	float  _flashEndsAt = 0f;

	// ----------------------- Game Over Screen -------------------------- //
	// ------------------------------------------------------------------- //
	// ------------------------------------------------------------------- //

	// Use saved best score for new best detection
	bool isNewBest => (Game?.Score ?? 0) > 0 && (Game?.Score ?? 0) >= (ChainReactionGame.Save?.BestScore ?? 0);
	bool _ovShowStats    = false;
	bool _ovShowSecrets  = false;
	bool _ovShowFrags    = false;
	bool _ovShowScore    = false;
	bool _ovShowButtons  = false;
	bool _secretsDone    = false;
	bool _revealingChests = false;
	int  _revealedChests  = 0;
	bool _secretSectionVisible = false;
	bool _chestRevealDone = false; // kept for compatibility
	int _displayFragments = 0;
	bool _fragsAnimating  = false;

	//
	int _perfectWavesThisRun = 0;
	int _currentWaveChestsCleared = 0;

async System.Threading.Tasks.Task StartGameOverSequence()
{
	if (_revealingChests) { Log.Warning("StartGameOverSequence called twice!"); return; }

    if (_revealingChests) return;
    _revealingChests = true;

	// Reset all 
    _ovShowStats   = false;
    _ovShowSecrets = false;
    _ovShowFrags   = false;
    _ovShowScore   = false;
    _ovShowButtons = false;
    _secretsDone   = false;
    _revealedChests = 0;
	_lastRevealedIdx = -1;
    _secretSectionVisible = false;
    _chestRevealDone = false;

	_showConfetti = false;
	_confettiIdx  = -1;
	_displayScore = 0;
	_scoreAnimating = false;

    // 1. Wave + reason already visible — wait then show stats
    await GameTask.DelaySeconds(1.0f);
	Log.Info($"Stats shown at {Time.Now}");
    _ovShowStats = true;
	Sound.Play("Whoosh_Stat");
    StateHasChanged();

    // 2. Secrets appear AFTER stats, not before
    await GameTask.DelaySeconds(0.8f);
	Log.Info($"Secrets shown at {Time.Now}");
    if (Game?.SecretChestRewards?.Count > 0)
    {
        _ovShowSecrets = true;
        _secretSectionVisible = true;
		Sound.Play("Whoosh_Stat");
        StateHasChanged();
        return; // player clicks to continue
    }

    await ShowFragsAndScore();
}

	bool _showConfetti = false;
	int  _confettiIdx  = -1;
	int _lastRevealedIdx = -1;

	// PET ANIMATION 
	bool _petFrameToggle  = false;
	float _petFrameTimer  = 0f;
	bool _petReacting  = false;
	float _petReactionTimer = 0f;
	string _petReactionSprite  = "";
	string _petBubbleText  = "";
	int _reactionCount = 0;

	string GetReactionLine(int total)
	{
		var _rng = new Random();
		string[] small = { "Ooh!", "Nice!", "Yay!", "Wheee~" };
		string[] mid   = { "Nice chain!", "Keep going!", "You're on fire!", "So good!" };
		string[] big   = { "WOW!!", "INCREDIBLE!", "AMAZING!!", "UNSTOPPABLE!" };

		string[] pool = total >= 8 ? big : total >= 4 ? mid : small;
		return pool[_rng.Next(pool.Length)];
	}

	string[] _catReactionSprites = {
		"ui/icons/cat/Cat_Shocked_Pose1.png",
		"ui/icons/cat/Cat_Shocked_Pose2.png",
	};


	void OnChestClick(int idx)
	{
		if (idx != _revealedChests) return;
		_revealedChests++;
		_lastRevealedIdx = idx;
		_confettiIdx = idx;
		_showConfetti = true;

		bool isLast = _revealedChests >= (Game?.SecretChestRewards?.Count ?? 0);
		Sound.Play(isLast ? "open_last_chest" : "open_chest_1");

		_ = ClearConfettiAfterDelay();

		if (isLast)
		{
			_secretsDone = true;
			_ = ShowFragsAndScore();
		}
		StateHasChanged();
	}

	async System.Threading.Tasks.Task ClearConfettiAfterDelay()
	{
		await GameTask.DelaySeconds(1.0f);
		_showConfetti = false;
		StateHasChanged();
	}

	async System.Threading.Tasks.Task ShowFragsAndScore()
	{
		await GameTask.DelaySeconds(0.3f);
		_ovShowFrags = true;
		_chestRevealDone = true;
		_displayFragments = 0;
		_fragsAnimating = true;
		
		Sound.Play(eventName: "Whoosh_Stat");
		StateHasChanged();

		await GameTask.DelaySeconds(0.6f);

		if (isNewBest)
		{
			Flash("flash-new-best", 1.0f, "🏆 NEW BEST!");
			Sound.Play("gameover_bestscore");
			await GameTask.DelaySeconds(0.4f);
		}

		_ovShowScore = true;
		Sound.Play(eventName: "Whoosh_Stat");
		_displayScore = 0;        // reset to 0
		_scoreAnimating = true;   // START the animation
		StateHasChanged();

		await GameTask.DelaySeconds(1.4f);
		_ovShowButtons = true;
		Sound.Play(eventName: "Whoosh_Stat");
		StateHasChanged();
	}


	bool _scoreAnimating = false;

	async System.Threading.Tasks.Task StartScoreCount()
	{
		// Wait for score section to fade in (0.5s delay + 0.4s animation)
		   await GameTask.DelaySeconds( 0.9f );
    	_scoreAnimating = true;
	}
	void OnGameStateChanged()
	{
		if ( Game?.IsGameOver == true )
		{
			_scoreAnimating = false;
			if ( !_revealingChests ) _ = StartGameOverSequence();
		}
		else if(Game?.IsRunning == true && Game?.Score == 0)
		{
			// Fresh game just started from menu — snap display to 0
			_displayScore   = 0;
			_scoreAnimating = false;
		}

		StateHasChanged();
	}

	string GetFragHint()
	{
		int total  = ChainReactionGame.Save?.TotalFragments ?? 0;
		int toNext = 50 - (total % 50);
		return toNext == 50 ? "Milestone reached!" : $"{toNext} until next unlock";
	}


	// -------------------------------------------------------------------- /
	// Sidebar
	bool _sidebarOpen = false;

	bool CanDet => Game?.IsRunning == true && Hand?.AnyPlaced == true && !(Hand?.IsDetonating ?? true)
					&& (TutorialOverlay.Instance?.IsActive == false || TutorialOverlay.Instance?.Step == 6 || TutorialOverlay.Instance?.Step == 9);
	// PopupLayer razor component ref
	PopupLayer _popupLayer;
	protected override void OnTreeFirstBuilt() 
	{
		_popupLayer = Scene.Components.GetInChildren<PopupLayer>( true );
    	if ( _popupLayer == null ) Log.Warning( "GameHud: PopupLayer not found in scene" );

		Sub();
	}

	protected override void OnUpdate()
	{
		if (!_sub) Sub();

		// Space bar detonation
		if (Input.Pressed("Jump") && CanDet) OnDet();

		// ------------- Pet -------------
		_petFrameTimer -= Time.Delta;
		if (_petFrameTimer <= 0f)
		{
			_petFrameTimer = 0.4f;
			_petFrameToggle = !_petFrameToggle;
			StateHasChanged();
		}
		
		// ------------- PET -------------
		if (_petReacting)
		{
			_petReactionTimer -= Time.Delta;
			if (_petReactionTimer <= 0f)
			{
				_petReacting = false;
				StateHasChanged();
			}
		}

		// Animate _displayScore toward the real score every frame (both live and game over)
		if ( Game != null && _scoreAnimating )
		{
			int target = Game.IsGameOver ? Game.Score : Game.Score;
			if ( _displayScore != target )
			{
				int diff = target - _displayScore;
				int step = (int)MathF.Max( 1f, MathF.Abs(diff) * Time.Delta * 6f );
				_displayScore += diff > 0 ? step : -step;
				if ( (diff > 0 && _displayScore > target) || (diff < 0 && _displayScore < target) )
					_displayScore = target;

				// PLay score tick (in GameOver)
				if (MathF.Abs(diff) > 50 && Time.Now - _lastScoreTick > 0.16f)
				{
					Sound.Play("coin_earned_single");
					_lastScoreTick = Time.Now;
				}

				StateHasChanged();
			}
		}
		else if ( !Game.IsGameOver )
		{
			int target = Game.Score;
			if ( _displayScore != target )
			{
				int diff = target - _displayScore;
				int step = (int)MathF.Max(1f, MathF.Abs(diff) * Time.Delta * 5f); // 60fps animation

				// Never step past the target
				if(step > MathF.Abs(diff)) 
				{
					step = (int)MathF.Abs(diff);
				}
				
				// Animate 
				_displayScore += diff > 0 ? step : -step;
				StateHasChanged();
			}
		}

		if (_fragsAnimating && Game != null)
		{
			int target = Game.FragmentsEarned;
			if (_displayFragments != target)
			{
				int diff = target - _displayFragments;
				int step = (int)MathF.Max(1f, MathF.Abs(diff) * Time.Delta * 4f);
				if (step > MathF.Abs(diff)) step = (int)MathF.Abs(diff);
				_displayFragments += diff > 0 ? step : -step;
				StateHasChanged();
			}
			else
			{
				_fragsAnimating = false;
			}
		}

		// Flash clear
		if (!string.IsNullOrEmpty(_flashClass) && Time.Now >= _flashEndsAt)
		{
			_flashClass = "";
			_flashText  = "";
			StateHasChanged();
		}

		// Screen Shake
		var shakeClass = ScreenShaker.Instance?.ShakeClass ?? "";
		if (shakeClass != _currentShake)
		{
			_currentShake = shakeClass;
			StateHasChanged();
		}
	}
	string _currentShake = "";
	float _lastScoreTick = 0f;  

	void Sub()
	{
		var h = Hand; var g = Game;
		if (h is null || g is null) return;
		if (_sub) return;

		// Tutorial subscriptions
		h.OnHandChanged        += () => { if(Hand?.AnyPlaced == true) TutorialOverlay.Instance?.TryAdvance(TutorialOverlay.TutorialTrigger.BombPlaced); };
		h.OnChainStep          += (i,t,c, p,m) => { if(i > 0) TutorialOverlay.Instance?.TryAdvance(TutorialOverlay.TutorialTrigger.ChainFired); };
		h.OnDetonationFinished += (t,c) => TutorialOverlay.Instance?.TryAdvance(TutorialOverlay.TutorialTrigger.WaveEnd);

		h.OnHandChanged        += StateHasChanged;
		h.OnChainStep          += OnChainStep;
		h.OnDetonationFinished += OnDetFinished;
		g.OnStateChanged       += OnGameStateChanged;
		g.OnChallengeResolved  += OnChallengeResolved; 
		h.OnChestCleared += OnChestCleared;
		g.OnScreenFlashRequest += OnFlashRequest;

		_sub = true;
		StateHasChanged();
	}
	void OnDet()
	{
		if (!CanDet) return;
		
		// tutorial
		TutorialOverlay.Instance?.TryAdvance(TutorialOverlay.TutorialTrigger.Detonated);

		_lastMultTier = 0;
		_hypePopupFired = false; // reset on each detonation

		Sound.Play("detonate_1");
		_chainSteps = BuildSteps(); _litStep = -1; 

		//SpawnPopup( "TEST +100", "coins", 50f, 400f );   // debug
		Game?.Detonate();
		StateHasChanged();
	}

	void OnRestart()
	{			
		_chainSteps.Clear(); _litStep = -1; 
		_displayScore = 0;
		_scoreAnimating = false;
		_perfectWavesThisRun = 0;

		// Replay
		Sound.Play(eventName: "replaybutton_ha");

		// Game Over 
		_ovShowStats = false; 
		_ovShowSecrets = false;
		_ovShowFrags = false; 
		_ovShowScore   = false;
		_ovShowButtons = false; 
		_secretsDone = false;
		_revealedChests = 0; 
		_revealingChests = false;
		_secretSectionVisible = false; 
		_chestRevealDone = false;
		_scoreAnimating = false; 
		_hypePopupFired = false;
		// frags
		_displayFragments = 0; 
		_fragsAnimating = false;

		_lastRevealedIdx = -1; // chest secret 

		_showConfetti = false;
		_confettiIdx  = -1;

		
		_msg = "Place your bombs then Detonate!";
		Game?.StartGame(Game.Mode, Game.SurvivalBombPool);
	}

	int _lastMultTier = 0;
	bool _hypePopupFired = false;
	void OnChainStep(int idx, int total, int clearedChestsSoFar, int projectedCleared, float mult)
	{
		_litStep = idx;

		if (idx > 0) // only chain-triggered bombs, not the first hand-placed one
			ScreenShaker.Instance?.Shake("shake-small", 0.25f);

		// Spawn pop up's
	 	if ( clearedChestsSoFar > 0 )
		{
			// Coin popup per chest cleared — cascades diagonally
			int earned = clearedChestsSoFar <= 1 ? 100 : clearedChestsSoFar <= 2 ? 150 : clearedChestsSoFar <= 4 ? 200 : 300;
			SpawnPopup( $"+{earned}", "coins", _popupX, 420f );
			_popupX += 3.5f;
			if ( _popupX > 55f ) _popupX = 28f;   // wrap so they don't drift off screen
		}

		// Add hype to the hype meter
		Game.AddHype(projectedCleared);

		  // Trigger hype max popup from HUD side
		if (Game?.HypeMaxActive == true && !_hypePopupFired)
		{
			_hypePopupFired = true;
			PopupLayer.Instance?.SpawnMultiplier("🔥 HYPE MAX!", "2× SCORE THIS WAVE", "mult-mega");
			ScreenShaker.Instance?.Shake("shake-strong", 0.45f);
			PopupLayer.Instance?.SpawnExplosionParticles(50f, 400f);
		}

		// Don't show multiplier popup when boss is alive — score is capped anyway
		if (Game?.BossAlive == true) 
		{
			// Still shake, still show coins, but no multiplier popup
			StateHasChanged();
			return;
		}

		// Spawn Popup multiplier overlay
		// projectedCleared — correct tier at each explosion step
		// it gives us directly (projected) the number of chests that are gonna be destroyed
		// on that explosion (which returns the right modifier at this point in time)
		if (projectedCleared >= 2)
		{
			/* Note : The key change is projectedCleared now escalates correctly with each bomb that fires — 
			so a 10-bomb chain that destroys 10 chests will show ×1 → ×1.5 → ×2 → ×3... 
			up to ×10 with matching sounds at each step, 
			regardless of whether the chests are destroyed early or late in the sequence */

			// Only spawn if tier increased — matches sound escalation exactly
			// tier going backwards means new chain group that won't beat peak — stay silent
			if (projectedCleared > _lastMultTier)
			{
				_lastMultTier = projectedCleared;

				// Old, HARD-CODED ONE : Only spawn if tier increased — matches sound escalation exactly
				@* var (val, label, cls) = chests switch {
					<= 2 => ("×1.5", "nice",              "mult-normal"),
					<= 4 => ("×2",   "CHAIN!",            "mult-normal"),
					<= 6 => ("×3",   "ON FIRE! 🔥",       "mult-big"),
					<= 8 => ("×4",   "CHAIN REACTION! 💥","mult-mega"),
					_    => ($"×{4f + (chests-8)*0.5f:F1}", "UNSTOPPABLE!! 🔥💥", "mult-insane"),
				}; *@

				// Use GetMultLabel for the label — matches the escalating text
				string label = BombHandSystem.GetMultLabel(projectedCleared );
				string val = projectedCleared switch {
					<= 2  => "×1.5",
					<= 3  => "×2",
					<= 5  => "×3",
					<= 7  => "×4",
					<= 8  => "×5",
					<= 9  => "×6",
					<= 10 => "×7",
					<= 11 => "×8",
					<= 13 => "×10",
					<= 14 => "×11",
					_ => $"×{11f + (projectedCleared - 14):F0}"
				};

				
				Log.Warning($"Multipler : {val}");
				Log.Warning($"Projected cleared : {projectedCleared}");
				string cls = projectedCleared  switch {
					<= 4  => "mult-normal",
					<= 6  => "mult-big",
					<= 8  => "mult-mega",
					_     => "mult-insane"
				};
				
				PopupLayer.Instance?.SpawnMultiplier(val, label, cls);

				// Boss dampening txt
				if (Game?.BossAlive == true)
				{
					var rng = new Random();
					float ox  = (float)(rng.NextDouble() * 20f - 10f); // x
					float oy  = (float)(rng.NextDouble() * 40f - 20f); // y
					float rot = (float)(rng.NextDouble() * 30f - 15f); // rot
					PopupLayer.Instance?.SpawnPopupRotated("⚠️ BOSS DAMPENING", "mult-cap-warning-popup", 
						42f + ox, 500f + oy, rot, 1.2f);
				}				
			}
		}
		StateHasChanged();
	}
	void OnDetFinished(int total, int coins)
	{
		Random _rng = new Random();

		// ── Cat reacts to the explosion ──
		if (total > 0)
		{
			int level = ChainReactionGame.Save?.PetLevel ?? 1;
			var reactions = PetVisuals.GetReactionSprites(level);
			if (reactions.Length > 0)
			{
				_petReacting = true;
				_petReactionTimer = 1.5f;
				_petReactionSprite = reactions[_reactionCount++ % reactions.Length];
				_petBubbleText = GetReactionLine(total);
				Sound.Play("cat_meow"); // consider a per-level sound too later
			}
		}


		// ── Final shake magnitude based on total chests cleared ──
		string finalShake = total switch {
			>= 8 => "shake-insane",
			>= 6 => "shake-strong",
			>= 4 => "shake-medium",
			>= 2 => "shake-small",
			_    => ""
		};
		if (!string.IsNullOrEmpty(finalShake))
			ScreenShaker.Instance?.Shake(finalShake, 0.55f);

		// Reset popup x drift and light up last chain step
		_popupX = 30f;
		_litStep = _chainSteps.Count - 1;

		// ── Big chain burst — 4+ chests gets a fan of random words ──
		if (total >= 4)
		{
			var rng = new System.Random();

			// Arc positions across the screen — creates a fan/spread effect
			float[] xs = { 25f, 32f, 40f, 47f, 54f };
			float[] ys = { 500f, 460f, 440f, 460f, 500f };

			// Word pools — randomly pick one from each category for variety
			string[] openers = { "INSANE", "EPIC", "MENTAL", "NASTY", "WILD", "SICK", "BRUTAL" };
			string[] middles = { "CHAIN", "COMBO", "BLAST", "BURST", "SURGE", "LINK" };
			string[] closers = { "REACTION", "SEQUENCE", "DETONATION", "EXPLOSION", "CARNAGE" };

			string[] words = {
				openers[rng.Next(openers.Length)],
				middles[rng.Next(middles.Length)],
				closers[rng.Next(closers.Length)],
				$"+{coins}",
				"🔥💥"
			};

			// Stagger each word with a small delay so they cascade in
			for (int i = 0; i < words.Length; i++)
				SpawnPopupDelayed(words[i], "burst-popup", xs[i], ys[i], i * 0.08f);
		}
		// ── Small chain — 2-3 chests gets a single compact summary ──
		else if (total >= 2)
		{
			SpawnPopup($"💥 {total} chests!", "chain-summary", 38f, 480f);
		}

		// ── Climax explosion sound scales with total chests ──
		ClimaxExplosionSound(total);

		// Only play if tier changed upward
		// Crowd reaction for massive chains
		if (total >= 2) // displayCleared >= 9
		{
			// Crowd escalating from a surprised gasp to full stadium roar
			string crowd = total switch {
				2 or 3 or 4 or 5 => "Small_Crowd_Applaude_1",
				6 or 7 => "crowd_applause_low_impact_1",
				8 or 9 => "crowd_impact_2",
				10 or 11 => "crowd_applause_low_impact_1",
				12 or 13 => "crowd_applause_mid_impact_1",
				14 => "crowd_applause_big_impact_1",
				_ => "Crowd_Applause_Pretty_Big_Impact_1"  // 10-12: absolute maximum
			};
			// Make sound volume go up procedurally based on total destroyed 
			if(total >= 5)
			{
				var handle = Sound.Play(crowd);
				handle.Volume = MathF.Min(0.6f, 0.3f + (total * 0.05f));
			}
			else // if < 5, then play basic level volume
			{
				Sound.Play(crowd);
			}
		}
		// Spawn a big pop up and flash if we did a 3 perfect wave streak
		// Check if all chests were cleared (total > 0 and grid has no chests left)
		// We track consecutive perfect waves (useful for game over stats)
		if (total >= (Game?.MinChests ?? 1))
		{
			_perfectWavesThisRun++;

			// ── Perfect streak reward at 3 in a row ──
			if (_perfectWavesThisRun >= 3 && _perfectWavesThisRun % 3 == 0)
			{
				Sound.Play("perfect_streak");
				SpawnPopup("🔥 PERFECT STREAK!", "burst-popup", 35f, 380f);
				SpawnPopup("×3 IN A ROW!", "mult-mega", 50f, 340f);
				SpawnPopup("🏆", "popup-xl", 62f, 380f);
				ScreenShaker.Instance?.Shake("shake-strong", 0.6f);
				Flash("flash-new-best", 1.2f, $"🔥 PERFECT STREAK ×{_perfectWavesThisRun}!");
			}
			else
			{
				_perfectWavesThisRun = 0; // reset on failure
			}
		}
		
		// ── Bottom message bar update ──
		_msg = total >= 4 ? $"💥 CHAIN REACTION! {total} chests · +{coins} pts"
			: total > 0  ? $"🔥 {total} chest{(total>1?"s":"")} cleared · +{coins} pts"
			: "No chests hit!";

		// Clear chain steps and multiplier overlay after delay
		_ = Clear(2.8f);
		StateHasChanged();
	}

	void ClimaxExplosionSound(int total)
	{
		// Play explosion climax sound
		switch(total)
		{
			case 0:
			break;

			case 1:
			break;

			case 2:
			Sound.Play("Climax_Explosion_Lv2");
			break;
			
			case 3:
			Sound.Play("Climax_Explosion_Lv3");
			break;

			case 4:
			Sound.Play("Climax_Explosion_Lv4");
			break;

			case 5:
			Sound.Play("Climax_Explosion_Lv5");
			break;

			case 6:
			Sound.Play("Climax_Explosion_Lv5");
			break;

			case 7:
			Sound.Play("Climax_Explosion_Lv5");
			break;

			case 8:
			Sound.Play("Climax_Explosion_Lv5");
			break;

			case 9:
			Sound.Play("Climax_Explosion_Lv5");
			break;

			case 10:
			Sound.Play("Climax_Explosion_Lv5");
			break;
		}
	}

	void OnChallengeResolved( bool completed, int bonus )
	{
		if ( completed )
		{
			_challengeMsg = $"⚡ +{bonus}";
			SpawnPopup( $"⚡ Challenge! +{bonus}", "challenge", 35f, 300f );
		}
		else
		{
			_challengeMsg = "";  // silent on fail
		}
		StateHasChanged();
	}

	async System.Threading.Tasks.Task Clear(float s)
	{
		// Mult overlay clears quickly — animation is done by then
		await GameTask.DelaySeconds( 1.2f );
		StateHasChanged();

		// Rest clears on the longer delay
		float remaining = s - 1.2f;
		if ( remaining > 0 ) await GameTask.DelaySeconds( remaining );
		_chainSteps.Clear(); _litStep = -1;
		_challengeMsg = "";

		if ( Game?.IsRunning == true ) _msg = "Place your bombs then Detonate!";
		
		StateHasChanged();
	}

	// Displays the steps of each bomb in the chain reaction
	List<string> BuildSteps()
	{
		var s = new List<string>();
		var sim = Hand?.Simulate();
		if (sim is null) return s;
		foreach (var n in sim.Sequence) s.Add(BombIcon(n.Type));
		return s;
	}

	protected override int BuildHash()
	{
		int a = System.HashCode.Combine(Game?.Wave, Game?.Score, Game?.Lives, 
										Game?.IsRunning, Game?.IsGameOver, 
										Hand?.SelectedSlot, Hand?.PlacedSlots.Count, _litStep);
										
		int b = System.HashCode.Combine(
			_chainSteps.Count, _displayScore,
			ChainReactionGame.Save?.ActiveModifiers?.Count ?? 0, _currentShake);

		int c = System.HashCode.Combine(
			_ovShowStats, _ovShowSecrets, _ovShowFrags,
			_ovShowScore, _ovShowButtons, _revealedChests,
			_displayFragments);

		int d = System.HashCode.Combine(
			_showConfetti, _confettiIdx);

		int e = System.HashCode.Combine(_musicHandles.Count);

		// Survival spawn indicator needs frequent re-render
		int f = System.HashCode.Combine(
			Game?.Mode, (int)((Game?.SurvivalSpawnTimerFraction ?? 0f) * 100f));

		int g = System.HashCode.Combine(_petReacting, _petFrameToggle);

		return a ^ b ^ c ^ d ^ e ^ f ^ g;
	}

	static string BombIcon(GridManager.BombType t) => t switch {
		GridManager.BombType.Cross    => "✛", GridManager.BombType.Sniper   => "↑",
		GridManager.BombType.Diagonal => "✕", GridManager.BombType.Square   => "■",
		GridManager.BombType.Chain    => "⛓", _ => "?" };

	// Replaces 'BombIcon' as a much more readable icon visuals for bombs
	// It's there until I make proper icons for bomb types
	static string Icon( GridManager.BombType t ) => t switch {
    GridManager.BombType.Cross    => "🎯",   // targeting reticle — readable as "hits in a cross"
    GridManager.BombType.Sniper   => "🏹",   // arrow — directional, fits the sniper identity
    GridManager.BombType.Diagonal => "⚡",   // lightning — diagonal energy, feels powerful
    GridManager.BombType.Square   => "💣",   // classic bomb — area damage, universally understood
    GridManager.BombType.Chain    => "🔗",   // chain link — exactly what it does
    _ => "?"
};

	static string BombName(GridManager.BombType t) => t switch {
		GridManager.BombType.Cross    => "Cross",    GridManager.BombType.Sniper   => "Sniper",
		GridManager.BombType.Diagonal => "Diagonal", GridManager.BombType.Square   => "Square",
		GridManager.BombType.Chain    => "Chain",    _ => "?" };

	static string BombHint(GridManager.BombType t) => t switch {
		GridManager.BombType.Cross    => "+ shape, 1 tile range",
		GridManager.BombType.Sniper   => "3 tiles in one direction",
		GridManager.BombType.Diagonal => "✕ shape, 2 tile range",
		GridManager.BombType.Square   => "3×3 area",
		GridManager.BombType.Chain    => "triggers nearby bombs",
		_ => "" };

	// ------------------------------------------------------------------ //
	// ------------------------- Floating Popup ------------------------- //
	// ------------------------------------------------------------------ //

	int _popupIdCounter = 0;

	void SpawnPopup( string text, string cssClass, float x, float y, float lifetime = 1.4f )
	{
		PopupLayer.Instance?.SpawnPopup( text, cssClass, x, y, lifetime );
	}

	// Converts a grid cell position to approximate screen % coordinates
	// Grid is centered in the area left of the 260px sidebar
	// Each cell is 110px wide + 6px gap = 116px step, grid is 5 cols = 580px total
	// Grid starts at roughly left: calc(50% - 130px - 290px) from viewport
	@* (float x, float y) GridCellToScreen( int row, int col )
	{
		// Grid is centered in viewport-minus-sidebar
		float cellStep = 116f;                    // 110px + 6px gap
		float viewportW = 1920f;                  // assumed viewport width

		// Grid center x = (viewport - sidebar) / 2 = (1920 - 260) / 2 = 830px = 43.2%
		// Col 0 center = gridCenter - 2 * cellStep = 830 - 232 = 598px = 31.1%
		float col0X = 925f;						   // px from left - tune this 
		float row0Y = 350f;                       // px from top — tune this

		float xPct = (col0X + col * cellStep) / viewportW * 100f;
		float yPx  = row0Y + row * cellStep;

		return (xPct, yPx);
	} *@
	(float x, float y) GridCellToScreen( int row, int col )
	{
		float cellStep = 116f;
		float viewportW = 1920f;
		float col0X = 820f;  // ← tune this until col 0 lines up
		float row0Y = 310f;  // ← tune this until row 0 lines up

		float xPct = (col0X + col * cellStep) / viewportW * 100f;
		float yPx  = row0Y + row * cellStep;

		return (xPct, yPx);
	}

	// spawns the coin popup right at the chest's grid position, with size scaling based on amount
	void OnChestCleared( int r, int c, int coins, float mult )
	{
		var (x, y) = GridCellToScreen( r, c );

		// explosion particles!
		PopupLayer.Instance?.SpawnExplosionParticles(x, y);

		// Main coin value — bigger text for bigger amounts
		string sizeClass = coins >= 400 ? "popup-xl" : coins >= 200 ? "popup-lg" : "popup-md";
		SpawnPopup( $"+{coins}", $"coins {sizeClass}", x, y );

		// If multiplier is active, spawn a smaller mult indicator slightly offset
		if ( mult > 1f )
			SpawnPopup( $"×{mult:F1}", "mult-tag", x + 8f, y - 2f );
	}

	void SpawnPopupDelayed( string text, string cssClass, float x, float y, float delay )
	{
		_ = SpawnPopupAfter( text, cssClass, x, y, delay );
	}

	async System.Threading.Tasks.Task SpawnPopupAfter( string text, string cssClass, float x, float y, float delay )
	{
		await GameTask.DelaySeconds( delay );
		SpawnPopup( text, cssClass, x, y );
	}
	
	// Main Menu button 
	void OnReturnToMenu()
	{
		_displayScore   = 0;
		_perfectWavesThisRun = 0;
		_chestRevealDone = false;
		_revealedChests = 0;
		_revealingChests = false;
		_scoreAnimating = false;
		_secretSectionVisible = false;
		Game?.ReturnToMenu();
	}

	// Score changing (animation)
	int _scoreTarget     = 0;
	int _scoreStepPerSec = 0; // how fast to count per second
	void OnScoreChanged(int newScore)
	{
		int diff = newScore - _displayScore;
		if(diff <= 0) { _displayScore = newScore; return; }
		_scoreTarget = newScore;
		// Count up over 0.4 seconds regardless of how big the jump is
		_scoreStepPerSec = (int)(diff / 0.4f);
		if(_scoreStepPerSec < 1) _scoreStepPerSec = 1;
	}

	// ---------------------------------------------------- //
	// --------------------- CHEST BOSS ------------------- //
	// ---------------------------------------------------- //
	void OnBossKilled(int score, int frags)
	{
		 SpawnPopup("👑 BOSS SLAIN!", "popup-mega", 50f, 360f);
		SpawnPopup($"+{score} pts", "popup-chain", 50f, 400f);
		SpawnPopup($"+{frags} 💠", "popup-nice", 50f, 440f);
		StateHasChanged();
	}

	string GetPetImagePath() => PetVisuals.GetImagePath(
		ChainReactionGame.Save?.PetLevel ?? 1,
		ChainReactionGame.Save?.PetUnlockShown ?? false,
		_petFrameToggle,
		_petReacting ? _petReactionSprite : null
	);
	
	// ---------------------------------------------------- //
	// -------------------- CONSUMABLES ------------------- //
	// ---------------------------------------------------- //

	bool HasAnyConsumable()
	{
		var save = ChainReactionGame.Save;
		if (save == null) return false;
		foreach (var cons in ConsumableCatalog.All)
			if (save.GetConsumableCount(cons.Id) > 0) return true;
		return false;
	}

	void OnUseConsumable(ConsumableId id)
	{
		var save = ChainReactionGame.Save;
		var game = Game;
		var hand = Hand;
		if (save == null || game == null) return;

		// Heart is handled separately — UseHeartConsumable validates and consumes
		if (id == ConsumableId.Heart)
		{
			bool used = game.UseHeartConsumable();
			if(used)
			{
				SpawnPopup("❤️ +1 Life!", "burst-popup", 50f, 300f);

				// tutorial case
				TutorialOverlay.Instance?.TryAdvance(TutorialOverlay.TutorialTrigger.HeartUsed);
			}
			Sound.Play("UI_Button");
			StateHasChanged();
			return;
		}

		if (!save.TryUseConsumable(id)) return;

		switch (id)
		{
			// +8s on timer
			case ConsumableId.FuseExtension:
				game.AddTime(8f);
				SpawnPopup("⏱️ +8s", "coins", 50f, 300f);
				Sound.Play("usetimeup");
				break;

			// Clears most dangerous column
			case ConsumableId.ColumnSweep:
				game.SweepDangerColumn();
				SpawnPopup("🧹 Swept!", "coins", 50f, 300f);
				Sound.Play("usesweep");
				break;

			// Doubles multiplier this wave
			case ConsumableId.Overdrive:
				game.ActivateOverdrive();
				SpawnPopup("💥 OVERDRIVE!", "burst-popup", 50f, 300f);
				Sound.Play("useoverdrive");
				break;

			// Reveals next wave chests
			case ConsumableId.Reveal:
				_ = game.RevealNextWave();
				SpawnPopup("🔮 Revealing...", "coins", 50f, 300f);
				Sound.Play("usereveal");
				break;

			// Redraws hand
			case ConsumableId.Redraw:
				hand?.DrawHand(game.Wave);
				SpawnPopup("🔄 New hand!", "coins", 50f, 300f);
				Sound.Play("useredraw");
				break;

			case ConsumableId.ClusterDrop:
				game.ActivateClusterDrop();
				SpawnPopup("💣 Cluster Dropped!", "burst-popup", 50f, 300f);
				Sound.Play("useclusterdrop");
				break;

			case ConsumableId.Overload:
				game.ActivateOverload();
				SpawnPopup("⚡ OVERLOAD!", "burst-popup", 50f, 300f);
				Sound.Play("useoverload");
				break;

			// Chain nuke
			 case ConsumableId.ChainNuke:
				game.ActivateChainNuke();
				SpawnPopup("☢️ CHAIN NUKE!", "burst-popup", 50f, 300f);
				Sound.Play("usenukebomb");
				break; 
		}

		StateHasChanged();
	}

	// ScreenFlash
	public void Flash(string cssClass, float duration, string text = "")
	{
		_flashClass  = cssClass;
		_flashText   = text;
		_flashEndsAt = Time.Now + duration;
		StateHasChanged();
	}
	void OnFlashRequest(string type, int wave)
	{
		switch (type)
		{
			case "boss":    Flash("flash-boss-spawn", 2.5f); break;
			case "wave":    Flash("flash-wave-start", 2.5f, $"WAVE {wave}"); break;
			case "newbest": Flash("flash-new-best",   1.0f, "🏆 NEW BEST!"); break;
			case "gameover":Flash("flash-game-over",  0.8f); break;
			case "bossdead":Flash("flash-boss-death", 0.6f); break;
			case "perfect": Flash("flash-wave-start", 1.2f, $"✨ PERFECT CLEAR! +{wave * 50}"); break;
		}
	}

	// **** PET COMPANION *** //
	string GetPetEmoji() => (ChainReactionGame.Save?.PetUnlockShown == true)
	? (ChainReactionGame.Save?.PetLevel ?? 1) switch
	{
		1 => "🥚",
		2 => "🐣",
		3 => "🐥",
		4 => "🦅",
		_ => "🐉"
	}
	: "📦";

	/* Wave Tier for grid-like Background cs changing color*/
	int GetWaveTier()
	{
		int wave = Game?.Wave ?? 1;
		if (wave < 5)  return 1;
		if (wave < 10) return 2;
		if (wave < 15) return 3;
		if (wave < 20) return 4;
		if (wave < 30) return 5;
		if (wave < 40) return 6;
		return 7;
	}

	/* MUSIC DEBUG */
	// ── Music debug ──

	float _loopStartTime = -1f;
	float _loopDuration  = 14.811f; // set this to match your actual loop length in seconds

	Dictionary<string, SoundHandle> _musicHandles = new();

	Dictionary<string, string> _debugTracks = new()
	{
		{ "music_menu",      "Menu Theme" },
		{ "1.bass_and_clap",    "In-Game" },
		{ "2.hit_hat_2",   "Tension" },
		{ "3.pulsed_violin",      "Boss Wave" },
		{ "music_gameover",  "Game Over" },
	};

	
	async System.Threading.Tasks.Task PlayTrackSynced(string key, float delay)
	{
		await GameTask.DelaySeconds(delay);
		if(!_musicHandles.ContainsKey(key)) // user may have cancelled during wait
			_musicHandles[key] = Sound.Play(key);
		StateHasChanged();
	}

	void StopDebugMusic()
	{
		foreach(var h in _musicHandles.Values)
			if(h.IsValid()) h.Stop();
		_musicHandles.Clear();
		_loopStartTime = -1f;
		StateHasChanged();
	}

	void PlayDebugTrack(string key)
	{
		// Toggle off — wait for next loop boundary
		if(_musicHandles.TryGetValue(key, out var existing) && existing.IsValid())
		{
			_ = StopTrackSynced(key);
			StateHasChanged();
			return;
		}

		// First track — start immediately
		if(_musicHandles.Count == 0)
		{
			_loopStartTime = Time.Now;
			_musicHandles[key] = Sound.Play(key);
			StateHasChanged();
			return;
		}

		// Subsequent tracks — wait for next loop boundary
		float elapsed  = (Time.Now - _loopStartTime) % _loopDuration;
		float waitTime = _loopDuration - elapsed;
		_ = PlayTrackSynced(key, waitTime);
		StateHasChanged();
	}

	async System.Threading.Tasks.Task StopTrackSynced(string key)
	{
		float elapsed  = (Time.Now - _loopStartTime) % _loopDuration;
		float waitTime = _loopDuration - elapsed;
		await GameTask.DelaySeconds(waitTime);

		if(_musicHandles.TryGetValue(key, out var handle) && handle.IsValid())
		{
			handle.Stop();
			_musicHandles.Remove(key);
		}
		StateHasChanged();
	}

	// Survival Mode - helper
	string FormatSurvivalTime(float seconds)
	{
		int m = (int)(seconds / 60f);
		int s = (int)(seconds % 60f);
		return $"{m:00}:{s:00}";
	}
}