189 lines
14 KiB
C#
189 lines
14 KiB
C#
#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<bool,string> 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
|