feat(cheap-gen): 阶段一B #5 drawButton 改返命中矩形(黄金首件·治 #1 翻车点根因)

降AI参与·阶段一B 改签名第一处(附A #5)。逐插件改逐插件回归的黄金样板。
drawButton 由返 void 改为回吐它绘制的归一化命中矩形 {x,y,w,h}——AI 的直觉写法
const btn = hudUi.drawButton(g,{x,y,w,h},{label}); if (hudUi.pointInRect(px,py,btn)) 现在就对,
治了 #1 翻车点(drawButton 返 void 误用→变量恒 undefined→命中短路→卡菜单)的根因,而非教绕路。

全链路同改(漏一个造新脏数据):
- hud-ui/impl.js:drawButton 模块函数 + 实例方法回吐归一化矩形 r(g=null 也回吐、绝不 undefined)
- hud-ui/api.d.ts:模块函数 + 实例方法 两处 :void → :Rect + JSDoc
- tools.mjs 形状门:删 drawButton 特例分支(api.d.ts 改后自然掉出 void 名集),
  泛化判据留给仍真返 void 的方法(drawText/drawPanel/drawBar/play/flashScreen)
- prompt.mjs/cheap_roles.py(双源)/skill:指引由"返void绝不接"翻为"回吐命中矩形可直接 pointInRect"
- check.test.mjs:drawButton 测试反转(赋值不再拦)+ 链式测改 drawText
- test_roles.py/test_toolkit.py:footgun 断言 + 形状门 drawButton 测试随签名更新

验证(逐插件回归矩阵):
- 形状门单测 11/11(amodel-gen)+ cheap-worker test_roles/test_toolkit 16/16 + hud-ui 19/19
- 确定性 impl 铁证:drawButton 回吐 {x,y,w,h}、g=null 不 undefined、回吐矩形直接 pointInRect 闭环
- 真生成代码(菜单+按钮游戏)用了 drawButton、check 未拦 drawButton,证明 #5 不破坏生成
- 双源 drawButton 文案一致;prompt.mjs node --check + cheap_roles.py py_compile 绿

正交发现(非 #5):该局九门 gen 栽在 save.get() 方法名幻觉(save 插件无 get)——
是另一处方法名摩擦,记为阶段一B 后续/补密度候选,不在 #5 范围。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-29 09:26:33 -07:00
parent 62f819ad43
commit dc49f5f866
9 changed files with 33 additions and 32 deletions

View File

@ -136,7 +136,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(...)`。
- ❌ `const btn = hudUi.drawButton(g, rect, {label}); if (btn && hudUi.pointInRect(x,y,btn)) {...}` → ✅ **`drawButton` 返 `void`(只绘制、返 undefined),绝不接其返回值判命中**:先自己定义按钮矩形 `const btn = {x,y,w,h};` 单独调 `hudUi.drawButton(g, btn, {label})` 绘制,命中用 `hudUi.pointInRect(x, y, btn)`(签名 `(px, py, rect)`;**命中判定只在 hudUi,collision 插件没有 pointInRect 方法**)。接返回值 → `btn===undefined` → 命中永远短路 → 按钮点不动、卡菜单(实测打地鼠类:`menuStartBtn = drawButton(...)` → undefined → 菜单点击短路 → 进不去 play)。另:**`drawButton` 第二参是 rect 对象 `{x,y,w,h}`、不是 label/坐标位置参**——别写 `drawButton(g, '开始', x, y, w, h)`。
- ✅ **`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)`。
- ❌ 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(), ... })` 写。

View File

@ -12,7 +12,7 @@ skill / 插件 api.d.ts / 起点范例。L3 context 由 agent 经 read_file 按
system prompt 正文与 prompt.mjs 保持语义一致:入口契约 createGame({plugins,bundle,viewport}) 五法 + _forensicsView、
红线(零裸时间随机 DOM / bundle.tick / ctx.time.nowMs() / 扁平注入插件 / 方法名以 api.d.ts 为准)、
本周收口的两条 footgun(drawButton 返 void 用 pointInRect 判命中、sceneFsm.define 逐场景调)、输入契约(handleTap/handleKey)。
本周收口的 footgun(sceneFsm.define 逐场景调、真返 void 的绘制/音效 API 别接返回值;drawButton 已改返命中矩形、可直接接住判命中)、输入契约(handleTap/handleKey)。
"""
# prompt 指 agent 去 read 的真 skill(不手抄;skill 内含两条 footgun 的 ❌→✅ 清单)。
@ -47,7 +47,7 @@ A 读手册(1)+ 读你的起点 game-logic.js(4)→ B 按 brief 定玩
- **插件扁平注入:直接 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〕;particles-juice 没有 burst〔事件 API 名以其 api.d.ts 为准〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ 实测易犯的幻觉 API」逐条列 ❌→✅。
- **hudUi.drawButton(g, rect, opts) 返回 void(只绘制、返 undefined)——绝不把它的返回值赋给变量再判命中**:❌ btn = hudUi.drawButton(g, rect, {label}); if (btn && hudUi.pointInRect(x,y,btn)) ... → btn 永远 undefined → 命中判定永远短路 → 卡菜单/按钮点不动(实测打地鼠类翻车点:menuStartBtn = drawButton(...) → undefined → 菜单点击短路 → 进不去 play)。✅ 先**自己定义按钮矩形** const btn = { x, y, w, h };**单独**调 hudUi.drawButton(g, btn, {label}) 绘制(不接返回值);命中用 hudUi.pointInRect(x, y, btn)(签名 (px, py, rect);**命中判定只在 hudUi,collision 插件没有 pointInRect 方法**)。**drawButton 第二参是 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) 这种位置参形态。
- **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 };可链式 .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/无美术 → 全程序化绘制即可。

View File

@ -21,11 +21,11 @@ def test_entry_contract_present():
def test_two_footguns_present():
"""本周收口的两条 footgun 正反例关键串在场。"""
"""收口的关键 footgun 正反例串在场(阶段一B #5 后 drawButton 改为回吐命中矩形、sceneFsm.define 逐场景调)。"""
p = build_system_prompt("t1")
# footgun①:drawButton 返 void,命中用 pointInRect 不要赋值
assert "drawButton" in p and "pointInRect" in p and "返回 void" in p
# footgun②:sceneFsm.define 逐场景调,不要单对象批量
# ①:drawButton 回吐命中矩形,命中用 pointInRect(改签名后 const btn = drawButton(...) 直接判命中是直觉写法)
assert "drawButton" in p and "pointInRect" in p and "命中矩形" in p
# ②:sceneFsm.define 逐场景调,不要单对象批量
assert "sceneFsm.define('menu'" in p and "单对象批量" in p

View File

@ -1,7 +1,7 @@
"""
test_toolkit.py — U2 工具底座(read/list/write)+ 形状门三态。
形状门三态(drawButton 赋值 FAIL / define 单对象 FAIL / 干净源 PASS)经 shell-out 现有 node tools.mjs check —
形状门:define 单对象 FAIL / 干净源 PASS / drawButton 赋值改签名后 PASS(#5 回吐命中矩形)经 shell-out 现有 node tools.mjs check —
便宜档与 Node 侧跑同一份门代码,口径必然一致。真 shell-out node(确定性命令,非 LLM gen 循环,前台串行)。
跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_toolkit.py
@ -32,8 +32,8 @@ def _write_logic(src: str, gid: str = GID) -> None:
assert r["ok"], r
# ── 形状门合成输入:触发目标 footgun,其余 check 门尽量满足以免掩盖 ──
_DRAWBUTTON_BAD = """export function createGame({ plugins, bundle, viewport }) {
# ── 形状门合成输入:define 触发目标 footgun;drawButton 改签名后赋值消费已合法(#5 回吐命中矩形),其余 check 门尽量满足以免掩盖 ──
_DRAWBUTTON_RETURN_OK = """export function createGame({ plugins, bundle, viewport }) {
const { hudUi } = plugins;
return {
init(boot) { this.ctx = boot.ctx; },
@ -87,12 +87,12 @@ def test_write_l3_ok():
assert r["ok"] and r["bytes"] > 0
def test_check_shape_drawbutton_fail():
def test_check_shape_drawbutton_ok():
# 阶段一B #5:drawButton 改返命中矩形后,const btn = drawButton(...); if (btn) 是合法直觉写法、形状门不再拦。
_scaffold()
_write_logic(_DRAWBUTTON_BAD)
_write_logic(_DRAWBUTTON_RETURN_OK)
r = cheap_run.check(GID)
assert not r["ok"], "drawButton 赋值应触发形状门①"
assert "drawButton" in r["output"]
assert r["ok"], f"drawButton 赋值消费应通过(改签名后返命中矩形):{r.get('output', '')}"
def test_check_shape_define_fail():

View File

@ -161,8 +161,8 @@ export declare function measureText(g: CanvasRenderingContext2D, text: string, o
export declare function drawText(g: CanvasRenderingContext2D, text: string, x: number, y: number, opts?: DrawTextOptions): void;
/** 绘制面板(圆角矩形手写路径后按 opts fill/stroke)。g 缺路径/填充方法时降级,不外抛。 */
export declare function drawPanel(g: CanvasRenderingContext2D, rect: Rect, opts?: PanelOptions): void;
/** 绘制按钮(drawPanel 背板 + 居中 drawText 标签;disabled/pressed 调整样式)。委托降级,不外抛。 */
export declare function drawButton(g: CanvasRenderingContext2D, rect: Rect, opts?: ButtonOptions): void;
/** 绘制按钮(drawPanel 背板 + 居中 drawText 标签;disabled/pressed 调整样式)。委托降级,不外抛。**回吐归一化命中矩形 {x,y,w,h}**:支持 `const btn = drawButton(g,{x,y,w,h},{label}); if (pointInRect(px,py,btn)){…}` 的直觉写法。 */
export declare function drawButton(g: CanvasRenderingContext2D, rect: Rect, opts?: ButtonOptions): Rect;
/** 绘制通用比例条(ratio 钳 [0,1];背槽 + 按 ratio 裁剪的填充条)。委托降级,不外抛。 */
export declare function drawBar(g: CanvasRenderingContext2D, rect: Rect, ratio: number, opts?: BarOptions): void;
@ -191,8 +191,8 @@ export interface HudUiPlugin extends Plugin {
drawText(g: CanvasRenderingContext2D, text: string, x: number, y: number, opts?: DrawTextOptions): void;
/** 绘制面板(fill/stroke 缺省用默认样式)。 */
drawPanel(g: CanvasRenderingContext2D, rect: Rect, opts?: PanelOptions): void;
/** 绘制按钮(font/fill/textColor/stroke 缺省用默认样式)。 */
drawButton(g: CanvasRenderingContext2D, rect: Rect, opts?: ButtonOptions): void;
/** 绘制按钮(font/fill/textColor/stroke 缺省用默认样式)。回吐归一化命中矩形 {x,y,w,h}(可直接 pointInRect 判命中)。 */
drawButton(g: CanvasRenderingContext2D, rect: Rect, opts?: ButtonOptions): Rect;
/** 绘制比例条(bg/fill/stroke 缺省用默认样式)。 */
drawBar(g: CanvasRenderingContext2D, rect: Rect, ratio: number, opts?: BarOptions): void;
/** 只读探针(仅测试/诊断;生产可删)。 */

View File

@ -320,9 +320,9 @@ export function drawPanel(g, rect, opts) {
* @returns {void}
*/
export function drawButton(g, rect, opts) {
if (!g) return;
const o = opts || {};
const r = normRect(rect);
if (!g) return r; // 无上下文也回吐归一化命中矩形,绝不让 const btn = drawButton(...) 拿到 undefined(治 #1 翻车点)
// 背板填充:默认强调色;pressed 略压暗、disabled 半透明(纯状态样式调整,非品类视觉)。
let fill = typeof o.fill === 'string' && o.fill ? o.fill : DEFAULT_ACCENT;
let textColor = typeof o.textColor === 'string' && o.textColor ? o.textColor : DEFAULT_TEXT_COLOR;
@ -346,6 +346,8 @@ export function drawButton(g, rect, opts) {
baseline: 'middle',
});
}
// 回吐归一化命中矩形 {x,y,w,h}:支持 const btn = drawButton(g,{x,y,w,h},{label}); if (hudUi.pointInRect(px,py,btn)){…} 的母语直觉写法(消除"返 void 误用"#1 翻车点的根因)。
return r;
}
/**
@ -510,7 +512,7 @@ export function createHudUiPlugin(opts) {
},
/** 绘制按钮(font/fill/textColor/stroke 缺省用默认样式)。 */
drawButton(g, rect, opts2) {
drawButton(
return drawButton( // 回传模块级 drawButton 的命中矩形,实例方法与模块函数同形(都回吐 {x,y,w,h})
g,
rect,
withDefaults(opts2, {

View File

@ -90,7 +90,7 @@ A 读手册(1)+ 读你的起点 game-logic.js(4)→ B 按 brief 定玩法(核心
- **插件扁平注入:直接 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 为准、别臆测**(M3 实测幻觉:scene-fsm 写 defineScenes〔应 define〕;particles-juice 没有 burst〔事件 API 名以其 api.d.ts 为准〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ M3 实测易犯的幻觉 API」逐条列 ❌→✅。
- **hudUi.drawButton(g, rect, opts) 返回 void(只绘制、返 undefined)——绝不把它的返回值赋给变量再判命中**:❌ btn = hudUi.drawButton(g, rect, {label}); if (btn && hudUi.pointInRect(x,y,btn)) ... → btn 永远 undefined → 命中判定永远短路 → 卡菜单/按钮点不动(实测打地鼠类翻车点:menuStartBtn = drawButton(...) → undefined → 菜单点击短路 → 进不去 play)。✅ 先**自己定义按钮矩形** const btn = { x, y, w, h };**单独**调 hudUi.drawButton(g, btn, {label}) 绘制(不接返回值);命中用 hudUi.pointInRect(x, y, btn)(签名 (px, py, rect);**命中判定只在 hudUi,collision 插件没有 pointInRect 方法**)。**drawButton 第二参是 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) 这种位置参形态。
- **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 };可链式 .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/无美术 → 全程序化绘制即可。

View File

@ -3,7 +3,7 @@
* owner:便宜档 M1 U1 | 跑法:node --test tools/amodel-gen/tests/check.test.mjs(cwd 任意)
*
* 【守的不变量】
* ① 返 void 的插件 API 返回值被赋值/链式消费 → catch(base2 drawButton 实证 + 泛化到所有 void 方法)。
* ① 返 void 的插件 API 返回值被赋值/链式消费 → catch(泛化到所有 void 方法;drawButton 阶段一B #5 改返命中矩形后已非 void、不在此集)。
* ② sceneFsm.define 单对象/数组批量误用 → catch(base3 2048 实证)。
* 零误杀:base1/4/5 实证的合法写法(独立语句、if(guard)call()、try/catch、比较、注释/字符串里的 API 名、非插件键同名方法)一律不报。
* _shapeGateErrors 真实读各插件 api.d.ts 抽 void 名集,故本测同时把关「api.d.ts 的 void 标注被正确解析」。
@ -20,10 +20,10 @@ const wrap = (body) => `export function createGame(){ function f(g){ ${body} } r
// ───────── ① 返 void API 返回值被消费 → catch ─────────
test('① base2 幻觉:drawButton 返回值赋值 → catch + 指向 pointInRect', () => {
const errs = _shapeGateErrors(wrap(`menuStartBtn = hudUi.drawButton(g, '开始游戏', bx, by, bw, bh);`), 'game-logic.js');
assert.ok(hasShape(errs), '应 catch drawButton 赋值');
assert.ok(errs.some((e) => e.includes('drawButton') && e.includes('pointInRect')), '应给出 pointInRect 改法');
test('① 阶段一B #5:drawButton 改返命中矩形 → 赋值是直觉写法、不再 catch', () => {
// 改签名后 drawButton 回吐 {x,y,w,h} 命中矩形,const btn = drawButton(...) 接着 pointInRect 判命中是母语直觉写法,形状门不得再拦(治 #1 翻车点根因而非教绕路)。
const errs = _shapeGateErrors(wrap(`const menuStartBtn = hudUi.drawButton(g, { x: bx, y: by, w: bw, h: bh }, { label: '开始游戏' });`), 'game-logic.js');
assert.ok(!hasShape(errs), 'drawButton 返命中矩形,赋值不应再被形状门拦');
});
test('① 泛化:其它 void 方法(drawText/play/flashScreen/drawBar)被赋值 → catch', () => {
@ -40,7 +40,7 @@ test('① 泛化:其它 void 方法(drawText/play/flashScreen/drawBar)被赋值
});
test('① 链式:void 方法后链式取值 → catch', () => {
const errs = _shapeGateErrors(wrap(`hudUi.drawButton(g, btn, {}).pressed;`), 'game-logic.js');
const errs = _shapeGateErrors(wrap(`hudUi.drawText(g, '得分', 0, 0, {}).width;`), 'game-logic.js');
assert.ok(hasShape(errs), '应 catch 链式消费');
});

View File

@ -217,11 +217,10 @@ export function _shapeGateErrors(rawSrc, f) {
const assigned = before.endsWith('=') && !/[=!<>]/.test(before.slice(-2, -1));
const chained = isChainedAfter(s, m.index + m[0].length - 1);
if (!assigned && !chained) continue;
if (method === 'drawButton') {
errs.push(`形状门 ${f}: drawButton(g,rect,opts) 返 void(只绘制、返 undefined)——别把它赋给变量再判命中(赋值后变量恒 undefined → 命中永远短路 → 卡菜单)。改:先定义按钮矩形 const btn={x,y,w,h};单独调 hudUi.drawButton(g,btn,{label}) 绘制;命中用 hudUi.pointInRect(x,y,btn) 或 collision.pointInRect(x,y,btn)`);
} else {
errs.push(`形状门 ${f}: ${key}.${method}() 返 void(只执行副作用、返 undefined)——别${assigned ? '把返回值赋给变量' : '在它后面链式取值'}(返回值恒 undefined → 后续判断/取值短路)。当独立语句调用即可:${key}.${method}(...);需要的状态走对应读取方法(见 src/plugins/${dir}/api.d.ts)`);
}
// 泛化判据:凡 api.d.ts 标注返 void 的插件 API 被赋值/链式消费即拦(void 名集动态从 api.d.ts 取)。
// 阶段一B #5:drawButton 已改返命中矩形 {x,y,w,h}、从 api.d.ts void 集自然剔除,故此处不再有 drawButton 特例——
// 改签名治了根因(直觉写法 const btn=drawButton(...) 现在就对),把"返 void 误用"的拦截留给仍真返 void 的方法(drawText/drawPanel/drawBar/play/flashScreen 等)。
errs.push(`形状门 ${f}: ${key}.${method}() 返 void(只执行副作用、返 undefined)——别${assigned ? '把返回值赋给变量' : '在它后面链式取值'}(返回值恒 undefined → 后续判断/取值短路)。当独立语句调用即可:${key}.${method}(...);需要的状态走对应读取方法(见 src/plugins/${dir}/api.d.ts)`);
}
// ② define 单对象/数组批量(第一参应是场景名字符串):匹配「.define( {」或「.define( [」。
if (/\.define\s*\(\s*[{[]/.test(s)) {