Files
blockout/BlockOutReplica/Assets/Scripts/Gameplay/BlockOutInteractionReview.cs
T
2026-10-20 13:21:04 +08:00

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