#if UNITY_EDITOR using System; using System.Collections; using BlockOutReplica.Infrastructure; using UnityEngine; namespace BlockOutReplica.Gameplay { public sealed partial class BlockOutGameController { // Driven by EditorApplication.update, not a controller coroutine. Reloading // levels intentionally stops controller coroutines but must not stop review. public IEnumerator ReviewInteractions() { var originalProgress = progress; var originalPlayer = Player; var originalEconomy = economy; var originalJourney = Journey; bool originalJourneyTab = JourneyTab; string originalJourneyNotice = JourneyNotice; int originalCelebrationLevel = JourneyCelebrationLevel; float originalCelebrationTime = JourneyCelebrationStartedAt; bool wasHome = ScreenState == BlockOutScreenState.Home; int checks = 0; Action check = (condition, message) => { if (!condition) throw new InvalidOperationException("check " + checks + ": " + message); checks++; }; try { economy = BlockOutEconomy.Load(); player = new BlockOutPlayerState(economy, "", _ => {}); journey = new BlockOutJourney(BlockOutJourneyConfig.Load(), "", _ => {}); progress = new BlockOutProgress(OriginalFlowRepository.Load(), false, 4); InitializeInteractions(); LoadCurrentLevel(); IsInputReviewRunning = true; yield return null; System.IO.Directory.CreateDirectory("Docs/iterations"); check(pieces.Count == 18, "real fourth level loaded"); timerRunning = true; float time = TimeLeft; RequestPause(); AdvanceClock(3); check(Modal == GameModal.Pause && TimeLeft == time, "pause freezes clock"); BeginDrag(gameCamera.WorldToScreenPoint(pieces[0].transform.position), gameCamera); check(!IsDragging, "modal blocks board input"); ScreenCapture.CaptureScreenshot("Docs/iterations/2026-10-19-interactions-pause.png"); yield return null; RequestQuit(); check(Modal == GameModal.Quit, "pause opens quit confirmation"); CloseModal(); check(Modal == GameModal.Pause && Player.Lives == 5, "quit cancellation preserves life"); CloseModal(); ClickBooster(BoosterType.Freeze); AdvanceClock(3); check(Player.Amount(BoosterType.Freeze) == 2 && freezeRemaining == 7 && TimeLeft == time, "freeze spends one and freezes 10s"); ClickBooster(BoosterType.Freeze); check(Player.Amount(BoosterType.Freeze) == 2, "active freeze cannot be charged twice"); RequestPause(); AdvanceClock(20); check(freezeRemaining == 7, "paused freeze duration preserved"); CloseModal(); AdvanceClock(8); check(freezeRemaining == 0 && Mathf.Abs(TimeLeft - (time - 1)) < .01f, "clock resumes after freeze"); ClickBooster(BoosterType.Rocket); check(SelectedBooster == BoosterType.Rocket && Player.Amount(BoosterType.Rocket) == 3, "target selection does not spend"); ClickBooster(BoosterType.Rocket); check(SelectedBooster == BoosterType.None && Player.Amount(BoosterType.Rocket) == 3, "repeat tap cancels free"); ClickBooster(BoosterType.Rocket); BeginDrag(new Vector2(-100,-100), gameCamera); check(Player.Amount(BoosterType.Rocket) == 3, "invalid target does not spend"); BlockPieceView red = pieces.Find(item => item.ColorId == 0); BeginDrag(gameCamera.WorldToScreenPoint(red.transform.position), gameCamera); check(pieces.Count == 17 && Player.Amount(BoosterType.Rocket) == 2 && InputLocked, "rocket commits exactly one block"); ClickBooster(BoosterType.Rocket); check(Player.Amount(BoosterType.Rocket) == 2, "busy input cannot spend again"); float deadline = Time.realtimeSinceStartup + 15f; while (InputLocked && Time.realtimeSinceStartup < deadline) yield return null; check(red == null && !InputLocked && activeExits == 0, "rocket animation ends and releases input"); int whiteCount = pieces.FindAll(item => item.ColorId == 10).Count; ClickBooster(BoosterType.Ufo); ScreenCapture.CaptureScreenshot("Docs/iterations/2026-10-19-interactions-targeting.png"); yield return null; BlockPieceView white = pieces.Find(item => item.ColorId == 10); BeginDrag(gameCamera.WorldToScreenPoint(white.transform.position), gameCamera); check(pieces.Count == 17 - whiteCount && Player.Amount(BoosterType.Ufo) == 2, "ufo removes selected color only"); deadline = Time.realtimeSinceStartup + 15f; while (InputLocked && Time.realtimeSinceStartup < deadline) yield return null; check(activeExits == 0 && !InputLocked && pieces.FindAll(item => item.ColorId == 10).Count == 0, "ufo completes full target batch"); check(doors.Find(item => item.ColorId == 10).transform.Find("Cleared Door Wall") != null, "eliminated color closes its unused door"); while (Player.SpendBooster(BoosterType.Freeze)) {} ClickBooster(BoosterType.Freeze); check(Modal == GameModal.None && freezeRemaining==10, "zero inventory freeze pays for immediate use"); time = TimeLeft; AdvanceClock(5); check(TimeLeft == time, "paid freeze stops clock"); BuySelectedBooster(); check(Player.Coins == 700 && Player.Amount(BoosterType.Freeze) == 0 && Modal == GameModal.None, "paid use spends source price without adding inventory"); BuySelectedBooster(); check(Player.Coins == 700, "duplicate purchase click ignored"); TimeLeft = .01f; freezeRemaining = 0; AdvanceClock(1); check(Modal == GameModal.OutOfTime, "timeout opens continue offer"); ContinueWithCoins(); check(Modal == GameModal.OutOfTime && Player.Coins == 700, "insufficient continue preserves board and coins"); Player.AwardCoins(1000); ContinueWithCoins(); check(TimeLeft == 30 && Modal == GameModal.None && Player.Coins == 800 && ContinueCost == 1900, "continue adds 30s and advances price"); ContinueWithCoins(); check(Player.Coins == 800, "double continue cannot charge twice"); TimeLeft = .01f; AdvanceClock(1); CloseModal(); check(Modal == GameModal.GiveUp, "timeout close requires give-up confirmation"); CloseModal(); check(Modal == GameModal.OutOfTime, "give-up cancellation returns to offer"); CloseModal(); GiveUpRun(); GiveUpRun(); check(Modal == GameModal.Failed && Player.Lives == 4 && progress.LevelNumber == 4, "failure costs one life and no progress"); RetryFailedLevel(); IsInputReviewRunning = true; yield return null; check(pieces.Count == 18 && Player.Lives == 4 && Player.Amount(BoosterType.Rocket) == 2, "retry preserves consumed inventory"); RequestRestart(); ConfirmRestart(); IsInputReviewRunning = true; yield return null; check(Player.Lives == 4, "unstarted restart costs no life"); timerRunning = true; RequestRestart(); ConfirmRestart(); IsInputReviewRunning = true; yield return null; check(Player.Lives == 3 && progress.LevelNumber == 4, "started restart costs exactly one life"); RequestQuit(); ConfirmQuit(); check(ScreenState == BlockOutScreenState.Failed && Modal == GameModal.Failed && Player.Lives == 2, "original unstarted quit charges once and opens FAILED"); CloseModal();check(ScreenState==BlockOutScreenState.Home,"closing FAILED returns Home"); RequestEnterLevel(); check(Modal == GameModal.PreLevel, "home opens pre-level"); CloseModal(); check(Player.Lives == 2 && ScreenState == BlockOutScreenState.Home, "cancel pre-level is free"); RequestEnterLevel(); EnterPreparedLevel(); IsInputReviewRunning = true; yield return null; check(ScreenState == BlockOutScreenState.Playing && Modal == GameModal.None, "pre-level enters playable board"); timerRunning = true; RequestQuit(); ConfirmQuit(); ConfirmQuit(); check(Player.Lives == 1 && ScreenState == BlockOutScreenState.Failed && Modal == GameModal.Failed, "started quit commits one debit and opens FAILED"); CloseModal();check(ScreenState==BlockOutScreenState.Home,"started quit FAILED close returns Home"); RequestSettings(); ToggleSounds(); ToggleMusic(); check(!Player.Sounds && !Player.Music, "settings toggles affect account"); ToggleSounds(); ToggleMusic(); CloseModal(); RequestCoinInfo(); check(Modal == GameModal.Info, "coin info opens without fake purchase"); CloseModal(); RequestLives(); BuyLifeRefill(); check(Player.Lives == 1 && Player.Coins == 800, "insufficient life refill cannot charge"); Player.AwardCoins(1000); BuyLifeRefill(); BuyLifeRefill(); check(Player.Lives == 5 && Player.Coins == 900, "life refill commits once"); CloseModal(); for (int i = 0; i < 5; i++) Player.LoseLife(); RequestEnterLevel(); check(Modal == GameModal.Lives && ScreenState == BlockOutScreenState.Home, "no-life entry blocked"); BuyLifeRefill(); CloseModal(); check(Player.Lives == 5 && Player.Coins == 0, "zero balance after valid refill remains playable"); // A genuine clear using all three normal booster removal operations. player = new BlockOutPlayerState(economy, "", _ => {}); progress = new BlockOutProgress(OriginalFlowRepository.Load(), false, 1); InitializeInteractions(); LoadCurrentLevel(); IsInputReviewRunning = true; yield return null; int startingCoins = Player.Coins; for (int index = 0; index < 3; index++) { BlockPieceView target = pieces[0]; ClickBooster(index == 0 ? BoosterType.Rocket : BoosterType.Ufo); BeginDrag(gameCamera.WorldToScreenPoint(target.transform.position), gameCamera); deadline = Time.realtimeSinceStartup + 15f; while (InputLocked && ScreenState != BlockOutScreenState.LevelComplete && Time.realtimeSinceStartup < deadline) yield return null; } check(ScreenState == BlockOutScreenState.LevelComplete && progress.LevelNumber == 2 && Player.Coins == startingCoins + 20, "actual last-block booster win commits progress and reward"); StartCoroutine(CompleteLevelFlow()); check(Player.Coins == startingCoins + 20, "duplicate win callback cannot award twice"); ScreenCapture.CaptureScreenshot("Docs/iterations/2026-10-19-interactions-win.png"); yield return null; ContinueAfterWin(); IsInputReviewRunning = true; yield return null; check(ScreenState == BlockOutScreenState.Playing && progress.LevelNumber == 2, "early win enters next board directly"); // Isolate post-threshold routing using the existing debug win hook. progress = new BlockOutProgress(OriginalFlowRepository.Load(), false, 9); LoadCurrentLevel(); IsInputReviewRunning = true; yield return null; startingCoins = Player.Coins; CompleteLevelForReview(); check(progress.LevelNumber == 10 && Player.Coins == startingCoins + 20 && Journey.IsQueued(10), "win 9 commits ordinary reward and queues Journey"); ContinueAfterWin(); check(ScreenState == BlockOutScreenState.Home && progress.LevelNumber == 10 && Player.Coins == startingCoins + 70 && Journey.IsClaimed(10) && !Journey.IsQueued(10), "win 9 returns Home and automatically grants 50 once"); GoHome(); check(Player.Coins == startingCoins + 70, "reopening Home cannot duplicate Journey reward"); ScreenCapture.CaptureScreenshot("Docs/iterations/2026-10-19-interactions-home.png"); yield return null; RequestEnterLevel(); EnterPreparedLevel(); IsInputReviewRunning = true; yield return null; check(ScreenState == BlockOutScreenState.Playing && CurrentLevelId == progress.CurrentLevelId, "home starts progressed level"); Debug.Log($"BLOCKOUT_INTERACTION_RUNTIME: passed=True, checks={checks}, persistentPlayerDataUntouched=True"); } finally { progress = originalProgress; player = originalPlayer; economy = originalEconomy; journey = originalJourney; if (Application.isPlaying && this != null) { InitializeInteractions(); LoadCurrentLevel(); if (wasHome) GoHome(false); JourneyTab = originalJourneyTab; JourneyNotice = originalJourneyNotice; JourneyCelebrationLevel = originalCelebrationLevel; JourneyCelebrationStartedAt = originalCelebrationTime; JourneyRevision++; IsInputReviewRunning = false; } } } } } #endif