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:
@@ -48,6 +48,69 @@ class EnemyProductionTests(unittest.TestCase):
|
||||
def test_generation_success_commits_matching_receipt_last(self):
|
||||
self.run_generation(True)
|
||||
|
||||
def test_flame_roar_requires_own_head_paws_and_mane_region(self):
|
||||
lion={**self.data,'profile':'biped_short','attackStyle':'flame_roar','boneMappingReady':True,
|
||||
'rootBone':'bone_0','chestBone':'bone_1','neckBone':'bone_2','headBone':'bone_3',
|
||||
'rightArm':'bone_4','leftArm':'bone_6','rightPawTip':'bone_5','leftPawTip':'bone_7',
|
||||
'rightFoot':'bone_8','leftFoot':'bone_10','rightFootTip':'bone_9','leftFootTip':'bone_11',
|
||||
'faceRight':'bone_8','faceLeft':'bone_10','maneRegionMin':[-.5,.5,-.3],
|
||||
'maneRegionMax':[.5,1,.3],'maneRigidityTolerance':.001,'attackHeadReach':.015,
|
||||
'attackHeadPitchDegrees':8,'attackSpreadDegrees':25}
|
||||
with mock.patch.object(Path,'read_text',return_value=json.dumps(lion)):
|
||||
self.assertEqual(pipeline.recipe(Path('fixture'))['attackStyle'],'flame_roar')
|
||||
for change in ({'headBone':''},{'headBone':'bone_1'},{'leftPawTip':'bone_5'},
|
||||
{'maneRegionMin':[]},{'maneRigidityTolerance':.1},{'attackHeadPitchDegrees':40},
|
||||
{'attackSpreadDegrees':0},{'profile':'quadruped_heavy'},{'boneMappingReady':False}):
|
||||
with mock.patch.object(Path,'read_text',return_value=json.dumps({**lion,**change})),self.assertRaises(ValueError):
|
||||
pipeline.recipe(Path('fixture'))
|
||||
|
||||
def test_turtle_requires_four_feet_shell_region_and_bind_up(self):
|
||||
turtle={**self.data,'profile':'quadruped_turtle','attackStyle':'turtle_bubble','boneMappingReady':True,
|
||||
'rootBone':'bone_0','shellBone':'bone_0','neckBone':'bone_1','headBone':'bone_2',
|
||||
'faceRight':'bone_4','faceLeft':'bone_6','frontRightFoot':'bone_4','frontLeftFoot':'bone_6',
|
||||
'hindRightFoot':'bone_8','hindLeftFoot':'bone_10','frontRightFootTip':'bone_5','frontLeftFootTip':'bone_7',
|
||||
'hindRightFootTip':'bone_9','hindLeftFootTip':'bone_11','rootTiltReference':'bind_up','walkSwingFraction':.2,
|
||||
'shellRegionMin':[-1,.3,-.2],'shellRegionMax':[1,1,1],'shellRigidityTolerance':.001,'attackHeadReach':.015}
|
||||
with mock.patch.object(Path,'read_text',return_value=json.dumps(turtle)):
|
||||
self.assertEqual(pipeline.recipe(Path('fixture'))['profile'],'quadruped_turtle')
|
||||
for change in ({'hindLeftFoot':'bone_4'},{'hindLeftFootTip':'bone_5'},{'shellRegionMin':[]},
|
||||
{'rootTiltReference':'bone_up'},{'walkSwingFraction':.5},{'attackStyle':'ranged_cast'},
|
||||
{'headBone':''},{'boneMappingReady':False},{'shellRigidityTolerance':.1}):
|
||||
with mock.patch.object(Path,'read_text',return_value=json.dumps({**turtle,**change})),self.assertRaises(ValueError):
|
||||
pipeline.recipe(Path('fixture'))
|
||||
|
||||
def test_heavy_bash_rejects_turtle_style_missing_rigid_regions_and_shared_head(self):
|
||||
heavy={**self.data,'profile':'quadruped_heavy','attackStyle':'braced_bash','boneMappingReady':True,
|
||||
'rootBone':'bone_0','chestBone':'bone_1','neckBone':'bone_2','headBone':'bone_3',
|
||||
'faceRight':'bone_5','faceLeft':'bone_7','frontRightFoot':'bone_5','frontLeftFoot':'bone_7',
|
||||
'hindRightFoot':'bone_9','hindLeftFoot':'bone_11','frontRightFootTip':'bone_6','frontLeftFootTip':'bone_8',
|
||||
'hindRightFootTip':'bone_10','hindLeftFootTip':'bone_12','rootTiltReference':'bind_up','walkSwingFraction':.2,
|
||||
'armorRegionMin':[-.3,.3,-.1],'armorRegionMax':[.3,.7,.5],'armorRigidityTolerance':.001,
|
||||
'hornRegionMin':[-.1,.7,-.6],'hornRegionMax':[.1,1,-.4],'hornRigidityTolerance':.001,
|
||||
'attackHeadReach':.015,'attackLunge':.025,'attackHeadPitchDegrees':10}
|
||||
with mock.patch.object(Path,'read_text',return_value=json.dumps(heavy)):
|
||||
self.assertEqual(pipeline.recipe(Path('fixture'))['profile'],'quadruped_heavy')
|
||||
for change in ({'attackStyle':'turtle_bubble'},{'hornRegionMin':[]},{'armorRigidityTolerance':.1},
|
||||
{'headBone':'bone_1'},{'hindLeftFoot':'bone_5'},{'attackLunge':.5},
|
||||
{'attackHeadPitchDegrees':0},{'boneMappingReady':False}):
|
||||
with mock.patch.object(Path,'read_text',return_value=json.dumps({**heavy,**change})),self.assertRaises(ValueError):
|
||||
pipeline.recipe(Path('fixture'))
|
||||
|
||||
def test_bird_requires_own_two_wing_mapping_and_matching_style(self):
|
||||
bird={**self.data,'profile':'avian_grounded','attackStyle':'wing_cast',
|
||||
'boneMappingReady':True,'rootBone':'bone_0','chestBone':'bone_1',
|
||||
'rightArm':'bone_2','leftArm':'bone_4','rightWingTip':'bone_3','leftWingTip':'bone_5',
|
||||
'rightFoot':'bone_6','leftFoot':'bone_8','rightFootTip':'bone_7','leftFootTip':'bone_9',
|
||||
'faceRight':'bone_6','faceLeft':'bone_8','wingSpreadDegrees':25}
|
||||
with mock.patch.object(Path,'read_text',return_value=json.dumps(bird)):
|
||||
self.assertEqual(pipeline.recipe(Path('fixture'))['profile'],'avian_grounded')
|
||||
for change in ({'rightWingTip':''},{'leftArm':'bone_2'},{'leftFootTip':'bone_7'},
|
||||
{'attackStyle':'ranged_cast'},{'wingSpreadDegrees':90},{'boneMappingReady':False}):
|
||||
with mock.patch.object(Path,'read_text',return_value=json.dumps({**bird,**change})),self.assertRaises(ValueError):
|
||||
pipeline.recipe(Path('fixture'))
|
||||
with mock.patch.object(Path,'read_text',return_value=json.dumps({**self.data,'attackStyle':'wing_cast'})),self.assertRaises(ValueError):
|
||||
pipeline.recipe(Path('fixture'))
|
||||
|
||||
def run_generation(self,success):
|
||||
root=REPO/'Tools/output/enemy_pipeline_tests'
|
||||
root.mkdir(exist_ok=True)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Contract guards for timing, frozen-source coverage and production readiness."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0,str(ROOT/'Tools'))
|
||||
from validate_skill_design import load,validate
|
||||
|
||||
class SkillDesignTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.data=[load(ROOT/p) for p in ['Config/Combat/skill_system_v1.proposal.json',
|
||||
'Config/Combat/skill_presentations_v1.proposal.json','Doc/Design/Sound/BattleSkills/manifest.json',
|
||||
'Config/Gameplay/greenfield.json']]
|
||||
|
||||
def test_covers_current_actors_without_claiming_runtime(self):
|
||||
r=validate(*self.data)
|
||||
self.assertEqual(r['skills'],len(self.data[3]['pets'])*2+len(self.data[3]['enemies']))
|
||||
self.assertFalse(r['runtimeIntegrated']);self.assertFalse(r['generatedAudio'])
|
||||
|
||||
def test_missing_actor_or_wrong_skill_kind_is_rejected(self):
|
||||
self.data[0]['skills'].pop()
|
||||
with self.assertRaisesRegex(ValueError,'coverage'):validate(*self.data)
|
||||
self.setUp();self.data[0]['skills'][1]['template']='ability_heal'
|
||||
with self.assertRaisesRegex(ValueError,'semantics'):validate(*self.data)
|
||||
|
||||
def test_animation_and_cooldown_conflicts_are_rejected(self):
|
||||
skill=next(s for s in self.data[0]['skills'] if s['id']=='pet.1.basic')
|
||||
skill['timing']=dict(releaseTicks=11,recoveryTicks=11)
|
||||
with self.assertRaisesRegex(ValueError,'cooldown'):validate(*self.data)
|
||||
skill['timing']=dict(releaseTicks=2,recoveryTicks=16)
|
||||
with self.assertRaisesRegex(ValueError,'marker'):validate(*self.data)
|
||||
|
||||
def test_missing_resource_cannot_claim_ready(self):
|
||||
asset=next(a for a in self.data[1]['assets'] if a['status']=='planned')
|
||||
asset['status']='existing'
|
||||
with self.assertRaisesRegex(ValueError,'missing'):validate(*self.data)
|
||||
|
||||
def test_unresolved_cue_and_invalid_audio_request_are_rejected(self):
|
||||
self.data[1]['profiles'][0]['bindings']['HitResolved'][0]['asset']='missing'
|
||||
with self.assertRaisesRegex(ValueError,'Unresolved'):validate(*self.data)
|
||||
self.setUp();self.data[2]['items'][0]['duration']=.5
|
||||
with self.assertRaisesRegex(ValueError,'1..30'):validate(*self.data)
|
||||
|
||||
def test_actions_compose_but_cannot_repeat_an_action_id(self):
|
||||
t=self.data[0]['templates'][0]
|
||||
t['actions'].append({**t['actions'][0],'id':'additional_hit'})
|
||||
validate(*self.data)
|
||||
t['actions'][1]['id']=t['actions'][0]['id']
|
||||
with self.assertRaisesRegex(ValueError,'Duplicate logical action'):validate(*self.data)
|
||||
|
||||
def test_model_forward_does_not_force_world_direction(self):
|
||||
self.data[1]['anchorPolicy']['worldFacing']='fixed_world_minus_z'
|
||||
with self.assertRaisesRegex(ValueError,'world facing'):validate(*self.data)
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in New Issue
Block a user