Add formal enemies, remaining formal partners and synthesized UI SFX
- 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>
This commit is contained in:
@@ -16,6 +16,22 @@ public static class BuildPrototype
|
||||
static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName;
|
||||
public static readonly string[] Screens = { "Stages", "Pinball", "Capture", "Pets", "Formation", "Battle" };
|
||||
const int PetCount = 10;
|
||||
const string Animal = "Assets/Bundles/Characters/Animal/";
|
||||
/// <summary>Validated formal partner art by pet id; other ids keep primitive stand-ins until their art passes review.</summary>
|
||||
static readonly System.Collections.Generic.Dictionary<int, string> FormalPets = new System.Collections.Generic.Dictionary<int, string>
|
||||
{
|
||||
[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",
|
||||
};
|
||||
/// <summary>Battle height in arena units; the authored models are ~0.55m and the lanes are 3 units apart.</summary>
|
||||
public const float FormalHeight = 1.5f;
|
||||
const string Enemy = "Assets/Bundles/Characters/Enemy/";
|
||||
/// <summary>Formal enemy art by enemy type id (greenfield.json enemies 0..4), nested under every Enemies/EnemyN slot as Type{t}.</summary>
|
||||
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" };
|
||||
/// <summary>Base enemy height in arena units; the runtime still scales captains x1.2 and bosses x1.55.</summary>
|
||||
public const float EnemyHeight = 1.2f;
|
||||
static Material _cream, _rock, _orange, _leaf, _dark, _white, _cyan, _grass, _path, _monster, _yellow, _water, _ember, _violet;
|
||||
|
||||
static Material Material(string name, string color)
|
||||
@@ -47,9 +63,56 @@ public static class BuildPrototype
|
||||
Shape("Pupil", root, PrimitiveType.Sphere, new Vector3(x, y, z - .1f), Vector3.one * .1f, _dark);
|
||||
}
|
||||
}
|
||||
static void SetLayer(Transform t)
|
||||
{
|
||||
t.gameObject.layer = 30; foreach (Transform child in t) SetLayer(child);
|
||||
}
|
||||
/// <summary>Top of the bind-pose vertices in <paramref name="space"/>; skinned Renderer.bounds is padded and unreliable in Edit Mode.</summary>
|
||||
public static float MeshTop(Transform visual, Transform space)
|
||||
{
|
||||
float top = 0; var mesh = new Mesh();
|
||||
try
|
||||
{
|
||||
foreach (var skin in visual.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||||
{
|
||||
skin.BakeMesh(mesh, true);
|
||||
foreach (var v in mesh.vertices) top = Mathf.Max(top, space.InverseTransformPoint(skin.transform.TransformPoint(v)).y);
|
||||
}
|
||||
}
|
||||
finally { UnityEngine.Object.DestroyImmediate(mesh); }
|
||||
return top;
|
||||
}
|
||||
/// <summary>Nests the formal partner prefab under a "Visual" node scaled to the battle height; returns false when none exists.</summary>
|
||||
static bool FormalVisual(int id, Transform root)
|
||||
{
|
||||
if (!FormalPets.TryGetValue(id, out var path)) return false;
|
||||
var asset = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
if (asset == null) throw new Exception("Missing formal partner " + path);
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(asset, root.gameObject.scene);
|
||||
go.name = "Visual"; go.transform.SetParent(root, false);
|
||||
go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one;
|
||||
go.transform.localScale = Vector3.one * (FormalHeight / Mathf.Max(.1f, MeshTop(go.transform, root)));
|
||||
SetLayer(go.transform); return true;
|
||||
}
|
||||
/// <summary>Nests all formal enemy types under one enemy slot, inactive; the runtime shows the one matching the spawned type. False until all five exist.</summary>
|
||||
static bool FormalEnemyVisuals(Transform slot)
|
||||
{
|
||||
var assets = FormalEnemies.Select(AssetDatabase.LoadAssetAtPath<GameObject>).ToArray();
|
||||
if (assets.Any(a => a == null)) return false;
|
||||
for (int t = 0; t < assets.Length; t++)
|
||||
{
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(assets[t], slot.gameObject.scene);
|
||||
go.name = "Type" + t; go.transform.SetParent(slot, false);
|
||||
go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one;
|
||||
go.transform.localScale = Vector3.one * (EnemyHeight / Mathf.Max(.1f, MeshTop(go.transform, slot)));
|
||||
SetLayer(go.transform); go.SetActive(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static Transform Pet(int id, Transform parent, string name)
|
||||
{
|
||||
var root = Group(name, parent);
|
||||
if (FormalVisual(id, root)) return root;
|
||||
if (id == 0)
|
||||
{
|
||||
Shape("Body", root, PrimitiveType.Sphere, new Vector3(0, .7f, 0), new Vector3(1.3f, 1.3f, 1), _rock);
|
||||
@@ -215,9 +278,13 @@ public static class BuildPrototype
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var enemy = Group("Enemy" + i, enemies);
|
||||
Shape("Body", enemy, PrimitiveType.Sphere, new Vector3(0, .48f, 0), new Vector3(.85f, .95f, .85f), _monster);
|
||||
Shape("Horn", enemy, PrimitiveType.Cube, new Vector3(0, 1.02f, 0), new Vector3(.2f, .35f, .2f), _cream).localRotation = Quaternion.Euler(0, 0, 20);
|
||||
Face(enemy, .65f, -.38f, .17f); enemy.gameObject.SetActive(false);
|
||||
if (!FormalEnemyVisuals(enemy))
|
||||
{
|
||||
Shape("Body", enemy, PrimitiveType.Sphere, new Vector3(0, .48f, 0), new Vector3(.85f, .95f, .85f), _monster);
|
||||
Shape("Horn", enemy, PrimitiveType.Cube, new Vector3(0, 1.02f, 0), new Vector3(.2f, .35f, .2f), _cream).localRotation = Quaternion.Euler(0, 0, 20);
|
||||
Face(enemy, .65f, -.38f, .17f);
|
||||
}
|
||||
enemy.gameObject.SetActive(false);
|
||||
}
|
||||
var projectiles = Group("Projectiles", root.transform);
|
||||
for (int i = 0; i < PetCount; i++)
|
||||
@@ -228,6 +295,17 @@ public static class BuildPrototype
|
||||
camera.cullingMask = 1 << 30; camera.transform.position = new Vector3(0, 16, -11); camera.transform.LookAt(new Vector3(0, 0, 4));
|
||||
var light = Group("BattleLight", root.transform).gameObject.AddComponent<Light>();
|
||||
light.type = LightType.Directional; light.intensity = 1; light.cullingMask = 1 << 30; light.transform.rotation = Quaternion.Euler(45, -30, 0);
|
||||
// A full rebuild must keep the authored battlefield (same result as AttachGreenfield.Build); BuildLanes runs after this.
|
||||
var battlefield = AssetDatabase.LoadAssetAtPath<GameObject>(World + "/Battlefield/Battlefield_Greenfield.prefab");
|
||||
if (battlefield != null)
|
||||
{
|
||||
var greenfield = (GameObject)PrefabUtility.InstantiatePrefab(battlefield, scene); greenfield.name = "Greenfield";
|
||||
greenfield.transform.SetParent(root.transform, false); greenfield.transform.SetSiblingIndex(0); SetLayer(greenfield.transform);
|
||||
foreach (Transform child in root.transform)
|
||||
if (child.name == "Ground" || child.name == "Lane" || child.name == "LeftBorder" || child.name == "RightBorder" || child.name == "BattleLight") child.gameObject.SetActive(false);
|
||||
}
|
||||
var enemyFeedback = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/Effects/EnemyCombat/EnemyCombatPresentation.prefab");
|
||||
if (enemyFeedback != null) PrefabUtility.InstantiatePrefab(enemyFeedback, root.transform);
|
||||
PrefabUtility.SaveAsPrefabAsset(root, World + "/PrototypeArena.prefab");
|
||||
}
|
||||
finally { UnityEngine.Object.DestroyImmediate(root); EditorSceneManager.ClosePreviewScene(scene); }
|
||||
@@ -242,8 +320,9 @@ public static class BuildPrototype
|
||||
Pet(id, root.transform, "PortraitPet");
|
||||
var camera = Group("PortraitCamera", root.transform).gameObject.AddComponent<Camera>(); camera.scene = scene;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = Color.clear; camera.cullingMask = 1 << 30;
|
||||
camera.orthographic = true; camera.orthographicSize = id == 3 ? 1.22f : id >= 9 ? 1.34f : 1.08f; camera.allowHDR = false;
|
||||
camera.transform.position = new Vector3(1.6f, 1.55f, -4); camera.transform.LookAt(new Vector3(0, id == 3 ? .94f : id >= 5 ? .86f : .82f, 0)); camera.targetTexture = rt;
|
||||
bool primitiveBird = id == 3 && !FormalPets.ContainsKey(id);
|
||||
camera.orthographic = true; camera.orthographicSize = primitiveBird ? 1.22f : id >= 9 ? 1.34f : 1.08f; camera.allowHDR = false;
|
||||
camera.transform.position = new Vector3(1.6f, 1.55f, -4); camera.transform.LookAt(new Vector3(0, primitiveBird ? .94f : id >= 5 ? .86f : .82f, 0)); camera.targetTexture = rt;
|
||||
var light = Group("PortraitLight", root.transform).gameObject.AddComponent<Light>(); light.type = LightType.Directional;
|
||||
light.intensity = 1.15f; light.cullingMask = 1 << 30; light.transform.rotation = Quaternion.Euler(35, -35, 0);
|
||||
camera.Render(); RenderTexture.active = rt;
|
||||
@@ -280,6 +359,45 @@ public static class BuildPrototype
|
||||
package.CheckConfigError(); BundleCollectorSettingData.SaveFile();
|
||||
return "Built authored arena with ten pets, ten portraits and six JSON UI prefabs; updated Art collectors.";
|
||||
}
|
||||
/// <summary>Swaps formal partners into the existing arena in place (keeps Lanes/Greenfield edits) and re-renders their portraits.</summary>
|
||||
public static string ApplyFormalHeroes()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new Exception("Apply formal heroes in Edit Mode");
|
||||
SetupMaterials();
|
||||
var root = PrefabUtility.LoadPrefabContents(World + "/PrototypeArena.prefab");
|
||||
try
|
||||
{
|
||||
foreach (int id in FormalPets.Keys)
|
||||
{
|
||||
var hero = root.transform.Find("Heroes/Hero" + id) ?? throw new Exception("Arena missing Hero" + id);
|
||||
foreach (var child in hero.Cast<Transform>().Where(c => c.name != "T2Crest").ToArray()) UnityEngine.Object.DestroyImmediate(child.gameObject);
|
||||
FormalVisual(id, hero);
|
||||
var crest = hero.Find("T2Crest"); if (crest != null) crest.localPosition = new Vector3(0, FormalHeight + .3f, 0);
|
||||
}
|
||||
PrefabUtility.SaveAsPrefabAsset(root, World + "/PrototypeArena.prefab");
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
foreach (int id in FormalPets.Keys) Portrait(id);
|
||||
return "Arena Hero" + string.Join("/", FormalPets.Keys) + " now nest formal partner prefabs; portraits re-rendered.";
|
||||
}
|
||||
/// <summary>Swaps the enemy stand-ins in the existing arena for the five formal enemy types, in place (keeps SlowMark and other edits).</summary>
|
||||
public static string ApplyFormalEnemies()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new Exception("Apply formal enemies in Edit Mode");
|
||||
var root = PrefabUtility.LoadPrefabContents(World + "/PrototypeArena.prefab");
|
||||
try
|
||||
{
|
||||
var enemies = root.transform.Find("Enemies") ?? throw new Exception("Arena missing Enemies");
|
||||
foreach (Transform slot in enemies)
|
||||
{
|
||||
foreach (var child in slot.Cast<Transform>().Where(c => c.name != "SlowMark").ToArray()) UnityEngine.Object.DestroyImmediate(child.gameObject);
|
||||
if (!FormalEnemyVisuals(slot)) throw new Exception("Formal enemy prefabs incomplete: " + string.Join(", ", FormalEnemies.Where(p => AssetDatabase.LoadAssetAtPath<GameObject>(p) == null)));
|
||||
}
|
||||
PrefabUtility.SaveAsPrefabAsset(root, World + "/PrototypeArena.prefab");
|
||||
return "Arena " + enemies.childCount + " enemy slots now nest formal Type0-" + (FormalEnemies.Length - 1) + " visuals.";
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
}
|
||||
public static string RefreshNewPortraits()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new Exception("Render portraits in Edit Mode");
|
||||
|
||||
Reference in New Issue
Block a user