Game/2D/ChainReactionGame.cs
using Sandbox;
using System;
using Sandbox.Services;
using System.Collections;
public sealed class ChainReactionGame : Component
{
	[Property] public GridManager Grid { get; set; }
	[Property] public BombHandSystem Hand { get; set; }

	// ── State ─────────────────────────────────────────────────────────────
	public int Wave { get; private set; } = 1;
	public int Score { get; private set; }
	public int BestScore { get; private set; } = 0;
	public int Lives { get; private set; } = 1;
	public int WaveTarget { get; private set; }
	public int WaveScore { get; private set; }
	public int MinChests { get; private set; } = 1;
	public float TurnTime { get; private set; }   // seconds left this turn
	public bool TimerActive { get; private set; }
	public bool IsRunning { get; private set; }
	public bool IsGameOver { get; private set; }

	public Random Rng { get; } = new Random();
	public static ChainReactionGame Instance { get; private set; }

	// ── Events ────────────────────────────────────────────────────────────
	public delegate void StateChanged();
	public delegate void ChestDestroyed( int r, int c, int coins, float mult );
	public event StateChanged OnStateChanged;
	public event ChestDestroyed OnChestDestroyed2;
	public delegate void ChallengeResolved( bool completed, int bonus );
	public event ChallengeResolved OnChallengeResolved;
	public delegate void BossKilled(int scoreBonus, int fragBonus);
	public event BossKilled OnBossKilled;
	public delegate void ScreenFlashRequest(string type, int wave);
	public event ScreenFlashRequest OnScreenFlashRequest;

	// For music & potential other things
	public delegate void GameStartedEvent();
	public delegate void GameReturnedEvent();
	public event GameStartedEvent OnGameStarted;
	public event GameReturnedEvent OnGameReturned;

	// ── Config ────────────────────────────────────────────────────────────
	public const int StartLives = 1;
	public const int MaxLives = 3;
	const int ChainBonus = 120;
	const float BaseTurnTime = 20f;  // seconds per turn wave 1
	const float MinTurnTime = 4f;   // floor at high waves

	// ── Challenges ─────────────────────────────────────────────────────────

	public WaveChallenge CurrentChallenge { get; private set; }

	int _secretChestsSpawnedThisRun = 0;

	int _chestsAtWaveStart = 0;

	// ── GameOver Stats ─────────────────────────────────────────────────────

	public int TotalChestsCleared { get; private set; }
	public int BestChainLength    { get; private set; }
	public int ChallengesCompleted { get; private set; }
	public int FragmentsEarned    { get; private set; }
	public List<SecretChestReward> SecretChestRewards { get; private set; } = new();
	public int PrevBestScore { get; private set; } = 0;

	// ── Lifecycle ─────────────────────────────────────────────────────────
	public bool GameStarted { get; set; } = false;

	// ── Game over ─────────────────────────────────────────────────────────
	public string GameOverReason { get; private set; } = "";

	// ── Modifiers ─────────────────────────────────────────────────────────

	bool _firstChainUsedThisWave = false;

	float _timerPenaltyNextWave = 0f; // cursed chest's timer curse

	// ── Events / Frags Count total ─────────────────────────────────────────
	public int BonusEventFragments { get; private set; } = 0; // lucky event..
	bool _luckyStreakFiredThisRun {get; set;}
	public int PerfectWaveFragments { get; private set; } = 0;
	public int BossFragments { get; private set; } = 0;

	protected override void OnStart()
	{
		Instance = this;

		// Load Save data once on boot, persist for the entire session
		Save = PlayerSave.Load();
    	Log.Info( $"Save loaded — fragments: {Save.TotalFragments}, best wave: {Save.BestWave}" );
		Initialize();

		// Wires up system once on boot, persist for the entire session
		// Idempotent subscription — protects against HandleDetonationFinished firing twice
		// (e.g. SlideChestsDown running twice in one wave transition) if OnStart ever runs more than once.
		Hand.OnDetonationFinished -= HandleDetonationFinished;
		Hand.OnDetonationFinished += HandleDetonationFinished;
		Hand.OnSecretChestDestroyed -= RollSecretChestReward;
		Hand.OnSecretChestDestroyed += RollSecretChestReward;

    	// Wait for components then show the menu
		_ = WaitThenShowMenu();
	}

	private void Initialize()
	{
		BestScore = Save?.BestScore ?? 0;
	}

	// ── Save System ────────────────────────────────────────────────────────

	public static PlayerSave Save { get; private set; }
	public bool HasMod(ModifierId id) => Save?.ModifierActive(id) ?? false;

	// ── GameMode ────────────────────────────────────────────────────────

	public GameMode Mode { get; private set; } = GameMode.Wave;

	// ── Survival mode state ──────────────────────────────────────────────
	float _survivalSpawnTimer = 0f;
	float _survivalSpawnInterval = 2.2f;
	float _survivalElapsed = 0f;
	bool _survivalSpawnFrozen = false; // pauses spawning while milestone overlay shows
	public float SurvivalSpawnTimerFraction => _survivalSpawnInterval > 0f
    ? 1f - (_survivalSpawnTimer / _survivalSpawnInterval) : 0f;
	public float SurvivalElapsed => _survivalElapsed;
	public int SurvivalSpawnCount { get; private set; } = 0;
	int _survivalFakeWave = 1;
	public int SurvivalFakeWave => _survivalFakeWave;
	public List<GridManager.BombType> SurvivalBombPool { get; private set; } = null;

	const float SurvivalMinInterval = 0.6f; // fastest spawn rate — tune to taste

	// ── Survival milestones ──────────────────────────────────────────────
	int _survivalLastMilestoneMinute = 0;
	public int SurvivalBonusBombSlots { get; private set; } = 0;
	const int MaxSurvivalBonusSlots = 2; // caps how far hand size can grow beyond base

	// ── Survival: continuous difficulty curve ───────────────────────
	const float SurvivalIntervalStart = 2.4f;  // was 4.2f
	const float SurvivalIntervalFloor = 0.7f;  // was 1.0f
	const float SurvivalRampMinutes   = 10f;    // was 8f
	const float SurvivalEmptyGraceSeconds = 0.8f; // caps true dead-air time on an empty board
	float _survivalEmptySince = -1f; // -1 = board isn't currently empty

	const int SurvivalBatchMaxStart = 4;  // unchanged
	const int SurvivalBatchMaxFloor = 6;  // was 3 — kept slightly higher so late-game doesn't go flat
	const int SurvivalBatchMinStart = 3;  // unchanged
	const int SurvivalBatchMinFloor = 4;  // was 2 — same reasoning

	// ── Survival: per-minute flavor modifiers (reset each minute) ──
	float _survivalPhaseIntervalMult = 1f;
	int   _survivalPhaseBatchBonus   = 0;
	bool  _survivalGoldRushMinute    = false;

	// Add near the other Survival constants
	const float SurvivalMinuteSpeedStep = 0.94f;  // each minute makes spawns ~6% faster
	const float SurvivalHardFloor = 0.5f;         // absolute fastest, never below


// ── Survival: score surge flavor ────────────────────────────────
float _survivalScoreBoostUntil = 0f;
const float SurvivalScoreBoostMult = 2f;


	// ── Leaderboard ────────────────────────────────────────────────────────

	float _sessionStartTime = 0f;
	async void SubmitLeaderboard( int score, int wave, int previousBestChainLength)
	{
		try
		{
			// s&box uses Stats for submission, Leaderboards for reading
			if ( score >= Save.BestScore )
				Stats.Increment( "chain_reaction_score", score );

			if ( wave >= Save.BestWave )
				Stats.Increment( "chain_reaction_wave", wave );

			if ( BestChainLength >= previousBestChainLength)
				Stats.Increment("chain_reaction_best_chain", BestChainLength);

			// Submit top Pets total xp (including level)
			int petScore = (Save.PetLevel * 1_000_000) + Save.PetXp;
			Stats.Increment("chain_reaction_pet", petScore);

			// Top sequence chain reaction + scores (in case multiple have same sequence)
			int chainScore = (BestChainLength * 10_000_000) + Score;
			Stats.Increment("chain_reaction_chain", chainScore);

			// medal tier (top 100 force contributor)
			int medalTier = Save?.ForceMedalTier ?? 0;
			if(medalTier > 0)
				Stats.Increment("chain_reaction_medal", medalTier);

			Log.Info( $"Stats submitted — score: {score}, wave: {wave}" );
		}
		catch ( System.Exception e )
		{
			Log.Warning( $"Leaderboard submit failed: {e.Message}" );
		}
	}

	// ── Gameplay ────────────────────────────────────────────────────────


	// Delay spawning chest & stuff (until tiles are created)
	async System.Threading.Tasks.Task WaitThenShowMenu()
	{
		// Wait for all components to initialize before firing first UI update
		await GameTask.DelaySeconds( 0.15f );
		OnStateChanged?.Invoke();
	}

	protected override void OnUpdate()
	{
		if (IsPaused) return;

		// Reactor mode runs its own loop inside IdleReactor — nothing to tick here
		if (Mode == GameMode.Idle) return;

		// Pause timer during tutorial
    	if (TutorialOverlay.Instance?.IsActive == true) return;
		if (!IsRunning || Hand.IsDetonating) return;

		// Survival has its own independent tick — doesn't use TimerActive at all
		if (Mode == GameMode.Survival)
		{
			TickHypeDrain(); 
			TickSurvival();
			return;
		}

		// Handle timer freeze event
		if (_timerFrozenFor > 0f)
		{
			_timerFrozenFor -= Time.Delta;
			OnStateChanged?.Invoke();
			return; // don't tick timer while frozen
		}

		TurnTime -= Time.Delta;
		OnStateChanged?.Invoke(); // tick every frame for the timer bar

		TickHypeDrain();   

		// Trigger Next Wave from Time Out :
		// 1 - (Detonate (if any bomb - can still lose if conditions are not met) 
		// 2 - OR lose a live right of the bat and go next wave)
		if ( TurnTime <= 0f )
		{
			TurnTime = 0f;
			TimerActive = false;
			// Time's up — force detonate whatever is placed, or lose a life
			if ( Hand.AnyPlaced )
				Hand.ExecuteDetonate();
			else
				HandleTimeUp();
		}
	}

	// If no bomb was on any tile when time was up.. automatic result - Next Wave trigger
	void HandleTimeUp()
	{
		Lives--;

		// Check game over 
		if ( Lives <= 0 ) { 
			TriggerGameOver("⏱️ Time ran out and lives reached zero! Be faster next time my friend :)"); 
			return; 
			
		} 

		// Inform player why he lost a life
		MilestoneOverlay.Instance?.ShowMessage("⏱️ Time's up — life lost!", Wave);
		Sound.Play("TimeIsUp");

		Wave++;
		WaveScore = 0;
		WaveTarget = ComputeWaveTarget( Wave );
		MinChests = MinChestsForWave( Wave );

		SlideChestsDown();

		// Same check here — time-up also slides chests down
		if ( Grid.AnyChestInLastRow() ) { TriggerGameOver("📦 A chest reached the bottom row! Too bad.. hehe"); return; }

		SpawnChests( ChestsForWave( Wave ) );
		Hand.DrawHand( Wave );
		StartTurnTimer();
		OnStateChanged?.Invoke();
	}

