- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting. - Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn): Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes. - Formal partners for all ten pets in the arena and portraits, including new VineSprite, ThunderLeopard and TideWhale. - Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands, configurable Unity run_script timeout, companion static stage. - Also includes concurrent work from another tool in this working tree (FlameLion, enemy combat FX, challenge mode, home explorer, server updates). - Resources registered in Doc/ResDesc_*.md; verified on isolated :18081. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
10 KiB
C#
145 lines
10 KiB
C#
#if UNITY_EDITOR
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using Object=UnityEngine.Object;
|
|
|
|
public static class BuildEnemyReview
|
|
{
|
|
[Serializable] public sealed class Recipe {public string id,assetRoot,staticPrefab,enemyPrefab,visualPrefab,reviewScene,usage;public float attackImpact,height,reviewOriginX;}
|
|
static Recipe Read(string path)
|
|
{
|
|
var r=JsonUtility.FromJson<Recipe>(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,"../..",path))));
|
|
if(string.IsNullOrEmpty(r.visualPrefab))r.visualPrefab=r.enemyPrefab;
|
|
if(r.reviewOriginX==0)r.reviewOriginX=20;
|
|
return r;
|
|
}
|
|
static bool Companion(Recipe r)=>r.usage=="companion_visual";
|
|
static string Prefix(Recipe r)=>Companion(r) ? "CompanionReview_"+r.id : "EnemyReview";
|
|
static void Folder(string path)
|
|
{
|
|
string current="Assets";
|
|
foreach(string part in path.Split('/').Skip(1)) {string next=current+"/"+part;if(!AssetDatabase.IsValidFolder(next))AssetDatabase.CreateFolder(current,part);current=next;}
|
|
}
|
|
public static string Publish(string recipePath="Config/Art/EnemyCharacters/RockBall.json")
|
|
{
|
|
var r=Read(recipePath);Folder(Path.GetDirectoryName(r.visualPrefab).Replace('\\','/'));
|
|
var scene=EditorSceneManager.NewPreviewScene();
|
|
try
|
|
{
|
|
var source=AssetDatabase.LoadAssetAtPath<GameObject>(r.assetRoot+"/"+r.id+"_Rigged.prefab");
|
|
var go=(GameObject)PrefabUtility.InstantiatePrefab(source,scene);go.name=r.id+(Companion(r) ? "_Companion" : "_Enemy");
|
|
var animator=go.GetComponentInChildren<Animator>();
|
|
if(animator.runtimeAnimatorController.animationClips.Select(c=>c.name).Distinct().Count()!=3)throw new Exception("Three actions required before publishing character visual");
|
|
PrefabUtility.SaveAsPrefabAsset(go,r.visualPrefab);
|
|
}
|
|
finally {EditorSceneManager.ClosePreviewScene(scene);}
|
|
AssetDatabase.SaveAssets();return r.visualPrefab;
|
|
}
|
|
public static string Build(string recipePath="Config/Art/EnemyCharacters/RockBall.json")
|
|
{
|
|
if(EditorApplication.isPlaying)throw new Exception("Build in Edit Mode");
|
|
var r=Read(recipePath);Publish(recipePath);
|
|
var existing=SceneManager.GetSceneByPath(r.reviewScene);
|
|
if(existing.IsValid() && existing.isLoaded)
|
|
{
|
|
if(existing.isDirty)throw new Exception("Existing character review has unsaved edits; preserve before rebuilding");
|
|
EditorSceneManager.CloseScene(existing,true);
|
|
}
|
|
Folder(Path.GetDirectoryName(r.reviewScene).Replace('\\','/'));
|
|
// Additive + a separate render layer preserves any user edits in other scenes.
|
|
var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive);
|
|
SceneManager.SetActiveScene(scene);
|
|
var root=new GameObject((Companion(r) ? "CompanionProductionReview_" : "EnemyProductionReview_")+r.id);
|
|
const int layer=29;
|
|
const string floorPath="Assets/Bundles/Scenes/Review/EnemyReviewFloor.mat";
|
|
var mat=AssetDatabase.LoadAssetAtPath<Material>(floorPath);
|
|
if(mat==null){mat=new Material(Shader.Find("Universal Render Pipeline/Lit"));mat.color=new Color(.48f,.56f,.50f);AssetDatabase.CreateAsset(mat,floorPath);}
|
|
float spacing=Mathf.Max(1,r.height*1.4f);
|
|
var floor=GameObject.CreatePrimitive(PrimitiveType.Plane);floor.name="Ground_Y0";floor.transform.SetParent(root.transform,false);
|
|
floor.transform.position=new Vector3(r.reviewOriginX,0,0);floor.transform.localScale=Vector3.one*(.8f*spacing);floor.GetComponent<Renderer>().sharedMaterial=mat;
|
|
string[] names={"Static","Idle","Walk","Attack","Cycle"};
|
|
for(int i=0;i<names.Length;i++)
|
|
{
|
|
var asset=AssetDatabase.LoadAssetAtPath<GameObject>(i==0 ? r.staticPrefab : r.visualPrefab);
|
|
var go=(GameObject)PrefabUtility.InstantiatePrefab(asset,root.transform);go.name=Prefix(r)+"_"+names[i];
|
|
go.transform.position=new Vector3(r.reviewOriginX+(i-2)*spacing,0,0);
|
|
var label=new GameObject("NameLabel");label.transform.SetParent(go.transform,false);label.transform.position=go.transform.position+Vector3.up*(r.height+.21f);
|
|
label.transform.localScale=Vector3.one/go.transform.lossyScale.x;
|
|
var text=label.AddComponent<TextMesh>();text.text=names[i];text.anchor=TextAnchor.MiddleCenter;text.characterSize=.01f;text.fontSize=42;text.color=new Color(.12f,.18f,.2f);
|
|
var marker=GameObject.CreatePrimitive(PrimitiveType.Cube);marker.name="ForwardMinusZ";marker.transform.SetParent(go.transform,false);
|
|
marker.transform.position=go.transform.position+new Vector3(0,.012f,-.4f);marker.transform.localScale=new Vector3(.035f,.015f,.22f)/go.transform.lossyScale.x;
|
|
Object.DestroyImmediate(marker.GetComponent<Collider>());
|
|
if(i>0)
|
|
{
|
|
var cycle=go.AddComponent<CharacterReviewAnimCycle>();cycle.enabled=false;
|
|
var so=new SerializedObject(cycle);var modes=so.FindProperty("motionNames");modes.arraySize=3;
|
|
for(int m=0;m<3;m++)modes.GetArrayElementAtIndex(m).stringValue=new[]{"Idle","Walk","Attack"}[m];
|
|
so.FindProperty("fixedMotion").intValue=i==4 ? -1 : i-1;
|
|
so.ApplyModifiedPropertiesWithoutUndo();cycle.enabled=true;
|
|
}
|
|
}
|
|
var sun=new GameObject(Prefix(r)+"Sun").AddComponent<Light>();sun.transform.SetParent(root.transform,false);
|
|
sun.type=LightType.Directional;sun.intensity=1.0f;sun.shadows=LightShadows.Soft;sun.cullingMask=1<<layer;sun.transform.rotation=Quaternion.Euler(48,-30,0);
|
|
var camera=new GameObject(Prefix(r)+"Camera").AddComponent<Camera>();camera.transform.SetParent(root.transform,false);
|
|
camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=new Color(.66f,.76f,.81f);camera.depth=50+r.reviewOriginX-20;camera.cullingMask=1<<layer;
|
|
camera.transform.position=new Vector3(r.reviewOriginX+1.1f*spacing,r.height*2,-4.5f*spacing);camera.transform.LookAt(new Vector3(r.reviewOriginX,r.height*.7f,0));
|
|
camera.orthographic=true;camera.orthographicSize=spacing;camera.nearClipPlane=.05f;camera.farClipPlane=100;
|
|
foreach(var t in root.GetComponentsInChildren<Transform>(true))t.gameObject.layer=layer;
|
|
RenderSettings.ambientMode=UnityEngine.Rendering.AmbientMode.Trilight;
|
|
RenderSettings.ambientSkyColor=new Color(.65f,.72f,.8f);
|
|
RenderSettings.ambientEquatorColor=new Color(.45f,.5f,.48f);
|
|
RenderSettings.ambientGroundColor=new Color(.3f,.28f,.25f);
|
|
EditorSceneManager.SaveScene(scene,r.reviewScene);
|
|
var view=SceneView.lastActiveSceneView;if(view!=null){view.LookAt(new Vector3(r.reviewOriginX,.35f,0),Quaternion.Euler(12,-12,0),3.6f);view.Repaint();}
|
|
return "Built additive review "+r.reviewScene+"; other scenes preserved";
|
|
}
|
|
public static async Task<string> Capture(string recipePath="Config/Art/EnemyCharacters/RockBall.json")
|
|
{
|
|
var r=Read(recipePath);var sourceCamera=GameObject.Find(Prefix(r)+"Camera").GetComponent<Camera>();
|
|
var names=new[]{"Static","Idle","Walk","Attack","Cycle"};
|
|
var scene=EditorSceneManager.NewPreviewScene();
|
|
var rt=new RenderTexture(1900,760,24);var tex=new Texture2D(1900,760,TextureFormat.RGB24,false);var active=RenderTexture.active;
|
|
try
|
|
{
|
|
// Fresh prefab instances have no review-cycle state or cached Animator
|
|
// pose. The live review and its labels are never paused or modified.
|
|
var container=new GameObject("IsolatedReviewCapture");SceneManager.MoveGameObjectToScene(container,scene);
|
|
var camera=Object.Instantiate(sourceCamera,container.transform);camera.scene=scene;camera.enabled=false;
|
|
foreach(var light in sourceCamera.transform.parent.GetComponentsInChildren<Light>())Object.Instantiate(light,container.transform);
|
|
var floor=sourceCamera.transform.parent.Find("Ground_Y0");if(floor!=null)Object.Instantiate(floor,container.transform,false);
|
|
for(int i=0;i<names.Length;i++)
|
|
{
|
|
var source=GameObject.Find(Prefix(r)+"_"+names[i]);
|
|
var asset=AssetDatabase.LoadAssetAtPath<GameObject>(i==0 ? r.staticPrefab : r.visualPrefab);
|
|
var go=(GameObject)PrefabUtility.InstantiatePrefab(asset,scene);go.name="Capture_"+names[i];
|
|
go.transform.position=source.transform.position;go.transform.rotation=source.transform.rotation;go.transform.localScale=source.transform.localScale;
|
|
var label=Object.Instantiate(source.transform.Find("NameLabel"),go.transform,false).GetComponent<TextMesh>();
|
|
label.text=i==0 ? "Static" : i==4 ? "Auto cycle" : names[i]+(i==3 ? " (impact)" : " (loop)");
|
|
var marker=source.transform.Find("ForwardMinusZ");if(marker!=null)Object.Instantiate(marker,go.transform,false);
|
|
foreach(var t in go.GetComponentsInChildren<Transform>(true))t.gameObject.layer=source.layer;
|
|
if(i==0)continue;
|
|
var animator=go.GetComponentInChildren<Animator>();animator.enabled=false;
|
|
foreach(var skin in go.GetComponentsInChildren<SkinnedMeshRenderer>())skin.updateWhenOffscreen=true;
|
|
string name=i==1 ? "Idle" : i==2 ? "Walk" : "Attack";
|
|
var clip=AssetDatabase.LoadAssetAtPath<AnimationClip>(r.assetRoot+"/Clips/"+name+".anim");
|
|
clip.SampleAnimation(animator.gameObject,clip.length*(i==1 ? .5f : i==2 ? .25f : r.attackImpact));
|
|
}
|
|
await Task.Delay(100);
|
|
camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,1900,760),0,0);tex.Apply();
|
|
string path="Assets/Screenshots/"+(Companion(r) ? "companion_" : "enemy_")+r.id.ToLowerInvariant()+"_review.png";
|
|
File.WriteAllBytes(Path.Combine(Application.dataPath,"../",path),tex.EncodeToPNG());AssetDatabase.Refresh();return path;
|
|
}
|
|
finally
|
|
{
|
|
RenderTexture.active=active;rt.Release();Object.DestroyImmediate(rt);Object.DestroyImmediate(tex);EditorSceneManager.ClosePreviewScene(scene);
|
|
}
|
|
}
|
|
}
|
|
#endif
|