diff --git a/AIClient/AgentScripts/BuildBattleSkills.cs b/AIClient/AgentScripts/BuildBattleSkills.cs new file mode 100644 index 0000000..ad0e3a6 --- /dev/null +++ b/AIClient/AgentScripts/BuildBattleSkills.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using System.Linq; +using HotUpdate.Gameplay; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object=UnityEngine.Object; + +public static class BuildBattleSkills +{ + public const string Bank="Assets/Bundles/Combat/BattleSkillPresentation.prefab"; + const string Arena="Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab"; + static readonly string[] Pets={"RockBall","FireCat","BudBud","SparkBird","BubbleTurtle","RockRhino","FlameLion","VineSprite","ThunderLeopard","TideWhale"}; + static readonly string[] Enemies={"Hornlet","Dashlet","Shellhorn","HornCaptain","GreatHorn"}; + static readonly string[] Effects={"FireCharge","Fireball","FireHit","RockHit","Shield"}; + static readonly string[] Sounds={"FireCharge","FireLaunch","FireHit","RockHit","Shield","Heal","Slow","Lightning","EnemyHit"}; + public static string Build() + { + if(EditorApplication.isPlaying)throw new Exception("Author in Edit Mode"); + var previous=SceneManager.GetActiveScene();var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive); + try + { + var plan=JObject.Parse(File.ReadAllText("../Config/Combat/partner_presentations_v1.json")); + SceneManager.SetActiveScene(scene);var root=new GameObject("BattleSkillPresentation");var player=root.AddComponent();var so=new SerializedObject(player);var effects=so.FindProperty("_effects");effects.arraySize=15; + var ground=so.FindProperty("_groundEffects");ground.arraySize=15;var lifetimes=so.FindProperty("_effectLifetimes");lifetimes.arraySize=15; + for(int i=0;i<15;i++) + { + JToken definition=i>=5?plan["effects"][i-5]:null;var list=effects.GetArrayElementAtIndex(i).FindPropertyRelative("instances");list.arraySize=i<5?(i==1?12:6):(int)definition["poolSize"]; + var path=i<5?"Assets/Bundles/Combat/SkillSlice/Vfx_Skill_"+Effects[i]+".prefab":(string)definition["path"];var prefab=AssetDatabase.LoadAssetAtPath(path);if(prefab==null)throw new Exception("Missing "+path); + ground.GetArrayElementAtIndex(i).boolValue=definition!=null&&(bool)definition["ground"];lifetimes.GetArrayElementAtIndex(i).floatValue=definition==null?.65f:(float)definition["lifetime"]; + for(int n=0;n();} + } + var profiles=so.FindProperty("_profiles");profiles.arraySize=10; + for(int i=0;i<10;i++)foreach(var property in ((JObject)plan["profiles"][i]).Properties())if(property.Name!="actor")profiles.GetArrayElementAtIndex(i).FindPropertyRelative(property.Name).intValue=(int)property.Value; + var clips=so.FindProperty("_clips");clips.arraySize=Sounds.Length; + for(int i=0;i(path); + } + var voices=so.FindProperty("_voices");voices.arraySize=8; + for(int i=0;i<8;i++){var go=new GameObject("Voice"+i);go.transform.SetParent(root.transform,false);var voice=go.AddComponent();voice.playOnAwake=false;voice.spatialBlend=0;voice.volume=.3f/8;voices.GetArrayElementAtIndex(i).objectReferenceValue=voice;} + for(int team=0;team<2;team++) + { + var actors=team==0?Pets:Enemies;var impacts=so.FindProperty(team==0?"_petImpacts":"_enemyImpacts");impacts.arraySize=actors.Length; + for(int i=0;i(true))t.gameObject.layer=30;PrefabUtility.SaveAsPrefabAsset(root,Bank);Object.DestroyImmediate(root); + } + finally{SceneManager.SetActiveScene(previous);EditorSceneManager.CloseScene(scene,true);} + var arena=PrefabUtility.LoadPrefabContents(Arena); + try + { + foreach(string group in new[]{"Heroes","Enemies"})foreach(Transform actor in arena.transform.Find(group)) + { + var muzzle=actor.Find("SkillMuzzle");if(muzzle==null){muzzle=new GameObject("SkillMuzzle").transform;muzzle.SetParent(actor,false);muzzle.localPosition=new Vector3(0,group=="Heroes"?.85f:.8f,-.48f);muzzle.gameObject.layer=30;} + } + var old=arena.GetComponentInChildren(true);if(old!=null)Object.DestroyImmediate(old.gameObject);PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Bank),arena.transform);PrefabUtility.SaveAsPrefabAsset(arena,Arena); + } + finally{PrefabUtility.UnloadPrefabContents(arena);} + return "Saved bounded battle skill bank, authored animation phases for 10 pets / 5 enemies, attached to existing arena"; + } +} diff --git a/AIClient/AgentScripts/BuildFishing.cs b/AIClient/AgentScripts/BuildFishing.cs new file mode 100644 index 0000000..f2d0f06 --- /dev/null +++ b/AIClient/AgentScripts/BuildFishing.cs @@ -0,0 +1,18 @@ +using System; +using System.IO; +using AIGame.EditorTools; +using UnityEditor; + +public static class BuildFishing +{ + public static string Build() + { + if (EditorApplication.isPlaying) throw new Exception("Build the fishing page in Edit Mode"); + string repo = Directory.GetParent(UnityEngine.Application.dataPath).Parent.FullName; + if (string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(repo, "Doc/UIPrefabCreater/UI_PrototypeFishing.json")))) throw new Exception("Fishing prefab failed"); + if (string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(repo, "Doc/UIPrefabCreater/UI_PrototypeChallenges.json")))) throw new Exception("Challenge entry failed"); + SetupUiBundleCollector.Setup(); + AssetDatabase.SaveAssets(); + return "Built the fishing page"; + } +} diff --git a/AIClient/AgentScripts/BuildFriends.cs b/AIClient/AgentScripts/BuildFriends.cs new file mode 100644 index 0000000..874c732 --- /dev/null +++ b/AIClient/AgentScripts/BuildFriends.cs @@ -0,0 +1,18 @@ +using System; +using System.IO; +using AIGame.EditorTools; +using UnityEditor; + +public static class BuildFriends +{ + public static string Build() + { + if (EditorApplication.isPlaying) throw new Exception("Build the friends page in Edit Mode"); + string repo = Directory.GetParent(UnityEngine.Application.dataPath).Parent.FullName; + if (string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(repo, "Doc/UIPrefabCreater/UI_PrototypeFriends.json")))) throw new Exception("Friends prefab failed"); + if (string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(repo, "Doc/UIPrefabCreater/UI_MainHUD.json")))) throw new Exception("Home button failed"); + SetupUiBundleCollector.Setup(); + AssetDatabase.SaveAssets(); + return "Built the friends page"; + } +} diff --git a/AIClient/AgentScripts/BuildPartnerSkillFx.cs b/AIClient/AgentScripts/BuildPartnerSkillFx.cs new file mode 100644 index 0000000..09e4c0a --- /dev/null +++ b/AIClient/AgentScripts/BuildPartnerSkillFx.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object=UnityEngine.Object; + +/// Ten persistent partner silhouettes, authored mesh particles and bounded lightning ribbons. +public static class BuildPartnerSkillFx +{ + public const string Folder="Assets/Bundles/Combat/Partners"; + static readonly Color[] Colors={new Color(1,.72f,.25f,.6f),new Color(1,.3f,.035f),new Color(.8f,1,.4f),new Color(.4f,.85f,1),new Color(.3f,.9f,1,.45f),new Color(.85f,.65f,.3f),new Color(1,.38f,.02f),new Color(.3f,.9f,.45f),new Color(.75f,.55f,1),new Color(.2f,.8f,1,.25f)}; + static Material Glow; + static void Material(){string path=Folder+"/PartnerSkillGlow.mat";Glow=AssetDatabase.LoadAssetAtPath(path);if(Glow==null){Glow=new Material(Shader.Find("AIGame/EnemyCombatParticle"));AssetDatabase.CreateAsset(Glow,path);}Glow.mainTexture=null;EditorUtility.SetDirty(Glow);} + static Mesh Ring=>AssetDatabase.LoadAssetAtPath("Assets/Bundles/Effects/EnemyCombat/CombatRing.asset"); + static void Folders(string path){if(AssetDatabase.IsValidFolder(path))return;string parent=Path.GetDirectoryName(path).Replace('\\','/');Folders(parent);AssetDatabase.CreateFolder(parent,Path.GetFileName(path));} + static Mesh SaveMesh(string name,List vertices,List triangles) + { + string path=Folder+"/"+name+".asset";var mesh=AssetDatabase.LoadAssetAtPath(path);if(mesh==null){mesh=new Mesh();AssetDatabase.CreateAsset(mesh,path);}else mesh.Clear(); + mesh.name=name;mesh.SetVertices(vertices);mesh.SetTriangles(triangles,0);mesh.uv=vertices.Select(v=>new Vector2(.5f+v.x*.4f,.5f+v.y*.4f)).ToArray();mesh.RecalculateBounds();mesh.RecalculateNormals();EditorUtility.SetDirty(mesh);return mesh; + } + static Mesh Gem()=>SaveMesh("SkillStone",new List{Vector3.up*.5f,Vector3.down*.5f,Vector3.left*.35f,Vector3.right*.35f,Vector3.forward*.35f,Vector3.back*.35f},new List{0,2,4,0,4,3,0,3,5,0,5,2,1,4,2,1,3,4,1,5,3,1,2,5}); + static Mesh Leaf()=>SaveMesh("SkillLeaf",new List{new Vector3(0,.6f,0),new Vector3(.32f,0,.06f),new Vector3(0,-.6f,0),new Vector3(-.32f,0,-.06f)},new List{0,1,2,0,2,3}); + static Mesh Cross()=>SaveMesh("SkillHealCross",new List{new Vector3(-.13f,-.5f,0),new Vector3(.13f,-.5f,0),new Vector3(-.13f,.5f,0),new Vector3(.13f,.5f,0),new Vector3(-.5f,-.13f,0),new Vector3(.5f,-.13f,0),new Vector3(-.5f,.13f,0),new Vector3(.5f,.13f,0)},new List{0,1,2,2,1,3,4,5,6,6,5,7}); + static Mesh Sphere(string name,bool dome) + { + var v=new List();var tri=new List();const int rows=6,cols=12; + for(int y=0;y<=rows;y++)for(int x=0;x<=cols;x++){float a=x/(float)cols*Mathf.PI*2,b=y/(float)rows*Mathf.PI*(dome?.5f:1);v.Add(new Vector3(Mathf.Cos(a)*Mathf.Sin(b),Mathf.Cos(b),Mathf.Sin(a)*Mathf.Sin(b))*.5f);} + for(int y=0;y();var tri=new List();const int count=64; + for(int i=0;i<=count;i++){float t=i/(float)count,a=t*Mathf.PI*4;var center=new Vector3(Mathf.Cos(a)*.45f,t-.5f,Mathf.Sin(a)*.45f);v.Add(center+Vector3.up*.022f);v.Add(center-Vector3.up*.022f);if(i();var main=ps.main;main.playOnAwake=false;main.loop=loop;main.duration=.75f;main.startLifetime=life;main.startSize=size;main.startSpeed=speed;main.startColor=color;main.maxParticles=24;main.startRotation3D=true;main.simulationSpace=ParticleSystemSimulationSpace.Local; + var emission=ps.emission;emission.rateOverTime=loop?2:0;if(!loop)emission.SetBursts(new[]{new ParticleSystem.Burst(0,(short)count)}); + var shape=ps.shape;shape.shapeType=ParticleSystemShapeType.Sphere;shape.radius=.3f; + var renderer=ps.GetComponent();renderer.sharedMaterial=Glow;renderer.renderMode=ParticleSystemRenderMode.Mesh;renderer.mesh=mesh;renderer.alignment=ParticleSystemRenderSpace.Local; + var colorLife=ps.colorOverLifetime;colorLife.enabled=true;var gradient=new Gradient();gradient.SetKeys(new[]{new GradientColorKey(Color.white,0),new GradientColorKey(color,1)},new[]{new GradientAlphaKey(.9f,0),new GradientAlphaKey(.65f,.5f),new GradientAlphaKey(0,1)});colorLife.color=gradient; + var sizeLife=ps.sizeOverLifetime;sizeLife.enabled=true;sizeLife.size=new ParticleSystem.MinMaxCurve(1,AnimationCurve.Linear(0,.7f,1,1.15f));return ps; + } + static ParticleSystem Halo(Transform parent,Color color,float size,bool ground=false,bool loop=false) + { + var ps=Part(parent,ground?"GroundRipple":"Halo",Ring,color,1,.55f,size,0,loop);if(ground)ps.transform.localRotation=Quaternion.Euler(90,0,0);return ps; + } + public static string Build() + { + if(EditorApplication.isPlaying)throw new Exception("Author in Edit Mode");Folders(Folder);Material();var config=JObject.Parse(File.ReadAllText("../Config/Combat/partner_presentations_v1.json"));var gem=Gem();var leaf=Leaf();var cross=Cross();var bubble=Sphere("SkillBubble",false);var dome=Sphere("SkillWaterDome",true);var helix=Helix(); + var previous=SceneManager.GetActiveScene();var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive); + try + { + SceneManager.SetActiveScene(scene); + for(int actor=0;actor<10;actor++) + { + var definition=config["effects"][actor];string name=(string)definition["name"];bool loop=(bool)definition["loop"];Color color=Colors[actor]; + var root=Part(null,"Vfx_"+name,actor==2||actor==7?leaf:actor==4||actor==9?bubble:gem,color,loop?2:8,.5f,actor==4?.2f:.12f,actor==2||actor==7?.6f:.4f,loop); + if(actor==0){Halo(root.transform,color,1.8f,false,true);var stones=Part(root.transform,"OrbitingArmor",gem,color,4,.65f,.35f,0,true);var shape=stones.shape;shape.shapeType=ParticleSystemShapeType.Circle;shape.radius=.95f;var rotation=stones.rotationOverLifetime;rotation.enabled=true;rotation.z=1.5f;} + if(actor==1){Halo(root.transform,new Color(1,.45f,.08f,.6f),1.5f);var sparks=Part(root.transform,"EmberSpray",gem,new Color(1,.8f,.2f),12,.35f,.10f,2);var shape=sparks.shape;shape.radius=.1f;} + if(actor==2){Halo(root.transform,new Color(.4f,1,.4f,.4f),1.65f,true);var petals=Part(root.transform,"BlossomPetals",leaf,new Color(1,.7f,.85f),12,.6f,.42f,1.1f);petals.transform.localPosition=Vector3.up*.55f;var plus=Part(root.transform,"HealingBloom",cross,new Color(.8f,1,.45f),1,.6f,.9f);plus.transform.localPosition=Vector3.up*1.35f;} + if(actor==3||actor==8) + { + var line=root.gameObject.AddComponent();line.useWorldSpace=false;line.positionCount=9;line.sharedMaterial=Glow;line.widthMultiplier=actor==3?.06f:.09f;line.startColor=color;line.endColor=Color.white;line.numCapVertices=2;line.enabled=false; + Halo(root.transform,color,actor==3?.9f:1.2f); + if(actor==8)for(int n=0;n<3;n++){var slash=Part(root.transform,"ThunderClaw"+n,leaf,new Color(.8f,.6f,1),1,.2f,1);slash.transform.localPosition=new Vector3((n-1)*.2f,0,0);slash.transform.localRotation=Quaternion.Euler(0,0,-25);slash.transform.localScale=new Vector3(.15f,1.2f,1);} + } + if(actor==4){Halo(root.transform,new Color(.4f,.9f,1,.4f),1.2f);var bubbles=Part(root.transform,"FloatingBubbleCage",bubble,new Color(.5f,.9f,1,.38f),9,.65f,.55f,.65f);var shape=bubbles.shape;shape.radius=.9f;} + if(actor==5){Halo(root.transform,new Color(1,.7f,.3f,.6f),2.5f,true);var chunks=Part(root.transform,"QuakeStones",gem,color,12,.55f,.35f,2f);var main=chunks.main;main.gravityModifier=.6f;var shape=chunks.shape;shape.shapeType=ParticleSystemShapeType.Cone;shape.angle=65;chunks.transform.localRotation=Quaternion.Euler(-90,0,0);} + if(actor==6){var flames=Part(root.transform,"ForwardFlamePlume",leaf,new Color(1,.45f,.03f),20,.4f,.5f,6);var shape=flames.shape;shape.shapeType=ParticleSystemShapeType.Cone;shape.angle=18;shape.radius=.12f;var main=flames.main;main.startSize3D=true;main.startSizeX=.3f;main.startSizeY=.9f;main.startSizeZ=.4f;} + if(actor==7){Halo(root.transform,new Color(.3f,1,.5f,.3f),1.8f,true);var vine=Part(root.transform,"RisingVines",helix,new Color(.3f,1,.45f,.8f),1,.65f,1.6f);vine.transform.localPosition=Vector3.up*.9f;var leaves=Part(root.transform,"GreenLeaves",leaf,new Color(.75f,1,.25f),10,.6f,.3f,.7f);leaves.transform.localPosition=Vector3.up*.6f;} + if(actor==9){Part(root.transform,"WaterDome",dome,new Color(.2f,.85f,1,.16f),1,.65f,2.7f,0,true);Halo(root.transform,new Color(.25f,.85f,1,.25f),1.9f,false,true);var tide=Halo(root.transform,new Color(.4f,.9f,1,.25f),2.2f,true,true);tide.transform.localPosition=Vector3.down*.75f;} + PrefabUtility.SaveAsPrefabAsset(root.gameObject,(string)definition["path"]);Object.DestroyImmediate(root.gameObject); + } + AssetDatabase.SaveAssets();return "Saved ten distinct partner skill prefabs and six low-poly effect meshes"; + } + finally{SceneManager.SetActiveScene(previous);EditorSceneManager.CloseScene(scene,true);} + } +} diff --git a/AIClient/AgentScripts/BuildPrototype.cs b/AIClient/AgentScripts/BuildPrototype.cs index f940d39..542fefb 100644 --- a/AIClient/AgentScripts/BuildPrototype.cs +++ b/AIClient/AgentScripts/BuildPrototype.cs @@ -20,16 +20,16 @@ public static class BuildPrototype /// Validated formal partner art by pet id; other ids keep primitive stand-ins until their art passes review. static readonly System.Collections.Generic.Dictionary FormalPets = new System.Collections.Generic.Dictionary { - [0] = Animal + "RockBall/RockBall_Rigged.prefab", [1] = Animal + "FireCat/FireCat_Companion.prefab", [2] = Animal + "BudBud/BudBud_Companion.prefab", - [3] = Animal + "SparkBird/SparkBird_Companion.prefab", [4] = Animal + "BubbleTurtle/BubbleTurtle_Companion.prefab", - [5] = Animal + "RockRhino/RockRhino_Companion.prefab", [6] = Animal + "FlameLion/FlameLion_Companion.prefab", - [7] = Animal + "VineSprite/VineSprite_Companion.prefab", [8] = Animal + "ThunderLeopard/ThunderLeopard_Companion.prefab", [9] = Animal + "TideWhale/TideWhale_Companion.prefab", + [0] = Animal + "RockBallStudio/RockBallStudio_Companion.prefab", [1] = Animal + "FireCatStudio/FireCatStudio_Companion.prefab", [2] = Animal + "BudBudStudio/BudBudStudio_Companion.prefab", + [3] = Animal + "SparkBirdStudio/SparkBirdStudio_Companion.prefab", [4] = Animal + "BubbleTurtle/BubbleTurtle_Companion.prefab", + [5] = Animal + "RockRhinoStudio/RockRhinoStudio_Companion.prefab", [6] = Animal + "FlameLion/FlameLion_Companion.prefab", + [7] = Animal + "VineSpriteStudio/VineSpriteStudio_Companion.prefab", [8] = Animal + "ThunderLeopardStudio/ThunderLeopardStudio_Companion.prefab", [9] = Animal + "TideWhaleStudio/TideWhaleStudio_Companion.prefab", }; /// Battle height in arena units; the authored models are ~0.55m and the lanes are 3 units apart. public const float FormalHeight = 1.5f; const string Enemy = "Assets/Bundles/Characters/Enemy/"; /// Formal enemy art by enemy type id (greenfield.json enemies 0..4), nested under every Enemies/EnemyN slot as Type{t}. - static readonly string[] FormalEnemies = { Enemy + "Hornlet/Hornlet_Enemy.prefab", Enemy + "Dashlet/Dashlet_Enemy.prefab", Enemy + "Shellhorn/Shellhorn_Enemy.prefab", Enemy + "HornCaptain/HornCaptain_Enemy.prefab", Enemy + "GreatHorn/GreatHorn_Enemy.prefab" }; + static readonly string[] FormalEnemies = { Enemy + "HornletStudio/HornletStudio_Enemy.prefab", Enemy + "DashletStudio/DashletStudio_Enemy.prefab", Enemy + "ShellhornStudio/ShellhornStudio_Enemy.prefab", Enemy + "HornCaptainStudio/HornCaptainStudio_Enemy.prefab", Enemy + "GreatHornStudio/GreatHornStudio_Enemy.prefab" }; /// Base enemy height in arena units; the runtime still scales captains x1.2 and bosses x1.55. public const float EnemyHeight = 1.2f; static Material _cream, _rock, _orange, _leaf, _dark, _white, _cyan, _grass, _path, _monster, _yellow, _water, _ember, _violet; @@ -305,6 +305,12 @@ public static class BuildPrototype if (child.name == "Ground" || child.name == "Lane" || child.name == "LeftBorder" || child.name == "RightBorder" || child.name == "BattleLight") child.gameObject.SetActive(false); } var enemyFeedback = AssetDatabase.LoadAssetAtPath("Assets/Bundles/Effects/EnemyCombat/EnemyCombatPresentation.prefab"); + var skillFeedback = AssetDatabase.LoadAssetAtPath("Assets/Bundles/Combat/BattleSkillPresentation.prefab"); + if(skillFeedback!=null) + { + foreach(string group in new[]{"Heroes","Enemies"})foreach(Transform actor in root.transform.Find(group)){var muzzle=new GameObject("SkillMuzzle").transform;muzzle.SetParent(actor,false);muzzle.localPosition=new Vector3(0,group=="Heroes"?.85f:.8f,-.48f);muzzle.gameObject.layer=30;} + PrefabUtility.InstantiatePrefab(skillFeedback,root.transform); + } if (enemyFeedback != null) PrefabUtility.InstantiatePrefab(enemyFeedback, root.transform); PrefabUtility.SaveAsPrefabAsset(root, World + "/PrototypeArena.prefab"); } @@ -390,7 +396,7 @@ public static class BuildPrototype var enemies = root.transform.Find("Enemies") ?? throw new Exception("Arena missing Enemies"); foreach (Transform slot in enemies) { - foreach (var child in slot.Cast().Where(c => c.name != "SlowMark").ToArray()) UnityEngine.Object.DestroyImmediate(child.gameObject); + foreach (var child in slot.Cast().Where(c => c.name != "SlowMark" && c.name != "SkillMuzzle").ToArray()) UnityEngine.Object.DestroyImmediate(child.gameObject); if (!FormalEnemyVisuals(slot)) throw new Exception("Formal enemy prefabs incomplete: " + string.Join(", ", FormalEnemies.Where(p => AssetDatabase.LoadAssetAtPath(p) == null))); } PrefabUtility.SaveAsPrefabAsset(root, World + "/PrototypeArena.prefab"); diff --git a/AIClient/AgentScripts/BuildSkillSlice.cs b/AIClient/AgentScripts/BuildSkillSlice.cs new file mode 100644 index 0000000..8cc3e86 --- /dev/null +++ b/AIClient/AgentScripts/BuildSkillSlice.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using System.Linq; +using HotUpdate.Gameplay; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object=UnityEngine.Object; + +/// Persistent, independent skill review. Never edits the production arena or account. +public static class BuildSkillSlice +{ + public const string Folder="Assets/Bundles/Combat/SkillSlice"; + public const string Prefab=Folder+"/SkillSlice.prefab"; + public const string Scene="Assets/Bundles/Scenes/Review/SkillSlice_Review.unity"; + static readonly string[] Names={"FireCharge","Fireball","FireHit","RockHit","Shield"}; + static readonly string[] SoundNames={"FireCharge","FireLaunch","FireHit","RockHit","Shield"}; + static void Folders(string path){if(AssetDatabase.IsValidFolder(path))return;string parent=Path.GetDirectoryName(path).Replace('\\','/');Folders(parent);AssetDatabase.CreateFolder(parent,Path.GetFileName(path));} + static ParticleSystem Part(Transform parent,string name,Color color,int count,float life,float size,bool loop=false,Mesh mesh=null) + { + var go=new GameObject(name);go.transform.SetParent(parent,false);go.layer=30;var ps=go.AddComponent(); + var main=ps.main;main.playOnAwake=false;main.loop=loop;main.duration=.5f;main.startLifetime=life;main.startSpeed=mesh!=null?0:.25f;main.startSize=size;main.startColor=color;main.maxParticles=32;main.simulationSpace=ParticleSystemSimulationSpace.Local; + var emission=ps.emission;emission.rateOverTime=loop?count:0;if(!loop)emission.SetBursts(new[]{new ParticleSystem.Burst(0,(short)count)}); + var shape=ps.shape;shape.shapeType=ParticleSystemShapeType.Sphere;shape.radius=loop?.035f:.08f; + var colorLife=ps.colorOverLifetime;colorLife.enabled=true;var gradient=new Gradient();gradient.SetKeys(new[]{new GradientColorKey(Color.white,0),new GradientColorKey(color,1)},new[]{new GradientAlphaKey(.85f,0),new GradientAlphaKey(0,1)});colorLife.color=gradient; + var sizeLife=ps.sizeOverLifetime;sizeLife.enabled=true;sizeLife.size=new ParticleSystem.MinMaxCurve(1,AnimationCurve.Linear(0,1,1,.1f)); + var renderer=ps.GetComponent();renderer.sharedMaterial=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Effects/EnemyCombat/CombatGlow.mat");renderer.renderMode=mesh==null?ParticleSystemRenderMode.Billboard:ParticleSystemRenderMode.Mesh;if(mesh!=null)renderer.mesh=mesh; + return ps; + } + static float Height(Transform root) + { + float top=0;var mesh=new Mesh();foreach(var skin in root.GetComponentsInChildren(true)){skin.BakeMesh(mesh,true);foreach(var v in mesh.vertices)top=Mathf.Max(top,root.InverseTransformPoint(skin.transform.TransformPoint(v)).y);}Object.DestroyImmediate(mesh);return top; + } + public static string Build() + { + if(EditorApplication.isPlaying)throw new Exception("Author in Edit Mode");Folders(Folder); + AssetDatabase.ImportAsset(Folder+"/trace.json",ImportAssetOptions.ForceUpdate);var trace=AssetDatabase.LoadAssetAtPath(Folder+"/trace.json");var data=JObject.Parse(trace.text); + var previous=SceneManager.GetActiveScene();var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive); + try + { + SceneManager.SetActiveScene(scene);var root=new GameObject("SkillSlice");var player=root.AddComponent(); + var camera=new GameObject("SkillSliceCamera").AddComponent();camera.transform.SetParent(root.transform,false);camera.transform.localPosition=new Vector3(8,9,-11);camera.transform.LookAt(new Vector3(0,.5f,1.1f));camera.orthographic=true;camera.orthographicSize=4.4f;camera.cullingMask=1<<30;camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=new Color(.09f,.13f,.17f);camera.depth=100; + var light=new GameObject("SkillSliceLight").AddComponent();light.transform.SetParent(root.transform,false);light.type=LightType.Directional;light.intensity=1.3f;light.transform.rotation=Quaternion.Euler(40,-35,0);light.cullingMask=1<<30; + var floor=GameObject.CreatePrimitive(PrimitiveType.Plane);floor.name="Ground_Y0";floor.transform.SetParent(root.transform,false);floor.transform.localScale=new Vector3(.6f,1,1);floor.transform.localPosition=new Vector3(0,-.005f,1);floor.GetComponent().sharedMaterial=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Review/EnemyReviewFloor.mat");Object.DestroyImmediate(floor.GetComponent()); + var so=new SerializedObject(player);so.FindProperty("_trace").objectReferenceValue=trace; + var views=so.FindProperty("_views");views.arraySize=((JArray)data["initial"]).Count; + int index=0; + foreach(var u in (JArray)data["initial"]) + { + int id=(int)u["id"];string actor=id==0?"RockBall":id==1?"FireCat":"Hornlet",kind=id<100?"Animal":"Enemy"; + var slot=new GameObject("Entity"+id).transform;slot.SetParent(root.transform,false);slot.localPosition=new Vector3(0,0,(int)u["z"]/1000f);slot.localRotation=Quaternion.Euler(0,id<100?180:0,0); + string suffix=id<100?"Companion":"Enemy";var model=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath("Assets/Bundles/Characters/"+kind+"/"+actor+"Studio/"+actor+"Studio_"+suffix+".prefab"),slot); + model.transform.localScale=Vector3.one*((id<100?1.5f:1.2f)/Height(model.transform)); + var muzzle=new GameObject("Muzzle").transform;muzzle.SetParent(slot,false);muzzle.localPosition=new Vector3(0,.8f,-.48f); + var label=new GameObject("State").AddComponent();label.transform.SetParent(slot,false);label.transform.localPosition=new Vector3(0,1.85f,0);label.transform.rotation=camera.transform.rotation;label.fontSize=60;label.characterSize=.032f;label.anchor=TextAnchor.MiddleCenter;label.color=Color.white; + string recipe="../Config/Art/"+(id==1?"Companions":"EnemyCharacters")+"/"+actor+".json";float impact=(float)JObject.Parse(File.ReadAllText(recipe))["attackImpact"]; + var v=views.GetArrayElementAtIndex(index++);v.FindPropertyRelative("id").intValue=id;v.FindPropertyRelative("root").objectReferenceValue=slot;v.FindPropertyRelative("muzzle").objectReferenceValue=muzzle;v.FindPropertyRelative("label").objectReferenceValue=label;v.FindPropertyRelative("animator").objectReferenceValue=model.GetComponentInChildren();v.FindPropertyRelative("impact").floatValue=impact; + } + var ring=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Effects/EnemyCombat/CombatRing.asset"); + const string corePath=Folder+"/FireballCore.asset";var core=AssetDatabase.LoadAssetAtPath(corePath); + if(core==null){var sphere=GameObject.CreatePrimitive(PrimitiveType.Sphere);core=Object.Instantiate(sphere.GetComponent().sharedMesh);core.name="FireballCore";Object.DestroyImmediate(sphere);AssetDatabase.CreateAsset(core,corePath);} + var effects=so.FindProperty("_effects");effects.arraySize=5; + for(int i=0;i<5;i++) + { + bool loop=i==0||i==1||i==4;Color color=i==4?new Color(.35f,.85f,1):i==3?new Color(1,.78f,.35f):new Color(1,.28f,.025f); + var authored=Part(root.transform,"Vfx_Skill_"+Names[i],color,loop?12:14,i==4?.35f:i<2?.2f:.35f,i==4?2.3f:i==1?.45f:i==0?.25f:.15f,loop,i==4?ring:null); + if(i<4){var renderer=authored.GetComponent();renderer.renderMode=ParticleSystemRenderMode.Mesh;renderer.mesh=core;} + if(i==4){var main=authored.main;main.startColor=new Color(.35f,.85f,1,.22f);main.startSize=1.8f;main.startLifetime=.6f;var emission=authored.emission;emission.rateOverTime=4;} + if(i==2||i==3){var flash=Part(authored.transform,"ImpactRing",color,1,.25f,1.35f,false,ring);var size=flash.sizeOverLifetime;size.size=new ParticleSystem.MinMaxCurve(1,AnimationCurve.Linear(0,.3f,1,1.2f));} + if(i==1)Part(authored.transform,"BrightCore",new Color(1,.85f,.25f),20,.12f,.2f,true); + var asset=PrefabUtility.SaveAsPrefabAsset(authored.gameObject,Folder+"/Vfx_Skill_"+Names[i]+".prefab");Object.DestroyImmediate(authored.gameObject); + var instances=effects.GetArrayElementAtIndex(i).FindPropertyRelative("instances");instances.arraySize=i==1?8:4; + for(int n=0;n();} + } + var clips=so.FindProperty("_clips");clips.arraySize=5; + for(int i=0;i<5;i++) + { + string path="Assets/Bundles/Sound/SFX/BattleSkills/Sfx_"+SoundNames[i]+".wav";AssetDatabase.ImportAsset(path,ImportAssetOptions.ForceUpdate);var importer=(AudioImporter)AssetImporter.GetAtPath(path);if(importer==null)throw new Exception("Missing "+path); + var sample=importer.defaultSampleSettings;sample.loadType=AudioClipLoadType.DecompressOnLoad;sample.compressionFormat=AudioCompressionFormat.PCM;sample.sampleRateSetting=AudioSampleRateSetting.PreserveSampleRate;importer.defaultSampleSettings=sample;importer.forceToMono=false;importer.SaveAndReimport();clips.GetArrayElementAtIndex(i).objectReferenceValue=AssetDatabase.LoadAssetAtPath(path); + } + var voices=so.FindProperty("_voices");voices.arraySize=8; + for(int i=0;i<8;i++){var go=new GameObject("Voice"+i);go.transform.SetParent(root.transform,false);var voice=go.AddComponent();voice.playOnAwake=false;voice.spatialBlend=0;voice.volume=.7f/8;voices.GetArrayElementAtIndex(i).objectReferenceValue=voice;} + var feedback=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath("Assets/Bundles/Effects/EnemyCombat/EnemyCombatPresentation.prefab"),root.transform);so.FindProperty("_enemyFeedback").objectReferenceValue=feedback.GetComponent(); + var status=new GameObject("TraceStatus").AddComponent();status.transform.SetParent(root.transform,false);status.transform.localPosition=new Vector3(0,.3f,-3.5f);status.transform.rotation=camera.transform.rotation;status.fontSize=60;status.characterSize=.04f;status.anchor=TextAnchor.MiddleCenter;status.color=new Color(.8f,.95f,1);so.FindProperty("_status").objectReferenceValue=status; + so.ApplyModifiedPropertiesWithoutUndo();foreach(var t in root.GetComponentsInChildren(true))t.gameObject.layer=30; + PrefabUtility.SaveAsPrefabAsset(root,Prefab);Object.DestroyImmediate(root); + PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Prefab),scene);EditorSceneManager.SaveScene(scene,Scene);AssetDatabase.SaveAssets();return "Saved independent "+Prefab+" and "+Scene; + } + finally{SceneManager.SetActiveScene(previous);EditorSceneManager.CloseScene(scene,true);} + } +} diff --git a/AIClient/AgentScripts/BuildT2Visuals.cs b/AIClient/AgentScripts/BuildT2Visuals.cs new file mode 100644 index 0000000..5fdb391 --- /dev/null +++ b/AIClient/AgentScripts/BuildT2Visuals.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +/// +/// T2 evolution visuals: each partner keeps its validated T1 prefab (mesh, rig, clips) and gains one evolution accessory +/// parented to its own head bone (RockRhino: chest/back). Output {Name}_T2.prefab variants, then nest them as Heroes/HeroN/VisualT2. +/// Accessory statics come from SetupCharacter at Characters/Animal/{Name}/{Name}T2/. +/// +public static class BuildT2Visuals +{ + const string Animal = "Assets/Bundles/Characters/Animal/"; + const string Arena = "Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab"; + const int BattleLayer = 30; + + sealed class Spec + { + public int Pet; public string Name, T1; + /// Accessory width as a fraction of the anchor region's width; Back mounts on the chest instead of the head. + public float Width = .75f, Drop; public bool Back; public string Bone; + // Bone is set where the top-most vertices are not the mount: RockBall's small crystal tip (BubbleTurtle wears it on the shell top on purpose), ThunderLeopard's ears. Drop lowers the mount by a fraction of the height (SparkBird's crest otherwise lifts it). + } + + static readonly Spec[] Specs = + { + new Spec { Pet = 0, Name = "RockBall", T1 = "RockBallStudio/RockBallStudio_Companion.prefab", Width = .55f, Bone = "bone_1" }, + new Spec { Pet = 1, Name = "FireCat", T1 = "FireCatStudio/FireCatStudio_Companion.prefab", Width = .55f }, + new Spec { Pet = 2, Name = "BudBud", T1 = "BudBudStudio/BudBudStudio_Companion.prefab", Width = .5f }, + new Spec { Pet = 3, Name = "SparkBird", T1 = "SparkBirdStudio/SparkBirdStudio_Companion.prefab", Width = .6f, Drop = .1f }, + new Spec { Pet = 4, Name = "BubbleTurtle", T1 = "BubbleTurtle/BubbleTurtle_Companion.prefab", Width = .55f }, + new Spec { Pet = 5, Name = "RockRhino", T1 = "RockRhinoStudio/RockRhinoStudio_Companion.prefab", Width = .55f, Back = true, Bone = "bone_1" }, + new Spec { Pet = 6, Name = "FlameLion", T1 = "FlameLion/FlameLion_Companion.prefab", Width = .55f }, + new Spec { Pet = 7, Name = "VineSprite", T1 = "VineSpriteStudio/VineSpriteStudio_Companion.prefab", Width = .8f }, + new Spec { Pet = 8, Name = "ThunderLeopard", T1 = "ThunderLeopardStudio/ThunderLeopardStudio_Companion.prefab", Width = .6f, Bone = "bone_4" }, + new Spec { Pet = 9, Name = "TideWhale", T1 = "TideWhaleStudio/TideWhaleStudio_Companion.prefab", Width = .55f }, + }; + + static string AccessoryPath(Spec s) => Animal + s.Name + "/" + s.Name + "T2/" + s.Name + "T2.prefab"; + static string T2Path(Spec s) => Animal + s.Name + "/" + s.Name + "_T2.prefab"; + static void SetLayer(Transform t) { t.gameObject.layer = BattleLayer; foreach (Transform c in t) SetLayer(c); } + + /// Bind-pose skinned points in with the dominant bone of each vertex. + static List<(Vector3 p, Transform bone)> Skin(GameObject root, Transform space) + { + var result = new List<(Vector3, Transform)>(); var mesh = new Mesh(); + try + { + foreach (var skin in root.GetComponentsInChildren(true)) + { + skin.BakeMesh(mesh, true); var weights = skin.sharedMesh.boneWeights; var bones = skin.bones; var v = mesh.vertices; + for (int i = 0; i < v.Length; i++) result.Add((space.InverseTransformPoint(skin.transform.TransformPoint(v[i])), bones[weights[i].boneIndex0])); + } + } + finally { UnityEngine.Object.DestroyImmediate(mesh); } + return result; + } + + static Bounds MeshBounds(GameObject go, Transform space) + { + var points = go.GetComponentsInChildren(true).SelectMany(f => f.sharedMesh.vertices.Select(v => space.InverseTransformPoint(f.transform.TransformPoint(v)))).ToArray(); + var b = new Bounds(points[0], Vector3.zero); foreach (var p in points) b.Encapsulate(p); return b; + } + + /// Builds {Name}_T2.prefab for every partner whose accessory static exists; returns one line per partner. + public static string BuildPrefabs() + { + if (EditorApplication.isPlaying) throw new Exception("Build T2 visuals in Edit Mode"); + var lines = new List(); + var scene = EditorSceneManager.NewPreviewScene(); + try + { + foreach (var s in Specs) + { + var accessory = AssetDatabase.LoadAssetAtPath(AccessoryPath(s)); + if (accessory == null) { lines.Add(s.Name + ": accessory missing, skipped"); continue; } + var t1 = AssetDatabase.LoadAssetAtPath(Animal + s.T1) ?? throw new Exception("Missing T1 " + s.T1); + var root = (GameObject)PrefabUtility.InstantiatePrefab(t1, scene); + var points = Skin(root, root.transform); + float h = points.Max(x => x.p.y) - points.Min(x => x.p.y); + // Anchor bone: the named one, else the bone owning most of the top 3% of vertices (the head). + Transform bone = s.Bone != null ? root.GetComponentsInChildren(true).First(t => t.name == s.Bone) + : points.OrderByDescending(x => x.p.y).Take(Math.Max(1, points.Count * 3 / 100)).GroupBy(x => x.bone).OrderByDescending(g => g.Count()).First().Key; + var region = points.Where(x => x.bone == bone).Select(x => x.p).ToArray(); + float cx = (region.Min(p => p.x) + region.Max(p => p.x)) / 2, width = region.Max(p => p.x) - region.Min(p => p.x); + var crown = region.Where(p => Mathf.Abs(p.x - cx) < width * .2f).ToArray(); + var top = crown.OrderByDescending(p => p.y).First(); + float cz = crown.Where(p => p.y > top.y - h * .06f).Average(p => p.z); + + var acc = (GameObject)PrefabUtility.InstantiatePrefab(accessory, scene); acc.name = "T2Accessory"; + acc.transform.SetParent(root.transform, false); + var b = MeshBounds(acc, acc.transform); + float scale = s.Width * width / Mathf.Max(1e-4f, Mathf.Max(b.size.x, b.size.z)); + acc.transform.localScale = Vector3.one * scale; + // Sink the base a quarter of its height into the head/back so it reads as worn, not floating. + float accHeight = b.size.y * scale; + acc.transform.localPosition = new Vector3(cx - b.center.x * scale, top.y - b.min.y * scale - accHeight * .25f - s.Drop * h, cz - b.center.z * scale); + acc.transform.SetParent(bone, true); + PrefabUtility.SaveAsPrefabAsset(root, T2Path(s)); + lines.Add(s.Name + ": " + T2Path(s) + " bone=" + bone.name + " width=" + width.ToString("F3") + " accScale=" + scale.ToString("F3") + " top=" + (top.y + accHeight * .75f).ToString("F3") + "/" + h.ToString("F3")); + UnityEngine.Object.DestroyImmediate(root); + } + } + finally { EditorSceneManager.ClosePreviewScene(scene); } + return string.Join("\n", lines); + } + + /// Nests each {Name}_T2 prefab as Heroes/HeroN/VisualT2 (inactive, same scale as Visual); runtime shows it for tier 2. + public static string ApplyArena() + { + if (EditorApplication.isPlaying) throw new Exception("Apply T2 visuals in Edit Mode"); + var root = PrefabUtility.LoadPrefabContents(Arena); var done = new List(); + try + { + foreach (var s in Specs) + { + var t2 = AssetDatabase.LoadAssetAtPath(T2Path(s)); if (t2 == null) continue; + var hero = root.transform.Find("Heroes/Hero" + s.Pet) ?? throw new Exception("Arena missing Hero" + s.Pet); + var visual = hero.Find("Visual") ?? throw new Exception("Hero" + s.Pet + " has no formal T1 Visual"); + var old = hero.Find("VisualT2"); if (old != null) UnityEngine.Object.DestroyImmediate(old.gameObject); + var go = (GameObject)PrefabUtility.InstantiatePrefab(t2, root.scene); go.name = "VisualT2"; + go.transform.SetParent(hero, false); go.transform.localPosition = visual.localPosition; go.transform.localRotation = visual.localRotation; go.transform.localScale = visual.localScale; + SetLayer(go.transform); go.SetActive(false); done.Add(s.Pet); + } + PrefabUtility.SaveAsPrefabAsset(root, Arena); + } + finally { PrefabUtility.UnloadPrefabContents(root); } + return "Arena VisualT2 nested for heroes " + string.Join(",", done); + } + + public static string Verify() + { + var arena = AssetDatabase.LoadAssetAtPath(Arena); var lines = new List(); + foreach (var s in Specs) + { + var t2 = AssetDatabase.LoadAssetAtPath(T2Path(s)); if (t2 == null) { lines.Add(s.Name + "=missing"); continue; } + var acc = t2.GetComponentsInChildren(true).FirstOrDefault(t => t.name == "T2Accessory"); + if (acc == null || acc.parent == t2.transform || acc.GetComponentInParent() != null) throw new Exception(s.Name + " accessory not parented to a bone"); + if (PrefabUtility.GetCorrespondingObjectFromSource(t2) == null) throw new Exception(s.Name + "_T2 is not a variant of its T1 prefab"); + var hero = arena.transform.Find("Heroes/Hero" + s.Pet); var v2 = hero?.Find("VisualT2"); + if (v2 == null || v2.gameObject.activeSelf || v2.GetComponentsInChildren(true).Any(t => t.gameObject.layer != BattleLayer)) throw new Exception("Hero" + s.Pet + " VisualT2 missing/active/wrong layer"); + if ((v2.localScale - hero.Find("Visual").localScale).sqrMagnitude > 1e-8f) throw new Exception("Hero" + s.Pet + " VisualT2 scale differs from Visual"); + lines.Add(s.Name + "=" + acc.parent.name); + } + return "PASS T2 " + string.Join(", ", lines); + } +} diff --git a/AIClient/AgentScripts/BuildTreasure.cs b/AIClient/AgentScripts/BuildTreasure.cs new file mode 100644 index 0000000..64df83b --- /dev/null +++ b/AIClient/AgentScripts/BuildTreasure.cs @@ -0,0 +1,17 @@ +using System; +using System.IO; +using AIGame.EditorTools; +using UnityEditor; + +public static class BuildTreasure +{ + public static string Build() + { + if (EditorApplication.isPlaying) throw new Exception("Build the friends page in Edit Mode"); + string repo = Directory.GetParent(UnityEngine.Application.dataPath).Parent.FullName; + if (string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(repo, "Doc/UIPrefabCreater/UI_PrototypeFriends.json")))) throw new Exception("Friends prefab failed"); + SetupUiBundleCollector.Setup(); + AssetDatabase.SaveAssets(); + return "Built the treasure button"; + } +} diff --git a/AIClient/AgentScripts/GreatHornStudioTrial.cs b/AIClient/AgentScripts/GreatHornStudioTrial.cs new file mode 100644 index 0000000..a45b68f --- /dev/null +++ b/AIClient/AgentScripts/GreatHornStudioTrial.cs @@ -0,0 +1,187 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object=UnityEngine.Object; + +/// Isolated candidate uses the ORIGINAL avatar, controller and finished clips. No animation regeneration. +public static class GreatHornStudioTrial +{ + const string Original="Assets/Bundles/Characters/Enemy/GreatHorn"; + const string Trial="Assets/Bundles/Characters/Enemy/GreatHornStudio"; + const string ScenePath="Assets/Bundles/Scenes/Review/Enemy_GreatHornStudio_Review.unity"; + static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../..")); + static void Check(bool value,string message){if(!value)throw new Exception(message);} + static void Write(string name,object data){string dir=Path.Combine(Repo,"Tools/output/enemy_studio_trial/GreatHorn");Directory.CreateDirectory(dir);File.WriteAllText(Path.Combine(dir,name),JsonConvert.SerializeObject(data,Formatting.Indented));} + static Vector3[] Vertices(GameObject go) + { + return go.GetComponentsInChildren().SelectMany(s=>{ + var m=s.sharedMesh;var weights=m.boneWeights;var binds=m.bindposes;var matrices=s.bones.Select((b,i)=>b.localToWorldMatrix*binds[i]).ToArray(); + return m.vertices.Select((v,i)=>{var w=weights[i];return matrices[w.boneIndex0].MultiplyPoint3x4(v)*w.weight0+matrices[w.boneIndex1].MultiplyPoint3x4(v)*w.weight1+matrices[w.boneIndex2].MultiplyPoint3x4(v)*w.weight2+matrices[w.boneIndex3].MultiplyPoint3x4(v)*w.weight3;}).ToArray(); + }).ToArray(); + } + static Bounds Bounds(Vector3[] points){var b=new Bounds(points[0],Vector3.zero);foreach(var p in points)b.Encapsulate(p);return b;} + static AnimationClip Clip(string name)=>AssetDatabase.LoadAssetAtPath(Original+"/Clips/"+name+".anim"); + public static string Inspect() + { + var prefab=AssetDatabase.LoadAssetAtPath(Original+"/GreatHorn_Enemy.prefab");var anim=prefab.GetComponentInChildren(); + return JsonConvert.SerializeObject(new{model=anim.name,scale=anim.transform.localScale.ToString("F6"),position=anim.transform.localPosition.ToString("F6"),rotation=anim.transform.localEulerAngles.ToString("F6"),avatar=AssetDatabase.GetAssetPath(anim.avatar),controller=AssetDatabase.GetAssetPath(anim.runtimeAnimatorController),bones=prefab.GetComponentsInChildren().Where(t=>t.name.StartsWith("bone_")).Select(t=>new{name=t.name,path=AnimationUtility.CalculateTransformPath(t,anim.transform),position=t.localPosition.ToString("F6"),rotation=t.localRotation.ToString("F6")}).ToArray()},Formatting.Indented); + } + public static string Build()=>BuildCore(true); + public static string Prepare()=>BuildCore(false); + static string BuildCore(bool verify) + { + Check(!EditorApplication.isPlaying,"Author trial in Edit Mode"); + string fbx=Trial+"/Rigged/GreatHornStudio_Rigged.fbx"; + AssetDatabase.ImportAsset(fbx,ImportAssetOptions.ForceUpdate); + var imp=(ModelImporter)AssetImporter.GetAtPath(fbx);imp.animationType=ModelImporterAnimationType.Generic;imp.avatarSetup=ModelImporterAvatarSetup.CreateFromThisModel;imp.importAnimation=false;imp.globalScale=1;imp.useFileScale=false;imp.bakeAxisConversion=true;imp.materialImportMode=ModelImporterMaterialImportMode.None;imp.meshCompression=ModelImporterMeshCompression.Off;imp.SaveAndReimport(); + var reference=AssetDatabase.LoadAssetAtPath(Original+"/GreatHorn_Enemy.prefab").GetComponentInChildren(); + var scene=EditorSceneManager.NewPreviewScene(); + try + { + var root=new GameObject("GreatHornStudio_Enemy");SceneManager.MoveGameObjectToScene(root,scene); + var model=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(fbx),scene);model.transform.SetParent(root.transform,false);model.name="Model"; + model.transform.localPosition=reference.transform.localPosition;model.transform.localRotation=reference.transform.localRotation;model.transform.localScale=reference.transform.localScale; + var anim=model.GetComponent();if(anim==null)anim=model.AddComponent();anim.avatar=reference.avatar;anim.runtimeAnimatorController=reference.runtimeAnimatorController;anim.applyRootMotion=false;anim.cullingMode=AnimatorCullingMode.AlwaysAnimate; + var material=AssetDatabase.LoadAssetAtPath(Trial+"/GreatHornStudio.mat");foreach(var skin in model.GetComponentsInChildren()) {skin.sharedMaterial=material;skin.updateWhenOffscreen=true;} + var expected=reference.GetComponentsInChildren().Where(t=>t.name.StartsWith("bone_")).ToDictionary(t=>AnimationUtility.CalculateTransformPath(t,reference.transform)); + var actual=model.GetComponentsInChildren().Where(t=>t.name.StartsWith("bone_")).ToDictionary(t=>AnimationUtility.CalculateTransformPath(t,model.transform)); + Check(expected.Count==22&&expected.Keys.OrderBy(x=>x).SequenceEqual(actual.Keys.OrderBy(x=>x)),"Original skeleton paths changed"); + foreach(var entry in expected){var t=actual[entry.Key];Check(Vector3.Distance(t.localPosition,entry.Value.localPosition)<.0001f&&Quaternion.Angle(t.localRotation,entry.Value.localRotation)<.01f&&Vector3.Distance(t.localScale,entry.Value.localScale)<.0001f,"Bind transform changed "+entry.Key);} + PrefabUtility.SaveAsPrefabAsset(root,Trial+"/GreatHornStudio_Enemy.prefab"); + } + finally{EditorSceneManager.ClosePreviewScene(scene);} + AssetDatabase.SaveAssets();return verify?Verify():"Prepared original skeleton/controller candidate; sole constraints pending"; + } + public static string Verify() + { + var scene=EditorSceneManager.NewPreviewScene(); + try + { + var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Trial+"/GreatHornStudio_Enemy.prefab"),scene);var anim=go.GetComponentInChildren();anim.enabled=false; + var reference=AssetDatabase.LoadAssetAtPath(Original+"/GreatHorn_Enemy.prefab").GetComponentInChildren();Check(anim.runtimeAnimatorController==reference.runtimeAnimatorController&&anim.avatar==reference.avatar,"Expected exact original controller/avatar"); + var bind=Vertices(go);var bounds=Bounds(bind);int triangles=go.GetComponentsInChildren().Sum(s=>s.sharedMesh.triangles.Length/3);Check(triangles<=2600,"Studio target tolerance exceeded");Check(Mathf.Abs(bounds.size.y-.8f)<.003f&&Mathf.Abs(bounds.min.y)<.003f,"Bind ground/height changed "+bounds); + var staticGo=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Trial+"/GreatHornStudio.prefab"),scene);var staticPoints=staticGo.GetComponentsInChildren().SelectMany(m=>m.sharedMesh.vertices.Select(v=>m.transform.TransformPoint(v))).ToArray();float bindError=bind.Max(v=>staticPoints.Min(p=>Vector3.Distance(v,p)));Check(bindError<.002f,"Weight transfer changed static geometry "+bindError); + Check(go.GetComponentsInChildren().All(s=>s.sharedMesh.boneWeights.All(w=>Mathf.Abs(w.weight0+w.weight1+w.weight2+w.weight3-1)<.001f)),"Invalid weights"); + var rows=new List();var root=go.GetComponentsInChildren().Single(t=>t.name=="bone_0"); + foreach(string name in new[]{"Idle","Walk","Attack"}) + { + var clip=Clip(name);Check(anim.runtimeAnimatorController.animationClips.Contains(clip),"Not original clip "+name); + foreach(var binding in AnimationUtility.GetCurveBindings(clip))Check(string.IsNullOrEmpty(binding.path)||anim.transform.Find(binding.path)!=null,"Unbound original curve "+binding.path); + Vector3[] start=null,end=null;float minY=float.MaxValue,maxY=float.MinValue,movement=0,tilt=0;Vector3 rootStart=Vector3.zero;float drift=0; + for(int frame=0;frame<=120;frame++) {clip.SampleAnimation(anim.gameObject,clip.length*frame/120);var points=Vertices(go);Check(points.All(p=>!float.IsNaN(p.x)&&!float.IsInfinity(p.x)),"Invalid mesh");var b=Bounds(points);if(frame==0){start=points;rootStart=root.position;}if(frame==120)end=points;minY=Mathf.Min(minY,b.min.y);maxY=Mathf.Max(maxY,b.min.y);movement=Mathf.Max(movement,points.Select((p,i)=>Vector3.Distance(p,start[i])).Max());tilt=Mathf.Max(tilt,Vector3.Angle(root.up,Vector3.up));drift=Mathf.Max(drift,Vector2.Distance(new Vector2(root.position.x,root.position.z),new Vector2(rootStart.x,rootStart.z)));Check(b.size.y>.64f,"Collapsed mesh "+name);} + float seam=start.Select((p,i)=>Vector3.Distance(p,end[i])).Max();Check(minY>=-.003f&&maxY<=.003f,"Ground contact failed "+name+" "+minY+".."+maxY);Check(tilt<25&&seam<.001f,"Orientation/seam failed "+name);Check(movement>.0002f,"Motion invisible "+name); + rows.Add(new{name,originalClip=AssetDatabase.GetAssetPath(clip),frames=121,minY,maxY,movement,tilt,drift,seam}); + } + Write("unity_verification.json",new{status="pass",triangles,bones=22,bindError,bindHeight=bounds.size.y,originalAvatar=true,originalController=true,originalClips=true,rows});return "PASS original 22-bone bind, exact original avatar/controller/3 clips, static geometry, weights and 121-frame motion samples"; + } + finally{EditorSceneManager.ClosePreviewScene(scene);} + } + static void Label(Transform parent,string text,Vector3 position,float size) + {var label=new GameObject("Label_"+text).AddComponent();label.transform.SetParent(parent,false);label.transform.localPosition=position;label.transform.localRotation=Quaternion.Euler(12,0,0);label.text=text;label.fontSize=60;label.characterSize=size;label.anchor=TextAnchor.MiddleCenter;label.color=new Color(.87f,.93f,1);} + public static string BuildReview() + { + Check(!EditorApplication.isPlaying,"Review authoring requires Edit Mode");var previous=SceneManager.GetActiveScene();var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive); + try + { + SceneManager.SetActiveScene(scene);var root=new GameObject("GreatHornStudioComparison");root.transform.position=new Vector3(300,0,0); + var names=new[]{"Static","Idle","Walk","Attack"}; + for(int row=0;row<2;row++) + { + string folder=row==0?Original:Trial,actor=row==0?"GreatHorn":"GreatHornStudio"; + for(int col=0;col<4;col++) + { + var asset=AssetDatabase.LoadAssetAtPath(folder+"/"+actor+(col==0?".prefab":"_Enemy.prefab"));var go=(GameObject)PrefabUtility.InstantiatePrefab(asset,scene);go.name=(row==0?"Original_":"Studio_")+names[col];go.transform.SetParent(root.transform,false);go.transform.localPosition=new Vector3((col-1.5f)*1.8f,0,row==0?1.3f:-1.3f); + var anim=go.GetComponentInChildren();if(anim!=null){anim.enabled=false;Clip(names[col]).SampleAnimation(anim.gameObject,Clip(names[col]).length*(col==1?.5f:col==2?.25f:.45f));} + Label(go.transform,(row==0?"Original":"Studio")+" / "+names[col],new Vector3(0,-.22f,-.1f),.025f); + if(anim!=null) + { + anim.enabled=true;var cycle=go.AddComponent();cycle.enabled=false;var so=new SerializedObject(cycle);so.FindProperty("fixedMotion").intValue=col-1;so.ApplyModifiedPropertiesWithoutUndo();cycle.enabled=true; + } + } + } + var floor=GameObject.CreatePrimitive(PrimitiveType.Plane);floor.name="Ground_Y0";floor.transform.SetParent(root.transform,false);floor.transform.localPosition=new Vector3(0,-.002f,0);floor.transform.localScale=new Vector3(.85f,1,.55f);floor.GetComponent().sharedMaterial=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Review/EnemyReviewFloor.mat");Object.DestroyImmediate(floor.GetComponent()); + var camera=new GameObject("GreatHornStudioCamera").AddComponent();camera.transform.SetParent(root.transform,false);camera.transform.localPosition=new Vector3(0,7,-10);camera.transform.LookAt(root.transform.position+new Vector3(0,.45f,0));camera.orthographic=true;camera.orthographicSize=2.05f;camera.aspect=2;camera.nearClipPlane=.1f;camera.farClipPlane=100;camera.cullingMask=1<<27;camera.backgroundColor=new Color(.075f,.105f,.14f);camera.clearFlags=CameraClearFlags.SolidColor;camera.depth=85; + var light=new GameObject("StudioReviewLight").AddComponent();light.transform.SetParent(root.transform,false);light.type=LightType.Directional;light.transform.rotation=Quaternion.Euler(40,-35,0);light.intensity=1.1f;light.cullingMask=1<<27; + foreach(var t in root.GetComponentsInChildren(true))t.gameObject.layer=27; + EditorSceneManager.SaveScene(scene,ScenePath); + } + finally{SceneManager.SetActiveScene(previous);EditorSceneManager.CloseScene(scene,true);} + return ScenePath; + } + public static string Diagnose() + { + var scene=EditorSceneManager.NewPreviewScene();var rows=new List(); + try + { + foreach(string folder in new[]{Original,Trial}) + { + string actor=folder==Original?"GreatHorn":"GreatHornStudio";var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(folder+"/"+actor+"_Enemy.prefab"),scene);var anim=go.GetComponentInChildren();anim.enabled=false; + foreach(string name in new[]{"Idle","Walk","Attack"}) + { + var clip=Clip(name);float minY=float.MaxValue,maxY=float.MinValue;int minFrame=0,minVertex=0;Vector3 minimum=Vector3.zero; + for(int f=0;f<=120;f++){clip.SampleAnimation(anim.gameObject,clip.length*f/120);var points=Vertices(go);int index=Enumerable.Range(0,points.Length).OrderBy(i=>points[i].y).First();if(points[index].y();var w=skin.sharedMesh.boneWeights[minVertex]; + rows.Add(new{actor,name,minY,maxY,minFrame,minVertex,point=new[]{minimum.x,minimum.y,minimum.z},weights=new[]{new{bone=skin.bones[w.boneIndex0].name,value=w.weight0},new{bone=skin.bones[w.boneIndex1].name,value=w.weight1},new{bone=skin.bones[w.boneIndex2].name,value=w.weight2},new{bone=skin.bones[w.boneIndex3].name,value=w.weight3}}}); + } + Object.DestroyImmediate(go); + } + Write("ground_diagnostics.json",rows);return JsonConvert.SerializeObject(rows,Formatting.Indented); + } + finally{EditorSceneManager.ClosePreviewScene(scene);} + } + public static string ExportFootConstraints() + { + var scene=EditorSceneManager.NewPreviewScene(); + try + { + var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Trial+"/GreatHornStudio_Enemy.prefab"),scene);var anim=go.GetComponentInChildren();anim.enabled=false;var skin=go.GetComponentInChildren();var mesh=skin.sharedMesh;var bind=Vertices(go);var bounds=Bounds(bind);var weights=mesh.boneWeights; + var meshVertices=mesh.vertices;var indices=Enumerable.Range(0,bind.Length).Where(i=>bind[i].y<.112f).ToArray();var sets=new List();var original=new List(); + foreach(int i in indices) + { + var w=weights[i];var pairs=new[]{(w.boneIndex0,w.weight0),(w.boneIndex1,w.weight1),(w.boneIndex2,w.weight2),(w.boneIndex3,w.weight3)};float left=pairs.Where(p=>new[]{"bone_18","bone_19","bone_20","bone_21"}.Contains(skin.bones[p.Item1].name)).Sum(p=>p.Item2);int start=left>.5f?18:14; + var bones=Enumerable.Range(start,4).Select(n=>Array.FindIndex(skin.bones,b=>b.name=="bone_"+n)).ToArray();sets.Add(bones);original.Add(bones.Select(b=>pairs.Where(p=>p.Item1==b).Sum(p=>p.Item2)).ToArray()); + } + var frames=new List(); + foreach(string name in new[]{"Idle","Walk","Attack"})for(int f=0;f<=120;f++) + { + var clip=Clip(name);clip.SampleAnimation(anim.gameObject,clip.length*f/120);var matrices=skin.bones.Select((b,i)=>b.localToWorldMatrix*mesh.bindposes[i]).ToArray(); + frames.Add(indices.Select((v,j)=>sets[j].Select(b=>matrices[b].MultiplyPoint3x4(meshVertices[v]).y).ToArray()).ToArray()); + } + Write("foot_constraints.json",new{modelScale=anim.transform.localScale.x,vertices=indices.Select((v,j)=>new{index=v,normalizedBind=new[]{(bind[v].x-bounds.center.x)/bounds.size.y,(bind[v].y-bounds.min.y)/bounds.size.y,(bind[v].z-bounds.center.z)/bounds.size.y},bones=sets[j].Select(b=>skin.bones[b].name).ToArray(),weights=original[j]}).ToArray(),frames});return "Exported "+indices.Length+" sole/foot vertices x 363 original action frames"; + } + finally{EditorSceneManager.ClosePreviewScene(scene);} + } + public static async Task Capture() + { + Check(!EditorApplication.isPlaying,"Capture in Edit Mode");var scene=EditorSceneManager.OpenScene(ScenePath,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 old=RenderTexture.active; + try + { + camera.enabled=false; + foreach(var cycle in root.GetComponentsInChildren())cycle.enabled=false; + foreach(var anim in root.GetComponentsInChildren()){anim.enabled=false;string name=anim.transform.parent.name.Split('_').Last();var clip=Clip(name);clip.SampleAnimation(anim.gameObject,clip.length*(name=="Idle"?.5f:name=="Walk"?.25f:.45f));} + 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 asset="Assets/Screenshots/greathorn_studio_comparison.png";File.WriteAllBytes(Path.Combine(Application.dataPath,"../",asset),tex.EncodeToPNG());AssetDatabase.ImportAsset(asset);return asset; + } + finally{RenderTexture.active=old;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");var go=Object.Instantiate(AssetDatabase.LoadAssetAtPath(Trial+"/GreatHornStudio_Enemy.prefab"));var anim=go.GetComponentInChildren();go.transform.position=new Vector3(300,0,0);var root=go.GetComponentsInChildren().Single(t=>t.name=="bone_0");var seen=new HashSet();float maxTilt=0; + try + { + for(int phase=0;phase<3;phase++) + { + anim.SetInteger("Motion",phase==1?1:0);if(phase==2)anim.SetTrigger("Attack");var until=DateTime.UtcNow.AddSeconds(2); + while(DateTime.UtcNow"Assets/Bundles/Characters/"+Kind+"/"+Actor+"Studio"; + public string Name=>Actor+"Studio"; + public string Animated=>Trial+"/"+Name+"_"+(Kind=="Enemy"?"Enemy":"Companion")+".prefab"; + public string Scene=>"Assets/Bundles/Scenes/Review/Studio_"+Actor+"_Review.unity"; + public string Out=>Path.Combine(Repo,"Tools/output/studio_trial",Actor); + } + static Ctx C(string actor,string kind,string prefab,float height)=>new Ctx{Actor=actor,Kind=kind,Prefab=prefab,Height=height}; + static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../..")); + static void Check(bool value,string message){if(!value)throw new Exception(message);} + static void Write(Ctx c,string name,object data){Directory.CreateDirectory(c.Out);File.WriteAllText(Path.Combine(c.Out,name),JsonConvert.SerializeObject(data,Formatting.Indented));} + static Vector3[] Vertices(GameObject go) + { + return go.GetComponentsInChildren().SelectMany(s=>{ + var m=s.sharedMesh;var weights=m.boneWeights;var binds=m.bindposes;var matrices=s.bones.Select((b,i)=>b.localToWorldMatrix*binds[i]).ToArray(); + return m.vertices.Select((v,i)=>{var w=weights[i];return matrices[w.boneIndex0].MultiplyPoint3x4(v)*w.weight0+matrices[w.boneIndex1].MultiplyPoint3x4(v)*w.weight1+matrices[w.boneIndex2].MultiplyPoint3x4(v)*w.weight2+matrices[w.boneIndex3].MultiplyPoint3x4(v)*w.weight3;}).ToArray(); + }).ToArray(); + } + static Bounds Bounds(Vector3[] points){var b=new Bounds(points[0],Vector3.zero);foreach(var p in points)b.Encapsulate(p);return b;} + static AnimationClip Clip(Ctx c,string name)=>AssetDatabase.LoadAssetAtPath(c.Original+"/Clips/"+name+".anim"); + static Animator Reference(Ctx c)=>AssetDatabase.LoadAssetAtPath(c.Original+"/"+c.Prefab).GetComponentInChildren(); + + public static string Build(string actor,string kind,string prefab,float height)=>BuildCore(C(actor,kind,prefab,height),true); + public static string Prepare(string actor,string kind,string prefab,float height)=>BuildCore(C(actor,kind,prefab,height),false); + static string BuildCore(Ctx c,bool verify) + { + Check(!EditorApplication.isPlaying,"Author trial in Edit Mode"); + string fbx=c.Trial+"/Rigged/"+c.Name+"_Rigged.fbx"; + AssetDatabase.ImportAsset(fbx,ImportAssetOptions.ForceUpdate); + var imp=(ModelImporter)AssetImporter.GetAtPath(fbx);imp.animationType=ModelImporterAnimationType.Generic;imp.avatarSetup=ModelImporterAvatarSetup.CreateFromThisModel;imp.importAnimation=false;imp.globalScale=1;imp.useFileScale=false;imp.bakeAxisConversion=true;imp.materialImportMode=ModelImporterMaterialImportMode.None;imp.meshCompression=ModelImporterMeshCompression.Off;imp.SaveAndReimport(); + var reference=Reference(c); + var scene=EditorSceneManager.NewPreviewScene(); + try + { + var root=new GameObject(Path.GetFileNameWithoutExtension(c.Animated));SceneManager.MoveGameObjectToScene(root,scene); + var model=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(fbx),scene);model.transform.SetParent(root.transform,false);model.name="Model"; + model.transform.localPosition=reference.transform.localPosition;model.transform.localRotation=reference.transform.localRotation;model.transform.localScale=reference.transform.localScale; + var anim=model.GetComponent();if(anim==null)anim=model.AddComponent();anim.avatar=reference.avatar;anim.runtimeAnimatorController=reference.runtimeAnimatorController;anim.applyRootMotion=false;anim.cullingMode=AnimatorCullingMode.AlwaysAnimate; + var material=AssetDatabase.LoadAssetAtPath(c.Trial+"/"+c.Name+".mat");foreach(var skin in model.GetComponentsInChildren()){skin.sharedMaterial=material;skin.updateWhenOffscreen=true;} + var expected=reference.GetComponentsInChildren().Where(t=>t.name.StartsWith("bone_")).ToDictionary(t=>AnimationUtility.CalculateTransformPath(t,reference.transform)); + var actual=model.GetComponentsInChildren().Where(t=>t.name.StartsWith("bone_")).ToDictionary(t=>AnimationUtility.CalculateTransformPath(t,model.transform)); + Check(expected.Count>0&&expected.Keys.OrderBy(x=>x).SequenceEqual(actual.Keys.OrderBy(x=>x)),"Original skeleton paths changed ("+expected.Count+" vs "+actual.Count+")"); + foreach(var entry in expected){var t=actual[entry.Key];Check(Vector3.Distance(t.localPosition,entry.Value.localPosition)<.0001f&&Quaternion.Angle(t.localRotation,entry.Value.localRotation)<.01f&&Vector3.Distance(t.localScale,entry.Value.localScale)<.0001f,"Bind transform changed "+entry.Key);} + PrefabUtility.SaveAsPrefabAsset(root,c.Animated); + } + finally{EditorSceneManager.ClosePreviewScene(scene);} + AssetDatabase.SaveAssets();return verify?Verify(c):"Prepared original skeleton/controller candidate; sole constraints pending"; + } + public static string Verify(string actor,string kind,string prefab,float height)=>Verify(C(actor,kind,prefab,height)); + static string Verify(Ctx c) + { + var scene=EditorSceneManager.NewPreviewScene(); + try + { + var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(c.Animated),scene);var anim=go.GetComponentInChildren();anim.enabled=false; + var reference=Reference(c);Check(anim.runtimeAnimatorController==reference.runtimeAnimatorController&&anim.avatar==reference.avatar,"Expected exact original controller/avatar"); + int boneCount=reference.GetComponentsInChildren().Count(t=>t.name.StartsWith("bone_")); + var bind=Vertices(go);var bounds=Bounds(bind);int triangles=go.GetComponentsInChildren().Sum(s=>s.sharedMesh.triangles.Length/3);int target=(int?)Newtonsoft.Json.Linq.JObject.Parse(File.ReadAllText(Path.Combine(c.Out,"trial.json")))["targetTriangles"]??2000;Check(triangles<=target*1.3f,"Studio target tolerance exceeded");Check(Mathf.Abs(bounds.size.y-c.Height)<.003f&&Mathf.Abs(bounds.min.y)<.003f,"Bind ground/height changed "+bounds); + var staticGo=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(c.Trial+"/"+c.Name+".prefab"),scene);var staticPoints=staticGo.GetComponentsInChildren().SelectMany(m=>m.sharedMesh.vertices.Select(v=>m.transform.TransformPoint(v))).ToArray();float bindError=bind.Max(v=>staticPoints.Min(p=>Vector3.Distance(v,p)));Check(bindError<.002f,"Weight transfer changed static geometry "+bindError); + Check(go.GetComponentsInChildren().All(s=>s.sharedMesh.boneWeights.All(w=>Mathf.Abs(w.weight0+w.weight1+w.weight2+w.weight3-1)<.001f)),"Invalid weights"); + var rows=new List();var root=go.GetComponentsInChildren().Single(t=>t.name=="bone_0"); + foreach(string name in new[]{"Idle","Walk","Attack"}) + { + var clip=Clip(c,name);Check(anim.runtimeAnimatorController.animationClips.Contains(clip),"Not original clip "+name); + foreach(var binding in AnimationUtility.GetCurveBindings(clip))Check(string.IsNullOrEmpty(binding.path)||anim.transform.Find(binding.path)!=null,"Unbound original curve "+binding.path); + Vector3[] start=null,end=null;float minY=float.MaxValue,maxY=float.MinValue,movement=0,tilt=0;Vector3 rootStart=Vector3.zero;float drift=0; + for(int frame=0;frame<=120;frame++){clip.SampleAnimation(anim.gameObject,clip.length*frame/120);var points=Vertices(go);Check(points.All(p=>!float.IsNaN(p.x)&&!float.IsInfinity(p.x)),"Invalid mesh");var b=Bounds(points);if(frame==0){start=points;rootStart=root.position;}if(frame==120)end=points;minY=Mathf.Min(minY,b.min.y);maxY=Mathf.Max(maxY,b.min.y);movement=Mathf.Max(movement,points.Select((p,i)=>Vector3.Distance(p,start[i])).Max());tilt=Mathf.Max(tilt,Vector3.Angle(root.up,Vector3.up));drift=Mathf.Max(drift,Vector2.Distance(new Vector2(root.position.x,root.position.z),new Vector2(rootStart.x,rootStart.z)));Check(b.size.y>c.Height*.8f,"Collapsed mesh "+name);} + float seam=start.Select((p,i)=>Vector3.Distance(p,end[i])).Max();Check(minY>=-.003f&&maxY<=.003f,"Ground contact failed "+name+" "+minY+".."+maxY);Check(tilt<25&&seam<.001f,"Orientation/seam failed "+name);Check(movement>.0002f,"Motion invisible "+name); + rows.Add(new{name,originalClip=AssetDatabase.GetAssetPath(clip),frames=121,minY,maxY,movement,tilt,drift,seam}); + } + Write(c,"unity_verification.json",new{status="pass",triangles,bones=boneCount,bindError,bindHeight=bounds.size.y,originalAvatar=true,originalController=true,originalClips=true,rows}); + return "PASS original "+boneCount+"-bone bind, exact original avatar/controller/3 clips, "+triangles+" tris, bindError="+bindError.ToString("F5")+", 121-frame motion samples"; + } + finally{EditorSceneManager.ClosePreviewScene(scene);} + } + /// Sole/foot vertices with their own four bone influences, sampled over the 363 original action frames. + public static string ExportFootConstraints(string actor,string kind,string prefab,float height) + { + var c=C(actor,kind,prefab,height);var scene=EditorSceneManager.NewPreviewScene(); + try + { + var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(c.Animated),scene);var anim=go.GetComponentInChildren();anim.enabled=false;var skin=go.GetComponentInChildren();var mesh=skin.sharedMesh;var bind=Vertices(go);var bounds=Bounds(bind);var weights=mesh.boneWeights; + var meshVertices=mesh.vertices;var indices=Enumerable.Range(0,bind.Length).Where(i=>bind[i].y{var w=weights[i];return new[]{w.boneIndex0,w.boneIndex1,w.boneIndex2,w.boneIndex3};}).ToArray(); + var own=indices.Select(i=>{var w=weights[i];return new[]{w.weight0,w.weight1,w.weight2,w.weight3};}).ToArray(); + var frames=new List(); + foreach(string name in new[]{"Idle","Walk","Attack"})for(int f=0;f<=120;f++) + { + var clip=Clip(c,name);clip.SampleAnimation(anim.gameObject,clip.length*f/120);var matrices=skin.bones.Select((b,i)=>b.localToWorldMatrix*mesh.bindposes[i]).ToArray(); + frames.Add(indices.Select((v,j)=>sets[j].Select(b=>matrices[b].MultiplyPoint3x4(meshVertices[v]).y).ToArray()).ToArray()); + } + Write(c,"foot_constraints.json",new{modelScale=anim.transform.localScale.x,vertices=indices.Select((v,j)=>new{index=v,normalizedBind=new[]{(bind[v].x-bounds.center.x)/bounds.size.y,(bind[v].y-bounds.min.y)/bounds.size.y,(bind[v].z-bounds.center.z)/bounds.size.y},bones=sets[j].Select(b=>skin.bones[b].name).ToArray(),weights=own[j]}).ToArray(),frames}); + return "Exported "+indices.Length+" sole/foot vertices x 363 original action frames"; + } + finally{EditorSceneManager.ClosePreviewScene(scene);} + } + static void Label(Transform parent,string text,Vector3 position,float size) + {var label=new GameObject("Label_"+text).AddComponent();label.transform.SetParent(parent,false);label.transform.localPosition=position;label.transform.localRotation=Quaternion.Euler(12,0,0);label.text=text;label.fontSize=60;label.characterSize=size;label.anchor=TextAnchor.MiddleCenter;label.color=new Color(.87f,.93f,1);} + /// Two-row comparison (original above, Studio below): Static / Idle / Walk / Attack. + public static string BuildReview(string actor,string kind,string prefab,float height) + { + var c=C(actor,kind,prefab,height); + Check(!EditorApplication.isPlaying,"Review authoring requires Edit Mode");var previous=SceneManager.GetActiveScene();var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive); + // GreatHorn used 1.8m columns at 0.8m height; keep that ratio so the ortho camera frames all four. + float spacing=c.Height*2.25f; + try + { + SceneManager.SetActiveScene(scene);var root=new GameObject(c.Name+"Comparison");root.transform.position=new Vector3(320,0,0); + var names=new[]{"Static","Idle","Walk","Attack"}; + for(int row=0;row<2;row++) + { + string folder=row==0?c.Original:c.Trial,actor0=row==0?c.Actor:c.Name; + for(int col=0;col<4;col++) + { + string path=col==0?folder+"/"+actor0+".prefab":(row==0?c.Original+"/"+c.Prefab:c.Animated); + var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(path),scene);go.name=(row==0?"Original_":"Studio_")+names[col];go.transform.SetParent(root.transform,false);go.transform.localPosition=new Vector3((col-1.5f)*spacing,0,row==0?spacing*.75f:-spacing*.75f); + var anim=go.GetComponentInChildren();if(anim!=null){anim.enabled=false;Clip(c,names[col]).SampleAnimation(anim.gameObject,Clip(c,names[col]).length*(col==1?.5f:col==2?.25f:.45f));} + Label(go.transform,(row==0?"Original":"Studio")+" / "+names[col],new Vector3(0,-.22f*c.Height/.8f,-.1f),.025f*c.Height/.8f); + } + } + var floor=GameObject.CreatePrimitive(PrimitiveType.Plane);floor.name="Ground_Y0";floor.transform.SetParent(root.transform,false);floor.transform.localPosition=new Vector3(0,-.002f,0);floor.transform.localScale=new Vector3(spacing*.5f,1,spacing*.32f);floor.GetComponent().sharedMaterial=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Review/EnemyReviewFloor.mat");Object.DestroyImmediate(floor.GetComponent()); + var camera=new GameObject(c.Name+"Camera").AddComponent();camera.transform.SetParent(root.transform,false);camera.transform.localPosition=new Vector3(0,7*c.Height/.8f,-10*c.Height/.8f);camera.transform.LookAt(root.transform.position+new Vector3(0,c.Height*.56f,0));camera.orthographic=true;camera.orthographicSize=2.05f*c.Height/.8f;camera.aspect=2;camera.nearClipPlane=.1f;camera.farClipPlane=100;camera.cullingMask=1<<27;camera.backgroundColor=new Color(.075f,.105f,.14f);camera.clearFlags=CameraClearFlags.SolidColor;camera.depth=85;camera.enabled=false; + var light=new GameObject("StudioReviewLight").AddComponent();light.transform.SetParent(root.transform,false);light.type=LightType.Directional;light.transform.rotation=Quaternion.Euler(40,-35,0);light.intensity=1.1f;light.cullingMask=1<<27; + foreach(var t in root.GetComponentsInChildren(true))t.gameObject.layer=27; + EditorSceneManager.SaveScene(scene,c.Scene); + } + finally{SceneManager.SetActiveScene(previous);EditorSceneManager.CloseScene(scene,true);} + return c.Scene; + } + public static async Task Capture(string actor,string kind,string prefab,float height) + { + var c=C(actor,kind,prefab,height); + Check(!EditorApplication.isPlaying,"Capture in Edit Mode");var scene=EditorSceneManager.OpenScene(c.Scene,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 old=RenderTexture.active; + try + { + foreach(var anim in root.GetComponentsInChildren()){anim.enabled=false;string name=anim.transform.parent.name.Split('_').Last();var clip=Clip(c,name);clip.SampleAnimation(anim.gameObject,clip.length*(name=="Idle"?.5f:name=="Walk"?.25f:.45f));} + 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 asset="Assets/Screenshots/"+c.Actor.ToLowerInvariant()+"_studio_comparison.png";File.WriteAllBytes(Path.Combine(Application.dataPath,"../",asset),tex.EncodeToPNG());AssetDatabase.ImportAsset(asset);return asset; + } + finally{RenderTexture.active=old;camera.targetTexture=null;rt.Release();Object.DestroyImmediate(rt);Object.DestroyImmediate(tex);EditorSceneManager.CloseScene(scene,true);} + } + public static async Task Play(string actor,string kind,string prefab,float height) + { + var c=C(actor,kind,prefab,height); + Check(EditorApplication.isPlaying,"Enter Play Mode first");var go=Object.Instantiate(AssetDatabase.LoadAssetAtPath(c.Animated));var anim=go.GetComponentInChildren();go.transform.position=new Vector3(320,0,0);var root=go.GetComponentsInChildren().Single(t=>t.name=="bone_0");var seen=new HashSet();float maxTilt=0; + try + { + for(int phase=0;phase<3;phase++) + { + anim.SetInteger("Motion",phase==1?1:0);if(phase==2)anim.SetTrigger("Attack");var until=DateTime.UtcNow.AddSeconds(2); + while(DateTime.UtcNowPath.GetFullPath(Path.Combine(Application.dataPath,"../..")); + static PrototypeFlow Flow=>UnityEngine.Object.FindObjectOfType(); + static T Get(string name)=>(T)typeof(PrototypeFlow).GetField(name,BindingFlags.Instance|BindingFlags.NonPublic).GetValue(Flow); + static void Set(string name,object value)=>typeof(PrototypeFlow).GetField(name,BindingFlags.Instance|BindingFlags.NonPublic).SetValue(Flow,value); + static object Call(string name)=>typeof(PrototypeFlow).GetMethod(name,BindingFlags.Instance|BindingFlags.NonPublic).Invoke(Flow,null); + static void Check(bool v,string message){if(!v)throw new Exception(message);} + static async Task Until(Func condition,int seconds=35){var end=DateTime.UtcNow.AddSeconds(seconds);while(!condition()){Check(DateTime.UtcNow().Single(b=>b.name=="Btn_"+name&&b.gameObject.activeInHierarchy);Check(button.interactable,"Disabled "+name);Canvas.ForceUpdateCanvases();var rect=button.GetComponent(); + var pointer=new PointerEventData(EventSystem.current){position=RectTransformUtility.WorldToScreenPoint(null,rect.TransformPoint(rect.rect.center)),button=PointerEventData.InputButton.Left};var hits=new List();EventSystem.current.RaycastAll(pointer,hits); + Check(hits.Count>0&&ExecuteEvents.GetEventHandler(hits[0].gameObject)==button.gameObject,"Blocked "+name);ExecuteEvents.Execute(button.gameObject,pointer,ExecuteEvents.pointerClickHandler); + } + static async Task ClickReady(string name){for(int n=0;;n++){try{Click(name);return;}catch(Exception)when(n<40){await Task.Delay(50);}}} + static async Task Capture() + { + var type=AppDomain.CurrentDomain.GetAssemblies().Select(a=>a.GetType("Unity.Pipeline.Editor.Commands.Capture.CaptureCommands")).First(t=>t!=null); + type.GetMethod("CaptureGameView",BindingFlags.Public|BindingFlags.Static).Invoke(null,new object[]{1080,1920,null,"Screenshots/battle_skills_network.png",false,0,"screen"});await Task.Delay(150); + } + public static string Assets() + { + var arena=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab");var player=arena.GetComponentInChildren(true);Check(player!=null&&player.EffectKinds==15&&player.ProfileCount==10,"Missing formal skill bank");var so=new SerializedObject(player);int count=0; + for(int i=0;i<15;i++){var list=so.FindProperty("_effects").GetArrayElementAtIndex(i).FindPropertyRelative("instances");count+=list.arraySize;for(int n=0;n(); + foreach(string file in files) + { + var data=JObject.Parse(File.ReadAllText(file));var manifest=JsonUtility.FromJson(data["manifest"].ToString());var events=data["events"].ToObject();Check(CombatTraceValidation.Events(manifest,events),"Invalid exported trace"); + var snapshot=JsonUtility.FromJson(data["save"].ToString());var config=JsonUtility.FromJson(data["config"].ToString());var stage=JsonUtility.FromJson(data["stage"].ToString());var game=new PrototypeGame(PrototypeSave.BattleSnapshot(snapshot.level,snapshot.stages,snapshot.formation));var battle=new PrototypeBattle("domain-authoritative-"+stage.id,game,config,stage.id); + battle.LoadTrace(manifest,events);for(int i=0;i<2401&&!battle.Finished;i++){battle.Tick();while(battle.TryDequeueCue(out var _)){}while(battle.TryDequeueSkillCue(out var _)){};} + Check(battle.TraceFinalStateMatches&&battle.TraceEventsConsumed==events.Length,"Final read model mismatch "+file); + foreach(var stat in data["manifest"]["stats"]??new JArray()){var h=battle.Heroes.Single(x=>x.Pet==(int)stat["pet"]);Check(Mathf.Abs(h.DamageDealt-(float)stat["damage"])<.01f&&Mathf.Abs(h.Healing-(float)stat["healing"])<.01f&&Mathf.Abs(h.Absorbed-(float)stat["absorbed"])<.01f&&h.SkillCasts==(int)stat["skills"],"Read model stats mismatch");} + var changed=events[0].seq;events[0].seq=999;Check(!CombatTraceValidation.Events(manifest,events),"Corrupt sequence accepted");events[0].seq=changed; + rows.Add(new{stage=stage.id,events=events.Length,won=battle.Won}); + } + string folder=Path.Combine(Repo,"Doc/Design/Combat/Runtime");Directory.CreateDirectory(folder);File.WriteAllText(Path.Combine(folder,"unity_domain.json"),JsonConvert.SerializeObject(new{status="pass",cases=files.Length,all20Stages=true,rows,corruptTraceRejected=true},Formatting.Indented));return "PASS 23 authoritative C++ traces -> C# read model: all 20 stages, three T2 capstones, exact final state/stats, corrupt sequences rejected"; + } + public static Task Visual()=>CaptureVisual(false); + public static Task VisualSkills()=>CaptureVisual(true); + static async Task CaptureVisual(bool signature) + { + Check(EditorApplication.isPlaying,"Use Play Mode");var data=JObject.Parse(File.ReadAllText(Path.Combine(Repo,"Tools/output/combat_runtime/traces/stage_20_t2_f0.json")));var snapshot=JsonUtility.FromJson(data["save"].ToString());var config=JsonUtility.FromJson(data["config"].ToString());var game=new PrototypeGame(PrototypeSave.BattleSnapshot(snapshot.level,snapshot.stages,snapshot.formation));var battle=new PrototypeBattle("isolated-trace-visual",game,config,20); + battle.LoadTrace(JsonUtility.FromJson(data["manifest"].ToString()),data["events"].ToObject()); + var arena=UnityEngine.Object.Instantiate(AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab"));var ui=UnityEngine.Object.Instantiate(AssetDatabase.LoadAssetAtPath("Assets/Bundles/UI/Prefab/UI_PrototypeBattle.prefab"));ui.name="UI_PrototypeBattle";ui.SetActive(false); + var host=new GameObject("IsolatedAuthoritativeVisual");host.SetActive(false);var flow=host.AddComponent();Action set=(n,v)=>typeof(PrototypeFlow).GetField(n,BindingFlags.Instance|BindingFlags.NonPublic).SetValue(flow,v); + try + { + set("_profile",new Func(()=>-1));set("_boundProfile",-1L);set("_game",game);set("_config",config);set("_battle",battle);set("_arena",arena);set("_settled",true);set("_speed",2);set("_page",Activator.CreateInstance(typeof(PrototypeFlow).GetNestedType("Panel",BindingFlags.NonPublic),new object[]{ui})); + var heroes=Enumerable.Range(0,10).Select(i=>arena.transform.Find("Heroes/Hero"+i)).ToArray();var enemies=Enumerable.Range(0,9).Select(i=>arena.transform.Find("Enemies/Enemy"+i)).ToArray();var types=enemies.Select(s=>Enumerable.Range(0,5).Select(t=>s.Find("Type"+t)).ToArray()).ToArray();var t1=heroes.Select(h=>h.Find("Visual")).ToArray();var t2=heroes.Select(h=>h.Find("VisualT2")).ToArray(); + set("_heroViews",heroes);set("_enemyViews",enemies);set("_enemyTypes",types);set("_heroT1Visuals",t1);set("_heroT2Visuals",t2);set("_heroAnimators",t1.Select(v=>v.GetComponentInChildren(true)).ToArray());set("_heroT2Animators",t2.Select(v=>v==null?null:v.GetComponentInChildren(true)).ToArray());set("_heroPulse",new float[10]);set("_heroSkillPulse",new float[10]);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,10).Select(i=>arena.transform.Find((name=="_projectiles"?"Projectiles/Shot":name=="_skillViews"?"SkillEffects/Skill":"SkillEffects/Shield")+i)).ToArray()); + var fx=arena.GetComponentInChildren();set("_skillFeedback",fx);set("_enemyFeedback",arena.GetComponentInChildren()); + Check(heroes.Concat(enemies).All(t=>t.Find("SkillMuzzle")!=null),"Missing authored muzzle");host.SetActive(true); + var end=DateTime.UtcNow.AddSeconds(18);Func ready=()=>signature?fx.GetComponentsInChildren().Any(l=>l.enabled):fx.ActiveShots>0;while(!ready()&&DateTime.UtcNow0,"No authoritative visual cue");set("_paused",true);await Task.Delay(60); + foreach(var ps in fx.GetComponentsInChildren().Where(ps=>ps.transform.parent.GetComponent()==null&&(ps.isPlaying||ps.isPaused))){ps.Simulate(.03f,true,false,true);ps.Pause(true);} + var point=(Vector3)typeof(PrototypeFlow).GetMethod("CombatPoint",BindingFlags.Instance|BindingFlags.NonPublic).Invoke(flow,new object[]{1});var muzzle=heroes[1].Find("SkillMuzzle");Check(Vector3.Distance(point,muzzle.position)<.001f,"T2 point must use authored scaled muzzle"); + var camera=arena.GetComponentInChildren();var rt=new RenderTexture(900,1200,24);var texture=new Texture2D(900,1200,TextureFormat.RGB24,false);var previous=RenderTexture.active; + try{camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;texture.ReadPixels(new Rect(0,0,900,1200),0,0);texture.Apply();File.WriteAllBytes(Path.Combine(Repo,"AIClient/Assets/Screenshots/"+(signature?"partner_skills_battle":"battle_skills_anchors")+".png"),texture.EncodeToPNG());} + finally{camera.targetTexture=null;RenderTexture.active=previous;rt.Release();UnityEngine.Object.Destroy(rt);UnityEngine.Object.Destroy(texture);} + File.WriteAllText(Path.Combine(Repo,signature?"Doc/Design/Combat/PartnerSkills/battle_visual.json":"Doc/Design/Combat/Runtime/unity_anchors.json"),JsonConvert.SerializeObject(new{status="pass",authoritative=true,persistentMuzzles=19,t2ScaleApplied=true,flight=fx.ActiveShots>0,signature,effects=fx.PlayedEffects,networkAccountWrites=false},Formatting.Indented));return "PASS final T2 authored muzzles and authoritative flight; no account writes"; + } + finally{UnityEngine.Object.Destroy(host);UnityEngine.Object.Destroy(ui);UnityEngine.Object.Destroy(arena);} + } + public static async Task Begin(string username) + { + Check(EditorApplication.isPlaying,"Use Play Mode");await Until(()=>Flow?.Game!=null&&!Flow.IsBusy); + SessionState.SetString("AIGame.Runtime.original",JsonUtility.ToJson(Flow.Game.State));SessionState.SetString("AIGame.Runtime.endpoint",Get("_httpBase"));SessionState.SetBool("AIGame.Runtime.sfx",PrototypeClientPrefs.Sfx); + AuthResponse auth=null;bool done=false;string error=null;Flow.StartCoroutine(AuthApi.Login(Endpoint,username,"isolated-formal-heroes-password",(r,e)=>{auth=r;error=e;done=true;}));await Until(()=>done);Check(auth!=null,"Isolated login "+error); + var session=new SessionData{Pid=auth.pid,Token=auth.token,Username=username};UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent().Hide(); + Set("_session",new Func(()=>session));Set("_profile",new Func(()=>session.Pid));Set("_httpBase",Endpoint);Set("_boundProfile",0L);Flow.Handle("Home");await Until(()=>Flow.Game!=null&&!Flow.IsBusy&&Get("_boundProfile")==session.Pid); + await ClickReady("Adventure");await Until(()=>Flow.Page=="UI_PrototypeStages");await ClickReady("Stage5");await Until(()=>Flow.Page=="UI_PrototypeFormation");await ClickReady("StartBattle");await Until(()=>Flow.Page=="UI_PrototypeBattle"&&Flow.Battle!=null); + Check(Flow.Battle.Authoritative,"Client fell back to local simulation");var remote=Get("_reply").battle;Check(remote.snapshot.rulesVersion==3,"Missing rule version");SessionState.SetString("AIGame.Runtime.battle",remote.id);SessionState.SetString("AIGame.Runtime.trace",remote.trace.traceHash); + var fx=Get("_skillFeedback");Check(fx!=null&&fx.EffectKinds==15&&fx.ProfileCount==10,"Missing authored feedback");PrototypeClientPrefs.Sfx=true; + await Until(()=>fx.ActiveShots>0);await ClickReady("Pause");float clock=Flow.Battle.Time;await Task.Delay(350);Check(clock==Flow.Battle.Time,"Pause did not freeze data"); + PrototypeClientPrefs.Sfx=false;await Task.Delay(50);Check(fx.GetComponentsInChildren().All(a=>!a.isPlaying),"Mute not applied");PrototypeClientPrefs.Sfx=true; + await Capture();return "PASS real HTTP begin + full paged hash validation + T2 stage 5, flight/pause/mute; paused for isolated server restart"; + } + public static async Task ResumeAndFinish() + { + Check(EditorApplication.isPlaying&&Get("_httpBase")==Endpoint,"Expected isolated battle"); + Call("Home");Flow.StartCoroutine((IEnumerator)Call("Synchronize"));await Until(()=>Flow.Battle!=null&&Flow.Page=="UI_PrototypeBattle"&&!Flow.IsBusy); + var remote=Get("_reply").battle;Check(remote.id==SessionState.GetString("AIGame.Runtime.battle","")&&remote.trace.traceHash==SessionState.GetString("AIGame.Runtime.trace",""),"Restart regenerated pending battle"); + Check(Flow.Battle.Authoritative,"Resume lost trace mode");await ClickReady("Speed");Check(Get("_speed")==2,"2x toggle failed"); + var fx=Get("_skillFeedback");bool shot=false,shield=false,attack=false;var until=DateTime.UtcNow.AddSeconds(55); + while(!Flow.Battle.Finished&&DateTime.UtcNow0;shield|=fx.ActiveShields>0;attack|=Flow.Battle.Enemies.Any(e=>e.Strikes>0); + await Task.Delay(25); + } + Check(Flow.Battle.Finished&&Flow.Battle.TraceFinalStateMatches,"Client final state differs from authoritative trace");Check(shot&&shield&&attack,"Incomplete visible fight"); + await Until(()=>!Flow.IsBusy&&string.IsNullOrEmpty(Flow.Game.State.pendingBattle));var receipt=Get("_reply").result;Check(receipt!=null&&receipt.won==Flow.Battle.Won,"Server result mismatch"); + foreach(var stat in receipt.stats){var hero=Flow.Battle.Heroes.Single(h=>h.Pet==stat.pet);Check(Mathf.Abs(hero.DamageDealt-stat.damage)<.01f&&Mathf.Abs(hero.Healing-stat.healing)<.01f&&Mathf.Abs(hero.Absorbed-stat.absorbed)<.01f&&hero.SkillCasts==stat.skills,"Stats mismatch for "+stat.pet);} + Check(fx.ActiveShots==0&&fx.ActiveCharges==0&&fx.ActiveShields==0,"End leaked presentation leases"); + string folder=Path.Combine(Repo,"Doc/Design/Combat/Runtime");Directory.CreateDirectory(folder);File.WriteAllText(Path.Combine(folder,"unity_network.json"),JsonConvert.SerializeObject(new{status="pass",endpoint=Endpoint,stage=5,tier=2,authoritative=true,hashVerified=true,serverRestartRecovery=true,pause=true,mute=true,speed2=true,flight=shot,shield,enemyAttack=attack,finalStateExact=true,statsMatch=true,events=Flow.Battle.TraceEventsConsumed,won=receipt.won,sounds=fx.PlayedSounds,cleanup=true,realAccountWrites=false},Formatting.Indented)); + return "PASS real paged HTTP battle after server restart: T2 stage 5, 2x, exact state/stats and server reward; screenshot battle_skills_network.png"; + } + public static async Task Cleanup() + { + PrototypeClientPrefs.Sfx=SessionState.GetBool("AIGame.Runtime.sfx",true);var net=UnityEngine.Object.FindObjectOfType();UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent().Hide(); + Set("_session",new Func(()=>net.Session));Set("_profile",new Func(()=>net.Session?.Pid??0));Set("_httpBase",SessionState.GetString("AIGame.Runtime.endpoint","http://127.0.0.1:8080"));Set("_boundProfile",0L);Flow.Handle("Home");await Until(()=>Flow.Game!=null&&!Flow.IsBusy&&Get("_httpBase")!=Endpoint); + var original=JsonUtility.FromJson(SessionState.GetString("AIGame.Runtime.original",""));var actual=Flow.Game.State;Check(actual.coins==original.coins&&actual.candy==original.candy&&actual.level==original.level&&actual.shots==original.shots&&actual.stages.SequenceEqual(original.stages)&&actual.formation.SequenceEqual(original.formation),"Real account changed"); + return "Restored real account and preferences; wallet, level, roster, formation unchanged"; + } +} diff --git a/AIClient/AgentScripts/VerifyEnemyCombatFx.cs b/AIClient/AgentScripts/VerifyEnemyCombatFx.cs index fc31e92..b014ad0 100644 --- a/AIClient/AgentScripts/VerifyEnemyCombatFx.cs +++ b/AIClient/AgentScripts/VerifyEnemyCombatFx.cs @@ -114,12 +114,12 @@ public static class VerifyEnemyCombatFx 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() + public static async Task PlayBattle(bool studio = false) { 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;} + foreach(var enemy in config.enemies){enemy.hp=studio?240: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); @@ -133,19 +133,30 @@ public static class VerifyEnemyCombatFx 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(); + if(studio)for(int s=0;s(true).All(r=>AssetDatabase.GetAssetPath(r.sharedMesh).Contains("/"+Actors[t]+"Studio/")),"Old enemy mesh in slot "+s+" type "+t); 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]); + var t1=heroes.Select(h=>h.Find("Visual")).ToArray();var t2=heroes.Select(h=>h.Find("VisualT2")).ToArray(); + set("_heroT1Visuals",t1);set("_heroT2Visuals",t2); + set("_heroAnimators",t1.Select(h=>h==null?null:h.GetComponentInChildren(true)).ToArray()); + set("_heroT2Animators",t2.Select(h=>h==null?null: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); + var walked=new HashSet();var attacked=new HashSet(); + host.SetActive(true);int last=0;bool captured=false;var until=DateTime.UtcNow.AddSeconds(10); while(!battle.Finished&&DateTime.UtcNowe.Alive)) + { + var anim=types[e.Index][e.Type].GetComponentInChildren();var state=anim.GetCurrentAnimatorStateInfo(0); + if(state.IsName("Walk"))walked.Add(e.Type);if(state.IsName("Attack"))attacked.Add(e.Type); + } if(!captured&&battle.Enemies.Any(e=>e.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());} + 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/"+(studio?"enemy_studio_arena":"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); } @@ -153,7 +164,8 @@ public static class VerifyEnemyCombatFx } 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}); + if(studio)Check(walked.Count==5&&attacked.Count==5,"Missing Studio Walk/Attack: "+string.Join(",",walked)+" / "+string.Join(",",attacked)); + Report(studio?"studio_arena":"battle",new{status="pass",realFlow=true,isolatedInMemory=true,networkRewardRequests=false,allFiveTypes=true,studio,walked=walked.OrderBy(t=>t),attacked=attacked.OrderBy(t=>t),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);} diff --git a/AIClient/AgentScripts/VerifyEnemyStudioBatch.cs b/AIClient/AgentScripts/VerifyEnemyStudioBatch.cs new file mode 100644 index 0000000..f48e220 --- /dev/null +++ b/AIClient/AgentScripts/VerifyEnemyStudioBatch.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object=UnityEngine.Object; + +/// One Play session exercises all four original controllers on their new Studio meshes. +public static class VerifyEnemyStudioBatch +{ + static readonly string[] Actors={"Hornlet","Dashlet","Shellhorn","HornCaptain"}; + static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../..")); + static void Check(bool v,string message){if(!v)throw new Exception(message);} + public static async Task PlayAll() + { + Check(EditorApplication.isPlaying,"Use Play Mode"); + var objects=new List();var animators=new List();var bones=new List();var seen=Actors.Select(a=>new HashSet()).ToArray();var tilt=new float[4]; + try + { + for(int i=0;i("Assets/Bundles/Characters/Enemy/"+actor+"Studio/"+actor+"Studio_Enemy.prefab");Check(prefab!=null,"Missing "+actor); + var go=Object.Instantiate(prefab);objects.Add(go);go.transform.position=new Vector3(400+i*3,0,0);var anim=go.GetComponentInChildren();animators.Add(anim);bones.Add(go.GetComponentsInChildren().Single(t=>t.name=="bone_0")); + var original=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Characters/Enemy/"+actor+"/"+actor+"_Enemy.prefab").GetComponentInChildren();Check(anim.avatar==original.avatar&&anim.runtimeAnimatorController==original.runtimeAnimatorController,"Original avatar/controller changed "+actor); + } + for(int phase=0;phase<3;phase++) + { + foreach(var anim in animators){anim.SetInteger("Motion",phase==1?1:0);if(phase==2)anim.SetTrigger("Attack");} + var until=DateTime.UtcNow.AddSeconds(2); + while(DateTime.UtcNow Overview() + { + Check(!EditorApplication.isPlaying,"Build overview in Edit Mode"); + var actors=Actors.Concat(new[]{"GreatHorn"}).ToArray();var heights=new[]{.5f,.5f,.5f,.6f,.8f}; + var previous=SceneManager.GetActiveScene();var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive);var old=RenderTexture.active;var rt=new RenderTexture(1800,900,24);var texture=new Texture2D(1800,900,TextureFormat.RGB24,false); + try + { + SceneManager.SetActiveScene(scene);var root=new GameObject("FiveEnemyStudioOverview");root.transform.position=new Vector3(440,0,0); + for(int row=0;row<2;row++)for(int i=0;i("Assets/Bundles/Characters/Enemy/"+name+"/"+name+"_Enemy.prefab");Check(prefab!=null,"Missing "+name); + var go=(GameObject)PrefabUtility.InstantiatePrefab(prefab,scene);go.transform.SetParent(root.transform,false);go.transform.localPosition=new Vector3((i-2)*1.8f,0,row==0?1.35f:-1.35f);go.transform.localScale=Vector3.one*(.8f/heights[i]); + var animator=go.GetComponentInChildren();animator.enabled=false;var clip=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Characters/Enemy/"+actors[i]+"/Clips/Idle.anim");clip.SampleAnimation(animator.gameObject,clip.length*.5f); + var label=new GameObject("Label_"+name).AddComponent();label.transform.SetParent(root.transform,false);label.transform.localPosition=go.transform.localPosition+new Vector3(0,-.22f,-.1f);label.transform.localRotation=Quaternion.Euler(12,0,0);label.text=(row==0?"Original":"Studio")+" / "+actors[i];label.fontSize=50;label.characterSize=.022f;label.anchor=TextAnchor.MiddleCenter;label.color=new Color(.95f,.98f,1); + } + var floor=GameObject.CreatePrimitive(PrimitiveType.Plane);floor.name="Ground_Y0";floor.transform.SetParent(root.transform,false);floor.transform.localPosition=new Vector3(0,-.002f,0);floor.transform.localScale=new Vector3(1.05f,1,.57f);floor.GetComponent().sharedMaterial=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Review/EnemyReviewFloor.mat");Object.DestroyImmediate(floor.GetComponent()); + var camera=new GameObject("EnemyStudioOverviewCamera").AddComponent();camera.transform.SetParent(root.transform,false);camera.transform.localPosition=new Vector3(0,7,-10);camera.transform.LookAt(root.transform.position+Vector3.up*.45f);camera.orthographic=true;camera.orthographicSize=2.3f;camera.aspect=2;camera.nearClipPlane=.1f;camera.farClipPlane=100;camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=new Color(.075f,.105f,.14f);camera.cullingMask=1<<26;camera.enabled=false; + var light=new GameObject("OverviewLight").AddComponent();light.transform.SetParent(root.transform,false);light.type=LightType.Directional;light.intensity=1.1f;light.transform.localRotation=Quaternion.Euler(40,-35,0);light.cullingMask=1<<26; + foreach(var t in root.GetComponentsInChildren(true))t.gameObject.layer=26; + EditorSceneManager.SaveScene(scene,"Assets/Bundles/Scenes/Review/Studio_Enemies_Overview.unity");await Task.Delay(100); + camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;texture.ReadPixels(new Rect(0,0,1800,900),0,0);texture.Apply();camera.targetTexture=null; + const string path="Assets/Screenshots/enemy_studio_batch_overview.png";File.WriteAllBytes(Path.Combine(Application.dataPath,"../",path),texture.EncodeToPNG());AssetDatabase.ImportAsset(path);return path; + } + finally{RenderTexture.active=old;rt.Release();Object.DestroyImmediate(rt);Object.DestroyImmediate(texture);SceneManager.SetActiveScene(previous);EditorSceneManager.CloseScene(scene,true);} + } +} diff --git a/AIClient/AgentScripts/VerifyFirstFive.cs b/AIClient/AgentScripts/VerifyFirstFive.cs new file mode 100644 index 0000000..c83f491 --- /dev/null +++ b/AIClient/AgentScripts/VerifyFirstFive.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using HotUpdate.Gameplay; +using HotUpdate.Net; +using HotUpdate.UI; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using TMPro; +using UnityEditor; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +/// Unseeded first-day acceptance via actual EventSystem clicks and isolated HTTP. +public static class VerifyFirstFive +{ + const string Endpoint = "http://127.0.0.1:18085"; + const string Key = "AIGame.FirstFive."; + static string Repo => Path.GetFullPath(Path.Combine(Application.dataPath, "../..")); + static string Output => Path.Combine(Repo, "Tools/output/first_five"); + static PrototypeFlow Flow => UnityEngine.Object.FindObjectOfType(); + static T Get(string field) => (T)typeof(PrototypeFlow).GetField(field, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(Flow); + static void Set(string field, object value) => typeof(PrototypeFlow).GetField(field, BindingFlags.Instance | BindingFlags.NonPublic).SetValue(Flow, value); + static void Check(bool value, string message) { if (!value) throw new Exception(message); } + static async Task Until(Func condition, int seconds = 35) + { + var end = DateTime.UtcNow.AddSeconds(seconds); + while (!condition()) { Check(DateTime.UtcNow < end, "Timeout: " + Flow?.Page); await Task.Delay(75); } + } + static Button Find(string name) => UnityEngine.Object.FindObjectsOfType