"""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")