using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using HotUpdate.Gameplay; using Newtonsoft.Json; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; public static class VerifyEnemyCombatFx { const string Folder="Assets/Bundles/Effects/EnemyCombat/"; const string Review="Assets/Bundles/Scenes/Review/EnemyCombatFx_Review.unity"; static readonly string[] Names={"HornletAttack","DashletAttack","ShellhornAttack","HornCaptainAttack","GreatHornAttack","HitSoft","HitArmor","HitShield"}; static readonly string[] Actors={"Hornlet","Dashlet","Shellhorn","HornCaptain","GreatHorn"}; static string Repo=>Directory.GetParent(Application.dataPath).Parent.FullName; static void Check(bool value,string message) {if(!value)throw new Exception(message);} static void Report(string name,object data) {string dir=Path.Combine(Repo,"Doc/Design/Combat/EnemyCombatFx");Directory.CreateDirectory(dir);File.WriteAllText(Path.Combine(dir,name+".json"),JsonConvert.SerializeObject(data,Formatting.Indented));} static PrototypeBattle Battle(int type,bool shield=false,bool lethal=false,int rules=2) { var c=JsonUtility.FromJson(File.ReadAllText(Path.Combine(Repo,"Config/Gameplay/greenfield.json"))); c.battleRules=rules;var stage=c.stages[0];stage.lanes=1;stage.enemyIds=Enumerable.Repeat(type,9).ToArray();stage.enemyLanes=new int[9];stage.waveGap=2;stage.spawnGap=.1f;stage.timeout=20;stage.elementsEnabled=false;stage.hpScale=stage.attackScale=1; c.pets[0].hp=100000;c.pets[0].attack=lethal?100000:1;c.pets[0].range=2;c.pets[0].cooldown=.5f; c.pets[0].ability.kind="shield";c.pets[0].ability.t1=shield?100000:0;c.pets[0].ability.duration=30;c.pets[0].ability.cooldown=30; c.enemies[type].hp=100;c.enemies[type].hpStep=0;c.enemies[type].speed=20;c.enemies[type].attack=1;c.enemies[type].cooldown=.5f; var save=PrototypeSave.BattleSnapshot(1,Enumerable.Repeat(1,10).ToArray(),new[]{0,-1,-1,-1,-1,-1}); return new PrototypeBattle("enemy-fx-qa-"+type,new PrototypeGame(save),c); } public static string Simulation() { var rows=new List(); for(int rules=1;rules<=2;rules++)for(int type=0;type<5;type++) { var a=Battle(type,false,false,rules);var b=Battle(type,false,false,rules);int strikes=0,hits=0; for(int tick=0;tick<400;tick++) { a.Tick();b.Tick();while(a.TryDequeueCue(out var cue)) {if(cue.Kind==PrototypeBattle.CueKind.EnemyAttack)strikes++;else hits++;Check(cue.Type==type,"Wrong type cue");} } Check(strikes==a.Enemies.Sum(e=>e.Strikes)&&strikes>0&&hits>0,"Dropped/duplicated cue for "+rules+"/"+type); Check(a.Heroes[0].Hp==b.Heroes[0].Hp&&a.Enemies.Sum(e=>e.Hp)==b.Enemies.Sum(e=>e.Hp)&&a.Won==b.Won&&a.Durability==b.Durability,"Consuming presentation changed battle"); rows.Add(new{rules,type,strikes,hits}); } var shield=Battle(0,true);int blocked=0;for(int i=0;i<80;i++){shield.Tick();while(shield.TryDequeueCue(out var cue))if(cue.Shield)blocked++;} Check(blocked>0&&shield.Heroes[0].Hp==shield.Heroes[0].MaxHp,"Shield feedback wrong"); var lethal=Battle(0,false,true);int impacts=0;for(int i=0;i<400;i++){lethal.Tick();while(lethal.TryDequeueCue(out var cue))if(cue.Kind==PrototypeBattle.CueKind.EnemyHit)impacts++;} Check(impacts==9&&lethal.Defeated==9,"Lethal impact missing/duplicated"); Report("simulation",new{status="pass",rows,blocked,lethalImpacts=impacts,presentationConsumptionDoesNotChangeResults=true});return "PASS classic+lanes x five types, exact strike counts, shield absorption, nine lethal impacts, presentation-independent results"; } public static string BuildReview() { Check(!EditorApplication.isPlaying,"Build review in Edit Mode");var original=SceneManager.GetActiveScene();var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive); try { SceneManager.SetActiveScene(scene);var root=new GameObject("EnemyCombatFxReview"); var camera=new GameObject("EnemyCombatFxCamera").AddComponent();camera.transform.SetParent(root.transform,false);camera.orthographic=true;camera.orthographicSize=3.7f;camera.transform.position=new Vector3(0,6,-12);camera.transform.LookAt(new Vector3(0,.4f,0));camera.cullingMask=1<<30;camera.backgroundColor=new Color(.045f,.075f,.105f);camera.clearFlags=CameraClearFlags.SolidColor;camera.depth=70; var light=new GameObject("ReviewLight").AddComponent();light.transform.SetParent(root.transform);light.type=LightType.Directional;light.transform.rotation=Quaternion.Euler(40,-25,0);light.intensity=1.2f;light.cullingMask=1<<30; var bank=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Folder+"EnemyCombatPresentation.prefab"),root.transform);bank.SetActive(false); for(int i=0;i<8;i++) { float x=i<5?(i-2)*2.6f:(i-6)*3.6f;float z=i<5?1.4f:-2; var slot=new GameObject("Sample"+i);slot.layer=30;slot.transform.SetParent(root.transform);slot.transform.position=new Vector3(x,0,z); string actor=Actors[Mathf.Min(i,4)];var model=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath("Assets/Bundles/Characters/Enemy/"+actor+"/"+actor+"_Enemy.prefab"),slot.transform); float height=i<5?1.2f:.85f;var mesh=new Mesh();float top=0; foreach(var skin in model.GetComponentsInChildren()) {skin.BakeMesh(mesh,true);foreach(var v in mesh.vertices)top=Mathf.Max(top,model.transform.InverseTransformPoint(skin.transform.TransformPoint(v)).y);}Object.DestroyImmediate(mesh);model.transform.localScale=Vector3.one*(height/Mathf.Max(top,.1f)); var fx=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Folder+"Vfx_Enemy_"+Names[i]+".prefab"),slot.transform);fx.name="Effect";fx.transform.localPosition=new Vector3(0,i<5?.65f:.5f,-.45f); var label=new GameObject("Label").AddComponent();label.transform.SetParent(slot.transform,false);label.transform.localPosition=new Vector3(0,-.4f,-.6f);label.transform.rotation=camera.transform.rotation;label.text=Names[i];label.fontSize=48;label.characterSize=.045f;label.anchor=TextAnchor.MiddleCenter;label.color=new Color(.85f,.93f,1); } foreach(var t in root.GetComponentsInChildren(true))t.gameObject.layer=30; EditorSceneManager.SaveScene(scene,Review); } finally{SceneManager.SetActiveScene(original);EditorSceneManager.CloseScene(scene,true);} return "Saved isolated "+Review; } public static async Task Capture() { Check(!EditorApplication.isPlaying,"Capture review in Edit Mode");var scene=EditorSceneManager.OpenScene(Review,OpenSceneMode.Additive);var root=scene.GetRootGameObjects().Single();var camera=root.GetComponentInChildren(); var rt=new RenderTexture(1800,900,24);var tex=new Texture2D(1800,900,TextureFormat.RGB24,false);var previous=RenderTexture.active; try { camera.enabled=false; foreach(var slot in root.GetComponentsInChildren().Where(t=>t.name.StartsWith("Sample"))) { int index=int.Parse(slot.name.Substring(6));var anim=slot.GetComponentInChildren();var clip=anim.runtimeAnimatorController.animationClips.First(c=>c.name.Contains(index<5?"Attack":"Idle"));clip.SampleAnimation(anim.gameObject,clip.length*(index<5?.46f:.25f)); var ps=slot.Find("Effect").GetComponent();ps.Simulate(.09f,true,true,true); } await Task.Delay(100);camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,1800,900),0,0);tex.Apply(); string path=Path.Combine(Repo,"AIClient/Assets/Screenshots/enemy_combat_fx_review.png");File.WriteAllBytes(path,tex.EncodeToPNG());AssetDatabase.ImportAsset("Assets/Screenshots/enemy_combat_fx_review.png");return path; } finally {RenderTexture.active=previous;camera.targetTexture=null;rt.Release();Object.DestroyImmediate(rt);Object.DestroyImmediate(tex);EditorSceneManager.CloseScene(scene,true);} } public static async Task Play() { Check(EditorApplication.isPlaying,"Enter Play Mode first");bool sfx=PrototypeClientPrefs.Sfx; var bank=Object.Instantiate(AssetDatabase.LoadAssetAtPath(Folder+"EnemyCombatPresentation.prefab"));var p=bank.GetComponent(); try { PrototypeClientPrefs.Sfx=true; for(int i=0;i<5;i++){p.Play(new PrototypeBattle.CombatCue(PrototypeBattle.CueKind.EnemyAttack,i,Vector3.zero,Vector3.back,i==4));await Task.Delay(100);p.Play(new PrototypeBattle.CombatCue(PrototypeBattle.CueKind.EnemyHit,i,Vector3.zero,Vector3.forward));} Check(p.PlayedEffects==15&&p.PlayedSounds>=10,"Missing attack/hit playback"); p.SetPlayback(true,1);var particles=bank.GetComponentsInChildren().Where(ps=>ps.particleCount>0).ToArray();Check(particles.Length>0,"No visible particle emitted");var times=particles.Select(ps=>ps.time).ToArray();await Task.Delay(150);Check(particles.Select((ps,i)=>Mathf.Abs(ps.time-times[i])<.001f).All(x=>x),"Pause failed"); p.SetPlayback(false,2);Check(particles.All(ps=>ps.main.simulationSpeed==2),"2x particle clock failed"); PrototypeClientPrefs.Sfx=false;Check(bank.GetComponentsInChildren().All(v=>!v.isPlaying),"SFX toggle left combat audio playing"); int before=p.PlayedSounds;p.Play(new PrototypeBattle.CombatCue(PrototypeBattle.CueKind.EnemyHit,0,Vector3.zero,Vector3.forward));Check(before==p.PlayedSounds,"Muted cue played sound"); for(int n=0;n<100;n++)p.Play(new PrototypeBattle.CombatCue(PrototypeBattle.CueKind.EnemyAttack,n%5,Vector3.zero,Vector3.back));Check(bank.GetComponentsInChildren().Length==8&&bank.GetComponentsInChildren().Length<140,"Unbounded pools"); PrototypeClientPrefs.Sfx=true;await Task.Delay(100);for(int i=0;i<5;i++)p.Play(new PrototypeBattle.CombatCue(PrototypeBattle.CueKind.EnemyAttack,i,Vector3.zero,Vector3.back));Check(bank.GetComponentsInChildren().Where(v=>v.isPlaying).Sum(v=>v.volume)<=.701f,"Combat mix budget exceeded"); bank.SetActive(false);Check(bank.GetComponentsInChildren(true).All(v=>!v.isPlaying),"Exit left audio playing"); Report("play",new{status="pass",allFiveTypes=true,muting=true,pause=true,doubleSpeed=true,boundedPool=true,mixGainBudget=.7f,exitCleanup=true});return "PASS Play: all five attacks/hits, shield, actual particle/audio playback, pause, 2x, mute, bounded pool and exit cleanup"; } finally{PrototypeClientPrefs.Sfx=sfx;Object.Destroy(bank);} } /// Uses the real PrototypeFlow.Update/RefreshBattle with an isolated in-memory battle. No login or reward request. public static async Task PlayBattle() { Check(EditorApplication.isPlaying,"Enter Play Mode first"); var battle=Battle(0);var config=battle.Config; config.stages[0].enemyIds=new[]{0,1,2,3,4,0,1,2,4};config.stages[0].waveGap=.5f; foreach(var enemy in config.enemies){enemy.hp=100;enemy.hpStep=0;enemy.speed=20;enemy.attack=1;enemy.cooldown=.5f;} config.pets[0].cooldown=.15f;battle.Heroes[0].Attack=60; var arena=Object.Instantiate(AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab")); var ui=Object.Instantiate(AssetDatabase.LoadAssetAtPath("Assets/Bundles/UI/Prefab/UI_PrototypeBattle.prefab"));ui.name="UI_PrototypeBattle";ui.SetActive(false); var host=new GameObject("IsolatedEnemyCombatFlowQA");host.SetActive(false);var flow=host.AddComponent(); var ft=typeof(PrototypeFlow);Action set=(name,value)=>ft.GetField(name,BindingFlags.Instance|BindingFlags.NonPublic).SetValue(flow,value); try { set("_profile",new Func(()=>1));set("_boundProfile",1L);set("_game",new PrototypeGame(PrototypeSave.BattleSnapshot(1,Enumerable.Repeat(1,10).ToArray(),new[]{0,-1,-1,-1,-1,-1})));set("_config",config);set("_battle",battle);set("_arena",arena);set("_settled",true);set("_speed",1); set("_page",Activator.CreateInstance(ft.GetNestedType("Panel",BindingFlags.NonPublic),new object[]{ui})); int pets=config.pets.Length; var heroes=Enumerable.Range(0,pets).Select(i=>arena.transform.Find("Heroes/Hero"+i)).ToArray(); var slots=Enumerable.Range(0,9).Select(i=>arena.transform.Find("Enemies/Enemy"+i)).ToArray(); var types=slots.Select(s=>Enumerable.Range(0,5).Select(i=>s.Find("Type"+i)).ToArray()).ToArray(); set("_heroViews",heroes);set("_enemyViews",slots);set("_enemyTypes",types); set("_heroAnimators",heroes.Select(h=>h.GetComponentInChildren(true)).ToArray());set("_heroPulse",new float[pets]);set("_heroSkillPulse",new float[pets]); 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,pets).Select(i=>arena.transform.Find((name=="_projectiles"?"Projectiles/Shot":name=="_skillViews"?"SkillEffects/Skill":"SkillEffects/Shield")+i)).ToArray()); var feedback=arena.GetComponentInChildren();Check(feedback!=null,"Arena feedback missing");set("_enemyFeedback",feedback); host.SetActive(true);int last=0;bool captured=false;var until=DateTime.UtcNow.AddSeconds(8); while(!battle.Finished&&DateTime.UtcNowe.Type==4&&e.Strikes>0)&&feedback.PlayedEffects>last) { captured=true;set("_paused",true);await Task.Delay(35); var camera=arena.GetComponentsInChildren().First();var rt=new RenderTexture(900,1200,24);var tex=new Texture2D(900,1200,TextureFormat.RGB24,false);var previous=RenderTexture.active; try{camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,900,1200),0,0);tex.Apply();File.WriteAllBytes(Path.Combine(Repo,"AIClient/Assets/Screenshots/enemy_combat_fx_battle.png"),tex.EncodeToPNG());} finally{camera.targetTexture=null;RenderTexture.active=previous;rt.Release();Object.Destroy(rt);Object.Destroy(tex);} set("_paused",false); } last=feedback.PlayedEffects;await Task.Delay(20); } Check(captured&&feedback.PlayedEffects>0&&battle.Enemies.Select(e=>e.Type).Distinct().Count()==5,"Real flow did not play every type"); Check(battle.Enemies.GroupBy(e=>e.Type).All(g=>g.Sum(e=>e.Strikes)>0),"Some type never attacked"); Report("battle",new{status="pass",realFlow=true,isolatedInMemory=true,networkRewardRequests=false,allFiveTypes=true,effects=feedback.PlayedEffects,sounds=feedback.PlayedSounds,captured,battle.Defeated}); return "PASS real PrototypeFlow battle: all five types attack, hit cues consumed, screenshot enemy_combat_fx_battle.png; no account writes"; } finally{Object.Destroy(host);Object.Destroy(ui);Object.Destroy(arena);} } }