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; /// Isolated T2 asset/pose checks and authoritative battle fixture. No network/account writes. 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(Animal+"TideWhaleStudio/TideWhaleStudio_Companion.prefab"); var t2=AssetDatabase.LoadAssetAtPath(T2);Check(t2!=null,"Missing T2"); Check(PrefabUtility.GetCorrespondingObjectFromSource(t2)==t1,"Not a variant of the formal Studio T1"); var a=t1.GetComponentInChildren(true);var b=t2.GetComponentInChildren(true); Check(a.avatar==b.avatar&&a.runtimeAnimatorController==b.runtimeAnimatorController,"Original rig/controller changed"); var acc=t2.GetComponentsInChildren(true).Single(t=>t.name=="T2Accessory");Check(acc.parent.name.StartsWith("bone_"),"Accessory not on bone"); var mesh=acc.GetComponentInChildren(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(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(Animal+"TideWhale/TideWhaleT2/TideWhaleT2_"+suffix+".png");Check(tex!=null&&tex.width==512&&tex.height==512,"Missing 512 texture");} var arena=AssetDatabase.LoadAssetAtPath(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(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.enabled=false; var crown=instance.GetComponentsInChildren().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(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 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(data["save"].ToString());var config=JsonUtility.FromJson(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(data["manifest"].ToString()),data["events"].ToObject()); var arena=Object.Instantiate(AssetDatabase.LoadAssetAtPath(Arena));var ui=Object.Instantiate(AssetDatabase.LoadAssetAtPath("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(); Action 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(()=>-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(true)).ToArray());set("_heroT2Animators",t2.Select(v=>v.GetComponentInChildren(true)).ToArray());set("_heroPulse",new float[10]);set("_heroSkillPulse",new float[10]);set("_enemyAnimators",types.Select(ts=>ts.Select(t=>t.GetComponentInChildren(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();set("_skillFeedback",fx);set("_enemyFeedback",arena.GetComponentInChildren()); // 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(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;ipaused+.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();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);} } }