Files
youle_cocos/cocoscreator_projects/scripts/verify-optional-animation-preview.mjs
T

115 lines
8.8 KiB
JavaScript

/** Run in Cocos Game View, with the imported cc module and prefab UUIDs from asset-db.
* Uses real Animation states and isolated nodes; no wire messages or application settings.
*/
export async function verifyOptionalAnimationPreview(e, prefabIds) {
const checks=[];
const assert=(ok,message)=>{if(!ok)throw Error(message);checks.push(message);};
const load=uuid=>new Promise((resolve,reject)=>e.assetManager.loadAny(uuid,(err,asset)=>err?reject(err):resolve(asset)));
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
const root=new e.Node('OptionalAnimationVerification');root.parent=e.director.getScene();
const disposable=[];
const restoreStorage=[];
try {
const menu=await load(prefabIds.menu),notice=await load(prefabIds.notice),settings=await load(prefabIds.settings);
const find=(node,name)=>{if(node.name===name)return node;for(const child of node.children){const found=find(child,name);if(found)return found;}return null;};
const cases=[[menu,['Spirit479','Spirit3120','Spirit76','Spirit11','Spirit12','Spirit14','Spirit17','Spirit13','Spirit7']],
[notice,['Spirit3121','Spirit3017','Spirit5','Spirit235','Spirit604','Spirit3057','Spirit15']]];
for(const [asset,names] of cases)for(const name of names){
const node=find(asset.data,name);
assert(node?.getComponent(e.Animation)&&node.getComponent('OptionalUiAnimation'),`${asset.name}/${name} has authored animation support`);
}
for(const name of ['音乐底','音效底','Spirit357','Spirit388']){
const node=find(settings.data,name);
assert(node?.getComponent(e.Animation)&&node.getComponent('OptionalSwitchAnimation'),`settings/${name} has authored switch animation support`);
}
const Switch=e.js.getClassByName('OptionalSwitchAnimation');
const bareSwitch=new e.Node('BareSwitch');bareSwitch.addComponent(Switch);bareSwitch.parent=root;
assert(bareSwitch.getComponent(e.Animation)&&bareSwitch.getComponent(e.Sprite),'mounting switch support provides both required components');
const horn=find(menu.data,'Spirit479'),hornClip=horn.getComponent(e.Animation).defaultClip;
assert(hornClip&&hornClip.wrapMode===e.AnimationClip.WrapMode.Loop,'horn owns a looping clip');
const idle=e.instantiate(find(menu.data,'Spirit11'));idle.parent=root;
const idleSprite=idle.getComponent(e.Sprite),original=idleSprite.spriteFrame;
const idleAnimation=idle.getComponent(e.Animation);
await sleep(80);
assert(idleSprite.spriteFrame===original&&!idleAnimation.defaultClip,'unassigned clip preserves the authored sprite');
idle.active=false;idleAnimation.defaultClip=hornClip;idle.active=true;
await sleep(100);
const idleState=idleAnimation.getState(hornClip.name);
assert(idleState.isPlaying&&idleState.time>0,'assigned clip starts on show and advances');
idle.active=false;
assert(!idleState.isPlaying,'hiding stops the animation');
const stoppedTime=idleState.time;await sleep(80);
assert(idleState.time===stoppedTime,'hidden animation does not advance');
idle.active=true;await sleep(40);
assert(idleState.isPlaying&&idleState.time<0.3,'showing again restarts the clip');
// Manual sampling verifies the authored frame schedule, independent of wall-clock jitter.
const sampleNode=e.instantiate(horn);sampleNode.parent=root;
const sampleAnimation=sampleNode.getComponent(e.Animation);sampleAnimation.stop();
const hornState=sampleAnimation.getState(hornClip.name),sampleSprite=sampleNode.getComponent(e.Sprite);
const samples=[];
for(const time of [0,0.49,0.5,0.99,1,1.99]){hornState.time=time;hornState.sample();samples.push(sampleSprite.spriteFrame);}
assert(samples[0]===samples[1]&&samples[0]!==samples[2]&&samples[2]===samples[3]&&samples[0]===samples[4]&&samples[0]===samples[5],
'horn preserves the original two-second frame schedule');
const onFrame=samples[0],offFrame=samples[2];
const switchNode=e.instantiate(find(settings.data,'音乐底'));switchNode.parent=root;
const view=switchNode.getComponent(Switch),sprite=switchNode.getComponent(e.Sprite),animation=switchNode.getComponent(e.Animation);
view.setState(true,onFrame,false);view.setState(false,offFrame,true);
assert(sprite.spriteFrame===offFrame,'switch without clips changes immediately');
// Intentionally author the wrong final frame and Loop: the state remains authoritative.
const onClip=e.AnimationClip.createWithSpriteFrames([offFrame,offFrame],20);onClip.name='TestSwitchOn';onClip.wrapMode=e.AnimationClip.WrapMode.Loop;
const offClip=e.AnimationClip.createWithSpriteFrames([onFrame,onFrame],20);offClip.name='TestSwitchOff';
disposable.push(onClip,offClip);view.onClip=onClip;view.offClip=offClip;
view.setState(true,onFrame,false);
assert(sprite.spriteFrame===onFrame,'initial state does not play a transition');
view.setState(false,offFrame,true);await sleep(35);
assert(animation.getState('TestSwitchOff').isPlaying,'off transition starts');
view.setState(true,onFrame,true);await sleep(250);
assert(sprite.spriteFrame===onFrame&&!animation.getState('TestSwitchOn').isPlaying,'rapid reversal finishes at latest state and plays once');
view.setState(false,offFrame,true);switchNode.active=false;
assert(sprite.spriteFrame===offFrame&&!animation.getState('TestSwitchOff').isPlaying,'hide cancels transition and restores latest state');
switchNode.active=true;await sleep(60);
assert(sprite.spriteFrame===offFrame&&!animation.getState('TestSwitchOff').isPlaying,'reopen preserves state without replaying stale transition');
view.offClip=null;view.setState(true,onFrame,true);view.setState(false,offFrame,true);await sleep(180);
assert(sprite.spriteFrame===offFrame,'missing destination clip cancels the old transition and keeps the new state');
// Exercise the real settings click handlers against isolated storage keys.
const LobbyPanel=Array.from(System.entries()).map(([,m])=>m).find(m=>m.LobbyPanel)?.LobbyPanel;
assert(LobbyPanel,'LobbyPanel is loaded for settings integration');
const panel=Object.create(LobbyPanel.prototype),tips=[];
panel.assets={settings,switches:[onFrame,offFrame]};panel.config={identity:{gameid:987654321,agentid:765432109}};
panel.context={launchMode:0};panel.model={canAct:true};panel.disposed=false;panel.modalReleases=[];
panel.tip=message=>tips.push(message);
panel.open=()=>{panel.modal=e.instantiate(settings);panel.modal.parent=root;return panel.modal;};
panel.close=()=>{for(const off of panel.modalReleases)off();panel.modalReleases=[];panel.modal.destroy();};
const musicKey='tsgame_bgm_987654321',soundKey='tsgame_sound_987654321765432109';
for(const key of [musicKey,soundKey]){
const previous=localStorage.getItem(key);restoreStorage.push(()=>previous===null?localStorage.removeItem(key):localStorage.setItem(key,previous));
localStorage.setItem(key,'1');
}
panel.openSettings();
let group=panel.modal.getChildByName('group-6');
const music=group.getChildByName('音乐底'),sound=group.getChildByName('音效底');
const musicView=music.getComponent(Switch);musicView.onClip=onClip;musicView.offClip=offClip;
assert(music.getComponent(e.Sprite).spriteFrame===onFrame&&sound.getComponent(e.Sprite).spriteFrame===onFrame,'settings initialize from persisted state');
music.emit(e.Button.EventType.CLICK);
assert(localStorage.getItem(musicKey)==='0'&&music.getComponent(e.Animation).getState('TestSwitchOff').isPlaying,'music click persists state and starts configured transition');
music.emit(e.Button.EventType.CLICK);await sleep(200);
assert(localStorage.getItem(musicKey)==='1'&&music.getComponent(e.Sprite).spriteFrame===onFrame,'rapid music clicks retain the latest persisted state');
sound.emit(e.Button.EventType.CLICK);
assert(localStorage.getItem(soundKey)==='0'&&sound.getComponent(e.Sprite).spriteFrame===offFrame,'sound click without a clip keeps immediate switching');
for(const name of ['Spirit357','Spirit388']){
const node=group.getChildByName(name),frame=node.getComponent(e.Sprite).spriteFrame;
node.emit(e.Button.EventType.CLICK);
assert(node.getComponent(e.Sprite).spriteFrame===frame&&tips.at(-1)==='原生震动与语音开关尚未接入',`${name} does not invent an unsupported switch state`);
}
assert(tips.length===2,'settings click handlers produce no unexpected error tips');
panel.close();panel.openSettings();group=panel.modal.getChildByName('group-6');
assert(group.getChildByName('音乐底').getComponent(e.Sprite).spriteFrame===onFrame&&group.getChildByName('音效底').getComponent(e.Sprite).spriteFrame===offFrame,
'reopening settings restores the latest music and sound states');
panel.close();
return {checks};
} finally {
for(const restore of restoreStorage)restore();
root.destroy();await sleep(50);for(const clip of disposable)clip.destroy();
}
}