140 lines
9.5 KiB
JavaScript
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.');
|