Files
blockout/BlockOutReplica/Assets/Editor/InteractionValidation.cs
T
2026-10-20 13:21:04 +08:00

160 lines
9.3 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using BlockOutReplica.Domain;
using BlockOutReplica.Gameplay;
using UnityEditor;
using UnityEngine;
namespace BlockOutReplica.Editor
{
public static class InteractionValidation
{
private static IEnumerator running;
private static readonly Stack<IEnumerator> routineStack=new Stack<IEnumerator>();
[MenuItem("Block Out/Validate Special Mechanics Runtime (Play Mode)")]
public static void ReviewSpecialMechanicsRuntime()
{
var game=UnityEngine.Object.FindObjectOfType<BlockOutGameController>();
if(!EditorApplication.isPlaying||game==null||running!=null)return;
running=game.ReviewSpecialMechanics();EditorApplication.update+=Step;
}
[MenuItem("Block Out/Validate First 20 Runtime (Play Mode)")]
public static void ReviewFirstTwenty()
{
var game=UnityEngine.Object.FindObjectOfType<BlockOutGameController>();
if(!EditorApplication.isPlaying||game==null||running!=null)return;
running=game.ReviewFirstTwentyPreview();EditorApplication.update+=Step;
}
[MenuItem("Block Out/Validate Paid Booster Flow (Play Mode)")]
public static void ReviewPaidBooster()
{
var game=UnityEngine.Object.FindObjectOfType<BlockOutGameController>();
if(!EditorApplication.isPlaying||game==null||running!=null)return;
running=game.ReviewPaidBoosterFlow();EditorApplication.update+=Step;
}
[MenuItem("Block Out/Capture UI Comparison (Play Mode)")]
public static void CaptureUiReview()
{
var game=UnityEngine.Object.FindObjectOfType<BlockOutGameController>();
if(!EditorApplication.isPlaying||game==null||running!=null)return;
running=game.ReviewUiComparison();EditorApplication.update+=Step;
}
[MenuItem("Block Out/Start Full Loop Sandbox (Play Mode)")]
public static void StartSandbox(){UnityEngine.Object.FindObjectOfType<BlockOutGameController>()?.BeginFullLoopSandbox(true);}
[MenuItem("Block Out/Restore Full Loop Sandbox")]
public static void RestoreSandbox(){UnityEngine.Object.FindObjectOfType<BlockOutGameController>()?.RestoreFullLoopSandbox();}
[MenuItem("Block Out/Validate Full Loop (Play Mode)")]
public static void RunFullLoop()
{
var game=UnityEngine.Object.FindObjectOfType<BlockOutGameController>();
if(!EditorApplication.isPlaying||game==null||running!=null)return;
running=game.ReviewFullLoop();EditorApplication.update+=Step;
}
[MenuItem("Block Out/Validate Economy And Progress")]
public static void ValidateModels()
{
int checks = 0;
Action<bool,string> check = (condition, message) => { if (!condition) throw new InvalidOperationException(message); checks++; };
var config = BlockOutEconomy.Load();
long now = DateTime.UtcNow.Ticks;
string saved = "";
int writes = 0;
var player = new BlockOutPlayerState(config, "", value => { saved = value; writes++; }, () => now);
check(player.Coins == 1000 && player.Lives == 5, "source initial economy");
foreach (BoosterType type in new[] {BoosterType.Freeze, BoosterType.Rocket, BoosterType.Ufo})
{
check(player.Amount(type) == 3, type + " initial inventory");
for (int i = 0; i < 3; i++) check(player.SpendBooster(type), type + " spend");
check(!player.SpendBooster(type) && player.Amount(type) == 0, type + " cannot overspend");
}
int before = writes;
check(player.BuyBooster(BoosterType.Freeze), "buy freeze");
check(writes == before + 1 && player.Coins == 700 && player.Amount(BoosterType.Freeze) == 1, "one atomic purchase save");
check(!player.BuyBooster(BoosterType.Ufo) && player.Coins == 700, "insufficient coins do not debit");
var reloaded = new BlockOutPlayerState(config, saved, _ => {}, () => now);
check(reloaded.Coins == 700 && reloaded.Amount(BoosterType.Freeze) == 1, "inventory and coins reload");
reloaded.ToggleSounds(); reloaded.ToggleMusic();
string settings = reloaded.ToJson();
var settingsReload = new BlockOutPlayerState(config, settings, _ => {}, () => now);
check(!settingsReload.Sounds && !settingsReload.Music, "settings reload");
check(!player.SpendCoins(701) && !player.SpendCoins(-1) && player.Coins == 700, "safe currency debit");
for (int i = 0; i < 5; i++) check(player.LoseLife(), "life lost");
check(!player.LoseLife() && player.Lives == 0, "lives never negative");
now += TimeSpan.FromMinutes(31).Ticks;
check(player.Lives == 1, "one life after 30 minutes");
check(player.Lives == 1, "refresh cannot duplicate refill");
now += TimeSpan.FromMinutes(240).Ticks;
check(player.Lives == 5 && player.SecondsUntilLife == 0, "offline refill caps at 5");
check(!player.RefillLives() && player.Coins == 700, "full lives cannot be charged");
player.LoseLife();
check(!player.RefillLives() && player.Coins == 700, "insufficient refill coins");
player.AwardCoins(900); before = writes;
check(player.RefillLives() && player.Lives == 5 && player.Coins == 700 && writes == before + 1, "atomic life purchase");
check(config.TimeFreezeDurationAsSeconds == 10 && config.PlayOnExtraTimeSeconds == 30, "source durations");
check(config.ContinuePrice(0) == 900 && config.ContinuePrice(1) == 1900 && config.ContinuePrice(2) == 2300, "source continue prices");
int useWrites=0;
var useConfig=new BlockOutEconomy{InitialRocketAmount=0};
var useWallet=new BlockOutPlayerState(useConfig,"",_=>useWrites++);
check(useWallet.CanUseBooster(BoosterType.Rocket)&&useWrites==0,"affordable empty rocket selection does not write");
check(useWallet.SpendBoosterUse(BoosterType.Rocket)&&useWallet.Coins==400&&useWallet.Amount(BoosterType.Rocket)==0&&useWrites==1,
"empty rocket pays single use atomically without inventory grant");
check(!useWallet.SpendBoosterUse(BoosterType.Rocket)&&useWallet.Coins==400&&useWrites==1,"underfunded paid use cannot change wallet");
check(useWallet.SpendBoosterUse(BoosterType.Freeze)&&useWallet.Coins==400&&useWallet.Amount(BoosterType.Freeze)==2&&useWrites==2,
"inventory use precedes coin use");
check(!useWallet.CanUseBooster(BoosterType.None)&&!useWallet.SpendBoosterUse(BoosterType.None)&&useWrites==2,
"none cannot spend currency");
var ids = new List<string>(); for (int i = 0; i < 12; i++) ids.Add("fixture-" + i);
var flow = new BlockOutFlowDefinition(ids, 12, 10);
var early = new BlockOutProgress(flow, false, 8);
check(!early.ShouldReturnHomeAfterWin, "win 8/current 9 continues in-game");
early.CompleteLevel();
check(early.LevelNumber == 9 && early.ShouldReturnHomeAfterWin, "win 9/current 10 returns home");
early.CompleteLevel();
check(early.LevelNumber == 10 && early.ShouldStartAtHome, "current level 10 resumes from Home");
var last = new BlockOutProgress(flow, false, 12);
check(last.CompleteLevel() && last.CatalogCompleted && last.CompletedLevels == 12, "last level catalog completion");
check(!last.CompleteLevel() && last.LevelNumber == 12, "terminal completion is idempotent");
Debug.Log($"BLOCKOUT_INTERACTION_MODELS: passed=True, checks={checks}");
}
[MenuItem("Block Out/Validate Interaction Flow (Play Mode)")]
public static void RunRuntime()
{
if (running != null) { Debug.LogWarning("Interaction review is already running."); return; }
var game = UnityEngine.Object.FindObjectOfType<BlockOutGameController>();
if (!EditorApplication.isPlaying || game == null) { Debug.LogError("Enter Play Mode first."); return; }
running = game.ReviewInteractions();
EditorApplication.update += Step;
}
private static void Step()
{
try
{
if(!EditorApplication.isPlaying){Finish();return;}
if(routineStack.Count==0)routineStack.Push(running);
while(routineStack.Count>0)
{
var active=routineStack.Peek();
if(!active.MoveNext()){routineStack.Pop();(active as IDisposable)?.Dispose();continue;}
if(active.Current is IEnumerator nested){routineStack.Push(nested);continue;}
return;
}
Finish();
}
catch (Exception exception)
{
Debug.LogError("BLOCKOUT_INTERACTION_REVIEW: passed=False, " + exception);
Finish();
}
}
private static void Finish()
{
EditorApplication.update -= Step;
var previous = running; running = null;
while(routineStack.Count>0)(routineStack.Pop() as IDisposable)?.Dispose();
(previous as IDisposable)?.Dispose();
}
}
}