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
+67 -10
View File
@@ -13,8 +13,10 @@ public static class FinalizeEnemyMotions
[Serializable] public sealed class Recipe
{
public string id,assetRoot,rootBone,chestBone,rightArm,leftArm,outputDir,profile,attackStyle,attackEvent,usage;
public string headBone,neckBone,frontRightFoot,frontLeftFoot,hindRightFoot,hindLeftFoot,rightFoot,leftFoot;
public float height,idleSeconds,idleBreath,attackSeconds,attackWindup,attackImpact,attackRecover;
public float attackLeanDegrees,attackArmDegrees,attackLunge,attackReach,idleSourceWeight,attackSourceWeight;
public float attackLeanDegrees,attackArmDegrees,attackLunge,attackReach,idleSourceWeight,attackSourceWeight,wingSpreadDegrees;
public float attackHeadReach,attackHeadPitchDegrees,attackSpreadDegrees;
}
sealed class Pose
{
@@ -79,7 +81,18 @@ public static class FinalizeEnemyMotions
var r=JsonUtility.FromJson<Recipe>(File.ReadAllText(Path.Combine(Repo,recipePath)));
if(string.IsNullOrEmpty(r.attackStyle))r.attackStyle="body_slam";
if(string.IsNullOrEmpty(r.attackEvent))r.attackEvent=r.usage=="companion_visual" ? "OnCompanionAttackImpact" : "OnEnemyAttackImpact";
if(r.attackStyle!="body_slam" && r.attackStyle!="ranged_cast")throw new ArgumentException("Unsupported attack style");
bool bird=r.profile=="avian_grounded";
bool turtle=r.profile=="quadruped_turtle";
bool heavy=r.profile=="quadruped_heavy";
bool quadruped=turtle || heavy;
bool roar=r.attackStyle=="flame_roar";
if(r.profile!="biped_short" && !bird && !quadruped)throw new ArgumentException("Unsupported anatomy");
if(r.attackStyle!="body_slam" && r.attackStyle!="ranged_cast" && r.attackStyle!="wing_cast" && r.attackStyle!="turtle_bubble" && r.attackStyle!="braced_bash" && !roar)throw new ArgumentException("Unsupported attack style");
if(roar && r.profile!="biped_short")throw new ArgumentException("Flame roar requires an upright biped");
if(bird!=(r.attackStyle=="wing_cast") || (bird && !(r.wingSpreadDegrees>0 && r.wingSpreadDegrees<=60)))
throw new ArgumentException("Invalid grounded-bird wing cast");
if(turtle!=(r.attackStyle=="turtle_bubble"))throw new ArgumentException("Invalid turtle bubble cast");
if(heavy!=(r.attackStyle=="braced_bash"))throw new ArgumentException("Invalid heavy quadruped bash");
if(r.height<=0 || !(0<r.attackWindup && r.attackWindup<r.attackImpact && r.attackImpact<r.attackRecover && r.attackRecover<1))
throw new ArgumentException("Invalid dimensions / attack phase order");
var sb=new StringBuilder();
@@ -91,10 +104,18 @@ public static class FinalizeEnemyMotions
var anim=model.GetComponent<Animator>();if(anim!=null) anim.enabled=false;
var bones=model.GetComponentsInChildren<Transform>().Where(t=>t!=model.transform && (t.name=="Armature" || t.name.StartsWith("bone_"))).ToArray();
var root=bones.First(t=>t.name==r.rootBone);
var head=quadruped || roar ? bones.First(t=>t.name==r.headBone) : null;
var neck=quadruped || roar ? bones.First(t=>t.name==r.neckBone) : null;
var feet=(quadruped ? new[]{r.frontRightFoot,r.frontLeftFoot,r.hindRightFoot,r.hindLeftFoot} : roar ? new[]{r.rightFoot,r.leftFoot} : new string[0]).Select(n=>bones.First(t=>t.name==n)).ToArray();
var footPositions=feet.Select(t=>t.position).ToArray();var footRotations=feet.Select(t=>t.rotation).ToArray();
var chest=bones.FirstOrDefault(t=>t.name==r.chestBone);
var arms=new[] {bones.FirstOrDefault(t=>t.name==r.rightArm),bones.FirstOrDefault(t=>t.name==r.leftArm)};
if(r.profile=="biped_short" && (chest==null || arms.Any(t=>t==null))) throw new Exception("Missing profile bones");
if(!quadruped && (chest==null || arms.Any(t=>t==null) || arms[0]==arms[1])) throw new Exception("Missing/distinct profile bones required");
if((heavy || roar) && (chest==null || chest==head))throw new Exception("Independent chest and head required");
var bind=Pose.Read(bones);var bindRootPosition=root.position;float bindBottom=Bottom(model);
var bindRootRotation=root.rotation;
var wingSides=bird || roar ? arms.Select(a=>Mathf.Sign(a.position.x-root.position.x)).ToArray() : new float[0];
if((bird || roar) && wingSides[0]==wingSides[1])throw new Exception("Arm/wing roots must be on opposite sides");
var skins=model.GetComponentsInChildren<SkinnedMeshRenderer>();
float unitScale=(skins.SelectMany(Vertices).Max(v=>v.y)-bindBottom)/r.height;
var source=AssetDatabase.LoadAllAssetsAtPath(r.assetRoot+"/Anims/"+r.id+"_"+name+".fbx").OfType<AnimationClip>().First(c=>!c.name.StartsWith("__preview__"));
@@ -114,28 +135,64 @@ public static class FinalizeEnemyMotions
bones[b].localScale=bind.s[b];
}
root.position=new Vector3(bindRootPosition.x,root.position.y,bindRootPosition.z);
if(r.profile=="biped_short")
if(roar)root.rotation=bindRootRotation;
if(quadruped)
{
root.rotation=bindRootRotation;
if(idle)head.position+=Vector3.up*(r.idleBreath*Mathf.Pow(Mathf.Sin(Mathf.PI*t),2)*unitScale);
else
{
float strike=Strike(r,t);
// Heavy animals drive the shoulder and head together; a turtle
// only extends its neck. Paws are planted in either case.
if(heavy)chest.position+=Vector3.back*(r.attackLunge*strike*unitScale);
neck.position+=Vector3.back*(r.attackHeadReach*strike*unitScale);
head.rotation=Quaternion.AngleAxis(-r.attackHeadPitchDegrees*strike,Vector3.right)*head.rotation;
}
}
if(r.profile=="biped_short" || bird)
{
if(idle)
{
float breath=Mathf.Pow(Mathf.Sin(Mathf.PI*t),2);
chest.localScale=Vector3.Scale(chest.localScale,new Vector3(1+.007f*breath,1+r.idleBreath/r.height*breath,1+.007f*breath));
if(roar)head.position+=Vector3.up*(r.idleBreath*breath*unitScale);
else chest.localScale=Vector3.Scale(chest.localScale,new Vector3(1+.007f*breath,1+r.idleBreath/r.height*breath,1+.007f*breath));
}
else
{
float strike=Strike(r,t);
root.rotation=Quaternion.AngleAxis(-r.attackLeanDegrees*strike,Vector3.right)*root.rotation;
root.position+=Vector3.back*(r.attackLunge*strike*unitScale);
foreach(var arm in arms)
if(!roar)
{
float armSign=r.attackStyle=="ranged_cast" ? 1 : -1;
root.rotation=Quaternion.AngleAxis(-r.attackLeanDegrees*strike,Vector3.right)*root.rotation;
root.position+=Vector3.back*(r.attackLunge*strike*unitScale);
}
for(int side=0;side<arms.Length;side++)
{
var arm=arms[side];
float armSign=r.attackStyle=="ranged_cast" || roar ? 1 : -1;
arm.rotation=Quaternion.AngleAxis(armSign*r.attackArmDegrees*strike,Vector3.right)*arm.rotation;
// Standing bird wings start outward/upward: negative X sweeps
// them toward -Z. Opposite Z rotations open each wing upward.
if(bird)arm.rotation=Quaternion.AngleAxis(wingSides[side]*r.wingSpreadDegrees*strike,Vector3.forward)*arm.rotation;
if(roar)arm.rotation=Quaternion.AngleAxis(wingSides[side]*r.attackSpreadDegrees*strike,Vector3.forward)*arm.rotation;
// A ranged cast presents both paws in front, without moving the actor.
// Small offline bone translation supplies reach for very short chibi arms.
if(r.attackStyle=="ranged_cast")arm.position+=Vector3.back*(r.attackReach*strike*unitScale);
if(r.attackStyle=="ranged_cast" || bird || roar)arm.position+=Vector3.back*(r.attackReach*strike*unitScale);
}
if(roar)
{
neck.position+=Vector3.back*(r.attackHeadReach*strike*unitScale);
head.rotation=Quaternion.AngleAxis(r.attackHeadPitchDegrees*strike,Vector3.right)*head.rotation;
}
}
}
// Planted feet are baked offline for the standing braced actions.
for(int foot=0;foot<feet.Length;foot++)
{
for(int b=0;b<bones.Length;b++)if(bones[b]!=feet[foot] && bones[b].IsChildOf(feet[foot]))
{bones[b].localPosition=bind.p[b];bones[b].localRotation=bind.q[b];bones[b].localScale=bind.s[b];}
feet[foot].position=footPositions[foot];feet[foot].rotation=footRotations[foot];
}
root.position+=Vector3.up*(bindBottom-Bottom(model));
poses[f]=Pose.Read(bones);
}