import {buildPreparedRoutes} from './prepared-routes-build.mjs'; import {buildMusic} from './music-assets.mjs'; import {applyLayeredFeedback} from './layered-feedback-build.mjs'; import {buildAudio} from './audio-assets.mjs'; import {applyBrightClassroom} from './bright-classroom-build.mjs'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { build } from 'esbuild'; const root = path.dirname(fileURLToPath(import.meta.url)); buildAudio(root);buildMusic(root);buildPreparedRoutes(root); await build({ absWorkingDir: root, entryPoints: ['src/main.mjs'], outfile: 'game.js', bundle: true, format: 'iife', platform: 'neutral', target: 'es2019', minify: true, plugins: [{ name: 'wechat-host', setup(b) { b.onLoad({ filter: /upstream[\\/]ui\.mjs$/ }, args => { let contents = fs.readFileSync(args.path, 'utf8'); contents=contents.replace("from './audio.mjs'", "from '../audio.mjs'"); const originalRules=contents.split('\n').find(line=>line.startsWith('function rulesFor(')); if(!originalRules)throw Error('上游规则文案入口已变化'); contents=contents.replace(originalRules,'const rulesFor=playerRules;'); contents=contents.replace("const $=id=>document.getElementById(id),canvas=$('board'),ctx=canvas.getContext('2d');","const $=id=>document.getElementById(id);let canvas=$('board'),ctx=canvas.getContext('2d'),previewOnly=false;"); contents=contents.replace(" updateTutorial(bad);", " if(previewOnly)return;updateTutorial(bad);"); const hintHandler=contents.split('\n').find(line=>line.startsWith("$('hint').onclick=")); if(!hintHandler)throw Error('上游指路入口已变化'); contents=contents.replace(hintHandler, `$('hint').onclick=()=>{record.hints++;focus=[];const segment=segmentHints.request(level,solution,path);setStatus(segment?'沿引导带连接两端;走偏可撤销或重开,正确到达后提示消失。':'已经完成。');draw();};`); contents=contents.replace("function start(i,open=true){", "function start(i,open=true){segmentHints.clear();"); contents=contents.replace("if(level.tutorial&&!bad.length&&!cleared)focus=", "if(false)focus="); contents+=`\nexport function hintView(){const h=segmentHints.view(level,solution,path);return h?{...h,visitedPolygons:polygons.filter(p=>path.includes(p.id)).map(p=>p.p.map(v=>[...v]))}:null;}\n`; contents="import {createHintController} from '../hints.mjs';\nconst segmentHints=createHintController();\n"+contents; contents+=` export function renderLesson(target,l,route,w,h){ const saved={canvas,ctx,level,solution,path,polygons,centers,unit,focus,cleared,channelViews,cellViews}; try{canvas={clientWidth:w,clientHeight:h,get width(){return target.width},set width(v){target.width=v},get height(){return target.height},set height(v){target.height=v}};ctx=target.getContext('2d');level={...l,tutorial:null};solution=[];path=route;focus=[];cleared=false;previewOnly=true;draw();return {centers:Object.fromEntries(centers),channels:channelViews,cells:cellViews};} finally{({canvas,ctx,level,solution,path,polygons,centers,unit,focus,cleared,channelViews,cellViews}=saved);previewOnly=false;} }`; contents="import {rulesFor as playerRules} from '../copy.mjs';\n"+contents; const old = contents.split('\n').find(line => line.startsWith("$('export').onclick=")); if (!old) throw Error('上游导出入口变化,需要重新检查微信适配'); contents = contents.replace(old, `$('export').onclick=()=>{saveRecord();exportRecords({version:pack.version,exportedAt:new Date().toISOString(),clearedIds:completed,records});setStatus('已保存试玩记录。');};`); contents="import {RESUME_KEY,readResume,resumeTarget,validResumePath,writeResume} from '../resume.mjs';\n"+contents; const bootstrap='start(initial>=0?initial:0,false);'; if(!contents.includes(bootstrap))throw Error('Resume bootstrap seam changed'); contents=contents.replace(bootstrap,`const savedResume=readResume(localStorage),resume=resumeTarget(playLevels,completed,savedResume,initialId);start(resume.index,false);path=validResumePath(level,resume.path,canAppend);if(path.length===level.cells.length&&!violations(level,path).length)path=[];draw();showPlay();`); contents=contents.replace("window.addEventListener('pagehide',saveRecord);","window.addEventListener('pagehide',()=>{saveRecord();persistResume();});"); contents+=`\nfunction persistResume(){if(level)writeResume(localStorage,playLevels,completed,level,path,cleared);}\n`; contents=contents.replace('if(level.tutorial&&!completed.includes(level.id))','if(level.tutorial&&!completed.includes(level.id)&&!path.length)'); contents+=` export function resumeGameplay(){const saved=readResume(localStorage),target=resumeTarget(playLevels,completed,saved,level.id);if(level.id!==playLevels[target.index].id||cleared){start(target.index,false);path=validResumePath(level,target.path,canAppend);draw();}showPlay();} `; const solveSeam='solution=solve(level)[0]';if(!contents.includes(solveSeam))throw Error('Prepared-route seam changed');contents=contents.replace(solveSeam,'solution=[...prepared.routes[level.id]]'); const firstDraw='if(open)showPlay();draw();';if(!contents.includes(firstDraw))throw Error('Initial board draw seam changed');contents=contents.replace(firstDraw,'if(open)showPlay();else draw();'); contents="import prepared from '../prepared-routes.json';\n"+contents; contents=applyBrightClassroom(contents); contents=applyLayeredFeedback(contents); contents=contents.replace(' if(previewOnly)return;updateTutorial(bad);',' if(previewOnly)return;persistResume();updateTutorial(bad);'); return { contents: "import {PACK,document,window,localStorage,exportRecords,performance} from '../platform.mjs';\n" + contents, loader: 'js' }; }); } }] }); console.log('Built campaign-200-r2: 200 levels');