	// ── Public API ────────────────────────────────────────────────────────
	public void StartGame(GameMode mode, List<GridManager.BombType> survivalBombPool)
	{
		Mode = mode;
		SurvivalBombPool = survivalBombPool;
	    Save = PlayerSave.Load(); // reload fresh before each run

		 // Safety — BombSlot2 is a permanent welcome gift, always ensure it's granted
		if(!Save.OwnsModifier(ModifierId.BombSlot2))
		{
			Save.OwnedModifiers.Add("BombSlot2");
			Save.OwnedBombs.Add(GridManager.BombType.Diagonal.ToString());
			Save.Save();
			Log.Info("StartGame — BombSlot2 was missing, re-granted");
		}

    	GameStarted = true;
		OnGameStarted?.Invoke();

		// ── Reactor (Idle) mode — completely separate loop, no waves involved ──
		if (mode == GameMode.Idle)
		{
			StartIdleMode();
			return;
		}

		// Reset random event state
		ClearRandomEvent();
		_lastEventWave              = 0;
		_consecutiveWavesNoLifeLoss = 0;
		_consecutiveFailedWaves     = 0;
		_livesAtWaveStart           = StartLives;
		_chainMasterFiredThisRun = false;
		BonusEventFragments = 0;
		_luckyStreakFiredThisRun = false;
		PerfectWaveFragments = 0;
		BossFragments = 0;

		// Base first wave data 
		Wave = 1;
		CurrentChallenge = RollChallenge( Wave ); // new challenge
		Score = 0;
		// Level 2 - Bonus Pet + Level 6 - Extra bonus life
		Lives = StartLives 
			+ (Save?.PetLevel >= 2 ? 1 : 0)
			+ (Save?.PetLevel >= 6 ? 1 : 0);
		WaveScore = 0;
		IsRunning = true;
		IsGameOver = false;
		_bossAlive = false;
		_secretChestsSpawnedThisRun = 0; // secret chest
		WaveTarget = ComputeWaveTarget( Wave );
		MinChests = MinChestsForWave( Wave );
		MilestoneOverlay.Instance?._queue.Clear(); // clear Milestone queue

		// cursed chest 
		_activeCursedChests = 0;
		_timerPenaltyNextWave = 0f;

		// mod 
 		_firstChainUsedThisWave = false;

		_sessionStartTime = Time.Now; // leaderboard

		// Hype bar
		_hypeMeter = 0f;
		_hypeMaxActive = false;
		_hypeTriggeredThisWave = false;

		// Game Over Stats
		TotalChestsCleared  = 0;
		BestChainLength     = 0;
		ChallengesCompleted = 0;
		FragmentsEarned     = 0;
		SecretChestRewards.Clear();

		Grid.InitGrid(); // 1. creates grid 
		if (Mode == GameMode.Survival)
    		Grid.ResizeGrid(7, 5); // taller grid — more buffer before bottom row
		else
			Grid.ResizeGrid(5, 5); // Wave Mode 
		SpawnChests( ChestsForWave( Wave ) ); // 2. spawn chests for wave -x
		TrySpawnSecretChest();  // 8% chance
		TrySpawnBossChest(); // added for consistency
		_chestsAtWaveStart = Grid.AllChestCells().Count; // retrieve all chests (num) for challenge checks
		int handSize = 1;
		if(HasMod(ModifierId.BombSlot2)) handSize = 2;
		if(HasMod(ModifierId.BombSlot3)) handSize = 3;
		if(HasMod(ModifierId.BombSlot4)) handSize = 4;
		Hand.HandSize = handSize;
		Hand.DrawHand( Wave );
		
		// if Survival Mode 
		if (Mode == GameMode.Survival)
		{
			_survivalSpawnTimer = 2.2f;
			_survivalSpawnInterval = 2.2f;
			_survivalElapsed = 0f;
			SurvivalSpawnCount = 0;
			_survivalLastMilestoneMinute = 0;
			_survivalPhase = -1;
			_survivalPhaseInitialized = false;
			_survivalEmptySince = -1f;
			SurvivalBonusBombSlots = 0;
			SurvivalGoalLabel = "";
			SurvivalGoalProgress = 0;
			SurvivalGoalTimeLeft = 0f;
			_survivalSpawnFrozen = false;
			TimerActive = false;

			if (!Save.SurvivalTutorialCompleted)
			{
				_survivalSpawnFrozen = true; // freeze while tutorial shows
				SurvivalTutorial.Instance?.Activate();
				Save.SurvivalTutorialCompleted = true;
				Save.Save();
			}

			// survival starts with one extra slot over wave mode's base
    		Hand.HandSize = Math.Max(handSize + 1, 3);
			Hand.DrawHand(1); // redraw with the new hand size
		}
		else // Wave Mode 
		{
			StartTurnTimer();
		}
		
		OnStateChanged?.Invoke(); // fires delegate for listening classes (esp. UI updates)

		Log.Info($"StartGame — OwnedModifiers: {string.Join(", ", Save.OwnedModifiers)}, HandSize: {handSize}");

	}

	public void UnfreezeSurvivalSpawn()
	{
		_survivalSpawnFrozen = false;
		_survivalSpawnTimer = _survivalSpawnInterval;
	}

	// Calls Detonate (from Gamehud.razor button OnClickEvent)
	public void Detonate()
	{
		if ( !IsRunning || Hand.IsDetonating || !Hand.AnyPlaced || IsPaused) return;
		TimerActive = false;
		Hand.ExecuteDetonate();
	}

	// Mainly call for UI updates
	public void OnChestDestroyed( int r, int c, int earned, float mult )
		=> OnChestDestroyed2?.Invoke( r, c, earned, mult );

	// ── Turn timer ────────────────────────────────────────────────────────
	// calculates how long a turn should last, and makes later waves faster.

	float _maxTurnTime = 20f; // track the actual max for this wave
	void StartTurnTimer()
	{
		float t;

		if (Wave <= 5)
			t = 24f - Wave * 0.8f;          // wave 1=23.2s, 5=20s — very relaxed early
		else if (Wave <= 15)
			t = 20f - (Wave - 5) * 0.9f;   // wave 6=19.1s, 15=11s
		else if (Wave <= 30)
			t = 11f - (Wave - 15) * 0.35f; // wave 16=10.6s, 30=5.75s
		else if (Wave <= 50)
			t = MathF.Max(7f, 5.75f - (Wave - 30) * 0.05f); // wave 30-50: 7s floor
		else
			t = MathF.Max(6f, 7f - (Wave - 50) * 0.02f);    // wave 50+: 6s floor

		if(Wave == 1 && HasMod(ModifierId.TimerBonus))
			t += 5f;

		// Cursed chest penalty — -2s per active cursed chest on grid
    	float cursePenalty = MathF.Min(_activeCursedChests * 2f, 6f); // max -6s from curses

		// Apply any queued penalties from previous wave
		t = MathF.Max(3f, t - _timerPenaltyNextWave - cursePenalty); // cap to 3s
		_timerPenaltyNextWave = 0f;	

		/*
			This gives players a proper learning curve — wave 1-5 are relaxed, 
			6-15 introduce pressure, 15-30 get genuinely hard, 
			30+ is consistently intense without the arbitrary 4s cliff.
		*/

		_maxTurnTime = t;
		TurnTime     = t;
		TimerActive  = true;

		// If milestone overlay is showing, immediately re-pause
		if (MilestoneOverlay.Instance?.IsActive == true)
		{
			TimerActive = false;
			Log.Info("StartTurnTimer: milestone active — re-pausing immediately");
		}

	}
	// Returns the current timer as a percentage/fraction of the original allowed turn time (for UI progress bar)
	public float TurnTimeFraction => TurnTime / _maxTurnTime;

	// ── Wave logic ────────────────────────────────────────────────────────
	async void HandleDetonationFinished( int chestsCleared, int coinsEarned )
	{
		// ------------------------ Handles the End of a Wave ------------------------- // 

		// If GameMode == Survival 
		if (Mode == GameMode.Survival)
		{
			await HandleSurvivalDetonationFinished(chestsCleared, coinsEarned);
			return;
		}

		// Tracks how player behaves in game (how much lives, for how many waves in a row)
		_livesAtWaveStart = Lives;

		TimerActive = false;
		TickHypeMeter(); // Hype meter
		int bonus = chestsCleared > 2 ? (chestsCleared - 2) * ChainBonus : 0; // Bonus points if we cleared more than 2 chests.
		int total = coinsEarned + bonus; // Total coins earned 

		// ── Challenge check ──────────────────────────────────────────────
		// Must happen BEFORE Score += total so the bonus gets included in this wave
		if ( CurrentChallenge != null && !CurrentChallenge.Completed )
		{
			bool success = CurrentChallenge.Type switch
			{
				ChallengeType.ClearAtLeast =>
					chestsCleared >= CurrentChallenge.Target,

				ChallengeType.ChainLength =>
					Hand.LastSimResult != null &&
					Hand.LastSimResult.Sequence.Any(n => n.Depth > 0),

				ChallengeType.ClearGem =>
					Hand.LastSimResult != null &&
					Hand.LastSimResult.HitChests.Any(kv =>
						kv.Value.WillDestroy &&
						kv.Value.ChestType == GridManager.ChestType.Gem),

				ChallengeType.ClearBombChest =>
					Hand.LastSimResult != null &&
					Hand.LastSimResult.HitChests.Any(kv =>
						kv.Value.WillDestroy &&
						kv.Value.ChestType == GridManager.ChestType.BombChest),

				ChallengeType.ClearInOneBlast =>
					Hand.LastSimResult != null &&
					Hand.LastSimResult.Sequence.Count(n => n.Depth == 0) == 1 &&
					chestsCleared > 0,

				ChallengeType.NoBombType =>
					!Hand.PlacedBombTypes.Contains(CurrentChallenge.ForbiddenBomb) &&
					chestsCleared >= MinChests,

				// ── New types ──
				ChallengeType.ClearAll =>
					Hand.LastSimResult != null &&
					chestsCleared >= CurrentChallenge.Target, // Target set to chest count at wave start

				ChallengeType.ChainDepth3 =>
					Hand.LastSimResult != null &&
					Hand.LastSimResult.Sequence.Any(n => n.Depth >= 3),

				ChallengeType.NoDamage =>
					chestsCleared >= MinChests && Lives == StartLives, // didn't lose a life

				_ => false
			};

			if (success)
			{
				CurrentChallenge.Completed = true;
				total += CurrentChallenge.BonusCoins;

				// check if has FirstChainDouble modifier (wave score x2)
				if(HasMod(ModifierId.FirstChainDouble) && !_firstChainUsedThisWave && chestsCleared >= 2)
				{
					total = (int)(total * 2f);
					_firstChainUsedThisWave = true;
				}

				OnChallengeResolved?.Invoke( true, CurrentChallenge.BonusCoins );
			}
			else
			{
				OnChallengeResolved?.Invoke(false, 0);
			}
		}
    	// ─────────────────────────────────────────────────────────────────

		// Game over stats (track)
		TotalChestsCleared += chestsCleared;
		int chainLen = Hand.LastSimResult?.Sequence.Count ?? 0;
		if ( chainLen > BestChainLength ) BestChainLength = chainLen;
		if ( CurrentChallenge?.Completed == true ) ChallengesCompleted++;

		// If we didn't clear the minimum required chest, we lose a life. 
		if ( chestsCleared < MinChests )
		{
			_consecutiveFailedWaves++; // track player behavior (how he performs)

			// I - Bonus Wave Action (random event)
			if (_bonusWaveActive)
			{
				// No life loss during bonus wave
			}

			// Overload CONSUMABLE — ignore min chest requirement
			else if (OverloadActive)
			{
				OverloadActive = false; // consume it
				// show popup
				// continue normally
			}

			else
			{
				Lives--;
				if(Lives <= 0)
				{
					TriggerGameOver($"You cleared {chestsCleared}/{MinChests} chests..");
					return;
				}

				// Still alive — show which source is keeping them alive
				bool hasPetLife   = Save?.PetLevel >= 2;
				bool hasHeartCons = (Save?.GetConsumableCount(ConsumableId.Heart) ?? 0) > 0;

				string lifeMsg = hasPetLife
					? "💔 Your pet shields you from defeat!"
					: hasHeartCons
						? "💔 Heart consumable absorbed the hit!"
						: "💔 You lost a life!";

				MilestoneOverlay.Instance?.ShowMessage(lifeMsg, Wave);
				Sound.Play("Heart_beat");
			}
		}
		else // success
		{
			_consecutiveFailedWaves = 0;

			if (Lives == _livesAtWaveStart)
				_consecutiveWavesNoLifeLoss++;
			else
				_consecutiveWavesNoLifeLoss = 0;

			// ── Perfect Clear — cleared every chest on the board ──
			bool isPerfect = chestsCleared >= _chestsAtWaveStart && _chestsAtWaveStart > 0;
			if (isPerfect)
			{
				int perfectBonus = Wave * 50; // scales with wave — wave 1=50, wave 10=500
				total += perfectBonus;
    			FragmentsEarned += 1; // +1 frag per perfect wave — small but meaningful
				PerfectWaveFragments += 1;
				OnScreenFlashRequest?.Invoke("perfect", Wave);
				Sound.Play("crowd_applause_big_impact_1"); // reuse a satisfying sound for now
			}
		}

		// II - Bonus Wave Action (random event) : multiply total score
		if (_bonusWaveActive)
			total = (int)(total * 3f);


		Score += total;	// updates score 
		WaveScore += total; // updates wave score 
		if ( Score > BestScore ) BestScore = Score; // updates NEW! best score

		// Reset overload regardless
		OverloadActive = false;

	    // This waits exactly as long as the animations take — no more, no less.
		// Buffer before next wave so UI animations complete (+coins msg etc..)
		  // Wait until cell flash animations finish after last bomb
		float delay = Hand.GetPostDetonationWait();
    	await GameTask.DelaySeconds( delay );

		// Move chests that weren't exploded, down.
		SlideChestsDown();

		// Move speed based on Wave num
		int bossMoveCooldown = Wave switch {
			>= 200 => 1,  // wave 200+: moves every wave
			>= 150 => 1,  // wave 150+: moves every wave  
			>= 100 => 2,  // wave 100+: moves every 2 waves
			>= 50  => 3,  // wave 50+:  moves every 3 waves
			_      => 3   // default:   moves every 3 waves
		};

		// Boss advances toward bottom (after every 'bossMoveCooldown' (i.e: 3) num of wave)
		if(_bossAlive && Wave - _bossSpawnedWave >= bossMoveCooldown)
		{
			MoveBossDown();
			_bossSpawnedWave = Wave; // reset counter
		}

		// After chests have been moved down, we check if that column is full (of chests) if so : Game Over
		if ( Grid.AnyChestInLastRow()) { TriggerGameOver("Mehh, why didn't you clear that chest before it reaches the last row!? 💔"); return; }

		// -------------------------------- Next Wave ----------------------------- //

		// Everything spawns in one frame together
		ClearRandomEvent(); // random events cleared
		int prevMinChests = MinChests;
		Wave++;
		WaveScore = 0;
		WaveTarget = ComputeWaveTarget( Wave );
		MinChests = MinChestsForWave( Wave );
		if (MinChests > prevMinChests)
		{
			MilestoneOverlay.Instance?.ShowMessage($"⚠️ Min chests raised to {MinChests}!", Wave);
			Sound.Play("Alert_Danger_MinChest");
		}

		// ------ Show milestone overlay if this wave is a milestone ------ //
		MilestoneOverlay.Instance?.ShowIfMilestone(Wave);

		// and wait for milestone overlay to be dismissed before starting timer
		if (MilestoneCatalog.GetForWave(Wave) != null)
		{
			// Wait until overlay is no longer active (give player some time to read)
			while (MilestoneOverlay.Instance?.IsActive == true)
				await GameTask.DelaySeconds(0.1f);
			
			// Apply grid expansion at milestones
			UpdateGridSize(Wave);
		}

		// ------- If no milestone to show, try showing random event overlay (based on rules inside TryTriggerRandomEvent()) ------ //

		// 1. Then check random event (only if no milestone this wave)
		if (MilestoneCatalog.GetForWave(Wave) == null)
			TryTriggerRandomEvent(Wave);

		// 2. Wait for either overlay to dismiss (give player some time to read)
		while (MilestoneOverlay.Instance?.IsActive == true)
			await GameTask.DelaySeconds(0.1f);

		// 3. Apply random event effects AFTER player sees the announcement
		ApplyRandomEvent();

		// ------------------------------------------------------------------------ //

		SpawnChests( ChestsForWave( Wave ) );
		TickHypeMeter(); // Hyper meter
		RecountCursedChests(); // cursed chest (malus time -2s)
		ConsumeOverdrive(); // reset consumable (if used)
		
		// Milestone flash if reached wave 100
		if(Wave == 100)
		{	
			MilestoneOverlay.Instance?.ShowEvent(RandomEventType.Wave100, Wave);
			Sound.Play("fanfare_100");

			await Task.Delay(3000);

			Sound.Play("applause_1");

			await Task.Delay(2000);

			Sound.Play("crowd_4_a");
			
		}
		// Milestone flash if reached wave 200
		else if(Wave == 200 )
		{
			MilestoneOverlay.Instance?.ShowEvent(RandomEventType.Wave100, Wave);
			Sound.Play("fanfare_200");

			await Task.Delay(3000);
			
			Sound.Play("applause_1");

			await Task.Delay(2000);

			Sound.Play("crowd_4_a");
		}
		// Milestone flash every 5 waves
		else if (Wave % 5 == 0)
		{
			OnScreenFlashRequest?.Invoke("wave", Wave); 
			Sound.Play("WaveMilestone");
		}
		
		TrySpawnSecretChest(); // secret chest
		_chestsAtWaveStart = Grid.AllChestCells().Count; // retrieve all chests (num) for challenge checks
		CurrentChallenge = RollChallenge(Wave); // challenge for this wave
		 _firstChainUsedThisWave = false; // reset mod
		Hand.DrawHand( Wave );
		TrySpawnBossChest(); // boss chest
		StartTurnTimer();
		OnStateChanged?.Invoke();
	}

