Files

115 lines
12 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using HotUpdate.Gameplay;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using Object=UnityEngine.Object;
/// <summary>Isolated T2 asset/pose checks and authoritative battle fixture. No network/account writes.</summary>
public static class VerifyTideWhaleT2
{
const string Animal="Assets/Bundles/Characters/Animal/";
const string T2=Animal+"TideWhale/TideWhale_T2.prefab";
const string Arena="Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab";
static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../.."));
static string Folder=>Path.Combine(Repo,"Doc/Design/Character/TideWhale/T2");
static void Check(bool value,string message){if(!value)throw new Exception(message);}
static void Report(string name,object value)=>File.WriteAllText(Path.Combine(Folder,name+".json"),JsonConvert.SerializeObject(value,Formatting.Indented));
public static string Assets()
{
Check(!EditorApplication.isPlaying,"Check assets in Edit Mode");
var t1=AssetDatabase.LoadAssetAtPath<GameObject>(Animal+"TideWhaleStudio/TideWhaleStudio_Companion.prefab");
var t2=AssetDatabase.LoadAssetAtPath<GameObject>(T2);Check(t2!=null,"Missing T2");
Check(PrefabUtility.GetCorrespondingObjectFromSource(t2)==t1,"Not a variant of the formal Studio T1");
var a=t1.GetComponentInChildren<Animator>(true);var b=t2.GetComponentInChildren<Animator>(true);
Check(a.avatar==b.avatar&&a.runtimeAnimatorController==b.runtimeAnimatorController,"Original rig/controller changed");
var acc=t2.GetComponentsInChildren<Transform>(true).Single(t=>t.name=="T2Accessory");Check(acc.parent.name.StartsWith("bone_"),"Accessory not on bone");
var mesh=acc.GetComponentInChildren<MeshFilter>(true).sharedMesh;int triangles=mesh.triangles.Length/3;
Check(triangles<=520&&mesh.uv.Length==mesh.vertexCount,"Triangle/UV budget invalid");
foreach(var r in acc.GetComponentsInChildren<Renderer>(true))Check(r.sharedMaterial!=null&&r.sharedMaterial.shader.name=="Test/XPbr","Missing accessory XPbr");
foreach(string suffix in new[]{"base","mix"}){var tex=AssetDatabase.LoadAssetAtPath<Texture2D>(Animal+"TideWhale/TideWhaleT2/TideWhaleT2_"+suffix+".png");Check(tex!=null&&tex.width==512&&tex.height==512,"Missing 512 texture");}
var arena=AssetDatabase.LoadAssetAtPath<GameObject>(Arena);var hero=arena.transform.Find("Heroes/Hero9");var visual=hero.Find("VisualT2");
Check(visual!=null&&!visual.gameObject.activeSelf&&PrefabUtility.GetCorrespondingObjectFromSource(visual.gameObject)==t2,"Arena has no inactive nested T2");
Check(visual.GetComponentsInChildren<Transform>(true).All(t=>t.gameObject.layer==30),"Battle layer changed");
Check(Vector3.Distance(visual.localScale,hero.Find("Visual").localScale)<1e-5f,"T1/T2 scales differ");
var scene=EditorSceneManager.NewPreviewScene();var instance=(GameObject)PrefabUtility.InstantiatePrefab(t2,scene);
try
{
var animator=instance.GetComponentInChildren<Animator>();animator.enabled=false;
var crown=instance.GetComponentsInChildren<Transform>().Single(t=>t.name=="T2Accessory");var position=crown.localPosition;var rotation=crown.localRotation;var scale=crown.localScale;
foreach(string motion in new[]{"Idle","Walk","Attack"})
{
var clip=AssetDatabase.LoadAssetAtPath<AnimationClip>(Animal+"TideWhale/Clips/"+motion+".anim");Check(clip!=null,"Missing original "+motion);
for(int i=0;i<=120;i++)
{
clip.SampleAnimation(animator.gameObject,clip.length*i/120);
Check(Vector3.Distance(position,crown.localPosition)<1e-5f&&Quaternion.Angle(rotation,crown.localRotation)<.001f&&Vector3.Distance(scale,crown.localScale)<1e-5f,"Accessory not rigid on head in "+motion);
var p=crown.position;Check(!float.IsNaN(p.x+p.y+p.z)&&!float.IsInfinity(p.x+p.y+p.z),"Invalid pose");
}
}
}
finally{Object.DestroyImmediate(instance);EditorSceneManager.ClosePreviewScene(scene);}
Report("unity_assets",new{status="pass",triangles,uvVertices=mesh.uv.Length,textureSize=512,bone=acc.parent.name,originalAvatar=true,originalController=true,originalThreeClips=true,poseSamples=363,arenaHero=9,inactiveByDefault=true,layer=30});
return "PASS TideWhale T2: "+triangles+" tris, 512 textures, "+acc.parent.name+", unchanged T1 rig/controller, 363 rigid attachment samples, Hero9 nested";
}
public static async Task<string> Battle()
{
Check(EditorApplication.isPlaying,"Enter Play Mode");
var data=JObject.Parse(File.ReadAllText(Path.Combine(Repo,"Tools/output/combat_runtime/traces/stage_20_t2_f1.json")));
var snapshot=JsonUtility.FromJson<BattleSnapshot>(data["save"].ToString());var config=JsonUtility.FromJson<PrototypeConfig>(data["config"].ToString());
var game=new PrototypeGame(PrototypeSave.BattleSnapshot(snapshot.level,snapshot.stages,snapshot.formation));var battle=new PrototypeBattle("tidewhale-t2-readonly",game,config,20);
battle.LoadTrace(JsonUtility.FromJson<CombatTraceManifest>(data["manifest"].ToString()),data["events"].ToObject<CombatEvent[]>());
var arena=Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(Arena));var ui=Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/UI/Prefab/UI_PrototypeBattle.prefab"));ui.name="UI_PrototypeBattle";ui.SetActive(false);
var host=new GameObject("TideWhaleT2ReadOnlyReview");host.SetActive(false);var flow=host.AddComponent<PrototypeFlow>();
Action<string,object> set=(name,value)=>typeof(PrototypeFlow).GetField(name,BindingFlags.Instance|BindingFlags.NonPublic).SetValue(flow,value);
Action refresh=()=>typeof(PrototypeFlow).GetMethod("RefreshBattle",BindingFlags.Instance|BindingFlags.NonPublic).Invoke(flow,null);
try
{
set("_profile",new Func<long>(()=>-1));set("_boundProfile",-1L);set("_game",game);set("_config",config);set("_battle",battle);set("_arena",arena);set("_settled",true);set("_speed",1);set("_page",Activator.CreateInstance(typeof(PrototypeFlow).GetNestedType("Panel",BindingFlags.NonPublic),new object[]{ui}));
var heroes=Enumerable.Range(0,10).Select(i=>arena.transform.Find("Heroes/Hero"+i)).ToArray();var enemies=Enumerable.Range(0,9).Select(i=>arena.transform.Find("Enemies/Enemy"+i)).ToArray();var types=enemies.Select(e=>Enumerable.Range(0,5).Select(i=>e.Find("Type"+i)).ToArray()).ToArray();var t1=heroes.Select(h=>h.Find("Visual")).ToArray();var t2=heroes.Select(h=>h.Find("VisualT2")).ToArray();
set("_heroViews",heroes);set("_enemyViews",enemies);set("_enemyTypes",types);set("_heroT1Visuals",t1);set("_heroT2Visuals",t2);set("_heroAnimators",t1.Select(v=>v.GetComponentInChildren<Animator>(true)).ToArray());set("_heroT2Animators",t2.Select(v=>v.GetComponentInChildren<Animator>(true)).ToArray());set("_heroPulse",new float[10]);set("_heroSkillPulse",new float[10]);set("_enemyAnimators",types.Select(ts=>ts.Select(t=>t.GetComponentInChildren<Animator>(true)).ToArray()).ToArray());set("_enemyModels",new PrototypeBattle.Enemy[9]);set("_enemyStrikes",new int[9]);
foreach(var name in new[]{"_projectiles","_skillViews","_shieldViews"})set(name,Enumerable.Range(0,10).Select(i=>arena.transform.Find((name=="_projectiles"?"Projectiles/Shot":name=="_skillViews"?"SkillEffects/Skill":"SkillEffects/Shield")+i)).ToArray());
var fx=arena.GetComponentInChildren<BattleSkillPresentation>();set("_skillFeedback",fx);set("_enemyFeedback",arena.GetComponentInChildren<EnemyCombatPresentation>());
// Freeze the fixture before sampling the short opening cast. Wall-clock waits
// can miss that pose while the Editor warms shaders on its first frame.
set("_paused",true);host.SetActive(true);battle.Tick();battle.Tick();
typeof(PrototypeFlow).GetMethod("DrainBattleFeedback",BindingFlags.Instance|BindingFlags.NonPublic).Invoke(flow,new object[]{battle.Time});refresh();
var whale=battle.Heroes.Single(h=>h.Pet==9);var animator=t2[9].GetComponentInChildren<Animator>(true);var crest=heroes[9].Find("T2Crest");
Check(whale.Tier==2&&t2[9].gameObject.activeSelf&&!t1[9].gameObject.activeSelf&&!crest.gameObject.activeSelf,"T2 visual/crest toggle failed");
whale.Tier=1;refresh();Check(t1[9].gameObject.activeSelf&&!t2[9].gameObject.activeSelf&&!crest.gameObject.activeSelf,"T1 fallback failed");whale.Tier=2;refresh();
Check(fx.EffectPlays[14]>0,"Opening water shield did not play");
// Shield is an instant cast (release==start), so the authored protocol
// intentionally keeps Idle. Sample the first nonzero-windup whale attack.
var action=data["events"].First(e=>(string)e["kind"]=="cast_start"&&(int?)e["source"]==9&&(int)e["release"]>(int)e["tick"]);
int targetTick=(int)action["tick"]+1;
Check(targetTick<2400,"Attack sample exceeds protocol tick budget");
for(int i=battle.TraceTick;i<targetTick&&!battle.Finished;i++)
{
battle.Tick();
// Seeking is read-model only; stale presentation cues are not replayed.
if(battle.TraceTick<targetTick-2){while(battle.TryDequeueCue(out var ignored)){}while(battle.TryDequeueSkillCue(out var old)){};}
}
Check(battle.TraceTick==targetTick,"Trace ended before whale attack sample");
typeof(PrototypeFlow).GetMethod("DrainBattleFeedback",BindingFlags.Instance|BindingFlags.NonPublic).Invoke(flow,new object[]{battle.Time});refresh();
bool attack=animator.GetCurrentAnimatorStateInfo(0).IsName("Attack");
Check(attack,"Whale nonzero-windup attack missing at time="+battle.Time+", start="+whale.CastStart+", release="+whale.CastRelease);
set("_paused",true);float paused=battle.Time;await Task.Delay(200);Check(battle.Time==paused,"Pause failed");
set("_speed",2);set("_paused",false);await Task.Delay(350);Check(battle.Time>paused+.35f,"Double speed did not advance battle clock");
set("_paused",true);await Task.Delay(40);
var point=(Vector3)typeof(PrototypeFlow).GetMethod("CombatPoint",BindingFlags.Instance|BindingFlags.NonPublic).Invoke(flow,new object[]{9});Check(Vector3.Distance(point,heroes[9].Find("SkillMuzzle").position)<.001f,"Scaled skill anchor mismatch");
var camera=arena.GetComponentInChildren<Camera>();var rt=new RenderTexture(900,1200,24);var texture=new Texture2D(900,1200,TextureFormat.RGB24,false);var previous=RenderTexture.active;
try{camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;texture.ReadPixels(new Rect(0,0,900,1200),0,0);texture.Apply();File.WriteAllBytes(Path.Combine(Folder,"battle.png"),texture.EncodeToPNG());}
finally{camera.targetTexture=null;RenderTexture.active=previous;rt.Release();Object.Destroy(rt);Object.Destroy(texture);}
Report("unity_battle",new{status="pass",authoritative=true,stage=20,trace="stage_20_t2_f1.json",formation=snapshot.formation,t1T2Toggle=true,placeholderHidden=true,attack,waterShield=fx.EffectPlays[14],pause=true,doubleSpeed=true,scaledMuzzle=true,networkAccountWrites=false});
return "PASS TideWhale T2 in authoritative stage 20: T1/T2 toggle, hidden placeholder, Attack/water shield, pause/2x, scaled muzzle; no account writes";
}
finally{Object.Destroy(host);Object.Destroy(ui);Object.Destroy(arena);}
}
}