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,levelassert.ok(row.children.indexOf(slot)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){ 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); });