	// Survival Mode func
	async System.Threading.Tasks.Task HandleSurvivalDetonationFinished(int chestsCleared, int coinsEarned)
	{
		float mult = _survivalElapsed <= _survivalScoreBoostUntil ? SurvivalScoreBoostMult : 1f;
		int earned = (int)MathF.Round(coinsEarned * mult);

		Score += earned;
		if (Score > BestScore) BestScore = Score;

		TotalChestsCleared += chestsCleared;

		TickSurvivalGoalProgress(chestsCleared);
		

		Hand.DrawHand(1);
		OnStateChanged?.Invoke();

		if (chestsCleared > 0)
		{
			_survivalAdvancePending = true; // block the ambient timer from firing until this resolves

			float delay = Hand.GetPostDetonationWait();
			await GameTask.DelaySeconds(delay);

			if (!IsRunning) { _survivalAdvancePending = false; return; }

			var (bmin, bmax) = GetSurvivalBaseBatch();
			int count = Rng.Next(bmin, bmax + 1);

			if (!SurvivalAdvanceAndSpawn(count)) // slide + game-over check + spawn, same path the timer uses
			{
				_survivalAdvancePending = false;
				return; // game over already triggered inside
			}

			_survivalSpawnTimer = _survivalSpawnInterval; // fresh countdown starts now — no leftover near-zero value from before you detonated
			_survivalAdvancePending = false;
			OnStateChanged?.Invoke();
		}
	}
	void SlideChestsDown()
	{
		// i.e : 5x5 grid - 0 == top row and Grid.Rows - 1 == 4 (last row - index start at 0)
		// so, we want to go from the bottom up,  while the 2nd (inner) loop checks every column left -> right.
		//
		// Why bottom -> top?
		// Because we're moving chests downward.
		// If we iterated top -> bottom, a chest could move into a row that was already processed this frame.
		//
		// Then, why -2 ? => because we don't want to move a chest from the LAST row (it'd be out of bounds from the grid)
		for ( int r = Grid.Rows - 2; r >= 0; r-- )
			for ( int c = 0; c < Grid.Cols; c++ )
			{
				var cell = Grid.Grid[r, c];
				if ( !cell.HasChest ) continue; // ignore cells that don't have any chest (even those exploded)
            	if ( cell.Chest == GridManager.ChestType.Secret ) continue;  // secret chest doesn't slide
				if (cell.Chest == GridManager.ChestType.Boss)   continue; // boss chest doesn't slide
				if (cell.Chest == GridManager.ChestType.Void)   continue; // void doesn't slide 

				int nr = r + 1; // increment row (to go down)
				var below = Grid.Grid[nr, c]; // fetch the grid cell a those new coords

				// if that cell is empty, then update both cells (the new empty one (above), and the new filled one (below))
				if ( below.IsEmpty )
				{
					below.Chest = cell.Chest;
					below.HitsLeft = cell.HitsLeft;
					cell.Chest = GridManager.ChestType.None;
					cell.HitsLeft = 0;
				}
				else if ( below.Chest == GridManager.ChestType.Secret )
				{
					// Normal chest passes through secret chest — swap positions
					// Secret chest floats up, normal chest sinks down
					below.Chest = cell.Chest;
					below.HitsLeft = cell.HitsLeft;
					cell.Chest = GridManager.ChestType.Secret;
					cell.HitsLeft = 1;
				}
			}
	}

	// Game Over (+ update UI OnStateChanged)
	void TriggerGameOver(string reason)
	{
		PrevBestScore = Save?.BestScore ?? 0; // capture BEFORE RecordRun overwrites it (for ov screen)
		GameOverReason = reason;
		IsRunning = false;

		 // In RecordRun, before calling Save.RecordRun:
        float minutesThisRun = (Time.Now - _sessionStartTime) / 60f;

		// base wave + any secret chest bonuses
		FragmentsEarned += Wave / 2; // wave 10 = 5 frags, wave 69 = 34 frags — not wave itself
		
		
		// Level 4 pet bonus — double frags on every milestone wave (10, 20, 30...)
		if (Save?.PetLevel >= 4 && Wave % 10 == 0)
			FragmentsEarned += Wave / 2; // doubles the wave/2 bonus earned this run
	
		// Best chain length (chain reaction)
		int previousChainLength = Save.BestChainLength;
		int newBestChainLength = 0;
		if (BestChainLength > previousChainLength) newBestChainLength = BestChainLength;
    
		Log.Info($"TriggerGameOver — FragmentsEarned: {FragmentsEarned}, Wave: {Wave}");

		// Record this run to persistent save
		Save.RecordRun(
			score:                 Score,
			wave:                  Wave,
			chestsCleared:         TotalChestsCleared,
			challengesCompleted:   ChallengesCompleted,
			fragmentsEarned:       FragmentsEarned,
			minutesPlayed: 		   minutesThisRun,
			bestChainLength:	   newBestChainLength
		);

		// Leaderboard results submission
		SubmitLeaderboard( Score, Wave, previousChainLength );
		
		TimerActive = false;
		OnScreenFlashRequest?.Invoke("gameover", 0);; // red flash screen
    	_ = ShowGameOverDelayed(); // delay game over
		Sound.Play("GameOver_1"); // game over trigger sound
	}
	
	async System.Threading.Tasks.Task ShowGameOverDelayed()
	{
		await GameTask.DelaySeconds(0.8f);   // let flash play first
		IsGameOver = true;
		Sound.Play("GameOver_2");
		OnStateChanged?.Invoke();
	}
	// ── Chest spawning ────────────────────────────────────────────────────
	int ChestsForWave(int wave)
	{
		int _base = wave < 3   ? 3
			: wave < 6         ? 4
			: wave < 10        ? 5
			: wave < 15        ? 6
			: wave < 20        ? 7
			: wave < 50        ? 8
			: wave < 100       ? 9 // grid expands 
			: wave < 150       ? 10 // grid expands 
			: wave < 300       ? 11
			: 12;              // 300+ — near-full 7×7 grid
		return Math.Min(_base + (GetDifficultyModifier() / 2), 14);
	}
	int MinChestsForWave(int wave)
	{
		int _base = wave < 15  ? 1
			: wave < 30        ? 2
			: wave < 75        ? 3
			: wave < 150       ? 4
			: wave < 300       ? 5
			: 6;               // 300+: must clear 6 minimum
		return Math.Min(_base + (GetDifficultyModifier() / 3), 7);
	}
	
	// Difficulty increases based on player in-game progression (modifiers)
	int GetDifficultyModifier()
	{
		// 0 = vanilla, 1 = slightly harder, 2 = harder
		return Save?.ActiveModifiers?.Count ?? 0;
	}
	int ComputeWaveTarget( int wave ) => 200 + wave * 150; // Score target for that wave 

