MatchMaster/tools/coin-madness/check-progress-animation.cjs

140 lines
9.5 KiB
JavaScript

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)<active?Math.min(wins,goal):0;
capped.focusGrowingTask();
}
assert.equal(offsets[offsets.length-1],wins===7?332:0);
assert(offsets.every(y=>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.');