Files
AIGame/AIClient/AgentScripts/VerifyCamp.cs
T

127 lines
12 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 TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public static class VerifyCamp
{
static PrototypeFlow Flow => UnityEngine.Object.FindObjectOfType<PrototypeFlow>();
static void Check(bool b,string message) {if(!b) throw new Exception(message);}
static T Get<T>(string field)=>(T)typeof(PrototypeFlow).GetField(field,BindingFlags.NonPublic|BindingFlags.Instance).GetValue(Flow);
static void Set(string field,object value)=>typeof(PrototypeFlow).GetField(field,BindingFlags.NonPublic|BindingFlags.Instance).SetValue(Flow,value);
static async Task Until(Func<bool> condition,int seconds=20)
{var end=DateTime.UtcNow.AddSeconds(seconds);while(!condition()){Check(DateTime.UtcNow<end,"Camp timed out at "+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,"Camp click blocked "+name);
ExecuteEvents.Execute(b.gameObject,pointer,ExecuteEvents.pointerClickHandler);
}
static async Task Capture(string name)
{
await Task.Delay(150);
var type=AppDomain.CurrentDomain.GetAssemblies().Select(a=>a.GetType("Unity.Pipeline.Editor.Commands.Capture.CaptureCommands")).First(t=>t!=null);
type.GetMethod("CaptureGameView",BindingFlags.Static|BindingFlags.Public).Invoke(null,new object[]{1080,1920,null,"Screenshots/camp_"+name+".png",false,0,"screen"});
}
static async Task SyncHome()
{Flow.Handle("Home");Set("_needsSync",true);Flow.Handle("Home");await Until(()=>!Flow.IsBusy);await Task.Delay(150);}
public static string Assets()
{
foreach(string screen in new[]{"Camp","Build"})
{
var prefab=AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/UI/Prefab/UI_Prototype"+screen+".prefab");Check(prefab!=null,"Missing camp prefab");
Check(prefab.GetComponentsInChildren<TextMeshProUGUI>(true).All(t=>t.font!=null),"Missing camp font");
foreach(var i in prefab.GetComponentsInChildren<Image>(true).Where(i=>i.type==Image.Type.Sliced)) Check(i.sprite!=null&&i.sprite.border.x>0&&i.sprite.border.y>0,"Invalid camp slice");
}
for(int i=0;i<3;i++)
{
Check(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/Scenes/Prototype/CampDecoration"+i+".prefab")!=null,"Missing decoration");
Check(AssetDatabase.LoadAssetAtPath<Sprite>("Assets/Bundles/UI/Texture/Prototype/proto_decor_"+i+".png")!=null,"Missing decoration portrait");
}
return "PASS two camp JSON prefabs, fonts/nine-slices and three authored decorations/portraits.";
}
public static async Task<string> Start()
{
await Until(()=>Flow?.Game!=null&&!Flow.IsBusy);
SessionState.SetString("AIGame.G1.original",JsonUtility.ToJson(Flow.Game.State));SessionState.SetString("AIGame.G1.originalEndpoint",Get<string>("_httpBase"));
AuthResponse account=null;string error=null;bool done=false;string user="g1live_camp_"+Guid.NewGuid().ToString("N");
Flow.StartCoroutine(AuthApi.Register("http://127.0.0.1:18081",user,"isolated-g1-test-password",(r,e)=>{account=r;error=e;done=true;}));
await Until(()=>done);Check(account!=null,"Camp register failed "+error);
File.WriteAllText(Path.Combine(Application.dataPath,"../../Tools/output/g1-live/account.txt"),user);
var session=new SessionData{Pid=account.pid,Token=account.token,Username=user};
Set("_session",new Func<SessionData>(()=>session));Set("_profile",new Func<long>(()=>session.Pid));Set("_httpBase","http://127.0.0.1:18081");Set("_boundProfile",0L);
Flow.Handle("Home");await Until(()=>!Flow.IsBusy);await Task.Delay(150);
Click("NavHome");await Until(()=>Flow.Page=="UI_PrototypeCamp");
Check(!Button("Construction").interactable&&!Button("Interact").interactable&&!Button("Training").interactable&&!Button("BallTree").interactable,"Fresh facilities unlocked too soon");await Capture("locked");
Click("Tent");await Task.Delay(100);Click("Ok");Click("Back");await Task.Delay(150);
Click("NavPinball");await Until(()=>Flow.Page=="UI_PrototypePinball");Click("Fire");await Until(()=>!Flow.IsBusy&&Flow.Game.State.shots==1);
Click("Back");await Task.Delay(150);Click("NavHome");await Until(()=>Flow.Page=="UI_PrototypeCamp");
Check(Button("Training").interactable&&!Button("BallTree").interactable,"Teaching training gate wrong");
Click("Training");await Until(()=>Flow.Page=="UI_PrototypePets");Check(Flow.Game.State.level==1,"Training doorway charged on open");
Click("Back");return "PASS real camp HUD entry, locked facilities, tent profile and first-shot training doorway without extra cost.";
}
public static async Task<string> Rest()
{
await SyncHome();Click("NavHome");await Until(()=>Flow.Page=="UI_PrototypeCamp");
Check(Button("BallTree").interactable&&Button("Interact").interactable&&!Button("Construction").interactable,"1-3 fixture unlock wrong");
int coins=Flow.Game.State.coins,balls=Flow.Game.State.balls,candy=Flow.Game.State.candy,tickets=Flow.Game.State.tickets;
Click("RestPet3");Click("Interact");await Until(()=>!Flow.IsBusy&&Flow.Game.State.campInteractions==1);
Check(Flow.Game.State.restPet==3&&Flow.Game.State.coins==coins&&Flow.Game.State.balls==balls&&Flow.Game.State.candy==candy&&Flow.Game.State.tickets==tickets,"Rest interaction awarded resources");
await Task.Delay(800);await Capture("rest");Click("BallTree");await Until(()=>Flow.Page=="UI_PrototypePinball");
Check(Flow.Game.State.balls==balls,"Ball tree opening duplicated production");Click("Back");return "PASS 1-3 rest gate, persisted electric partner greeting and no rewards; ball tree shares same inventory.";
}
public static async Task<string> BuildFlow()
{
await SyncHome();Click("NavHome");await Until(()=>Flow.Page=="UI_PrototypeCamp");Check(Button("Construction").interactable,"1-5 build still locked");await Capture("unlocked");
Click("Construction");await Until(()=>Flow.Page=="UI_PrototypeBuild");Check(Flow.Game.State.coins==1000,"Camp fixture wallet wrong");await Capture("catalogue");
Click("Decoration0");Click("CampSlot0");await Capture("preview");Check(Flow.Game.State.coins==1000&&Flow.Game.State.decorations[0]==0,"Preview purchased early");
Click("CancelPreview");Check(Flow.Game.State.coins==1000&&Flow.Game.State.campLayout.All(i=>i==-1)&&!Button("Place").interactable,"Cancel preview mutated authoritative state");
Click("CampSlot0");Click("Place");await Until(()=>!Flow.IsBusy&&Flow.Game.State.campLayout[0]==0);Check(Flow.Game.State.coins==880&&Flow.Game.State.decorations[0]==1,"Purchase mismatch");await Capture("placed");
Click("Decoration0");Click("CampSlot1");Click("Place");await Until(()=>!Flow.IsBusy&&Flow.Game.State.campLayout[1]==0);Check(Flow.Game.State.coins==880&&Flow.Game.State.campLayout[0]==-1,"Move charged or duplicated");
Click("CampSlot1");Click("Store");await Until(()=>!Flow.IsBusy&&Flow.Game.State.campLayout[1]==-1);Check(Flow.Game.State.decorations[0]==1&&Flow.Game.State.coins==880,"Store destroyed ownership or refunded");await Capture("warehouse");
Click("Decoration0");Click("CampSlot1");Click("Place");await Until(()=>!Flow.IsBusy&&Flow.Game.State.campLayout[1]==0);
Click("Decoration1");Click("CampSlot1");Check(!Button("Place").interactable,"Occupied preview allows overwrite");Click("CampSlot2");
var api=Get<PrototypeApi>("_api");var command=api.Command("camp.place",Flow.Game.State.revision,slot:2,item:1);PrototypeReply response=null;string error=null;
Flow.StartCoroutine(api.Send(command,(r,e)=>{response=r;error=e;}));await Until(()=>response!=null||error!=null);Check(response?.ok==true,"Purchase before lost ack failed");
var session=Get<Func<SessionData>>("_session")();string key="AIGame.G1.pending."+Uri.EscapeDataString("http://127.0.0.1:18081")+"."+session.Pid;
PlayerPrefs.SetString(key,JsonUtility.ToJson(command));PlayerPrefs.Save();Set("_needsSync",true);Flow.Handle("Home");await Until(()=>!Flow.IsBusy&&Get<PrototypeReply>("_reply").replayed);
Check(Flow.Game.State.coins==700&&Flow.Game.State.campLayout.SequenceEqual(new[]{-1,0,1})&&!api.HasPending,"Replayed purchase charged twice or lost layout");await Capture("recovery");Click("Ok");
Set("_boundProfile",0L);Flow.Handle("Home");await Until(()=>!Flow.IsBusy);await Task.Delay(150);Click("NavHome");await Until(()=>Flow.Page=="UI_PrototypeCamp");Click("Construction");await Until(()=>Flow.Page=="UI_PrototypeBuild");
Check(Flow.Game.State.coins==700&&Flow.Game.State.campLayout.SequenceEqual(new[]{-1,0,1})&&Flow.Game.State.restPet==3&&Flow.Game.State.campInteractions==1,"Camp session reattach lost state");await Capture("restored");
return "PASS live build: preview/cancel free, buy 120 once, move/store/warehouse free, occupied refusal, lost-ack purchase 180 once and layout/interaction restored after reattach.";
}
public static async Task<string> ReviewPreview()
{
await SyncHome();Click("NavHome");await Until(()=>Flow.Page=="UI_PrototypeCamp");Click("Construction");await Until(()=>Flow.Page=="UI_PrototypeBuild");
int coins=Flow.Game.State.coins;Click("Decoration2");Click("CampSlot0");Check(Button("Place").interactable,"Affordable preview disabled");await Capture("preview");
Click("CancelPreview");Check(Flow.Game.State.coins==coins&&!Button("Place").interactable,"Final preview cancellation changed wallet");
return "PASS final slot label spacing, affordable preview and cancellation without purchase.";
}
public static async Task<string> Insufficient()
{
await SyncHome();Click("NavHome");await Until(()=>Flow.Page=="UI_PrototypeCamp");Click("Construction");await Until(()=>Flow.Page=="UI_PrototypeBuild");
Click("Decoration2");Click("CampSlot0");Check(Flow.Game.State.coins==239&&!Button("Place").interactable,"Insufficient preview allowed purchase");
Check(UnityEngine.Object.FindObjectsOfType<TextMeshProUGUI>().Single(t=>t.name=="Txt_Preview").text.Contains("当前不足"),"Missing insufficient funds reason");await Capture("insufficient");
Click("CancelPreview");return "PASS insufficient wallet displays reason, disables confirmation and allows free cancel.";
}
public static async Task<string> Navigation()
{
await Until(()=>!Flow.IsBusy);Flow.Handle("Home");await Task.Delay(150);Click("NavHome");await Until(()=>Flow.Page=="UI_PrototypeCamp");
Check(Button("Construction").interactable,"Final HUD camp routing failed");Click("Back");await Until(()=>Flow.Page=="Home");
return "PASS original MainHUD.Home event routes to camp; Back returns to illustrated home.";
}
}