"""Focused guards: anatomy/phase validation and atomic generation promotion.""" import copy import hashlib import json from pathlib import Path import sys import tempfile import types import unittest from unittest import mock REPO=Path(__file__).resolve().parents[2] sys.path.insert(0,str(REPO/'Tools')) import enemy_character_pipeline as pipeline import generate_animal_walk as generator class EnemyProductionTests(unittest.TestCase): def setUp(self): self.data=json.loads((REPO/'Config/Art/EnemyCharacters/RockBall.json').read_text(encoding='utf-8')) def test_recipe_rejects_wrong_anatomy_and_phase_order(self): with mock.patch.object(Path,'read_text',return_value=json.dumps(self.data)): self.assertEqual(pipeline.recipe(Path('fixture'))['id'],'RockBall') for change in ({'profile':'quadruped_unvalidated'},{'attackImpact':.1},{'walkStride':10}): data={**self.data,**change} with mock.patch.object(Path,'read_text',return_value=json.dumps(data)),self.assertRaises(ValueError): pipeline.recipe(Path('fixture')) def test_generation_failure_preserves_previous_artifacts_and_receipt(self): self.run_generation(False) def test_companion_role_and_legacy_enemy_recipe(self): with mock.patch.object(Path,'read_text',return_value=json.dumps(self.data)): legacy=pipeline.recipe(Path('fixture')) self.assertEqual(legacy['visualPrefab'],self.data['enemyPrefab']) self.assertEqual(legacy['attackEvent'],'OnEnemyAttackImpact') companion={**self.data,'usage':'companion_visual','attackStyle':'ranged_cast', 'visualPrefab':'Assets/Bundles/Characters/Animal/FireCat/FireCat_Companion.prefab'} companion.pop('enemyPrefab') with mock.patch.object(Path,'read_text',return_value=json.dumps(companion)): result=pipeline.recipe(Path('fixture')) self.assertEqual(result['attackEvent'],'OnCompanionAttackImpact') for change in ({'usage':'unknown'},{'boneMappingReady':False},{'attackStyle':'unknown'}, {'visualPrefab':'../outside.prefab'},{'attackReach':20}): with mock.patch.object(Path,'read_text',return_value=json.dumps({**companion,**change})),self.assertRaises(ValueError): pipeline.recipe(Path('fixture')) 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) with tempfile.TemporaryDirectory(dir=root) as temporary: base=Path(temporary).resolve() self.assertTrue(base.is_relative_to(root.resolve())) out=base/'motion';out.mkdir() rig=base/'rig_fixture.dat';rig.write_text('same-rig-fixture') (out/'animated.fbx').write_text('previous-fixture') (out/'motion_request.json').write_text('{"status":"previous"}') runner=types.ModuleType('backend.unimate_runner') runner._cfg=lambda: {'unimate_jobs_dir':'/fixture'} runner._scp_to=lambda *a,**k: None runner._ssh=lambda *a,**k: None def submit(**kwargs): if not success:raise RuntimeError('fixture inference failure') (kwargs['directory']/'animated.fbx').write_text('new-fixture') return {'remote_job':'fixture-only-no-network'} runner.submit_unimate_job=submit backend=types.ModuleType('backend');backend.__path__=[];backend.unimate_runner=runner argv=['generate_animal_walk','--rig',str(rig),'--out',str(out),'--motion','Attack','--face-right','right','--face-left','left'] with mock.patch.dict(sys.modules,{'backend':backend,'backend.unimate_runner':runner}),mock.patch.object(sys,'argv',argv): if success:generator.main() else: with self.assertRaises(RuntimeError):generator.main() record=json.loads((out/'motion_request.json').read_text()) if success: self.assertEqual((out/'animated.fbx').read_text(),'new-fixture') self.assertEqual(record['status'],'generated') self.assertEqual(record['source_fbx_sha256'],hashlib.sha256((out/'animated.fbx').read_bytes()).hexdigest()) else: self.assertEqual((out/'animated.fbx').read_text(),'previous-fixture') self.assertEqual(record,{'status':'previous'}) if __name__=='__main__':unittest.main()