- 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>
127 lines
10 KiB
Python
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")
|