- 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>
106 lines
7.0 KiB
C#
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.";
|
|
}
|
|
}
|