	// Spawn a specific number of chests on grid cells (rule : it spawns chest ONLY in the toppest rows (the first 2 ones))
	void SpawnChests( int count )
	{
		// ── Use pre-rolled positions from Reveal CONSUMABLE, if available ── //
		if (_prerolledChests != null)
		{
			foreach (var (r, c, type) in _prerolledChests)
			{
				var cell = Grid.Grid[r, c];
				if (!cell.IsEmpty) continue; // safety check

				cell.Chest = type;
				if (type == GridManager.ChestType.Gem && HasMod(ModifierId.GemsOneHit))
					cell.HitsLeft = 1;
				else
					cell.HitsLeft = type == GridManager.ChestType.Gem ? 2 : 1;
			}
			_prerolledChests = null; // consume and clear
			Sound.Play("next_wave_spawn");
			return;
		}

		// ── Tutorial wave overrides ── //
		bool isTutorial = TutorialOverlay.Instance?.IsActive == true 
               || (Save?.TutorialCompleted == false && Wave == 1);

		if (isTutorial && Wave == 1)
		{
			// Force clear rows 0-1 completely before placing
			for (int r = 0; r < 2; r++)
				for (int c = 0; c < Grid.Cols; c++)
				{
					Grid.Grid[r, c].Chest    = GridManager.ChestType.None;
					Grid.Grid[r, c].HitsLeft = 0;
				}

			// Guaranteed gem at center top
			Grid.Grid[0, 2].Chest    = GridManager.ChestType.Gem;
			Grid.Grid[0, 2].HitsLeft = 2;

			// Fill rest with normals
			var emptyW1 = new List<GridManager.Cell>();
			for (int r = 0; r < 2; r++)
				for (int c = 0; c < Grid.Cols; c++)
					if (Grid.Grid[r, c].IsEmpty) emptyW1.Add(Grid.Grid[r, c]);
			Shuffle(emptyW1);

			int placed = 0;
			foreach (var cell in emptyW1)
			{
				if (placed >= count - 1) break;
				cell.Chest    = GridManager.ChestType.Normal;
				cell.HitsLeft = 1;
				placed++;
			}

			Sound.Play("next_wave_spawn");
			return;
		}

		if (isTutorial && Wave == 2)
		{
			// Force a cross of bomb-chests so any bomb placement triggers a 5+ chain
			// Pattern on a 5x5 grid:
			//   row 0: cols 1, 3
			//   row 1: cols 0, 2, 4
			//   Leaves cols (0,0),(0,2),(0,4),(1,1),(1,3) as empty for bomb placement
			//   Any placement adjacent to this pattern chains through all 5 bomb-chests

			var pattern = new (int r, int c)[]
			{
				(0, 1), (0, 3),
				(1, 0), (1, 2), (1, 4)
			};

			foreach (var (r, c) in pattern)
			{
				var cell = Grid.Grid[r, c];
				if (!cell.IsEmpty) continue;
				cell.Chest    = GridManager.ChestType.BombChest;
				cell.HitsLeft = 1;
			}

			// Fill a few normal chests in row 2 so the grid doesn't look empty
			Grid.Grid[2, 1].Chest    = GridManager.ChestType.Normal;
			Grid.Grid[2, 1].HitsLeft = 1;
			Grid.Grid[2, 3].Chest    = GridManager.ChestType.Normal;
			Grid.Grid[2, 3].HitsLeft = 1;

			Sound.Play("next_wave_spawn");
			return;
		}

		// --------------------- Bomb chest cap — scales with grid size and wave ------------ //
		int gridCells    = Grid.Rows * Grid.Cols;
		int existingBombChests = CountBombChestsOnGrid();

		// Base cap: roughly 20-30% of grid cells, scales up with grid size
		// 5x5=25 cells → cap ~5-6, 6x6=36 → cap ~8-9, 7x7=49 → cap ~12
		float capRatio = Wave switch {
			< 10  => 0.12f,   // early: very few bomb chests allowed
			< 20  => 0.16f,
			< 35  => 0.20f,
			< 60  => 0.24f,
			< 100 => 0.28f,
			< 200 => 0.32f,
			_     => 0.36f    // 300+: still capped, just more room on bigger grid
		};
		int bombChestCap = (int)MathF.Max(2f, gridCells * capRatio);
		// ----------------------------------------------------------------------------------- //

		// normal spawn 
		var empty = new List<GridManager.Cell>(); // create a new List of Cells (from GridManager's definition);

		// ===================================================================================== //
		// =========================== CHEST SPAWN ROW RULES ================================== //
		// ===================================================================================== //
		// Spawn rows scale with wave — higher waves spawn chests deeper into the grid (new system)
		int spawnRows = Wave switch {
			>= 200 => 4,  // rows 0-3
			>= 100 => 3,  // rows 0-2
			_      => 2   // rows 0-1 (default)
		};

		for (int r = 0; r < spawnRows; r++)
			for (int c = 0; c < Grid.Cols; c++)
				if (Grid.Grid[r, c].IsEmpty) empty.Add(Grid.Grid[r, c]);
		
		// 1. Collect all Cells in the first two top-row of GridManager's (Row 0 & Row 1 only) (old system)
		// for ( int r = 0; r < 2; r++ ) // nested loop explanation :  a. it scans a first row (r=0)
		// 	for ( int c = 0; c < Grid.Cols; c++ ) // b. in that row, we scan every cols (c < Grid.Cols). Once it's done, it goes up to increment r++ & scan second row (r=1).
		// 		if ( Grid.Grid[r, c].IsEmpty ) empty.Add( Grid.Grid[r, c] ); // 2. adds ONLY grid cells (coords) that are EMPTY 
		// ===================================================================================== //

		// 3. shuffle that, so chest placement are randomized (not picked in order according to the grid's empty cells collection)
		Shuffle( empty );

		// ── Progressive placement bias ──
		// Early waves: cluster chests together so accidental chains happen
		// Late waves: spread chests apart so you need to plan
		if (Wave >= 8)
		{
			empty.Sort((a, b) =>
			{
				float scoreA = GetPlacementScore(a.Row, a.Col);
				float scoreB = GetPlacementScore(b.Row, b.Col);
				return scoreB.CompareTo(scoreA);
			});
		}

		// ── Modifier: bomb chests near normal chests ──
		if (HasMod(ModifierId.BombChestsNearChest))
		{
			empty.Sort((a, b) => {
				int scoreA = AdjacentNormalChestCount(a.Row, a.Col);
				int scoreB = AdjacentNormalChestCount(b.Row, b.Col);
				return scoreB.CompareTo(scoreA);
			});
		}

		// Modifier: bomb chests always spawn near a normal chest
		if(HasMod(ModifierId.BombChestsNearChest))
		{
			empty.Sort((a, b) => {
				int scoreA = AdjacentNormalChestCount(a.Row, a.Col);
				int scoreB = AdjacentNormalChestCount(b.Row, b.Col);
				return scoreB.CompareTo(scoreA); // descending
			});
		}

		// 4. Min(x,y) is added so that no matter what, it never loops more than the actual free grid Cells available (bc empty)
		for(int i = 0; i < MathF.Min(count, empty.Count); i++)
		{
			var type = RollChestType(Wave);

			// Enforce bomb chest cap — demote to normal if over cap
			if (type == GridManager.ChestType.BombChest && 
				existingBombChests >= bombChestCap)
			{
				type = GridManager.ChestType.Normal;
			}

			// NERF bomb chest specifically to avoid too big clusters at higher waves
			if (type == GridManager.ChestType.BombChest)
			{
				if (Wave >= 25 && TooCloseToExistingBombChest(empty[i].Row, empty[i].Col))
					type = GridManager.ChestType.Normal; // demote if too close
				else
					existingBombChests++; // track as we place
			}

			empty[i].Chest = type;

			// 1. Base hits for chest type
			int baseHits = type switch 
			{
				GridManager.ChestType.Gem    => 2, // 2 hits
				GridManager.ChestType.Locked => 3, // 3 hits
				_                            => 1
			};

			// Level 3 pet bonus — multi-hit chests spawn pre-damaged
			if (Save?.PetLevel >= 3 && baseHits > 1)
				baseHits--;

			// 2. Modifier — GemsOneHit reduces gem hits to 1 (always wins)
			if (type == GridManager.ChestType.Gem && HasMod(ModifierId.GemsOneHit))
				baseHits = 1;

			// 3. Cursed wave event — adds extra hits on top of final base
			// Applied AFTER modifier so GemsOneHit + CursedWave = 2 hits (not 3)
			empty[i].HitsLeft = baseHits + ExtraHitsRequired;
		}

		// Spawn chest sfx
		Sound.Play("next_wave_spawn");
	}

	// Returns a chest type to add, based on Wave number (rules defined below)
	GridManager.ChestType RollChestType(int wave)
	{
		// Gold Rush event — only gems this wave
		if (_goldRushActive == true)
			return GridManager.ChestType.Gem;
			
		int roll = Rng.Next(100);

		if(wave < 4)
			// gems appear, no bomb chests yet
			return GridManager.ChestType.Normal; // waves 1-3: ALL normal, no gems, no bombs
			// return roll < 70 ? GridManager.ChestType.Normal : GridManager.ChestType.Gem;

		if(wave < 7)
		// first bomb chests — rare, exciting when they appear
			return roll < 55 ? GridManager.ChestType.Normal 
				: roll < 80 ? GridManager.ChestType.Gem 
				:              GridManager.ChestType.BombChest; // ~20% chance

		if(wave < 10)
		// bomb chests getting more common
			return roll < 45 ? GridManager.ChestType.Normal 
				: roll < 70 ? GridManager.ChestType.Gem 
				:              GridManager.ChestType.BombChest; // ~30% chance

		if(wave < 20)
			// bomb chests common but not dominant
			return roll < 45 ? GridManager.ChestType.Normal 
				: roll < 70 ? GridManager.ChestType.Gem 
				:              GridManager.ChestType.BombChest;

		if(wave < 35)
			// balanced chaos
			return roll < 35 ? GridManager.ChestType.Normal 
				: roll < 60 ? GridManager.ChestType.Gem 
				:              GridManager.ChestType.BombChest;

		 // Wave 75+ — Locked chests appear
    if (wave >= 75 && wave < 100)
        return roll < 20 ? GridManager.ChestType.Locked
             : roll < 45 ? GridManager.ChestType.Normal
             : roll < 65 ? GridManager.ChestType.Gem
             :              GridManager.ChestType.BombChest;

    // Wave 100+ — Cursed chests appear
    if (wave >= 100 && wave < 200)
        return roll < 15 ? GridManager.ChestType.Locked
             : roll < 25 ? GridManager.ChestType.Cursed
             : roll < 45 ? GridManager.ChestType.Normal
             : roll < 65 ? GridManager.ChestType.Gem
             :              GridManager.ChestType.BombChest;

    // Wave 200+ — Void chests appear
    if (wave >= 200)
        return roll < 10 ? GridManager.ChestType.Void
             : roll < 20 ? GridManager.ChestType.Cursed
             : roll < 30 ? GridManager.ChestType.Locked
             : roll < 50 ? GridManager.ChestType.Gem
             :              GridManager.ChestType.BombChest;

		// late game — bomb chests significant but gems still matter
		return roll < 25 ? GridManager.ChestType.Normal 
			: roll < 50 ? GridManager.ChestType.Gem 
			:              GridManager.ChestType.BombChest;
	}
	// Helper for Modifier: bomb chests 
	// Answer question : "how many normal chests are directly adjacent to this cell?"
	int AdjacentNormalChestCount(int r, int c)
	{
		int count = 0;
	    int[] dr = {-1, 1, 0, 0};  // row offsets: up, down, stay, stay
    	int[] dc = {0,  0, -1, 1};  // column offsets: stay, stay, left, right

		/* directions (when looping i < 4):
			d=0: dr=-1, dc=0 → one row up, same column → up
			d=1: dr=1,  dc=0 → one row down, same column → down
			d=2: dr=0,  dc=-1 → same row, one column left → left
			d=3: dr=0,  dc=1 → same row, one column right → right
		*/

		for(int d = 0; d < 4; d++)
		{
			int nr = r + dr[d];   // new row = current row + direction offset
        	int nc = c + dc[d];   // new col = current col + direction offset

			if(Grid.InBounds(nr, nc) && // neighbour exists on the grid
			Grid.Grid[nr, nc].Chest == GridManager.ChestType.Normal) // neighbour has a normal chest
					count++;
		}
		return count;  // how many of the 4 neighbours are normal chests -> determines a score 
	}

		// ===== RANGE BONUS MODIFIER ==== //
	public int GetRangeBonus(GridManager.BombType type)
	{
		// Sniper bomb +1 range anytime it's equipped
		if(type == GridManager.BombType.Sniper && Instance?.HasMod(ModifierId.SniperRange) == true)
			return 1;

		// Wave 5 and more => we have +1 range on cross bomb
		if(type == GridManager.BombType.Cross && Instance?.HasMod(ModifierId.CrossRangeAfterWave5) == true && (Instance?.Wave ?? 0) >= 5)
			return 1;

		return 0;
	}

	// --------------------------------------------------------------- //
	// ------------------------- SECRET CHEST ------------------------ //
	// --------------------------------------------------------------- //
	bool HasSecretChestOnGrid()
	{
		for ( int r = 0; r < Grid.Rows; r++ )
			for ( int c = 0; c < Grid.Cols; c++ )
				if ( Grid.Grid[r, c].Chest == GridManager.ChestType.Secret ) return true;
		return false;
	}

	void TrySpawnSecretChest()
	{
		if ( _secretChestsSpawnedThisRun >= 3 ) return;  // max 3 per run
		if ( HasSecretChestOnGrid() ) return;             // max 1 on grid at once
		if ( Rng.Next( 100 ) >= 5 ) return;              // 8% chance

		// Only spawn in rows 2-3 (middle of grid)
		// avoids row 0-1 (normal chest spawn zone) and row 4 (bottom / danger zone)
		var candidates = new List<GridManager.Cell>();
		for ( int r = 2; r < Grid.Rows - 1; r++ )
			for ( int c = 0; c < Grid.Cols; c++ )
				if ( Grid.Grid[r, c].IsEmpty ) candidates.Add( Grid.Grid[r, c] );

		if ( candidates.Count == 0 ) return;

		Shuffle( candidates );
		var cell = candidates[0];
		cell.Chest = GridManager.ChestType.Secret;
		cell.HitsLeft = 1;
		_secretChestsSpawnedThisRun++;

		MilestoneOverlay.Instance?.ShowMessage("✨ A Secret Chest appeared!", Wave);
		Sound.Play("secret_chest_alert");
	}

	void Shuffle<T>( List<T> list )
	{
		for ( int i = list.Count - 1; i > 0; i-- )
		{ int j = Rng.Next( i + 1 ); (list[i], list[j]) = (list[j], list[i]); }
	}


	// --------------------------------------------------------------- //
	// -------------------------- BOSS CHEST ------------------------- //
	// --------------------------------------------------------------- //

