Files
MatchMaster/tools/preview-pyramid.cjs
T
2026-10-12 15:00:49 +08:00

81 lines
12 KiB
JavaScript

// Local-only preview: actual Cocos engine, editable prefab, source components and unchanged art.
// Never connects to production or invokes real payment. Sample rewards/weights are not published.
const fs=require('node:fs'),path=require('node:path'),http=require('node:http'),ts=require('typescript');
const root=path.resolve(__dirname,'..'),port=Number(process.argv[2]||8766);
const engine='C:/ProgramData/cocos/editors/Creator/2.4.15/resources/engine/bin/cocos2d-js-for-preview.js';
const manifest=require('./pyramid-art-manifest.json');
// Resolve editor-updated frame UUIDs (including the summit cake) from current metadata.
for(const name of [...Object.keys(manifest),'cake']){const file=path.join(root,'assets/pyramid/art',name+(name==='body'?'.jpg':'.png')+'.meta');if(!fs.existsSync(file))continue;
const meta=JSON.parse(fs.readFileSync(file,'utf8')),frame=Object.values(meta.subMetas||{})[0];if(frame)manifest[name]={...manifest[name],uuid:frame.uuid};}
const catalogPath=process.env.PYRAMID_CATALOG_PATH||path.resolve(root,'../server/laf-cloud/functions/pyramid/config/v3-catalog.json');
const catalog=fs.existsSync(catalogPath)?JSON.parse(fs.readFileSync(catalogPath,'utf8').replace(/^\uFEFF/,'')):null;
const transpile=file=>ts.transpileModule(fs.readFileSync(path.join(root,file),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017,experimentalDecorators:true}}).outputText;
const html=`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>金字塔 · Cocos 隔离预览</title><style>html,body{margin:0;background:#332724;color:#fff;font:14px sans-serif}#bar{min-height:42px;display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center}button,select{padding:6px}#game{height:calc(100vh - 170px);max-width:600px;margin:auto}</style></head><body><div id="bar">隔离演示 · 非正式配置<select id="state"><option>第一层</option><option>第二层</option><option>第三层</option><option>第三层已抽宝箱</option><option>第四层</option><option>第五层</option><option>第六层</option><option>支付未决</option><option>已结束</option></select><button id="reset">切换</button></div><div id="game"><canvas id="GameCanvas"></canvas></div><script>window.CC_EDITOR=false;window.CC_PREVIEW=true;window.CC_DEBUG=true;window.CC_BUILD=false;window.CC_TEST=false;</script><script src="/engine.js"></script><script src="/preview.js"></script></body></html>`;
const boot=`
window.previewErrors=[];addEventListener('error',e=>previewErrors.push(e.message));addEventListener('unhandledrejection',e=>previewErrors.push(String(e.reason)));
const art=${JSON.stringify(manifest)},catalog=${JSON.stringify(catalog)},frames={},listeners=[];let state,view;
const services={busy:false,deliveryBusy:false,error:'',lastDraw:null,data:()=>state,now:()=>Date.now(),subscribe:fn=>(listeners.push(fn),()=>{const i=listeners.indexOf(fn);if(i>=0)listeners.splice(i,1);}),
bundle:async()=>({load:(name,type,done)=>done(null,frames[name.replace(/^art\\//,'')])}),sync:async()=>{services.error='';emit();return true;},recordPriceView:async()=>{},
buy:async()=>{state.canPay=false;state.canDraw=true;state.drawCount=1;emit();return true;},
act:async action=>{if(action==='draw'){const level=state.currentLevel,r=state.levels[level-1].rewards.find(r=>!r.gain);r.gain=true;r.deliveryStatus='pending';state.drawCount=0;state.canDraw=false;state.pendingGrants=[{id:'pending'}];services.lastDraw={requestId:'preview-'+Date.now(),rewardId:r.rewardId,goods:r.goods,previousLevel:level};if(r.up)state.currentLevel++;}else{state.pendingGrants=[{id:'final'}];state.canClaimFinal=false;}state.revision++;emit();return true;},
deliver:async()=>{const final=state.pendingGrants.some(g=>g.id==='final');state.levels.forEach(l=>l.rewards.forEach(r=>{if(r.deliveryStatus==='pending'||final){r.gain=true;r.deliveryStatus='delivered';}else if(state.currentLevel===6){r.gain=true;r.deliveryStatus='unclaimed';}}));state.pendingGrants=[];state.completed=final;state.canPay=state.currentLevel<6;state.canClaimFinal=state.currentLevel===6&&!final;state.revision++;emit();return true;}};
function emit(){listeners.slice().forEach(fn=>fn());}
function fixture(name){const chestPositions=[3,3,1,-1,0,-1],chests=['chest-1','chest-2','chest-3',null,'chest-4',null],targets=[[0,1,2,3,4,5],[0,4],[0,3],[1],[1],[]];
const levels=Array.from({length:6},(_,i)=>({level:i+1,itemId:i<5?'pay_test':null,itemPrice:i<5?100:null,rewards:Array.from({length:i<5?6-i:1},(_,j)=>({rewardId:'l'+(i+1)+'r'+j,weight:i<5?Probability.PYRAMID_WEIGHTS[i][j]:null,goods:j===chestPositions[i]?[{id:1001,count:100},{id:2001,count:1}]:[{id:1001,count:100}],icon:j===chestPositions[i]?chests[i]:null,up:targets[i].includes(j),gain:false,deliveryStatus:'not_eligible'}))}));
const level=({'第二层':2,'第三层':3,'第三层已抽宝箱':3,'第四层':4,'第五层':5,'第六层':6})[name]||1;state={periodId:'preview',revision:Date.now(),startAt:Date.now()-10000,endAt:name==='已结束'?Date.now()-1:Date.now()+3*86400000+6*3600000,status:name==='已结束'?'ended':'active',currentLevel:level,drawCount:0,canPay:level<6&&name!=='支付未决'&&name!=='已结束',canDraw:false,canClaimFinal:level===6,completed:false,pendingOrder:name==='支付未决'?{orderId:'local'}:null,pendingGrants:[],levels};
if(catalog){const tier=Number(new URLSearchParams(location.search).get('tier'))||1,t=catalog.tiers.find(t=>t.tier_id===tier)||catalog.tiers[0];
const ids={coins:1001,freeze:2001,hammer:2002,wand:2003,infinite_minutes:2005};
state.levels=catalog.layers.map((l,i)=>({level:l.layer,itemId:i?'pyramid_preview':null,itemPrice:t.price_fen_by_layer[i],rewards:t.slots.filter(s=>s.layer===l.layer).map(s=>({rewardId:s.slot_id,weight:l.weights[s.position-1],up:l.advance_positions.includes(s.position),goods:Object.entries(s.reward).map(([key,count])=>({id:ids[key],count:count*(key==='infinite_minutes'?60:1)})),icon:s.chest?'chest-'+Math.min(4,l.layer-1):null,gain:false,deliveryStatus:'not_eligible'}))}));
state.levels.push({level:6,itemId:null,itemPrice:null,rewards:[]});state.manifestVersion='local-v3-preview';state.offerTier=t.tier_id;
if(level===1&&name!=='已结束'&&name!=='支付未决'){state.canPay=false;state.canDraw=true;state.drawCount=1;}
}
if(name==='第三层已抽宝箱'){state.levels[2].rewards[catalog?3:1].gain=true;state.levels[2].rewards[catalog?3:1].deliveryStatus='delivered';}
if(level===6)levels.forEach(l=>l.rewards.forEach(r=>{r.gain=true;r.deliveryStatus='unclaimed';}));emit();}
window.previewService=services;
function moduleCode(code,require,all=false){const exports={};new Function('exports','require',code)(exports,require);return all?exports:exports.default;}
const Digits=moduleCode(${JSON.stringify(transpile('assets/pyramid/scripts/PyramidDigits.ts'))},()=>{});
const Probability=moduleCode(${JSON.stringify(transpile('assets/pyramid/scripts/PyramidProbability.ts'))},()=>{},true);
const AnimationDebug=moduleCode(${JSON.stringify(transpile('assets/pyramid/scripts/PyramidAnimationDebug.ts'))},()=>Probability);
const NumberToImage=moduleCode(${JSON.stringify(transpile('assets/Script/NumberToImage.ts'))},()=>{});
const Reward=moduleCode(${JSON.stringify(transpile('assets/Script/Reward.ts'))},()=>({default:NumberToImage}));
cc.js._setClassId('f30d3ipM7dP6JubTtb9L2yZ',Reward);cc.js.setClassName('Reward',Reward);
const Panel=moduleCode(${JSON.stringify(transpile('assets/pyramid/scripts/PyramidPanel.ts'))},name=>name.includes('PyramidProbability')?Probability:({default:name.includes('PyramidAnimationDebug')?AnimationDebug:name.includes('PyramidDigits')?Digits:services}));
const prefabData=${fs.readFileSync(path.join(root,'assets/pyramid/prefab/Pyramid.prefab'),'utf8')};
cc.js._setClassId(prefabData.find(x=>x.__type__&&!x.__type__.startsWith('cc.')).__type__,Panel);
cc.assetManager.init({importBase:'/library',nativeBase:'/library'});
new Promise((resolve,reject)=>cc.assetManager.loadBundle('http://127.0.0.1:${port}/internal',(e)=>e?reject(e):resolve())).then(()=>cc.game.run({id:'GameCanvas',debugMode:1,frameRate:30,renderMode:0,showFPS:false,groupList:['default'],collisionMatrix:[[true]]},async()=>{
cc.view.resizeWithBrowserSize(true);cc.view.setDesignResolutionSize(1080,2000,cc.ResolutionPolicy.SHOW_ALL);
const loadAsset=uuid=>new Promise((resolve,reject)=>cc.assetManager.loadAny({uuid},(e,a)=>e?reject(e):resolve(a)));
const [rewardPrefab,publicFont,font]=await Promise.all(['42afc6d4-1976-49c6-9595-91e157447e82','48e7a52f-b3c2-4b03-9786-d8d6615850b8','fa9a438e-1f24-47fe-bbcd-b75abcff2ea8'].map(loadAsset));
NumberToImage.publicFont=publicFont;NumberToImage.font=font;
const getBundle=cc.assetManager.getBundle.bind(cc.assetManager);
cc.assetManager.getBundle=name=>name==='shop'?{load:(name,type,done)=>done(null,rewardPrefab)}:getBundle(name);
await Promise.all(Object.keys(art).map(name=>new Promise((resolve,reject)=>{
const ext=name==='body'?'.jpg':'.png';cc.assetManager.loadRemote('/art/'+name+ext,{ext},(e,texture)=>{if(e)return reject(e);frames[name]=new cc.SpriteFrame(texture);resolve();});
})));
const details=new cc.deserialize.Details(),prefab=cc.deserialize(prefabData,details);
for(let i=0;i<details.uuidList.length;i++){const uuid=details.uuidList[i],name=Object.keys(art).find(n=>art[n].uuid===uuid);details.uuidObjList[i][details.uuidPropList[i]]=name?frames[name]:cc.Material.getBuiltinMaterial('2d-sprite');}
const scene=new cc.Scene(),canvas=new cc.Node('Canvas');scene.addChild(canvas);canvas.setPosition(540,1000);const c=canvas.addComponent(cc.Canvas);c.designResolution=cc.size(1080,2000);c.fitWidth=true;c.fitHeight=true;
cc.fx={GameConfig:{GM_INFO:{uid:'local-preview'}}};
cc.director.runSceneImmediate(scene);fixture('第一层');const node=cc.instantiate(prefab);canvas.addChild(node);view=node.getComponent(Panel);await view.initialize(()=>{});window.pyramidPanel=view;
document.getElementById('reset').onclick=()=>{view.debugSession=null;fixture(document.getElementById('state').value);};
const play=document.createElement('button');play.textContent='播放抽奖动画';document.getElementById('bar').appendChild(play);
play.onclick=async()=>{play.disabled=true;play.textContent='动画播放中';try{await view.action();play.textContent='播放抽奖动画';}
catch(error){play.textContent=String(error.message||error);}finally{play.disabled=false;}};window.previewReady=true;
const dismiss=document.createElement('button');dismiss.textContent='关闭领奖';document.getElementById('bar').appendChild(dismiss);
dismiss.onclick=()=>{if(view.rewardAnimation)view.rewardAnimation.getComponent('Reward').onNodeClick();};
const help=document.createElement('button');help.textContent='查看概率';document.getElementById('bar').appendChild(help);help.onclick=()=>view.help();
}));`;
const internal=path.join(root,'build/wechatgame/assets/internal');
http.createServer((req,res)=>{const url=decodeURIComponent(new URL(req.url,'http://localhost').pathname);let file;
if(url==='/'){res.setHeader('Content-Type','text/html; charset=utf-8');return res.end(html);}
if(url==='/preview.js'){res.setHeader('Content-Type','text/javascript; charset=utf-8');return res.end(boot);}
if(url==='/engine.js')file=engine;
else if(url.startsWith('/art/'))file=path.resolve(root,'assets/pyramid/art',url.slice(5));
else if(url.startsWith('/library/'))file=path.resolve(root,'library/imports',url.slice(9));
else if(url.startsWith('/internal/')){let rel=url.slice(10);if(rel==='config.json')rel=fs.readdirSync(internal).find(n=>n.startsWith('config.')&&n.endsWith('.json'))||rel;file=path.resolve(internal,rel);}
if(!file||url.includes('..')||!fs.existsSync(file)||!fs.statSync(file).isFile()){res.writeHead(404);return res.end();}
res.setHeader('Content-Type',file.endsWith('.js')?'text/javascript':file.endsWith('.json')?'application/json':file.endsWith('.jpg')?'image/jpeg':'image/png');fs.createReadStream(file).pipe(res);
}).listen(port,'127.0.0.1',()=>console.log('Pyramid preview http://127.0.0.1:'+port));