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:
udx
2026-10-21 12:23:55 +08:00
co-authored by Claude Opus 5.5
parent 6c64210331
commit 89e0fa246f
780 changed files with 7117051 additions and 15906 deletions
+78 -2
View File
@@ -14,8 +14,11 @@ public static class VerifyEnemyMotions
{
[Serializable] public sealed class Recipe
{
public string id,assetRoot,staticPrefab,enemyPrefab,rootBone,rightFootTip,leftFootTip,outputDir,usage,attackEvent;
public float height,walkStride,maxRootTilt,groundTolerance,attackImpact,attackSeconds,attackLunge;
public string id,assetRoot,staticPrefab,enemyPrefab,rootBone,rightFootTip,leftFootTip,outputDir,usage,attackEvent,profile,rightWingTip,leftWingTip;
public string attackStyle,headBone,chestBone,rightPawTip,leftPawTip;
public float height,walkStride,walkLift,maxRootTilt,groundTolerance,attackImpact,attackSeconds,attackLunge;
public float attackHeadReach,attackHeadPitchDegrees,maneRigidityTolerance;
public float[] maneRegionMin,maneRegionMax;
public int maxTriangles;
}
static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../.."));
@@ -52,7 +55,21 @@ public static class VerifyEnemyMotions
var model=anim.gameObject;var bones=go.GetComponentsInChildren<Transform>();
var root=bones.First(b=>b.name==r.rootBone);
var footR=bones.First(b=>b.name==r.rightFootTip);var footL=bones.First(b=>b.name==r.leftFootTip);
var wings=r.profile=="avian_grounded" ? new[]{bones.First(b=>b.name==r.rightWingTip),bones.First(b=>b.name==r.leftWingTip)} : new Transform[0];
bool roar=r.attackStyle=="flame_roar";
var head=roar ? bones.First(b=>b.name==r.headBone) : null;
var chest=roar ? bones.First(b=>b.name==r.chestBone) : null;
var paws=roar ? new[]{r.rightPawTip,r.leftPawTip}.Select(n=>bones.First(b=>b.name==n)).ToArray() : new Transform[0];
var bind=Vertices(go);var bb=BoundsOf(bind);
var headBind=roar ? head.rotation : Quaternion.identity;var chestBind=roar ? chest.rotation : Quaternion.identity;
var headPosition=roar ? head.position : Vector3.zero;
var mane=roar ? Enumerable.Range(0,bind.Length).Where(i=>Enumerable.Range(0,3).All(c=>bind[i][c]/r.height>=r.maneRegionMin[c] && bind[i][c]/r.height<=r.maneRegionMax[c])).ToArray() : new int[0];
var footTips=new[]{footR,footL};
var pawGeometry=roar ? Enumerable.Range(0,2).Select(f=>Enumerable.Range(0,bind.Length).Where(i=> {
var d=footTips.Select(t=>Vector2.Distance(new Vector2(bind[i].x,bind[i].z),new Vector2(t.position.x,t.position.z))).ToArray();
return bind[i].y<r.height*.18f && d[f]==d.Min() && d[f]<r.height*.3f;
}).ToArray()).ToArray() : new int[0][];
if(roar)Require(mane.Length>=20 && pawGeometry.All(p=>p.Length>=5),"Mane and both feet require actual geometry samples");
int triangles=go.GetComponentsInChildren<SkinnedMeshRenderer>().Sum(s=>s.sharedMesh.triangles.Length/3);
Require(Mathf.Abs(bb.size.y-r.height)<.002f && Mathf.Abs(bb.min.y)<.002f,"Bind height/bottom invalid");
Require(triangles<=r.maxTriangles,"Triangle budget exceeded");
@@ -63,6 +80,7 @@ public static class VerifyEnemyMotions
finally {Object.DestroyImmediate(approved);}
Require(shape<.002f,"Approved mesh changed");
sb.AppendLine($"BIND height={bb.size.y:F6} bottom={bb.min.y:F6} tris={triangles} staticVertexError={shape:F6}");
if(roar)sb.AppendLine($"ROAR bindManeVertices={mane.Length} footVertices={string.Join("/",pawGeometry.Select(p=>p.Length))}");
var clips=new Dictionary<string,AnimationClip>();
foreach(string name in walkOnly ? new[] {"Walk"} : new[] {"Idle","Walk","Attack"})
{
@@ -72,6 +90,9 @@ public static class VerifyEnemyMotions
Require(AnimationUtility.GetAnimationClipSettings(clip).loopTime==(name!="Attack"),"Wrong loop flag "+name);
Vector3[] start=null,end=null;float minY=float.MaxValue,maxY=float.MinValue,maxTilt=0,movement=0,minHeight=float.MaxValue;
var right=new List<float>();var left=new List<float>();var roots=new List<Vector3>();
var wingStart=new Vector3[wings.Length];var wingReach=new float[wings.Length];var rootStartRotation=Quaternion.identity;
var pawStart=new Vector3[paws.Length];var pawReach=new float[paws.Length];var pawSpread=new float[paws.Length];var lifts=new float[2];
var actualStride=new[]{new List<float>(),new List<float>()};float maneError=0,headForward=0,headStart=0,headPitch=0;int minSupport=2;
for(int f=0;f<=120;f++)
{
clip.SampleAnimation(model,clip.length*f/120f);
@@ -80,10 +101,53 @@ public static class VerifyEnemyMotions
movement=Mathf.Max(movement,Difference(start,p));minY=Mathf.Min(minY,b.min.y);maxY=Mathf.Max(maxY,b.min.y);
minHeight=Mathf.Min(minHeight,b.size.y);maxTilt=Mathf.Max(maxTilt,Vector3.Angle(root.up,Vector3.up));
right.Add((footR.position-root.position).z);left.Add((footL.position-root.position).z);roots.Add(root.position);
if(f==0)rootStartRotation=root.rotation;
if(roar)
{
var bodyDelta=chest.rotation*Quaternion.Inverse(chestBind);var bodyInverse=Quaternion.Inverse(bodyDelta);
maxTilt=Mathf.Max(maxTilt,Vector3.Angle(bodyDelta*Vector3.up,Vector3.up));
var headInverse=Quaternion.Inverse(head.rotation*Quaternion.Inverse(headBind));
maneError=Mathf.Max(maneError,mane.Max(i=>Vector3.Distance(bind[i],headInverse*(p[i]-head.position)+headPosition)));
var relativeHead=bodyInverse*(head.position-chest.position);
if(f==0)headStart=relativeHead.z;headForward=Mathf.Max(headForward,headStart-relativeHead.z);
var facing=bodyInverse*(head.rotation*Quaternion.Inverse(headBind)*Vector3.back);
headPitch=Mathf.Max(headPitch,Mathf.Asin(Mathf.Clamp(facing.y,-1,1))*Mathf.Rad2Deg);
for(int arm=0;arm<paws.Length;arm++)
{
var relative=bodyInverse*(paws[arm].position-chest.position);
if(f==0)pawStart[arm]=relative;
pawReach[arm]=Mathf.Max(pawReach[arm],pawStart[arm].z-relative.z);
pawSpread[arm]=Mathf.Max(pawSpread[arm],Mathf.Abs(relative.x)-Mathf.Abs(pawStart[arm].x));
}
int support=0;
for(int foot=0;foot<2;foot++)
{
float bottom=pawGeometry[foot].Min(i=>p[i].y);Require(bottom>=-r.groundTolerance,"Foot penetrates floor");
if(bottom<=r.groundTolerance)support++;lifts[foot]=Mathf.Max(lifts[foot],bottom);
actualStride[foot].Add(pawGeometry[foot].Average(i=>p[i].z-root.position.z));
}
minSupport=Mathf.Min(minSupport,support);
}
for(int w=0;w<wings.Length;w++)
{
// Remove actor pitch/yaw so body lean cannot pass as a wing sweep.
var relative=Quaternion.Inverse(root.rotation*Quaternion.Inverse(rootStartRotation))*(wings[w].position-root.position);
if(f==0)wingStart[w]=relative;
wingReach[w]=Mathf.Max(wingReach[w],wingStart[w].z-relative.z);
}
}
float seam=Difference(start,end);float strideR=right.Max()-right.Min(),strideL=left.Max()-left.Min();
float drift=roots.Max(p=>Vector2.Distance(new Vector2(p.x,p.z),new Vector2(roots[0].x,roots[0].z)));
sb.AppendLine($"{name} len={clip.length:F3} loop={name!="Attack"} bottom=[{minY:F6},{maxY:F6}] minHeight={minHeight:F5} tilt={maxTilt:F3} movement={movement:F5} seam={seam:F6} rootXZ={drift:F5} feet={strideR:F5}/{strideL:F5}");
if(roar)
{
var strides=actualStride.Select(v=>v.Max()-v.Min()).ToArray();
sb.AppendLine($"ROAR {name} maneError={maneError:F6} support={minSupport} actualFootStrides={string.Join("/",strides.Select(v=>v.ToString("F5")))} lifts={string.Join("/",lifts.Select(v=>v.ToString("F5")))} headReach={headForward:F5} headPitchUp={headPitch:F3} pawReach={string.Join("/",pawReach.Select(v=>v.ToString("F5")))} pawSpread={string.Join("/",pawSpread.Select(v=>v.ToString("F5")))}");
Require(maneError<=r.maneRigidityTolerance,"Mane deformation failed\n"+sb);
if(name=="Walk")Require(minSupport>=1 && strides.All(v=>v>r.walkStride*.5f) && lifts.All(v=>v>r.walkLift*.4f),"Actual biped foot gait failed\n"+sb);
else Require(minSupport==2,"Roar and idle must brace on both feet\n"+sb);
if(name=="Attack")Require(headForward>r.attackHeadReach*.5f && headPitch>r.attackHeadPitchDegrees*.4f && pawReach.All(v=>v>.008f) && pawSpread.All(v=>v>.004f),"Head release / both forepaw spread unreadable\n"+sb);
}
Require(minY>=-r.groundTolerance && maxY<=r.groundTolerance,"Ground contact failed\n"+sb);
Require(maxTilt<=r.maxRootTilt && minHeight>r.height*.8f,"Orientation failed\n"+sb);
Require(seam<.001f,"Pose seam failed\n"+sb);
@@ -94,6 +158,11 @@ public static class VerifyEnemyMotions
Require(movement>.02f && drift<r.attackLunge*1.5f+.001f,"Attack unreadable/drifting\n"+sb);
var events=AnimationUtility.GetAnimationEvents(clip);
Require(events.Length==1 && events[0].functionName==r.attackEvent && Mathf.Abs(events[0].time-r.attackImpact*r.attackSeconds)<.001f,"Impact event invalid");
if(wings.Length>0)
{
sb.AppendLine($"WINGS bodyRelativeForwardReach={wingReach[0]:F5}/{wingReach[1]:F5}");
Require(wingReach.All(v=>v>.01f),"Both bird wings must visibly sweep forward\n"+sb);
}
}
}
if(walkOnly)
@@ -114,6 +183,10 @@ public static class VerifyEnemyMotions
string report=Path.Combine(Repo,r.outputDir,"verification.txt");Directory.CreateDirectory(Path.GetDirectoryName(report));File.WriteAllText(report,sb.ToString());
return "PASS\n"+sb;
}
catch(Exception e)
{
string output=Path.Combine(Repo,r.outputDir,"last_failure.txt");Directory.CreateDirectory(Path.GetDirectoryName(output));File.WriteAllText(output,DateTime.UtcNow.ToString("O")+"\n"+e);throw;
}
finally {EditorSceneManager.ClosePreviewScene(scene);}
}
public static async Task<string> Live(string recipePath="Config/Art/EnemyCharacters/RockBall.json")
@@ -123,6 +196,8 @@ public static class VerifyEnemyMotions
var unit=GameObject.Find(prefix+"_Cycle");Require(unit!=null,"Open this character's production review");
var anim=unit.GetComponentInChildren<Animator>();var seen=new HashSet<string>();float maxTilt=0;
var root=unit.GetComponentsInChildren<Transform>().First(t=>t.name==r.rootBone);
var chest=r.attackStyle=="flame_roar" ? unit.GetComponentsInChildren<Transform>().First(t=>t.name==r.chestBone) : null;
var chestBind=chest!=null ? AssetDatabase.LoadAssetAtPath<GameObject>(r.assetRoot+"/"+r.id+"_Rigged.prefab").GetComponentsInChildren<Transform>(true).First(t=>t.name==r.chestBone).rotation : Quaternion.identity;
var position=unit.transform.position;
var deadline=DateTime.UtcNow.AddSeconds(10);
while(DateTime.UtcNow<deadline)
@@ -132,6 +207,7 @@ public static class VerifyEnemyMotions
// Production meshes stay non-readable. Real skin vertices were
// checked in Edit Mode; live QA observes animation/transform state.
maxTilt=Mathf.Max(maxTilt,Vector3.Angle(root.up,Vector3.up));
if(chest!=null)maxTilt=Mathf.Max(maxTilt,Vector3.Angle(chest.rotation*Quaternion.Inverse(chestBind)*Vector3.up,Vector3.up));
Require(Vector3.Distance(position,unit.transform.position)<.001f,"Unexpected root motion");
await Task.Delay(80);
}