154 lines
13 KiB
JavaScript
154 lines
13 KiB
JavaScript
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'),ts=require('typescript');
|
|
class Node {
|
|
constructor(name){Object.assign(this,{name,children:[],components:new Map(),active:true,x:0,y:0,width:100,height:100,scaleX:1,scaleY:1});}
|
|
addChild(n){n.parent=this;this.children.push(n);} getChildByName(n){return this.children.find(c=>c.name===n);}
|
|
get childrenCount(){return this.children.length;} setPosition(x,y){this.x=x;this.y=y;} setContentSize(w,h){this.width=w;this.height=h;}
|
|
setScale(s){this.scaleX=this.scaleY=s;} addComponent(C){const c=new C();c.node=this;this.components.set(C,c);return c;}
|
|
getComponent(C){return this.components.get(C);} setSiblingIndex(i){const a=this.parent.children;a.splice(a.indexOf(this),1);a.splice(i,0,this);}
|
|
destroy(){this.destroyed=true;} removeAllChildren(){this.children=[];} targetOff(){} on(){} removeFromParent(){this.parent.children=this.parent.children.filter(n=>n!==this);}
|
|
}
|
|
Node.EventType={TOUCH_END:'touchend'};
|
|
class Sprite{} Sprite.SizeMode={CUSTOM:0,TRIMMED:1};Sprite.Type={SLICED:1};
|
|
class Label{} Label.HorizontalAlign={CENTER:1};Label.VerticalAlign={CENTER:1};Label.Overflow={NONE:0};
|
|
class Button{} Button.Transition={NONE:0,SCALE:1};
|
|
class Graphics{clear(){}rect(...bounds){this.bounds=bounds;}roundRect(){}fill(){}stroke(){}moveTo(){}lineTo(){}close(){}}
|
|
class Digits{set(value){this.value=value;}}
|
|
function fixture(){
|
|
const source={revision:1,periodId:'p',currentLevel:1,endAt:100000000,canDraw:true,canClaimFinal:false,completed:false,levels:Array.from({length:6},(_,i)=>({level:i+1,rewards:Array.from({length:i<5?6-i:0},(_,j)=>({rewardId:`${i}-${j}`,up:j===0,goods:[{id:[2001,2002,2003,1001,2005,1001][j],count:3}],weight:1}))}))};
|
|
const service={busy:false,data:()=>source,now:()=>1000},cc={Node,Sprite,Label,SpriteFrame:class{},Button,Graphics,Component:class{},Color:class{},BlockInputEvents:class{},
|
|
_decorator:{ccclass:C=>C,property:()=>()=>{}},isValid:n=>!!n&&!n.destroyed,view:{getVisibleSize:()=>({width:1080,height:2000})}};
|
|
const context={exports:{},cc,require:name=>name.includes('Probability')?{remainingProbabilities:(_,rs)=>rs.map(()=>1/rs.length),probabilityText:()=> '10%'}:{default:name.includes('Digits')?Digits:service}};
|
|
vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/pyramid/scripts/PyramidPanel.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017,experimentalDecorators:true}}).outputText,context);
|
|
const panel=new context.exports.default();panel.node=new Node('Panel');panel.design=new Node('Design');panel.rewardRoot=new Node('Rewards');panel.tip=new Node('Tooltip');panel.ready=true;
|
|
panel.countdownFrames=Array.from({length:10},(_,i)=>({digit:String(i),getRect:()=>({width:59,height:65})}));panel.countdownBackgroundFrame={name:'timer_bg'};
|
|
for(const name of ['Free','Action','Countdown','Help']){const n=new Node(name);panel.design.addChild(n);n.addComponent(Sprite);n.addComponent(Button);}
|
|
panel.frames=new Proxy({free:{name:'free'},claim:{name:'claim'}},{get:(o,k)=>o[k]||(o[k]={name:k,getRect:()=>({width:100,height:100})})});
|
|
panel.price={'0':{}};panel.animationDebug=()=>false;
|
|
const prefab=JSON.parse(fs.readFileSync('assets/pyramid/prefab/Pyramid.prefab'));
|
|
for(let level=1;level<=5;level++){
|
|
const src=prefab.find(n=>n._name===`Level_${level}`),row=new Node(src._name);row.setContentSize(src._contentSize.width,src._contentSize.height);panel.rewardRoot.addChild(row);
|
|
for(const ref of src._children){const item=prefab[ref.__id__],n=new Node(item._name);n.setContentSize(item._contentSize.width,item._contentSize.height);row.addChild(n);}
|
|
}
|
|
return {panel,source,service,cc};
|
|
}
|
|
test('only currentLevel is highlighted; every passed layer has one full-row mask',()=>{
|
|
const {panel,source}=fixture();
|
|
for(const current of [1,2,3,4,5,6,2]){
|
|
source.currentLevel=current;panel.fillRewardRows(source);
|
|
for(let level=1;level<=5;level++){
|
|
const row=panel.rewardRow(level),bg=row.getChildByName('CurrentLevelBackground'),cover=row.getChildByName('PassedLevelMask');
|
|
assert.equal(bg.active,level===current);assert.equal(cover.active,level<current);
|
|
if(bg.active){assert.equal(bg.getComponent(Sprite).spriteFrame.name,`row-${6-level}`);assert.equal(bg.getComponent(Sprite).sizeMode,Sprite.SizeMode.CUSTOM);}
|
|
assert.equal(cover.width,bg.width);assert.equal(cover.height,bg.height);
|
|
assert.equal(cover.x,bg.x);assert.equal(cover.y,bg.y);
|
|
assert.deepEqual(cover.getComponent(Graphics).bounds,[-bg.width/2,-bg.height/2,bg.width,bg.height]);
|
|
const arrows=row.getChildByName('Arrows');assert.equal(row.children.at(-1),arrows);assert.equal(row.children.at(-2),cover);
|
|
panel.rewardSlots(row).forEach(slot=>assert.ok(row.children.indexOf(slot)<row.children.indexOf(cover)));
|
|
assert.equal(row.children.filter(n=>n.name==='PassedLevelMask').length,1);
|
|
}
|
|
}
|
|
});
|
|
test('only current up arrows move gently and old arrows stop being updated after advancing',()=>{
|
|
const {panel,source}=fixture();panel.fillRewardRows(source);
|
|
const first=panel.rewardRow(1).getChildByName('Arrows').getChildByName('Slot_1').getChildByName('Up');
|
|
const future=panel.rewardRow(2).getChildByName('Arrows').getChildByName('Slot_1').getChildByName('Up');
|
|
panel.update(.3);assert.equal(first.y,95);assert.equal(future.y,90);
|
|
source.currentLevel=2;panel.fillRewardRows(source);panel.update(.9);
|
|
assert.equal(panel.currentArrows.length,1);assert.equal(panel.currentArrows[0].y,85);
|
|
assert.equal(panel.rewardRow(1).getChildByName('Arrows').getChildByName('Slot_1').getChildByName('Up').y,90);
|
|
});
|
|
test('summit is free and disabled until level six, then claim; current draw button disappears',()=>{
|
|
const {panel,source}=fixture(),claim=panel.design.getChildByName('Free'),draw=panel.design.getChildByName('Action');
|
|
panel.render();assert.equal(claim.getComponent(Sprite).spriteFrame.name,'free');assert.equal(claim.getComponent(Button).interactable,false);assert.equal(draw.active,true);
|
|
source.currentLevel=6;source.canClaimFinal=true;panel.render();assert.equal(claim.getComponent(Sprite).spriteFrame.name,'claim');assert.equal(claim.getComponent(Button).interactable,true);assert.equal(draw.active,false);
|
|
source.completed=true;panel.render();assert.equal(claim.getComponent(Button).interactable,false);
|
|
});
|
|
test('claim button pulses both axes every two seconds and stops on completion',()=>{
|
|
const {panel,source}=fixture(),button=panel.design.getChildByName('Free');
|
|
panel.render();assert.equal(panel.claimPulseNode,null);
|
|
source.currentLevel=6;source.canClaimFinal=true;panel.render();
|
|
const scale=value=>{assert.ok(Math.abs(button.scaleX-value)<1e-8);assert.equal(button.scaleX,button.scaleY);};
|
|
scale(1);panel.update(.2);scale(.9);panel.update(.2);scale(1.1);panel.render();scale(1.1);panel.update(.2);scale(1);
|
|
panel.update(1.4);scale(1);panel.update(.2);scale(.9);
|
|
source.completed=true;panel.render();scale(1);assert.equal(panel.claimPulseNode,null);panel.update(.35);scale(1);
|
|
});
|
|
test('summit glow uses the requested artwork behind the cake and rotates',()=>{
|
|
const prefab=JSON.parse(fs.readFileSync('assets/pyramid/prefab/Pyramid.prefab'));
|
|
const glow=prefab.find(n=>n._name==='SummitGlow'),cake=prefab.find(n=>n._name==='SummitPrize');
|
|
const siblings=prefab[cake._parent.__id__]._children.map(ref=>prefab[ref.__id__]);
|
|
assert.ok(siblings.indexOf(glow)<siblings.indexOf(cake));
|
|
assert.deepEqual(glow._trs.array.slice(0,2),cake._trs.array.slice(0,2));
|
|
assert.equal(prefab[glow._components[0].__id__]._spriteFrame.__uuid__,JSON.parse(fs.readFileSync('assets/pyramid/art/glow.png.meta')).subMetas.glow.uuid);
|
|
const {panel}=fixture();panel.summitGlow=new Node('SummitGlow');panel.summitGlow.angle=0;panel.update(.5);assert.equal(panel.summitGlow.angle,-30);
|
|
});
|
|
test('coin quantity omits x, tools keep x, and probability uses the specified close sprite',async()=>{
|
|
const {panel}=fixture();panel.fillRewardRows(panel.state());
|
|
assert.equal(panel.rewardRow(1).getChildByName('Slot_1').getChildByName('Quantity').getComponent(Digits).value,'x3');
|
|
assert.equal(panel.rewardRow(1).getChildByName('Slot_4').getChildByName('Quantity').getComponent(Digits).value,3);
|
|
const prefab=JSON.parse(fs.readFileSync('assets/pyramid/prefab/Pyramid.prefab'));
|
|
const meta=JSON.parse(fs.readFileSync('assets/res/public/xiaoguanbi.png.meta'));
|
|
assert.equal(prefab.find(n=>n.probabilityCloseFrame).probabilityCloseFrame.__uuid__,meta.subMetas.xiaoguanbi.uuid);
|
|
panel.probabilityCloseFrame={name:'xiaoguanbi'};
|
|
panel.ensureProbabilityAssets=async()=>{};panel.continueHint=()=>{};panel.duration=()=>{};await panel.help();
|
|
const box=panel.probabilityModal.getChildByName('ProbabilityBox');
|
|
assert.equal(box.getChildByName('CloseProbability').getChildByName('Icon').getComponent(Sprite).spriteFrame,panel.probabilityCloseFrame);
|
|
assert.equal(box.getChildByName('Probability_0-3').getChildByName('Quantity').getComponent(Digits).value,3);
|
|
[0,1,2].forEach(i=>assert.equal(box.getChildByName(`Probability_0-${i}`).getChildByName('Prize').y,0));
|
|
assert.equal(box.getChildByName('Probability_0-3').getChildByName('Prize').y,15);
|
|
});
|
|
test('panel countdown uses days/hours at 24h, otherwise zero-padded HH:MM:SS',()=>{
|
|
const {panel,source}=fixture();
|
|
for(const [seconds,text] of [[90000,'1天1时'],[86400,'1天0时'],[86399,'23:59:59'],[3661,'01:01:01'],[59,'00:00:59'],[0,'00:00:00'],[-1,'00:00:00']]){
|
|
source.endAt=1000+seconds*1000;panel.renderClock();
|
|
const glyphs=panel.clockText.children;
|
|
assert.equal(glyphs.map(n=>n.getComponent(Sprite)?.spriteFrame.digit??n.getComponent(Label).string).join(''),text);
|
|
assert.ok(glyphs.filter(n=>n.getComponent(Label)).every(n=>!/[0-9]/.test(n.getComponent(Label).string)));
|
|
assert.ok(glyphs.filter(n=>n.getComponent(Sprite)).every(n=>n.getComponent(Sprite).sizeMode===Sprite.SizeMode.CUSTOM));
|
|
}
|
|
const prefab=JSON.parse(fs.readFileSync('assets/pyramid/prefab/Pyramid.prefab')),component=prefab.find(n=>n.countdownFrames);
|
|
const images=component.countdownFrames.map((ref,i)=>({ref,name:String(i),source:`assets/coin_madness/texture/countdown_digits/clock_${i}.png`}));
|
|
images.push({ref:component.countdownBackgroundFrame,name:'background',source:'assets/coin_madness/texture/ui/timer_bg.png'});
|
|
images.forEach(({ref,name,source})=>{
|
|
const local=`assets/pyramid/art/countdown/${name}.png`,meta=JSON.parse(fs.readFileSync(local+'.meta'));
|
|
const original=JSON.parse(fs.readFileSync(source+'.meta'));
|
|
assert.equal(ref.__uuid__,meta.subMetas[name].uuid);
|
|
assert.notEqual(meta.uuid,original.uuid);assert.notEqual(ref.__uuid__,Object.values(original.subMetas)[0].uuid);
|
|
assert.ok(fs.readFileSync(local).equals(fs.readFileSync(source)));
|
|
assert.ok(!JSON.stringify(prefab).includes(Object.values(original.subMetas)[0].uuid));
|
|
});
|
|
});
|
|
test('real digit renderer creates sized sprites and positions multi-digit amounts without overlap',()=>{
|
|
const {cc}=fixture(),context={exports:{},cc};
|
|
vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/pyramid/scripts/PyramidDigits.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017,experimentalDecorators:true}}).outputText,context);
|
|
const node=new Node('Amount'),digits=node.addComponent(context.exports.default);
|
|
const frames=Object.fromEntries(['3','2','0'].map(c=>[c,{getRect:()=>({width:33,height:37})}]));
|
|
digits.set('3200',frames,34,180);
|
|
assert.equal(node.children.length,4);
|
|
node.children.forEach((glyph,i)=>{
|
|
assert.equal(glyph.getComponent(Sprite).sizeMode,Sprite.SizeMode.CUSTOM);
|
|
assert.equal(glyph.height,34);assert.ok(glyph.width>0);
|
|
if(i)assert.ok(glyph.x-glyph.width/2>=node.children[i-1].x+node.children[i-1].width/2-1e-8);
|
|
});
|
|
});
|
|
test('minutes and hours keep matching digit height, unit size and spacing inside and outside tooltips',()=>{
|
|
const {panel,cc}=fixture(),context={exports:{},cc};
|
|
vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/pyramid/scripts/PyramidDigits.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017,experimentalDecorators:true}}).outputText,context);
|
|
const frames=Object.fromEntries(Array.from({length:10},(_,i)=>{
|
|
const meta=JSON.parse(fs.readFileSync(`assets/pyramid/art/digits/${i}.png.meta`)).subMetas[i];
|
|
return [i,{getRect:()=>({width:meta.width,height:meta.height})}];
|
|
}));
|
|
panel.number=(parent,value,x,y,height,width)=>{const n=new Node('SpriteNumber');parent.addChild(n);n.setPosition(x,y);n.addComponent(context.exports.default).set(value,frames,height,width);return n;};
|
|
for(const seconds of [60,900,1800,3540,3600,10800])for(const tooltip of [false,true]){
|
|
const parent=new Node('Parent');parent.setScale(tooltip?1:.9);
|
|
panel.duration(parent,seconds,0,0,125,34,62,33,tooltip?.9:1);
|
|
const duration=parent.getChildByName('Duration'),number=duration.getChildByName('SpriteNumber'),unit=duration.getChildByName(seconds>=3600?'Hours':'Minutes');
|
|
assert.equal(number.scaleX,1);assert.equal(number.height,34);assert.equal(unit.width,62);assert.equal(unit.height,33);
|
|
assert.equal(unit.getComponent(Sprite).sizeMode,Sprite.SizeMode.CUSTOM);
|
|
assert.ok(Math.abs((unit.x-unit.width/2)-(number.x+number.width/2)-2)<1e-8);
|
|
assert.equal(parent.scaleX*duration.scaleX,.9);
|
|
}
|
|
const prefab=JSON.parse(fs.readFileSync('assets/pyramid/prefab/Pyramid.prefab'));
|
|
const atlas=JSON.parse(fs.readFileSync('assets/common/publicFont.plist.meta'));
|
|
assert.equal(prefab.find(n=>n.minuteFrame).minuteFrame.__uuid__,atlas.subMetas['big_fenzhong.png'].uuid);
|
|
});
|