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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user