160 lines
9.3 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|