103 lines
11 KiB
C#
103 lines
11 KiB
C#
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;
|
|
|
|
/// <summary>Presentation routing/lifecycle tests and independent contact sheet; never writes an account.</summary>
|
|
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<ParticleSystem>().Where(ps=>ps.transform.parent.GetComponent<ParticleSystem>()==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<GameObject>(Bank));var p=root.GetComponent<BattleSkillPresentation>();
|
|
var config=JsonUtility.FromJson<PrototypeConfig>(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<string,int,int,int> 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<LineRenderer>().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<LineRenderer>().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<ParticleSystem>().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<AudioSource>().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<ParticleSystem>().All(ps=>!ps.IsAlive())&&root.GetComponentsInChildren<AudioSource>().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<object>();var used=new HashSet<int>();
|
|
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<CombatEvent[]>();
|
|
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<string> 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<GameObject>("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>()){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<Transform>(true))t.gameObject.layer=30;
|
|
var effect=Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>((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<ParticleSystem>();ps.Play(true);foreach(var child in effect.GetComponentsInChildren<ParticleSystem>())if(child.main.loop)child.Emit(1);ps.Simulate(.14f,true,false,true);ps.Pause(true);
|
|
foreach(var line in effect.GetComponentsInChildren<LineRenderer>()){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<TextMesh>();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>();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>();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";
|
|
}
|
|
}
|