115 lines
8.8 KiB
JavaScript
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();
|
|
}
|
|
}
|