Files
AIGame/Tools/build_skill_design_examples.py
T
udxandClaude Opus 5.5 89e0fa246f 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>
2026-10-21 12:23:55 +08:00

127 lines
10 KiB
Python

"""Build isolated skill-system design examples; never edits live gameplay config."""
from pathlib import Path
import hashlib
import json
ROOT=Path(__file__).resolve().parents[1]
def write(path,data):
target=ROOT/path;target.parent.mkdir(parents=True,exist_ok=True)
target.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
def build():
source=ROOT/'Config/Gameplay/greenfield.json'
gameplay=json.loads(source.read_text(encoding='utf-8-sig'))
def template(id,selector,delivery,effect,release,recovery,parameters):
if effect=='damage':action=dict(id='main',trigger='delivery_hit',type='damage',amount='attack_scaled',scale='ability_value' if id.startswith('ability_') else 'one')
elif effect=='heal':action=dict(id='main',trigger='delivery_hit',type='heal',amount='ability_value')
else:action=dict(id='main',trigger='delivery_hit',type='apply_status',statusId=effect+'_main',value='ability_value',duration='ability_duration')
return dict(id=id,selector=selector,delivery=delivery,actions=[action],
timing=dict(releaseTicks=release,recoveryTicks=recovery),parameters=parameters,
targetLost='expire',channel='main_action',snapshot='caster_on_start_target_on_hit')
basic=dict(cooldown='basic_cooldown',range='basic_range',amount='actor_attack')
special=dict(cooldown='ability_cooldown',range='ability_range',amount='ability_value',targetCount='ability_target_count')
templates=[
template('basic_melee','front_enemy_same_lane','instant','damage',9,11,basic),
template('basic_ranged','front_enemy_same_lane','projectile','damage',6,6,basic),
template('ability_burst','front_enemy_same_lane','projectile','damage',8,8,special),
template('ability_chain','front_enemy_same_lane','instant','damage',8,8,special),
template('ability_heal','lowest_hp_ratio_ally','instant','heal',8,8,{**special,'radius':'ability_radius'}),
template('ability_shield','self','instant','shield',0,8,{**special,'duration':'ability_duration'}),
template('ability_slow','front_enemy_same_lane','instant','slow',8,8,{**special,'duration':'ability_duration','bossScale':'boss_control_scale'}),
template('enemy_melee','front_blocker_same_lane','instant','damage',6,6,basic),
]
skills=[]
for p in gameplay['pets']:
for slot in ('basic','ability'):
kind=('basic_ranged' if p['range']>2 else 'basic_melee') if slot=='basic' else 'ability_'+p['ability']['kind']
presentation='prototype_'+kind
if p['id']==1:presentation='firecat_'+slot
if p['id']==0:presentation='rockball_'+slot
row=dict(id=f"pet.{p['id']}.{slot}",actorKind='pet',actorId=p['id'],slot=slot,
sourcePointer='/pets/'+str(p['id']),template=kind,presentation=presentation)
if p['id']==1:row['timing']=dict(releaseTicks=9 if slot=='basic' else 11,recoveryTicks=9 if slot=='basic' else 11)
skills.append(row)
for e in gameplay['enemies']:
skills.append(dict(id=f"enemy.{e['id']}.basic",actorKind='enemy',actorId=e['id'],slot='basic',
sourcePointer='/enemies/'+str(e['id']),template='enemy_melee',presentation='prototype_enemy_melee'))
plan=dict(schemaVersion=1,status='design_only',runtimeEnabled=False,proposedBattleRules=3,
source=dict(path='Config/Gameplay/greenfield.json',sha256AtAuthoring=hashlib.sha256(source.read_bytes()).hexdigest(),
values='resolve_from_frozen_gameplay_snapshot'),
clock=dict(tickMs=50,cooldownRounding='ceil',simulationUsesWallClock=False),
numeric=dict(hpScale=1000,positionScale=1000,factorScale=10000,rounding='explicit_integer_rules_v1'),
scheduling=dict(channel='main_action',priorities=dict(ability=100,basic=10),
cooldownStarts='cast_start',cancelRefund='none',initialCooldownTicks=0,
interruptBeforeRelease=['death','hard_control'],releasedProjectileSurvivesCaster=True),
projectile=dict(speedMmPerTick=800,maxLifetimeTicks=40,arrival='ceil_distance_at_release',
targetLost='expire',retarget=False,collisionAuthority='simulation'),
statuses=[dict(id='shield_main',merge='max_remaining_replace_expiry',value='flat_hp'),
dict(id='slow_main',merge='min_factor_max_expiry',value='remaining_speed_factor')],
templates=templates,skills=skills)
write('Config/Combat/skill_system_v1.proposal.json',plan)
assets=[]
def asset(id,type,path,status='planned'):
assets.append(dict(id=id,type=type,path=path,status=status))
for name,seconds,mark in [('FireCat',1.1,.5),('RockBall',1,.45)]:
asset('anim.'+name.lower()+'.attack','animation',f'Assets/Bundles/Characters/Animal/{name}/Clips/Attack.anim','existing')
assets[-1].update(clipSeconds=seconds,releaseNormalized=mark)
for name in ['FireCharge','FireProjectile','FireHit','RockHit','Heal','Shield','Slow','Lightning','EnemyHit']:
asset('vfx.'+name,'vfx',f'Assets/Bundles/Effects/BattleSkills/{name}/{name}.prefab')
if name!='FireProjectile':asset('sfx.'+name,'sfx',f'Assets/Bundles/Sound/SFX/BattleSkills/Sfx_{name}.wav')
def cue(kind,asset,anchor,**kwargs):return dict(kind=kind,asset=asset,anchor=anchor,**kwargs)
def profile(id,element,animation=None,fire=False):
hits=[cue('vfx','vfx.'+element,'target_hit',when='valid_hit'),cue('sfx','sfx.'+element,'target_hit',when='valid_hit')]
bindings={'HitResolved':hits}
if animation:bindings['CastStarted']=[cue('animation',animation,'model',fit='cast_duration',rootMotion=False)]
if fire:
bindings.setdefault('CastStarted',[]).extend([cue('vfx','vfx.FireCharge','cast',token='charge'),cue('sfx','sfx.FireCharge','cast',token='charge')])
bindings['CastReleased']=[dict(kind='stop_token',token='charge'),cue('sfx','sfx.FireLaunch','cast')]
bindings['ProjectileSpawned']=[cue('vfx','vfx.FireProjectile','projectile',token='projectile')]
return dict(id=id,bindings=bindings,cancelStops=['charge'],endStops=['charge'],
resourceFallback='prototype',seekReplaysOneShots=False)
asset('sfx.FireLaunch','sfx','Assets/Bundles/Sound/SFX/BattleSkills/Sfx_FireLaunch.wav')
elements={'basic_melee':'RockHit','basic_ranged':'EnemyHit','ability_burst':'FireHit','ability_chain':'Lightning',
'ability_heal':'Heal','ability_shield':'Shield','ability_slow':'Slow','enemy_melee':'EnemyHit'}
profiles=[profile('prototype_'+k,v) for k,v in elements.items()]
profiles.extend([profile('firecat_basic','FireHit','anim.firecat.attack',True),
profile('firecat_ability','FireHit','anim.firecat.attack',True),
profile('rockball_basic','RockHit','anim.rockball.attack'),profile('rockball_ability','Shield')])
for p in profiles:
if 'shield' in p['id'] or p['id']=='rockball_ability':
p['bindings']={'StatusApplied':[cue('vfx','vfx.Shield','body',token='status'),cue('sfx','sfx.Shield','body')]}
view=dict(schemaVersion=1,status='design_only',runtimeEnabled=False,
clock='battle_playback_clock',eventDedup=['battleId','eventSeq','cueIndex'],
anchors=['model','cast','body','target_hit','projectile'],
anchorPolicy=dict(configuredSocketFirst=True,fallback='bounds_based',localForward='-Z',worldFacing='source_to_target'),
audio=dict(bus='BattleSfx',initialVoiceCap=12,mutedStillConsumesEvent=True,
pause='pause_owned_sources',speed='keep_pitch_drop_expired_one_shots'),
lifecycle=dict(statusStop='StatusRemoved',projectileStop='ProjectileExpired_or_HitResolved',battleStop='BattleEnded'),
assets=assets,profiles=profiles)
write('Config/Combat/skill_presentations_v1.proposal.json',view)
prompts={
'FireCharge':'A short gentle magical flame gathering sound, soft rising airy crackle, cute fantasy cat casting, close and dry, one isolated event, no music, no speech',
'FireLaunch':'One short compact magical fireball launch, bright soft ignition and a quick airy whoosh, cute mobile fantasy game, no explosion, no music, no speech',
'FireHit':'One small magical fireball impact, crisp warm pop with a brief soft fiery crackle tail, friendly cartoon fantasy, no heavy bass, no music, no speech',
'RockHit':'One small rounded stone creature striking, compact stone knock and a little gravel scatter, friendly cartoon fantasy game, no music, no speech',
'Heal':'One gentle magical healing chime, warm bell sparkle with a short soft airy tail, friendly pastoral fantasy game, no music bed, no speech',
'Shield':'One soft magical stone shield forming, low rounded stone resonance with a tiny clear sparkle, short dry fantasy game effect, no music, no speech',
'Slow':'One gentle icy magical pulse, light crystalline tick with a soft watery freeze texture, short isolated cartoon game effect, no music, no speech',
'Lightning':'One short small magical electric zap, crisp bright crackle and a soft pop, friendly cartoon fantasy, no harsh buzz, no music, no speech',
'EnemyHit':'One short soft cartoon creature hit, compact muted thump, dry isolated mobile fantasy game effect, no voice, no music, no speech',
}
sound=dict(schemaVersion=1,status='planned_not_generated',batchId='battle_skills_v1',
service='http://127.0.0.1:8012',guide='D:/UD/AI/XModel/TangoFlux_AI调用指南.md',
designDir='Doc/Design/Sound/BattleSkills',outputDir='Tools/output/battle_skills_sfx_v1',
bundleDir='AIClient/Assets/Bundles/Sound/SFX/BattleSkills',
defaults=dict(duration=2,steps=50,guidance_scale=4.5),maxInflight=2,
sourceFormat=dict(container='wav',sampleRate=44100,channels=2),
releaseLicenseReview='pending_upstream_review_per_guide',
items=[dict(id='Sfx_'+name,seed=52001+i,prompt=prompt,duration=2) for i,(name,prompt) in enumerate(prompts.items())])
write('Doc/Design/Sound/BattleSkills/manifest.json',sound)
return plan,view,sound
if __name__=='__main__':
p,v,s=build();print(f"Wrote design examples: {len(p['skills'])} skill bindings, {len(v['profiles'])} presentation profiles, {len(s['items'])} planned SFX")