- 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>
57 lines
2.8 KiB
Python
57 lines
2.8 KiB
Python
"""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()
|