	public bool BossAlive => _bossAlive;
	bool _bossAlive = false;
	int _bossMaxHp = 5; // set when boss spawns
	public int BossMaxHp { get; private set; } = 5;
	int _bossSpawnedWave = -1;
	void TrySpawnBossChest(int baseBossHp = 1)
	{
		    Log.Info($"TrySpawnBossChest — wave={Wave}, bossAlive={_bossAlive}, wave%10={Wave%10}");

		// Random Event - Boss Rush (Active ?)
		if (_bossRushActive)
		{
			// Spawn 3 mini-bosses with 2 HP each in different positions
			int spawned = 0;
			for (int r = 0; r < Grid.Rows && spawned < 3; r++)
				for (int c = 0; c < Grid.Cols && spawned < 3; c++)
					if (Grid.Grid[r, c].IsEmpty)
					{
						Grid.Grid[r, c].Chest    = GridManager.ChestType.Boss;
						Grid.Grid[r, c].HitsLeft = 2; // mini version
						_bossAlive = true;
						spawned++;
					}
			_bossRushActive = false;
			OnScreenFlashRequest?.Invoke("boss", Wave);
			return;
		}

		if(Wave % 10 != 0) return;   // only on wave 10, 20, 30...
		if(_bossAlive) return;      // boss already on grid (one)

		_bossSpawnedWave = Wave;

		// Hp's increments based on wave (difficulty up!)
		int bossHp = Wave switch
		{
			<= 10 => 3, // wave 10 - easy 
			<= 20 => 5,	// wave 20 - less easy
			<= 30 => 7, // wave 30 - difficult
			<= 40 => 9, // wave 40 - more difficult..
			_ 	  => 10 + (Wave / 10) - 4 // i.e : wave 50=11 (incremental)
		};

		// Compute move cooldown based on current wave for the announcement
		int moveCooldown = Wave switch {
			>= 200 => 1,
			>= 150 => 1,
			>= 100 => 2,
			>= 50  => 3,
			_      => 3
		};

		// Spawn in center of grid (row 2, col 2 on a 5x5)
		var center = Grid.Grid[2, 2];
		if (!center.IsEmpty) 
		{
			for (int r = 1; r <= 3; r++)
				for (int c = 0; c < Grid.Cols; c++)
					if (Grid.Grid[r, c].IsEmpty)
					{
						Grid.Grid[r, c].Chest    = GridManager.ChestType.Boss;
						Grid.Grid[r, c].HitsLeft = bossHp; // ← use correct HP
						_bossMaxHp = bossHp;
						BossMaxHp  = bossHp;
						_bossAlive = true;

						MilestoneOverlay.Instance?.ShowBoss(Wave, bossHp, moveCooldown); // ← show milestone
						OnScreenFlashRequest?.Invoke("boss", Wave);
						Sound.Play("boss_alert");
						return;
					}
			return;
		}	

		// event
		MilestoneOverlay.Instance?.ShowBoss(Wave, bossHp, moveCooldown);

		// Initialize Boss Chest Cell - 
		center.Chest = GridManager.ChestType.Boss;
		center.HitsLeft = bossHp;
		_bossMaxHp = bossHp;
		BossMaxHp = bossHp;
		_bossAlive = true;
		OnScreenFlashRequest?.Invoke("boss", Wave); // Screen flash 
		Sound.Play("boss_alert"); // Spawn chest sfx
	}
	public void NotifyBossKilled()
	{
		_bossAlive   = false;

		int scoreBonus = 500 * (Wave / 10);      // wave 10=500, 20=1000, 30=1500...
    	int fragBonus  = 10 + (Wave / 10) * 5;   // wave 10=15, 20=20, 30=25...

		FragmentsEarned += 15;
		BossFragments += 15;
		OnBossKilled?.Invoke(scoreBonus, fragBonus);
		OnStateChanged?.Invoke();

		// Spawn chest sfx
		Sound.Play("destroy_boss");
	}

	void MoveBossDown()
	{
		// Find boss position first
		int bossR = -1, bossC = -1;
		for(int r = 0; r < Grid.Rows; r++)
			for(int c = 0; c < Grid.Cols; c++)
				if(Grid.Grid[r, c].Chest == GridManager.ChestType.Boss)
				{ bossR = r; bossC = c; break; }

		if(bossR < 0) return; // no boss found

		// Boss in last row already — game over
		if(bossR >= Grid.Rows - 1)
		{
			TriggerGameOver("👑 Boss reached the bottom row. Are you gonna give up now ?😏");
			return;
		}

		// Try to move down
		var below = Grid.Grid[bossR + 1, bossC];
		if(below.IsEmpty)
		{
			below.Chest    = GridManager.ChestType.Boss;
			below.HitsLeft = Grid.Grid[bossR, bossC].HitsLeft;
			Grid.Grid[bossR, bossC].Chest    = GridManager.ChestType.None;
			Grid.Grid[bossR, bossC].HitsLeft = 0;
			OnStateChanged?.Invoke();
		}
		else
		{
			// Cell below occupied — boss pushes chest off the grid (boss is heavy) 
			// Or alternative: do nothing this wave, try again next time
			// yeah for now "do nothing" since pushing chests off is weird
		}
	}

	// ------------------------------------------------------------- //
	// ------------------------- CHALLENGES ------------------------ //
	// ------------------------------------------------------------- //

	WaveChallenge RollChallenge(int wave)
	{
		var types = new List<ChallengeType>();

		// Always available
		types.Add(ChallengeType.ClearAtLeast);
		types.Add(ChallengeType.ChainLength);

		// Unlocks progressively
		if(wave >= 3)  types.Add(ChallengeType.ClearGem);
		if(wave >= 5)  types.Add(ChallengeType.NoBombType);
		if(wave >= 4 && Grid.AllChestCells().Count >= 3) types.Add(ChallengeType.ClearInOneBlast);
		if(wave >= 6)  types.Add(ChallengeType.ClearBombChest);

		// High wave exclusive challenges
		if(wave >= 10) types.Add(ChallengeType.ClearAll);      // clear every chest on board
		if(wave >= 15) types.Add(ChallengeType.ChainDepth3);   // trigger a 3-depth chain
		if(wave >= 20) types.Add(ChallengeType.NoDamage);      // clear MinChests without losing life

		var type = types[Rng.Next(types.Count)];

		// Bonus scales with wave AND difficulty tier
		int basebonus = 200 + wave * 80;
		int bonus = type switch
		{
			ChallengeType.ClearAll      => basebonus * 2,
			ChallengeType.ChainDepth3   => (int)(basebonus * 1.5f),
			ChallengeType.NoDamage      => (int)(basebonus * 1.8f),
			ChallengeType.NoBombType    => (int)(basebonus * 1.3f),
			ChallengeType.ClearInOneBlast => (int)(basebonus * 1.4f),
			_ => basebonus
		};

		// ClearAtLeast target scales with wave
		int clearTarget = wave < 5 ? 2 : wave < 10 ? 3 : wave < 20 ? 4 : 5;

		return type switch
		{
			ChallengeType.ClearAtLeast => new WaveChallenge {
				Type = type, Target = clearTarget,
				Label = $"Clear {clearTarget}+ chests", BonusCoins = bonus },

			ChallengeType.ChainLength => new WaveChallenge {
				Type = type, Target = 2,
				Label = "Trigger a chain reaction", BonusCoins = bonus },

			ChallengeType.ClearGem => new WaveChallenge {
				Type = type, Target = 1,
				Label = "Destroy a Gem chest 💎", BonusCoins = bonus + 100 },

			ChallengeType.NoBombType => new WaveChallenge {
				Type = type,
				ForbiddenBomb = Hand.BombAt(Rng.Next(Hand.HandSize)),
				Label = $"Clear without using {BombLabel(Hand.BombAt(0))}", BonusCoins = bonus + 150 },

			ChallengeType.ClearInOneBlast => new WaveChallenge {
				Type = type, Target = 1,
				Label = "Clear chests with 1 bomb only", BonusCoins = bonus + 200 },

			ChallengeType.ClearBombChest => new WaveChallenge {
				Type = type,
				Label = "Detonate a Bomb Chest 💣", BonusCoins = bonus + 100 },

			ChallengeType.ClearAll => new WaveChallenge {
				Type = type, Target = _chestsAtWaveStart,
				Label = "Clear ALL chests this wave", BonusCoins = bonus },

			ChallengeType.ChainDepth3 => new WaveChallenge {
				Type = type,
				Label = "Trigger a 3-chain reaction", BonusCoins = bonus },

			ChallengeType.NoDamage => new WaveChallenge {
				Type = type,
				Label = "Clear without losing a life", BonusCoins = bonus },

			_ => new WaveChallenge {
				Type = ChallengeType.ClearAtLeast, Target = 2,
				Label = "Clear 2+ chests", BonusCoins = basebonus }
		};
	}

	static string BombLabel( 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 ⛓",
			_ => "?"
	};

	// Changes grid color every x waves and then to add a sense of progression
	public string WaveGridColor => (Wave / 5) switch
	{
		0 => "#0d0820",   // waves 1-4:  deep purple
		1 => "#08101f",   // waves 5-9:  deep blue
		2 => "#081a12",   // waves 10-14: deep teal
		3 => "#1a0808",   // waves 15-19: deep red
		4 => "#1a1208",   // waves 20-24: deep amber
		_ => "#0d0820"    // cycle back
	};

	public string WaveParticleColor
	{
		get
		{
			int tier = (Wave / 5) % 6;
			return tier switch {
				0 => "rgba(167,139,250,",
				1 => "rgba(52,211,153,",
				2 => "rgba(251,191,36,",
				3 => "rgba(244,114,182,",
				4 => "rgba(96,165,250,",
				_ => "rgba(251,146,60,"
			};
		}
	}
	// Happens to open Secret chests on GameOver screen
	void RollSecretChestReward()
	{
		int roll = Rng.Next( 100 );

		SecretChestReward reward;

		if ( roll < 70 )  // Common — coins
		{
			int coins = Rng.Next( 50, 151 );
			reward = new SecretChestReward {
				Icon = "🪙", Label = $"+{coins} coins",
				Coins = coins, Fragments = 0, IsRare = false
			};
			Score += coins;
		}
		else if ( roll < 95 )  // Uncommon — fragments
		{
			int frags = Rng.Next( 3, 9 );
			reward = new SecretChestReward {
				Icon = "💠", Label = $"+{frags} fragments",
				Coins = 0, Fragments = frags, IsRare = false
			};
			FragmentsEarned += frags;
			Log.Info($"Secret reward rolled: +{frags} frags, FragmentsEarned now: {FragmentsEarned}");

		}
		else  // Rare — run modifier
		{
			reward = new SecretChestReward {
				Icon = "🌟", Label = "RARE DROP!",
				Coins = 0, Fragments = 15, IsRare = true
			};
			FragmentsEarned += 15;
			
		}
		SecretChestRewards.Add( reward );
	}

	// -------------------------------------------------------- //
	// ------------------- REACTOR (IDLE) --------------------- //
	// -------------------------------------------------------- //

	/// <summary>
	/// Hands control to IdleReactor. Wave state is deliberately left inert —
	/// IsRunning stays false so the wave HUD, bomb wheel and hype meter all
	/// hide themselves without needing mode checks scattered everywhere.
	/// </summary>
	void StartIdleMode()
	{
		IsRunning   = false;
		IsGameOver  = false;
		TimerActive = false;
		IsPaused    = false;

		Score      = 0;
		Wave       = 1;
		GameOverReason = "";

		// The shared grid is not used by the reactor — wipe it so nothing stale
		// can flash on screen during the transition.
		Grid?.ClearAllChests();

		IdleReactor.Instance?.Begin();
		OnStateChanged?.Invoke();
	}

	// -------------------------------------------------------- //
	// ----------------- Return to Main Menu ------------------ // 
	// -------------------------------------------------------- //
	public void ReturnToMenu()
	{
		// Reactor writes its LastSeen stamp here so offline earnings start ticking
		if (Mode == GameMode.Idle)
			IdleReactor.Instance?.End();

		OnGameReturned?.Invoke();
		GameStarted = false;
		IsGameOver  = false;
		IsRunning   = false;
		OnStateChanged?.Invoke();
	}

	// -------------------------------------------------------- //
	// ----------------- TUTORIAL FOR 1st GAME ---------------- // 
	// -------------------------------------------------------- //

	public void PauseTimer()
	{
		TimerActive = false;
		Log.Info($"PauseTimer called — TimerActive now false");
	}

	public void ResumeTimer() => TimerActive = true;

	// ────────────────────────────────────────────────────────────────
	// ────────────────────── Consumable effects ──────────────────────
	// ────────────────────────────────────────────────────────────────
	// FuseExtension — adds time to current wave
	public void AddTime(float seconds)
	{
		TurnTime = MathF.Min(TurnTime + seconds, _maxTurnTime); // cap at max, don't extend max (so it visually change the bar)
		OnStateChanged?.Invoke();
	}

	// ColumnSweep — destroys all chests in the most dangerous column
	public void SweepDangerColumn()
	{
		// Find column with most chests
		int bestCol = -1, bestCount = 0;
		for (int c = 0; c < Grid.Cols; c++)
		{
			int count = 0;
			for (int r = 0; r < Grid.Rows; r++)
				if (Grid.Grid[r, c].HasChest) count++;
			if (count > bestCount) { bestCount = count; bestCol = c; }
		}

		if (bestCol < 0) return;

		for (int r = 0; r < Grid.Rows; r++)
		{
			var cell = Grid.Grid[r, bestCol];
			if (!cell.HasChest) continue;
			cell.Chest = GridManager.ChestType.None;
			cell.HitsLeft = 0;
			TotalChestsCleared++;
		}
		OnStateChanged?.Invoke();
	}

	// Heart consumable
	public bool UseHeartConsumable()
	{
		if (Lives >= MaxLives) return false;
		if (Save?.GetConsumableCount(ConsumableId.Heart) <= 0) return false;
		Save.ConsumeConsumable(ConsumableId.Heart);
		Lives++;
		OnStateChanged?.Invoke();
		return true;
	}

