fix(gen): 模板起局鲁棒性 menuStartBtn 解耦 render + targets 键/起局约定进 prompt v1.6.1 + gen.mjs 入口守卫 (W-S1 修复单②)
T1 六款模板(_template + feiyi/puzzle/shop/story/trpg)起局/重开按钮命中矩形: 从 render 副作用回写(menuStartBtn = renderMenu(...).startBtn)改为抽纯几何 menuStartRect/overRestartRect + 在场景进入点 onEnter 确定性赋值、render 只画; 治并发 rAF 饥饿下 render 未跑过=命中矩形恒 null=卡 menu(归因案4)。 no-render 探针坐实:修复前六款均卡 menu、修复后均按坐标起局进 play。 T2 cheap-system.md 升 v1.6.1,红线段增两条硬约束:①点 target 玩法 state 键名 固定为 targets(不得改成 items 等,C5 驱动器族选择依赖)②起局命中矩形须在状态 进入点建立、不依赖渲染帧;各模板 _forensicsView 加同义契约注释。cheap_roles.py _SYSTEM_PROMPT 与 md 正文逐字节 parity 同步,registry.yaml 版本 bump 对齐。 T3 gen.mjs 顶层裸调 main() 加入口守卫(import.meta.url === pathToFileURL(argv[1])), 被 import 时不再触发真生成(footgun,曾 ~116s)、直接执行行为不变。 T4 puzzle-game-design.md §9 死链经核实在本 commit 不存在(唯一 play-spec 引用已 正确指向存在的 puzzle.play-spec.json),未伪造改动;littlejs-game-dev.md 补一条 起局 onEnter 指导闭合 doc-code。 验证:test_roles 12/12 + check_registry 17 对齐 + md↔_SYSTEM_PROMPT parity True + 6 模板 esbuild build 全绿 + amodel-gen tests 15+11 + 模板 node --test 除 _template-shop/headless-boot(既存红,与基线同)外全绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
96de98b345
commit
34923812c3
@ -139,6 +139,7 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
- ❌ `sceneFsm.defineScenes({...})` → ✅ `sceneFsm.define(name, {onEnter,onExit,update,render})` **逐个**定义(**没有** `defineScenes`)。
|
||||
- ❌ `sceneFsm.define({ menu:{...}, play:{...}, over:{...} })` 单对象批量注册 → ✅ **每个场景各调一次** `sceneFsm.define('menu', {...}); sceneFsm.define('play', {...}); sceneFsm.define('over', {...})`(`define(name, handlers)` 第一参恒为场景名**字符串**;传对象 → 第一参变成 `[object Object]`、menu/play/over 全没注册 → `transition('play')` 找不到目标态被忽略 → 卡菜单进不去 play。实测合成/2048 类翻车点)。可链式 `.define(...).define(...)`。**回调表里 `update(dt)` 只收 dt、`render(g)` 只收 g、与五法同形——别臆造场景名/state 首参(#15),绝不写 `update(scene, dt)`。**
|
||||
- ✅ **`hudUi.drawButton(g, rect, opts)` 回吐它绘制的命中矩形 `{x,y,w,h}`**(阶段一B #5 改签名):直觉写法就对——`const btn = hudUi.drawButton(g, {x,y,w,h}, {label}); if (hudUi.pointInRect(px,py,btn)) {...}`,drawButton 返回命中矩形、直接接住判命中即可(也可先单独定义 rect 再画再 pointInRect,两种都对)。命中判定 `hudUi.pointInRect(px, py, rect)`(签名 `(px, py, rect)`;**只在 hudUi,collision 插件没有 pointInRect 方法**)。另:**`drawButton` 第二参是 rect 对象 `{x,y,w,h}`、不是 label/坐标位置参**——别写 `drawButton(g, '开始', x, y, w, h)`。
|
||||
- ✅ **起局/交互按钮的命中矩形在场景 `onEnter`(状态进入点)确定性建立、`render` 只画不写**——绝不让它只在 `render` 副作用里生成(`menuStartBtn = renderMenu(...)` 那种回写):并发渲染帧饥饿时 `render` 可能一次都没跑过 → 命中矩形恒 `null` → 菜单点不动、一局起不来(卡 menu 头号成因)。把按钮几何抽成纯函数,`onEnter` 调它设命中区、`render` 调它画,命中与绘制同源不漂移(`_template*` 六款范例的 `menuStartRect/overRestartRect` 即此写法)。
|
||||
- ❌ scene 的 update 回调臆造首参写成 `update(running, dt)` → ✅ **scene 的 `update` 回调只收一个参数 `(dt)`**(`sceneFsm.define(name, { update(dt){…} })`;`sceneFsm.update(dt)` 只透传 dt,没有 running 之类首参)。写错首参 → 真正的 dt 落到第二参=undefined → `state.elapsedMs += undefined = NaN` → 不再 spawn/计时、计时显示「NaNs」、游戏空转看着像没 bug 实则永不推进(实测 wanglanmei-v2:菜单能进 play 但无顾客、营业额恒 0)。**任何回调签名都以 `src/plugins/<name>/api.d.ts` 为准,别臆造参数。**
|
||||
- ❌ `update(dt)` 把 dt 当毫秒直接累加 `elapsedMs += dt` → ✅ **dt 单位是「秒」(≈1/60)**;游戏内毫秒计时(倒计时/spawn 间隔/耐心,常量如 60000/1300/4800)累加要 **`elapsedMs += dt * 1000`**。漏 ×1000 → 计时慢 1000 倍 → 永不到 spawn 阈值=零顾客、一局永不结束(实测 wanglanmei-v2 翻车点;`_template` 范例里 `remainMs - dt*1000` 就是这个换算)。
|
||||
- ❌ `_forensicsView()` 在**外层**先算好 phase/targets 再闭包返回(`const phase=...; return { state(){ return { phase } } }`)→ ✅ **phase/targets/score 必须在 `state()` 函数体内部实时计算**(host 只在 boot 调一次 `_forensicsView()`,外层算的值定格成 boot 快照=永远 menu、targets 永远空 → 自动验收/驱动看不到目标、永判死菜单。实测 wanglanmei-v2:游戏真在 play 但门读到的 phase 恒 menu)。照 `_template/src/game-logic.js` 的 `state: () => ({ phase: sceneFsm.current(), ... })` 写。
|
||||
|
||||
@ -79,11 +79,11 @@ A 读手册(1 + 按品类 5/6/7/8/9)+ 读你的起点 game-logic.js(4)
|
||||
- **插件扁平注入:直接 const { sceneFsm, sessionScore, hudUi, timerScheduler } = plugins(用到才解构)**;**绝不写 opts.runtime.plugins**(那是 L1 内部的,你见不到 → 写了就 undefined → boot 崩 reading 'define')。游戏只用插件、不 new、不 register。
|
||||
- **可用插件键(全 11 已注入,用到才取)**:sceneFsm / sessionScore / hudUi / timerScheduler / save / gamefeel / juice / palettePost / audioMusic / collision / physics。
|
||||
- **插件方法名一律以 api.d.ts 为准、别臆测**(实测幻觉:scene-fsm 写 defineScenes〔应 define〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ 实测易犯的幻觉 API」逐条列 ❌→✅。
|
||||
- **hudUi.drawButton(g, rect, opts) 回吐它绘制的命中矩形 { x, y, w, h }**:✅ 直觉写法就对——const btn = hudUi.drawButton(g, { x, y, w, h }, { label }); if (hudUi.pointInRect(px, py, btn)) 命中处理(drawButton 返回命中矩形,直接接住判命中即可)。命中判定走 hudUi.pointInRect(px, py, rect)(**只在 hudUi,collision 插件没有 pointInRect 方法**)。**第二参是 rect 对象 { x, y, w, h }、不是 label/坐标位置参**——别写 drawButton(g, '开始', x, y, w, h) 这种位置参形态。
|
||||
- **hudUi.drawButton(g, rect, opts) 回吐它绘制的命中矩形 { x, y, w, h }**:✅ 直觉写法就对——const btn = hudUi.drawButton(g, { x, y, w, h }, { label }); if (hudUi.pointInRect(px, py, btn)) 命中处理(drawButton 返回命中矩形,直接接住判命中即可)。命中判定走 hudUi.pointInRect(px, py, rect)(**只在 hudUi,collision 插件没有 pointInRect 方法**)。**第二参是 rect 对象 { x, y, w, h }、不是 label/坐标位置参**——别写 drawButton(g, '开始', x, y, w, h) 这种位置参形态。**起局/交互按钮的命中矩形必须在状态进入点(scene onEnter)确定性建立、render 只画不赋值**:绝不让命中矩形只在 render 副作用里生成——并发渲染帧饥饿下 render 可能没跑过一次=矩形恒 null=菜单点不动、一局起不来;把按钮几何抽成纯函数,onEnter 调它设命中区、render 调它画,同一来源不漂移。
|
||||
- **sceneFsm.define(name, handlers) 逐场景调一次——绝不单对象批量注册**:❌ sceneFsm.define({ menu: {...}, play: {...}, over: {...} }) → 第一参当成了场景名「[object Object]」、menu/play/over 全没真注册 → 后续 transition('play') 找不到目标态被忽略 → 卡菜单(实测合成/2048 类翻车点)。✅ **每个场景各调一次**:sceneFsm.define('menu', { onEnter(){...} }); sceneFsm.define('play', { onEnter(){...}, update(dt){...} }); sceneFsm.define('over', { onEnter(){...} });(define 第一参恒为场景名字符串、第二参才是回调表 { onEnter, onExit, update, render };**回调表里 update 只收 (dt)、render 只收 (g)、与五法同形——别臆造场景名/state 首参,绝不写 update(scene, dt)**;可链式 .define(...).define(...))。
|
||||
- **画面读 viewport.w/h**(viewport 是入参;别硬编码 390×844)。
|
||||
- **美术资产(若 assets/manifest.json 非空)**:先 read_file('assets/manifest.json') 看有哪些资产;init(boot) 里 const assets = boot.assets || {};render 里 const img = assets['ref名']?.image; if (img) g.drawImage(img, x, y, w, h); else 程序化回退。**ref = manifest 条目 file 去扩展名**(shopkeeper-idle.jpg → 'shopkeeper-idle')。缺图/加载失败时 assets[ref].image 为 null(host 已容错)→ 务必走程序化回退、别崩。BGM/音效经 plugins.audioMusic(不在 boot.assets)。无 manifest/无美术 → 全程序化绘制即可。
|
||||
- **必须实现 _forensicsView()**(可测性红线 + 自动驱动契约):返回 { state(){…}, measures(){…} };state() 至少 { phase, score }。**若游戏靠点击推进(经营点客/打地鼠/点离散目标等):state().targets:[{x,y,occupied}] 列当前屏上目标,可点的(等待的客/冒头的鼠)置 occupied:true、不可点置 false** —— 自动验收据此点 occupied:true 的目标真驱动你的游戏。**score 用累计进展(营收/得分),随推进上升** —— 自动验收据 score 上升判"真有进展"。不写则验证读不到状态(check 会拦)。**钉死:phase/targets/score 必须在 state() 函数体内部实时读取/计算——绝不在 _forensicsView() 外层先算好再闭包返回**(host 只在 boot 调一次 _forensicsView,外层算的值会被定格成 boot 快照=永远 menu、targets 永远空 → 自动验收看不到顾客、永判死菜单)。
|
||||
- **必须实现 _forensicsView()**(可测性红线 + 自动驱动契约):返回 { state(){…}, measures(){…} };state() 至少 { phase, score }。**若游戏靠点击推进(经营点客/打地鼠/点离散目标等):state().targets:[{x,y,occupied}] 列当前屏上目标,可点的(等待的客/冒头的鼠)置 occupied:true、不可点置 false** —— 自动验收据此点 occupied:true 的目标真驱动你的游戏。**键名固定为 `targets`,不得改成 items/objects 等**:判卷驱动器靠 state 有无 `targets` 键选驱动器族(点目标族 vs 循环按键族),改名=选错族=盲驱动器驱动不了你的游戏=H 进展门挂。**score 用累计进展(营收/得分),随推进上升** —— 自动验收据 score 上升判"真有进展"。不写则验证读不到状态(check 会拦)。**钉死:phase/targets/score 必须在 state() 函数体内部实时读取/计算——绝不在 _forensicsView() 外层先算好再闭包返回**(host 只在 boot 调一次 _forensicsView,外层算的值会被定格成 boot 快照=永远 menu、targets 永远空 → 自动验收看不到顾客、永判死菜单)。
|
||||
- **关键事件用 ctx.log 记日志**:在 init/场景切换/得分/出错处 调 ctx.log('tag', 信息)。**ctx 来自 init 的入参 boot,即「const ctx = boot.ctx」——绝不是 boot.boot.ctx**(host 传入的 boot 已是 {ctx, mainContext, canvas, seed, assets},多套一层 .boot=undefined → ctx=null → 无输入、游戏点不动;check 会拦 .boot.ctx)。插件调用已自动记,你只补游戏语义事件。
|
||||
- **输入契约(钉死·根治"启动不了")**:输入订阅已由 L1(game.js wrapper)接管,**你绝不自己订阅输入**——不写 ctx.getInput、不自建 pendingClicks 队列、不在 update(dt) 里挑时机消费点击。你只写实例方法 **handleTap(x, y)**(点击主输入:经营点客 / 打地鼠 / 点按钮等;**menu/玩中/结算各 phase 的判定全写在它内部**),L1 会在每次 pointerdown 时**直达**调用它;键盘玩法(方向键 / 空格)写 **handleKey(key)**,L1 在 keydown 时调用。check 会拦「调了 getInput」与「一个输入方法都没暴露」。
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
id: config.cheap-system
|
||||
version: 1.6.0
|
||||
version: 1.6.1
|
||||
---
|
||||
|
||||
你是 A-model 游戏生成 agent。目标产物 = 一款**能跑能玩、有内容、不易同质化的高质小游戏**(多文件 LittleJS),落在 ⟦G⟧/。
|
||||
@ -49,11 +49,11 @@ A 读手册(1 + 按品类 5/6/7/8/9)+ 读你的起点 game-logic.js(4)
|
||||
- **插件扁平注入:直接 const { sceneFsm, sessionScore, hudUi, timerScheduler } = plugins(用到才解构)**;**绝不写 opts.runtime.plugins**(那是 L1 内部的,你见不到 → 写了就 undefined → boot 崩 reading 'define')。游戏只用插件、不 new、不 register。
|
||||
- **可用插件键(全 11 已注入,用到才取)**:sceneFsm / sessionScore / hudUi / timerScheduler / save / gamefeel / juice / palettePost / audioMusic / collision / physics。
|
||||
- **插件方法名一律以 api.d.ts 为准、别臆测**(实测幻觉:scene-fsm 写 defineScenes〔应 define〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ 实测易犯的幻觉 API」逐条列 ❌→✅。
|
||||
- **hudUi.drawButton(g, rect, opts) 回吐它绘制的命中矩形 { x, y, w, h }**:✅ 直觉写法就对——const btn = hudUi.drawButton(g, { x, y, w, h }, { label }); if (hudUi.pointInRect(px, py, btn)) 命中处理(drawButton 返回命中矩形,直接接住判命中即可)。命中判定走 hudUi.pointInRect(px, py, rect)(**只在 hudUi,collision 插件没有 pointInRect 方法**)。**第二参是 rect 对象 { x, y, w, h }、不是 label/坐标位置参**——别写 drawButton(g, '开始', x, y, w, h) 这种位置参形态。
|
||||
- **hudUi.drawButton(g, rect, opts) 回吐它绘制的命中矩形 { x, y, w, h }**:✅ 直觉写法就对——const btn = hudUi.drawButton(g, { x, y, w, h }, { label }); if (hudUi.pointInRect(px, py, btn)) 命中处理(drawButton 返回命中矩形,直接接住判命中即可)。命中判定走 hudUi.pointInRect(px, py, rect)(**只在 hudUi,collision 插件没有 pointInRect 方法**)。**第二参是 rect 对象 { x, y, w, h }、不是 label/坐标位置参**——别写 drawButton(g, '开始', x, y, w, h) 这种位置参形态。**起局/交互按钮的命中矩形必须在状态进入点(scene onEnter)确定性建立、render 只画不赋值**:绝不让命中矩形只在 render 副作用里生成——并发渲染帧饥饿下 render 可能没跑过一次=矩形恒 null=菜单点不动、一局起不来;把按钮几何抽成纯函数,onEnter 调它设命中区、render 调它画,同一来源不漂移。
|
||||
- **sceneFsm.define(name, handlers) 逐场景调一次——绝不单对象批量注册**:❌ sceneFsm.define({ menu: {...}, play: {...}, over: {...} }) → 第一参当成了场景名「[object Object]」、menu/play/over 全没真注册 → 后续 transition('play') 找不到目标态被忽略 → 卡菜单(实测合成/2048 类翻车点)。✅ **每个场景各调一次**:sceneFsm.define('menu', { onEnter(){...} }); sceneFsm.define('play', { onEnter(){...}, update(dt){...} }); sceneFsm.define('over', { onEnter(){...} });(define 第一参恒为场景名字符串、第二参才是回调表 { onEnter, onExit, update, render };**回调表里 update 只收 (dt)、render 只收 (g)、与五法同形——别臆造场景名/state 首参,绝不写 update(scene, dt)**;可链式 .define(...).define(...))。
|
||||
- **画面读 viewport.w/h**(viewport 是入参;别硬编码 390×844)。
|
||||
- **美术资产(若 assets/manifest.json 非空)**:先 read_file('assets/manifest.json') 看有哪些资产;init(boot) 里 const assets = boot.assets || {};render 里 const img = assets['ref名']?.image; if (img) g.drawImage(img, x, y, w, h); else 程序化回退。**ref = manifest 条目 file 去扩展名**(shopkeeper-idle.jpg → 'shopkeeper-idle')。缺图/加载失败时 assets[ref].image 为 null(host 已容错)→ 务必走程序化回退、别崩。BGM/音效经 plugins.audioMusic(不在 boot.assets)。无 manifest/无美术 → 全程序化绘制即可。
|
||||
- **必须实现 _forensicsView()**(可测性红线 + 自动驱动契约):返回 { state(){…}, measures(){…} };state() 至少 { phase, score }。**若游戏靠点击推进(经营点客/打地鼠/点离散目标等):state().targets:[{x,y,occupied}] 列当前屏上目标,可点的(等待的客/冒头的鼠)置 occupied:true、不可点置 false** —— 自动验收据此点 occupied:true 的目标真驱动你的游戏。**score 用累计进展(营收/得分),随推进上升** —— 自动验收据 score 上升判"真有进展"。不写则验证读不到状态(check 会拦)。**钉死:phase/targets/score 必须在 state() 函数体内部实时读取/计算——绝不在 _forensicsView() 外层先算好再闭包返回**(host 只在 boot 调一次 _forensicsView,外层算的值会被定格成 boot 快照=永远 menu、targets 永远空 → 自动验收看不到顾客、永判死菜单)。
|
||||
- **必须实现 _forensicsView()**(可测性红线 + 自动驱动契约):返回 { state(){…}, measures(){…} };state() 至少 { phase, score }。**若游戏靠点击推进(经营点客/打地鼠/点离散目标等):state().targets:[{x,y,occupied}] 列当前屏上目标,可点的(等待的客/冒头的鼠)置 occupied:true、不可点置 false** —— 自动验收据此点 occupied:true 的目标真驱动你的游戏。**键名固定为 `targets`,不得改成 items/objects 等**:判卷驱动器靠 state 有无 `targets` 键选驱动器族(点目标族 vs 循环按键族),改名=选错族=盲驱动器驱动不了你的游戏=H 进展门挂。**score 用累计进展(营收/得分),随推进上升** —— 自动验收据 score 上升判"真有进展"。不写则验证读不到状态(check 会拦)。**钉死:phase/targets/score 必须在 state() 函数体内部实时读取/计算——绝不在 _forensicsView() 外层先算好再闭包返回**(host 只在 boot 调一次 _forensicsView,外层算的值会被定格成 boot 快照=永远 menu、targets 永远空 → 自动验收看不到顾客、永判死菜单)。
|
||||
- **关键事件用 ctx.log 记日志**:在 init/场景切换/得分/出错处 调 ctx.log('tag', 信息)。**ctx 来自 init 的入参 boot,即「const ctx = boot.ctx」——绝不是 boot.boot.ctx**(host 传入的 boot 已是 {ctx, mainContext, canvas, seed, assets},多套一层 .boot=undefined → ctx=null → 无输入、游戏点不动;check 会拦 .boot.ctx)。插件调用已自动记,你只补游戏语义事件。
|
||||
- **输入契约(钉死·根治"启动不了")**:输入订阅已由 L1(game.js wrapper)接管,**你绝不自己订阅输入**——不写 ctx.getInput、不自建 pendingClicks 队列、不在 update(dt) 里挑时机消费点击。你只写实例方法 **handleTap(x, y)**(点击主输入:经营点客 / 打地鼠 / 点按钮等;**menu/玩中/结算各 phase 的判定全写在它内部**),L1 会在每次 pointerdown 时**直达**调用它;键盘玩法(方向键 / 空格)写 **handleKey(key)**,L1 在 keydown 时调用。check 会拦「调了 getInput」与「一个输入方法都没暴露」。
|
||||
|
||||
|
||||
@ -121,7 +121,7 @@ prompts:
|
||||
# 首版正文 = _SYSTEM_PROMPT 内置原文逐字节对齐(含 ⟦G⟧/⟦SCAFFOLD_DESC⟧ 占位符),默认行为字节不变;
|
||||
# 调一条 prompt = 改 04-config/cheap-system.md 正文 + 升 version,下次生成自动生效。
|
||||
- id: config.cheap-system
|
||||
version: 1.6.0 # v1.6.0:打磨不降档(创始人 07-04 定调「档位切 scope 不切 polish」)——删 spike「能跑能玩即可」残留、hitstop 出禁项改结算标配、设计块强制落盘+finish 前收尾自查、新增 ⑩ 难度曲线、⑨ 经营决策模式指针。v1.5.0:策划知识包 v2——⑨ 否决项加判定句式+内嵌正反例;8/9 条计数口径统一;prompt.mjs 冻结为 A/B 史料(不再双源同改)。v1.4.0:W-GENRE 品类路由并集(剧情+6/TRPG+7/非遗+8/解谜+9)
|
||||
version: 1.6.1 # v1.6.1:起局鲁棒性 + targets 键契约进红线——①起局/交互按钮命中矩形必须在状态进入点 onEnter 确定性建立、render 只画(治并发 rAF 饥饿下卡 menu)②点目标玩法 state 键名固定 `targets`(驱动器契约 C5 依赖,改名=盲驱动器选错族=H 门挂)。v1.6.0:打磨不降档(创始人 07-04 定调「档位切 scope 不切 polish」)——删 spike「能跑能玩即可」残留、hitstop 出禁项改结算标配、设计块强制落盘+finish 前收尾自查、新增 ⑩ 难度曲线、⑨ 经营决策模式指针。v1.5.0:策划知识包 v2——⑨ 否决项加判定句式+内嵌正反例;8/9 条计数口径统一;prompt.mjs 冻结为 A/B 史料(不再双源同改)。v1.4.0:W-GENRE 品类路由并集(剧情+6/TRPG+7/非遗+8/解谜+9)
|
||||
stage: "04-config"
|
||||
owner: WS2
|
||||
file: 04-config/cheap-system.md
|
||||
|
||||
@ -34,7 +34,7 @@ import {
|
||||
isStepTappable, hitStation, windowRatio, stepPoints,
|
||||
} from './core.js';
|
||||
// 场景级绘制(画在 boot.mainContext);换观感改 render.js。
|
||||
import { renderMenu, renderPlay, renderOver } from './render.js';
|
||||
import { renderMenu, renderPlay, renderOver, menuStartRect, overRestartRect } from './render.js';
|
||||
// 资产装载(本骨架无外部美术,容错返空集;要加美术放 assets/manifest.json 再读 boot.assets)。
|
||||
import { loadAssets } from './assets.js';
|
||||
|
||||
@ -71,7 +71,8 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
let redos = 0; // 回炉次数(过窗)
|
||||
let roundTimer = null; // 60s 开工倒计时句柄
|
||||
|
||||
/* ── 命中区(菜单/结算按钮):render 回调实时算并写回(所见即所点)── */
|
||||
/* ── 命中区(菜单/结算按钮):**在场景进入点 onEnter 确定性赋值**(见 defineScenes),render 只读不写。
|
||||
* 绝不只在 render 副作用里生成——并发 rAF 饥饿下 render 没跑过=矩形恒 null=菜单点不动、一局起不来。── */
|
||||
let menuStartBtn = null;
|
||||
let overRestartBtn = null;
|
||||
|
||||
@ -190,8 +191,8 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
/** 注册三场景到 scene-fsm(每场景各调一次 define;回调 render 只收 g、update 只收 dt)。 */
|
||||
function defineScenes() {
|
||||
sceneFsm.define(SCENE_MENU, {
|
||||
onEnter() { floats = []; menuStartBtn = null; },
|
||||
render(g) { menuStartBtn = renderMenu(g, hudUi, { vw: VW, vh: VH }).startBtn; },
|
||||
onEnter() { floats = []; menuStartBtn = menuStartRect({ vw: VW, vh: VH }); }, // 进入 menu 即定命中区(不等 render)
|
||||
render(g) { renderMenu(g, hudUi, { vw: VW, vh: VH }); }, // render 只画,不写命中矩形
|
||||
});
|
||||
sceneFsm.define(SCENE_PLAY, {
|
||||
render(g) {
|
||||
@ -203,11 +204,14 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
},
|
||||
});
|
||||
sceneFsm.define(SCENE_OVER, {
|
||||
onEnter() { if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; } },
|
||||
onEnter() {
|
||||
if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; }
|
||||
overRestartBtn = overRestartRect({ vw: VW, vh: VH }); // 进入 over 即定命中区
|
||||
},
|
||||
render(g) {
|
||||
overRestartBtn = renderOver(g, hudUi, {
|
||||
renderOver(g, hudUi, { // render 只画,不写命中矩形
|
||||
vw: VW, vh: VH, wage, worksDone, redos, best: sessionScore.getBest(),
|
||||
}).restartBtn;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -254,6 +258,7 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
/**
|
||||
* 取证视图【自动验收契约·改坏=九门挂】:state() 内**实时**算(绝不外层先算好闭包返回)。
|
||||
* · targets:每工位一项,occupied=true 表示「当前就绪、窗口内可点」的工位——自动验收只点 occupied:true 的工位。
|
||||
* **键名固定为 `targets`,不得改成 items/objects 等**:判卷驱动器(C5)靠 state 有无 `targets` 键选驱动器族,改名=选错族=盲驱动点不到=H 门挂。
|
||||
* · score:= 工钱,随完成工序单调上升——自动验收据它判「真有进展」。
|
||||
* 加机制时这两个键的语义务必保持(targets 列可点目标、score 随进展升)。
|
||||
*/
|
||||
|
||||
@ -144,18 +144,31 @@ function drawFloats(g, floats) {
|
||||
|
||||
/* ────────── 场景绘制入口(导出给 game-logic 的 defineScenes 用)────────── */
|
||||
|
||||
/**
|
||||
* 起局/重开按钮的命中矩形(纯几何,零副作用):game-logic 在**状态进入点 onEnter** 据此确定性建命中区,
|
||||
* renderMenu/renderOver 据此画——同一来源,命中与绘制永不漂移。**绝不把命中矩形只在 render 副作用里
|
||||
* 生成**:并发渲染帧饥饿下 render 可能尚未跑过一次,命中矩形就恒为 null → 菜单点不动、一局起不来(卡 menu 案4)。
|
||||
*/
|
||||
export function menuStartRect(view) {
|
||||
const w = 240, h = 60;
|
||||
return { x: view.vw / 2 - w / 2, y: view.vh * 0.58 - h / 2, w, h };
|
||||
}
|
||||
export function overRestartRect(view) {
|
||||
const w = 240, h = 60;
|
||||
return { x: view.vw / 2 - w / 2, y: view.vh * 0.66 - h / 2, w, h };
|
||||
}
|
||||
|
||||
/**
|
||||
* menu:匾额 + 标题 + 开工按钮。换坊名/副标在这里改。
|
||||
* @returns {{startBtn:{x,y,w,h}}} 命中区(drawButton 返回的归一化矩形)。
|
||||
* @returns {{startBtn:{x,y,w,h}}} 命中区(与 menuStartRect 同源)。
|
||||
*/
|
||||
export function renderMenu(g, hud, view) {
|
||||
drawBackdrop(g, view.vw, view.vh);
|
||||
g.fillStyle = 'rgba(18,12,6,0.55)'; g.fillRect(0, 0, view.vw, view.vh);
|
||||
hud.drawText(g, '老陶坊', view.vw / 2, view.vh * 0.30, { font: '800 46px sans-serif', color: C.text, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '按工序点亮就绪的工位,火候别过——60 秒出最多成品!', view.vw / 2, view.vh * 0.30 + 60, { font: '15px sans-serif', color: C.textDim, align: 'center', baseline: 'middle' });
|
||||
const w = 240, h = 60;
|
||||
// drawButton 返回它绘制的命中矩形(#5),直接接住当命中区——别再自己另算坐标。
|
||||
const startBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.58 - h / 2, w, h },
|
||||
// 命中矩形由纯几何 menuStartRect 给出(onEnter 已据它建命中区),这里画在同一矩形上——所见即所点。
|
||||
const startBtn = hud.drawButton(g, menuStartRect(view),
|
||||
{ label: '开工', font: '700 22px sans-serif', fill: C.accent, textColor: '#241a10', radius: 30 });
|
||||
return { startBtn };
|
||||
}
|
||||
@ -201,8 +214,7 @@ export function renderOver(g, hud, view) {
|
||||
hud.drawText(g, '出炉:' + names, view.vw / 2, view.vh * 0.40, { font: '600 18px sans-serif', color: C.done, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '成品 ' + (view.worksDone ? view.worksDone.length : 0) + ' 回炉 ' + view.redos, view.vw / 2, view.vh * 0.46, { font: '600 18px sans-serif', color: C.text, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '最佳工钱 ' + view.best, view.vw / 2, view.vh * 0.46 + 30, { font: '600 16px sans-serif', color: C.accent, align: 'center', baseline: 'middle' });
|
||||
const w = 240, h = 60;
|
||||
const restartBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.66 - h / 2, w, h },
|
||||
const restartBtn = hud.drawButton(g, overRestartRect(view),
|
||||
{ label: '再开一天', font: '700 22px sans-serif', fill: C.accent, textColor: '#241a10', radius: 30 });
|
||||
return { restartBtn };
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ import {
|
||||
generateBoard, remainingMatches, hitTile, matchesRule, hitScore, levelsTotal, levelBonus,
|
||||
} from './core.js';
|
||||
// 场景级绘制(画在 boot.mainContext);换观感改 render.js。
|
||||
import { renderMenu, renderPlay, renderOver } from './render.js';
|
||||
import { renderMenu, renderPlay, renderOver, menuStartRect, overRestartRect } from './render.js';
|
||||
// 资产装载(本骨架无外部美术,容错返空集;要加美术放 assets/manifest.json 再读 boot.assets)。
|
||||
import { loadAssets } from './assets.js';
|
||||
|
||||
@ -74,7 +74,8 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
let roundTimer = null; // 一局限时句柄
|
||||
let won = false; // 终局是否通关胜利
|
||||
|
||||
/* ── 命中区(菜单/结算按钮):render 回调实时算并写回(所见即所点)── */
|
||||
/* ── 命中区(菜单/结算按钮):**在场景进入点 onEnter 确定性赋值**(见 defineScenes),render 只读不写。
|
||||
* 绝不只在 render 副作用里生成——并发 rAF 饥饿下 render 没跑过=矩形恒 null=菜单点不动、一局起不来。── */
|
||||
let menuStartBtn = null;
|
||||
let overRestartBtn = null;
|
||||
|
||||
@ -213,8 +214,8 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
/** 注册三场景到 scene-fsm(每场景各调一次 define;回调 render 只收 g、update 只收 dt)。 */
|
||||
function defineScenes() {
|
||||
sceneFsm.define(SCENE_MENU, {
|
||||
onEnter() { board = null; floats = []; menuStartBtn = null; },
|
||||
render(g) { menuStartBtn = renderMenu(g, hudUi, { vw: VW, vh: VH }).startBtn; },
|
||||
onEnter() { board = null; floats = []; menuStartBtn = menuStartRect({ vw: VW, vh: VH }); }, // 进入 menu 即定命中区(不等 render)
|
||||
render(g) { renderMenu(g, hudUi, { vw: VW, vh: VH }); }, // render 只画,不写命中矩形
|
||||
});
|
||||
sceneFsm.define(SCENE_PLAY, {
|
||||
render(g) {
|
||||
@ -232,13 +233,16 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
},
|
||||
});
|
||||
sceneFsm.define(SCENE_OVER, {
|
||||
onEnter() { if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; } },
|
||||
onEnter() {
|
||||
if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; }
|
||||
overRestartBtn = overRestartRect({ vw: VW, vh: VH }); // 进入 over 即定命中区
|
||||
},
|
||||
render(g) {
|
||||
overRestartBtn = renderOver(g, hudUi, {
|
||||
renderOver(g, hudUi, { // render 只画,不写命中矩形
|
||||
vw: VW, vh: VH, won,
|
||||
score: sessionScore.getScore(), levelsCleared, bestStreak,
|
||||
best: sessionScore.getBest(),
|
||||
}).restartBtn;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -292,6 +296,7 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
* 取证视图【自动验收契约·改坏=九门挂】:state() 内**实时**算(绝不外层先算好闭包返回)。
|
||||
* · targets:每格一项,**occupied=true 只标「当前该点的正确解」**(solve 阶段、未清、符合本关规则、非锁定)——
|
||||
* 解谜品类的取证面兼职"求解器",自动验收只点 occupied:true 的格、驱动解谜推进。
|
||||
* **键名固定为 `targets`,不得改成 items/objects 等**:判卷驱动器(C5)靠 state 有无 `targets` 键选驱动器族,改名=选错族=盲驱动点不到=H 门挂。
|
||||
* · score:随点对/过关单调上升——自动验收据它判「真有进展」。
|
||||
* 加机制换主题都别动这两个键的语义(干扰项/已清格/观察窗内一律 occupied:false)。
|
||||
*/
|
||||
|
||||
@ -152,9 +152,23 @@ function drawFloats(g, floats) {
|
||||
|
||||
/* ────────── 场景绘制入口(导出给 game-logic 的 defineScenes 用)────────── */
|
||||
|
||||
/**
|
||||
* 起局/重开按钮的命中矩形(纯几何,零副作用):game-logic 在**状态进入点 onEnter** 据此确定性建命中区,
|
||||
* renderMenu/renderOver 据此画——同一来源,命中与绘制永不漂移。**绝不把命中矩形只在 render 副作用里
|
||||
* 生成**:并发渲染帧饥饿下 render 可能尚未跑过一次,命中矩形就恒为 null → 菜单点不动、一局起不来(卡 menu 案4)。
|
||||
*/
|
||||
export function menuStartRect(view) {
|
||||
const w = 240, h = 60;
|
||||
return { x: view.vw / 2 - w / 2, y: view.vh * 0.58 - h / 2, w, h };
|
||||
}
|
||||
export function overRestartRect(view) {
|
||||
const w = 240, h = 60;
|
||||
return { x: view.vw / 2 - w / 2, y: view.vh * 0.66 - h / 2, w, h };
|
||||
}
|
||||
|
||||
/**
|
||||
* menu:标题 + 玩法一句话 + 示例元素 + 开始按钮。换主题在 THEME/这里改文案。
|
||||
* @returns {{startBtn:{x,y,w,h}}} 命中区(drawButton 返回的归一化矩形)。
|
||||
* @returns {{startBtn:{x,y,w,h}}} 命中区(与 menuStartRect 同源)。
|
||||
*/
|
||||
export function renderMenu(g, hud, view) {
|
||||
drawBackdrop(g, view.vw, view.vh);
|
||||
@ -165,9 +179,8 @@ export function renderMenu(g, hud, view) {
|
||||
drawGlyph(g, 'circle', view.vw / 2 - 70, cy, 22, THEME.colors[0].fill);
|
||||
drawGlyph(g, 'diamond', view.vw / 2, cy, 22, THEME.colors[1].fill);
|
||||
drawGlyph(g, 'star', view.vw / 2 + 70, cy, 22, THEME.colors[2].fill);
|
||||
const w = 240, h = 60;
|
||||
// drawButton 返回它绘制的命中矩形,直接接住当命中区——别再自己另算坐标。
|
||||
const startBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.58 - h / 2, w, h },
|
||||
// 命中矩形由纯几何 menuStartRect 给出(onEnter 已据它建命中区),这里画在同一矩形上——所见即所点。
|
||||
const startBtn = hud.drawButton(g, menuStartRect(view),
|
||||
{ label: '开始解谜', font: '700 22px sans-serif', fill: C.accent, textColor: C.bannerText, radius: 30 });
|
||||
return { startBtn };
|
||||
}
|
||||
@ -202,8 +215,7 @@ export function renderOver(g, hud, view) {
|
||||
hud.drawText(g, '本局得分', view.vw / 2, view.vh * 0.31 + 42, { font: '600 14px sans-serif', color: C.textDim, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '通过 ' + view.levelsCleared + ' 关 最高连击 ×' + view.bestStreak, view.vw / 2, view.vh * 0.45, { font: '600 20px sans-serif', color: C.text, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '最佳纪录 ' + view.best, view.vw / 2, view.vh * 0.45 + 32, { font: '600 16px sans-serif', color: C.accent, align: 'center', baseline: 'middle' });
|
||||
const w = 240, h = 60;
|
||||
const restartBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.66 - h / 2, w, h },
|
||||
const restartBtn = hud.drawButton(g, overRestartRect(view),
|
||||
{ label: '再解一局', font: '700 22px sans-serif', fill: C.accent, textColor: C.bannerText, radius: 30 });
|
||||
return { restartBtn };
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ import {
|
||||
isTappable, hitCustomer, patienceRatio,
|
||||
} from './core.js';
|
||||
// 场景级绘制(画在 boot.mainContext);换观感改 render.js。
|
||||
import { renderMenu, renderPlay, renderOver } from './render.js';
|
||||
import { renderMenu, renderPlay, renderOver, menuStartRect, overRestartRect } from './render.js';
|
||||
// 资产装载(本骨架无外部美术,容错返空集;要加美术放 assets/manifest.json 再读 boot.assets)。
|
||||
import { loadAssets } from './assets.js';
|
||||
|
||||
@ -67,7 +67,8 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
let lost = 0; // 流失顾客数
|
||||
let roundTimer = null; // 60s 营业倒计时句柄
|
||||
|
||||
/* ── 命中区(菜单/结算按钮):render 回调实时算并写回(所见即所点)── */
|
||||
/* ── 命中区(菜单/结算按钮):**在场景进入点 onEnter 确定性赋值**(见 defineScenes),render 只读不写。
|
||||
* 绝不只在 render 副作用里生成——并发 rAF 饥饿下 render 没跑过=矩形恒 null=菜单点不动、一局起不来。── */
|
||||
let menuStartBtn = null;
|
||||
let overRestartBtn = null;
|
||||
|
||||
@ -171,8 +172,8 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
/** 注册三场景到 scene-fsm(每场景各调一次 define;回调 render 只收 g、update 只收 dt)。 */
|
||||
function defineScenes() {
|
||||
sceneFsm.define(SCENE_MENU, {
|
||||
onEnter() { customers = []; floats = []; menuStartBtn = null; },
|
||||
render(g) { menuStartBtn = renderMenu(g, hudUi, { vw: VW, vh: VH }).startBtn; },
|
||||
onEnter() { customers = []; floats = []; menuStartBtn = menuStartRect({ vw: VW, vh: VH }); }, // 进入 menu 即定命中区(不等 render)
|
||||
render(g) { renderMenu(g, hudUi, { vw: VW, vh: VH }); }, // render 只画,不写命中矩形
|
||||
});
|
||||
sceneFsm.define(SCENE_PLAY, {
|
||||
render(g) {
|
||||
@ -184,11 +185,14 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
},
|
||||
});
|
||||
sceneFsm.define(SCENE_OVER, {
|
||||
onEnter() { if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; } },
|
||||
onEnter() {
|
||||
if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; }
|
||||
overRestartBtn = overRestartRect({ vw: VW, vh: VH }); // 进入 over 即定命中区
|
||||
},
|
||||
render(g) {
|
||||
overRestartBtn = renderOver(g, hudUi, {
|
||||
renderOver(g, hudUi, { // render 只画,不写命中矩形
|
||||
vw: VW, vh: VH, currency, served, lost, best: sessionScore.getBest(),
|
||||
}).restartBtn;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -236,6 +240,7 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
/**
|
||||
* 取证视图【自动验收契约·改坏=九门挂】:state() 内**实时**算(绝不外层先算好闭包返回)。
|
||||
* · targets:每桌一项,occupied=true 表示有「可点的等待顾客」——自动验收只点 occupied:true 的桌、驱动上菜。
|
||||
* **键名固定为 `targets`,不得改成 items/objects 等**:判卷驱动器(C5)靠 state 有无 `targets` 键选驱动器族,改名=选错族=盲驱动点不到=H 门挂。
|
||||
* · score:= 营收,随上菜单调上升——自动验收据它判「真有进展」。
|
||||
* 加机制时这两个键的语义务必保持(targets 列可点目标、score 随进展升)。
|
||||
*/
|
||||
|
||||
@ -115,18 +115,31 @@ function drawFloats(g, floats) {
|
||||
|
||||
/* ────────── 场景绘制入口(导出给 game-logic 的 defineScenes 用)────────── */
|
||||
|
||||
/**
|
||||
* 起局/重开按钮的命中矩形(纯几何,零副作用):game-logic 在**状态进入点 onEnter** 据此确定性建命中区,
|
||||
* renderMenu/renderOver 据此画——同一来源,命中与绘制永不漂移。**绝不把命中矩形只在 render 副作用里
|
||||
* 生成**:并发渲染帧饥饿下 render 可能尚未跑过一次,命中矩形就恒为 null → 菜单点不动、一局起不来(卡 menu 案4)。
|
||||
*/
|
||||
export function menuStartRect(view) {
|
||||
const w = 240, h = 60;
|
||||
return { x: view.vw / 2 - w / 2, y: view.vh * 0.58 - h / 2, w, h };
|
||||
}
|
||||
export function overRestartRect(view) {
|
||||
const w = 240, h = 60;
|
||||
return { x: view.vw / 2 - w / 2, y: view.vh * 0.66 - h / 2, w, h };
|
||||
}
|
||||
|
||||
/**
|
||||
* menu:招牌 + 标题 + 开始按钮。换店名/副标在这里改。
|
||||
* @returns {{startBtn:{x,y,w,h}}} 命中区(drawButton 返回的归一化矩形)。
|
||||
* @returns {{startBtn:{x,y,w,h}}} 命中区(与 menuStartRect 同源)。
|
||||
*/
|
||||
export function renderMenu(g, hud, view) {
|
||||
drawBackdrop(g, view.vw, view.vh);
|
||||
g.fillStyle = 'rgba(20,12,6,0.55)'; g.fillRect(0, 0, view.vw, view.vh);
|
||||
hud.drawText(g, '烘焙小馆', view.vw / 2, view.vh * 0.30, { font: '800 46px sans-serif', color: C.text, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '点击等待的顾客上菜,60 秒内赚最多营收!', view.vw / 2, view.vh * 0.30 + 60, { font: '15px sans-serif', color: C.textDim, align: 'center', baseline: 'middle' });
|
||||
const w = 240, h = 60;
|
||||
// drawButton 返回它绘制的命中矩形(#5),直接接住当命中区——别再自己另算坐标。
|
||||
const startBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.58 - h / 2, w, h },
|
||||
// 命中矩形由纯几何 menuStartRect 给出(onEnter 已据它建命中区),这里画在同一矩形上——所见即所点。
|
||||
const startBtn = hud.drawButton(g, menuStartRect(view),
|
||||
{ label: '开始营业', font: '700 22px sans-serif', fill: C.accent, textColor: '#1d1208', radius: 30 });
|
||||
return { startBtn };
|
||||
}
|
||||
@ -164,8 +177,7 @@ export function renderOver(g, hud, view) {
|
||||
hud.drawText(g, '当日营收', view.vw / 2, view.vh * 0.32 + 38, { font: '600 14px sans-serif', color: C.textDim, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '订单 ' + view.served + ' 流失 ' + view.lost, view.vw / 2, view.vh * 0.46, { font: '600 20px sans-serif', color: C.text, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '最佳营收 $ ' + view.best, view.vw / 2, view.vh * 0.46 + 32, { font: '600 16px sans-serif', color: C.accent, align: 'center', baseline: 'middle' });
|
||||
const w = 240, h = 60;
|
||||
const restartBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.66 - h / 2, w, h },
|
||||
const restartBtn = hud.drawButton(g, overRestartRect(view),
|
||||
{ label: '再来一日', font: '700 22px sans-serif', fill: C.accent, textColor: '#1d1208', radius: 30 });
|
||||
return { restartBtn };
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ import {
|
||||
initialStats, mergeEndings,
|
||||
} from './core.js';
|
||||
// 场景级绘制(画在 boot.mainContext);换观感改 render.js。
|
||||
import { renderMenu, renderPlay, renderOver } from './render.js';
|
||||
import { renderMenu, renderPlay, renderOver, menuStartRect, overRestartRect } from './render.js';
|
||||
// 资产装载(本骨架无外部美术,容错返空集;要加美术放 assets/manifest.json 再读)。
|
||||
import { loadAssets } from './assets.js';
|
||||
|
||||
@ -67,7 +67,9 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
let elapsedMs = 0; // play 内累计毫秒(呼吸提示相位用)
|
||||
const endingIds = allEndingIds(STORY_NODES);
|
||||
|
||||
/* ── 命中区:选项矩形由 core.layoutChoices 统一算(命中=绘制=取证三者同源);按钮由 render 回写 ── */
|
||||
/* ── 命中区:选项矩形由 core.layoutChoices 统一算(命中=绘制=取证三者同源);
|
||||
* 起局/重开按钮**在场景进入点 onEnter 确定性赋值**(见 defineScenes),render 只读不写——
|
||||
* 绝不只在 render 副作用里生成(并发 rAF 饥饿下 render 没跑过=矩形恒 null=菜单点不动、一局起不来)。── */
|
||||
let choiceRects = [];
|
||||
let menuStartBtn = null;
|
||||
let overRestartBtn = null;
|
||||
@ -160,8 +162,8 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
/** 注册三场景到 scene-fsm(每场景各调一次 define;回调 render 只收 g、update 只收 dt)。 */
|
||||
function defineScenes() {
|
||||
sceneFsm.define(SCENE_MENU, {
|
||||
onEnter() { menuStartBtn = null; },
|
||||
render(g) { menuStartBtn = renderMenu(g, hudUi, { vw: VW, vh: VH, title: STORY_TITLE, subtitle: STORY_SUBTITLE }).startBtn; },
|
||||
onEnter() { menuStartBtn = menuStartRect({ vw: VW, vh: VH }); }, // 进入 menu 即定命中区(不等 render)
|
||||
render(g) { renderMenu(g, hudUi, { vw: VW, vh: VH, title: STORY_TITLE, subtitle: STORY_SUBTITLE }); }, // render 只画
|
||||
});
|
||||
sceneFsm.define(SCENE_PLAY, {
|
||||
render(g) {
|
||||
@ -174,12 +176,12 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
},
|
||||
});
|
||||
sceneFsm.define(SCENE_OVER, {
|
||||
onEnter() { overRestartBtn = null; },
|
||||
onEnter() { overRestartBtn = overRestartRect({ vw: VW, vh: VH }); }, // 进入 over 即定命中区
|
||||
render(g) {
|
||||
overRestartBtn = renderOver(g, hudUi, {
|
||||
renderOver(g, hudUi, { // render 只画,不写命中矩形
|
||||
vw: VW, vh: VH, endingNode, stats, statNames: STAT_NAMES,
|
||||
unlocked: unlockedEndings, endingIds, nodes: STORY_NODES,
|
||||
}).restartBtn;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -225,6 +227,7 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
* 取证视图【自动验收契约·改坏=九门挂】:state() 内**实时**算(绝不外层先算好闭包返回)。
|
||||
* · targets:play 态 = 当前每个选项一项(中心坐标,occupied=true 可点)——自动验收只点
|
||||
* occupied:true 的选项、驱动剧情推进;menu/over 态 = 空数组(键保留,值为空 → driver 不误点)。
|
||||
* **键名固定为 `targets`,不得改成 items/choices 等**:判卷驱动器(C5)靠 state 有无 `targets` 键选驱动器族,改名=选错族=盲驱动点不到=H 门挂。
|
||||
* · score:= 已做选择数,随推进单调上升——自动验收据它判「真有进展」。
|
||||
* 加机制时这两个键的语义务必保持(targets 列可点选项、score 随推进升)。
|
||||
*/
|
||||
|
||||
@ -103,10 +103,25 @@ function drawStats(g, hud, statNames, stats, vw, y) {
|
||||
|
||||
/* ────────── 场景绘制入口(导出给 game-logic 的 defineScenes 用)────────── */
|
||||
|
||||
/**
|
||||
* 起局/重开按钮的命中矩形(纯几何,零副作用):game-logic 在**状态进入点 onEnter** 据此确定性建命中区,
|
||||
* renderMenu/renderOver 据此画——同一来源,命中与绘制永不漂移。**绝不把命中矩形只在 render 副作用里
|
||||
* 生成**:并发渲染帧饥饿下 render 可能尚未跑过一次,命中矩形就恒为 null → 菜单点不动、一局起不来(卡 menu 案4)。
|
||||
* 起局按钮居 ≈0.58H(零-LLM 验收 driver 起局扫点的可达区,别挪出 0.46H–0.90H)。
|
||||
*/
|
||||
export function menuStartRect(view) {
|
||||
const w = 240, h = 60;
|
||||
return { x: view.vw / 2 - w / 2, y: view.vh * 0.58 - h / 2, w, h };
|
||||
}
|
||||
export function overRestartRect(view) {
|
||||
const w = 240, h = 60;
|
||||
return { x: view.vw / 2 - w / 2, y: view.vh * 0.66 - h / 2, w, h };
|
||||
}
|
||||
|
||||
/**
|
||||
* menu:标题 + 引子卡 + 开始按钮。
|
||||
* 按钮位于视口中线偏下(≈0.58H)——这是零-LLM 验收 driver 起局扫点的可达区,别挪出 0.46H–0.90H。
|
||||
* @returns {{startBtn:{x,y,w,h}}} 命中区(drawButton 返回的命中矩形)。
|
||||
* @returns {{startBtn:{x,y,w,h}}} 命中区(与 menuStartRect 同源)。
|
||||
*/
|
||||
export function renderMenu(g, hud, view) {
|
||||
drawBackdrop(g, view.vw, view.vh);
|
||||
@ -118,9 +133,8 @@ export function renderMenu(g, hud, view) {
|
||||
wrapText(g, view.subtitle, cardX + 18, view.vh * 0.32 + 20, cardW - 36, 26, 4);
|
||||
hud.drawText(g, '每个选择,都通向不同的结局', view.vw / 2, view.vh * 0.50,
|
||||
{ font: '14px sans-serif', color: C.dim, align: 'center', baseline: 'middle' });
|
||||
const w = 240, h = 60;
|
||||
// drawButton 返回它绘制的命中矩形,直接接住当命中区——别再自己另算坐标。
|
||||
const startBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.58 - h / 2, w, h },
|
||||
// 命中矩形由纯几何 menuStartRect 给出(onEnter 已据它建命中区),这里画在同一矩形上——所见即所点。
|
||||
const startBtn = hud.drawButton(g, menuStartRect(view),
|
||||
{ label: '开始阅读', font: '700 22px sans-serif', fill: C.accent, textColor: '#241708', radius: 30 });
|
||||
return { startBtn };
|
||||
}
|
||||
@ -187,8 +201,7 @@ export function renderOver(g, hud, view) {
|
||||
const line = endingIds.map((id) => (got.has(id) ? '「' + nodes[id].title + '」' : '???')).join(' ');
|
||||
hud.drawText(g, line, vw / 2, vh * 0.53 + 26,
|
||||
{ font: '13px sans-serif', color: C.dim, align: 'center', baseline: 'middle' });
|
||||
const w = 240, h = 60;
|
||||
const restartBtn = hud.drawButton(g, { x: vw / 2 - w / 2, y: vh * 0.66 - h / 2, w, h },
|
||||
const restartBtn = hud.drawButton(g, overRestartRect(view),
|
||||
{ label: '再读一次', font: '700 22px sans-serif', fill: C.accent, textColor: '#241708', radius: 30 });
|
||||
return { restartBtn };
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ import {
|
||||
cardTappable, hitCard,
|
||||
} from './core.js';
|
||||
// 场景级绘制(画在 boot.mainContext);换观感改 render.js。
|
||||
import { renderMenu, renderPlay, renderOver } from './render.js';
|
||||
import { renderMenu, renderPlay, renderOver, menuStartRect, overRestartRect } from './render.js';
|
||||
// 资产装载(本骨架无外部美术,容错返空集;要加美术放 assets/manifest.json 再读 boot.assets)。
|
||||
import { loadAssets } from './assets.js';
|
||||
|
||||
@ -78,7 +78,8 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
let nextCardId = 1; // 卡自增 id
|
||||
let roundTimer = null; // 60s 试炼倒计时句柄
|
||||
|
||||
/* ── 命中区(菜单/结算按钮):render 回调实时算并写回(所见即所点)── */
|
||||
/* ── 命中区(菜单/结算按钮):**在场景进入点 onEnter 确定性赋值**(见 defineScenes),render 只读不写。
|
||||
* 绝不只在 render 副作用里生成——并发 rAF 饥饿下 render 没跑过=矩形恒 null=菜单点不动、一局起不来。── */
|
||||
let menuStartBtn = null;
|
||||
let overRestartBtn = null;
|
||||
|
||||
@ -220,8 +221,8 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
/** 注册三场景到 scene-fsm(每场景各调一次 define;回调 render 只收 g、update 只收 dt)。 */
|
||||
function defineScenes() {
|
||||
sceneFsm.define(SCENE_MENU, {
|
||||
onEnter() { cards = []; floats = []; menuStartBtn = null; },
|
||||
render(g) { menuStartBtn = renderMenu(g, hudUi, { vw: VW, vh: VH }).startBtn; },
|
||||
onEnter() { cards = []; floats = []; menuStartBtn = menuStartRect({ vw: VW, vh: VH }); }, // 进入 menu 即定命中区(不等 render)
|
||||
render(g) { renderMenu(g, hudUi, { vw: VW, vh: VH }); }, // render 只画,不写命中矩形
|
||||
});
|
||||
sceneFsm.define(SCENE_PLAY, {
|
||||
render(g) {
|
||||
@ -236,12 +237,15 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
},
|
||||
});
|
||||
sceneFsm.define(SCENE_OVER, {
|
||||
onEnter() { if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; } },
|
||||
onEnter() {
|
||||
if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; }
|
||||
overRestartBtn = overRestartRect({ vw: VW, vh: VH }); // 进入 over 即定命中区
|
||||
},
|
||||
render(g) {
|
||||
overRestartBtn = renderOver(g, hudUi, {
|
||||
renderOver(g, hudUi, { // render 只画,不写命中矩形
|
||||
vw: VW, vh: VH, gold, floor, kills, opens, crits, heroDown,
|
||||
level: hero.level, best: sessionScore.getBest(), outcome: sessionScore.getOutcome(),
|
||||
}).restartBtn;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -287,6 +291,7 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
/**
|
||||
* 取证视图【自动验收契约·改坏=九门挂】:state() 内**实时**算(绝不外层先算好闭包返回)。
|
||||
* · targets:本层每张卡一项,occupied=true 表示「可点的遭遇卡」——自动验收只点 occupied:true 的卡、驱动掷骰。
|
||||
* **键名固定为 `targets`,不得改成 items/cards 等**:判卷驱动器(C5)靠 state 有无 `targets` 键选驱动器族,改名=选错族=盲驱动点不到=H 门挂。
|
||||
* · score:= 金币战利品,随结算单调上升(失败也有安慰分)——自动验收据它判「真有进展」。
|
||||
* 加机制时这两个键的语义务必保持(targets 列可点目标、score 随进展升)。
|
||||
*/
|
||||
|
||||
@ -128,9 +128,23 @@ function drawFloorBanner(g, hud, view) {
|
||||
|
||||
/* ────────── 场景绘制入口(导出给 game-logic 的 defineScenes 用)────────── */
|
||||
|
||||
/**
|
||||
* 起局/重开按钮的命中矩形(纯几何,零副作用):game-logic 在**状态进入点 onEnter** 据此确定性建命中区,
|
||||
* renderMenu/renderOver 据此画——同一来源,命中与绘制永不漂移。**绝不把命中矩形只在 render 副作用里
|
||||
* 生成**:并发渲染帧饥饿下 render 可能尚未跑过一次,命中矩形就恒为 null → 菜单点不动、一局起不来(卡 menu 案4)。
|
||||
*/
|
||||
export function menuStartRect(view) {
|
||||
const w = 240, h = 60;
|
||||
return { x: view.vw / 2 - w / 2, y: view.vh * 0.60 - h / 2, w, h };
|
||||
}
|
||||
export function overRestartRect(view) {
|
||||
const w = 240, h = 60;
|
||||
return { x: view.vw / 2 - w / 2, y: view.vh * 0.66 - h / 2, w, h };
|
||||
}
|
||||
|
||||
/**
|
||||
* menu:塔门 + 标题 + 玩法一句话 + 开始按钮。换冒险名/副标在这里改。
|
||||
* @returns {{startBtn:{x,y,w,h}}} 命中区(drawButton 返回的矩形)。
|
||||
* @returns {{startBtn:{x,y,w,h}}} 命中区(与 menuStartRect 同源)。
|
||||
*/
|
||||
export function renderMenu(g, hud, view) {
|
||||
drawBackdrop(g, view.vw, view.vh);
|
||||
@ -140,9 +154,8 @@ export function renderMenu(g, hud, view) {
|
||||
// 玩法核心一屏读懂(零阅读上手:三行图例)
|
||||
hud.drawText(g, '⚔ 怪物:高奖励·失败扣血', view.vw / 2, view.vh * 0.42, { font: '600 15px sans-serif', color: C.textDim, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '◆ 宝箱:稳收益 ✦ 奇遇:险中带甜', view.vw / 2, view.vh * 0.42 + 26, { font: '600 15px sans-serif', color: C.textDim, align: 'center', baseline: 'middle' });
|
||||
const w = 240, h = 60;
|
||||
// drawButton 返回它绘制的命中矩形,直接接住当命中区——别再自己另算坐标。
|
||||
const startBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.60 - h / 2, w, h },
|
||||
// 命中矩形由纯几何 menuStartRect 给出(onEnter 已据它建命中区),这里画在同一矩形上——所见即所点。
|
||||
const startBtn = hud.drawButton(g, menuStartRect(view),
|
||||
{ label: '进塔试炼', font: '700 22px sans-serif', fill: C.accent, textColor: '#160c2a', radius: 30 });
|
||||
return { startBtn };
|
||||
}
|
||||
@ -175,8 +188,7 @@ export function renderOver(g, hud, view) {
|
||||
hud.drawText(g, '爬到第 ' + view.floor + ' 层 · Lv.' + view.level, view.vw / 2, view.vh * 0.44, { font: '700 20px sans-serif', color: C.text, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '击败 ' + view.kills + ' · 开箱 ' + view.opens + ' · 大成功 ' + view.crits, view.vw / 2, view.vh * 0.44 + 30, { font: '600 16px sans-serif', color: C.textDim, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '最高纪录 金 ' + view.best, view.vw / 2, view.vh * 0.44 + 60, { font: '600 16px sans-serif', color: C.accent, align: 'center', baseline: 'middle' });
|
||||
const w = 240, h = 60;
|
||||
const restartBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.66 - h / 2, w, h },
|
||||
const restartBtn = hud.drawButton(g, overRestartRect(view),
|
||||
{ label: '再闯一次', font: '700 22px sans-serif', fill: C.accent, textColor: '#160c2a', radius: 30 });
|
||||
return { restartBtn };
|
||||
}
|
||||
|
||||
@ -22,8 +22,9 @@
|
||||
import { spawnTarget, ROUND_MS, TARGET_RADIUS } from './core.js';
|
||||
// 资产装载(本款无外部资产,loadAssets 容错返回空集;结构留全便于加美术)。
|
||||
import { loadAssets } from './assets.js';
|
||||
// 场景级绘制(画在 boot.mainContext;命中区由 render 的按钮矩形给出,所见即所点)。
|
||||
import { renderMenu, renderPlay, renderOver } from './render.js';
|
||||
// 场景级绘制(画在 boot.mainContext);起局/重开按钮命中矩形由纯几何 menuStartRect/overRestartRect 给出,
|
||||
// 在场景进入点 onEnter 确定性建立(不依赖渲染帧),render 只负责画。
|
||||
import { renderMenu, renderPlay, renderOver, menuStartRect, overRestartRect } from './render.js';
|
||||
|
||||
/** 固定逻辑步(与 host FIXED_DT 对齐)。 */
|
||||
const FIXED_DT = 1 / 60;
|
||||
@ -54,6 +55,8 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
let target = null;
|
||||
let roundTimer = null;
|
||||
let remainMs = 0;
|
||||
// 【起局契约·钉死】起局/重开按钮命中矩形在**场景进入点 onEnter** 确定性赋值(见 defineScenes),
|
||||
// render 只读不写——绝不只在 render 副作用里生成(并发 rAF 饥饿下 render 没跑过=矩形恒 null=菜单点不动)。
|
||||
let menuStartBtn = null;
|
||||
let overRestartBtn = null;
|
||||
|
||||
@ -112,15 +115,15 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
/** 注册 3 个场景到 scene-fsm(状态名你定;render 回调把命中区暴露给 onTap)。 */
|
||||
function defineScenes() {
|
||||
sceneFsm.define(SCENE_MENU, {
|
||||
onEnter() { target = null; },
|
||||
render(g) { menuStartBtn = renderMenu(g, hudUi).startBtn; },
|
||||
onEnter() { target = null; menuStartBtn = menuStartRect(); }, // 进入 menu 即定命中区(不等 render)
|
||||
render(g) { renderMenu(g, hudUi); }, // render 只画,不写命中矩形
|
||||
});
|
||||
sceneFsm.define(SCENE_PLAY, {
|
||||
render(g) { renderPlay(g, hudUi, { score: sessionScore.getScore(), remainMs, target }); },
|
||||
});
|
||||
sceneFsm.define(SCENE_OVER, {
|
||||
onEnter() { target = null; if (roundTimer != null) { timerScheduler.cancel(roundTimer); roundTimer = null; } },
|
||||
render(g) { overRestartBtn = renderOver(g, hudUi, { score: sessionScore.getScore(), best: sessionScore.getBest() }).restartBtn; },
|
||||
onEnter() { target = null; if (roundTimer != null) { timerScheduler.cancel(roundTimer); roundTimer = null; } overRestartBtn = overRestartRect(); }, // 进入 over 即定命中区
|
||||
render(g) { renderOver(g, hudUi, { score: sessionScore.getScore(), best: sessionScore.getBest() }); }, // render 只画
|
||||
});
|
||||
}
|
||||
|
||||
@ -166,7 +169,13 @@ export function createGame({ plugins, bundle, viewport }) {
|
||||
inited = false;
|
||||
},
|
||||
|
||||
/** 取证视图(可选·建议):host.state()/onReady/九门据此读状态 + 度量(JSON 安全)。 */
|
||||
/**
|
||||
* 取证视图(可选·建议):host.state()/onReady/九门据此读状态 + 度量(JSON 安全)。
|
||||
* 【targets 键契约·C5】点目标玩法(点客/打地鼠/点离散目标)必须在 state() 里暴露 `targets` 数组
|
||||
* [{x,y,occupied}]——**键名固定为 `targets`,不得改成 items/objects 等**:判卷驱动器靠 state 有无
|
||||
* `targets` 键选驱动器族(点目标族 vs 循环按键族),改名=选错族=盲驱动器点不到你的目标=H 进展门挂。
|
||||
* 本款是单目标点击,暴露 `target`(单数,同被驱动器识别);多目标一律用 `targets` 数组。
|
||||
*/
|
||||
_forensicsView() {
|
||||
return {
|
||||
state: () => ({
|
||||
|
||||
@ -89,6 +89,15 @@ export function centerButtonRect(cy, label) {
|
||||
return { x: W / 2 - w / 2, y: cy - h / 2, w, h };
|
||||
}
|
||||
|
||||
/**
|
||||
* 起局/重开按钮的命中矩形(纯几何,零副作用):game-logic 在**状态进入点 onEnter** 据此确定性
|
||||
* 建命中区,renderMenu/renderOver 据此画——同一来源,命中与绘制永不漂移。
|
||||
* **绝不把命中矩形只在 render 副作用里生成**:并发渲染帧饥饿下 render 可能尚未跑过一次,
|
||||
* 命中矩形就恒为 null → 菜单点不动、一局起不来(卡 menu 归因案4)。
|
||||
*/
|
||||
export function menuStartRect() { return centerButtonRect(H * 0.56, '开始'); }
|
||||
export function overRestartRect() { return centerButtonRect(H * 0.6, '再来一局'); }
|
||||
|
||||
/** 画按钮(委托 hud-ui,本款视觉色经 opts 注入)。 */
|
||||
function drawButton(g, hud, rect, label) {
|
||||
hud.drawButton(g, rect, { label, font: '600 22px sans-serif', fill: COLOR.accent, textColor: '#06201d', radius: 30 });
|
||||
@ -106,7 +115,7 @@ export function renderMenu(g, hud) {
|
||||
drawBackdrop(g);
|
||||
hud.drawText(g, '点圆得分', W / 2, H * 0.32, { font: '700 44px sans-serif', color: COLOR.text, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '限时 20 秒,点中圆圈得分', W / 2, H * 0.32 + 48, { font: '16px sans-serif', color: COLOR.textDim, align: 'center', baseline: 'middle' });
|
||||
const startBtn = centerButtonRect(H * 0.56, '开始');
|
||||
const startBtn = menuStartRect();
|
||||
drawButton(g, hud, startBtn, '开始');
|
||||
return { startBtn };
|
||||
}
|
||||
@ -131,7 +140,7 @@ export function renderOver(g, hud, view) {
|
||||
hud.drawText(g, '时间到!', W / 2, H * 0.28, { font: '700 40px sans-serif', color: COLOR.text, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '本局得分 ' + view.score, W / 2, H * 0.28 + 60, { font: '600 26px sans-serif', color: COLOR.target, align: 'center', baseline: 'middle' });
|
||||
hud.drawText(g, '最佳分 ' + view.best, W / 2, H * 0.28 + 100, { font: '18px sans-serif', color: COLOR.textDim, align: 'center', baseline: 'middle' });
|
||||
const restartBtn = centerButtonRect(H * 0.6, '再来一局');
|
||||
const restartBtn = overRestartRect();
|
||||
drawButton(g, hud, restartBtn, '再来一局');
|
||||
return { restartBtn };
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
|
||||
import { writeFileSync, readFileSync, mkdirSync, cpSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url'; // 入口守卫用:判定本模块是否被「直接执行」(见文件末尾)
|
||||
import { scaffold, scaffoldSaa, readFileTool, writeFileTool, listDirTool, check, build, buildSaa, stage, gameDir, ensurePlaySpec } from './tools.mjs';
|
||||
import { chat, MODEL } from './m3.mjs';
|
||||
import { TOOLS, buildSystemPrompt, DESIGN_TOOLS, buildDesignSystemPrompt } from './prompt.mjs';
|
||||
@ -326,4 +327,10 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error('gen.mjs 顶层异常:', e); process.exit(1); });
|
||||
// ── 入口守卫(footgun 兜底)────────────────────────────────────────────────
|
||||
// 缘由:gen.mjs 曾因顶层裸调 main() 被「import 一次」就跑真生成(~116s、烧钱烧时)。
|
||||
// 仅当作为脚本「直接执行」(node …/gen.mjs …)时跑 main;被 import(测试/工具引用)时不触发。
|
||||
// idiom 同 scripts/build.mjs:比对 import.meta.url 与 process.argv[1] 的规范 file:// URL。
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main().catch((e) => { console.error('gen.mjs 顶层异常:', e); process.exit(1); });
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user