Files

93 lines
8.7 KiB
C#

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<GameObject>(Prefab);Check(prefab!=null,"Missing review prefab");var player=prefab.GetComponent<SkillSlicePlayer>();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;n<slots.arraySize;n++){var ps=(ParticleSystem)slots.GetArrayElementAtIndex(n).objectReferenceValue;Check(ps!=null&&!ps.main.playOnAwake&&ps.main.loop==(i==0||i==1||i==4),"Bad effect lifecycle");count++;}}
var clips=so.FindProperty("_clips");Check(clips.arraySize==5,"Five sounds required");for(int i=0;i<5;i++){var clip=(AudioClip)clips.GetArrayElementAtIndex(i).objectReferenceValue;Check(clip!=null&&clip.frequency==44100&&clip.channels==2&&clip.length<1,"Bad skill audio");}
Check(so.FindProperty("_voices").arraySize==8,"Voice limit");foreach(var view in player.Views)Check(view.root!=null&&view.muzzle!=null&&view.animator!=null&&!view.animator.applyRootMotion,"Incomplete actor binding");
Report("assets",new{status="pass",prefab=Prefab,actors=5,effectKinds=5,poolInstances=count,voices=8,audioClips=5});return "PASS persisted skill slice: five actors, five effects, 24 instances, five clips, eight voices";
}
public static string Finish()
{
Check(!EditorApplication.isPlaying,"Stop Play first");
foreach(string path in new[]{"enemy_studio_arena","skill_slice_fireball","skill_slice_shield"})AssetDatabase.ImportAsset("Assets/Screenshots/"+path+".png",ImportAssetOptions.ForceUpdate);
return "Open scenes: "+string.Join(", ",Enumerable.Range(0,SceneManager.sceneCount).Select(i=>SceneManager.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<Camera>();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<ParticleSystem>().Where(ps=>ps.transform.parent.GetComponent<ParticleSystem>()==null&&(ps.isPlaying||ps.isPaused))) {ps.Simulate(.03f,true,false,true);ps.Pause(true);}
Report("particles_"+name,root.GetComponentsInChildren<ParticleSystem>().Where(ps=>ps.particleCount>0).Select(ps=>new{name=ps.name,count=ps.particleCount,position=ps.transform.position.ToString(),bounds=ps.GetComponent<ParticleSystemRenderer>().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<string> Snapshots()
{
Check(EditorApplication.isPlaying,"Use Play Mode");var root=Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(Prefab));root.transform.position=new Vector3(200,0,0);var p=root.GetComponent<SkillSlicePlayer>();
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<string> Play()
{
Check(EditorApplication.isPlaying,"Use Play Mode");bool sfx=PrototypeClientPrefs.Sfx;
var root=Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(Prefab));root.transform.position=new Vector3(200,0,0);var p=root.GetComponent<SkillSlicePlayer>();
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<TextAsset>("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<hp,"Fireball arrival did not apply its trace damage");
for(int i=0;i<1000&&!p.Finished;i++)p.Advance(1f/60);Final(p);int count=p.ConsumedEvents;p.Advance(10);Check(p.ConsumedEvents==count,"Finished playback consumed twice");
p.Restart();p.SetPlayback(false,2);for(int i=0;i<1000&&!p.Finished;i++)p.Advance(1f/60);Final(p);
p.Restart();for(int i=0;i<100&&!p.Finished;i++)p.Advance(.73f);Final(p);
PrototypeClientPrefs.Sfx=false;p.Restart();p.Advance(2);Check(p.PlayedSounds==0&&root.GetComponentsInChildren<AudioSource>().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<AudioSource>(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.UtcNow<until)
{
if(p.ActiveShots>0)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<TextAsset>("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);}
}
}