const fs = require('fs'), ts = require('typescript'), assert = require('node:assert/strict'); const storage = new Map(), tweens = []; const cc = {Component: class {}, Sprite: class {}, v2: (x,y) => ({x,y}), Vec2: {ZERO:{x:0,y:0}}, BlockInputEvents: class {}, macro:{MAX_ZINDEX:9999}, isValid:n=>!n.destroyed, instantiate:n=>{const copy=node();copy.original=n;return copy;}, sys: {localStorage: {getItem: k => storage.has(k) ? storage.get(k) : null, setItem: (k,v) => storage.set(k,v)}}, _decorator: {ccclass: C => C, property: (...a) => a.length >= 2 ? undefined : () => {}}, Tween: {stopAllByTarget() {}}, tween: node => { const steps = [], record={node, steps}; tweens.push(record); record.callbacks=[]; return {delay(time){record.delay=time;return this;},to(time, value) {steps.push({time,value}); return this;}, call(fn){record.callbacks.push({after:steps.length,fn});return this;}, start() {}}; }}; const out = {}, result = ts.transpileModule(fs.readFileSync('assets/coin_madness/script/CoinMadnessPanel.ts','utf8'), { compilerOptions: {target:ts.ScriptTarget.ES2017,module:ts.ModuleKind.CommonJS,experimentalDecorators:true}, reportDiagnostics:true}); assert.equal(result.diagnostics.length,0); new Function('require','exports','cc',result.outputText)(()=>({default:class{}}),out,cc); function node() { const children = new Map(), component = {setText(text) {this.text=text;}, stopAutoScroll(){}, scrollToOffset(){}}; return {active:true,scaleX:1,scaleY:1,opacity:255,height:100,anchorY:.5,getComponent:()=>component, convertToWorldSpaceAR:()=>({x:0,y:0}),convertToNodeSpaceAR:p=>p, addChild(n){n.parent=this;},addComponent(){},destroy(){this.destroyed=true;}, getChildByName(name) {if(!children.has(name))children.set(name,node());return children.get(name);}}; } function panel(wins, scope='test:alice', period='week1') { const p = new out.default(); p.api={progressScope:scope}; p.data={state:{totalWins:wins,claimed:[false],config:{periodId:period,endsAt:Date.now()+100000,stages:[{wins:10}]}}}; p.card=node(); const viewport=node();viewport.height=848;viewport.parent=p.card; p.content={children:[node()],parent:viewport,scaleX:1,scaleY:1};p.content.children[0].parent=p.content; p.syncProgress(p.data.state); p.paintProgress(p.content.children[0],0,false); return p; } const first=panel(2); assert.equal(first.shownWins,2); const next=panel(7); assert.equal(next.shownWins,2); next.update(.2); assert.equal(next.shownWins,2); next.update(.2); assert.equal(next.shownWins,2,'hold old progress for 0.4 seconds'); assert.equal(next.progressDuration,.32,'half a bar takes 0.32 seconds'); next.update(.16); assert(next.shownWins>2&&next.shownWins<7); assert(Math.abs(next.taskProgress(10)-4.5)<1e-9,'progress remains linear'); const mid = next.shownWins; next.syncProgress(next.data.state); assert.equal(next.shownWins,mid); next.update(3); assert.equal(next.shownWins,7); assert.equal(tweens.length,0); const complete=panel(12), row=complete.content.children[0]; assert(!row.getChildByName('Claim').active); complete.update(4); assert(!row.getChildByName('Claim').active); assert.equal(tweens.length,1); assert(row.getChildByName('Normal').active); assert(row.getChildByName('Progress').active); assert.equal(tweens[0].delay,.16); complete.paintProgress(row,0,false); assert(!row.getChildByName('Ready').active,'remain normal during hold and first half'); assert.equal(tweens[0].callbacks[0].after,2,'switch at minimum scale'); tweens[0].callbacks[0].fn(); assert(row.getChildByName('Ready').active); assert(row.getChildByName('Claim').active); assert(!row.getChildByName('Progress').active); assert(!complete.pendingReadyRows.has(row)); assert.equal(tweens[0].node,row,'pulse the original task row'); assert.equal(row.opacity,255); assert.equal(row.parent,complete.content); assert.deepEqual(tweens[0].steps.map(s=>s.value.scaleX),[1.08,.1,1.08,1]); assert.deepEqual(tweens[0].steps.map(s=>s.time),[.12,.096,.064,.112]); complete.paintProgress(row,0,false); complete.update(1); assert.equal(tweens.length,1); row.scaleX=.1; row.scaleY=.1; complete.stopReadyPulse(row); assert.equal(row.scaleX,1); assert.equal(row.scaleY,1); assert.equal(complete.readyScales.size,0); assert(!row.destroyed); assert.equal(panel(12).shownWins,12); assert.equal(tweens.length,1); assert.equal(panel(5,'test:bob').shownWins,5); assert.equal(panel(1,'test:alice','week2').shownWins,1); const interrupted=panel(20); interrupted.update(1.6); const seen=Number(storage.get(interrupted.progressKey)); assert.equal(panel(20).shownWins,seen); const prefab=JSON.parse(fs.readFileSync('assets/coin_madness/prefab/CoinMadness.prefab')); const binding=prefab.find(n=>n.content&&n.card), content=prefab[binding.content.__id__], viewport=prefab[content._parent.__id__]; for(const ref of content._children) { const task=prefab[ref.__id__], y=content._trs.array[1]+task._trs.array[1]; assert(Math.abs(task._trs.array[0])+task._contentSize.width*1.08/2<=viewport._contentSize.width/2); assert(Math.abs(y)+task._contentSize.height*1.08/2<=viewport._contentSize.height/2); } assert(viewport._components.some(r=>prefab[r.__id__].__type__==='cc.Mask')); // Opening focus centers a growing row, clamps at both ends, and never overrides later scrolling. for (const [shown, expected] of [[0,0],[8,157],[35,812]]) { const focus = new out.default(), offsets=[]; focus.shownWins=shown; focus.fromWins=shown; focus.targetWins=shown+5; focus.data={state:{claimed:[2,3,5,10,15,20,25,30,40,50].map(wins=>wins<=shown),config:{stages:[2,3,5,10,15,20,25,30,40,50].map(wins=>({wins}))}}}; focus.content={height:1692,children:Array.from({length:10},(_,i)=>({y:-99-i*166})),parent:{height:880, getComponent:()=>({stopAutoScroll(){},scrollToOffset:p=>offsets.push(p.y)})}}; focus.focusGrowingTask(); assert.deepEqual(offsets,[expected]); focus.focusGrowingTask(); assert.equal(offsets.length,1); if (shown===8) { focus.fromWins=10; focus.shownWins=10; focus.focusGrowingTask(); assert.deepEqual(offsets,[157,323]); focus.stopProgressFollow(); focus.targetWins=25; focus.shownWins=15; focus.focusGrowingTask(); assert.equal(offsets.length,2,'manual scrolling stops automatic following'); } } const sequential=new out.default(); // At seven wins the five-win reward stays at the top while later rows animate. for (const wins of [7, 1]) { const capped=new out.default(), offsets=[]; capped.targetWins=wins; capped.shownWins=0; capped.data={state:{claimed:Array(10).fill(false),config:{stages:[2,3,5,10,15,20,25,30,40,50].map(wins=>({wins}))}}}; capped.content={height:1692,children:Array.from({length:10},(_,i)=>({y:-99-i*166})),parent:{height:880, getComponent:()=>({stopAutoScroll(){},scrollToOffset:p=>offsets.push(p.y)})}}; for(let active=0;active<10;active++) { capped.taskProgress=goal=>capped.data.state.config.stages.findIndex(s=>s.wins===goal)y<=(wins===7?332:0)),'automatic follow retains last claimable row'); capped.followOffset=-1; capped.focusedProgressIndex=-1; capped.shownWins=wins; capped.focusGrowingTask(); assert.equal(offsets[offsets.length-1],0,'reopening positions the first outstanding reward'); capped.data.state.claimed[2]=true; capped.focusGrowingTask(); assert.equal(offsets[offsets.length-1],0,'old rewards keep the opening position after claim updates'); } // A previously claimed first task must be skipped, even while new wins animate. const oldReward=new out.default(), oldOffsets=[]; oldReward.shownWins=8; oldReward.targetWins=25; oldReward.data={state:{claimed:[true,false,false,false],config:{stages:[2,3,5,10].map(wins=>({wins}))}}}; oldReward.content={height:1000,children:[0,1,2,3].map(i=>({y:-99-i*166})),parent:{height:500, getComponent:()=>({stopAutoScroll(){},scrollToOffset:p=>oldOffsets.push(p.y)})}}; oldReward.focusGrowingTask(); assert.deepEqual(oldOffsets,[166]); oldReward.shownWins=25; oldReward.focusGrowingTask(); assert.deepEqual(oldOffsets,[166],'new progress does not scroll away from old rewards'); sequential.api={progressScope:'sequential'}; const state={totalWins:7,claimed:[false,false],config:{periodId:'test',stages:[{wins:5},{wins:10}]}}; storage.set('goldMiner:shownProgress:sequential:test','2'); sequential.syncProgress(state); assert(Math.abs(sequential.progressDuration-.704)<1e-9); sequential.progressElapsed=.56; assert(Math.abs(sequential.taskProgress(5)-3.25)<1e-9); assert.equal(sequential.taskProgress(10),2,'next task waits for current progress'); sequential.progressElapsed=.944; assert.equal(sequential.taskProgress(5),5); assert(Math.abs(sequential.taskProgress(10)-4.5)<1e-9); sequential.progressElapsed=4; assert.equal(sequential.taskProgress(10),7); const goalSpeeds=new out.default();goalSpeeds.api={progressScope:'goal-speeds'}; storage.set('goldMiner:shownProgress:goal-speeds:test','0'); goalSpeeds.syncProgress({totalWins:20,claimed:[false,false,false,false,false,false,false],config:{periodId:'test',stages:[5,6,10,11,20,30,31].map(wins=>({wins}))}}); let begin=0; goalSpeeds.progressSteps.forEach(s=>{assert.equal(s.begin,begin);assert(Math.abs(s.duration-.64*(s.end-s.start)/s.goal)<1e-9);begin+=s.duration;}); const goal20=goalSpeeds.progressSteps[4]; for(const wins of [2,5,8,12,18]) { goalSpeeds.progressElapsed=.4+goal20.begin+wins*.64/20; assert(Math.abs(goalSpeeds.taskProgress(20)-wins)<1e-8,'same speed across all progress values in goal 20'); } console.log('PASS sequential progress, 0.64-second full bar, proportional duration, ready transition and focus.');