/** 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(); } }