	// ───────── Overdrive ───────────────────────────────────────────
	// — next detonation multiplier is doubled
	public bool OverdriveActive { get; private set; } = false;
	public void ActivateOverdrive()
	{
		OverdriveActive = true;
		OnStateChanged?.Invoke();
	}
	public void ConsumeOverdrive() // called in HandleDetonationFinished
	{
		OverdriveActive = false;
	}
	

	// ───────── ChainNuke (removed for now) ──────────────────────────
	// ChainNuke — force-links all placed bombs to a chain reaction
	public void ActivateChainNuke()
	{
		if (!Hand.AnyPlaced) return;
		TimerActive = false;
		Hand.ExecuteDetonateWithNuke();
	}

	// ── Reveal consumable ─────────────────────────────────────────
	// Reveal — shows next wave chest positions for 3 seconds
	public bool RevealActive { get; private set; } = false;
	public List<(int r, int c, GridManager.ChestType type)> RevealedPositions { get; private set; } = new();

	// Stored pre-rolled chest positions from Reveal consumable
	// null = no reveal active, use normal spawn
	List<(int r, int c, GridManager.ChestType type)> _prerolledChests = null;

	public async System.Threading.Tasks.Task RevealNextWave()
	{
		RevealedPositions.Clear();
		_prerolledChests = new();

		var empty = new List<GridManager.Cell>();
		for (int r = 0; r < 2; r++)
			for (int c = 0; c < Grid.Cols; c++)
				if (Grid.Grid[r, c].IsEmpty) empty.Add(Grid.Grid[r, c]);

		Shuffle(empty);

		int count = ChestsForWave(Wave + 1);
		for (int i = 0; i < MathF.Min(count, empty.Count); i++)
		{
			var type = RollChestType(Wave + 1);
			var cell = empty[i];
			_prerolledChests.Add((cell.Row, cell.Col, type));
			RevealedPositions.Add((cell.Row, cell.Col, type));
		}

		RevealActive = true;
		OnStateChanged?.Invoke();

		await GameTask.DelaySeconds(3f);

		RevealActive = false;
		// Keep _prerolledChests — it gets consumed in SpawnChests next wave
		OnStateChanged?.Invoke();
	}


	// ── ClusterDrop Consumable ─────────────────────────────────────────
	// ClusterDrop — spawns 3 bomb chests in optimal positions
	public void ActivateClusterDrop()
	{
		// Find empty cells in rows 0-3, score by adjacency to existing chests
		var candidates = new List<(int r, int c, int score)>();
		for (int r = 0; r < Grid.Rows - 1; r++)
			for (int c = 0; c < Grid.Cols; c++)
				if (Grid.Grid[r, c].IsEmpty)
				{
					int score = AdjacentNormalChestCount(r, c);
					candidates.Add((r, c, score));
				}

		// Sort by adjacency score descending — place near existing chests
		candidates.Sort((a, b) => b.score.CompareTo(a.score));

		int placed = 0;
		foreach (var (r, c, _) in candidates)
		{
			if (placed >= 3) break;
			var cell = Grid.Grid[r, c];
			if (!cell.IsEmpty) continue;
			cell.Chest = GridManager.ChestType.BombChest;
			cell.HitsLeft = 1;
			placed++;
		}

		Sound.Play("next_wave_spawn");
		OnStateChanged?.Invoke();
	}

	// ── Overload Consumable ─────────────────────────────────────────
	// Overload — ignore min chest requirement this wave
	public bool OverloadActive { get; private set; } = false;
	public void ActivateOverload()
	{
		OverloadActive = true;
		OnStateChanged?.Invoke();
	}

	// ── Random Events ──────────────────────────────────────────── //
	public RandomEventType? CurrentRandomEvent { get; private set; } = null;
	public bool RandomEventActive => CurrentRandomEvent.HasValue;
	float _timerFrozenFor = 0;

	bool _doubleDropActive = false;
	bool _goldRushActive = false;
	bool _bonusWaveActive = false;
	bool _ghostBombsActive = false;
	bool _mysteryWaveActive = false;
	bool _bombJamActive = false;
	bool _bossRushActive = false;
	int _extraHitsRequired = 0;
	bool _chainMasterFiredThisRun = false; 

	public bool IsDoubleDropActive  => _doubleDropActive;
	public bool IsGoldRushActive    => _goldRushActive;
	public bool IsBonusWaveActive   => _bonusWaveActive;
	public bool IsGhostBombsActive  => _ghostBombsActive;
	public bool IsMysteryWaveActive => _mysteryWaveActive;
	public bool IsBombJamActive     => _bombJamActive;
	public int  ExtraHitsRequired   => _extraHitsRequired;

	// ── Player Behavior tracking ─────────────────────────────────────────
	int _consecutiveWavesNoLifeLoss = 0;
	int _livesAtWaveStart           = 0;
	int _consecutiveFailedWaves = 0;

	// Wave ranges that guarantee a random event fires
	static readonly (int min, int max)[] EventWindows = {
		(8,  12),  (18, 23),  (30, 40),  (50, 65),
		(80, 100), (120,145), (170,200), (240,280),
		(320,370), (420,480)
	};

	int _lastEventWave = 0;

	// Mirror random event type
	bool _mirrorWaveActive = false;
	public bool IsMirrorWaveActive => _mirrorWaveActive;

	void TryTriggerRandomEvent(int wave)
	{
		// Check if we're in an event window and haven't fired one recently
		foreach (var (min, max) in EventWindows)
		{
			if (wave < min || wave > max) continue;
			if (_lastEventWave >= min) continue; // already fired in this window

			// Roll — 60% chance per wave within the window
			// Guaranteed on last wave of window
			bool guaranteed = wave == max;
			if (!guaranteed && Rng.Next(100) >= 60) continue;

			FireRandomEvent(wave);
			_lastEventWave = wave;
			return;
		}

		// Player behavior triggers
		TryTriggerBehaviorEvent(wave);
	}

	void FireRandomEvent(int wave)
	{
		// Weight positive vs negative based on wave (and player's lives)
		var pool = BuildEventPool(wave);
		var type = pool[Rng.Next(pool.Count)];
		CurrentRandomEvent = type;

		// Show milestone overlay with event description
		MilestoneOverlay.Instance?.ShowEvent(type, wave);
	}

	List<RandomEventType> BuildEventPool(int wave)
	{
		var pool = new List<RandomEventType>();

		// Always available positives
		pool.Add(RandomEventType.BonusWave);
		pool.Add(RandomEventType.FragmentRain);
		pool.Add(RandomEventType.TimerFreeze);
		pool.Add(RandomEventType.DoubleDrop);
		pool.Add(RandomEventType.ChestBonanza);

		// Unlocks as waves progress
		if (wave >= 15) pool.Add(RandomEventType.GoldRush);
		if (wave >= 25) pool.Add(RandomEventType.MysteryWave);
		if (wave >= 30) pool.Add(RandomEventType.GhostBombs);
		if (wave >= 45) pool.Add(RandomEventType.MirrorWave);

		// Negatives unlock later — player needs to be ready
		if (wave >= 20) pool.Add(RandomEventType.CursedWave);
		if (wave >= 30) pool.Add(RandomEventType.SpeedRound);
		//if (wave >= 40) pool.Add(RandomEventType.FogOfWar);
		if (wave >= 50) pool.Add(RandomEventType.BombJam);

		// Interesting ones
		if (wave >= 35) pool.Add(RandomEventType.BossRush);
		if (wave >= 45) pool.Add(RandomEventType.MirrorWave);

		// If player is struggling (lost life this wave), bias toward positive
		if (Lives < StartLives)
		{
			pool.Add(RandomEventType.BonusWave);
			pool.Add(RandomEventType.TimerFreeze);
			pool.Add(RandomEventType.FragmentRain);
		}

		return pool;
	}

	void TryTriggerBehaviorEvent(int wave)
	{
		// Chain Master — player just did a massive chain
		// (checked after detonation in HandleDetonationFinished)
		// Lucky Streak — 5 waves no life loss
		// etc — these fire instantly, no window needed

		// ── Lucky Streak — 5 consecutive waves without losing a life ── //
		// Only fires once per streak, not every wave after 5
		if (_consecutiveWavesNoLifeLoss == 5)
		{
			_consecutiveWavesNoLifeLoss = 0; // reset counter

			if(!_luckyStreakFiredThisRun)
			{
				//MilestoneOverlay.Instance?.ShowEvent(RandomEventType.FragmentRain, wave);
				_luckyStreakFiredThisRun = true;
				_lastEventWave = wave;
				CurrentRandomEvent = RandomEventType.FragmentRain;
				ApplyRandomEvent(); // instant effect
				MilestoneOverlay.Instance?.ShowMessage("🍀 Lucky Streak! +5 💠", Wave);
				return;
			}
		}

		// ── Chain Master — cleared 8+ chests last detonation ── //
		// -> Only fires once, then resets
		if ((Hand?.LastSimResult?.ClearedCount ?? 0) >= 8 && !_chainMasterFiredThisRun)
		{
			_chainMasterFiredThisRun = true; 
			MilestoneOverlay.Instance?.ShowEvent(RandomEventType.DoubleDrop, wave);
			// Don't apply yet — applied after overlay dismissed
			_lastEventWave = wave;
			CurrentRandomEvent = RandomEventType.DoubleDrop;
			Sound.Play("DoubleDrop_Coins");
			return;
		}

		// ── Mercy Wave — failed MinChests 2 waves in a row ── //
		// (tracked separately — see below)
		// => struggling player gets a bonus ── //
		if (_consecutiveFailedWaves >= 2)
		{
			_consecutiveFailedWaves = 0;
			MilestoneOverlay.Instance?.ShowEvent(RandomEventType.BonusWave, wave);
			Sound.Play("BonusWave");
			// Don't apply yet — try apply on next wave
			_lastEventWave  = wave;
			_bonusWaveActive = true;
			CurrentRandomEvent = RandomEventType.BonusWave;
		}
	}

	// Call at start of each wave in HandleDetonationFinished
	void ApplyRandomEvent()
	{
		if (!CurrentRandomEvent.HasValue) return;

		switch (CurrentRandomEvent.Value)
		{
			 case RandomEventType.FragmentRain:
            	FragmentsEarned += 5; 
				BonusEventFragments += 5;
				break;

			case RandomEventType.MysteryWave:
				_mysteryWaveActive = true;
				break;

			case RandomEventType.TimerFreeze:
				_timerFrozenFor = 8f; // count down in OnUpdate
				break;

			case RandomEventType.DoubleDrop:
				_doubleDropActive = true;
				break;

			case RandomEventType.CursedWave:
				_extraHitsRequired = 1; // all chests need +1 hit
				break;

			case RandomEventType.SpeedRound:
				TurnTime *= 0.5f;
				_maxTurnTime *= 0.5f;
				break;

			case RandomEventType.ChestBonanza:
				SpawnChests(3); // 3 extra chests on top of normal
				break;

			case RandomEventType.GoldRush:
				_goldRushActive = true; // flag checked in SpawnChests
				break;

			case RandomEventType.BombJam:
            _bombJamActive = true;
            break;

			case RandomEventType.BossRush:
			_bossRushActive = true; // flag checked in TrySpawnBossChest
			break;
			
			// BonusWave event — all chests worth 3×
			// in ExecuteDetonate() -> float bonusMult  = ChainReactionGame.Instance?.IsBonusWaveActive == true ? 3f : 1f;
			case RandomEventType.BonusWave: // if player failed chests requirements 2 times in a row 
			_bonusWaveActive = true;
			break;

			case RandomEventType.MirrorWave:
			_mirrorWaveActive = true;
			ApplyMirrorWave();
			break;
		}
	}

	// Reset event at end of wave
	void ClearRandomEvent()
	{
		 CurrentRandomEvent   = null;
		_doubleDropActive     = false;
		_goldRushActive       = false;
		_bonusWaveActive      = false;
		_ghostBombsActive     = false;
		_mysteryWaveActive    = false;
		_bombJamActive        = false;
		_bossRushActive       = false;
		_mirrorWaveActive 	  = false;
		_extraHitsRequired    = 0;
		_timerFrozenFor       = 0f;
		// note : here we don't reset _consecutiveWavesNoLifeLoss or _consecutiveFailedWaves
		// — those track across waves intentionally
	}

