Files
AIGame/AIClient/AgentScripts/VerifyFormalHeroes.cs
T

225 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using HotUpdate.Gameplay;
using HotUpdate.Net;
using HotUpdate.UI;
using UnityEditor;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>Checks that formal partner art replaced the battle stand-ins and animates in a real isolated battle.</summary>
public static class VerifyFormalHeroes
{
const string Endpoint = "http://127.0.0.1:18081";
const string Arena = "Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab";
static PrototypeFlow Flow => UnityEngine.Object.FindObjectOfType<PrototypeFlow>();
static void Check(bool value, string message) { if (!value) throw new Exception(message); }
static T Get<T>(string name) => (T)typeof(PrototypeFlow).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(Flow);
static void Set(string name, object value) => typeof(PrototypeFlow).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic).SetValue(Flow, value);
static async Task Until(Func<bool> condition, int seconds = 25)
{
var end = DateTime.UtcNow.AddSeconds(seconds);
while (!condition()) { Check(DateTime.UtcNow < end, "Formal heroes timed out: " + Flow?.Page); await Task.Delay(100); }
}
static void Click(string name)
{
var b = UnityEngine.Object.FindObjectsOfType<Button>().Single(x => x.name == "Btn_" + name && x.gameObject.activeInHierarchy);
Check(b.interactable, "Disabled " + name); Canvas.ForceUpdateCanvases();
var rect = b.GetComponent<RectTransform>();
var pointer = new PointerEventData(EventSystem.current) { position = RectTransformUtility.WorldToScreenPoint(null, rect.TransformPoint(rect.rect.center)), button = PointerEventData.InputButton.Left };
var hits = new List<RaycastResult>(); EventSystem.current.RaycastAll(pointer, hits);
Check(hits.Count > 0 && ExecuteEvents.GetEventHandler<IPointerClickHandler>(hits[0].gameObject) == b.gameObject, "Blocked " + name);
ExecuteEvents.Execute(b.gameObject, pointer, ExecuteEvents.pointerClickHandler);
}
static async Task Capture(string name)
{
await Task.Delay(200);
var type = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetType("Unity.Pipeline.Editor.Commands.Capture.CaptureCommands")).First(t => t != null);
type.GetMethod("CaptureGameView", BindingFlags.Public | BindingFlags.Static).Invoke(null, new object[] { 1080, 1920, null, "Screenshots/formal_heroes_" + name + ".png", false, 0, "screen" });
}
/// <summary>Pet ids whose arena hero nests formal art (BuildPrototype.FormalPets).</summary>
static readonly int[] Formal = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
public static string Assets()
{
var arena = AssetDatabase.LoadAssetAtPath<GameObject>(Arena);
var lines = new List<string>();
foreach (int id in Formal)
{
var hero = arena.transform.Find("Heroes/Hero" + id);
var visual = hero.Find("Visual");
Check(visual != null && PrefabUtility.IsAnyPrefabInstanceRoot(visual.gameObject), "Hero" + id + " lacks nested formal prefab");
Check(hero.Cast<Transform>().All(c => c.name == "Visual" || c.name == "VisualT2" || c.name == "T2Crest"), "Hero" + id + " still has primitive parts");
Check(visual.GetComponentsInChildren<Transform>(true).All(t => t.gameObject.layer == 30), "Hero" + id + " visual not on battle layer 30");
var animator = visual.GetComponentInChildren<Animator>(true);
Check(animator != null && animator.runtimeAnimatorController != null && !animator.applyRootMotion, "Hero" + id + " animator missing");
var instance = (GameObject)PrefabUtility.InstantiatePrefab(arena);
try
{
var h = instance.transform.Find("Heroes/Hero" + id); h.gameObject.SetActive(true);
float min = float.MaxValue, max = float.MinValue; var mesh = new Mesh();
foreach (var skin in h.Find("Visual").GetComponentsInChildren<SkinnedMeshRenderer>())
{
skin.BakeMesh(mesh, true);
foreach (var v in mesh.vertices) { float y = h.InverseTransformPoint(skin.transform.TransformPoint(v)).y; min = Mathf.Min(min, y); max = Mathf.Max(max, y); }
}
UnityEngine.Object.DestroyImmediate(mesh);
Check(Mathf.Abs(max - 1.5f) < .05f && Mathf.Abs(min) < .05f, "Hero" + id + " height/ground wrong " + min + ".." + max);
lines.Add("Hero" + id + "=" + PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(visual.gameObject) + " y=" + min.ToString("F2") + ".." + max.ToString("F2"));
}
finally { UnityEngine.Object.DestroyImmediate(instance); }
}
foreach (int id in Enumerable.Range(0, 10).Except(Formal)) Check(arena.transform.Find("Heroes/Hero" + id + "/Visual") == null, "Hero" + id + " unexpectedly formal");
return "PASS " + string.Join("; ", lines);
}
public static Task<string> Play() => PlaySeeded(null, 1);
/// <summary><paramref name="seededUser"/>: an existing isolated :18081 account (password below) whose save was seeded to field later partners.</summary>
public static async Task<string> PlaySeeded(string seededUser, int stage)
{
Check(EditorApplication.isPlaying, "Enter Play Mode first");
await Until(() => Flow?.Game != null && !Flow.IsBusy);
SessionState.SetString("AIGame.FormalHeroes.original", JsonUtility.ToJson(Flow.Game.State));
SessionState.SetString("AIGame.FormalHeroes.originalEndpoint", Get<string>("_httpBase"));
AuthResponse account = null; string error = null; bool done = false;
string user = seededUser ?? "formalheroes_" + Guid.NewGuid().ToString("N");
Directory.CreateDirectory(Path.Combine(Application.dataPath, "../../Tools/output/formal_heroes"));
File.WriteAllText(Path.Combine(Application.dataPath, "../../Tools/output/formal_heroes/account.txt"), user);
Action<AuthResponse, string> onAuth = (r, e) => { account = r; error = e; done = true; };
Flow.StartCoroutine(seededUser != null ? AuthApi.Login(Endpoint, user, "isolated-formal-heroes-password", onAuth) : AuthApi.Register(Endpoint, user, "isolated-formal-heroes-password", onAuth));
await Until(() => done);
Check(account != null, "Isolated auth failed " + error);
var session = new SessionData { Pid = account.pid, Token = account.token, Username = user };
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
Set("_session", new Func<SessionData>(() => session));
Set("_profile", new Func<long>(() => session.Pid));
Set("_httpBase", Endpoint);
Set("_boundProfile", 0L);
PlayerPrefs.DeleteKey("AIGame.G1.pending." + Uri.EscapeDataString(Endpoint) + "." + session.Pid);
Flow.Handle("Home");
await Until(() => Flow.Game != null && !Flow.IsBusy);
Click("Adventure"); await Until(() => Flow.Page == "UI_PrototypeStages");
Click("Stage" + stage); await Until(() => Flow.Page == "UI_PrototypeFormation");
Click("StartBattle"); await Until(() => Flow.Page == "UI_PrototypeBattle" && Flow.Battle != null);
var arena = Get<GameObject>("_arena");
var fielded = Flow.Battle.Heroes.Select(h => h.Pet).Where(p => Formal.Contains(p)).Distinct().ToArray();
Check(fielded.Length > 0, "No formal partner fielded in 1-" + stage);
// Tier-2 partners show VisualT2 (same rig + evolution accessory); read whichever visual is currently shown.
Animator Shown(int p) { var hero = arena.transform.Find("Heroes/Hero" + p); var t2 = hero.Find("VisualT2"); return (t2 != null && t2.gameObject.activeSelf ? t2 : hero.Find("Visual")).GetComponentInChildren<Animator>(); }
await Task.Delay(300);
var evolved = Flow.Battle.Heroes.Where(h => h.Tier == 2 && arena.transform.Find("Heroes/Hero" + h.Pet + "/VisualT2") != null).Select(h => h.Pet).Distinct().ToArray();
foreach (var p in evolved) Check(arena.transform.Find("Heroes/Hero" + p + "/VisualT2").gameObject.activeSelf && !arena.transform.Find("Heroes/Hero" + p + "/Visual").gameObject.activeSelf, "Hero" + p + " tier 2 does not show VisualT2");
var animators = fielded.ToDictionary(p => p, Shown);
var attacked = new HashSet<int>(); var end = DateTime.UtcNow.AddSeconds(stage > 1 ? 45 : 20);
string shot = seededUser != null ? "seeded_" : "";
await Capture(shot + "idle"); bool attackShot = false;
while (attacked.Count < fielded.Length && DateTime.UtcNow < end && !Flow.Battle.Finished)
{
foreach (var kv in animators) if (kv.Value.isActiveAndEnabled && kv.Value.GetCurrentAnimatorStateInfo(0).IsName("Attack")) attacked.Add(kv.Key);
if (attacked.Count >= 1 && !attackShot) { attackShot = true; await Capture(shot + "attack"); }
await Task.Delay(50);
}
Check(attacked.Count == fielded.Length, "Attack never played for " + string.Join(",", fielded.Except(attacked)));
Click("Speed");
await Until(() => animators.Values.All(a => !a.isActiveAndEnabled || Mathf.Approximately(a.speed, 2)), 5);
return "PASS formal partners " + string.Join(",", fielded) + " (T2 visuals " + string.Join(",", evolved) + ") fielded in 1-" + stage + ", played Attack and follow 2x speed; screenshots formal_heroes_" + shot + "idle/attack.png";
}
public static async Task<string> OpenSeedAccount()
{
Check(EditorApplication.isPlaying, "Enter Play Mode first");
await Until(() => Flow?.Game != null && !Flow.IsBusy);
SessionState.SetString("AIGame.FormalHeroes.original", JsonUtility.ToJson(Flow.Game.State));
SessionState.SetString("AIGame.FormalHeroes.originalEndpoint", Get<string>("_httpBase"));
AuthResponse account = null; string error = null; bool done = false;
string user = "formalheroes_" + Guid.NewGuid().ToString("N");
Directory.CreateDirectory(Path.Combine(Application.dataPath, "../../Tools/output/formal_heroes"));
File.WriteAllText(Path.Combine(Application.dataPath, "../../Tools/output/formal_heroes/account.txt"), user);
Flow.StartCoroutine(AuthApi.Register(Endpoint, user, "isolated-formal-heroes-password", (r, e) => { account = r; error = e; done = true; }));
await Until(() => done);
Check(account != null, "Isolated register failed " + error);
var session = new SessionData { Pid = account.pid, Token = account.token, Username = user };
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
Set("_session", new Func<SessionData>(() => session));
Set("_profile", new Func<long>(() => session.Pid));
Set("_httpBase", Endpoint);
Set("_boundProfile", 0L);
PlayerPrefs.DeleteKey("AIGame.G1.pending." + Uri.EscapeDataString(Endpoint) + "." + session.Pid);
Flow.Handle("Home");
await Until(() => Flow.Game != null && !Flow.IsBusy && Flow.Game.State.schema >= 9);
return user;
}
/// <summary>Reload the isolated account after its save was seeded, then field SparkBird and BubbleTurtle in 1-1.</summary>
public static async Task<string> BattleBirdTurtle()
{
Check(EditorApplication.isPlaying, "Enter Play Mode first");
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
Set("_boundProfile", 0L);
Flow.Handle("Home");
await Until(() => Flow.Game != null && !Flow.IsBusy && Flow.Game.State.stages[3] == 1 && Flow.Game.State.stages[4] == 1 && Flow.Game.State.formation[0] == 4 && Flow.Game.State.formation[1] == 3);
Click("Adventure"); await Until(() => Flow.Page == "UI_PrototypeStages");
Click("Stage1"); await Until(() => Flow.Page == "UI_PrototypeFormation");
Check(Flow.Game.State.formation[0] == 4 && Flow.Game.State.formation[1] == 3, "Seeded formation was replaced");
Click("StartBattle"); await Until(() => Flow.Page == "UI_PrototypeBattle" && Flow.Battle != null, 40);
var arena = Get<GameObject>("_arena");
var fielded = Flow.Battle.Heroes.Select(h => h.Pet).OrderBy(p => p).ToArray();
Check(fielded.SequenceEqual(new[] { 3, 4 }), "1-1 fielded " + string.Join(",", fielded));
Check(Flow.Battle.Heroes.Single(h => h.Pet == 4).Slot == 0 && Flow.Battle.Heroes.Single(h => h.Pet == 3).Slot == 1, "Turtle is not in front of the bird");
// Tier-2 partners show VisualT2 (same rig + evolution accessory); read whichever visual is currently shown.
Animator Shown(int p) { var hero = arena.transform.Find("Heroes/Hero" + p); var t2 = hero.Find("VisualT2"); return (t2 != null && t2.gameObject.activeSelf ? t2 : hero.Find("Visual")).GetComponentInChildren<Animator>(); }
await Task.Delay(300);
var evolved = Flow.Battle.Heroes.Where(h => h.Tier == 2 && arena.transform.Find("Heroes/Hero" + h.Pet + "/VisualT2") != null).Select(h => h.Pet).Distinct().ToArray();
foreach (var p in evolved) Check(arena.transform.Find("Heroes/Hero" + p + "/VisualT2").gameObject.activeSelf && !arena.transform.Find("Heroes/Hero" + p + "/Visual").gameObject.activeSelf, "Hero" + p + " tier 2 does not show VisualT2");
var animators = fielded.ToDictionary(p => p, Shown);
foreach (var kv in animators) Check(kv.Value != null && arena.transform.Find("Heroes/Hero" + kv.Key).gameObject.activeInHierarchy, "Hero" + kv.Key + " inactive");
var attacked = new HashSet<int>(); var end = DateTime.UtcNow.AddSeconds(25);
await Capture("bird_turtle_idle"); bool attackShot = false;
while (attacked.Count < fielded.Length && DateTime.UtcNow < end && !Flow.Battle.Finished)
{
foreach (var kv in animators) if (kv.Value.isActiveAndEnabled && kv.Value.GetCurrentAnimatorStateInfo(0).IsName("Attack")) attacked.Add(kv.Key);
if (attacked.Count >= 1 && !attackShot) { attackShot = true; await Capture("bird_turtle_attack"); }
await Task.Delay(50);
}
Check(attacked.Count == fielded.Length, "Attack never played for " + string.Join(",", fielded.Except(attacked)));
Click("Speed");
await Until(() => animators.Values.All(a => !a.isActiveAndEnabled || Mathf.Approximately(a.speed, 2)), 5);
return "PASS SparkBird and BubbleTurtle fielded in 1-1, feet on the lane, Attack played, 2x speed followed.";
}
public static async Task<string> Abandon()
{
if (Flow?.Page == "UI_PrototypeBattle" && Flow.Battle != null && !Flow.Battle.Finished)
{
Click("Abandon");
await Task.Delay(200);
Click("Ok");
await Until(() => Flow.Battle == null || Flow.Battle.Finished && !Flow.IsBusy);
}
return "Battle settled";
}
public static async Task<string> Cleanup()
{
var net = UnityEngine.Object.FindObjectOfType<GameNet>();
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
Set("_session", new Func<SessionData>(() => net.Session));
Set("_profile", new Func<long>(() => net.Session?.Pid ?? 0));
Set("_httpBase", SessionState.GetString("AIGame.FormalHeroes.originalEndpoint", "http://127.0.0.1:8080"));
Set("_boundProfile", 0L);
Flow.Handle("Home");
await Until(() => Flow.Game != null && !Flow.IsBusy);
var original = JsonUtility.FromJson<PrototypeSave>(SessionState.GetString("AIGame.FormalHeroes.original", ""));
Check(Flow.Game.State.coins == original.coins && Flow.Game.State.candy == original.candy && Flow.Game.State.shots == original.shots && Flow.Game.State.level == original.level, "Real account was changed by formal heroes test");
return "PASS cleanup: real account restored; battle check used isolated :18081 only.";
}
}