Files
AIGame/AIClient/AgentScripts/VerifyG3.cs
T
udxandClaude Opus 5.5 89e0fa246f Add formal enemies, remaining formal partners and synthesized UI SFX
- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to
  buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting.
- Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn):
  Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type
  in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes.
- Formal partners for all ten pets in the arena and portraits, including new
  VineSprite, ThunderLeopard and TideWhale.
- Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands,
  configurable Unity run_script timeout, companion static stage.
- Also includes concurrent work from another tool in this working tree
  (FlameLion, enemy combat FX, challenge mode, home explorer, server updates).
- Resources registered in Doc/ResDesc_*.md; verified on isolated :18081.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:23:55 +08:00

298 lines
16 KiB
C#

using System;
using System.Collections;
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 Newtonsoft.Json;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>G3 guide copy and an editor-only battle frame sample. Not a WeChat device pass.</summary>
public static class VerifyG3
{
const string Endpoint = "http://127.0.0.1:18081";
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 = 30)
{
var end = DateTime.UtcNow.AddSeconds(seconds);
while (!condition())
{
Check(DateTime.UtcNow < end, "G3 timed out: " + Flow?.Page);
await Task.Delay(100);
}
}
static Button Button(string name) => UnityEngine.Object.FindObjectsOfType<Button>().Single(b => b.name == "Btn_" + name && b.gameObject.activeInHierarchy);
static void Click(string name)
{
var b = Button(name);
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 + " by " + (hits.Count == 0 ? "nothing" : hits[0].gameObject.name));
ExecuteEvents.Execute(b.gameObject, pointer, ExecuteEvents.pointerClickHandler);
}
public static string Domain()
{
var save = new PrototypeSave();
Check(MainHudView.GuideTask(save) == "发射第一枚弹珠", "Fresh guide");
save.shots = 1; save.tickets = 1;
Check(MainHudView.GuideTask(save) == "用捕捉券获得芽芽", "Capture guide");
save.captureTutorialDone = true;
Check(MainHudView.GuideTask(save) == "用糖果做一次共享训练", "Train guide");
save.level = 2;
Check(MainHudView.GuideTask(save) == "布阵并完成教学关 1-1", "Battle guide");
save.cleared = true;
Check(MainHudView.GuideTask(save) == "领取首通目标奖励", "Claim guide");
save.goalClaimed = true;
Check(MainHudView.GuideTask(save) == "教学目标已完成", "Done guide");
return "PASS first-day guide order.";
}
public static async Task<string> Play()
{
Check(EditorApplication.isPlaying, "Enter Play Mode first");
await Until(() => Flow?.Game != null && !Flow.IsBusy);
SessionState.SetString("AIGame.G3.original", JsonUtility.ToJson(Flow.Game.State));
SessionState.SetString("AIGame.G3.originalEndpoint", Get<string>("_httpBase"));
int quality = PrototypeClientPrefs.Quality;
SessionState.SetInt("AIGame.G3.quality", quality);
PrototypeClientPrefs.Quality = 0;
AuthResponse account = null; string error = null; bool done = false;
string user = "g3_" + Guid.NewGuid().ToString("N");
string dir = Path.Combine(Application.dataPath, "../../Tools/output/g3");
Directory.CreateDirectory(dir);
File.WriteAllText(Path.Combine(dir, "account.txt"), user);
Flow.StartCoroutine(AuthApi.Register(Endpoint, user, "isolated-g3-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.Page == "Home");
var task = UnityEngine.Object.FindObjectsOfType<TextMeshProUGUI>().First(t => t.name == "Txt_Task" && t.gameObject.activeInHierarchy);
Check(task.text == "发射第一枚弹珠", "Home guide was " + task.text);
Click("Adventure");
await Until(() => Flow.Page == "UI_PrototypeStages");
Click("Stage1");
await Until(() => Flow.Page == "UI_PrototypeFormation");
Click("StartBattle");
await Until(() => Flow.Page == "UI_PrototypeBattle" && Flow.Battle != null, 40);
var samples = new List<float>();
int heroes = 0, enemies = 0;
var arena = Get<GameObject>("_arena");
Flow.StartCoroutine(Sample(samples, () =>
{
heroes = Math.Max(heroes, ActiveChildren(arena, "Heroes"));
enemies = Math.Max(enemies, ActiveChildren(arena, "Enemies"));
}));
await Until(() => samples.Count >= 90 || Flow.Battle.Finished, 20);
Check(samples.Count >= 30, "Too few battle frames");
Check(heroes >= 1 && heroes <= 6, "Hero count " + heroes);
Check(enemies <= 40, "Enemy count " + enemies);
Click("Abandon");
await Task.Delay(200);
Click("Ok");
await Until(() => UnityEngine.Object.FindObjectsOfType<TextMeshProUGUI>().Any(t => t.name == "Txt_ResultInfo" && t.gameObject.activeInHierarchy && t.text.Contains("建议:")));
string advice = UnityEngine.Object.FindObjectsOfType<TextMeshProUGUI>().First(t => t.name == "Txt_ResultInfo" && t.gameObject.activeInHierarchy).text;
float sum = samples.Sum();
var report = new
{
device = "Unity Editor " + SystemInfo.operatingSystem + " / " + SystemInfo.graphicsDeviceName,
quality = "流畅",
note = "Editor sample only. Does not satisfy the WeChat 30 FPS target.",
frames = samples.Count,
averageFps = Math.Round(samples.Count / sum, 1),
minFrameMs = Math.Round(samples.Min() * 1000, 2),
maxFrameMs = Math.Round(samples.Max() * 1000, 2),
heroes,
enemies,
advice
};
File.WriteAllText(Path.Combine(Application.dataPath, "../../Doc/Design/G3/editor_frame_sample.json"), JsonConvert.SerializeObject(report, Formatting.Indented));
PrototypeClientPrefs.Quality = quality;
return "PASS guide, abandon advice, heroes " + heroes + ", enemies " + enemies + ", editor avg fps " + report.averageFps;
}
static IEnumerator Sample(List<float> samples, Action observe)
{
while (samples.Count < 90 && Flow.Battle != null && !Flow.Battle.Finished)
{
yield return null;
samples.Add(Time.unscaledDeltaTime);
observe();
}
}
static int ActiveChildren(GameObject arena, string name)
{
var root = arena.transform.Find(name);
if (root == null) return 0;
int count = 0;
for (int i = 0; i < root.childCount; i++)
if (root.GetChild(i).gameObject.activeInHierarchy) count++;
return count;
}
static async Task Shot()
{
int shots = Flow.Game.State.shots;
Click("Fire");
await Until(() => !Flow.IsBusy && Flow.Game.State.shots == shots + 1 && string.IsNullOrEmpty(Flow.Game.State.pendingShot), 20);
}
static string HomeTask() => UnityEngine.Object.FindObjectsOfType<TextMeshProUGUI>().First(t => t.name == "Txt_Task" && t.gameObject.activeInHierarchy).text;
static async Task Home(string guide)
{
if (Flow.Page != "Home") { Click("Back"); await Until(() => Flow.Page == "Home" && !Flow.IsBusy); }
Check(MainHudView.GuideTask(Flow.Game.State) == guide, "Guide state was " + MainHudView.GuideTask(Flow.Game.State));
Check(HomeTask() == guide, "Home guide was " + HomeTask());
await Task.Delay(200);
}
public static async Task<string> Loop()
{
Check(EditorApplication.isPlaying, "Enter Play Mode first");
await Until(() => Flow?.Game != null && !Flow.IsBusy);
SessionState.SetString("AIGame.G3.original", JsonUtility.ToJson(Flow.Game.State));
SessionState.SetString("AIGame.G3.originalEndpoint", Get<string>("_httpBase"));
AuthResponse account = null; string error = null; bool done = false;
string user = "g3loop_" + Guid.NewGuid().ToString("N");
string dir = Path.Combine(Application.dataPath, "../../Tools/output/g3");
Directory.CreateDirectory(dir);
File.WriteAllText(Path.Combine(dir, "account.txt"), user);
Flow.StartCoroutine(AuthApi.Register(Endpoint, user, "isolated-g3-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.Page == "Home");
var fresh = Flow.Game.State;
Check(fresh.balls == 20 && fresh.candy == 0 && fresh.coins == 0 && fresh.tickets == 0 && fresh.level == 1 && fresh.shots == 0, "Fresh teaching wallet wrong");
await Home("发射第一枚弹珠");
Click("NavPinball");
await Until(() => Flow.Page == "UI_PrototypePinball");
await Shot();
Check(Flow.Game.State.tickets == 1, "First shot did not grant a ticket");
int extra = 0;
while (Flow.Game.State.candy < 30)
{
extra++;
Check(extra <= 6, "Could not reach training candy");
await Shot();
}
int shotCandy = Flow.Game.State.candy, shotCoins = Flow.Game.State.coins, shots = Flow.Game.State.shots;
await Home("用捕捉券获得芽芽");
Click("NavPinball");
await Until(() => Flow.Page == "UI_PrototypePinball");
Click("Capture");
await Until(() => Flow.Page == "UI_PrototypeCapture");
Click("Capture");
await Until(() => !Flow.IsBusy && Flow.Game.State.captureTutorialDone && Flow.Game.State.stages[2] == 1 && Flow.Game.State.tickets == 0);
Click("Ok");
await Until(() => Flow.Page == "UI_PrototypePets" && !Flow.IsBusy);
Click("Train");
await Until(() => !Flow.IsBusy && Flow.Game.State.level == 2);
Check(Flow.Game.State.candy == shotCandy - 30, "Training did not spend 30 candy");
await Home("布阵并完成教学关 1-1");
int coins = Flow.Game.State.coins, candy = Flow.Game.State.candy, balls = Flow.Game.State.balls;
Click("Adventure");
await Until(() => Flow.Page == "UI_PrototypeStages");
Click("Stage1");
await Until(() => Flow.Page == "UI_PrototypeFormation");
Check(Flow.Game.State.formation[0] == 0 && Flow.Game.State.formation[1] == 1, "Teaching formation changed");
Click("StartBattle");
await Until(() => Flow.Page == "UI_PrototypeBattle" && Flow.Battle != null, 40);
Click("Speed");
await Until(() => Flow.Battle.Finished && !Flow.IsBusy && Flow.Game.State.cleared, 90);
Check(Flow.Battle.Won, "Stage 1-1 was not won");
Check(Flow.Game.State.coins == coins + 80 && Flow.Game.State.candy == candy + 40 && Flow.Game.State.balls == balls + 3, "First-clear reward mismatch");
Click("Home");
await Home("领取首通目标奖励");
Click("Tasks");
await Until(() => Flow.Page == "UI_PrototypeTasks");
Click("ClaimGoal");
await Until(() => !Flow.IsBusy && Flow.Game.State.goalClaimed);
Check(Flow.Game.State.balls == balls + 8, "Teaching goal was not +5");
Click("Ok");
Click("Back");
await Until(() => Flow.Page == "Home" && !Flow.IsBusy);
Check(MainHudView.GuideTask(Flow.Game.State) == "教学目标已完成", "Goal flag did not finish the guide");
int ready = 0;
if (Flow.Game.State.dailyProgress[0] >= 3) ready++;
if (Flow.Game.State.dailyProgress[1] >= 1) ready++;
Check(HomeTask() == ready + "项奖励可领取", "Home did not switch to daily tasks: " + HomeTask());
Check(Flow.Game.State.coins == shotCoins + 80 && Flow.Game.State.candy == shotCandy + 10 && Flow.Game.State.tickets == 0 && Flow.Game.State.balls == 20 - shots + 8, "Teaching wallet identity failed");
var report = new
{
account = user,
note = "Teaching loop only. Mail, sign-in, daily claims, shop and stages 1-2 through 1-5 are not included, so this is not the 79-ball day ceiling.",
shots,
extraShotsAfterTicket = extra,
shotCandy,
shotCoins,
balls = Flow.Game.State.balls,
candy = Flow.Game.State.candy,
coins = Flow.Game.State.coins,
tickets = Flow.Game.State.tickets,
level = Flow.Game.State.level,
budBudOwned = Flow.Game.State.stages[2] == 1,
cleared = Flow.Game.State.cleared,
goalClaimed = Flow.Game.State.goalClaimed,
dailyReady = ready,
homeAfterGoal = HomeTask()
};
File.WriteAllText(Path.Combine(Application.dataPath, "../../Doc/Design/G3/first_day_loop.json"), JsonConvert.SerializeObject(report, Formatting.Indented));
return "PASS teaching loop: " + shots + " shots, capture, train Lv.2, 1-1 clear, goal +5. Wallet balls " + report.balls + ", candy " + report.candy + ", coins " + report.coins + ".";
}
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.G3.originalEndpoint", "http://127.0.0.1:8080"));
Set("_boundProfile", 0L);
PrototypeClientPrefs.Quality = SessionState.GetInt("AIGame.G3.quality", 1);
Flow.Handle("Home");
await Until(() => Flow.Game != null && !Flow.IsBusy);
var original = JsonUtility.FromJson<PrototypeSave>(SessionState.GetString("AIGame.G3.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 G3 test");
return "PASS cleanup: real account restored; G3 check used isolated :18081 only.";
}
}