	// Mirror event 
	void ApplyMirrorWave()
	{
		// Mirror all existing chest positions left ↔ right
		// Step 1 — SNAPSHOT: photograph all positions before touching anything
		// i.e : snapshot = [ (1, 0, Normal, 1), (1, 4, Gem, 2) ]
		var snapshot = new List<(int r, int c, GridManager.ChestType type, int hits)>();
		// Snpashot : only retrieve cells that have chest
		for (int r = 0; r < Grid.Rows; r++)
			for (int c = 0; c < Grid.Cols; c++)
				if (Grid.Grid[r, c].HasChest)
					snapshot.Add((r, c, Grid.Grid[r, c].Chest, Grid.Grid[r, c].HitsLeft));

		// Step 2 — CLEAR: wipe the grid clean
		// i.e Grid[1,0].Chest = None
		// Grid[1,4].Chest = None
		// Grid is now empty at those positions.
		foreach (var (r, c, _, _) in snapshot)
		{
			Grid.Grid[r, c].Chest    = GridManager.ChestType.None;
			Grid.Grid[r, c].HitsLeft = 0;
		}

		// Step 3 — PLACE MIRRORED: put each chest at its mirrored column
		// (0,1) → mirroredC = (4 - 1) - 1 = 3 → place at (0,3)
		// (0,3) → mirroredC = (4 - 1) - 3 = 1 → place at (0,1)
		// Result: chests swapped left ↔ right
		foreach (var (r, c, type, hits) in snapshot)
		{
			int GridSize = Grid.Cols - 1;

			int mirroredC =  GridSize - c; // i.e : (5-1) - 0 = 4
			var target = Grid.Grid[r, mirroredC]; //  ← was just cleared, so IsEmpty = true

			// The target.IsEmpty guard handles collision  (it's from the grid cell) — 
			// if two chests mirror to the same cell (e.g. both at col 2 center), 
			// the second one just skips.
			if (target.IsEmpty) // i.e : Grid[1, 4].Chest = Normal ✅ -> it's a ref to the grid cell btw
			{
				target.Chest    = type;
				target.HitsLeft = hits;
			}
		}
		// What mirroring does :
		/*  col 0 → 4  (leftmost ↔ rightmost)
			col 1 → 3
			col 2 → 2  (center stays center)
			col 3 → 1
			col 4 → 0 
		*/

		OnStateChanged?.Invoke();
	}

	void UpdateGridSize(int wave)
	{
		int newRows = wave switch {
			>= 100 => 7,
			>= 50  => 6,
			>= 20  => 6,   // ← was 150, now 20
			_      => 5
		};
		int newCols = wave switch {
			>= 100 => 7,
			>= 50  => 6,
			>= 20  => 5,   // ← 6 rows but still 5 cols — feels wider not just taller
			_      => 5
		};

		if (newRows == Grid.Rows && newCols == Grid.Cols) return;
		Grid.ResizeGrid(newRows, newCols);
	}

	/// <summary>
	///  Void chest (new chest)
	/// </summary>
	/// <param name="row"></param>
	/// <param name="col"></param>
	public void TriggerVoidExplosion(int row, int col)
	{
		// Remove all chests in 2×2 area around the void chest
		for (int r = row - 1; r <= row + 1; r++)
			for (int c = col - 1; c <= col + 1; c++)
			{
				if (!Grid.InBounds(r, c)) continue;
				var cell = Grid.Grid[r, c];
				if (!cell.HasChest) continue;
				if (cell.Chest == GridManager.ChestType.Void) continue; // skip self

				// Flat 50 score — no multiplier, no bonus
				// Void clears are strategic, not skill-based
				Score += 50;
				OnChestDestroyed(r, c, 50, 1f);
				TotalChestsCleared++;

				cell.Chest = GridManager.ChestType.None;
				cell.HitsLeft = 0;
			}

		//ScreenFlash.Instance?.WaveStart(Wave); // reuse flash for effect
		Sound.Play("chest_break");
		OnStateChanged?.Invoke();
	}

	// used for cursed chest so far
	public void QueueTimerPenalty(float seconds)
	{
		_timerPenaltyNextWave += seconds;
	}

	int _activeCursedChests = 0;
	public int ActiveCursedChests => _activeCursedChests;
	void RecountCursedChests()
	{
		_activeCursedChests = 0;
		for (int r = 0; r < Grid.Rows; r++)
			for (int c = 0; c < Grid.Cols; c++)
				if (Grid.Grid[r, c].Chest == GridManager.ChestType.Cursed)
					_activeCursedChests++;
	}


	// * -------------------------------------------------------------------- //
	// * ------------------------- Hype meter state ------------------------- //
	// * -------------------------------------------------------------------- //

	float _hypeMeter = 0f;          // 0.0 to 1.0
	bool _hypeMaxActive = false;    // temp multiplier active
	bool _hypeTriggeredThisWave = false; // prevent double trigger
	public float HypeMeter => _hypeMeter;
	public bool HypeMaxActive => _hypeMaxActive;

	public void AddHype(int projectedCleared)
	{
		if (_hypeMaxActive) return; // draining, not filling

		float gain = projectedCleared switch {
			>= 8 => 0.40f,
			>= 6 => 0.30f,
			>= 4 => 0.20f,
			>= 2 => 0.10f,
			_    => 0f
		};

		_hypeMeter = MathF.Min(1f, _hypeMeter + gain);

		if (_hypeMeter >= 1f && !_hypeTriggeredThisWave)
		{
			_hypeTriggeredThisWave = true;
			TriggerHypeMax();
		}

		OnStateChanged?.Invoke();
	}

	void TriggerHypeMax()
	{
		_hypeMaxActive = true;
		// PopupLayer fires from GameHud — just set state here
		OnStateChanged?.Invoke();
	}

	// Called at start of each wave in HandleDetonationFinished
	public void TickHypeMeter()
	{
		// Don't drain here — OnUpdate handles continuous drain
		// Just reset the trigger flag so hype max can fire again next time
		if (!_hypeMaxActive)
			_hypeTriggeredThisWave = false;
	}


	// * -------------------------------------------------------------------- //
	// * ------------------ Smart Chest Placement Difficulty ---------------- //
	// * -------------------------------------------------------------------- //

	// ── How desirable is this cell for placement ──
	float GetPlacementScore(int r, int c)
	{
		float score = 0f;
		int centerC = Grid.Cols / 2;
		float distFromCenter = MathF.Abs(c - centerC);
		int adjacentCount = AdjacentNormalChestCount(r, c);

		if (Wave < 8)
		{
			// Very easy — cluster everything in center
			score += (Grid.Cols - distFromCenter) * 3f;
			score += adjacentCount * 4f;
		}
		else if (Wave < 20)
		{
			// Easy — slight center bias, some clustering
			score += (Grid.Cols - distFromCenter) * 1.5f;
			score += adjacentCount * 2f;
			score += Rng.Next(4); // noise keeps it feeling natural
		}
		else if (Wave < 50)
		{
			// Was too weak — double the penalties
			score += distFromCenter * 1.5f;
			score -= adjacentCount * 2f;  // was 0.5f — this is what breaks clusters
			score += Rng.Next(3);
		}
		else if (Wave < 100)
		{
			// Hard — prefer spread, penalize clustering
			score += distFromCenter * 2f;
			score -= adjacentCount * 1.5f;
			score += Rng.Next(2);
		}
		else if (Wave < 200)
		{
			// Very hard — edges preferred, clustering punished
			bool isEdge = c == 0 || c == Grid.Cols - 1;
			score += distFromCenter * 3f;
			score -= adjacentCount * 2.5f;
			if (isEdge) score += 2f;
			score += Rng.Next(2);
		}
		else
		{
			// Wave 200+ — maximum spread, corners and edges strongly preferred
			bool isEdge = c == 0 || c == Grid.Cols - 1;
			bool isCorner = isEdge && (r == 0 || r == Grid.Rows - 1);
			score += distFromCenter * 4f;
			score -= adjacentCount * 3f;
			if (isEdge)   score += 3f;
			if (isCorner) score += 2f;
			// No noise at 200+ — placement is deliberately punishing
		}

		return score;
	}

	bool TooCloseToExistingBombChest(int r, int c, int minDist = 2)
	{
		for (int row = 0; row < Grid.Rows; row++)
			for (int col = 0; col < Grid.Cols; col++)
			{
				if (Grid.Grid[row, col].Chest != GridManager.ChestType.BombChest) continue;
				if (MathF.Abs(row - r) <= minDist && MathF.Abs(col - c) <= minDist)
					return true;
			}
		return false;
	}

	// ── Count existing bomb chests across the entire grid ──
	int CountBombChestsOnGrid()
	{
		int count = 0;
		for (int r = 0; r < Grid.Rows; r++)
			for (int c = 0; c < Grid.Cols; c++)
				if (Grid.Grid[r, c].Chest == GridManager.ChestType.BombChest)
					count++;
		return count;
	}


	// * -------------------------------------------------------------------- //
	// * ------------------------- GAME MODE SURVIVAL ----------------------- //
	// * -------------------------------------------------------------------- //

	float _survivalSlideTimer = 0f;
	float _survivalSlideInterval = 3.0f; // starts slower than spawn

	int _survivalPhase = 0;
	bool _survivalBombChestBoost = false;

	bool _survivalPhaseInitialized = false;

	// true from the moment a detonation clears chests until its own board advance resolves 
	// — prevents the ambient timer from sneaking in a second slide during that gap
	bool _survivalAdvancePending = false; 

	void TickSurvival()
	{
		if (_survivalSpawnFrozen || MilestoneOverlay.Instance?.IsActive == true || _survivalAdvancePending)
			return;

		_survivalElapsed += Time.Delta;

		int minute = (int)(_survivalElapsed / 60f);
		if (minute != _survivalPhase || !_survivalPhaseInitialized)
		{
			_survivalPhase = minute;
			_survivalPhaseInitialized = true;
			OnSurvivalMinuteChange(minute);
		}

		// ── Micro-goal countdown ──
		if (string.IsNullOrEmpty(SurvivalGoalLabel))
		{
			RollSurvivalGoal();
		}
		else
		{
			SurvivalGoalTimeLeft -= Time.Delta;
			if (SurvivalGoalTimeLeft <= 0f)
				RollSurvivalGoal(); // expired, no penalty — just a fresh target
		}

		bool boardEmpty = Grid.AllChestCells().Count == 0;
		if (boardEmpty)
		{
			if (_survivalEmptySince < 0f)
				_survivalEmptySince = _survivalElapsed;
			else if (_survivalElapsed - _survivalEmptySince >= SurvivalEmptyGraceSeconds)
				_survivalSpawnTimer = 0f;
		}
		else
		{
			_survivalEmptySince = -1f;
		}

		_survivalSpawnTimer -= Time.Delta;
		if (_survivalSpawnTimer <= 0f)
		{
			_survivalSpawnTimer = _survivalSpawnInterval;
			SurvivalSpawnWave();
		}
	}
	void SpawnSurvivalMiniBoss()
	{
		// find an empty top-row cell
		var empty = new List<GridManager.Cell>();
		for (int c = 0; c < Grid.Cols; c++)
			if (Grid.Grid[0, c].IsEmpty) empty.Add(Grid.Grid[0, c]);
		
		if (empty.Count == 0) return;
		
		var cell = empty[Rng.Next(empty.Count)];
		cell.Chest = GridManager.ChestType.Boss;
		cell.HitsLeft = 4; // tune HP
		
		MilestoneOverlay.Instance?.ShowMessage("👑 A boss chest appeared! Clear it before it reaches the bottom!", 0, 4f);
		Sound.Play("boss_alert");
	}
	
	void SurvivalSpawnWave()
	{
		_survivalFakeWave = 1 + (SurvivalSpawnCount / 12);

		var (baseMin, baseMax) = GetSurvivalBaseBatch();
		int min = Math.Max(2, baseMin + _survivalPhaseBatchBonus);
		int max = Math.Max(min, baseMax + _survivalPhaseBatchBonus);

		int count = Rng.Next(min, max + 1);

		if (!SurvivalAdvanceAndSpawn(count))
			return;

		_survivalSpawnInterval = MathF.Max(
			SurvivalIntervalFloor * 0.8f,
			GetSurvivalBaseInterval() * _survivalPhaseIntervalMult
		);

		OnStateChanged?.Invoke();
	}

	// Survival-only: slide chests down, and if a chest lands on a bomb cell, trigger it
	List<(int r, int c)> _survivalTriggeredBombs = new();

	void SurvivalSlideAndCheckBombTriggers()
	{
		_survivalTriggeredBombs.Clear();

		for (int r = Grid.Rows - 2; r >= 0; r--)
			for (int c = 0; c < Grid.Cols; c++)
			{
				var cell = Grid.Grid[r, c];
				if (!cell.HasChest) continue;
				if (cell.Chest == GridManager.ChestType.Secret) continue;
				if (cell.Chest == GridManager.ChestType.Boss) continue;
				if (cell.Chest == GridManager.ChestType.Void) continue;

				int nr = r + 1;
				var below = Grid.Grid[nr, c];

				// Chest slides into a cell with a placed bomb — mark for trigger
				if (below.HasBomb && !below.HasChest)
				{
					below.Chest = cell.Chest;
					below.HitsLeft = cell.HitsLeft;
					cell.Chest = GridManager.ChestType.None;
					cell.HitsLeft = 0;

					_survivalTriggeredBombs.Add((nr, c));
				}
				else if (below.IsEmpty)
				{
					below.Chest = cell.Chest;
					below.HitsLeft = cell.HitsLeft;
					cell.Chest = GridManager.ChestType.None;
					cell.HitsLeft = 0;
				}
			}
	}

