Files
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

106 lines
7.0 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>Play Mode check that real clicks and server results trigger the expected UI one-shots, and the SFX toggle mutes them.</summary>
public static class VerifyUiSfx
{
const string Endpoint = "http://127.0.0.1:18081";
static PrototypeFlow Flow => UnityEngine.Object.FindObjectOfType<PrototypeFlow>();
static UiSfx Player => UnityEngine.Object.FindObjectOfType<UiSfx>();
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 T Sfx<T>(string name) => (T)typeof(UiSfx).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(Player);
static async Task Until(Func<bool> condition, int seconds = 25)
{
var end = DateTime.UtcNow.AddSeconds(seconds);
while (!condition()) { Check(DateTime.UtcNow < end, "UI SFX 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);
}
/// <summary>Clicks a button and returns the last UiSound it caused (null when nothing played).</summary>
static async Task<UiSound?> Sound(string button, Func<bool> settled)
{
float before = Sfx<float>("_lastAt");
Click(button); await Until(settled); await Task.Delay(250);
return Mathf.Approximately(Sfx<float>("_lastAt"), before) ? (UiSound?)null : Sfx<UiSound>("_last");
}
public static async Task<string> Play()
{
Check(EditorApplication.isPlaying, "Enter Play Mode first");
await Until(() => Flow?.Game != null && !Flow.IsBusy && Player != null);
Check(Player.Source != null && Enum.GetValues(typeof(UiSound)).Cast<UiSound>().All(s => Player.Clip(s) != null), "UiSfx prefab not loaded with clips");
SessionState.SetString("AIGame.UiSfx.original", JsonUtility.ToJson(Flow.Game.State));
SessionState.SetString("AIGame.UiSfx.originalEndpoint", Get<string>("_httpBase"));
SessionState.SetBool("AIGame.UiSfx.sfx", PrototypeClientPrefs.Sfx);
PrototypeClientPrefs.Sfx = true;
AuthResponse account = null; string error = null; bool done = false;
string user = "uisfx_" + Guid.NewGuid().ToString("N");
Directory.CreateDirectory(Path.Combine(Application.dataPath, "../../Tools/output/ui_sfx_v2"));
File.WriteAllText(Path.Combine(Application.dataPath, "../../Tools/output/ui_sfx_v2/account.txt"), user);
Flow.StartCoroutine(AuthApi.Register(Endpoint, user, "isolated-ui-sfx-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);
Flow.Handle("Home");
await Until(() => Flow.Game != null && !Flow.IsBusy);
var log = new List<string>();
void Expect(string step, UiSound? actual, UiSound? expected) { log.Add(step + "=" + (actual?.ToString() ?? "none")); Check(actual == expected, step + " played " + (actual?.ToString() ?? "nothing") + ", expected " + (expected?.ToString() ?? "nothing")); }
Expect("hud.settings", await Sound("Settings", () => Flow.Page == "UI_PrototypeSettings"), UiSound.Open);
Expect("settings.quality", await Sound("CycleQuality", () => true), UiSound.Toggle);
Click("CycleQuality"); Click("CycleQuality"); await Task.Delay(100);
Expect("settings.sfxOff", await Sound("ToggleSfx", () => !PrototypeClientPrefs.Sfx), UiSound.Toggle);
Expect("muted.close", await Sound("Close", () => Flow.Page == "Home"), null);
Expect("muted.settings", await Sound("Settings", () => Flow.Page == "UI_PrototypeSettings"), null);
Click("ToggleSfx"); await Until(() => PrototypeClientPrefs.Sfx);
Expect("settings.close", await Sound("Close", () => Flow.Page == "Home"), UiSound.Close);
Expect("hud.rewards", await Sound("Rewards", () => Flow.Page == "UI_PrototypeChest"), UiSound.Open);
Expect("chest.claimDaily", await Sound("ClaimDailyChest", () => !Flow.IsBusy && Flow.Game.State.chestDailyClaimed == 1), UiSound.Claim);
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
Expect("chest.back", await Sound("Back", () => Flow.Page == "Home"), UiSound.Close);
return "PASS " + string.Join(", ", log);
}
public static async Task<string> Cleanup()
{
var net = UnityEngine.Object.FindObjectOfType<GameNet>();
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
PrototypeClientPrefs.Sfx = SessionState.GetBool("AIGame.UiSfx.sfx", true);
Set("_session", new Func<SessionData>(() => net.Session));
Set("_profile", new Func<long>(() => net.Session?.Pid ?? 0));
Set("_httpBase", SessionState.GetString("AIGame.UiSfx.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.UiSfx.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 UI SFX test");
return "PASS cleanup: real account and SFX preference restored; checks used isolated :18081 only.";
}
}