using System; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; using HotUpdate.Gameplay; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine.SceneManagement; using UnityEngine; using Object=UnityEngine.Object; public static class VerifySkillSlice { const string Prefab="Assets/Bundles/Combat/SkillSlice/SkillSlice.prefab"; static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../..")); static void Check(bool value,string message){if(!value)throw new Exception(message);} static void Report(string name,object value){string folder=Path.Combine(Repo,"Doc/Design/Combat/SkillSlice");Directory.CreateDirectory(folder);File.WriteAllText(Path.Combine(folder,name+".json"),JsonConvert.SerializeObject(value,Formatting.Indented));} public static string Assets() { var prefab=AssetDatabase.LoadAssetAtPath(Prefab);Check(prefab!=null,"Missing review prefab");var player=prefab.GetComponent();Check(player!=null&&player.Views.Length==5,"Five actors required"); var so=new SerializedObject(player);Check(so.FindProperty("_trace").objectReferenceValue!=null,"Missing frozen trace"); var effects=so.FindProperty("_effects");Check(effects.arraySize==5,"Five effect kinds required");int count=0; for(int i=0;i<5;i++){var slots=effects.GetArrayElementAtIndex(i).FindPropertyRelative("instances");Check(slots.arraySize>=4,"Too few effect slots");for(int n=0;nSceneManager.GetSceneAt(i)).Select(s=>s.name+" dirty="+s.isDirty)); } static void Final(SkillSlicePlayer p){Check(p.Finished&&p.ConsumedEvents==p.EventCount&&p.FinalStateMatches,"Trace final state mismatch");Check(p.ActiveShots==0&&p.ActiveCharges==0&&p.ActiveShields==0,"Leaked leases at battle end");} static async Task Capture(GameObject root,string name) { var camera=root.GetComponentInChildren();var old=RenderTexture.active;var rt=new RenderTexture(1200,1200,24);var tex=new Texture2D(1200,1200,TextureFormat.RGB24,false); try { // Frozen manual-clock snapshots need a particle geometry update before rendering. // Simulates only live root emitters; the authoritative clock/state stay paused. foreach(var ps in root.GetComponentsInChildren().Where(ps=>ps.transform.parent.GetComponent()==null&&(ps.isPlaying||ps.isPaused))) {ps.Simulate(.03f,true,false,true);ps.Pause(true);} Report("particles_"+name,root.GetComponentsInChildren().Where(ps=>ps.particleCount>0).Select(ps=>new{name=ps.name,count=ps.particleCount,position=ps.transform.position.ToString(),bounds=ps.GetComponent().bounds.ToString(),layer=ps.gameObject.layer}).ToArray()); await Task.Delay(50);camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,1200,1200),0,0);tex.Apply();File.WriteAllBytes(Path.Combine(Repo,"AIClient/Assets/Screenshots/skill_slice_"+name+".png"),tex.EncodeToPNG()); } finally{camera.targetTexture=null;RenderTexture.active=old;rt.Release();Object.Destroy(rt);Object.Destroy(tex);} } public static async Task Snapshots() { Check(EditorApplication.isPlaying,"Use Play Mode");var root=Object.Instantiate(AssetDatabase.LoadAssetAtPath(Prefab));root.transform.position=new Vector3(200,0,0);var p=root.GetComponent(); try{p.enabled=false;p.Restart();p.Advance(.7f);p.SetPlayback(true,1);await Capture(root,"fireball");p.Restart();p.Advance(1.95f);p.SetPlayback(true,1);await Capture(root,"shield");return "Captured independent skill snapshots";} finally{Object.Destroy(root);} } public static async Task Play() { Check(EditorApplication.isPlaying,"Use Play Mode");bool sfx=PrototypeClientPrefs.Sfx; var root=Object.Instantiate(AssetDatabase.LoadAssetAtPath(Prefab));root.transform.position=new Vector3(200,0,0);var p=root.GetComponent(); try { p.enabled=false;p.Restart(); Check(p.ActiveCharges==1&&p.ActiveShields==1,"Tick zero should start fire charge and shield"); float clock=p.Clock;int consumed=p.ConsumedEvents;p.SetPlayback(true,1);p.Advance(3);Check(p.Clock==clock&&p.ConsumedEvents==consumed,"Pause advanced the simulation");p.SetPlayback(false,1); var trace=JObject.Parse(AssetDatabase.LoadAssetAtPath("Assets/Bundles/Combat/SkillSlice/trace.json").text); var first=((JArray)trace["events"]).First(e=>(string)e["kind"]=="projectile");float arrival=(int)first["arrival"]*.05f;var target=p.Views.Single(v=>v.id==(int)first["target"]);int hp=target.hp; p.Advance(arrival-.001f);Check(target.hp==hp&&p.ActiveShots>0,"Fireball applied damage before arrival");p.Advance(.002f);Check(target.hp().All(a=>!a.isPlaying),"Mute ignored"); PrototypeClientPrefs.Sfx=true;p.Restart();p.Advance(.3f);Check(p.PlayedSounds>0,"No sound playback"); p.enabled=true;root.SetActive(false);Check(p.ActiveShots==0&&p.ActiveCharges==0&&p.ActiveShields==0&&root.GetComponentsInChildren(true).All(a=>!a.isPlaying),"Disable did not clear resources"); root.SetActive(true);p.enabled=false;p.Restart();float middle=((int)first["tick"]+(int)first["arrival"])*.025f; p.Advance(middle);p.SetPlayback(true,1);await Capture(root,"fireball"); p.Restart();p.Advance(1.95f);p.SetPlayback(true,1);await Capture(root,"shield"); p.enabled=true;p.Restart(); bool shot=false,shield=false;var until=DateTime.UtcNow.AddSeconds(20); while(!p.Finished&&DateTime.UtcNow0)shot=true; if(p.Clock>1.9f&&p.ActiveShields>0)shield=true; await Task.Delay(15); } Final(p);Check(shot&&shield&&p.PlayedSounds>0,"Missing live effects"); string traceHash;using(var hash=SHA256.Create())traceHash=BitConverter.ToString(hash.ComputeHash(AssetDatabase.LoadAssetAtPath("Assets/Bundles/Combat/SkillSlice/trace.json").bytes)).Replace("-","").ToLowerInvariant(); Report("play",new{status="pass",traceSha256=traceHash,authoritativeCppTrace=true,productionRulesChanged=false,events=p.EventCount,won=p.Won,pause=true,speed2=true,lowFrameRate=true,arrivalDamage=true,finalStateExact=true,mute=true,disableCleanup=true,liveSounds=p.PlayedSounds,fireballScreenshot=shot,shieldScreenshot=shield,networkAccountWrites=false}); return "PASS skill slice: arrival-aligned damage, 1x/2x/pause/low-FPS identical final state, mute/cleanup, live VFX/SFX and screenshots"; } finally{PrototypeClientPrefs.Sfx=sfx;Object.Destroy(root);} } }