	// Thin wrappers so we never touch the real Wave-driven side effects (grid resize, boss spawn, etc.)
	int SurvivalChestsForWave(int fakeWave) => ChestsForWave(fakeWave); // reuse existing curve directly — no side effects in that method

	void SurvivalSpawnChests(int count)
	{
		var empty = new List<GridManager.Cell>();
		int spawnRows = _survivalFakeWave switch { >= 200 => 4, >= 100 => 3, _ => 2 };
		for (int r = 0; r < spawnRows; r++)
			for (int c = 0; c < Grid.Cols; c++)
				if (Grid.Grid[r, c].IsEmpty) empty.Add(Grid.Grid[r, c]);

		Shuffle(empty);

		for (int i = 0; i < MathF.Min(count, empty.Count); i++)
		{
			var type = RollSurvivalChestType();
			empty[i].Chest = type;
			int baseHits = type switch {
				GridManager.ChestType.Gem    => 2,
				GridManager.ChestType.Locked => 3,
				_                            => 1
			};
			empty[i].HitsLeft = baseHits;
		}

		_survivalGoldRushMinute = false; // one-shot, consumed after this batch
		Sound.Play("next_wave_spawn");
	}

	// Survival-specific chest roll — small bomb-chest chance from the very start,
	// scales up gently as _survivalFakeWave climbs
	GridManager.ChestType RollSurvivalChestType()
	{
		if (_survivalGoldRushMinute)
			return GridManager.ChestType.Gem;

		int roll = Rng.Next(100);
		int bombBoost = _survivalBombChestBoost ? 15 : 0; // shifts 15% of roll odds toward bomb chests

		if (_survivalFakeWave <= 2)
			return roll < (75 - bombBoost) ? GridManager.ChestType.Normal
				: roll < (90 - bombBoost) ? GridManager.ChestType.Gem
				:                            GridManager.ChestType.BombChest; // 10% → 25% when boosted

		if (_survivalFakeWave <= 6)
			return roll < (55 - bombBoost) ? GridManager.ChestType.Normal
				: roll < (78 - bombBoost) ? GridManager.ChestType.Gem
				:                            GridManager.ChestType.BombChest; // 22% → 37% when boosted

		// Late game — shared wave-mode curve already scales bomb chest odds with wave;
		// boost doesn't stack here to avoid runaway density at high fakeWave
		return RollChestType(_survivalFakeWave);
	}

	void SurvivalClearRandomRow()
	{
		var rowsWithChests = new List<int>();
		for (int r = 0; r < Grid.Rows; r++)
			for (int c = 0; c < Grid.Cols; c++)
				if (Grid.Grid[r, c].HasChest) { rowsWithChests.Add(r); break; }

		if (rowsWithChests.Count == 0) return;
		int row = rowsWithChests[Rng.Next(rowsWithChests.Count)];

		for (int c = 0; c < Grid.Cols; c++)
		{
			var cell = Grid.Grid[row, c];
			if (!cell.HasChest) continue;
			Score += 50;
			TotalChestsCleared++;
			cell.Chest = GridManager.ChestType.None;
			cell.HitsLeft = 0;
		}
	}

	GridManager.ChestType RollSurvivalChestTypeEarly()
	{
		int roll = Rng.Next(100);
		return roll < 75 ? GridManager.ChestType.Normal
			: roll < 90 ? GridManager.ChestType.Gem
			:              GridManager.ChestType.BombChest; // ~10% from the very start
	}

	static float Lerp(float a, float b, float t) => a + (b - a) * t;

	float GetSurvivalBaseInterval()
	{
		float t = MathF.Max(0f, MathF.Min(1f, _survivalElapsed / 60f / SurvivalRampMinutes));
		float baseInterval = Lerp(SurvivalIntervalStart, SurvivalIntervalFloor, t);

		// Compounding per-minute pressure — keeps escalating past the ramp window,
		// so long runs never plateau into "safe" territory.
		int minute = (int)(_survivalElapsed / 60f);
		float minuteMult = MathF.Pow(SurvivalMinuteSpeedStep, minute);

		return MathF.Max(SurvivalHardFloor, baseInterval * minuteMult);
	}

	(int min, int max) GetSurvivalBaseBatch()
	{
		float t = MathF.Max(0f, MathF.Min(1f, _survivalElapsed / 60f / SurvivalRampMinutes));
		int max = (int)MathF.Round(Lerp(SurvivalBatchMaxStart, SurvivalBatchMaxFloor, t));
		int min = (int)MathF.Round(Lerp(SurvivalBatchMinStart, SurvivalBatchMinFloor, t));
		min = Math.Max(min, 2);
		max = Math.Max(max, min);
		return (min, max);
	}

	// Shared by both the periodic timer and detonation-triggered top-ups —
	// keeps slide/game-over/spawn logic in exactly one place.
	bool SurvivalAdvanceAndSpawn(int count)
	{
		SlideChestsDown();

		if (Grid.AnyChestInLastRow())
		{
			Lives--;

			if (Lives <= 0)
			{
				TriggerGameOver("💀 The board broke through. Survival run over.");
				return false;
			}

			// Breach — survivable. Wipe the bottom two rows and keep going.
			SurvivalEmergencyClear(2);
			_hypeMeter = 0f;              // combo dies on a breach — real cost
			_hypeMaxActive = false;
			_hypeTriggeredThisWave = false;

			MilestoneOverlay.Instance?.ShowMessage(
				$"💔 BREACH! Bottom cleared · {Lives} {(Lives == 1 ? "life" : "lives")} left", 0, 2f);
			OnScreenFlashRequest?.Invoke("gameover", 0);
			Sound.Play("Heart_beat");
		}

		SurvivalSpawnChests(count);
		SurvivalSpawnCount++;
		return true;
	}

	// Added

	public bool IsPaused { get; private set; } = false;

	public void PauseForMenu()
	{
		IsPaused = true;
		OnStateChanged?.Invoke();
	}

	public void ResumeFromMenu()
	{
		IsPaused = false;
		OnStateChanged?.Invoke();
	}

	// survival a

	async void OnSurvivalMinuteChange(int minute)
	{
		int phaseInCycle = minute % 6;

		_survivalPhaseIntervalMult = 1f;
		_survivalPhaseBatchBonus   = 0;
		_survivalBombChestBoost    = false;

		string title = "";
		bool showBanner = minute > 0; // nothing to announce at t=0, run just started
		const float bannerDuration = 2f;

		switch (phaseInCycle)
		{
			case 0:
				if (minute > 0) showBanner = false; // still a "quiet" baseline minute
				break;

			case 1:
				if (SurvivalBonusBombSlots < MaxSurvivalBonusSlots)
				{
					SurvivalBonusBombSlots++;
					Hand.HandSize++;
					title = $"💣 Extra bomb slot! ({Hand.HandSize} total)";
				}
				else
				{
					Score += 500;
					title = "⚡ +500 bonus!";
				}
				break;

			case 2:
				_survivalPhaseIntervalMult = 0.94f;
				_survivalBombChestBoost = true;
				title = "💣 More bomb chests incoming!";
				break;

			case 3:
				SpawnSurvivalMiniBoss();
				showBanner = false;
				break;

			case 4:
				_survivalGoldRushMinute = true;
				title = "💎 Gold Rush! Gems incoming!";
				break;

			case 5:
				_survivalScoreBoostUntil = _survivalElapsed + 10f;
				title = "⭐ 2x SCORE for 10 seconds!";
				break;
		}

		// ── Frags scale with minutes survived — minute 1 gives just a few, grows from there ──
		if (minute > 0)
		{
			int frags = 2 + minute; // minute 1 = 3 frags, minute 5 = 7, minute 9 = 11
			FragmentsEarned += frags;

			if (showBanner)
				MilestoneOverlay.Instance?.ShowMessage($"⏱️ {minute} min · {title} (+{frags} 💠)", 0, bannerDuration);
		}

		if (showBanner)
		{
			Sound.Play("WaveMilestone");
			_survivalSpawnFrozen = true;
			OnStateChanged?.Invoke();

			while (MilestoneOverlay.Instance?.IsActive == true)
				await GameTask.DelaySeconds(0.1f);

			_survivalSpawnFrozen = false;
			_survivalSpawnTimer = _survivalSpawnInterval;
		}

		OnStateChanged?.Invoke();
	}

	void TickHypeDrain() // survival mode hype bar drain
	{
		if (_hypeMeter <= 0f || !IsRunning) return;
		if (MilestoneOverlay.Instance?.IsActive == true) return;

		// Survival drains much faster — hype is a short, hot window you fight to keep alive.
		// Wave mode keeps its slower original pacing.
		float drainRate = Mode == GameMode.Survival
			? (_hypeMaxActive ? 0.14f : 0.045f)   // max: ~7s of 2x, idle: ~22s to empty
			: (_hypeMaxActive ? 0.08f : 0.012f);

		_hypeMeter = MathF.Max(0f, _hypeMeter - drainRate * Time.Delta);

		if (_hypeMaxActive && _hypeMeter <= 0f)
		{
			_hypeMaxActive = false;
			_hypeTriggeredThisWave = false;
		}

		OnStateChanged?.Invoke();
	}

	void SurvivalEmergencyClear(int rowCount)
	{
		for (int r = Grid.Rows - rowCount; r < Grid.Rows; r++)
		{
			if (r < 0) continue;
			for (int c = 0; c < Grid.Cols; c++)
			{
				var cell = Grid.Grid[r, c];
				if (!cell.HasChest) continue;
				cell.Chest = GridManager.ChestType.None;
				cell.HitsLeft = 0;
			}
		}
	}

	// ── Survival micro-goals ──
	public enum SurvivalGoalType { ClearChests, BigDetonation, ClearGems, ChainLength }

	SurvivalGoalType _survivalGoalType;
	public string SurvivalGoalLabel { get; private set; } = "";
	public int SurvivalGoalProgress { get; private set; }
	public int SurvivalGoalTarget { get; private set; }
	public float SurvivalGoalTimeLeft { get; private set; }
	const float SurvivalGoalDuration = 25f;

	void RollSurvivalGoal()
	{
		int minute = (int)(_survivalElapsed / 60f);
		int tier = Math.Clamp(minute, 0, 5);

		_survivalGoalType = (SurvivalGoalType)Rng.Next(4);

		switch (_survivalGoalType)
		{
			case SurvivalGoalType.ClearChests:
				SurvivalGoalTarget = 8 + tier * 3;
				SurvivalGoalLabel  = $"Clear {SurvivalGoalTarget} chests";
				break;
			case SurvivalGoalType.BigDetonation:
				SurvivalGoalTarget = 4 + tier;
				SurvivalGoalLabel  = $"Clear {SurvivalGoalTarget} in one blast";
				break;
			case SurvivalGoalType.ClearGems:
				SurvivalGoalTarget = 2 + tier;
				SurvivalGoalLabel  = $"Destroy {SurvivalGoalTarget} gems 💎";
				break;
			case SurvivalGoalType.ChainLength:
				SurvivalGoalTarget = 3 + tier;
				SurvivalGoalLabel  = $"Chain {SurvivalGoalTarget} bombs ⛓";
				break;
		}

		SurvivalGoalProgress = 0;
		SurvivalGoalTimeLeft = SurvivalGoalDuration;
		OnStateChanged?.Invoke();
	}

	void CompleteSurvivalGoal()
	{
		int minute = (int)(_survivalElapsed / 60f);
		int scoreReward = 400 + minute * 200;
		int fragReward  = 1 + minute / 2;

		Score += scoreReward;
		FragmentsEarned += fragReward;
		_hypeMeter = MathF.Min(1f, _hypeMeter + 0.25f);  // goals feed the combo

		MilestoneOverlay.Instance?.ShowMessage($"✅ {SurvivalGoalLabel} · +{scoreReward} +{fragReward}💠", 0, 1.5f);
		Sound.Play("perfect_streak");

		RollSurvivalGoal();
	}

	void TickSurvivalGoalProgress(int chestsCleared)
	{
		if (string.IsNullOrEmpty(SurvivalGoalLabel)) return;

		switch (_survivalGoalType)
		{
			case SurvivalGoalType.ClearChests:
				SurvivalGoalProgress += chestsCleared;
				break;

			case SurvivalGoalType.BigDetonation:
				if (chestsCleared >= SurvivalGoalTarget)
					SurvivalGoalProgress = SurvivalGoalTarget;
				break;

			case SurvivalGoalType.ClearGems:
				int gems = Hand.LastSimResult?.HitChests
					.Count(kv => kv.Value.WillDestroy && kv.Value.ChestType == GridManager.ChestType.Gem) ?? 0;
				SurvivalGoalProgress += gems;
				break;

			case SurvivalGoalType.ChainLength:
				int chain = Hand.LastSimResult?.Sequence.Count ?? 0;
				if (chain >= SurvivalGoalTarget)
					SurvivalGoalProgress = SurvivalGoalTarget;
				break;
		}

		if (SurvivalGoalProgress >= SurvivalGoalTarget)
			CompleteSurvivalGoal();
	}
}