using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using HotUpdate.Gameplay; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using Object=UnityEngine.Object; /// Presentation routing/lifecycle tests and independent contact sheet; never writes an account. public static class VerifyPartnerSkillFx { static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../..")); const string Folder="Doc/Design/Combat/PartnerSkills"; const string Bank="Assets/Bundles/Combat/BattleSkillPresentation.prefab"; static void Check(bool condition,string message){if(!condition)throw new Exception(message);} static void Report(string name,object value){Directory.CreateDirectory(Path.Combine(Repo,Folder));File.WriteAllText(Path.Combine(Repo,Folder,name+".json"),JsonConvert.SerializeObject(value,Formatting.Indented));} static ParticleSystem[] Roots(BattleSkillPresentation p)=>p.GetComponentsInChildren().Where(ps=>ps.transform.parent.GetComponent()==null).ToArray(); static Vector3 Point(int id)=>new Vector3(id<100?-2:2,1,(id<100?id%3:id%3)-1); public static string Behavior() { Check(EditorApplication.isPlaying,"Use Play Mode");bool original=PrototypeClientPrefs.Sfx;var root=Object.Instantiate(AssetDatabase.LoadAssetAtPath(Bank));var p=root.GetComponent(); var config=JsonUtility.FromJson(File.ReadAllText(Path.Combine(Repo,"Config/Gameplay/greenfield.json"))); try { PrototypeClientPrefs.Sfx=true;Check(p.EffectKinds==15&&p.ProfileCount==10,"Missing profiles");int cast=1; Action cue=(kind,pet,target,c)=>p.Play(new CombatEvent{kind=kind,source=pet,target=target,cast=c,skill="pet."+pet+".ability",arrival=10,expires=20},0,Point,config); for(int pet=0;pet<10;pet++) { int c=cast++;cue("cast_start",pet,100,c);cue("release",pet,100,c); cue(pet==0||pet==9?"shield":pet==2||pet==7?"heal":pet==4||pet==5?"slow":"hit",pet,pet==0||pet==2||pet==7||pet==9?pet:100,c); cue("cast_end",pet,100,c); } cue("hit",4,100,cast++);cue("hit",0,100,cast++); for(int i=5;i<15;i++)Check(p.EffectPlays[i]>0,"Unreached signature effect "+i); for(int i=0;i<9;i++)Check(p.SoundPlays[i]>0,"Unreached sound "+i); int sounds=p.SoundPlays[7];int chain=cast++;for(int target=100;target<103;target++)cue("hit",3,target,chain);Check(p.SoundPlays[7]==sounds+1,"Chain amplified lightning sound"); foreach(var line in root.GetComponentsInChildren().Where(l=>l.enabled)) { Check(Vector3.Distance(line.transform.TransformPoint(line.GetPosition(0)),Point(3))<.001f||Vector3.Distance(line.transform.TransformPoint(line.GetPosition(0)),Point(8))<.001f,"Beam source detached"); Check(Enumerable.Range(100,3).Any(t=>Vector3.Distance(line.transform.TransformPoint(line.GetPosition(line.positionCount-1)),Point(t))<.001f),"Beam end detached"); } int count=p.PlayedEffects; p.Play(new CombatEvent{kind="hit",source=3,target=100,cast=cast++,tick=0},1,Point,config);Check(p.PlayedEffects==count,"Stale catch-up replayed a hit"); // Recycle the four beam roots while earlier flash leases still exist. for(int i=0;i<12;i++)cue("hit",3,100,cast++);Check(Roots(p).Length==76,"Pool expanded");p.Render(.3f,Point,_=>true);Check(root.GetComponentsInChildren().All(l=>!l.enabled),"Expired/recycled beam leaked"); cue("projectile",1,100,cast++);p.Render(.2f,Point,_=>true);Check(p.ActiveShots==1,"No projectile");var shot=Roots(p).Single(ps=>ps.isPlaying&&ps.name.Contains("Fireball"));Vector3 before=shot.transform.position; p.SetPlayback(true,2);p.Render(.2f,Point,_=>true);Check(Vector3.Distance(before,shot.transform.position)<.001f&&shot.isPaused,"Pause moved projectile");Check(root.GetComponentsInChildren().All(ps=>Mathf.Approximately(ps.main.simulationSpeed,2)),"2x missed child emitter");p.SetPlayback(false,2); p.Render(.3f,Point,_=>false);Check(p.ActiveShots==0,"Dead target retained projectile"); cue("death",0,0,cast++);Check(p.ActiveShields==1,"Death did not remove shield");cue("shield_expired",9,9,cast++);Check(p.ActiveShields==0,"Status expiry retained shield"); cue("cast_start",1,100,cast);Check(p.ActiveCharges==1,"No charge");cue("cancel",1,100,cast++);Check(p.ActiveCharges==0,"Cancel retained charge"); PrototypeClientPrefs.Sfx=false;sounds=p.PlayedSounds;cue("hit",3,100,cast++);Check(p.PlayedSounds==sounds&&root.GetComponentsInChildren().All(a=>!a.isPlaying),"Mute failed");PrototypeClientPrefs.Sfx=true; int[] effects=p.EffectPlays.ToArray(),audio=p.SoundPlays.ToArray();cue("battle_end",0,0,0);Check(p.ActiveShots+p.ActiveCharges+p.ActiveShields+p.ActiveBeams==0&&root.GetComponentsInChildren().All(ps=>!ps.IsAlive())&&root.GetComponentsInChildren().All(a=>!a.isPlaying),"Battle end retained resources"); cue("shield",0,0,cast++);root.SetActive(false);Check(p.ActiveShields==0,"Disable retained shield");root.SetActive(true); // Replay every exported server trace through the actual presentation consumer. var rows=new List();var used=new HashSet(); foreach(var file in Directory.GetFiles(Path.Combine(Repo,"Tools/output/combat_runtime/traces"),"*.json")) { p.ResetPlayback();var trace=JObject.Parse(File.ReadAllText(file));var events=trace["events"].ToObject(); foreach(var e in events){float time=e.tick*.05f;p.Play(e,time,Point,config);p.Render(time,Point,_=>true);if(e.source>=0&&e.source<10)used.Add(e.source);} Check(p.ActiveShots+p.ActiveCharges+p.ActiveShields+p.ActiveBeams==0,"Trace end leak "+file);rows.Add(new{trace=Path.GetFileName(file),events=events.Length,effects=p.PlayedEffects,sounds=p.PlayedSounds}); } Check(rows.Count==23&&used.Count==10,"Incomplete trace coverage"); Report("behavior",new{status="pass",profiles=10,effectKinds=15,poolRoots=76,voices=8,audioClips=9,signatureEffectPlays=effects,soundPlays=audio,chainSoundOnce=true,beamEndpoints=true,poolReuse=true,pause=true,speed2=true,staleSkip=true,death=true,expiry=true,cancel=true,mute=true,endCleanup=true,disableCleanup=true,traceCases=rows.Count,traceActors=used.Count,rows,networkAccountWrites=false});return "PASS 10 signatures / 9 sounds, 23 server traces, chain dedup, beams, pause/2x/mute/reuse/cleanup"; } finally{PrototypeClientPrefs.Sfx=original;Object.Destroy(root);} } public static async Task ContactSheet() { Check(EditorApplication.isPlaying,"Use Play Mode");var root=new GameObject("PartnerSkillContactSheet");root.transform.position=new Vector3(500,0,0); try { var config=JObject.Parse(File.ReadAllText(Path.Combine(Repo,"Config/Combat/partner_presentations_v1.json"))); var arena=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab"); for(int pet=0;pet<10;pet++) { var cell=new GameObject("Cell"+pet);cell.transform.SetParent(root.transform,false);cell.transform.localPosition=new Vector3((pet%5-2)*4.2f,0,(pet<5?1:-1)*3.4f); var hero=Object.Instantiate(arena.transform.Find("Heroes/Hero"+pet+"/Visual").gameObject,cell.transform);hero.SetActive(true);hero.transform.localPosition=Vector3.zero;hero.transform.localRotation=Quaternion.identity; foreach(var animator in hero.GetComponentsInChildren()){animator.enabled=false;var clip=animator.runtimeAnimatorController.animationClips.FirstOrDefault(c=>c.name.Contains("Idle"));if(clip!=null)clip.SampleAnimation(animator.gameObject,.25f);} foreach(var t in hero.GetComponentsInChildren(true))t.gameObject.layer=30; var effect=Object.Instantiate(AssetDatabase.LoadAssetAtPath((string)config["effects"][pet]["path"]),cell.transform);effect.transform.localPosition=(bool)config["effects"][pet]["ground"]?Vector3.up*.04f:Vector3.up*.9f; if(pet==6)effect.transform.localRotation=Quaternion.Euler(0,60,0); var ps=effect.GetComponent();ps.Play(true);foreach(var child in effect.GetComponentsInChildren())if(child.main.loop)child.Emit(1);ps.Simulate(.14f,true,false,true);ps.Pause(true); foreach(var line in effect.GetComponentsInChildren()){for(int n=0;n<9;n++)line.SetPosition(n,new Vector3(-1.5f+n*.1875f,n==0||n==8?0:(n%2==0?.1f:-.1f),0));line.enabled=true;} var label=new GameObject("Label");label.transform.SetParent(cell.transform,false);label.layer=30;label.transform.localPosition=new Vector3(0,0,-1.6f);label.transform.rotation=Quaternion.Euler(55,0,0);var text=label.AddComponent();text.text=(string)config["profiles"][pet]["actor"];text.anchor=TextAnchor.MiddleCenter;text.fontSize=36;text.characterSize=.065f;text.color=new Color(.8f,.9f,1); } var cameraObject=new GameObject("ReviewCamera");cameraObject.transform.SetParent(root.transform,false);cameraObject.transform.localPosition=new Vector3(0,20,-15);cameraObject.transform.LookAt(root.transform.position+Vector3.up*.4f);var camera=cameraObject.AddComponent();camera.cullingMask=1<<30;camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=new Color(.035f,.055f,.08f);camera.orthographic=true;camera.orthographicSize=7.5f; var lightObject=new GameObject("ReviewLight");lightObject.transform.SetParent(root.transform,false);lightObject.transform.rotation=Quaternion.Euler(50,-25,0);var light=lightObject.AddComponent();light.type=LightType.Directional;light.intensity=1.25f;light.cullingMask=1<<30; await Task.Delay(80);var old=RenderTexture.active;var rt=new RenderTexture(2000,1200,24);var tex=new Texture2D(2000,1200,TextureFormat.RGB24,false); try{camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,2000,1200),0,0);tex.Apply();File.WriteAllBytes(Path.Combine(Repo,"AIClient/Assets/Screenshots/partner_skills_overview.png"),tex.EncodeToPNG());} finally{camera.targetTexture=null;RenderTexture.active=old;rt.Release();Object.Destroy(rt);Object.Destroy(tex);} Report("visual",new{status="captured",profiles=10,screenshot="AIClient/Assets/Screenshots/partner_skills_overview.png",sampleSeconds=.14,networkAccountWrites=false});return "Captured ten partner skill signatures"; } finally{Object.Destroy(root);} } public static string Finish() { Check(!EditorApplication.isPlaying,"Stop Play first");foreach(string name in new[]{"partner_skills_overview","partner_skills_battle"})AssetDatabase.ImportAsset("Assets/Screenshots/"+name+".png",ImportAssetOptions.ForceUpdate);return "Imported partner skill contact sheet"; } }