feat(runner-v2): P1 王蓝莓上引擎=游戏宿主装载契约 spike 实(mini-desktop real 引擎 5 门全过)
Runner v2 全弧第一相(HJ-RUNNER-ARC-001 / P1):用王蓝莓把"上层游戏宿主装载契约"
从零 spike 到"引擎真掌帧 + 游戏美术与引擎粒子同屏可玩",解锁 P2 泛化 / P3 生成。
契约与共享件
- 新增装载契约 game-runtime/src/core/game-host.d.ts(第9类 additive·暂名):
GameHostBootContext / GameInstance / GameHostFactory;游戏=纯模块工厂,零引擎 import,
能力经下层 ctx.getEngine()(受控面 6 项零改),绘制面 render(g) 的 g=引擎 mainContext
- makeEngineCaps 闭包提升为可 import 模块 host-dev/engine-caps.js(createEngineCaps):
host-dev/ref/未来生成游戏共用;engineMode 短路 / __engineCalls 探针 / null-engine 语义逐行不变
王蓝莓 real 通道(games/wanglanmei-ref)
- 增 ?engine=real:主循环从浏览器 RAF 改挂 engineInit 五回调(掌帧唯一源=引擎);
游戏美术绘制面从 #game 改引擎 mainContext(同屏合并);bundle 延后到 setupReal 内以
mainContext 建(getContext2d 闭包快照修);保 ?engine=stub 逐像素确定性回滚
- game-shop.js 抽为独立 GameHostFactory(零引擎 import,P3 生成 agent 可复制范本)
- entry.js 动态 import('littlejsengine')=node-import-safe ∧ esbuild iife 入包两全
- 驱动 e2e-input.cdp.cjs real 改造:禁 stepFrames 手摇(防双掌帧),真触摸+轮询;
门②同屏像素判据(暖色 warmHits ∧ 引擎粒子 brightHits 同张 #game-engine)
真机验收(mini-desktop real 引擎,5 门)
引擎掌帧 dFrame=31∈[20,45] / 同屏 warmHits=682∧brightHits=13028 同张 #game-engine /
particles.spawnEmitter+synthSfx call-ID 运行时真调引擎 / CDP 517 真触摸跑通整局
(第1天接待18·营收¥79.9·连击9) / console 零错;本机 88 单测全绿(含修复 4 红+补回归)
证据:games/wanglanmei-ref/evidence/p1-real/(e2e-evidence.json pass=true + 5 截图肉眼复核)
诚实 caveat:门② brightHits 在结算屏混入亮 UI,同屏合并由 warmHits(美术落引擎画布)+
call-ID(粒子真调引擎)+ 引擎架构(粒子渲同一 mainContext)三证成立,非阻断。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
db9b7b623e
commit
99b6c82dec
@ -23,6 +23,8 @@
|
||||
> **Wave1 脊柱 4 模块契约已补并锁**(aigc/runtime/feed/telemetry,2026-06-08):经契约先行起草 + 一致性评审 + 收口(§2.1)+ 主 agent 校验(YAML 语法/裸 select* 0/错误码段 101-104 独占/Flyway V2-5 唯一)。
|
||||
> **Wave2 变现契约已补并锁**(ad/trade,2026-06-08):`ad.yaml`+`trade.yaml`(API #1)+ `V6.0.0`/`V7.0.0`(DB #2),消费既有 #7 ad-slot / #5 events 不重造;经主 agent 校验(YAML 语法/错误码 111·106 独占/Flyway V6-7 唯一/金额用「分」/资金幂等键+状态机/ad↔trade Feign seam 一致);ad 计费 `uk_trace` 由主 agent 收紧为 `(trace_id,event_type,tenant_id)` 防 reward 被 impression 静默吞掉。**pay=复用 yudao-pay 后置**(收单 P1)。
|
||||
> **Wave3 跨主线收口(进行中)**:新增 `studio`(创作主链路最小版编排,错误码 112 / Flyway V8)+ `compliance`(锁风门 Gate,发布前合规扫描)两模块契约。Phase A 已收口 project 侧:publish 唯一化(返回 admitted+gates 门禁聚合)、ProjectApi seam 定稿(getCurrentVersionId GAP-3 / createProject 写类 RPC)、GAP-4 把 `events.schema.json` 信封 envelope/user 由 snake_case 统一为 camelCase(与 telemetry.yaml/前端 SDK 对齐)。compliance gate 在 project.submitPublish 内仅留 Phase C 接入 seam(TODO 注释),尚未接 ComplianceGateApi。其余模块(community/ip/biz)待后续波次补。
|
||||
>
|
||||
> **游戏宿主装载契约(指针登记,不折进上表跨仓 8 类)**:Runner v2 P1 新立 additive 第 9 类契约,**落 `game-runtime/src/core/game-host.d.ts`**(与 `api.d.ts` 同目录,**非**本目录)。理由:它是 game-runtime 内部「host↔游戏工厂」接线面,消费方只在 game-runtime(host boot + 生成 agent),落同目录 import 方向顺(`./api.d.ts`);落本目录会让跨仓 SSOT 反向相对 import `../game-runtime/src`(拆仓即断)。消费方=host boot(装游戏)+ 生成 agent(产出靶);与受控面 6 项 additive(受控面零改)。详见 `docs/agent-specs/2026-06-13-P1-王蓝莓上引擎-execution.md` §5/§12-A1。
|
||||
|
||||
```
|
||||
contracts/
|
||||
|
||||
370
docs/agent-specs/2026-06-13-P1-王蓝莓上引擎-execution.md
Normal file
370
docs/agent-specs/2026-06-13-P1-王蓝莓上引擎-execution.md
Normal file
@ -0,0 +1,370 @@
|
||||
# P1 · 王蓝莓上引擎(spike 游戏宿主装载契约)· 执行版
|
||||
|
||||
> **状态**:execution 版 · 2026-06-13 · 给执行 agent 实现。配套 review 版 [`2026-06-13-P1-王蓝莓上引擎-review.md`](./2026-06-13-P1-王蓝莓上引擎-review.md);母梁 [`2026-06-13-runner-v2-arc-review.md`](./2026-06-13-runner-v2-arc-review.md) §5.1。
|
||||
> **修订**:2026-06-13 对抗复审(deep·全程读真源码)→ 纳入 4 major(real 就绪标记 / driver 范式切换 / 同屏暖色阈值坐实 / bundle 延后建)+ 5 项不明假设处置(IDL 落点改 game-runtime/src·缺省分叉显式声明·game 工厂硬独立·部署改 git-clone-scratch·步骤0 降常规核对)。逐条裁决见 §12。
|
||||
> **模型分流**:一律 opus(创始人拍)。
|
||||
> **前提(创始人已拍)**:装载契约 = 新立 additive 第 9 类契约(不污染 `PluginContext`);生命周期 = `init/update/render/onInput` + `destroy`;boot 注入引擎 `mainContext` 给游戏层。
|
||||
> **铁律**:受控面 6 项契约(`getContext2d/onFrame/getInput/getAudioContext/time/random`,`api.d.ts:269/281/286/303`)**零改**;引擎 import 只活 entry/host(Q4);区分 verified/inferred/assumption,凡引代码事实带 file:line。
|
||||
> **装载契约落点(复审改)**:IDL 落 `game-runtime/src/core/game-host.d.ts`(与 `api.d.ts` 同目录),**不**落 `contracts/`——`contracts/` 是跨三仓五工位的**业务契约** SSOT(API/DB/SDK/event…,README §1),装载契约是 game-runtime 内部「host↔游戏工厂」接线面,消费方只在 game-runtime(host boot + 生成 agent),且让跨仓 SSOT 反向相对 import `../game-runtime/src/*` 违「契约即 SSOT」且 contracts 拆独立仓时即断(README §1 明列拆仓计划)。落同目录后 import 方向自然(`import type { PluginContext } from './api.d.ts'`)。详见 §12-A1。
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标 / 范围边界
|
||||
|
||||
### 1.1 目标
|
||||
给 `wanglanmei-ref` 加 `?engine=real` 通道:① 引擎真掌帧(`engineInit` 五回调驱动游戏 `update/render`,废浏览器 RAF);② 游戏美术与引擎粒子**同屏可玩**(游戏绘制面从 `#game` 2D 改引擎 `mainContext`,合并成一张画布);③ 人在 mini-desktop 浏览器真玩;④ 保 `?engine=stub` 回滚(逐像素确定性)。同时把 `makeEngineCaps`(`host.js:197-318`)提升为可 import 模块 `host-dev/engine-caps.js`,host-dev / ref 共用。
|
||||
|
||||
### 1.2 范围内
|
||||
- 新立装载契约工件 **`game-runtime/src/core/game-host.d.ts`**(additive 第 9 类契约;落 game-runtime/src,**非** contracts/,详见 §12-A1)。
|
||||
- `host-dev/engine-caps.js` 提升(搬 `host.js:197-318` 闭包为 `createEngineCaps({LJS, engineMath, engineMode, recHook})`);`host.js` 改 import。
|
||||
- `wanglanmei-ref` real 通道改造:`entry.js` import 引擎、`main.js` 主循环挂引擎五回调、绘制面改 `mainContext`、`createHostDevContext` 注 `engineFactory`、`mockNowMs` 改逻辑帧钟、RAF 门控。
|
||||
- ref 内拆出符合装载契约的 game 工厂(**硬约束:独立文件 `game-shop.js`,默认导出 `GameHostFactory`,零引擎 import;禁 main.js 内联糊过**,详见步骤 3 + §12-A3)。
|
||||
- `e2e-input.cdp.cjs` 加 `engine=real` 通道断言 + 同屏像素判据 + **real 通道 driver 范式切换(真触摸+轮询,禁 stepFrames 手摇,详见步骤 8 + §12-M2)**。
|
||||
- `index.html` 加 `__refBootedReal` 就绪标记接线(real 通道;详见步骤 4.3 + §12-M1)。
|
||||
- `SIZES.md` 增"游戏产物"口径。
|
||||
|
||||
### 1.3 范围外(非目标)
|
||||
- 受控面 6 项契约(零改)。
|
||||
- P2(泛化宿主/装载任意 GamePackage)→ 母 spec Phase D(O2 已拍)。
|
||||
- P3(生成游戏按契约产出)→ W-G1 自身 spec。
|
||||
- 后端发布主链 / game-studio / feed / 生成管线。
|
||||
- 不接 OSS;不改 feed 引擎选型。
|
||||
- 不动 stub 通道既有行为(逐行保旧)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 前置
|
||||
|
||||
| # | 前置 | 状态 / 证据 |
|
||||
|---|---|---|
|
||||
| P-1 | A0 引擎掌帧地基已收口(real 通道范式可用) | verified:`host.js:917` engineInit 五回调 + `host.js:919` setupAfterEngine(mainContext);门0 8/8(`3e5cab4`) |
|
||||
| P-2 | A1-A4 引擎能力包装已落地(makeEngineCaps 已接线) | verified:`host.js:359` `engineFactory: makeEngineCaps`;`engine-math.js` 已独立模块 |
|
||||
| P-3 | mini-desktop 具备 esbuild + chrome(6c6g 禁 chrome) | verified:`a6-runbook.sh` / `ref-e2e-runbook.sh` 均跑 mini-desktop |
|
||||
| P-4 | ref 现状:49 单测绿 + e2e 四件套(stub 基线) | verified:`BUILD.md` RESULT.txt 49/49 |
|
||||
| P-5 | LittleJS 已是 ref devDep(构建可引擎入包) | inferred:host-dev 同仓共用 `littlejsengine`;执行首步须核 `games/wanglanmei-ref` 经 monorepo node_modules 可解析(与 host-dev entry 同口径) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 涉及文件路径
|
||||
|
||||
| 文件 | 动作 | 关键行锚 |
|
||||
|---|---|---|
|
||||
| `game-runtime/src/core/game-host.d.ts` | **新增** | 装载契约 IDL(§5);落 src/core 与 `api.d.ts` 同目录(**非** contracts/,§12-A1) |
|
||||
| `contracts/README.md` | 改(索引指针) | 不折进跨仓 8 类;加一行指针:「游戏宿主装载契约(game-runtime 内部,落 `game-runtime/src/core/game-host.d.ts`)」(§12-A1) |
|
||||
| `host-dev/engine-caps.js` | **新增** | `createEngineCaps`(搬 `host.js:197-318`) |
|
||||
| `host-dev/host.js` | 改 | `makeEngineCaps` 闭包 → import `createEngineCaps`(`host.js:197-318` 删,`host.js:204/359` 改) |
|
||||
| `games/wanglanmei-ref/entry.js` | 改 | `entry.js:15` 增 `import * as LJS from 'littlejsengine'` |
|
||||
| `games/wanglanmei-ref/src/main.js` | 改(real 通道) | `main.js:112`(engineFactory,real 须延后到 engineInit().then 内建·§12-M4)/`617-661`(frame→game.update/render)/`648`(drawFrame g→mainContext)/`668-691`(RAF 门控)/`618`(mockNowMs)/`583-602`(real 输入改挂 mainCanvas·§12-M4) |
|
||||
| `games/wanglanmei-ref/src/game-shop.js` | **新增(硬约束·非建议)** | 把 main.js 游戏逻辑/绘制段封成装载契约 game 工厂;默认导出 `GameHostFactory`、零引擎 import(步骤 3 + §12-A3) |
|
||||
| `games/wanglanmei-ref/index.html` | 改 | real 通道 `__refBootedReal` 就绪标记接线(§12-M1);stub 仍用 `__refBooted` |
|
||||
| `games/wanglanmei-ref/test/e2e-input.cdp.cjs` | 改 | 加 `engine=real` 通道(rect 选择器切 `#game-engine`、waitRefBoot 等 `__refBootedReal`、real 范式=真触摸+轮询禁 stepFrames、同屏像素判据)(步骤 8 + §12-M1/M2/M3) |
|
||||
| `games/wanglanmei-ref/test/ref-e2e-runbook.sh` | 改(或旁建 real 版) | **沿 git-clone-scratch 口径**(`ref-e2e-runbook.sh:20-21`,**非** tar-pipe);URL 加 `&engine=real`;构建后 `grep -q ParticleEmitter` 核引擎入包(§12-A4) |
|
||||
| `SIZES.md` | 改 | 增"游戏产物=引擎+包装层"口径 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据流
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant HTML as index.html
|
||||
participant Boot as bootRef(main.js)
|
||||
participant Caps as engine-caps.js
|
||||
participant LJS as littlejsengine
|
||||
participant Game as game(装载契约工厂)
|
||||
|
||||
HTML->>Boot: bootRef({canvas,statusEl})
|
||||
alt engine=real
|
||||
Boot->>Boot: setGLEnable(false)+setDebugWatermark(false)+setCanvasClearColor(BLACK)
|
||||
Boot->>Caps: createEngineCaps({LJS,engineMath,engineMode:'real',recHook})
|
||||
Caps-->>Boot: engineFactory(particles/audio/math 三面)
|
||||
Boot->>LJS: engineInit(gameInit,gameUpdate,...,gameRender,...)
|
||||
LJS-->>Boot: .then() → mainContext 就绪
|
||||
Boot->>Boot: createHostDevContext({context2d:mainContext, engineFactory, clock:逻辑帧钟})
|
||||
Boot->>Game: game.init(ctx, mainContext)
|
||||
loop 引擎内部 RAF
|
||||
LJS->>Game: gameUpdate → game.update(FIXED_DT)
|
||||
LJS->>Game: gameRender → game.render(mainContext)
|
||||
Game->>LJS: ctx.getEngine().particles.spawnEmitter(粒子自渲落 mainContext)
|
||||
end
|
||||
else engine=stub(保旧)
|
||||
Boot->>Boot: prepareCanvas(#game) 2D + createHostDevContext(无 engineFactory)
|
||||
Boot->>Boot: 浏览器 RAF + mockNowMs 墙钟 → frame(dtReal) → drawFrame(#game)
|
||||
end
|
||||
```
|
||||
|
||||
**关键数据流不变量**:
|
||||
- real:掌帧唯一源=引擎(游戏不自起 RAF);绘制面唯一=引擎 `mainContext`(游戏美术+引擎粒子同屏);插件能力经 `ctx.getEngine()`(下层已冻)。
|
||||
- **real 时序硬约束(复审 M1/M4)**:`createHostDevContext`(bundle)、`game.init`、输入桥挂 `mainCanvas`、就绪标记 `__refBootedReal=true` **全部在 `engineInit().then(setupReal)` 内**(上图 `.then() → mainContext 就绪` 之后)——引擎掌帧的必然结构:引擎先起、`mainContext`/`mainCanvas` 才有;顶层提前建 bundle 会快照错 ctx(`host.js:323-324`)。
|
||||
- stub:与今天 `main.js` 逐行等价(回滚锚);就绪标记仍用 `index.html` 的 `__refBooted`(`index.html:81`)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 装载契约 IDL(新立 additive 第 9 类契约 · `game-runtime/src/core/game-host.d.ts`)
|
||||
|
||||
> **设计原则**:游戏 = 纯模块工厂(零引擎 import,Q4)。host 经 `engineInit` 五回调驱动游戏生命周期;游戏绘制面 `render(g)` 的 `g` = 引擎 `mainContext`(boot 注入);插件能力经下层已冻结 `ctx.getEngine()`(不在本契约重复定义)。**与 `api.d.ts` 是 additive 关系:本契约只新增 `GameHostFactory`/`GameInstance`/`GameHostBootContext` 三个类型,受控面 6 项零改。**
|
||||
> **落点(复审改)**:落 `game-runtime/src/core/game-host.d.ts`(与 `api.d.ts` 同目录),故 import 方向自然=同目录 sibling `./api.d.ts`;**不**落 contracts/(理由见 §12-A1)。
|
||||
|
||||
```typescript
|
||||
// game-runtime/src/core/game-host.d.ts —— 游戏宿主装载契约(第 9 类契约 · additive · game-runtime 内部)
|
||||
// owner:Runner v2 P1 | 消费方:集成段 host(boot 游戏)+ 生成 agent(产出靶)
|
||||
//
|
||||
// 【为什么新立、不折进 api.d.ts】
|
||||
// api.d.ts 的 PluginContext 是「插件↔引擎」面(getEngine() 等,api.d.ts:303,已冻结);
|
||||
// 本契约是「游戏整体↔引擎宿主」面——游戏的 update/render 主体不是插件、不走 PluginContext。
|
||||
// 两者正交(边界模型「同一职不留两条并行路」):插件经 getEngine() 拿能力,游戏经本契约挂宿主。
|
||||
// 引擎 import 只活集成段 host(Q4 铁律),游戏工厂零引擎 import。
|
||||
// 【为什么落 game-runtime/src 而非 contracts/】
|
||||
// contracts/ 是跨三仓五工位的业务契约 SSOT(API/DB/SDK/event…,contracts/README.md §1);
|
||||
// 本契约是 game-runtime 内部「host↔游戏工厂」接线面,消费方只在 game-runtime,
|
||||
// 故与 api.d.ts 同目录、import 方向顺;落 contracts 会让跨仓 SSOT 反向相对 import ../game-runtime/src(拆仓即断)。
|
||||
|
||||
import type { PluginContext } from './api.d.ts';
|
||||
|
||||
/**
|
||||
* 游戏挂宿主时拿到的 boot 上下文。host 在游戏 init 时一次性注入。
|
||||
* - ctx:下层已冻结受控面(PluginContext)。游戏经 ctx.getEngine() 拿插件能力(粒子/音频/数学),
|
||||
* 经 ctx.getInput() 拿受控输入,经 ctx.random/time 拿受控随机/时钟。**不得绕过 ctx 触碰裸引擎/DOM。**
|
||||
* - mainContext:引擎 mainContext(setGLEnable(false) 后的纯 2D 绘制面)。游戏 render(g) 的 g 即它;
|
||||
* 游戏美术画在此面 → 与引擎粒子(引擎自渲落同一 mainContext)同屏。**此字段是装载契约的核心**
|
||||
* (游戏取引擎绘制面的唯一约定,不借 PluginContext.getContext2d——那是插件面)。
|
||||
* - seed:主随机种子(确定性);canvas:可见画布引用(输入 rect 映射用,real=引擎 mainCanvas)。
|
||||
*/
|
||||
export interface GameHostBootContext {
|
||||
ctx: PluginContext; // 下层已冻结受控面(复用)
|
||||
mainContext: CanvasRenderingContext2D; // 引擎 mainContext(boot 注入,render 绘制面)
|
||||
canvas: HTMLCanvasElement; // 可见画布(real=引擎 mainCanvas,输入 rect 映射用)
|
||||
seed: number; // 主随机种子
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏实例:host 经引擎五回调驱动的稳定生命周期。
|
||||
* 生命周期对齐 engineInit 五回调(host.js:883-887 engGameInit/Update/Render):
|
||||
* · init ← gameInit(引擎首帧前一次性):建游戏对象、订阅 ctx.getInput()、装资产。
|
||||
* · update(dt) ← gameUpdate(引擎每逻辑步,dt=FIXED_DT 恒定):core.update/事件/特效计时。
|
||||
* · render(g) ← gameRender(引擎清屏后,g=mainContext):画游戏美术;引擎粒子已自渲落同一 g。
|
||||
* · onInput(ev)(可选):归一化输入事件回调。**注**:ref 经 ctx.getInput().on 订阅即可,
|
||||
* onInput 是「不便用 ctx.getInput 的极简游戏」的备选入口;二者择一,ref 用前者。
|
||||
* · destroy():卸载卫生(P2 装载多游戏复位用;P1 ref 单游戏可空实现但须声明)。
|
||||
*/
|
||||
export interface GameInstance {
|
||||
init(boot: GameHostBootContext): void | Promise<void>; // 异步:装资产(ref loadAssets 异步)
|
||||
update(dt: number): void; // 逻辑步(dt 秒,引擎恒定步长)
|
||||
render(g: CanvasRenderingContext2D): void; // g=mainContext(boot 注入的同一面)
|
||||
onInput?(ev: { type: string; x?: number; y?: number; key?: string }): void; // 可选输入入口
|
||||
destroy(): void; // 卸载(幂等)
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏工厂:纯模块导出(零引擎 import)。host 调它建 GameInstance。
|
||||
* 生成 agent 的唯一产出靶 = 实现并默认导出一个 GameHostFactory。
|
||||
* 【硬约束·§12-A3】必须是「独立、可被 P3 生成 agent 复制的工厂模块」:
|
||||
* · 独立文件默认导出(ref=games/wanglanmei-ref/src/game-shop.js 的 `export default createShopGame`);
|
||||
* · 零引擎 import(引擎能力一律经 boot.ctx.getEngine());
|
||||
* · 禁在 main.js 内用内联闭包「形似实现」糊过——否则 P3 缺真实参照实现,违 P1「spike 装载契约到可被 P3 复制」目标。
|
||||
* @param opts 工厂选项(无玩法语义;预留 difficulty/variant 等 additive 扩展位)。
|
||||
*/
|
||||
export type GameHostFactory = (opts?: Record<string, unknown>) => GameInstance;
|
||||
```
|
||||
|
||||
**IDL 三条不可破约定(对齐边界模型)**:
|
||||
1. **掌帧唯一源=引擎**:`GameInstance` 不含任何 RAF/计时器;`update/render` 只被引擎五回调驱动。**推论(§12-M2)**:取证 driver 在 real 通道亦不得手摇 `stepFrames`(否则与引擎 RAF 双掌帧,破此条)——真触摸后等引擎自然推帧 + 轮询状态。
|
||||
2. **绘制面唯一=引擎 `mainContext`**:`render(g)` 的 g 必为 boot 注入的 `mainContext`;游戏美术与引擎粒子同屏。**推论(§12-M4)**:real 通道 `bundle` 必须在 `engineInit().then` 内、以 `context2d=mainContext` 建(受控面 getContext2d 一次性快照闭包捕获值,`host.js:323-324`);不得在顶层先建再改 ctx。
|
||||
3. **能力唯一面=下层 `ctx.getEngine()`**:粒子/音频/easing 经它(不在本契约重复定义引擎能力面)。
|
||||
|
||||
**additive 关系明确**:`game-host.d.ts` 仅 `import type { PluginContext } from './api.d.ts'`,不修改 `api.d.ts` 任何导出;受控面 6 项零改。
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键实现步骤(有序)
|
||||
|
||||
### 步骤 0:核前置(执行首步,read-only · 常规核对)
|
||||
- mini-desktop scratch clone 后核 `games/wanglanmei-ref/entry.js` 加 `import * as LJS from 'littlejsengine'` 能被 esbuild 解析(与 host-dev entry 同 monorepo node_modules)。**口径(复审降级·§12-A5)**:此项已被 host-dev 既有事实强力支撑(`host-dev/entry.js` 末行已 `import * as LJS from 'littlejsengine'` 且 host-dev real 门 6/6 已过,同 monorepo node_modules)→ 不是可能阻断的高不确定项,作**常规构建核对**:构建 ref-bundle 后 `grep -q ParticleEmitter dist/ref-bundle.js` 即可(无需专门 /tmp dry-build 阻断门)。
|
||||
|
||||
### 步骤 1:新立装载契约工件 `game-runtime/src/core/game-host.d.ts`(§12-A1)
|
||||
- 按 §5 IDL 落盘到 **`game-runtime/src/core/game-host.d.ts`**(与 `api.d.ts` 同目录);import 方向=同目录 sibling `./api.d.ts`。
|
||||
- `contracts/README.md` **不**把它折进跨仓 8 类清单,仅加一行指针登记:「游戏宿主装载契约(game-runtime 内部,落 `game-runtime/src/core/game-host.d.ts`),消费方=host boot + 生成 agent」(理由 §12-A1)。
|
||||
- **验证**:`tsc --noEmit`(若仓有 ts 校验)或 node 类型注释自洽;确认只 `import type { PluginContext } from './api.d.ts'`、不改 `api.d.ts` 任何导出(diff 证 api.d.ts 零改)。
|
||||
|
||||
### 步骤 2:提升 engine-caps.js(搬运已有件,语义零变)
|
||||
2.1 新建 `host-dev/engine-caps.js`,导出 `createEngineCaps({ LJS, engineMath, engineMode, recHook })`:
|
||||
- 把 `host.js:197-318` `makeEngineCaps` 闭包体整体搬入,**逐行保语义**:
|
||||
- 首行 `if (engineMode !== 'real') return null;`(保 null-engine 回归短路,`host.js:198`)。
|
||||
- `rec` 改为参数注入:`const rec = recHook || (() => {});`(**默认 no-op**;原闭包内 `window.__engineCalls.push` 语义由调用方经 recHook 提供)。
|
||||
- `mathRaw = engineMath`(原 `buildEngineMath(LJS)` 的结果由调用方传入,或 `createEngineCaps` 内部 `buildEngineMath(LJS)`——二选一,保 `host.js:204` 同口径)。
|
||||
- particles/audio.synth 三面包装体(`host.js:214-316`)逐字搬入,引擎引用经入参 `LJS.*`。
|
||||
2.2 `host.js` 改:删内联 `makeEngineCaps`(`197-318`),改 `import { createEngineCaps } from './engine-caps.js'`;在 `buildBundle`(`host.js:353`)处 `engineFactory: () => createEngineCaps({ LJS, engineMath: buildEngineMath(LJS), engineMode, recHook: id => { try { (window.__engineCalls||(window.__engineCalls=[])).push(id); } catch(_){} } })`。
|
||||
- **关键**:host-dev 仍注入 rec(门② 探针在 host-dev 取证产物里有,符合现状——host-dev 不是渠道产物)。
|
||||
2.3 **验证(回归基线)**:mini-desktop 跑 `a6-runbook.sh`(门0 间接 + 门②)+ gate0 driver → **门0 8/8 + real 门 6/6 逐项不变**(提升不得改任何判据数值)。
|
||||
|
||||
### 步骤 3:ref 拆出装载契约 game 工厂(`game-shop.js` · 硬约束独立文件 · §12-A3)
|
||||
- **硬约束**:必须是独立文件 `games/wanglanmei-ref/src/game-shop.js`,`export default createShopGame`(`GameHostFactory` 形态),**零引擎 import**(引擎能力一律经 `boot.ctx.getEngine()`)。**禁**在 main.js 内联闭包形似实现糊过——P3 生成 agent 需要一个真实、可复制的参照工厂(§12-A3)。stub 通道亦经此工厂的 stub 适配驱动(绘制面 g=`#game` ctx),保 stub 行为不变由逐像素门⑤兜。
|
||||
- 把 `main.js` 的游戏逻辑段与绘制段封成 `createShopGame(): GameInstance`(默认导出 `GameHostFactory`):
|
||||
- `init(boot)`:搬 `main.js:158-178` 游戏对象创建(`createShopCore`/`createLinePicker`/`createHostFx` 等)+ `main.js:165-167` 异步 `loadAssets` + `main.js:582-611` 输入订阅(经 `boot.ctx.getInput()`)。保存 `boot.mainContext` 为绘制面。
|
||||
- `update(dt)`:搬 `main.js:617-647` 逻辑段(`mockNowMs += dt*1000`;`core.update(dtWorld)`;`bundle.tick(dt)`;`applyEvents`;`maybeReplayBufferedServe`;`fx.step`;rush 联动;闲时台词;ghosts 清理;`rebuildZones`)。**注**:`bundle.tick` 仍由游戏 update 调(驱动插件 onFrame 计时;引擎粒子自渲与此解耦,同 host.js A2 注释 `host.js:726-728`)。
|
||||
- `render(g)`:搬 `main.js:648-658` 绘制段(`drawFrame(g, {...})` + `palettePost.renderPost(g)`)。**g=boot.mainContext**。
|
||||
- `destroy()`:cancel 输入订阅 + dispose registry(P1 可最小实现,须声明)。
|
||||
- **难点(real 坐标系)**:引擎 `mainContext` 无 DPR 变换(引擎每帧 `updateCanvas` 重置为单位阵)。游戏按逻辑 390×844 绘制 → `render(g)` 须在 `g.save()` 内 `g.setTransform(DPR,0,0,DPR,0,0)` + `g.imageSmoothingEnabled=false`,对齐 `host.js:489-490` renderParticles 的补偿做法。stub 通道 g=`prepareCanvas(#game)` 已自带 DPR 变换(`main.js:69`)→ 不补。
|
||||
|
||||
### 步骤 4:main.js bootRef real 通道接线
|
||||
4.1 读 `engineMode = getParam('engine', 'stub')`(**缺省 stub** 保旧;real 显式开。注:host-dev 缺省 real,但 ref 缺省 stub 更稳——真玩取证显式带 `&engine=real`)。
|
||||
4.2 **stub 分支**:逐行保旧(`prepareCanvas(#game)` + `createHostDevContext` 无 engineFactory + 浏览器 RAF + `frame(dtReal)` + `drawFrame(#game)`)。把现有 `main.js` 主体收进 stub 分支(或经 game 工厂的 stub 适配,但 stub 务必行为不变——逐像素门⑤兜)。
|
||||
4.3 **real 分支**(仿 `host.js:897-928`;控制流=实质重构,不是局部插桩——§12-M4):
|
||||
- `LJS.setGLEnable(false)` / `setShowSplashScreen(false)` / `setDebugWatermark(false)` / `setCanvasClearColor(LJS.BLACK)` / `setCanvasFixedSize(vec2(390*DPR, 844*DPR))`(全在 engineInit 前)。
|
||||
- 建独立 `engineRoot` div 挂 body(不复用 `#wrap`,引擎会覆盖容器样式,`host.js:911-915`)。
|
||||
- `engineMath = buildEngineMath(LJS)`;`engineFactory = () => createEngineCaps({ LJS, engineMath, engineMode:'real', recHook })`(取证时注 recHook,渠道不注 —— 见步骤 7)。
|
||||
- `engineInit(engGameInit, engGameUpdate, engGameUpdatePost, engGameRender, engGameRenderPost, [], engineRoot).then(() => setupReal(LJS.mainContext)).catch(e => { window.__refBootError=String(e&&e.message||e); window.__refBootedReal=false; ... })`。
|
||||
- **`setupReal(mainContext)` —— 一切 bundle/game/输入/就绪标记都在此(engineInit().then 内)**:
|
||||
- **【M4 硬约束·bundle 必须延后到此建】** `bundle = createHostDevContext({ context2d: mainContext, seed, audioFactory, clock: () => mockNowMs, engineFactory })`。**context2d 必为 mainContext**——受控面 getContext2d 一次性快照闭包捕获的 ctx(`host.js:323-324` 注释明确「读闭包捕获值,故必须就绪后建 bundle」);**绝不可在顶层先用 `#game` ctx 建 bundle 再改**,否则 getContext2d 快照到 stub `#game` ctx,受控面 context2d 与游戏 render 面分裂(= spec 要消除的渲染面分裂,会换形式复现)。可直接引 `host.js:353-371`(buildBundle)+ `host.js:379-381`(setupAfterEngine 在 then 内建 bundle)作样板。
|
||||
- 建 game 工厂 → `game.init({ ctx: bundle.context, mainContext, canvas: LJS.mainCanvas, seed })`(异步,`init` 内 await loadAssets)。
|
||||
- 挂 `window.__refHost`、`window.__refForensics`、`window.__refChecklist`(**全部移进 then 内挂,real 下顶层不可早挂**——顶层时 bundle/game 尚不存在)。
|
||||
- `LJS.mainCanvas.id='game-engine'`(CDP 取证选择器,仿 `host.js:830`);暴露 `window.__engine` 双时钟(仿 `host.js:840`)。
|
||||
- **【M1 硬约束·real 专属就绪标记】** game.init 完成后置 `window.__refBootedReal = true`(仿 host-dev `__hostBootedReal`,`host.js:920`)。**这是 real 通道唯一就绪信号**——必须在 engineInit().then 内、game.init/资产/受控面全就绪后置。**stub 通道照旧用 `index.html` 的 `__refBooted`(bootRef().then 置,`index.html:81`),real 通道额外置 `__refBootedReal`**。driver 在 real 下改等 `__refBootedReal`(步骤 8 + §12-M1)。
|
||||
- 五回调:`engGameUpdate(){ game.update(FIXED_DT); }`;`engGameRender(){ game.render(mainContext); }`;其余空/留痕(`engGameInit(){ window.__refEngineInitFired=true; }`)。五回调函数定义在 bootRef 作用域(engineInit 须先拿到),实体经 holder 转发到 setupReal 内真实现(仿 `host.js:880-887` 的 `_engStep`/`_renderParticles` holder 范式)。
|
||||
- **mockNowMs**:real 下由 `game.update` 内 `mockNowMs += dt*1000` 累加(逻辑帧钟,非墙钟,同 `host.js:526`)。
|
||||
- **RAF 门控**:`startRaf/rafLoop`(`main.js:668-691`)仅 stub 调;real 分支不调(引擎内部 RAF 驱动五回调)。
|
||||
4.4 输入(real·控制流重构 §12-M4):real 下输入监听**改挂 `LJS.mainCanvas`**(不再硬绑传入的 `opts.canvas`=`#game`;坐标按引擎 canvas rect,仿 `host.js:536-538` `inputTarget = real ? LJS.mainCanvas : canvas`);`toCanvasXY` 的 `getBoundingClientRect` 改读 `LJS.mainCanvas`。监听挂载 + `inputBridge` 取用同样移进 `setupReal`(bundle 就绪后才有 `bundle.inputBridge`)。经 `bundle.inputBridge._emit` 灌受控面;游戏经 `boot.ctx.getInput().on` 订阅(同 `main.js:604-611`,订阅在 game 工厂 `init` 内)。**stub 通道输入逐行不变**(挂 `#game`,`main.js:583-602`)。
|
||||
|
||||
### 步骤 5:entry.js 引擎入包
|
||||
- `entry.js:15` 后增 `import * as LJS from 'littlejsengine';` + `export const Engine = LJS;`(仿 `host-dev/entry.js:21/28`)。**Q4**:引擎 import 只活 entry/host,game 工厂零引擎 import(构建后核 `game-shop.js` 编译产物经 LJS namespace 间接,不直 import)。
|
||||
|
||||
### 步骤 6:SIZES.md 口径
|
||||
- 增"游戏产物(引擎+包装层)"表:ref stub=197KB(不打引擎)/ ref real≈540KB raw min(引擎真入);标 gz 实测(`size.mjs`);注"插件增量法不覆盖游戏产物口径"。
|
||||
|
||||
### 步骤 7:探针绝不入渠道产物(物理保证)
|
||||
- `createEngineCaps` 的 `recHook` **默认 no-op**;`engineFactory` 默认不传 recHook。
|
||||
- **王蓝莓真玩取证**:取证脚本经 URL/全局开关让 main.js real 分支注入 `recHook = id => window.__engineCalls.push(id)`(仅取证路径)。
|
||||
- **渠道产物(未来 P2/P3 出仓 bundle)**:esbuild 入口不引取证注入点 → recHook 恒 no-op → 探针代码无副作用、不挂 `window.__engineCalls`。**P1 阶段 ref-bundle 即取证用**,注 recHook 可接受(ref-bundle 非渠道产物,同 host-bundle 现状 `host.js:202`)。
|
||||
|
||||
### 步骤 8:e2e driver real 通道改造清单(范式切换·非增量 · §12-M1/M2/M3)
|
||||
|
||||
> **背景(复审 major 2/1)**:现行 `e2e-input.cdp.cjs` driver 主体建在 **`window.__refHost.stepFrames(n)` 手摇逻辑帧** 上(`e2e-input.cdp.cjs:109`,贯穿 `:144` tapAt 内 `stepFrames(3)`、`:236/:277` 营业循环 `stepFrames(20)`),坐标 rect 写死 `document.getElementById('game')`(`:127`),就绪只等 `__refBooted/__refHost`(`:76`)。这三处在 real 通道**整体失效**——必须按下列清单改造,**不是「加个 engine=real 通道断言」的增量**。
|
||||
|
||||
real 通道 driver 改造(保 stub 通道路径逐行不变,real 走新分支):
|
||||
|
||||
(a) **就绪标记(M1)**:`waitRefBoot` 在 `engine=real` 时改等 **`window.__refBootedReal`**(而非 `__refBooted`)。real 下 `__refBooted`(`index.html:81` bootRef().then 置)会在引擎/game 就绪前被置 true(bootRef 顶层同步段先 resolve),driver 据它驱动会打空/读 undefined → 假失败或间歇绿。host-dev 正是为此引入 `__hostBootedReal`(`host.js:920`,注释「CDP 取证须等 __hostBootedReal 再驱动」`host.js:851/931`)。新分支:`evaluate('({ b: !!window.__refBootedReal, e: window.__refBootError||null, h: !!window.__refHost })')`。
|
||||
|
||||
(b) **坐标 rect 选择器(M3)**:`initMapping` 的 `document.getElementById('game')`(`:127`)在 `engine=real` 时切到 **`document.getElementById('game-engine')`**(引擎自建可见画布,`host.js:830`;real 下 `#game` 不是可见画布)。否则取错画布/null → 坐标全错。stub 通道仍用 `#game`。
|
||||
|
||||
(c) **确定性序列范式切换(M2·硬·边界铁律)**:real 通道**禁用 `stepFrames` 手摇**——引擎内部 RAF 已掌帧(装载契约第 1 条「掌帧唯一源=引擎」),game 不暴露/不手摇 `stepFrames`,否则与引擎 RAF 双掌帧违铁律。原「tapAt 后 stepFrames(3)」「循环 stepFrames(20)」改为:**真触摸 → `delay(ms)` 等引擎自然推帧 → 轮询 `__refForensics.state()`/`measures()` 判相位推进**(轮询到目标态或超时)。这与 stub 的「手摇定量推帧」是**范式切换**(harness §5-坑6 已订:逐像素门交 stub 通道独占手摇,引擎通道只证掌帧+渲染落 2D+真玩闭环,不比逐像素哈希)。real 真玩去掉 `mode=evidence`(real 取证不靠 stepFrames,靠真触摸+轮询)。
|
||||
|
||||
(d) **同屏像素扫描**:合 gate0 G0-7 + a6 A6-5 像素扫描,CANVAS=`#game-engine`,判据见 §8 门②(暖色阈值已开工前坐实,见门②)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 边界失败路径
|
||||
|
||||
| 失败 | 触发 | 处置 |
|
||||
|---|---|---|
|
||||
| 引擎 engineInit 失败 | WebGL/资源异常 | 镜像 `window.__refBootError`(仿 `host.js:925`),status 提示"可加 `?engine=stub` 回滚";门④/CDP 据此判 real 失败→回滚 stub |
|
||||
| **driver 在引擎就绪前驱动(打空/读 undefined)** | real 下 `__refBooted` 早于引擎/game 就绪被置(§12-M1) | `__refBootedReal` 只在 engineInit().then 内 game.init 完成后置;driver real 分支等它(步骤 8a) |
|
||||
| 资产装载缺失 | 图片/mp3 缺 | 游戏 init 走程序化回退(保 `main.js:165` 容错),照常启动;取证 `waitAssets` 超时如实记录续跑(`e2e-input.cdp.cjs:87`) |
|
||||
| 粒子渲到分离画布(假阳性) | 绘制面未真改 mainContext | 门② 同屏像素判据逮(游戏画布粒子命中=0=fail),见 §8 门② |
|
||||
| AudioContext headless suspended | 无手势 | `--autoplay-policy=no-user-gesture-required`(harness §5);门② 音频降软门(同 `a6-engine-wiring.cjs:89`) |
|
||||
| engine-caps 提升回归 | 探针/短路语义漂移 | 步骤 2.3 门0 8/8 + real 门 6/6 回归卡住;不过即 revert engine-caps.js |
|
||||
| 受控面被改 | 误扩 6 项 | code review + 契约 diff 卡死(硬约束) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 验证方法(harness 门)
|
||||
|
||||
> 全在 mini-desktop 跑(6c6g 禁 chrome)。**部署口径=沿现行 `ref-e2e-runbook.sh` 的 git-clone-scratch(复审改·§12-A4)**:`git clone --depth 50 --branch dev/2.0.0 ssh://git@101.200.34.71:2222/...` → `git checkout <精确SHA>` 到 scratch(`ref-e2e-runbook.sh:20-21`)→ esbuild 锁参构建 → IP gate → systemd-run serve(`static-serve.cjs` 根=`games/wanglanmei-ref` 端口 4320,`:44`)+chrome 两单元(`:47`)→ 就绪 10×0.5s + serve 5×6s 在线 + CDP 10×2s 两段探活(`:55/:59/:64`)→ driver(`:69`)→ journal 归档 → 停单元。chrome 旗标含 `--autoplay-policy=no-user-gesture-required`(`ref-e2e-runbook.sh:48`,harness §5)。**real 版只需在 runbook 的 driver URL 加 `&engine=real` + 构建后 `grep -q ParticleEmitter dist/ref-bundle.js` 核引擎入包**。**严禁裸 `./` rsync / tar-pipe 整树**(6c6g cwd 易重置到仓根吞整树,harness §6-律4);git-clone-scratch 是 ref runbook 既有口径,勿改成 host-dev/a6 的 tar-pipe staging 配方。
|
||||
|
||||
**构建命令**(mini-desktop,cwd=`game-runtime`):
|
||||
```
|
||||
node scripts/build.mjs games/wanglanmei-ref/entry.js games/wanglanmei-ref/dist/ref-bundle.js --global-name=WanglanmeiRef
|
||||
# 此时引擎真入包 ~540KB;核对:grep -q ParticleEmitter dist/ref-bundle.js
|
||||
```
|
||||
**真玩 URL**:`http://localhost:4320/?engine=real&seed=305441741`(取证模式 `&mode=evidence` 用 `__refHost` 确定性驱动;真人交互去掉 mode)。
|
||||
|
||||
| 门 | 判据(可执行阈值) | driver / 口径 |
|
||||
|---|---|---|
|
||||
| **① 掌帧** | `window.__engine.snapshot()` 两采样间隔 500ms,`frame` 增量 ∈ `[20,45]`(@60fps ~30,排单步假推进+失控空转) | 复用 gate0 G0-2(`gate0-engine-takeover.cjs:75`);ref driver 加同款断言(ref 暴露 `window.__engine`) |
|
||||
| **② 同屏像素(堵假阳性)** | 截 `#game-engine`(引擎接管后 DOM 可见、=mainContext 的 canvas,`host.js:830`)。**两类命中均 >0**:(a) **游戏美术·暖色(开工前已坐实,非占位)**:店面暖色主色像素命中数 `warmHits > N`(N=200,按 780×1688 抽样网格调,详见下「门②(a) 暖色判据」);(b) **引擎粒子·亮色**:burst 白 `brightHits>0` ∧ `maxCh>80`(a6 A6-5 口径,`a6-engine-wiring.cjs:111`)。**为何这证合并非两画布**:若粒子渲到另一张分离 canvas,则截 `#game-engine` 时粒子命中=0(粒子不在这张)→ (b) fail;若游戏渲到 #game 旧画布、引擎渲 mainContext,则截 `#game-engine` 时游戏命中=0→ (a) fail。**两类同时命中 ⟺ 两者真在同一张 `#game-engine` 上** | ref driver 新增(合 gate0 G0-7 + a6 A6-5 像素扫描,CANVAS=`#game-engine`) |
|
||||
| **③ 门②真接线** | 取证注 recHook 后,`window.__engineCalls` 含 `particles.spawnEmitter` call-ID(经 particles-juice 真实 spawnEmitter,非直调引擎) | 复用 a6 A6-2(`a6-engine-wiring.cjs:79`);ref driver 触发游戏内真粒子(serve_ok 飞行/burst)后读 `__engineCalls` |
|
||||
| **④ CDP 真玩 + 四件套** | CDP 真触摸逐 zone 真玩闭环(开店→进货→营业→结算→升级),首日接待数>0、serve_ok>0、到达结算、升级 `upgrades.speed1=true`;四件套 evidence(输入轨迹 JSONL/截图+canvas 哈希/probe JSONL/checklist JSON)落 `evidence/round-N/`。**zone 名以 `main.js` computeZones 为唯一真相**(serve-/inc-/up-,`render.js:967/975/981`),驱动器猜语义名必踩坑(harness §2-律4) | 扩 `e2e-input.cdp.cjs` 加 real 通道(步骤 8:`&engine=real`、选择器 `#game-engine`、等 `__refBootedReal`、真触摸+轮询,harness §2-律2) |
|
||||
| **⑤ stub 回归** | `?engine=stub&mode=evidence` 菜单帧双开哈希一致(清档 reload 后比,harness §5-坑6);ref 49 单测仍绿 | 现有 `e2e-input.cdp.cjs` 门 B(stub 通道不变) |
|
||||
| **⑥ 提升回归** | engine-caps 提升后:host-dev 门0 8/8 + real 门(a6)6/6 逐项不变 | 步骤 2.3 |
|
||||
| **⑦ console 干净** | `uncaught=[]`(real 通道,含未捕获 error/unhandledrejection) | ref driver(`main.js:90-96` uncaught 归集) |
|
||||
|
||||
> **门④ driver 范式(复审 M2)**:real 通道 driver=真触摸+轮询(**禁 stepFrames 手摇**,否则双掌帧违铁律),坐标走 `#game-engine` rect,就绪等 `__refBootedReal`——见步骤 8。zone 名以 `main.js computeZones` 为唯一真相(`serve-`/`inc-`/`up-`,`render.js:967/975/981`),驱动器猜语义名必踩坑(harness §2-律4)。
|
||||
|
||||
### 门②(a) 暖色判据(开工前坐实 · 复审 major 3)
|
||||
|
||||
> **背景**:gate0 G0-7 的 `coloredHits` 判据是 **青蓝**(`b>120 && b>r+30`,`gate0-engine-takeover.cjs:111`),a6 A6-5 是 **亮度**(`max(r,g,b)>80`,`:111`,注释明告青蓝口径对引擎白粒子失效)——二者均**不适配**王蓝莓暖色店面(R 高 B 低,与青蓝正相反)。harness §6-2 已订「换引擎能力后像素门颜色假设必同步重核」。故门②(a) 判据**开工前从 `palette.js` 坐实**,不留执行时占位。
|
||||
|
||||
**已坐实色域**(源:`games/wanglanmei-ref/src/palette.js:23-41` BASE 暖调家族,已读真源核对):
|
||||
|
||||
| 用途 | 色名 | RGB | R−B | 命中 |
|
||||
|---|---|---|---|---|
|
||||
| 柜台/货架木带 | `wood` | (169,118,79) | 90 | ✓ |
|
||||
| 深木描边 | `woodDark` | (124,84,55) | 69 | (R<140 不计,wood 覆盖) |
|
||||
| 砖橙按钮/警示 | `brick` | (201,111,74) | 127 | ✓ |
|
||||
| 金/钱强调 | `gold` | (232,169,66) | 166 | ✓ |
|
||||
| 琥珀进度条 | `amber` | (240,196,117) | 123 | ✓ |
|
||||
| 差评红 | `danger` | (178,72,54) | 124 | ✓ |
|
||||
| **纸底(须排除)** | `paper` `#f6e7c9` | (246,231,201) | 45 | ✗ 反例 |
|
||||
| 高亮纸(须排除) | `paperHi` | (255,246,227) | 28 | ✗ 反例 |
|
||||
| 暖天光(须排除) | `sky` | (243,217,168) | 75 | ✗ 反例 |
|
||||
| 奶油粒子亮色(须排除) | `cream` | (255,243,220) | 35 | ✗ 反例(近白,归引擎粒子域) |
|
||||
|
||||
**判据(暖木/砖/金色域,排纸底与暖天光)**:
|
||||
```
|
||||
warmHit ⟺ (r > 140) && (r - b > 85) && (g < r)
|
||||
```
|
||||
- 命中:wood(R169,R−B90,118<169)✓ / brick(201,127,✓) / gold(232,166,✓) / amber(240,123,✓) / danger(178,124,✓)——店面木带/货架/按钮/HUD 金额(`render.js` 大量 `pal.c('wood'|'brick'|'gold'|'amber')`,已读真源:`render.js:120/165/179/200/253/308` 等);
|
||||
- 排除:paper(R−B45<85)✗ / paperHi(28)✗ / sky(75<85)✗ / cream(35)✗——纸底/天光/近白粒子均落空,堵「把暖纸背景误判成店面美术」的假阳性;
|
||||
- 鲁棒性:rush 压力态色板经 `shiftHsl(hue −10, sat 1.18, light 0.9)`(`palette.js:57`)把暖色**更暖**(R−B 进一步拉大、饱和升),仍稳在色域内,不会因 rush 漂出;
|
||||
- 阈值 `warmHits > 200`:扫 780×1688 backing(或抽样网格,与 gate0/a6 同扫法),店面木带+货架+按钮+招牌占屏面积远超 200 抽样点;过窄(真渲也判 0=假阴性)与过宽(纸底误命中=假阳性)两侧均已由上面色域不等式+反例锚定。**N 在 driver 落地时按实际抽样网格密度微调,但色域不等式不动**(不等式是堵假阳性的客观锚,开工前定死)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 完成条件
|
||||
|
||||
- 装载契约 **`game-runtime/src/core/game-host.d.ts`** 落盘(§12-A1)+ `contracts/README.md` 指针登记(不折进跨仓 8 类);只 `import type { PluginContext } from './api.d.ts'`、`api.d.ts` 零改(diff 证)。
|
||||
- game 工厂 `game-shop.js` 为**独立文件、默认导出 `GameHostFactory`、零引擎 import**(非 main.js 内联,§12-A3 硬约束)。
|
||||
- `host-dev/engine-caps.js` 提升完成;host.js import 它;**门0 8/8 + real 门 6/6 回归逐项不变**(evidence 证)。
|
||||
- ref `?engine=real` 通道:门① 掌帧 ∈[20,45] ∧ 门② 同屏(`#game-engine` 上游戏美术 `warmHits>200`(§门②(a) 坐实暖色判据)+ 引擎粒子 `brightHits>0∧maxCh>80` 双命中)∧ 门③ `particles.spawnEmitter` call-ID ∧ 门④ CDP 真玩闭环(**真触摸+轮询范式,非 stepFrames 手摇**)+四件套 ∧ 门⑦ console 零未捕获——**全绿**。
|
||||
- real 通道就绪标记 `__refBootedReal` 接线正确(driver real 分支等它,§12-M1);bundle 在 engineInit().then 内以 `context2d=mainContext` 建(§12-M4)。
|
||||
- ref `?engine=stub` 门⑤ 逐像素双开哈希一致 + 49 单测绿(stub 通道行为逐行不变)。
|
||||
- SIZES.md 增游戏产物口径(gz 实测)。
|
||||
- 受控面 6 项契约零改(硬约束守住)。
|
||||
- 人在 mini-desktop 浏览器 `?engine=real` 真玩一局(开店→营业→结算→升级),截图归档。
|
||||
|
||||
---
|
||||
|
||||
## 10. 回滚
|
||||
|
||||
- **代码**:`?engine=stub` 与今天 `main.js` 逐行等价;real 过不了→用户/取证走 stub,既有交付零连累。
|
||||
- **契约**:`game-runtime/src/core/game-host.d.ts` additive(落 src/core,§12-A1),移除不破存量;受控面零改。
|
||||
- **模块**:engine-caps.js 提升若回归→git revert 该文件 + host.js import 行(host.js 退内联闭包),ref real 自带 engineFactory 经 createEngineCaps 不连累。
|
||||
- **构建**:ref-bundle stub 版(197KB,不打引擎)随时可重出,回退引擎入包。
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险与假设(诚实标注)
|
||||
|
||||
- **[verified]** 装载契约全仓不存在(grep 零命中);ref 游戏层 file:line(`main.js:112/583-602/618/621/648/668-691`,`stepFrames` 手摇 `:664/:109`);host.js real 范式(`host.js:323-324/353-371/379-381/488-490/536-538/830/840/904/917-920/930-932`,缺省 real `:340`);index.html `__refBooted` 置位时机(`index.html:80-81`);driver 现状(`e2e-input.cdp.cjs:76/109/127/144/236/277`);makeEngineCaps 闭包(`host.js:197-318`)+ engineFactory 注入(`host.js:359`);burst colorStart 白(`particles-juice/impl.js:217`,gate0 青蓝 `:111` / a6 亮度 `:111`);palette 暖色 BASE(`palette.js:23-41`,rush shiftHsl `:57`);zone kind 枚举(`render.js:967/975/981`);ref runbook git-clone-scratch(`ref-e2e-runbook.sh:20-21/44/47/48`);contracts 跨仓 SSOT 定位(`contracts/README.md:1-3` §1,含拆仓计划);sdk-interface.d.ts 自洽无 game-runtime import(先例)。
|
||||
- **[verified·复审降级]** ref entry 加引擎 import 能被 esbuild 解析入包——host-dev/entry.js 末行已 `import * as LJS from 'littlejsengine'` 且 host-dev real 门 6/6 已过(同 monorepo node_modules),已被既有事实强力支撑;作常规构建核对(`grep -q ParticleEmitter`,步骤 0),非阻断门(§12-A5)。
|
||||
- **[inferred]** ref real ~540KB 不触 B1 入场券——基于 host-dev entry 544KB 实测 + gz 量级;P1 用 `size.mjs` 实测游戏产物 gz 坐实(步骤 6)。
|
||||
- **[已裁·非待确认]** game 工厂**必须**独立文件 `game-shop.js`、默认导出、零引擎 import(§12-A3 硬约束,不再留「可 main.js 内联」口子)。
|
||||
- **[已裁·非占位]** 门②(a) 暖色判据已开工前从 `palette.js` 坐实具体色域(`r>140 && r−b>85 && g<r`,排纸底/天光/近白,见 §8「门②(a) 暖色判据」)——核心门判据不带占位落地。
|
||||
- **[待确认·留创始人]** 工件命名 `game-host.d.ts`(暂名);若创始人偏好其它名(如 `game-loadable.d.ts`),执行版改名即可,形态/落点不变。**这是唯一留给创始人的 open item,不阻断执行**(命名改名零成本)。
|
||||
|
||||
---
|
||||
|
||||
## 12. 复审裁决依据(2026-06-13 对抗复审 · 逐条)
|
||||
|
||||
> 复审口径:execution 是落地契约 + 高风险(立新契约 + 改真游戏主循环),deep 校准全程读真源码、凡引代码事实带 file:line。无 fatal;4 major 全纳入、5 项不明假设逐条处置。下表为各条裁决与依据,正文已落地。
|
||||
|
||||
| 编号 | 复审条目 | 裁决 | 落地位置 / 依据 |
|
||||
|---|---|---|---|
|
||||
| **M1** | real 就绪标记缺失(`__refBooted` 早于引擎/game 就绪被置,driver 据它打空/flaky) | **纳入·硬约束**:新增 real 专属 `window.__refBootedReal`(只在 `engineInit().then` 内、game.init 完成后置);driver real 分支改等它 | 步骤 4.3(setupReal 末置 `__refBootedReal`)+ 步骤 8a(driver 等它)+ §7 失败路径;依据 `index.html:81`/`host.js:920/851/931`/`e2e-input.cdp.cjs:76` |
|
||||
| **M2** | stepFrames 手摇范式在引擎掌帧下失效(且沿用=双掌帧违铁律);改造被低估为「加通道断言」 | **纳入·范式切换**:real driver 禁 stepFrames,改真触摸+`delay`等引擎自然推帧+轮询状态;明列这是相对 stub 的范式切换非增量 | 步骤 8c(确定性序列范式切换)+ §8 门④范式注 + IDL 不可破约定 1 推论;依据 `e2e-input.cdp.cjs:109/144/236/277`、harness §5-坑6 |
|
||||
| **M3** | 门②(a) 暖色阈值与 gate0/a6 青蓝/亮度口径相反,却留执行时占位 | **纳入·开工前坐实**:从 `palette.js` BASE 取定暖色色域不等式 `r>140 && r−b>85 && g<r` + 纸底/天光/近白反例排除;核心门判据不带占位 | §8「门②(a) 暖色判据」(已坐实色域表+不等式+反例);依据 `palette.js:23-41/57`、`gate0:111`、`a6:111`、harness §6-2 |
|
||||
| **M4** | render(g) 注入路径 / bundle 重建顺序未钉死;ref 顶层同步建 bundle 会快照错 ctx | **纳入·硬步骤**:real bundle 必须在 `engineInit().then(setupReal)` 内、以 `context2d=mainContext` 建(不得顶层先建再改);输入桥+game.init+就绪标记同移进 setupReal;明示这是 bootRef 控制流重构 | 步骤 4.3(M4 硬约束)+ 4.4(输入移进 setupReal)+ IDL 不可破约定 2 推论;依据 `host.js:323-324/353-371/379-381/536-538`、`main.js:112/584/590-602` |
|
||||
| **A1** | IDL 落 contracts/ 并反向相对 import `../game-runtime/src/api.d.ts`(import 方向/落点合理性) | **改落点**:落 `game-runtime/src/core/game-host.d.ts`(与 api.d.ts 同目录,import=sibling `./api.d.ts`);不折进 contracts/ 跨仓 8 类(仅 README 指针)。理由:contracts/ 是跨三仓五工位业务契约 SSOT,本契约是 game-runtime 内部接线面,让 SSOT 反向相对依赖业务源码违「契约即 SSOT」且拆仓即断 | 头部「装载契约落点」+ §3 文件表 + §5 IDL 落点注 + 步骤 1;依据 `contracts/README.md:1-3`(拆仓计划)、`sdk-interface.d.ts`(自洽先例) |
|
||||
| **A2** | ref 缺省 stub vs host-dev 缺省 real(两套缺省心智) | **显式声明此分叉是有意为之**:真玩取证显式带 `&engine=real` 更稳、stub 兜逐像素回归;P3 复制 ref 作模板时须知缺省 stub=「新游戏默认不上引擎」是过渡现状,引擎是正路(boot 注入装载契约) | 步骤 4.1(缺省 stub)+ 本表显式声明;依据 `host.js:340`(host-dev real)vs `main.js:82`(ref play/stub) |
|
||||
| **A3** | game 工厂「建议非强制」留 main.js 内联口子(P3 缺真实参照实现) | **钉成硬边界**:game 工厂必须独立文件、默认导出、零引擎 import、禁 main.js 内联糊过 | §1.2 + 步骤 3 开头 + IDL GameHostFactory 注 + §9 完成条件 + §11;依据 P1 目标(spike 装载契约到可被 P3 复制) |
|
||||
| **A4** | §8 部署写 tar-pipe,与现行 ref-e2e-runbook.sh(git-clone-scratch)不符 | **统一为 git-clone-scratch**:real 版只在 runbook driver URL 加 `&engine=real` + 构建后 grep ParticleEmitter;勿用 host-dev/a6 的 tar-pipe staging 配方 | §8 部署口径段;依据 `ref-e2e-runbook.sh:20-21/44/47/48` |
|
||||
| **A5** | 步骤0 inferred 风险被高估(host-dev 已证可入包) | **降级为常规构建核对**:grep ParticleEmitter 即可,非可能阻断的高不确定项 | 步骤 0 + §11;依据 host-dev/entry.js 已 import 引擎且 real 门 6/6 已过 |
|
||||
171
docs/agent-specs/2026-06-13-P1-王蓝莓上引擎-review.md
Normal file
171
docs/agent-specs/2026-06-13-P1-王蓝莓上引擎-review.md
Normal file
@ -0,0 +1,171 @@
|
||||
# P1 · 王蓝莓上引擎(spike 游戏宿主装载契约)· 评审版
|
||||
|
||||
> **状态**:review 版 · 2026-06-13 · 给创始人决策(结论先行,不写代码级执行细节,多用 Mermaid)。**已过对抗复审**(2026-06-13 deep·全程读真源码):**无致命项**;4 重大项 + 5 项不明假设已全部纳入 execution 版钉死(逐条裁决见 execution §12)。readiness = **ready-with-caveats**(仅 1 个非阻断 open item:契约工件命名,见 §8)。
|
||||
> **定位**:本文是 Runner v2 全弧 **P1 相**(第一相)的评审版。母梁见 [`2026-06-13-runner-v2-arc-review.md`](./2026-06-13-runner-v2-arc-review.md) §5.1。P1 的任务 = **把"游戏宿主装载契约"(arc 上层契约)用王蓝莓 spike 到真跑在引擎上**;P2/P3 归并入母 spec(见 §7)。
|
||||
> **前提(创始人已拍)**:① 推进 P1(全弧第一相);② **O1 装载契约 = 新立 additive 第 9 类契约**(不污染既有插件契约 `PluginContext`),生命周期 = `init/update/render/onInput` + `destroy`,boot 时把引擎 `mainContext` 注入游戏层;③ O2 = P2 合并进母 spec Phase D;④ 不用 fable,一律 opus。
|
||||
> **复审改点(结论级,详见 execution §12)**:契约工件**落 `game-runtime/src/core/game-host.d.ts`**(与 `api.d.ts` 同目录,**非** contracts/——contracts 是跨三仓五工位业务契约 SSOT,本契约是 game-runtime 内部 host↔游戏工厂接线面,让 SSOT 反向依赖业务源码且拆仓即断);real 通道补 `__refBootedReal` 就绪标记 + driver 范式从「stepFrames 手摇」切到「真触摸+轮询」(引擎掌帧下手摇=双掌帧违铁律);同屏门暖色阈值开工前坐实(不留占位);bundle 延后到 engineInit().then 内以 mainContext 建(防渲染面分裂)。
|
||||
> **关联**:边界铁律 [[engine-plugin-boundary-model]]、A0 地基 [[a0-engine-takeover-done]]、harness 配方 `.agents/skills/game-e2e-cdp-harness.md`(§5 门0 / §6 门②)、A0 双通道(`?engine=stub` 回滚)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论先行
|
||||
|
||||
**P1 = 给 `wanglanmei-ref` 加 `?engine=real` 通道,用它把"游戏宿主装载契约"(新立 additive 第 9 类契约)spike 实,证三件事并人在 mini-desktop 浏览器真玩:**
|
||||
1. **引擎真掌帧**(`engineInit` 五回调驱动游戏 `update/render`,废 `main.js` 自管的浏览器 RAF);
|
||||
2. **游戏美术与引擎粒子同屏可玩**(游戏绘制面从自带 `#game` 2D 画布改引擎 `mainContext`,两面合并成一张画布);
|
||||
3. **回滚保真**(`?engine=stub` 通道逐行保旧行为 + 逐像素确定性,A0 双通道范式)。
|
||||
|
||||
**同时把已存在但仍是闭包的 `makeEngineCaps`(`host.js:197-318`)提升为可 import 模块 `host-dev/engine-caps.js`**,host-dev / ref / 未来生成游戏共用同一份引擎能力包装层——**这是"模块化已有件"(搬运),非从零造**(arc §0-F2 已订正原稿"engine-caps.js 不存在=真新增工作")。
|
||||
|
||||
**装载契约的本质(O1 已拍形态)**:一款游戏 = 一个**纯模块工厂**(零引擎 import,Q4 铁律),暴露稳定生命周期 `init(ctx, mainCtx)` / `update(dt)` / `render(g)` / `onInput(ev)` / `destroy()`;其中 `render(g)` 的 `g` = 引擎 `mainContext`(boot 时注入),插件能力仍经下层已冻结的 `ctx.getEngine()` 拿。该契约落 **`game-runtime/src/core/game-host.d.ts`**(与既有 `api.d.ts` 同目录,复审改点;理由见上「复审改点」),与 `api.d.ts` 是 **additive 关系**——**受控面 6 项契约零改**(`getContext2d/getInput/getAudioContext/getEngine`,`api.d.ts:269/281/286/303`)。**game 工厂必须是独立、可被 P3 复制的实现**(独立文件默认导出、零引擎 import,禁 main.js 内联糊过——这是 P3 生成 agent 的真实参照靶,复审钉死的硬边界)。
|
||||
|
||||
> ⚠️ **为何必须新立、不能借 `PluginContext`**:ref 的游戏层(`core.update`/`drawFrame(g)`,`main.js:621/648`)**不是插件**、不走 `PluginContext`;今天全仓没有"游戏整体↔引擎 `mainContext`"的约定面(`grep GameModule/装载契约` 在 `contracts/`+`src/` **零命中**,已复证)。`getEngine()`(`api.d.ts:303`)是**插件**拿引擎能力的面,凑不上"游戏 update/render 主体怎么挂引擎"。这正是装载契约要补的缺口——它与 `PluginContext` 一个都不能替另一个(边界模型"同一职不留两条并行路")。
|
||||
|
||||
---
|
||||
|
||||
## 2. 装载契约决策(O1 已拍)
|
||||
|
||||
| 决策点 | 已拍结论 | 理由 / 证据 |
|
||||
|---|---|---|
|
||||
| **归属** | **新立 additive 第 9 类契约**,不折进 `api.d.ts` 插件面 | 避免污染插件契约语义,与边界模型"一职一路"一致;`PluginContext`=插件面、`GameHost`=游戏面,两者正交(arc O1 建议①)。 |
|
||||
| **落点(复审改)** | **`game-runtime/src/core/game-host.d.ts`**(与 `api.d.ts` 同目录),**非** `contracts/` | contracts/ 是跨三仓五工位的**业务契约** SSOT(`contracts/README.md` §1,含拆仓计划);本契约是 game-runtime 内部 host↔游戏工厂接线面,消费方只在 game-runtime,落同目录 import 方向顺(`./api.d.ts`);落 contracts 会让 SSOT 反向相对 import `../game-runtime/src`,违「契约即 SSOT」且拆仓即断(execution §12-A1)。 |
|
||||
| **生命周期粒度** | `init(ctx, mainCtx)` / `update(dt)` / `render(g)` / `onInput(ev)` / `destroy()` 五件 | `init/update/render/onInput` 对齐引擎五回调(`engGameInit/Update/Render`,`host.js:883-887`);`destroy` 补卸载卫生(P2 装载多游戏时复位)。其余(`resize/onVisibility`)additive 后补。 |
|
||||
| **绘制面约定** | `render(g)` 的 `g` = 引擎 `mainContext`,**boot 时注入游戏层** | A0 已证 `setGLEnable(false)` 后引擎纯 2D 渲进 `mainContext`(`host.js:904/919`);游戏绘制面改它即与引擎粒子同屏(消除渲染面分裂)。**不借 `PluginContext`**。 |
|
||||
| **能力获取** | 插件能力经下层已冻结 `ctx.getEngine()`;游戏绝不 `import littlejsengine` | Q4 铁律:引擎 import 只活 entry/host(`entry.js:18-21`/`host.js:44`)。 |
|
||||
|
||||
**装载关系图(游戏 ↔ host ↔ 引擎)**:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph host["集成段 host 层(唯一 import 引擎处·Q4 铁律)"]
|
||||
EI["LJS.engineInit 五回调<br/>(引擎内部 RAF 驱动)"]
|
||||
EC["engine-caps.js 模块<br/>(提升自 host.js:197 闭包·非从零)<br/>particles/audio.synth/math 三面薄包装"]
|
||||
BOOT["bootRef real 分支<br/>setGLEnable(false)+建 engineFactory<br/>+ 注入 mainContext 给游戏层"]
|
||||
end
|
||||
subgraph game["游戏侧(装载契约·零引擎 import·新立第9契约)"]
|
||||
GH["game.init(ctx, mainCtx)<br/>game.update(dt)<br/>game.render(g=mainContext)<br/>game.onInput(ev)<br/>game.destroy()"]
|
||||
end
|
||||
subgraph low["下层契约(已冻结·复用·零改)"]
|
||||
PC["PluginContext.getEngine()<br/>受控面 6 项 + 引擎能力面"]
|
||||
end
|
||||
EI -->|gameUpdate→| GH
|
||||
EI -->|gameRender→ 引擎 mainContext| GH
|
||||
BOOT -->|注入 mainCtx| GH
|
||||
EC -->|engineFactory| BOOT
|
||||
PC -. 插件能力 ctx.getEngine() .-> GH
|
||||
GH -. 禁 .->|import littlejsengine| X1["❌ Q4 违例"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 同屏方案与最大风险
|
||||
|
||||
### 3.1 同屏方案(real 通道改造主干)
|
||||
|
||||
| 改造点 | stub 通道(保旧,回滚锚) | real 通道(新增,引擎掌帧) |
|
||||
|---|---|---|
|
||||
| **掌帧源** | 浏览器 RAF + `mockNowMs`(墙钟近似),`main.js:668-691` | 引擎 `engineInit` 五回调;RAF 旁置/门控;`mockNowMs` 改逻辑帧钟(每逻辑步 +FIXED_DT×1000) |
|
||||
| **逻辑段** | `frame(dtReal)` 内 `core.update/bundle.tick/applyEvents/fx.step` | 同段移入 `game.update(dt)`,由 `gameUpdate` 回调驱动 |
|
||||
| **绘制段** | `drawFrame(g)`+`palettePost.renderPost(g)`,g=`#game` 2D | 同段移入 `game.render(g)`,g=引擎 `mainContext`(boot 注入),由 `gameRender` 回调驱动 |
|
||||
| **插件受控面 context2d** | host `prepareCanvas(#game)` 的 ctx | 快照引擎 `mainContext`(`createHostDevContext` 的 `context2d` 改它) |
|
||||
| **粒子能力** | `engineFactory` 未传 → `getEngine()`=null → 粒子 `NOOP_HANDLE`(无粒子) | 经 `engine-caps.js` 建 `engineFactory` → 粒子真经引擎 `ParticleEmitter` 自渲落 `mainContext` |
|
||||
|
||||
### 3.2 最大风险(先标在最前)
|
||||
|
||||
| 风险 | 等级 | 缓解 / 回滚 |
|
||||
|---|---|---|
|
||||
| **真渲同屏过不了 / 假阳性(粒子渲到另一张分离画布,游戏画布无粒子命中)** | **P0** | 这是 P1 第一道真门,也是全弧第一道门。判据必须堵死假阳性:**截取引擎接管后 DOM 可见那张 canvas(=`mainContext` 的 canvas,A0 已给它打 id `#game-engine`,`host.js:830`),断言同一张画布上既有游戏美术像素命中(店面暖色主色)又有引擎粒子像素命中(burst 白 maxCh>80)**。**复审改(M3)**:店面暖色阈值**开工前已从 `palette.js` 坐实具体色域**(`r>140 && r−b>85 && g<r`,排纸底/天光/近白),**不留执行时占位**——gate0/a6 的青蓝/亮度口径与暖色正相反,留占位会假阳/假阴(execution §8「门②(a)」)。若粒子渲到另一张画布 → 游戏画布粒子命中=0 → fail。**回滚=`?engine=stub`**。 |
|
||||
| **掌帧冲突**(`main.js` 自管 RAF + `mockNowMs` 与引擎掌帧直接冲突) | **P0** | A0 双通道已证引擎掌帧可行;real 通道废 RAF、`mockNowMs` 改逻辑帧钟,对齐 `host.js` real 分支既定做法(`host.js:526/884`)。 |
|
||||
| **real 通道就绪标记缺失(复审 M1)**——driver 在引擎/game 就绪前驱动→打空/间歇绿 | **P0** | real 下 `__refBooted`(`index.html:81` bootRef().then 置)早于引擎就绪;补 real 专属 `__refBootedReal`(只在 engineInit().then 内 game.init 完成后置,仿 host-dev `__hostBootedReal` `host.js:920`),driver real 分支等它(execution 步骤 4.3/8a)。 |
|
||||
| **driver 范式失效(复审 M2)**——`stepFrames` 手摇在引擎掌帧下双掌帧违铁律、`#game` rect 取错画布 | **P0** | real driver **范式切换**(非增量):禁 stepFrames,改真触摸+轮询;rect 选择器切 `#game-engine`。与 stub「手摇定量推帧」分道(stub 兜逐像素,real 只证掌帧+渲染+真玩,harness §5-坑6)。execution 步骤 8。 |
|
||||
| **渲染面分裂(复审 M4)**——real 顶层同步建 bundle 会快照错 ctx(受控面 context2d≠游戏 render 面) | **P0** | bundle 必须延后到 engineInit().then 内、以 `context2d=mainContext` 建(getContext2d 一次性快照闭包捕获值,`host.js:323-324`);输入桥+game.init+就绪标记同移进。execution 步骤 4.3(M4)。 |
|
||||
| **engine-caps 模块化引入回归**(探针/短路语义漂移) | P1 | 提升须保 `engineMode` 短路 / `__engineCalls` 探针 / null-engine 回归语义**逐行不变**;门0 8/8 + real 门 6/6 作回归基线。`recHook` 设计成可注入(默认 no-op)——**探针绝不入渠道产物**(见 §6)。 |
|
||||
| **receHook 探针泄进渠道产物** | P1 | 物理保证:`createEngineCaps({recHook})` 默认 `recHook=no-op`;ref-bundle 构建时**不传 recHook**(或传 no-op),仅王蓝莓真玩取证脚本注入 rec。渠道产物 esbuild 入口不引取证注入点 → 探针代码 tree-shake 不进包。 |
|
||||
| **体积**(ref 197KB→~540KB,引擎真入包) | P2 | ~540KB raw min(host-dev entry 实测 544KB 量级);gz 远在 B1 入场券 gz≤350KB 内(LittleJS 完整版 raw≈55KB gz)→ 不触红线。SIZES.md 增"游戏产物=引擎+包装层"口径(与插件增量分离)。**勿引"13KB"**(已废)。 |
|
||||
| **受控面被动** | P0 | 受控面 6 项契约**零改**是硬约束(`engineFactory` 是既有可选入参 `api.d.ts:405`);装载契约是 additive 新增,不改下层既冻 6 项。 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收门概览(arc §5.1 六门 + 韧性)
|
||||
|
||||
| 门 | 判据 | 口径来源 |
|
||||
|---|---|---|
|
||||
| **① 掌帧** | 引擎掌帧帧增量区间 `[20,45]/500ms` | gate0 G0-2(`gate0-engine-takeover.cjs:75`) |
|
||||
| **② 同屏像素** | **同一张画布(`#game-engine`)上**:游戏美术暖色命中(`warmHits>200`,色域 `r>140 && r−b>85 && g<r`,**开工前坐实非占位**·M3)∧ 引擎粒子像素命中(burst 白 `maxCh>80`、`brightHits>0`);粒子渲到另一画布则游戏画布命中=0=fail | a6 A6-5(`maxCh/brightHits`)+ 暖色域自 `palette.js:23-41`(execution §8「门②(a)」) |
|
||||
| **③ 真接线门②** | `window.__engineCalls` 含 `particles.spawnEmitter` call-ID(插件运行时真调引擎) | a6 A6-2(`a6-engine-wiring.cjs:79`) |
|
||||
| **④ CDP 真玩** | CDP 真触摸逐 zone 真玩(开店→进货→营业→结算→升级)+ 四件套 evidence;**driver 范式=真触摸+轮询,禁 stepFrames 手摇·M2**;rect 选择器 `#game-engine`、就绪等 `__refBootedReal`;zone 名以 `main.js` `computeZones` 为唯一真相 | ref e2e 扩 real 通道(execution 步骤 8)+ harness §2-律4 |
|
||||
| **⑤ stub 回归** | `?engine=stub` 逐像素确定性回归仍绿(双开哈希一致) | ref e2e 门 B + harness §5-坑6 |
|
||||
| **⑥ 提升回归** | engine-caps 提升后门0 8/8 + real 门 6/6 逐项不变 | gate0 + a6 |
|
||||
| **⑦ console 干净** | console 零未捕获错误 | ref e2e(`uncaught=[]`) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 回滚
|
||||
|
||||
- **代码回滚**:`?engine=stub` 通道与今天 `main.js` 行为逐行等价(RAF + `#game` 2D 画布 + `mockNowMs` 墙钟)。real 通道过不了 → 用户/取证一律走 stub,stub 既有交付(T1b-α 49 单测 + e2e 四件套)零连累。
|
||||
- **契约回滚**:装载契约 `game-host.d.ts` 是 additive 新增工件,受控面 6 项零改 → 移除新增不破存量。
|
||||
- **模块回滚**:engine-caps.js 提升若引入回归(门0/real 门红),可临时让 host.js 退回内联闭包(git revert 该文件 + host.js import 行),不连累 ref real 通道(ref 自带 engineFactory 经 createEngineCaps)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 爆炸半径
|
||||
|
||||
| 触及 | 文件 | 性质 |
|
||||
|---|---|---|
|
||||
| **ref 主循环重做** | `games/wanglanmei-ref/{entry.js:15, src/main.js:112/583-602/617-691/648}` | real 通道改造(stub 保旧);含输入改挂 mainCanvas(M4) |
|
||||
| **装载契约新工件** | **`game-runtime/src/core/game-host.d.ts`**(新增,复审改落点)+ `contracts/README.md`(仅指针登记) | additive 第 9 类契约(game-runtime 内部) |
|
||||
| **engine-caps 模块化** | `host-dev/engine-caps.js`(新增,搬 `host.js:197-318`)+ `host-dev/host.js`(改 import) | 搬运已有件,语义零变 |
|
||||
| **ref 装载适配** | **`games/wanglanmei-ref/src/game-shop.js`(新增·硬独立文件,A3)**:拆出符合装载契约的 game 工厂(默认导出、零引擎 import,禁 main.js 内联);entry import 引擎 | real 通道接线 |
|
||||
| **ref 就绪标记** | `games/wanglanmei-ref/index.html`(real 通道接 `__refBootedReal`,M1) | 就绪信号(stub 仍用 `__refBooted`) |
|
||||
| **SIZES 口径** | `SIZES.md` 增"游戏产物(引擎+包装层)"新表 | 文档(与插件增量分离) |
|
||||
| **受控面 6 项契约** | `api.d.ts:269/281/286/303` | **零改(硬约束)** |
|
||||
| **harness driver** | `e2e-input.cdp.cjs` real 通道**范式切换**(真触摸+轮询/选择器 `#game-engine`/等 `__refBootedReal`/同屏暖色判据);非「加断言」增量(M1/M2/M3) | 取证范式重写 |
|
||||
|
||||
**P1 不触及**:后端发布主链 / game-studio / feed / 生成管线(那些是 P2/P3,归母 spec)。
|
||||
|
||||
### 双通道全景(real / stub)
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
URL{"?engine="}
|
||||
URL -->|real(新增·引擎掌帧)| R0
|
||||
URL -->|stub(保旧·回滚锚)| S0
|
||||
subgraph R["real 通道"]
|
||||
R0["entry import LJS<br/>引擎真入 ref-bundle ~540KB"] --> R1["setGLEnable(false)<br/>engineInit 五回调"]
|
||||
R1 --> R2["createEngineCaps 建 engineFactory<br/>注入 createHostDevContext"]
|
||||
R2 --> R3["game.update←gameUpdate<br/>game.render(mainContext)←gameRender<br/>mockNowMs=逻辑帧钟·废 RAF"]
|
||||
R3 --> R4["验收:门①掌帧/②同屏/③call-ID/④真玩/⑦console"]
|
||||
end
|
||||
subgraph S["stub 通道"]
|
||||
S0["不 import 引擎<br/>ref-bundle 197KB"] --> S1["prepareCanvas(#game) 2D<br/>浏览器 RAF + mockNowMs 墙钟"]
|
||||
S1 --> S2["frame(dtReal) 原样<br/>drawFrame(g=#game)"]
|
||||
S2 --> S3["验收:门⑤逐像素双开哈希一致"]
|
||||
end
|
||||
R4 -.过不了回滚.-> S3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. P2 / P3 归并(O2 已拍)
|
||||
|
||||
- **P2(泛化宿主:装载任意 GamePackage)合并进母 spec `2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md` Phase D**(O2 已拍)。P1 验实的装载契约 = P2 的产品化基底:母 spec Phase D 的"ref 经发布主链入 feed"升级为"engine-aware 包(按装载契约 + `packageUrl` 加载,`game-package.schema.json:66`)入 feed 跑引擎真玩"。P2 不在本 spec 展开。
|
||||
- **P3(W-G1 生成游戏按同一装载契约产出)** 归 W-G1 自身 spec:agent 写码于插件库、产装载契约 bundle、20 基准 bake-off。P1 的装载契约 IDL 是 P3 生成 agent 的**唯一产出靶**。
|
||||
|
||||
---
|
||||
|
||||
## 8. 待确认 / 风险与假设(诚实标注)
|
||||
|
||||
> **对抗复审结论(2026-06-13·deep·全程读真源码)**:**无致命项**。装载契约 IDL 五件(init/update/render/onInput/destroy)形态合理、与引擎五回调对齐准确(`host.js:883-887`);additive 关系成立(受控面 6 项零改 `api.d.ts:269/281/286/303/405`);双通道回滚有 A0 实证。**4 重大项已全部纳入 execution 版钉死**(real 就绪标记 M1 / driver 范式切换 M2 / 暖色阈值坐实 M3 / bundle 延后建 M4),均为「会致执行 agent 临场返工或门假绿/假红的硬装配问题」,不翻盘但必须在 execution 钉死。**5 项不明假设逐条处置**(落点改 game-runtime/src·缺省分叉显式·game 工厂硬独立·部署 git-clone-scratch·步骤0 降常规核对)。逐条裁决见 execution §12。
|
||||
> **唯一 open item(非阻断)**:装载契约工件命名(见末条)。
|
||||
|
||||
**缺省值分叉(复审 A2·显式声明此分叉有意为之)**:host-dev 缺省 `engine=real`(`host.js:340`),ref 缺省 `engine=stub`(`main.js:82` play 模式走 stub 通道)。**有意不统一**:真玩取证显式带 `&engine=real` 更稳、stub 兜逐像素回归;但须知 P3 复制 ref 作模板时,缺省 stub=「新游戏默认不上引擎」是过渡现状,**引擎是正路**(boot 注入装载契约)——这点 P3 spec 须承接。
|
||||
|
||||
- **[verified]** 装载契约全仓不存在(`grep GameModule/装载契约/engineInit` 在 `contracts/`+`src/` 零命中);既有契约 = `PluginContext`(`api.d.ts:269/281/286/303`,6 受控面 + `getEngine()`)+ `packageUrl`(`game-package.schema.json:66`)。
|
||||
- **[verified]** `engine-caps.js` 文件不存在,但内容已大半存在:`makeEngineCaps` 闭包(`host.js:197-318`)已包装 particles/audio.synth/math 三面并已注入 `engineFactory`(`host.js:359`);`engine-math.js` 已独立成模块(`host.js:47` import)。→ 提升=搬运。
|
||||
- **[verified]** ref 游戏层 `core.update`(`main.js:621`)/`drawFrame(g)`(`main.js:648`,g=`prepareCanvas(#game)`)/RAF(`main.js:668-691`)/`mockNowMs` 步进钟(`main.js:618`);`createHostDevContext` 仅 4 键无 `engineFactory`(`main.js:112`)→ 粒子今天全 `NOOP_HANDLE`。
|
||||
- **[verified]** real 通道引擎掌帧范式已在 host.js 跑通:`engineInit` 五回调(`host.js:917`)+ `setupAfterEngine(LJS.mainContext)`(`host.js:919`)+ **就绪标记 `__hostBootedReal` 在 then 内 game 就绪后置(`host.js:920`,注释明告 CDP 须等它再驱动 `:851/931`)**+ bundle 在 then 内建(`host.js:323-324/353-371/379-381`)+ 输入挂 mainCanvas(`host.js:536-538`)+ `setGLEnable(false)`(`host.js:904`)+ 双时钟 `window.__engine`(`host.js:840`)+ `mainCanvas.id='game-engine'`(`host.js:830`)。**这些 host-dev 已踩并解决的坑(M1/M4),ref 必须继承(execution §12)。**
|
||||
- **[verified]** burst 粒子 `colorStart` 缺省白 `{r:1,g:1,b:1,a:...}`(`toEngineSpec`,`particles-juice/impl.js:217`),故引擎粒子像素门判 `brightHits/maxCh`(青蓝口径已对引擎渲染失效,harness §6-2);游戏美术暖色门另判暖色域(M3,自 `palette.js:23-41`)。
|
||||
- **[verified·复审降级]** ref 接引擎 import 能入包——host-dev/entry.js 末行已 import 引擎且 real 门 6/6 已过(同 monorepo node_modules);常规构建 `grep ParticleEmitter` 核对(A5),非阻断门。
|
||||
- **[inferred]** ref ~540KB 不触 B1 入场券——基于 host-dev entry 544KB 实测 + gz 量级;P1 用 `size.mjs` 实测游戏产物 gz 坐实。
|
||||
- **[assumption]** "好玩基线 v2 五要素"精确判据不在本 spec 范围(P3 验收门事);P1 只证装载契约可让真游戏真跑在引擎上同屏可玩。
|
||||
- **[open item·留创始人·非阻断]** 装载契约工件命名 `game-host.d.ts`(暂名,arc 称"游戏宿主装载契约");若创始人偏好其它名(如 `game-loadable.d.ts`),执行版改名即可,**形态/落点(game-runtime/src/core)不变**。改名零成本,不阻断执行。
|
||||
184
docs/agent-specs/2026-06-13-WG1-20经典轻游戏基准-review.md
Normal file
184
docs/agent-specs/2026-06-13-WG1-20经典轻游戏基准-review.md
Normal file
@ -0,0 +1,184 @@
|
||||
# 《W-G1 · 20 经典轻游戏基准》设计稿(review 版)
|
||||
|
||||
> **状态**:review 版 · 2026-06-13 · 只读调研产出(未改任何运行时代码)。
|
||||
> **定位**:本基准是"**便宜模型(M3/M2.7/DS-V4)+ 当前引擎(LittleJS)/插件库**"能产出什么的**实测靶**。20 款经典轻游戏铺满机制谱系,既验生成管线打通,又压测能力上限、**暴露引擎/插件库短板**。
|
||||
> **调研口径**:覆盖判定**只认源码导出**(api.d.ts 手写门面 + impl.js 实际 return 面 + host.js `makeEngineCaps()`),不认"我以为有"。证据锚点全部带 file:line。
|
||||
> **关联**:边界模型见 memory `engine-plugin-boundary-model`;本基准是 Runner v2 全弧 P3(W-G1)的靶集,见 `2026-06-13-runner-v2-arc-review.md`。
|
||||
|
||||
---
|
||||
|
||||
## 结论先行(给创始人的三句话)
|
||||
|
||||
1. **能打的多数**:20 款里约 **12 款 ✅当前插件/引擎已直接覆盖**(井字棋/2048/Flappy/打砖块/贪吃蛇/Pong/扫雷/Simon/Snake 类…),便宜模型在现有能力面上应能产出可玩件——这批先做冒烟,验管线。
|
||||
2. **缺口集中在 4 类能力**(本基准核心产出):**① 文本/分数 HUD 渲染**(无插件门面)、**② 网格/棋盘状态抽象**(无 grid/tilemap)、**③ 连续碰撞 CCD**(collision 仅离散 MTV,高速小球会穿透)、**④ 通用状态机 FSM**(gamefeel 仅 3 个特化计时器)。这 4 类决定 Tetris/Asteroids/Breakout-高速/Match-3 的产出质量。
|
||||
3. **真短板不在"造不出",在"agent 要自己补层"**:上述①②④引擎/插件其实有底层原语或属 agent 生成域(LJS.drawText 存在但未包装、网格状态属 agent 生成域),但**没有插件门面=每款游戏的便宜模型都要重造一遍轮子**→ 一致性差、token 贵、易错。**建议:W-G1 实测后,把高频缺口(文本 HUD / 网格)补成插件门面**,而非让 20 个 prompt 各自硬写。
|
||||
|
||||
---
|
||||
|
||||
## A. 事实基:当前能力面盘点(verified,带证据锚点)
|
||||
|
||||
### A.1 插件库实际导出(7 个业务插件 + 1 example)
|
||||
|
||||
| 插件 | 公开能力(源码实际 return 面) | 证据锚点 |
|
||||
|---|---|---|
|
||||
| **collision** | 离散几何相交:`circleVsCircle`/`circleVsAabb`/`aabbVsAabb`/**`satVsPolygon`(凸多边形+MTV)**;射线 `raycastCircle`/`raycastAabb`(RayHit);**`SpatialHash`(均匀网格 broadphase 粗筛)** | `collision/impl.js:268/295/337/395/462/500`、`SpatialHash:112-264`;api.d.ts:`Manifold`/`RayHit:80/97` |
|
||||
| **physics-lite** | 离散运动数学:`integrateProjectile`/`Projectile`(抛体+重力)、`springStep`/`criticalDamping`、`moveWithConstraint`、`clampSpeed`/`applyFriction` | `physics-lite/impl.js:104/127/181/194/241/268/286` |
|
||||
| **particles-juice** | `spawnEmitter`(preset/config)、`hitStop`、`shakeScreen`、`flashScreen`、`pulseScale`、`getPreset`;渲染输出经受控面偏移 | `particles-juice/impl.js:438/504/524/544/562`、`getPreset:163`;api.d.ts façade `:204-240` |
|
||||
| **gamefeel** | `InputBuffer`、`CoyoteTimer`、`ComboWindow`、**easing 标准库 11 条**(纯函数,经引擎门面) | `gamefeel/impl.js: InputBuffer:232/CoyoteTimer:319/ComboWindow:390/makeEasingFacade:205`;api.d.ts `:108/154/196` |
|
||||
| **audio-music** | `playSfx`(blip/thud/chime)、`play`/`loop`/`stop`(BGM)、`setIntensity`/`setIntensityLayering`、`loadSong` | `audio-music/impl.js:438/455/463/467/471/481`;preset `:71-75` |
|
||||
| **palette-post** | 调色板映射 `applyPaletteMap`/`lerpPaletteMap`、HSL `shiftHsl`/`rgbToHsl`/`hslToRgb`、后处理 `setPost`(vignette/dither/scanline)/`renderPost` | `palette-post/impl.js:142/210/238/62/90/392/412`;dither `:435` |
|
||||
| **save-progress** | KV 持久化 `get`/`set`/`remove`/`clear`(命名空间隔离+损坏容错+单条上限) | `save-progress/impl.js:219/243/285/294` |
|
||||
| **runtime-probe** | 取证埋点 `getRecords`/`toJSONL`/`verify`(哈希链)— 非玩法能力,e2e 证据用 | `runtime-probe/impl.js:330/335/340` |
|
||||
|
||||
### A.2 `makeEngineCaps()` 引擎透传真实粒度(host.js,只此三面)
|
||||
|
||||
| 面 | 真实粒度 | 证据锚点 |
|
||||
|---|---|---|
|
||||
| **particles** | `spawnEmitter(spec)` 薄包装引擎 `ParticleEmitter`(27 参全映射),像素↔世界阻抗换算 | `host.js:213-273`(构造 `:243-266`) |
|
||||
| **audio.synth** | `synthSfx`(包 `zzfxG`)/`synthSong`(包 `zzfxM`)→ PCM 样本(不播放,×0.3 播放层补) | `host.js:279-316` |
|
||||
| **math** | `lerp`/`smoothStep` + `easing` 11 条等价曲线(POWER/BACK/ELASTIC 族),纯函数 | `host.js:204-209` + `engine-math.js:56-87` |
|
||||
|
||||
> 引擎本体(LittleJS)远不止这三面(有 `drawText`/`drawTile`/`tileCollision`/相机等),但**插件墙内只透出这三面**;其余引擎能力**不构成 agent 的能力词汇**(一职一路,见边界 memory)。
|
||||
|
||||
### A.3 受控面 6 项(api.d.ts:264-304,引擎可换边界)
|
||||
|
||||
`getContext2d()` / `onFrame()` / `getInput()`(归一化输入:pointer 三态+key 两态)/ `getAudioContext()` / `time`(受控时钟)/ `random`(确定性随机)。**输入只有 5 类事件**(`api.d.ts:218`),**无手势识别**(swipe/拖拽需 agent 在生成域自合成 pointer 序列)。
|
||||
|
||||
### A.4 模板哲学(决定"什么算覆盖")
|
||||
|
||||
`tech-decisions.md:38` 终裁:**玩法/美术/关卡/UI = agent 写码生成域**;插件=能力 API。故"覆盖判定"口径:**该机制所需能力,是否有插件门面或引擎透传面兜底**;纯玩法逻辑(回合制、计分、棋盘状态机)是 agent 自写域,**不算缺口**——除非它需要的底层原语(文本渲染、网格抽象)无门面。
|
||||
|
||||
---
|
||||
|
||||
## B. 20 款经典轻游戏基准表
|
||||
|
||||
> 覆盖判定:**✅** 插件/引擎透传面已覆盖 | **🟡** 引擎有底层原语/属生成域但无门面(agent 须自写补层,高频则建议补门面)| **❌** 缺口(无门面且 agent 自写成本高/易错)
|
||||
> 难度档:**易**=单机制+静态/网格 | **中**=2-3 机制叠加+实时 | **难**=多机制+连续物理/程序化/高频碰撞
|
||||
|
||||
| # | 游戏 | 核心机制 | 所需能力清单 | 当前覆盖判定 | 难度 | 一句话生成种子 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | **井字棋** | 3×3 落子棋/胜负判定 | 网格状态、tap、文本"X/O"、胜负 AI | 🟡 网格/文本🟡、输入✅、AI=纯逻辑 | 易 | 3×3 棋盘点击落子 X,AI 落 O,三连判胜 |
|
||||
| 2 | **2048** | 网格滑动合并/数字 | 4×4 网格、swipe、文本数字、合并逻辑 | 🟡 网格+文本🟡(高频缺口)、swipe=合成、合并=纯逻辑 | 中 | 4×4 数字方块,滑动合并同值,生成新块,显分 |
|
||||
| 3 | **贪吃蛇** | 网格移动/自增长/自碰撞 | 网格、方向键、自碰撞(网格等值)、计分 | 🟡 网格=agent 自管、输入✅、碰撞=网格逻辑、计分文本🟡 | 易 | 蛇沿网格吃食物变长,撞墙/撞自己结束 |
|
||||
| 4 | **Flappy Bird** | 重力跳跃/管道避障/无尽 | 重力(抛体)、tap、AABB 碰撞、程序化管道、计分 | ✅ physics 抛体`:104`、collision `aabbVsAabb:337`、输入✅;文本🟡 | 中 | 点击让小鸟上跳,重力下落,穿随机高度管道 |
|
||||
| 5 | **打砖块 Breakout** | 弹球反弹/挡板/砖块网格 | 弹球物理、AABB/圆碰撞、挡板输入、砖块网格、反弹角 | ✅ collision `circleVsAabb:295`+MTV 反弹、physics 限速;高速球🟡 | 中 | 挡板接弹球击碎砖阵,球随挡板位置改反弹角 |
|
||||
| 6 | **Pong** | 双挡板/弹球/简单 AI | 弹球、AABB 碰撞、挡板输入、AI 追球 | ✅ collision/physics/输入✅;AI=纯逻辑 | 易 | 左玩家右 AI 各控挡板,漏球对方得分 |
|
||||
| 7 | **扫雷** | 网格揭示/数字提示/标记 | 网格、左右键 tap、文本数字、洪水填充 | 🟡 网格+文本🟡、输入✅、洪水填充=纯逻辑 | 中 | 点击揭示显周围雷数,右键标记,踩雷结束 |
|
||||
| 8 | **Simon 记忆** | 序列记忆/颜色按钮/渐进 | 序列状态机、tap、音效反馈、计时 | ✅ audio `playSfx:481`、gamefeel 计时、输入✅;序列=纯逻辑 | 易 | 四色按钮按序闪烁播音,玩家复现,逐轮加长 |
|
||||
| 9 | **记忆翻牌** | 翻牌配对/网格 | 网格、tap、翻转动画(easing)、配对逻辑 | ✅ easing`:205`/输入✅;网格🟡、图案=agent 绘 | 易 | 牌面朝下,翻两张配对,全配对获胜 |
|
||||
| 10 | **太空侵略者** | 波次敌阵/射击/碰撞 | 敌阵网格移动、射击 tap、子弹 AABB、波次、HUD | ✅ collision AABB+SpatialHash`:112`、particles 爆炸;文本🟡 | 中 | 炮台左右移动射击,敌阵整体平移下压,清屏过关 |
|
||||
| 11 | **俄罗斯方块 Tetris** | 下落方块/网格堆叠/消行 | 网格(堆叠态)、方向键旋转移动、消行、计时下落、文本 | 🟡 网格堆叠=agent 自管(**高频痛点**)、输入✅、文本🟡 | 难 | 七种方块下落,旋转堆叠,满行消除,逐级加速 |
|
||||
| 12 | **小行星 Asteroids** | 惯性飞船/任意角度/程序化碎裂 | 惯性物理、旋转射击、多边形碰撞、程序化分裂、环绕 | ✅ collision `satVsPolygon:395`+RayHit、physics `applyFriction:286`;高速子弹 CCD❌ | 难 | 飞船惯性漂移射击,大陨石击中裂小块,屏幕环绕 |
|
||||
| 13 | **消消乐 Match-3** | 网格交换/三连消除/下落填充 | 网格、swipe 交换、三连检测、重力下落、连锁、HUD | 🟡 网格+文本🟡(**高频痛点**)、swipe=合成、消除=逻辑、easing✅ | 难 | 8×8 宝石网格,交换凑三连消除,上方填充连锁 |
|
||||
| 14 | **跳跃者 Doodle Jump** | 垂直无尽/平台跳/重力 | 重力跳(抛体)、左右倾斜、平台 AABB、程序化平台、相机 | ✅ physics 抛体`:104`、collision AABB、coyote`:319`;相机=受控面、文本🟡 | 中 | 持续上跳踩平台,左右移动,平台程序化生成 |
|
||||
| 15 | **愤怒小鸟(简版)** | 抛射弹道/拖拽蓄力/碰撞倒塌 | 抛体弹道、**拖拽手势**、多体碰撞、弹簧/冲量 | 🟡 physics 抛体✅/弹簧✅、collision MTV✅;**拖拽=agent 合成 pointer 链** | 难 | 拖拽弹弓蓄力发射,抛物线击中结构使其倒塌 |
|
||||
| 16 | **跑酷 Runner** | 横向无尽/跳跃滑铲/障碍 | 自动横移、tap/swipe 跳铲、AABB、程序化障碍、加速、HUD | ✅ physics 跳、collision AABB、buffer`:232`+coyote;swipe=合成、文本🟡 | 中 | 自动向右跑,点击跳跃避障,距离计分,逐渐加速 |
|
||||
| 17 | **打地鼠** | 随机出现/限时 tap/计分 | 随机定时生成、tap 命中(点 vs 圆)、计时、计分、反馈 | ✅ random✅、collision 点/圆判、particles 命中;文本🟡 | 易 | 地鼠随机洞口冒出,限时点中得分,超时消失 |
|
||||
| 18 | **见缝插针** | 旋转/时机 tap/碰撞检测 | 旋转角积分、tap 时机、角度/扇区碰撞、渐进 | ✅ math 角度、collision `raycastCircle:462`、particles;文本🟡 | 中 | 中心圆持续转,点击射针,针不能相撞,逐根加针 |
|
||||
| 19 | **节奏点击** | 时间窗判定/节奏/连击 | 时间窗(combo)、tap 精度、音效、连击计分、easing | ✅ `ComboWindow:390`+`InputBuffer`、audio、easing;文本🟡 | 易 | 音符落判定线时点击,perfect/good 分级,连击加成 |
|
||||
| 20 | **弹球变体(多球)** | 多球/道具掉落/物理反弹 | 多体弹球、圆/AABB 碰撞、道具 AABB、屏震闪白 juice、HUD | ✅ collision 圆+AABB、physics 限速、particles `shakeScreen:524`/`flashScreen:544`;高速多球 CCD🟡 | 难 | 挡板接多球击砖,砖落道具(增球/扩板),命中屏震闪白 |
|
||||
|
||||
---
|
||||
|
||||
## C. 能力覆盖审计(本基准核心价值)
|
||||
|
||||
### C.1 20 款合计考到的能力维度 × 谁兜
|
||||
|
||||
| 能力维度 | 由谁兜 | 覆盖状态 | 涉及款数 |
|
||||
|---|---|---|---|
|
||||
| 离散几何碰撞(圆/AABB/多边形 MTV) | **collision** `:268-431` | ✅ | 4,5,6,10,12,15,17,18,20 |
|
||||
| 射线命中(RayHit) | **collision** `:462/500` | ✅ | 12,18 |
|
||||
| 空间粗筛(broadphase) | **collision** `SpatialHash:112` | ✅ | 10,20 |
|
||||
| 抛体/重力 | **physics-lite** `:104` | ✅ | 4,12,14,15,16 |
|
||||
| 摩擦/限速/惯性 | **physics-lite** `:286/268` | ✅ | 12,16 |
|
||||
| 弹簧阻尼 | **physics-lite** `:194` | ✅ | 15 |
|
||||
| 输入缓冲/郊狼/连击窗 | **gamefeel** `:232/319/390` | ✅ | 8,14,16,19 |
|
||||
| easing 标准库(11 条) | **gamefeel/math 门面** `:205` | ✅ | 9,11,13,18,19 |
|
||||
| 粒子/屏震/闪白/命中停顿 | **particles-juice** `:438/504/524/544`+引擎透传 | ✅ | 10,12,17,20 |
|
||||
| 音效/BGM/强度分层 | **audio-music** `:455-481`+引擎透传 | ✅ | 8,19,20 |
|
||||
| 调色/后处理 | **palette-post** `:392/412` | ✅(可选) | 全部可选 |
|
||||
| 存档/进度 | **save-progress** `:219-294` | ✅ | 高分榜可选 |
|
||||
| tap/方向键输入 | **受控面 getInput** `:218` | ✅ | 全部 |
|
||||
| 确定性随机/程序化 | **受控面 random** `:35` | ✅ | 4,7,10,11,13,14,16,17 |
|
||||
| **文本/分数 HUD 渲染** | **无插件门面**(引擎 drawText 未透出) | **🟡 缺门面** | 几乎全部 |
|
||||
| **网格/棋盘状态抽象** | **无 grid/tilemap 门面**(agent 生成域自管) | **🟡 缺门面** | 1,2,3,7,9,11,13 |
|
||||
| **连续碰撞 CCD(swept)** | **collision 仅离散 MTV**(grep swept/ccd 全空) | **❌ 缺口** | 5,12,20(高速小目标) |
|
||||
| **通用状态机 FSM** | **无**(gamefeel 仅 3 特化计时器) | **🟡 agent 自写** | 8,11、回合制类 |
|
||||
| **手势识别(swipe/拖拽)** | **无门面**(agent 合成 pointer 序列) | **🟡 自合成** | 2,13,15,16 |
|
||||
| **寻路 A*/导航** | **无**(grep astar/pathfind 全空) | **❌ 缺口(已规避)** | (完整 Pacman 鬼 AI——故未入选) |
|
||||
|
||||
### C.2 点名"会暴露能力缺口"的款 + 缺口性质
|
||||
|
||||
**❌ 真缺口(无门面+agent 自写成本高/易错):**
|
||||
|
||||
1. **连续碰撞 CCD(swept)** — 暴露款 **#5 高速球/#12 子弹/#20 多球**。
|
||||
- 性质:collision 只有离散 MTV/穿透判定(`circleVsAabb:295`),无 swept/toi(grep `swept|ccd|continuous|toi|tunnel` 全空;`impl.js:21` 自述能力止于"MTV/穿透/RayHit")。
|
||||
- 后果:**高速小球穿砖/穿墙(tunneling)**,便宜模型多半产出"球偶尔卡进/穿透"的 bug 件。
|
||||
- 绕法:agent 用 `raycastAabb:500` 沿速度向量手做 toi(可行但每款重写、易错)→ 高频则补 collision swept 门面。
|
||||
|
||||
2. **寻路 A*/导航网格** — 本基准**已主动规避**(完整 Pacman 鬼寻路未入选)。
|
||||
- 性质:grep `astar|pathfind|navmesh` 全空;`SpatialHash` 只做粗筛非寻路。
|
||||
- 结论:**任何需要敌人智能寻路的品类当前造不出**=引擎/插件库明确边界。需要时须新增寻路插件。
|
||||
|
||||
**🟡 半缺口(底层原语存在/属生成域,但无门面→每款重造轮子):**
|
||||
|
||||
3. **文本/分数 HUD 渲染** — 暴露款 **几乎全部**(2/3/7/10/11/13/16…)。
|
||||
- 性质:无任何文本渲染门面(grep `drawText|renderText` 全空);引擎 `LJS.drawText` 存在但未经插件透出,`makeEngineCaps()` 三面不含文本(`host.js:210-317`)。
|
||||
- 后果:每个游戏的便宜模型都得自己 `ctx.getContext2d().fillText` 手画 HUD——风格各异、字体/对齐易错。
|
||||
- **最高频缺口**:20 款里 ~15 款需文本 → **强烈建议补 text/HUD 门面插件(W-G1 实测后第一优先)**。
|
||||
|
||||
4. **网格/棋盘状态抽象** — 暴露款 **#1/2/3/7/9/11/13**。
|
||||
- 性质:无 grid/tilemap 门面(grep `tilemap|gridMap` 全空);属 agent 生成域,但坐标↔索引/邻接/遍历/合并每款重写。
|
||||
- 后果:2048/Tetris/Match-3 网格状态机是核心难度,便宜模型自管易出"合并方向/消行/连锁"bug。
|
||||
- 建议:W-G1 观察便宜模型自写网格出错率;若高,补 grid 工具门面。
|
||||
|
||||
5. **手势识别(swipe/拖拽)** — 暴露款 **#2/13(swipe)/#15(拖拽)/#16**。
|
||||
- 性质:受控面 `getInput` 只给 5 类原始事件(`api.d.ts:218`),无 swipe/drag 合成。
|
||||
- 后果:agent 须自缓存 pointerdown→move→up 算方向/距离;愤怒小鸟拖拽蓄力尤其易错。
|
||||
- 建议:swipe/drag 是触屏轻游戏刚需 → 可补进 gamefeel(已管输入缓冲,自然归属)。
|
||||
|
||||
6. **通用状态机 FSM** — 暴露款 **#8 Simon/#11 Tetris** 及回合制类。
|
||||
- 性质:gamefeel 仅 3 个特化计时器,无通用 FSM 原语。
|
||||
- 后果:多状态玩法自写状态切换易乱;属生成域,优先级低于文本/网格(纯逻辑模型相对擅长)。
|
||||
|
||||
### C.3 一句话审计结论
|
||||
|
||||
**20 款里 ~12 款能力面齐备(✅),可立即冒烟;余 8 款集中暴露 4 类短板。最痛的不是"造不出",而是"文本 HUD + 网格状态"两个高频能力没有门面——便宜模型 20 个 prompt 各造一遍轮子,一致性、token、出错率三重受损。真·造不出的硬边界只有两条:连续碰撞 CCD(高速穿透)与寻路(已规避)。**
|
||||
|
||||
---
|
||||
|
||||
## D. 分批/分档执行建议
|
||||
|
||||
> 原则:先用能力面齐备的易档验管线打通(冒烟),再用中/难档+缺口款压测能力上限、量化便宜模型在缺口处的产出质量。
|
||||
|
||||
### 批次 1 · 冒烟批(验管线打通)— 6 款 · 全 ✅ · 难度易
|
||||
**#6 Pong / #8 Simon / #17 打地鼠 / #9 记忆翻牌 / #3 贪吃蛇 / #19 节奏点击**
|
||||
- 理由:能力面 100% 齐备,机制单一。目标=证明"便宜模型+插件库+new-api+harness 门"端到端跑通,产可玩件。
|
||||
- 验收:管线零阻塞 + ≥4/6 过好玩基线评估门。
|
||||
|
||||
### 批次 2 · 主力批(验常见机制覆盖)— 8 款 · ✅ 为主 · 难度中
|
||||
**#4 Flappy / #5 打砖块 / #10 太空侵略者 / #14 Doodle Jump / #16 跑酷 / #7 扫雷 / #18 见缝插针 / #1 井字棋**
|
||||
- 理由:覆盖物理/碰撞/波次/程序化/网格主流机制,带少量 🟡(文本/网格/swipe)。
|
||||
- 核心产出:**量化文本 HUD/网格/swipe 三个高频 🟡 的便宜模型自写出错率**——"是否要补门面"的决策数据。
|
||||
- 验收:≥6/8 可玩;逐款记录 agent 在 🟡 缺口处的补层方式与 bug。
|
||||
|
||||
### 批次 3 · 压测批(测能力上限+暴露缺口)— 6 款 · 含 ❌/难 🟡 · 难度难
|
||||
**#11 Tetris / #13 Match-3 / #12 Asteroids / #15 愤怒小鸟 / #20 多球弹球 / #2 2048**
|
||||
- 理由:每款压一/多个短板——Tetris/Match-3/2048=网格状态机上限;Asteroids/多球弹球=CCD 缺口;愤怒小鸟=拖拽手势缺口。
|
||||
- 核心产出:**证实 C.2 缺口判定**——便宜模型在 CCD/网格/拖拽处的产出质量(穿透 bug?消行 bug?蓄力 bug?),给创始人"引擎短板实测画像"。
|
||||
- 验收:**不强求高通过率**(本批测上限);**重点产出缺口实证报告**→ 喂"是否补 text/grid/swipe/swept 门面"的决策。
|
||||
|
||||
### 跨批观测项(W-G1 真正要回答的问题)
|
||||
1. 便宜模型(M3/M2.7/DS-V4)在三档的产出质量梯度 → 哪档开始崩,定能力天花板。
|
||||
2. 四类短板的实测影响:文本/网格/CCD/拖拽各拖垮多少款 → 排补门面优先级(预判:文本 HUD > 网格 > swipe/拖拽 > CCD swept)。
|
||||
3. harness 门的判定有效性:能否稳定逮住"穿透/消行错/不可玩"——门兜底是便宜模型路线的命门。
|
||||
|
||||
---
|
||||
|
||||
## 风险与假设(诚实标注)
|
||||
|
||||
- **[verified]** 全部覆盖判定基于源码导出(api.d.ts + impl.js return 面 + host.js makeEngineCaps),证据锚点见 A/B/C 各 file:line。缺口判定用"源码证伪法"(grep 关键原语全空+impl.js 自述能力边界),非按预期名臆断(遵 memory 红线 `engine-plugin-boundary-model` §1.5)。
|
||||
- **[inferred]** "便宜模型能产出 ✅ 款的可玩件"是推断非实测——这正是 W-G1 要验的;本基准只确证"能力面齐备",不预断模型产出质量。
|
||||
- **[inferred]** 难度档(易/中/难)是按"机制数 × 实时性 × 缺口数"的工程推断,非用户体感难度。
|
||||
- **[assumption]** 好玩基线 v2 的具体五要素未在本次检索中定位到原文(`add-game-template.md:7` 仅言"改挂评估门"),验收门精确判据需在 W-G1 开工前从评估门 spec 取齐。
|
||||
- **[未决]** 20 款最终取舍可由创始人按"想优先验证的机制谱系"调整;CCD/寻路两条硬边界是否本期补门面,建议**等批次 3 实测数据**再拍,不预先投入。
|
||||
296
docs/agent-specs/2026-06-13-runner-v2-arc-review.md
Normal file
296
docs/agent-specs/2026-06-13-runner-v2-arc-review.md
Normal file
@ -0,0 +1,296 @@
|
||||
# Runner v2 全弧 · 顶层评审版(HJ-RUNNER-ARC-001)
|
||||
|
||||
> **状态**:review 版 · 2026-06-13(**对抗复审整改稿 r2**)· 给创始人决策(结论先行,不写代码级执行细节,多用 Mermaid)。
|
||||
> **定位**:本文不是执行 spec。它确立 **一块基石("游戏挂引擎"装载契约)解锁三相(P1 spike → P2 泛化 → P3 生成)** 的顶层主梁,并给方案 A/B 收敛裁决。落地时各相另备 review+execution 双 spec。
|
||||
> **前提(创始人已拍)**:① 推进范围 = **P1→P2→P3 全弧**,顺序解锁、我连续驱动;② **W-G1 前置 DeepSeek key 已就绪**,W-G1 排进 P3。
|
||||
> **关联**:A0 地基 [[a0-engine-takeover-done]]、边界模型 [[engine-plugin-boundary-model]]、20 经典基准 `2026-06-13-WG1-20经典轻游戏基准-review.md`、母 spec `2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md`、三契约冻结提交 `28a57b8`。
|
||||
|
||||
> ### 复审整改说明(r2,2026-06-13)
|
||||
> 本轮收到的对抗复审结论体为空(JSON=null)。为不空转,主笔架构师**亲自对照 spec 所引源码做了一轮对抗核验**(全 verified,证据带 file:line),逮出 3 条致命 + 2 条重大并就地整改;下列整改均可被源码复证,整改清单见 §0。**这不替代一轮独立外部对抗复审**——本稿主梁已重锚,建议仍走一次外部复审确认再冻结分相 execution。
|
||||
>
|
||||
> **整改的主干裁决(一句话)**:原稿把"统一基石"命名为一份**尚不存在的 `GameModule` 契约**,且与既有 `PluginContext` 混为一谈——这本身就是它声称要避免的"双词汇/缝合"。整改把主梁**重锚到已冻结的真契约**(`PluginContext.getEngine()` + `packageUrl`,母 spec P2 三契约,`28a57b8`),把"游戏怎么挂引擎"明确为**一份仍待起草的 additive 装载契约**(暂名"游戏宿主装载契约"),升级为创始人待拍 open item,**不再当作既成事实断言**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 对抗复审整改清单(r2)
|
||||
|
||||
> 自核 5 项(3 致命 F1-F3 + 2 重大 M1-M2)全部纳入。证据列均为可复证 file:line(路径相对 `game-runtime/`,契约相对仓根 `contracts/`)。
|
||||
|
||||
| # | 级别 | 原稿问题 | 整改 | 证据(已复证) |
|
||||
|---|---|---|---|---|
|
||||
| **F1** | 致命 | 把统一基石命名为 `GameModule` 契约并当作"已统一的单一事实源",但**全仓不存在该契约工件**,且与既有 `PluginContext`(插件契约)混淆 → 实为"另造一套词汇",违背它自己声称的"勿缝合/同一职不留两条路"。 | 主梁重锚:**统一基石 = 母 spec P2 已冻结的真契约**(`PluginContext.getEngine()` + GamePackage `packageUrl`)。"游戏如何挂引擎"作为**仍缺的一块 additive 装载契约**("游戏宿主装载契约")显式立项,**升级为 open item O1,不再断言已成**。改 §1/§4。 | `grep GameModule` 在 `contracts/`+`src/core/`+`host-dev/` **零命中**;既有契约 = `api.d.ts:264-303 PluginContext`(6 受控面 + `getEngine()`);母 spec L163「P2 三契约✅冻结」(`28a57b8`) |
|
||||
| **F2** | 致命 | 断言"`engine-caps.js` 当前不存在=真新增工作"、"`makeEngineCaps` 仍是闭包待抽"。**已过时**:A1/A4 已落地共享数学门面 + caps 工厂 + 注入。 | 修正基线:`engine-math.js` 已是共享模块、`makeEngineCaps` 已包装 particles/audio/math 且已注入 `engineFactory`。真实剩余工作**收窄为**"把 `makeEngineCaps` 闭包**提升为可 import 模块**供 ref/生成游戏共用",非从零造。改 §3.1/§4.3/§5.1/§7。 | `host-dev/engine-math.js` 存在(`buildEngineMath`,`host.js:47` import);`makeEngineCaps` 已包装三面(`host.js:197-318`)且已注入(`host.js:359 engineFactory: makeEngineCaps`);提交 `e591e9b`/`3e5cab4` |
|
||||
| **F3** | 致命 | "ref 与引擎同屏"被描述为"改主循环+换画布"。**漏了契约缺口**:ref 的 `core.update`/`drawFrame(g)` 是**游戏层**(非插件),不经 `PluginContext`,今天没有"游戏层↔引擎 mainContext"的约定面——这正是 F1 缺的那块装载契约。 | 把"同屏"明确为**装载契约的核心待解项**:游戏层取引擎 `mainContext` 的方式必须由 O1 的装载契约定义,**不能借 `PluginContext`**(那是插件面)。P1 的真正产出 = **把这块装载契约 spike 实**,不止"换几行画布"。改 §1 风险/§4.1/§5.1。 | ref 插件经 `createHostDevContext`(`main.js:112`=PluginContext);但 `drawFrame(g,…)`(`main.js:648`)/`core.update` 是游戏层,画在 `#game`(`main.js:83 prepareCanvas`),在契约面之外 |
|
||||
| **M1** | 重大 | A/B"收敛"裁决建立在 F1 的虚构契约上,收敛性无法验证。 | 收敛裁决重新落在真契约:A=P1 把"装载契约 spike 实(含 caps 模块化)",B=P2 复用同一装载契约 + `packageUrl` 加载链。收敛=共用 caps 模块 + 同一装载契约 + 同一 `packageUrl` 端点,**可被 import 图与端点验证**。改 §4.3。 | §4.3 重写 |
|
||||
| **M2** | 重大 | O1(生命周期粒度)默认了"GameModule 是新契约"这一未明示假设。 | 把"装载契约是新立 additive 契约 / 还是折进既有面"本身列为 O1 主问题;生命周期粒度降为 O1 子问题。改 §8。 | §8 O1 重写 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论先行
|
||||
|
||||
**一块基石 = 一套"游戏挂引擎"的统一约定,由两层构成**:
|
||||
- **下层(已冻结、复用)= 母 spec P2 三契约**:`PluginContext.getEngine()`(受控引擎能力面,`api.d.ts:264-303`)+ GamePackage `manifest.packageUrl`(交付/加载锚,`game-package.schema.json:66`)+ SDK storage 根契约。这层是**插件如何拿引擎能力 + 包如何交付**的单一事实源,`28a57b8` 已冻结。
|
||||
- **上层(仍缺、本弧核心)= "游戏宿主装载契约"**:定义"一款**游戏整体**(不止插件)如何挂到 LittleJS 引擎宿主上"——游戏暴露稳定生命周期、绘制面取引擎 `mainContext`、经 `engineInit` 五回调驱动。**这层契约今天全仓不存在**(§0-F1 复证),是 P1 要 spike 实的真东西。其形态(新立 additive 契约 / 折进既有面 / 生命周期粒度)是创始人待拍项 **O1**,本稿不预断。
|
||||
|
||||
> ⚠️ **不要把上层叫成已存在的契约**。原稿用 `GameModule` 命名它并当作"已统一",但它既不在 `contracts/` 也不在 `api.d.ts`;而 `PluginContext` 是**插件**契约,不是**游戏宿主**契约。两者一个都不能替另一个。Q4 铁律(引擎 `import` 只活集成段 host,禁泄进游戏/插件)对两层都成立。
|
||||
|
||||
这套约定一次设计,P1/P2/P3 三相共用:
|
||||
1. **P1 用王蓝莓把上层装载契约 spike 出来**(方案 A:给 `wanglanmei-ref` 加 `?engine=real` 通道,主循环从浏览器 RAF 改挂 `engineInit` 五回调,**游戏层绘制面**从 `#game` 改引擎 `mainContext`,保 `?engine=stub` 回滚=A0 双通道);并把**已存在但仍是闭包**的 `makeEngineCaps` 提升为可 import 模块(§0-F2)。
|
||||
2. **P2 把宿主泛化成"装载任意 GamePackage"**(同一装载契约 + 复用 `packageUrl` 加载链:`packageUrl→iife bundle`;后端写包、`game-studio` 增"引擎包"分支取代掏空的 `startRuntime`)。
|
||||
3. **P3 W-G1 生成游戏按同一装载契约产出**(agent 写码于插件库,20 基准做 bake-off,经 P1 宿主 + e2e harness 评测)。
|
||||
|
||||
**为何"三相同一基石"**:四路侦察确证地面有一道结构性裂缝——真引擎宿主(`host-dev` real)**有掌帧无游戏**,真游戏(`wanglanmei-ref`)**有游戏无引擎**,生成管线产物喂给**已掏空的 1.x runtime**(既不接引擎、也被 W-CLEAN 停用)。三处断口的**同一个根**是"缺上层那份'游戏整体↔引擎'的装载约定"(下层 `getEngine()` 已解决'插件↔引擎',但游戏的 `update/render` 主体仍在契约面之外)。先把这份装载约定 spike 实、再泛化、再让生成按它产出,是唯一不缝合、不返工的收敛路径。**反面**:若 P1/P2/P3 各自造一套接引擎方式(ref 一套、Runner 一套、生成 agent 一套),三套漂移 = 边界模型"同一职不留两条并行路"被破,agent 在多套词汇间犯迷糊,历史游戏兼容性失锚。
|
||||
|
||||
**最大风险(先标在最前)**:**P1 的"真渲同屏"= 把上层装载契约从零 spike 实,不是补几行**——关键难点是:① ref 的游戏层(`core.update`/`drawFrame(g)`,**非插件**)今天没有"取引擎 `mainContext`"的约定面,不能借 `PluginContext`(§0-F3);② 游戏美术画在自带 `#game` 2D 画布、引擎粒子自渲进引擎 `mainContext`,**两面分裂**待合并;③ `main.js` 用浏览器 RAF + `mockNowMs` 自掌帧,与 `engineInit` 掌帧**直接冲突**。这是全弧的第一道真门,过不了则 P2/P3 全部悬空。
|
||||
|
||||
---
|
||||
|
||||
## 2. 背景与问题(四路侦察的地面真相)
|
||||
|
||||
四路只读侦察(全 verified,证据带 file:line)拼出的真相:**"N 款游戏跑在引擎上、人在浏览器真玩、进 feed"这条链今天是断的,断在四处**。
|
||||
|
||||
### 2.1 四处断口
|
||||
|
||||
| 侦察 | 断口 | 关键证据(file:line) |
|
||||
|---|---|---|
|
||||
| **a1/a4** | **王蓝莓上引擎≠补几行**:渲染面分裂 + 掌帧冲突 | `main.js:112` `createHostDevContext` 未传 `engineFactory`(仅 4 键 context2d/seed/audioFactory/clock)→ `plugin.js` 返 null → 粒子全 `NOOP_HANDLE`;游戏美术画在 `main.js:83` `prepareCanvas(#game)` 的 2D `g` 上(`drawFrame(g,…):648`),引擎粒子自渲进 `mainContext`(`host.js:243` `new ParticleEmitter`)=两张分离画布;`main.js:617-691` 自管 `frame(dtReal)`+RAF,与 `host.js:884` 引擎掌帧不兼容 |
|
||||
| **a4** | **host-dev real 有掌帧无游戏**:只挂九插件演示能力 | `host.js:428/874` 留痕"host-dev 无真游戏";`bootHostDev` 只收 `{canvas,statusEl}`(`host.js:150-156`),**零"装载任意游戏"约定**;引擎五回调 `engGameInit/Update/Render` 转发到插件演示(`host.js:883-887`) |
|
||||
| **a2** | **生成管线是断的**:执行器被停用 + 产物喂掏空 host | `AigcTemplateConstants.java:46` `SUPPORTED_TEMPLATE_IDS = List.of()`(W-CLEAN 主动停用)→ 任何任务判 `NO_TEMPLATE_MATCH` 零产出;M-b 产 `GameConfig` JSON → 喂 `game-studio/src/host/runtime/index.ts` 的 `startRuntime`,该 host W-CLEAN 后只画"模板体系重构中"+发 `game_error`(`index.ts:56-113`),**完全非 engine-aware** |
|
||||
| **a3** | **feed 真空 + studio 是 mock** | 真后端 `feed/stream` 返 `list:[]`(W-CLEAN 清掉存量游戏);`:4173` 是 mock 构建(取到 mock "霓虹躲避"假数据,未接真 staging);基建全活(new-api `:3000` healthy / MySQL / cloud `:48080` 跑 1 天+ / admin `:4174`) |
|
||||
|
||||
### 2.2 母 spec(T1b-β execution)已覆盖什么、缺什么
|
||||
|
||||
母 spec `2026-06-13-T1b-β…execution.md` 已**冻结开工**并覆盖:Phase A(A0-A6 引擎真接线,**已收口** `e591e9b`+`3e5cab4`)、Phase B(Runner v2 = 复用后端 `packageUrl` 走 DB-manifest 降级)、Phase C(1.x 退役 srcdoc→URL)、Phase D(参考件经发布主链入 feed)。
|
||||
|
||||
**但母 spec 的 Phase D = "ref 经发布主链入 feed",其产物仍是跑在 stub 插件上的 ref(`main.js:15` 不 import 引擎)**——**它不含"ref 跑在引擎上"**。a4 把这道缝明确标为"结构缺口、归属波次待确认"。**Runner v2 全弧正是补这道缝、并把它与生成主线收敛成一条主梁的波次**:母 spec 完成"引擎能 spike / 包能交付 / feed 能翻态"的零件,本弧负责"用一份契约把这些零件串成'游戏真跑在引擎上、可泛化、可生成'的整链"。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph 今天["地面真相(四处断口)"]
|
||||
E["引擎宿主<br/>host-dev real<br/>有掌帧·无游戏"]
|
||||
G["真游戏<br/>wanglanmei-ref<br/>有游戏·无引擎"]
|
||||
P["生成管线 M-b<br/>停用·产物喂掏空 host"]
|
||||
F["feed 真空<br/>studio=mock"]
|
||||
E -. 不交汇 .- G
|
||||
P -. 喂 .-> X["1.x runtime<br/>掏空·非 engine-aware"]
|
||||
F -. 接 .-> M["mock 假数据"]
|
||||
end
|
||||
今天 ==Runner v2 全弧:两层基石==> 明天
|
||||
subgraph 明天["收敛后"]
|
||||
L0["下层·已冻结(复用)<br/>PluginContext.getEngine()<br/>+ packageUrl"]
|
||||
C["上层·仍缺(本弧 spike)<br/>游戏宿主装载契约<br/>形态待拍 O1"]
|
||||
L0 -.托底.-> C
|
||||
C --> A1["P1 ref spike<br/>装载契约验实·真渲同屏"]
|
||||
C --> A2["P2 泛化宿主<br/>装载任意 GamePackage"]
|
||||
C --> A3["P3 生成游戏<br/>按同一装载契约产出"]
|
||||
end
|
||||
```
|
||||
|
||||
> **图读法**:下层(`getEngine()`/`packageUrl`)已冻结、三相直接复用;上层"游戏宿主装载契约"是本弧 P1 要 spike 实的真新增物,其形态由 O1 拍定后才进 `contracts/`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 目标 / 非目标
|
||||
|
||||
### 3.1 目标
|
||||
1. **钉死一份"游戏宿主装载契约"(上层)**,三相共用、单一事实源;**复用**下层已冻结的 `PluginContext.getEngine()` + `packageUrl`,不另造一套引擎能力面。装载契约形态(新立 additive 契约 / 折进既有面)按 **O1** 拍定。
|
||||
2. **把 `makeEngineCaps` 从闭包提升为可 import 模块**(暂名 `host-dev/engine-caps.js`),host-dev / ref / 未来生成游戏共用同一份引擎能力包装层,物理上杜绝"两份漂移"。**注意(§0-F2)**:数学门面 `engine-math.js`、caps 工厂 `makeEngineCaps`、`engineFactory` 注入**均已存在并已接线**(A1/A4,`e591e9b`/`3e5cab4`);本目标剩余工作只是把闭包搬成独立文件,非从零造。
|
||||
3. **P1**:用 `wanglanmei-ref` 把该装载契约 spike 到"**引擎真掌帧 + 游戏美术与引擎粒子同屏可玩**",人在浏览器真玩,保 `?engine=stub` 回滚。
|
||||
4. **P2**:把宿主泛化为"**装载任意 GamePackage 跑引擎**",复用 `packageUrl` 打通 `packageUrl→bundle→GamePlayer` 链,一款 engine-aware 游戏经发布主链进 feed、真人试玩。
|
||||
5. **P3**:W-G1 用便宜模型(M3/M2.7/DS-V4)按同一装载契约 **agent 写码于插件库**产出游戏,20 基准 bake-off,经 P1 宿主 + e2e harness 评测、金丝雀入 feed N 款。
|
||||
|
||||
### 3.2 非目标
|
||||
- **不动受控面 6 项契约**(`getContext2d/onFrame/getInput/getAudioContext/time/random`,`api.d.ts:264-304`)——引擎可换边界守住。
|
||||
- **不动 feed 终裁引擎选型**(LittleJS feed 侧不动;渠道线小游戏引擎另行竞标,HJ-CH-001)。
|
||||
- **不重做 M-b 执行器的全部范式细节**(P3 只定"生成按上层装载契约产 bundle"的接口边界,模型竞标/评估门判据细节归 W-G1 自身 spec)。
|
||||
- **本稿不冻结上层装载契约的精确 IDL**(仅给 §4.1 草案形态供 O1 决策;拍定后由分相 execution spec 落进 `contracts/`,本稿不预冻字段)。
|
||||
- **不接 OSS/MinIO**(Runner v2 沿母 spec 默认降级 = DB-manifest 带版本 + immutable 头;切 OSS 留上线前)。
|
||||
- **不新增寻路/CCD swept 等引擎缺件**(20 基准已标两条硬边界;是否补门面等 P3 实测数据再拍)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 推荐方案:两层契约(主梁)
|
||||
|
||||
> **主梁 = 下层复用 + 上层 spike**。下层(`getEngine()`/`packageUrl`)已冻结,照搬;上层"游戏宿主装载契约"是本弧真正要立的东西,形态待 O1 拍。本节给出装载契约的**草案形态**(仅供 O1 决策参考,未拍前不进 `contracts/`)。
|
||||
|
||||
### 4.1 上层装载契约的草案形态(待 O1 拍)
|
||||
|
||||
一款游戏 = 一个**纯模块工厂**(不 import 引擎、不碰裸 DOM),暴露稳定生命周期;由集成段 host 经 `engineInit` 五回调驱动;**插件能力**经下层 `ctx.getEngine()` 拿,**游戏整体的绘制面**经装载契约约定为引擎 `mainContext`。
|
||||
|
||||
> ⚠️ **下层 vs 上层的职责切割(F1/F3 的解法)**:`getEngine()`(`api.d.ts:303`)是**插件**拿引擎能力的面,已冻结;它**解决不了**"游戏的 `update/render` 主体怎么挂引擎、怎么拿 `mainContext`"——因为游戏层(ref 的 `core.update`/`drawFrame`)不是插件、不走 `PluginContext`。这正是上层装载契约要补的缺口,**不能拿 `getEngine()` 硬凑**。下图的 `game.*` 生命周期与"`render(g)` 的 `g` 来自引擎 `mainContext`"都属待拍的装载契约草案,非既有契约。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph host["集成段 host(唯一 import 引擎处·Q4 铁律)"]
|
||||
EI["LJS.engineInit(五回调)"]
|
||||
EC["engine-caps.js 模块(提升自 host.js:197 闭包)<br/>particles/audio.synth/math 三面薄包装<br/>=复用下层 getEngine() 背书"]
|
||||
CTX["createHostDevContext<br/>受控面 6 项 + engineFactory(均已存在)"]
|
||||
end
|
||||
subgraph game["游戏侧(装载契约·零引擎 import·形态待 O1)"]
|
||||
GM["game.init(ctx)<br/>game.update(dt)<br/>game.render(g)<br/>game.onInput(ev)"]
|
||||
end
|
||||
EI -->|gameUpdate 回调| GM
|
||||
EI -->|gameRender 回调→引擎 mainContext| GM
|
||||
EC -->|插件能力经 ctx.getEngine()【下层·已冻】| GM
|
||||
CTX -. 游戏绘制面=引擎 mainContext【上层·待 O1】 .-> GM
|
||||
GM -. 禁 .->|直接 import littlejsengine| X1["❌ Q4 违例"]
|
||||
```
|
||||
|
||||
**四条不可破的约定**:
|
||||
1. **掌帧唯一源 = 引擎**:游戏不自起 RAF;`update/render` 由 `engineInit` 五回调驱动(消除 a1/a4 的掌帧冲突)。
|
||||
2. **绘制面唯一 = 引擎 `mainContext`**(`setGLEnable(false)` 纯 2D):游戏美术与引擎粒子渲到**同一张画布**(消除 a1 的渲染面分裂)。**此条是装载契约的核心待解项**(F3)。
|
||||
3. **能力唯一面(插件)= `ctx.getEngine()` 受控面【下层已冻】**:粒子/音频/easing 经包装层(边界模型"引擎有→薄包装");引擎缺件(collision/gamefeel)经插件补层。
|
||||
4. **引擎 import 唯一活点 = host**:游戏/插件零直接 import(Q4 铁律,`entry.js:19-20`)。
|
||||
|
||||
### 4.2 三相复用同一套契约
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
L0["下层契约【已冻·复用】<br/>PluginContext.getEngine() + packageUrl"]
|
||||
C["上层装载契约【本弧 spike·形态待 O1】"]
|
||||
EC["engine-caps.js 模块<br/>(提升自 host.js:197 闭包·非从零)"]
|
||||
L0 -.托底.-> C
|
||||
L0 -.托底.-> EC
|
||||
C --> P1
|
||||
C --> P2
|
||||
C --> P3
|
||||
EC -.共用.-> P1
|
||||
EC -.共用.-> P2
|
||||
EC -.共用.-> P3
|
||||
subgraph P1["P1·ref spike 装载契约"]
|
||||
direction LR
|
||||
P1a["?engine=real 通道<br/>main.js RAF→engineInit 五回调<br/>drawFrame(g)→mainContext"]
|
||||
end
|
||||
subgraph P2["P2·泛化宿主(同契约产品化)"]
|
||||
direction LR
|
||||
P2a["GamePlayer 增'引擎包'分支<br/>复用 packageUrl→iife bundle<br/>取代掏空的 startRuntime"]
|
||||
end
|
||||
subgraph P3["P3·生成游戏(同契约产出)"]
|
||||
direction LR
|
||||
P3a["agent 写码于插件库<br/>产装载契约 bundle<br/>20 基准 bake-off"]
|
||||
end
|
||||
```
|
||||
|
||||
### 4.3 方案 A / B 收敛裁决(重锚到真契约)
|
||||
|
||||
侦察给了两条缝合方向:**A = 给 ref 加 `?engine=real` 通道**(改 `main.js` 主循环挂 `engineInit`);**B = 把 `host-dev` 泛化为"游戏宿主壳"**(`bootHostDev` 加 game 入参,游戏作被装载模块)。
|
||||
|
||||
**裁决:A 与 B 不是两套并行入口,而是同一主梁的两个相位——A=P1 用王蓝莓 spike 上层装载契约,B 的"泛化宿主"=P2 对同一装载契约的产品化。** 收敛性**可被验证**(非口头断言):A 与 P2 必须 ① import 同一个 `engine-caps.js` 模块、② 实现同一份装载契约生命周期、③ 经同一个 `packageUrl` 端点交付/加载——这三点都是 import 图 + 端点可查的硬约束。理由:
|
||||
|
||||
| 维度 | 方案 A(P1 先做) | 方案 B(P2 承接) |
|
||||
|---|---|---|
|
||||
| **改动面** | 小:动 ref `main.js` 主循环 + entry import 引擎 + 复用既有 `engineFactory` 键;保 stub 回滚 | 大:动 host 装载约定 + studio GamePlayer/inject + 后端写包 + 发布主链 |
|
||||
| **取证链** | 复用 ref 现成 CDP 真触摸 + 四件套 evidence(`e2e-input.cdp.cjs`) | 复用 staging 发布链 + feed 走查 |
|
||||
| **风险** | 验"装载契约能不能让一款真游戏真跑在引擎上同屏" | 验"装载契约能不能装载任意包 + 经产品链入 feed" |
|
||||
| **依赖** | A0 地基(已收口)+ O1 装载契约形态拍定 | 依赖 P1 装载契约定稿 + 母 spec Phase B/C/D 零件 |
|
||||
|
||||
**结论**:先做 A(P1)用最小改动、最强取证链把装载契约**验实**;再把验实的契约做成 B(P2)的"泛化宿主"产品化。**装载契约=主干,A/B 是它的两个相位,不是两个竞争方案。** 这正是"勿另造一套"的兑现:A 与 P2 共用 `engine-caps.js` 与同一份装载契约接口,B 只是把"单一 ref 游戏"换成"任意 GamePackage 包加载"。
|
||||
|
||||
**共享模块的真实状态(§0-F2 纠正)**:`engine-math.js`(数学门面)已是独立共享模块,`makeEngineCaps`(粒子/音频/math 三面包装)已实现并已经 `engineFactory: makeEngineCaps` 注入(`host.js:359`)——**但 `makeEngineCaps` 仍是 `host.js:197` `bootHostDev` 内的闭包**(依赖 `engineMode`/顶层 `LJS` import/`window.__engineCalls` 探针),ref/生成游戏**无法直接 import**。剩余工作 = **把这个已存在的闭包提升为可 import 模块** `host-dev/engine-caps.js`(保 `engineMode` 短路 / `__engineCalls` 探针 / null-engine 回归语义逐行不变),host-dev / ref / 生成游戏共用——否则三相各自重建一份等价工厂 = 与"勿另造"直接冲突。**这是"模块化已有件",不是"从零造新件"**(原稿"engine-caps.js 不存在=真新增工作"已订正)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 分相 P1 / P2 / P3
|
||||
|
||||
### 5.1 P1 — 用王蓝莓 spike 装载契约(方案 A)
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| **目标** | `wanglanmei-ref` 加 `?engine=real` 通道:引擎真掌帧 + 游戏美术与引擎粒子**同屏可玩**,人在浏览器真玩;保 `?engine=stub` 回滚。同时把已存在的 `makeEngineCaps` 闭包**提升为可 import 模块** `engine-caps.js`(§0-F2,非从零造)。**前置依赖 O1**:装载契约形态(游戏层怎么取 `mainContext`)须先拍定。 |
|
||||
| **关键设计** | ① `entry.js` 增 `import * as LJS`(引擎真入 ref-bundle,Q4 只活 entry/host);② `main.js` 把 `frame(dtReal)` 拆成逻辑段(`core.update/bundle.tick/applyEvents/fx.step`)挂 `gameUpdate`、绘制段(`drawFrame/palettePost.renderPost`)挂 `gameRender`,**游戏层目标画布从 `#game` 改引擎 `mainContext`**(`setGLEnable(false)` 纯 2D;此口径由 O1 装载契约定义,非借 `PluginContext`);③ 插件受控面 `context2d` 改快照引擎 `mainContext`;④ 粒子/音频/easing 经**提升后的** `engine-caps.js` 包装层(复用下层 `getEngine()` 背书);⑤ `mockNowMs` 在 real 下改逻辑帧钟语义,删/旁置 `main.js:668-690` 浏览器 RAF(对齐 A0 双通道)。 |
|
||||
| **验收门** | ① mini-desktop 真渲门:引擎掌帧帧增量区间断言(对齐 G0-2)+ **游戏层美术与引擎粒子同屏**像素命中(非两张分离画布,即装载契约的 `mainContext` 约定真生效);② 真接线门②:`window.__engineCalls` 含 `particles.spawnEmitter` 等 call-ID(插件运行时真调引擎);③ CDP 真触摸逐 zone 真玩(开店→进货→营业→结算→升级)+ 四件套 evidence;④ `?engine=stub` 回滚通道逐像素确定性回归仍绿;⑤ `engine-caps.js` 提升后既有回归(门0 8/8 + real 门 6/6)逐项不变;⑥ console 零未捕获错误。 |
|
||||
| **爆炸半径** | `games/wanglanmei-ref/{entry.js:15, src/main.js:112/617-691/648}`(主循环重做);**模块化已有件**:把 `host.js:197-318` `makeEngineCaps` 提升为 `host-dev/engine-caps.js` + `host-dev/host.js` 改为 import 它(保 `engineMode` 短路/探针语义不变;`engine-math.js` 已是独立模块无需动);`SIZES.md` 增"游戏产物(引擎+包装层)"新口径(ref 197KB→~540KB,见 §6)。受控面 6 项契约**零改**(`engineFactory` 是既有可选入参 `api.d.ts:405`);**新增的是上层装载契约工件**(形态待 O1)。 |
|
||||
|
||||
### 5.2 P2 — 泛化宿主:装载任意 GamePackage(方案 B)
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| **目标** | 同一装载契约 + 包加载:复用 `packageUrl→iife bundle`;后端写包;`game-studio` GamePlayer/inject 增"引擎包"分支取代掏空的 `startRuntime`;studio 改 staging 重建。产出≥1 款 engine-aware 游戏(先用 ref 级)经发布主链入 feed、真人试玩。 |
|
||||
| **关键设计** | ① **包加载形态**:GamePackage `manifest.packageUrl` 指向 iife bundle(沿母 spec §5 = 复用后端 `packageUrl` 走 DB-manifest 端点 `/app-api/runtime/package/{versionId}/manifest`,天然含 versionId 版本化);② **GamePlayer 分支**:`GamePlayer.vue:128-168` 取包后,inject 不再硬绑掏空的 `startRuntime`(`inject.ts:104`),改"加载引擎 bundle + 经装载契约 boot"分支——**这是取代 `runtime/index.ts` 掏空壳的关键**;③ CSP 须放行 script-src(Runner origin)+connect-src(bundle 取源)(母 spec §8 四项兼容矩阵);④ studio `npm run build -- --mode staging` 重建,消除 mock 中间件掩盖(a3 阻断 B)。 |
|
||||
| **验收门** | ① 后端 feed/stream 出现该游戏(`status=PUBLISHED(4)`);② 真人浏览器开 feed→点卡→**引擎 bundle 真渲非"重构中"**+`game_loaded`+`game_play_end`;③ 防 demo 兜底假阳性(母 spec 门4:checksum=发布版本 ∧ gameId/versionId 匹配 ∧ 兜底触发即 fail);④ 链路③⑤真走查解封。 |
|
||||
| **爆炸半径** | `game-studio/src/host/{GamePlayer.vue, inject.ts, runtime/index.ts(掏空壳→引擎包分支), contract.ts}`;后端发布主链(`ProjectServiceImpl/PublishOrchestrationServiceImpl/FeedServiceImpl`,被动验证);`contracts/game-package.schema.json:66` `packageUrl`(契约已留位,additive)。**与母 spec Phase B/C/D 高度重叠**——P2 是把母 spec 的"ref 入 feed(跑 stub)"升级为"engine-aware 包入 feed(跑引擎)",须与母 spec owner 对齐避免双写。 |
|
||||
|
||||
### 5.3 P3 — W-G1 生成游戏按同一装载契约产出
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| **目标** | 便宜模型(M3/M2.7/DS-V4)按上层装载契约 **agent 写码于插件库**产出游戏 bundle(非 GameConfig JSON),20 基准做 bake-off,经 P1 宿主 + e2e harness 评测、过门金丝雀入 feed N 款。 |
|
||||
| **关键设计** | ① **范式切换**:从"模板填参出 GameConfig JSON"(M-b 现状)改为"agent 写装载契约代码→esbuild 打 iife bundle"(HJ-GEN-001 终裁);② **白名单/契约前置**:新模板体系(插件库 templateId 或新生成包类型)注册进 `AigcTemplateConstants.SUPPORTED_TEMPLATE_IDS`(解 a2 阻断①)+ prompt registry 适配(`contracts/prompts`);③ **靶集**:20 经典基准三批(冒烟 6 易 / 主力 8 中 / 压测 6 难),暴露 4 类能力短板(文本 HUD / 网格 / CCD / 拖拽);④ **评测**:复用 agent-loop-v1 工具箱(judge/ledger/预算闸/CDP 真玩),过 P1 宿主真渲 + harness 门兜底。 |
|
||||
| **验收门** | ① 黄金集批跑 acceptRate≥0.8 分模板(HJ-GEN-001 口径);② 金丝雀 N 款 feedVisible 真渲;③ new-api `logs.quota` 成本分解对照 L1 门(¥0.15);④ harness 门稳定逮"穿透/消行错/不可玩"(便宜模型路线命门)。 |
|
||||
| **爆炸半径** | `game-cloud` aigc(`AigcGenerateExecutor`/`AigcTemplateConstants`/生成-构建-取证管线);`contracts/{prompts, templates 或新包类型}`;agent-loop-v1 工具箱。**前置硬卡已解(DeepSeek key 就绪)**;生成范式从 JSON→bundle 是 P3 最大改动面(产物形态变 = host 消费方式变,靠 P2 的引擎包分支兜)。 |
|
||||
|
||||
### 5.4 三相依赖与解锁顺序
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A0["A0 地基<br/>✅ 已收口"] --> P1
|
||||
O1["O1 装载契约形态<br/>⚠️ 待创始人拍"] --> P1
|
||||
EC["engine-caps.js<br/>模块(P1 提升·非从零)"] --> P1
|
||||
P1["P1 ref spike<br/>装载契约验实"] --> P2["P2 泛化宿主<br/>包加载+入 feed"]
|
||||
P1 --> P3["P3 W-G1 生成<br/>按契约产出"]
|
||||
P2 --> P3
|
||||
KEY["DeepSeek key<br/>✅ 就绪"] --> P3
|
||||
style P1 fill:#fff3cd
|
||||
style P2 fill:#d1ecf1
|
||||
style P3 fill:#d4edda
|
||||
style O1 fill:#f8d7da
|
||||
```
|
||||
|
||||
> P1 解锁前先解 **O1**(装载契约形态拍定)。P3 双依赖 P1(契约验实)+ P2(引擎包加载链):生成的 bundle 要既"按装载契约挂引擎"(P1 验实)、又"经包加载进 feed 试玩"(P2 打通)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键权衡
|
||||
|
||||
### 6.1 体积:ref 197KB→~540KB 是否触 Tier1 体积口径?
|
||||
- **事实**:ref 现 197KB(不打引擎,esbuild dry-check 实测 `has zzfxG:false`);接引擎后 ~540KB(host-dev entry 实测 544KB 含 `engineInit+ParticleEmitter`)。SIZES.md 当前是**插件增量法**(合计插件 gz≈12KB,自律线 60K),**不覆盖"游戏产物=引擎+包装层"口径**。
|
||||
- **判定**:~540KB raw 是 **min 未 gz** 量级;LittleJS 完整版 raw≈55KB gz(v1.18.19),加游戏代码后游戏产物 gz 远在 B1 入场券 **gz≤350KB / raw≤1.5MB**(tiered-engine 记忆)之内——**不触红线**。**但须给 SIZES.md 增"游戏产物口径"新表**(与"插件增量"分离),并按"引擎+包装层"重锚(母 spec 裁决①已要求重锚,但仅针对 host-dev,未覆盖游戏产物——本弧补上)。**勿再引"13KB"**(js13k 极限分支,已废,tiered-engine 记忆)。
|
||||
|
||||
### 6.2 确定性逐像素如何与引擎 RAF 共存?
|
||||
- **事实**:引擎一个 RAF 内追帧步数 N 由墙钟抖动决定(`littlejs.esm.js:256` `frameTimeBufferMS` 循环),外部轮询 `frameCount>=startFrame+frames` 会 OVERSHOOT → same-seed 重跑像素不一致(A0 已核验)。
|
||||
- **判定**:**real 通道不承担逐像素确定性,由 `?engine=stub` 通道独占兜底**(A0 既定双通道策略)。real 通道只证"引擎掌帧 + 同屏真渲 + call-ID 真接线";逐像素哈希回归走 stub。P1 验收门据此分两套(real=同屏可玩+call-ID / stub=逐像素回归),不强求 real same-pixel。
|
||||
|
||||
### 6.3 契约形态对生成 agent 的约束?
|
||||
- **正向**:上层装载契约是 agent 的**唯一产出靶**(写 `init/update/render/onInput`,插件能力经下层 `ctx.getEngine()`),一职一路、词汇收敛(边界模型);20 基准里 ~12 款能力面齐备可直接产出。**前提是 O1 把装载契约形态拍死**,否则 agent 没有稳定产出靶。
|
||||
- **代价/风险**:4 类高频短板(文本 HUD / 网格 / CCD swept / 拖拽手势)**当前无插件门面**——便宜模型 20 个 prompt 各造一遍轮子,一致性/token/出错率三重受损(20 基准 C.2)。**这是 P3 实测要量化的核心数据**,据此决定"是否把高频缺口(文本 HUD/网格)补成插件门面"——**不预先投入,等批次 3 实测再拍**。
|
||||
|
||||
---
|
||||
|
||||
## 7. 风险 / 兼容性
|
||||
|
||||
| 风险 | 等级 | 缓解 / 回滚 |
|
||||
|---|---|---|
|
||||
| **装载契约缺口(F1/F3)未补就开工** | **P0** | 全仓无"游戏整体↔引擎"装载契约,`getEngine()` 是插件面凑不上(§0-F1/F3)。**必须 O1 先拍装载契约形态、再开 P1**;否则 P1 边做边发明接口 = 又一条平行路。本稿已把它从"既成事实"降为待拍 open item。 |
|
||||
| **P1 真渲同屏过不了**(渲染面合并 + 掌帧重做难度被低估) | **P0** | 这是全弧第一道真门;A0 双通道已证引擎掌帧可行,ref 需把"自管画布+RAF"换成"引擎 mainContext+五回调"+按装载契约接游戏层绘制面。**回滚 = `?engine=stub`**(行为与 α 逐行等价,A0 已保留)。过不了则全弧暂停、不连累 stub 既有交付。 |
|
||||
| **受控面被动了** | P0 | 受控面 6 项契约**零改**是硬约束(`engineFactory` 是既有可选入参);P1/P2/P3 任何相若需改受控面=触发架构复议,不得自行扩。**上层装载契约是 additive 新增,不得改下层既冻 6 项**。 |
|
||||
| **P2 与母 spec Phase B/C/D 双写** | P1 | P2 高度重叠母 spec;须与母 spec owner 对齐——P2 = 把母 spec"ref 入 feed(stub)"升级为"engine-aware 包入 feed(引擎)",是承接非另起。 |
|
||||
| **历史游戏兼容** | P1 | 插件公开 API(EmitterConfig 等)冻结不变 → 老游戏照跑(包装/补层是内部事,边界模型);引擎包装的能力改走引擎渲染、像素可能微异=可移植性提升(创始人最初诉求)。上层装载契约对存量 ref 而言是 additive 通道(`?engine=stub` 即旧行为)。 |
|
||||
| **engine-caps 模块化引入回归** | P1 | 把**已存在的** `host.js:197-318` `makeEngineCaps` 闭包提升为模块须保 `engineMode` 短路 / `__engineCalls` 探针 / null-engine 回归语义逐行不变;门0 8/8 既有回归 + real 引擎门 6/6 作回归基线(§0-F2:这是搬运已有件,非新写)。 |
|
||||
| **P3 生成范式 JSON→bundle 改动大** | P1 | 产物形态变 = host 消费方式变,靠 P2 引擎包分支兜底;harness 门兜底是便宜模型路线命门。P3 评估门判据细节归 W-G1 自身 spec,不在本弧预烤。 |
|
||||
| **契约回滚** | — | 上层装载契约走 `?engine=stub` 通道 / Runner URL 复用 packageUrl / storage 均 additive,回滚=移除新增不破存量。 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 待创始人拍的 Open Items
|
||||
|
||||
| # | 待拍项 | 选项 / 默认建议 |
|
||||
|---|---|---|
|
||||
| **O1**(升级·F1/M2) | **上层"游戏宿主装载契约"的归属与形态**——这是本弧第一待拍:①**归属**:新立第 9 类契约(`contracts/` 下独立工件)/ 还是折进既有 `core/api.d.ts` 作 `GameHostContract` additive 段?②**生命周期粒度**:`init/update/render/onInput` 四件够不够?是否要 `resize/destroy/onVisibility`?③**绘制面约定**:游戏层取引擎 `mainContext` 的口径(不能借 `PluginContext`)。 | 建议:①新立 additive 契约(避免污染插件契约语义,与边界模型"一职一路"一致);②取 `init/update/render/onInput`+`destroy`(卸载卫生),其余 additive 后补;③`mainContext` 经装载契约 boot 时注入游戏层。**P1 开工前必须拍 ①③,否则 spike 无稳定靶。** |
|
||||
| **O2** | **P2 与母 spec T1b-β Phase D 的边界**:是把母 spec Phase D 直接升级为"engine-aware 包入 feed"(合并),还是 P2 作为 Phase D 之后的独立增量波次? | 建议:合并——母 spec Phase D 的"ref 入 feed"本就该是 engine-aware 才有意义(否则 feed 里跑 stub 无产品价值)。**待拍是否改母 spec 范围。** |
|
||||
| **O3** | **P3 生成产物白名单注册口径**:新模板体系用"单一通用装载契约 templateId"还是"按品类多 templateId"注册进 `SUPPORTED_TEMPLATE_IDS`? | 建议:先单一通用 templateId 验管线,品类区分留 prompt 层;**待拍。** |
|
||||
| **O4** | **高频缺口补门面时机**:文本 HUD / 网格门面是否在 P3 前预补,还是等批次 3 实测出错率再补? | 建议(20 基准既定):**不预补,等实测数据**;但若创始人想压 P3 一致性,可预补文本 HUD(最高频,~15/20 款需要)。**待拍。** |
|
||||
| **O5** | **N(首批入 feed 款数)**:P2 先 1 款 ref 验链,P3 金丝雀 N 款——N 定多少、品类怎么选? | 建议:P3 先冒烟批 6 款过门、取 ≥3 款金丝雀入 feed;**待拍。** |
|
||||
| **O6**(流程) | **本稿是否需补一轮独立外部对抗复审再冻结分相 execution?**(本轮对抗 JSON 为空,整改由主笔自核完成,§0) | 建议:主梁已重锚且自核证据可复证,但 F1 触主干契约裁决,**建议补一轮外部对抗复审确认 O1 归属裁决后再开 P1 execution**。**待拍。** |
|
||||
|
||||
---
|
||||
|
||||
## 风险与假设(诚实标注·r2 已订正)
|
||||
- **[verified·订正]** **上层"游戏宿主装载契约"全仓不存在**(`grep GameModule` 在 `contracts/`+`src/core/`+`host-dev/` 零命中)——这是 §0-F1 的根据。既有契约只有 `PluginContext`(插件面,`api.d.ts:264-303`)+ `packageUrl`(`game-package.schema.json:66`),二者均已冻结(母 spec L163 / `28a57b8`)。
|
||||
- **[verified·订正]** **`engine-caps.js` 文件确不存在,但其内容已大半存在并接线**:`engine-math.js`(数学门面)已独立成模块(`host.js:47` import);`makeEngineCaps`(粒子/音频/math 三面包装)已实现于 `host.js:197-318` 并已注入 `engineFactory: makeEngineCaps`(`host.js:359`)。故剩余工作 = 把闭包提升为可 import 模块(搬运),**非"从零造"**(原稿"真新增工作"已订正为 §0-F2)。
|
||||
- **[verified]** 四处断口、SIZES 口径、母 spec 覆盖范围——源码/spec 实证,证据锚点见 §2/§4/§5 各 file:line。
|
||||
- **[verified]** ref 游戏层(`core.update`/`drawFrame(g,648)`)画在 `#game`(`main.js:83`)、不走 `PluginContext`(`main.js:112` 仅插件)——§0-F3 据此:游戏层取 `mainContext` 须靠待立的装载契约。
|
||||
- **[inferred]** "便宜模型能产出 ✅ 款可玩件"是 P3 待验推断(20 基准 C 已标);本弧只确立契约与评测路径,不预断模型产出质量。
|
||||
- **[inferred]** ref 接引擎后 ~540KB 不触 B1 入场券——基于 host-dev entry 544KB 实测 + gz 量级推断;P1 须用 `size.mjs` 实测游戏产物 gz 坐实。
|
||||
- **[assumption]** 好玩基线 v2 五要素精确判据未在本次定位到原文(散见多 spec),P3 验收门精确判据须开工前从评估门 spec 取齐。
|
||||
- **[流程]** 本轮对抗复审结论体为空(JSON=null),§0 整改清单由主笔架构师自核源码生成(全可复证);是否补一轮外部对抗复审见 O6。
|
||||
- **[未决]** O1-O6 六项待创始人拍。
|
||||
@ -20,3 +20,19 @@
|
||||
|
||||
合计插件 gz 增量 ≈ **12100B**(自律线 60K;超 60K 须创始人显式放行,执行版 §5)。
|
||||
|
||||
---
|
||||
|
||||
## 游戏产物(引擎+包装层)——P1 王蓝莓上引擎口径
|
||||
|
||||
> **口径区别**:上表「增量法」量的是**插件净增**(基线壳→+core→逐插件叠加,不含引擎)。游戏产物(一款 game-shop bundle)= **引擎 + 能力插件库 + 游戏代码** 整包,是另一口径,不并入上表。
|
||||
> 同一 esbuild 锁参(`bundle+minify+iife+es2019+sourcemap=false`);gz=`zlib.gzipSync(buf,{level:9})`。
|
||||
|
||||
| 产物 | 引擎入包 | raw | gz | B1 入场券(gz≤350KB) | 备注 |
|
||||
|---|---|--:|--:|:--|---|
|
||||
| ref **stub** 通道 | 否 | ~197KB | — | ✔ | 不打引擎(回滚锚;引擎 import 经 engineMode 短路不触达运行时,但 esbuild 仍随 entry 引擎 import 打入——故 stub/real 同一份 bundle,差异在运行时通道,不在产物字节) |
|
||||
| ref **real**(引擎真入) | 是(`littlejsengine.esm.js` 640KB 源经 tree-shake+min) | **264KB** | **88KB** | ✔(gz 88KB ≪ 350KB) | `entry.js`/`main.js` import 引擎(Q4:只 entry/host);`game-shop.js` 游戏工厂零引擎 import(metafile 实证)。`grep -q ParticleEmitter` 核入包 |
|
||||
|
||||
> **实测来源**:6c6g `node scripts/build.mjs games/wanglanmei-ref/entry.js → /tmp` dry-build(raw=264476 / gz=89583,2026-06-13)。引擎入包凭据:metafile `inputs` 含 `node_modules/littlejsengine/dist/littlejs.esm.js`,且仅 `main.js`/`entry.js` import 之(Q4)。
|
||||
> **注**:插件增量法不覆盖游戏产物口径——勿把游戏产物字节并入上表「合计插件 gz 增量」。mini-desktop 可经 `size.mjs` 复测覆盖本节。
|
||||
> **勿引「13KB / 55KB」自研 Canvas 旧口径**(已废,见 [.agents tech-decisions §1.1]);引擎=LittleJS 完整版。
|
||||
|
||||
|
||||
@ -5,13 +5,38 @@
|
||||
* 【构建】沿 scripts/build.mjs 锁参(bundle+minify+iife+es2019+sourcemap=false),见 BUILD.md:
|
||||
* node scripts/build.mjs games/wanglanmei-ref/entry.js games/wanglanmei-ref/dist/ref-bundle.js --global-name=WanglanmeiRef
|
||||
*
|
||||
* 【定位】仿 host-dev/entry.js:把「core 协议 + 所用插件 + 游戏(逻辑/渲染/宿主)」汇成一个
|
||||
* iife 全局(WanglanmeiRef),index.html 调 bootRef 启动。α 不接 littlejsengine(裁定同 host-dev)。
|
||||
* 本文件 import-safe(node 可 import 不执行任何浏览器调用),boot 由页面显式触发。
|
||||
* 【定位】仿 host-dev/entry.js:把「core 协议 + 所用插件 + 游戏(逻辑/渲染/宿主)+ 引擎」汇成一个
|
||||
* iife 全局(WanglanmeiRef),index.html 调 bootRef 启动。
|
||||
*
|
||||
* 【P1 · Option C:引擎 import 只落本入口,且经动态 import 延后求值(mustFix #1)】
|
||||
* 起因:本游戏的 import-graph.test.mjs(MODULES 表第 24 行)显式断言 `../entry.js` 可在 node 导入。
|
||||
* 而 littlejsengine 的 ESM 在**模块装载期**就触 window/AudioContext(esm.js:5556/6753),
|
||||
* 顶层 `import * as LJS from 'littlejsengine'` 会让 node 一 import entry.js 即抛 ReferenceError → 该测红。
|
||||
* 修法:把引擎 import 从「顶层静态」改为「**包装层内动态 import**」:
|
||||
* · node import entry.js 时**不**触发引擎装载(bootRef 未被调用)→ entry.js node import-safe → 过 import-graph。
|
||||
* · esbuild(bundle+iife,无 splitting)会把动态 import 的引擎**内联进单文件产物**
|
||||
* (实测改写为 `Promise.resolve().then(()=>(引擎init(), 引擎命名空间))`,无 fetch/无外部 chunk)
|
||||
* → 引擎仍真入 ref-bundle(metafile littlejsengine + 产物 engineInit 符号齐备,已 dry-build 亲验)。
|
||||
* · 浏览器运行期调 bootRef 时才求值引擎(window/AudioContext 已存在),拿到名字空间注入 main.js real 通道。
|
||||
* Q4 铁律不破:引擎 import 仍只活本入口;main.js(host 层)经注入用引擎、不直接 import;game-shop.js 游戏工厂零引擎 import。
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { bootRef } from './src/main.js';
|
||||
import { bootRef as bootRefCore } from './src/main.js';
|
||||
|
||||
export { bootRef };
|
||||
/**
|
||||
* 【Option C 包装层】对外 bootRef:动态 import 引擎名字空间后注入、委托 main.js 的真实现。
|
||||
* index.html 调的是 iife 全局 WanglanmeiRef.bootRef(即本包装)。
|
||||
* - 动态 import 延后求值:node import entry.js 不触引擎(保 entry.js node import-safe,过 import-graph 第 24 行);
|
||||
* esbuild 把引擎内联进 ref-bundle(real 入包不丢,已 dry-build metafile 亲验)。
|
||||
* - stub 通道 main.js 不触达引擎,注入与否无副作用(保回滚锚行为逐行不变)。
|
||||
* - 调用方已显式给 engine 则尊重其值,不重复装载(便于 host-dev 等复用)。
|
||||
* @param {{canvas: HTMLCanvasElement, statusEl?: HTMLElement, engine?: any}} [opts]
|
||||
* @returns {Promise<object>} refHost 句柄
|
||||
*/
|
||||
export async function bootRef(opts) {
|
||||
// 动态 import:求值时机延后到 bootRef 被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = (opts && opts.engine) || (await import('littlejsengine'));
|
||||
return bootRefCore(Object.assign({}, opts, { engine }));
|
||||
}
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
{
|
||||
"checklist": {
|
||||
"shougan": {
|
||||
"tapFeedbackMaxFrames": 0,
|
||||
"tapFeedbackWithin3": true,
|
||||
"everyTapScalePulse": true,
|
||||
"comboUiShown": false,
|
||||
"flightEasingUsed": true,
|
||||
"hitStopTriggered": false,
|
||||
"bufferedServeReplays": 0
|
||||
},
|
||||
"meishu": {
|
||||
"allDrawsViaPalette": true,
|
||||
"vignetteEnabled": true,
|
||||
"rushPaletteSeen": false,
|
||||
"assetsLoaded": "13/13",
|
||||
"noPrimaryPlaceholderRects": true
|
||||
},
|
||||
"yinyue": {
|
||||
"audioUnlocked": false,
|
||||
"bgmReady": false,
|
||||
"bgmCrossfadeSeen": false,
|
||||
"sfxKindCount": 0,
|
||||
"sfxKinds": []
|
||||
},
|
||||
"shendu": {
|
||||
"loopReachable": true,
|
||||
"loopOps": 25,
|
||||
"failureReachable": true,
|
||||
"failureReason": "reputation",
|
||||
"priceTwoSidedLoss": true,
|
||||
"upgradeTreeSize": 8,
|
||||
"firstUpgradeDay": 2
|
||||
},
|
||||
"juese": {
|
||||
"keeperSpriteResident": true,
|
||||
"lineGroups": 14,
|
||||
"lineTotal": 30,
|
||||
"linesShownLive": 0
|
||||
}
|
||||
},
|
||||
"measures": {
|
||||
"particle": {
|
||||
"gold": 0,
|
||||
"cream": 0,
|
||||
"smoke": 0
|
||||
},
|
||||
"audio": {
|
||||
"sfxKinds": [
|
||||
"tick",
|
||||
"shelve",
|
||||
"arrive",
|
||||
"coin",
|
||||
"empty",
|
||||
"upgrade"
|
||||
],
|
||||
"bgm": {
|
||||
"ready": true,
|
||||
"failed": false,
|
||||
"idleLevel": 0.23916264021400713,
|
||||
"rushLevel": 5.689900643400063e-12
|
||||
}
|
||||
},
|
||||
"assetsMissing": [],
|
||||
"frames": 6152,
|
||||
"taps": 517,
|
||||
"tapFeedbackMaxFrames": 1,
|
||||
"comboUiShown": 1,
|
||||
"hitStopCount": 0,
|
||||
"rushPaletteSeen": true,
|
||||
"bgmCrossfadeSeen": true,
|
||||
"linesShown": 20,
|
||||
"bufferedServeReplays": 0
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,358 @@
|
||||
{
|
||||
"meta": {
|
||||
"url": "http://127.0.0.1:4320/?engine=real&seed=305441741",
|
||||
"seed": 305441741,
|
||||
"startedAt": "2026-06-13T17:53:05.383Z",
|
||||
"harness": "e2e-input.cdp.cjs(主会话)",
|
||||
"engineMode": "real",
|
||||
"coordMap": {
|
||||
"rect": {
|
||||
"l": 0,
|
||||
"t": 0,
|
||||
"w": 390,
|
||||
"h": 844,
|
||||
"vw": 390,
|
||||
"vh": 844
|
||||
},
|
||||
"map": {
|
||||
"l": 0,
|
||||
"t": 0,
|
||||
"sx": 1,
|
||||
"sy": 1
|
||||
},
|
||||
"canvas": "#game-engine"
|
||||
},
|
||||
"endedAt": "2026-06-13T17:54:49.277Z"
|
||||
},
|
||||
"gates": {
|
||||
"engineFrameDrive": {
|
||||
"dFrame": 31,
|
||||
"range": [
|
||||
20,
|
||||
45
|
||||
],
|
||||
"s0": {
|
||||
"frame": 41,
|
||||
"time": 0.6666666666666666,
|
||||
"timeReal": 0.7665380000000002,
|
||||
"paused": false,
|
||||
"timeScale": 1
|
||||
},
|
||||
"s1": {
|
||||
"frame": 72,
|
||||
"time": 1.1833333333333333,
|
||||
"timeReal": 1.2832379999999994,
|
||||
"paused": false,
|
||||
"timeScale": 1
|
||||
}
|
||||
},
|
||||
"engineWiringCallId": {
|
||||
"hasSpawn": true,
|
||||
"sample": [
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx"
|
||||
]
|
||||
},
|
||||
"sameScreenPixels": {
|
||||
"nonEmpty": true,
|
||||
"width": 780,
|
||||
"height": 1688,
|
||||
"warmHits": 682,
|
||||
"brightHits": 13028,
|
||||
"maxCh": 254
|
||||
},
|
||||
"closedLoopRealInput": true,
|
||||
"engineTakeover": true
|
||||
},
|
||||
"failures": [],
|
||||
"zonesSeenByPhase": {
|
||||
"real-prep-day1": [
|
||||
"dec-soda",
|
||||
"inc-soda",
|
||||
"dec-snack",
|
||||
"inc-snack",
|
||||
"dec-bread",
|
||||
"inc-bread",
|
||||
"price-slider",
|
||||
"up-speed1",
|
||||
"up-shelf1",
|
||||
"up-unlock_ice",
|
||||
"up-decor1",
|
||||
"up-unlock_toy",
|
||||
"up-speed2",
|
||||
"up-shelf2",
|
||||
"up-decor2",
|
||||
"openShop"
|
||||
],
|
||||
"real-day2-prep": [
|
||||
"dec-soda",
|
||||
"inc-soda",
|
||||
"dec-snack",
|
||||
"inc-snack",
|
||||
"dec-bread",
|
||||
"inc-bread",
|
||||
"price-slider",
|
||||
"up-speed1",
|
||||
"up-shelf1",
|
||||
"up-unlock_ice",
|
||||
"up-decor1",
|
||||
"up-unlock_toy",
|
||||
"up-speed2",
|
||||
"up-shelf2",
|
||||
"up-decor2",
|
||||
"openShop"
|
||||
]
|
||||
},
|
||||
"stateSnapshots": [
|
||||
{
|
||||
"tag": "real-menu",
|
||||
"state": {
|
||||
"phase": "title",
|
||||
"day": 1,
|
||||
"cash": 1200,
|
||||
"reputation": 62,
|
||||
"priceMul": 1,
|
||||
"stock": {},
|
||||
"unlocked": [
|
||||
"soda",
|
||||
"snack",
|
||||
"bread"
|
||||
],
|
||||
"upgrades": {},
|
||||
"customers": 0,
|
||||
"combo": 0,
|
||||
"today": {
|
||||
"revenueJiao": 0,
|
||||
"soldCostJiao": 0,
|
||||
"served": 0,
|
||||
"angry": 0,
|
||||
"stockout": 0,
|
||||
"walkaway": 0,
|
||||
"rescued": 0,
|
||||
"bigSales": 0,
|
||||
"maxCombo": 0
|
||||
},
|
||||
"lifetime": {
|
||||
"days": 0,
|
||||
"served": 0,
|
||||
"angry": 0,
|
||||
"netJiao": 0,
|
||||
"upgradesBought": 0
|
||||
},
|
||||
"gameoverReason": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "real-after-stock-day1",
|
||||
"state": {
|
||||
"phase": "prep",
|
||||
"day": 1,
|
||||
"cash": 840,
|
||||
"reputation": 62,
|
||||
"priceMul": 1,
|
||||
"stock": {
|
||||
"soda": 6,
|
||||
"snack": 6,
|
||||
"bread": 6
|
||||
},
|
||||
"unlocked": [
|
||||
"soda",
|
||||
"snack",
|
||||
"bread"
|
||||
],
|
||||
"upgrades": {},
|
||||
"customers": 0,
|
||||
"combo": 0,
|
||||
"today": {
|
||||
"revenueJiao": 0,
|
||||
"soldCostJiao": 0,
|
||||
"served": 0,
|
||||
"angry": 0,
|
||||
"stockout": 0,
|
||||
"walkaway": 0,
|
||||
"rescued": 0,
|
||||
"bigSales": 0,
|
||||
"maxCombo": 0
|
||||
},
|
||||
"lifetime": {
|
||||
"days": 0,
|
||||
"served": 0,
|
||||
"angry": 0,
|
||||
"netJiao": 0,
|
||||
"upgradesBought": 0
|
||||
},
|
||||
"gameoverReason": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "real-after-serving-day1",
|
||||
"state": {
|
||||
"phase": "settle",
|
||||
"day": 1,
|
||||
"cash": 1239,
|
||||
"reputation": 41.8,
|
||||
"priceMul": 1,
|
||||
"stock": {
|
||||
"soda": 0,
|
||||
"snack": 0,
|
||||
"bread": 0
|
||||
},
|
||||
"unlocked": [
|
||||
"soda",
|
||||
"snack",
|
||||
"bread"
|
||||
],
|
||||
"upgrades": {},
|
||||
"customers": 0,
|
||||
"combo": 0,
|
||||
"today": {
|
||||
"revenueJiao": 799,
|
||||
"soldCostJiao": 360,
|
||||
"served": 18,
|
||||
"angry": 0,
|
||||
"stockout": 13,
|
||||
"walkaway": 0,
|
||||
"rescued": 0,
|
||||
"bigSales": 0,
|
||||
"maxCombo": 9
|
||||
},
|
||||
"lifetime": {
|
||||
"days": 1,
|
||||
"served": 18,
|
||||
"angry": 0,
|
||||
"netJiao": 39,
|
||||
"upgradesBought": 0
|
||||
},
|
||||
"gameoverReason": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"hashes": {},
|
||||
"screenshots": [
|
||||
"real-menu.png",
|
||||
"real-prep-day1.png",
|
||||
"real-sameScreen.png",
|
||||
"real-settle-day1.png",
|
||||
"real-day2-upgraded.png"
|
||||
],
|
||||
"boot": {
|
||||
"anchors": [
|
||||
"t_boot",
|
||||
"t_plugins_ready",
|
||||
"t_game_init",
|
||||
"t_first_paint",
|
||||
"t_input_bound",
|
||||
"t_game_start"
|
||||
],
|
||||
"initOk": true,
|
||||
"registeredPlugins": [
|
||||
"collision",
|
||||
"physics-lite",
|
||||
"gamefeel",
|
||||
"particles-juice",
|
||||
"palette-post",
|
||||
"save-progress",
|
||||
"audio-music",
|
||||
"runtime-probe"
|
||||
]
|
||||
},
|
||||
"checklist": {
|
||||
"shougan": {
|
||||
"tapFeedbackMaxFrames": 0,
|
||||
"tapFeedbackWithin3": true,
|
||||
"everyTapScalePulse": true,
|
||||
"comboUiShown": false,
|
||||
"flightEasingUsed": true,
|
||||
"hitStopTriggered": false,
|
||||
"bufferedServeReplays": 0
|
||||
},
|
||||
"meishu": {
|
||||
"allDrawsViaPalette": true,
|
||||
"vignetteEnabled": true,
|
||||
"rushPaletteSeen": false,
|
||||
"assetsLoaded": "13/13",
|
||||
"noPrimaryPlaceholderRects": true
|
||||
},
|
||||
"yinyue": {
|
||||
"audioUnlocked": false,
|
||||
"bgmReady": false,
|
||||
"bgmCrossfadeSeen": false,
|
||||
"sfxKindCount": 0,
|
||||
"sfxKinds": []
|
||||
},
|
||||
"shendu": {
|
||||
"loopReachable": true,
|
||||
"loopOps": 25,
|
||||
"failureReachable": true,
|
||||
"failureReason": "reputation",
|
||||
"priceTwoSidedLoss": true,
|
||||
"upgradeTreeSize": 8,
|
||||
"firstUpgradeDay": 2
|
||||
},
|
||||
"juese": {
|
||||
"keeperSpriteResident": true,
|
||||
"lineGroups": 14,
|
||||
"lineTotal": 30,
|
||||
"linesShownLive": 0
|
||||
}
|
||||
},
|
||||
"measures": {
|
||||
"particle": {
|
||||
"gold": 0,
|
||||
"cream": 0,
|
||||
"smoke": 0
|
||||
},
|
||||
"audio": {
|
||||
"sfxKinds": [
|
||||
"tick",
|
||||
"shelve",
|
||||
"arrive",
|
||||
"coin",
|
||||
"empty",
|
||||
"upgrade"
|
||||
],
|
||||
"bgm": {
|
||||
"ready": true,
|
||||
"failed": false,
|
||||
"idleLevel": 0.23916264021400713,
|
||||
"rushLevel": 5.689900643400063e-12
|
||||
}
|
||||
},
|
||||
"assetsMissing": [],
|
||||
"frames": 6152,
|
||||
"taps": 517,
|
||||
"tapFeedbackMaxFrames": 1,
|
||||
"comboUiShown": 1,
|
||||
"hitStopCount": 0,
|
||||
"rushPaletteSeen": true,
|
||||
"bgmCrossfadeSeen": true,
|
||||
"linesShown": 20,
|
||||
"bufferedServeReplays": 0
|
||||
},
|
||||
"softLive": {
|
||||
"rushPaletteSeen": true,
|
||||
"bgmCrossfadeSeen": true,
|
||||
"sfxKindCount": 0,
|
||||
"hitStopTriggered": false,
|
||||
"audioUnlocked": false
|
||||
},
|
||||
"pass": true
|
||||
}
|
||||
@ -0,0 +1,517 @@
|
||||
{"seq":0,"wallMs":1781373187514,"kind":"tap","zoneId":"newGame","x":195,"y":588,"clientX":195,"clientY":588}
|
||||
{"seq":1,"wallMs":1781373188147,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
|
||||
{"seq":2,"wallMs":1781373188339,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
|
||||
{"seq":3,"wallMs":1781373188626,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
|
||||
{"seq":4,"wallMs":1781373188770,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
|
||||
{"seq":5,"wallMs":1781373188903,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
|
||||
{"seq":6,"wallMs":1781373189037,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
|
||||
{"seq":7,"wallMs":1781373189171,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
|
||||
{"seq":8,"wallMs":1781373189305,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
|
||||
{"seq":9,"wallMs":1781373189454,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
|
||||
{"seq":10,"wallMs":1781373189587,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
|
||||
{"seq":11,"wallMs":1781373189720,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
|
||||
{"seq":12,"wallMs":1781373189853,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
|
||||
{"seq":13,"wallMs":1781373189987,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
|
||||
{"seq":14,"wallMs":1781373190120,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
|
||||
{"seq":15,"wallMs":1781373190254,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
|
||||
{"seq":16,"wallMs":1781373190404,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
|
||||
{"seq":17,"wallMs":1781373190538,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
|
||||
{"seq":18,"wallMs":1781373190670,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
|
||||
{"seq":19,"wallMs":1781373190820,"kind":"tap","zoneId":"openShop","x":195,"y":779,"clientX":195,"clientY":779}
|
||||
{"seq":20,"wallMs":1781373191019,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":21,"wallMs":1781373191152,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":22,"wallMs":1781373191288,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":23,"wallMs":1781373191603,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":24,"wallMs":1781373191736,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":25,"wallMs":1781373191869,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":26,"wallMs":1781373192185,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":27,"wallMs":1781373192318,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":28,"wallMs":1781373192452,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":29,"wallMs":1781373192768,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":30,"wallMs":1781373192902,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":31,"wallMs":1781373193036,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":32,"wallMs":1781373193352,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":33,"wallMs":1781373193485,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":34,"wallMs":1781373193618,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":35,"wallMs":1781373193935,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":36,"wallMs":1781373194069,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":37,"wallMs":1781373194201,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":38,"wallMs":1781373194518,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":39,"wallMs":1781373194652,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":40,"wallMs":1781373194785,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":41,"wallMs":1781373195101,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":42,"wallMs":1781373195235,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":43,"wallMs":1781373195369,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":44,"wallMs":1781373195685,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":45,"wallMs":1781373195818,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":46,"wallMs":1781373195952,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":47,"wallMs":1781373196268,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":48,"wallMs":1781373196402,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":49,"wallMs":1781373196535,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":50,"wallMs":1781373196851,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":51,"wallMs":1781373196985,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":52,"wallMs":1781373197128,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":53,"wallMs":1781373197435,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":54,"wallMs":1781373197568,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":55,"wallMs":1781373197701,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":56,"wallMs":1781373198018,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":57,"wallMs":1781373198151,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":58,"wallMs":1781373198284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":59,"wallMs":1781373198601,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":60,"wallMs":1781373198735,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":61,"wallMs":1781373198868,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":62,"wallMs":1781373199185,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":63,"wallMs":1781373199318,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":64,"wallMs":1781373199451,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":65,"wallMs":1781373199768,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":66,"wallMs":1781373199902,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":67,"wallMs":1781373200034,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":68,"wallMs":1781373200351,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":69,"wallMs":1781373200484,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":70,"wallMs":1781373200618,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":71,"wallMs":1781373200934,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":72,"wallMs":1781373201068,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":73,"wallMs":1781373201201,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":74,"wallMs":1781373201518,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":75,"wallMs":1781373201651,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":76,"wallMs":1781373201784,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":77,"wallMs":1781373202101,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":78,"wallMs":1781373202240,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":79,"wallMs":1781373202368,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":80,"wallMs":1781373202684,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":81,"wallMs":1781373202817,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":82,"wallMs":1781373202951,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":83,"wallMs":1781373203268,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":84,"wallMs":1781373203401,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":85,"wallMs":1781373203535,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":86,"wallMs":1781373203851,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":87,"wallMs":1781373203984,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":88,"wallMs":1781373204118,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":89,"wallMs":1781373204434,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":90,"wallMs":1781373204567,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":91,"wallMs":1781373204701,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":92,"wallMs":1781373205018,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":93,"wallMs":1781373205151,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":94,"wallMs":1781373205284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":95,"wallMs":1781373205601,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":96,"wallMs":1781373205734,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":97,"wallMs":1781373205869,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":98,"wallMs":1781373206185,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":99,"wallMs":1781373206318,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":100,"wallMs":1781373206451,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":101,"wallMs":1781373206768,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":102,"wallMs":1781373206901,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":103,"wallMs":1781373207035,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":104,"wallMs":1781373207351,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":105,"wallMs":1781373207485,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":106,"wallMs":1781373207618,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":107,"wallMs":1781373207934,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":108,"wallMs":1781373208068,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":109,"wallMs":1781373208201,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":110,"wallMs":1781373208517,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":111,"wallMs":1781373208651,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":112,"wallMs":1781373208784,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":113,"wallMs":1781373209100,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":114,"wallMs":1781373209234,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":115,"wallMs":1781373209368,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":116,"wallMs":1781373209684,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":117,"wallMs":1781373209818,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":118,"wallMs":1781373209951,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":119,"wallMs":1781373210267,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":120,"wallMs":1781373210401,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":121,"wallMs":1781373210534,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":122,"wallMs":1781373210851,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":123,"wallMs":1781373210984,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":124,"wallMs":1781373211117,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":125,"wallMs":1781373211434,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":126,"wallMs":1781373211567,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":127,"wallMs":1781373211702,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":128,"wallMs":1781373212018,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":129,"wallMs":1781373212151,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":130,"wallMs":1781373212285,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":131,"wallMs":1781373212600,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":132,"wallMs":1781373212734,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":133,"wallMs":1781373212867,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":134,"wallMs":1781373213184,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":135,"wallMs":1781373213317,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":136,"wallMs":1781373213451,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":137,"wallMs":1781373213767,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":138,"wallMs":1781373213901,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":139,"wallMs":1781373214036,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":140,"wallMs":1781373214351,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":141,"wallMs":1781373214484,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":142,"wallMs":1781373214617,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":143,"wallMs":1781373214934,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":144,"wallMs":1781373215067,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":145,"wallMs":1781373215201,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":146,"wallMs":1781373215518,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":147,"wallMs":1781373215651,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":148,"wallMs":1781373215786,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":149,"wallMs":1781373216101,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":150,"wallMs":1781373216234,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":151,"wallMs":1781373216367,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":152,"wallMs":1781373216684,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":153,"wallMs":1781373216817,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":154,"wallMs":1781373216951,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":155,"wallMs":1781373217269,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":156,"wallMs":1781373217400,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":157,"wallMs":1781373217534,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":158,"wallMs":1781373217851,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":159,"wallMs":1781373217984,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":160,"wallMs":1781373218117,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":161,"wallMs":1781373218434,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":162,"wallMs":1781373218567,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":163,"wallMs":1781373218701,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":164,"wallMs":1781373219017,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":165,"wallMs":1781373219150,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":166,"wallMs":1781373219284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":167,"wallMs":1781373219602,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":168,"wallMs":1781373219736,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":169,"wallMs":1781373219869,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":170,"wallMs":1781373220184,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":171,"wallMs":1781373220318,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":172,"wallMs":1781373220451,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":173,"wallMs":1781373220767,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":174,"wallMs":1781373220901,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":175,"wallMs":1781373221034,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":176,"wallMs":1781373221350,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":177,"wallMs":1781373221484,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":178,"wallMs":1781373221617,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":179,"wallMs":1781373221934,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":180,"wallMs":1781373222067,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":181,"wallMs":1781373222202,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":182,"wallMs":1781373222517,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":183,"wallMs":1781373222651,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":184,"wallMs":1781373222784,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":185,"wallMs":1781373223100,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":186,"wallMs":1781373223234,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":187,"wallMs":1781373223368,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":188,"wallMs":1781373223684,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":189,"wallMs":1781373223817,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":190,"wallMs":1781373223951,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":191,"wallMs":1781373224268,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":192,"wallMs":1781373224401,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":193,"wallMs":1781373224534,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":194,"wallMs":1781373224851,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":195,"wallMs":1781373224985,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":196,"wallMs":1781373225117,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":197,"wallMs":1781373225434,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":198,"wallMs":1781373225567,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":199,"wallMs":1781373225700,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":200,"wallMs":1781373226017,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":201,"wallMs":1781373226151,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":202,"wallMs":1781373226284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":203,"wallMs":1781373226601,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":204,"wallMs":1781373226735,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":205,"wallMs":1781373226867,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":206,"wallMs":1781373227183,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":207,"wallMs":1781373227317,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":208,"wallMs":1781373227451,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":209,"wallMs":1781373227769,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":210,"wallMs":1781373227901,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":211,"wallMs":1781373228034,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":212,"wallMs":1781373228350,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":213,"wallMs":1781373228484,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":214,"wallMs":1781373228620,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":215,"wallMs":1781373228955,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":216,"wallMs":1781373229084,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":217,"wallMs":1781373229217,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":218,"wallMs":1781373229536,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":219,"wallMs":1781373229667,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":220,"wallMs":1781373229801,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":221,"wallMs":1781373230117,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":222,"wallMs":1781373230251,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":223,"wallMs":1781373230387,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":224,"wallMs":1781373230701,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":225,"wallMs":1781373230840,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":226,"wallMs":1781373230970,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":227,"wallMs":1781373231285,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":228,"wallMs":1781373231419,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":229,"wallMs":1781373231550,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":230,"wallMs":1781373231867,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":231,"wallMs":1781373232000,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":232,"wallMs":1781373232134,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":233,"wallMs":1781373232451,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":234,"wallMs":1781373232584,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":235,"wallMs":1781373232717,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":236,"wallMs":1781373233034,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":237,"wallMs":1781373233168,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":238,"wallMs":1781373233300,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":239,"wallMs":1781373233617,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":240,"wallMs":1781373233755,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":241,"wallMs":1781373233884,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":242,"wallMs":1781373234200,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":243,"wallMs":1781373234334,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":244,"wallMs":1781373234467,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":245,"wallMs":1781373234784,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":246,"wallMs":1781373234919,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":247,"wallMs":1781373235052,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":248,"wallMs":1781373235362,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":249,"wallMs":1781373235485,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":250,"wallMs":1781373235619,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":251,"wallMs":1781373235934,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":252,"wallMs":1781373236068,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":253,"wallMs":1781373236200,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":254,"wallMs":1781373236517,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":255,"wallMs":1781373236650,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":256,"wallMs":1781373236783,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":257,"wallMs":1781373237100,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":258,"wallMs":1781373237234,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":259,"wallMs":1781373237367,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":260,"wallMs":1781373237685,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":261,"wallMs":1781373237817,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":262,"wallMs":1781373237952,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":263,"wallMs":1781373238267,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":264,"wallMs":1781373238400,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":265,"wallMs":1781373238534,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":266,"wallMs":1781373238850,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":267,"wallMs":1781373238984,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":268,"wallMs":1781373239117,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":269,"wallMs":1781373239435,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":270,"wallMs":1781373239568,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":271,"wallMs":1781373239700,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":272,"wallMs":1781373240017,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":273,"wallMs":1781373240150,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":274,"wallMs":1781373240283,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":275,"wallMs":1781373240601,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":276,"wallMs":1781373240733,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":277,"wallMs":1781373240867,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":278,"wallMs":1781373241184,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":279,"wallMs":1781373241317,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":280,"wallMs":1781373241450,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":281,"wallMs":1781373241766,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":282,"wallMs":1781373241900,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":283,"wallMs":1781373242033,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":284,"wallMs":1781373242350,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":285,"wallMs":1781373242484,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":286,"wallMs":1781373242619,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":287,"wallMs":1781373242933,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":288,"wallMs":1781373243066,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":289,"wallMs":1781373243201,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":290,"wallMs":1781373243517,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":291,"wallMs":1781373243650,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":292,"wallMs":1781373243784,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":293,"wallMs":1781373244100,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":294,"wallMs":1781373244234,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":295,"wallMs":1781373244367,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":296,"wallMs":1781373244683,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":297,"wallMs":1781373244816,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":298,"wallMs":1781373244950,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":299,"wallMs":1781373245267,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":300,"wallMs":1781373245400,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":301,"wallMs":1781373245533,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":302,"wallMs":1781373245850,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":303,"wallMs":1781373245984,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":304,"wallMs":1781373246117,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":305,"wallMs":1781373246433,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":306,"wallMs":1781373246566,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":307,"wallMs":1781373246701,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":308,"wallMs":1781373247016,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":309,"wallMs":1781373247150,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":310,"wallMs":1781373247284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":311,"wallMs":1781373247601,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":312,"wallMs":1781373247734,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":313,"wallMs":1781373247868,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":314,"wallMs":1781373248183,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":315,"wallMs":1781373248316,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":316,"wallMs":1781373248449,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":317,"wallMs":1781373248766,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":318,"wallMs":1781373248900,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":319,"wallMs":1781373249033,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":320,"wallMs":1781373249349,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":321,"wallMs":1781373249501,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":322,"wallMs":1781373249638,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":323,"wallMs":1781373249949,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":324,"wallMs":1781373250083,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":325,"wallMs":1781373250217,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":326,"wallMs":1781373250534,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":327,"wallMs":1781373250666,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":328,"wallMs":1781373250800,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":329,"wallMs":1781373251116,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":330,"wallMs":1781373251250,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":331,"wallMs":1781373251383,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":332,"wallMs":1781373251699,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":333,"wallMs":1781373251833,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":334,"wallMs":1781373251966,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":335,"wallMs":1781373252283,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":336,"wallMs":1781373252416,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":337,"wallMs":1781373252550,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":338,"wallMs":1781373252866,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":339,"wallMs":1781373253000,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":340,"wallMs":1781373253133,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":341,"wallMs":1781373253452,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":342,"wallMs":1781373253582,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":343,"wallMs":1781373253718,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":344,"wallMs":1781373254032,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":345,"wallMs":1781373254166,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":346,"wallMs":1781373254300,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":347,"wallMs":1781373254616,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":348,"wallMs":1781373254749,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":349,"wallMs":1781373254884,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":350,"wallMs":1781373255201,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":351,"wallMs":1781373255333,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":352,"wallMs":1781373255466,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":353,"wallMs":1781373255782,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":354,"wallMs":1781373255916,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":355,"wallMs":1781373256050,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":356,"wallMs":1781373256366,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":357,"wallMs":1781373256499,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":358,"wallMs":1781373256635,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":359,"wallMs":1781373256950,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":360,"wallMs":1781373257082,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":361,"wallMs":1781373257216,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":362,"wallMs":1781373257533,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":363,"wallMs":1781373257667,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":364,"wallMs":1781373257799,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":365,"wallMs":1781373258117,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":366,"wallMs":1781373258249,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":367,"wallMs":1781373258375,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":368,"wallMs":1781373258683,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":369,"wallMs":1781373258819,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":370,"wallMs":1781373258950,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":371,"wallMs":1781373259266,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":372,"wallMs":1781373259400,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":373,"wallMs":1781373259533,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":374,"wallMs":1781373259849,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":375,"wallMs":1781373259982,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":376,"wallMs":1781373260116,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":377,"wallMs":1781373260432,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":378,"wallMs":1781373260566,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":379,"wallMs":1781373260700,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":380,"wallMs":1781373261015,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":381,"wallMs":1781373261149,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":382,"wallMs":1781373261282,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":383,"wallMs":1781373261599,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":384,"wallMs":1781373261732,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":385,"wallMs":1781373261865,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":386,"wallMs":1781373262182,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":387,"wallMs":1781373262315,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":388,"wallMs":1781373262448,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":389,"wallMs":1781373262768,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":390,"wallMs":1781373262898,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":391,"wallMs":1781373263032,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":392,"wallMs":1781373263348,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":393,"wallMs":1781373263482,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":394,"wallMs":1781373263615,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":395,"wallMs":1781373263932,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":396,"wallMs":1781373264066,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":397,"wallMs":1781373264199,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":398,"wallMs":1781373264515,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":399,"wallMs":1781373264649,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":400,"wallMs":1781373264782,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":401,"wallMs":1781373265099,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":402,"wallMs":1781373265232,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":403,"wallMs":1781373265365,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":404,"wallMs":1781373265682,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":405,"wallMs":1781373265815,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":406,"wallMs":1781373265949,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":407,"wallMs":1781373266265,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":408,"wallMs":1781373266398,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":409,"wallMs":1781373266532,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":410,"wallMs":1781373266848,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":411,"wallMs":1781373266982,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":412,"wallMs":1781373267115,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":413,"wallMs":1781373267432,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":414,"wallMs":1781373267566,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":415,"wallMs":1781373267700,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":416,"wallMs":1781373268015,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":417,"wallMs":1781373268148,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":418,"wallMs":1781373268284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":419,"wallMs":1781373268598,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":420,"wallMs":1781373268732,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":421,"wallMs":1781373268865,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":422,"wallMs":1781373269182,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":423,"wallMs":1781373269318,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":424,"wallMs":1781373269449,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":425,"wallMs":1781373269766,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":426,"wallMs":1781373269917,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":427,"wallMs":1781373270049,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":428,"wallMs":1781373270366,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":429,"wallMs":1781373270499,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":430,"wallMs":1781373270633,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":431,"wallMs":1781373270948,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":432,"wallMs":1781373271082,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":433,"wallMs":1781373271215,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":434,"wallMs":1781373271532,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":435,"wallMs":1781373271665,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":436,"wallMs":1781373271798,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":437,"wallMs":1781373272115,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":438,"wallMs":1781373272248,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":439,"wallMs":1781373272383,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":440,"wallMs":1781373272700,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":441,"wallMs":1781373272832,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":442,"wallMs":1781373272967,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":443,"wallMs":1781373273282,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":444,"wallMs":1781373273416,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":445,"wallMs":1781373273549,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":446,"wallMs":1781373273866,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":447,"wallMs":1781373273998,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":448,"wallMs":1781373274132,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":449,"wallMs":1781373274448,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":450,"wallMs":1781373274582,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":451,"wallMs":1781373274715,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":452,"wallMs":1781373275032,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":453,"wallMs":1781373275165,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":454,"wallMs":1781373275298,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":455,"wallMs":1781373275616,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":456,"wallMs":1781373275749,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":457,"wallMs":1781373275882,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":458,"wallMs":1781373276198,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":459,"wallMs":1781373276332,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":460,"wallMs":1781373276465,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":461,"wallMs":1781373276781,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":462,"wallMs":1781373276915,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":463,"wallMs":1781373277048,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":464,"wallMs":1781373277365,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":465,"wallMs":1781373277498,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":466,"wallMs":1781373277634,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":467,"wallMs":1781373277966,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":468,"wallMs":1781373278100,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":469,"wallMs":1781373278233,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":470,"wallMs":1781373278548,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":471,"wallMs":1781373278682,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":472,"wallMs":1781373278815,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":473,"wallMs":1781373279134,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":474,"wallMs":1781373279265,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":475,"wallMs":1781373279398,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":476,"wallMs":1781373279716,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":477,"wallMs":1781373279848,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":478,"wallMs":1781373279982,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":479,"wallMs":1781373280299,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":480,"wallMs":1781373280432,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":481,"wallMs":1781373280565,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":482,"wallMs":1781373280881,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":483,"wallMs":1781373281015,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":484,"wallMs":1781373281148,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":485,"wallMs":1781373281464,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":486,"wallMs":1781373281598,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":487,"wallMs":1781373281731,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":488,"wallMs":1781373282048,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":489,"wallMs":1781373282181,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":490,"wallMs":1781373282315,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":491,"wallMs":1781373282632,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":492,"wallMs":1781373282765,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":493,"wallMs":1781373282900,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":494,"wallMs":1781373283214,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":495,"wallMs":1781373283348,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":496,"wallMs":1781373283481,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":497,"wallMs":1781373283798,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":498,"wallMs":1781373283932,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":499,"wallMs":1781373284064,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":500,"wallMs":1781373284382,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":501,"wallMs":1781373284515,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":502,"wallMs":1781373284649,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":503,"wallMs":1781373284965,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":504,"wallMs":1781373285098,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":505,"wallMs":1781373285231,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":506,"wallMs":1781373285548,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":507,"wallMs":1781373285681,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":508,"wallMs":1781373285814,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":509,"wallMs":1781373286131,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":510,"wallMs":1781373286264,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":511,"wallMs":1781373286397,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":512,"wallMs":1781373286714,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
|
||||
{"seq":513,"wallMs":1781373286866,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
|
||||
{"seq":514,"wallMs":1781373286999,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
|
||||
{"seq":515,"wallMs":1781373288600,"kind":"tap","zoneId":"confirmSettle","x":195,"y":594,"clientX":195,"clientY":594}
|
||||
{"seq":516,"wallMs":1781373288794,"kind":"tap","zoneId":"up-speed1","x":108,"y":442,"clientX":108,"clientY":442}
|
||||
@ -0,0 +1,6 @@
|
||||
{"seq":0,"anchor":"t_boot","tMono":0,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"428bfee0","hash":"aaca40e4","extra":{"by":"ref-host","stage":"pre-init","engineMode":"real"},"warning":null}
|
||||
{"seq":1,"anchor":"t_plugins_ready","tMono":0,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"aaca40e4","hash":"71ff4c61","extra":{"initialized":7,"ok":true},"warning":null}
|
||||
{"seq":2,"anchor":"t_game_init","tMono":0,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"71ff4c61","hash":"3c4297be","extra":{"assetsLoaded":13,"assetsTotal":13,"loopProofOk":true},"warning":null}
|
||||
{"seq":3,"anchor":"t_first_paint","tMono":0,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"3c4297be","hash":"9f2c7a57","extra":{"frame":1},"warning":null}
|
||||
{"seq":4,"anchor":"t_input_bound","tMono":16.667,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"9f2c7a57","hash":"321fb8ee","extra":{"stage":"registered","type":"pointerdown"},"warning":null}
|
||||
{"seq":5,"anchor":"t_game_start","tMono":16.667,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"321fb8ee","hash":"470c61d2","extra":{"mode":"play","engineMode":"real"},"warning":null}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 898 KiB |
BIN
game-runtime/games/wanglanmei-ref/evidence/p1-real/real-menu.png
Normal file
BIN
game-runtime/games/wanglanmei-ref/evidence/p1-real/real-menu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 908 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@ -5,8 +5,11 @@
|
||||
· 集成段(mini-desktop)锁参构建:见 BUILD.md(产物 dist/ref-bundle.js,iife 全局 WanglanmeiRef);
|
||||
· serve 本目录(test/harness/static-serve.cjs),浏览器打开;
|
||||
· ?seed=12345 注入主随机种子(缺省固定种子,确定可复现);
|
||||
· ?mode=evidence 取证模式:不起 RAF,由 window.__refHost.stepFrames/tap 确定性驱动;
|
||||
· 取证导出:window.__refForensics / window.__refChecklist(字段说明见 FORENSICS.md)。
|
||||
· ?engine=stub(缺省·回滚锚)/ ?engine=real(引擎掌帧·游戏美术与引擎粒子同屏,P1);
|
||||
· ?mode=evidence 取证模式:stub 下不起 RAF,由 window.__refHost.stepFrames/tap 确定性驱动;
|
||||
· 取证导出:window.__refForensics / window.__refChecklist(字段说明见 FORENSICS.md);
|
||||
· 就绪标记:__refBooted(bootRef().then 置,两通道通用);real 通道额外置 __refBootedReal(main.js setupReal 内 game.init 完成后置,
|
||||
CDP 取证 real 分支须等它再驱动——见 e2e-input.cdp.cjs;M1)。
|
||||
注意:页面对外文本走 IP gate(零受保护字样);本页标题即对外名「暖阳小卖部」。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
|
||||
729
game-runtime/games/wanglanmei-ref/src/game-shop.js
Normal file
729
game-runtime/games/wanglanmei-ref/src/game-shop.js
Normal file
@ -0,0 +1,729 @@
|
||||
/**
|
||||
* game-shop.js — 暖阳小卖部 游戏工厂(装载契约 GameHostFactory 实现 · 硬独立文件 · 零引擎 import)
|
||||
* owner:Runner v2 P1 | 消费方:main.js(host boot 调本工厂建 GameInstance)+ P3 生成 agent(复制参照靶)
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【本文件是什么·A3 硬约束】
|
||||
* 这是「游戏宿主装载契约」(game-runtime/src/core/game-host.d.ts,第 9 类契约)的 ref 参照实现:
|
||||
* · 默认导出一个 GameHostFactory(createShopGame(opts) → GameInstance);
|
||||
* · 零引擎 import(不 `import littlejsengine`;引擎能力一律经 boot.ctx.getEngine(),Q4 铁律);
|
||||
* · 必须是独立、可被 P3 生成 agent 直接复制的工厂模块(禁 main.js 内联闭包糊过——P3 需真实参照)。
|
||||
* GameInstance 生命周期对齐引擎五回调(init/update/render/onInput?/destroy,见契约):
|
||||
* · init(boot):建游戏对象(shop-core/line-picker/fx/audio)、装资产、订阅 boot.ctx.getInput();保存 boot.mainContext 为绘制面。
|
||||
* · update(dt):逻辑步(core.update / bundle.tick / applyEvents / fx.step / 联动)。
|
||||
* · render(g):绘制段(drawFrame(g,...) + palettePost.renderPost(g));g=host 注入的绘制面(real=引擎 mainContext / stub=#game ctx)。
|
||||
* · destroy():卸载卫生(P1 最小实现,须声明)。
|
||||
*
|
||||
* 【host↔游戏 接线 seam(为何 opts 携带 runtime)】
|
||||
* 装载契约 boot 只承「契约脊柱」{ctx, mainContext, canvas, seed}(跨游戏稳定面)。
|
||||
* 而本游戏复用的能力插件实例(collision/gamefeel/juice/palettePost/audioMusic/save)+ host 私有 bundle
|
||||
* (deriveContextFor / frameCount,非 PluginContext 成员)由 host 拥有并经**工厂 opts** 注入(host↔本游戏配对的实现细节)。
|
||||
* GameHostFactory 的 opts 形参正为此 additive 扩展位预留(契约 §5)。这样:契约面(boot)保持纯净,
|
||||
* host wiring(具体 runtime 句柄)走 opts,P3 生成游戏由其 host 按同形提供 runtime。
|
||||
*
|
||||
* 【红线遵守】
|
||||
* - 零裸 Date.now/Math.random/AudioContext —— 全部经 boot.ctx 受控面 / host 注入的 audio director;
|
||||
* - 不 import littlejsengine(引擎能力经 boot.ctx.getEngine());
|
||||
* - 游戏逻辑与 stub/real 通道无关(同一份工厂;绘制面差异由 host 注入的 g 吸收,DPR 补偿由 host 在 render 外层处理)。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { aabbVsAabb } from '../../../src/plugins/collision/impl.js';
|
||||
import { createShopCore } from './shop-core.js';
|
||||
import { createLinePicker, LINES } from './shopkeeper-lines.js';
|
||||
import { createPalette } from './palette.js';
|
||||
import { loadAssets, PRODUCT_IMAGE_KEY } from './assets.js';
|
||||
import { createHostFx } from './hostfx.js';
|
||||
import { createAudioDirector } from './audio-director.js';
|
||||
import { drawFrame, computeZones, shelfSlotRect, W as VIEW_W, H as VIEW_H, LAYOUT } from './render.js';
|
||||
import { proveCoreLoop, proveFailure } from './loop-proof.js';
|
||||
import { estimateDayOutcome, ECON, fmtYuan, productOf } from './balance.js';
|
||||
|
||||
/** 固定逻辑步(与 host FIXED_DT 对齐;引擎 real 通道恒定步长,stub evidence 同值)。 */
|
||||
const FIXED_DT = 1 / 60;
|
||||
|
||||
/**
|
||||
* 【装载契约 GameHostFactory】建暖阳小卖部游戏实例。
|
||||
* @param {Object} [opts] 工厂选项(host wiring + additive 扩展位)。
|
||||
* @param {Object} opts.runtime host 注入的运行时句柄(host 拥有,本游戏复用):
|
||||
* - plugins:已注册并 init 的能力插件实例 { collision, physics, gamefeel, juice, palettePost, audioMusic, save };
|
||||
* - bundle:host 受控面 bundle(deriveContextFor/frameCount 等 host 私有编排成员,非 PluginContext);
|
||||
* - getRealAudioCtx:取真实 AudioContext 的回调(audio director 用,host 管解锁时机)。
|
||||
* @returns {import('../../../src/core/game-host.d.ts').GameInstance}
|
||||
*/
|
||||
export default function createShopGame(opts) {
|
||||
const o = opts || {};
|
||||
const runtime = o.runtime || {};
|
||||
const plugins = runtime.plugins || {};
|
||||
const bundle = runtime.bundle; // host 私有 bundle(deriveContextFor/frameCount);本游戏复用 host 编排面
|
||||
const getRealAudioCtx = runtime.getRealAudioCtx || (() => null);
|
||||
|
||||
// 能力插件实例(host 已注册+init;本游戏经其公开 API 用,一职一路)。
|
||||
const collision = plugins.collision;
|
||||
const juice = plugins.juice;
|
||||
const palettePost = plugins.palettePost;
|
||||
const audioMusic = plugins.audioMusic;
|
||||
const save = plugins.save;
|
||||
|
||||
/* ── 以下闭包态在 init(boot) 内填充(boot 提供受控面/绘制面/种子/可见画布)── */
|
||||
/** @type {import('../../../src/core/game-host.d.ts').GameHostBootContext|null} */
|
||||
let boot = null;
|
||||
/** @type {import('../../../src/core/api.d.ts').PluginContext|null} */
|
||||
let ctx = null;
|
||||
let seed = 0;
|
||||
// 【mustFix #2:并轨成单一推进时钟】步进时钟读数(毫秒)。
|
||||
// 原缺陷:本游戏曾持私有 mockNowMs 并在 update 内 += dt*1000 自增,与 host 喂 bundle.clock 的钟分裂 →
|
||||
// bundle.clock 恒 0 → 输入事件 tMs=0 → InputBuffer 记 0 → 接待缓冲补打窗口判恒假(measures.bufferedServeReplays 恒 0)。
|
||||
// 修法:删私有自增,本钟改为「host 受控时钟(boot.ctx.time.nowMs(),即 host 唯一推进的 mockNowMs)的读数」。
|
||||
// - update→applyEvents→render 同步流:在 update 首行刷新本快照(host 已在调 update 前自增,值=本帧逻辑钟),
|
||||
// 供本流内一切相对时间计算(say/banner/ghosts 过期/idle 节拍)逐行读同一帧值(与原 update 首行自增后逐行一致)。
|
||||
// - onTap(输入回调,帧间触发):另读 ctx.time.nowMs() 取**当刻**值(见 onTap 内 pendingServe.tMs),保与
|
||||
// InputBuffer 同次 _emit 盖的 tMs 同钟同刻 → 补打窗口判真实生效。
|
||||
let mockNowMs = 0;
|
||||
|
||||
// 游戏对象(init 内建)。
|
||||
let core = null, pickLine = null, pal = null, fx = null, audio = null, assets = null;
|
||||
// boot 期纯逻辑证明(init 内跑,挂 forensics)。
|
||||
let loopProof = null, failureProof = null, priceProof = null;
|
||||
// 输入订阅句柄(destroy 时取消)。
|
||||
let subDown = null, subMove = null, subUp = null;
|
||||
// 是否已 init(destroy 幂等 + update/render 在 init 前空跑保护)。
|
||||
let inited = false;
|
||||
// 音频是否已解锁(host 拥有 AudioContext 解锁时机,经 _onAudioUnlock 通知本游戏;游戏据此起 BGM + 空点 tick 门控)。
|
||||
let audioUnlocked = false;
|
||||
|
||||
/* ── 宿主 UI 态(init 内初始化,避免实例间共享)── */
|
||||
const ui = {
|
||||
bubbleText: '',
|
||||
bubbleUntil: 0,
|
||||
bubbleShownAt: -9999,
|
||||
keeperWaveUntil: 0,
|
||||
tickerText: '小本生意,用心经营。',
|
||||
banner: null, // {text,color,until,dur}
|
||||
panelSlide: 1, // prep 面板滑入 0→1
|
||||
settleAnim: 0, // 日结计数 0→1
|
||||
/** @type {Array<{slot:number, avatar:number, emote:string, until:number}>} 离场幽灵(视觉余韵)。 */
|
||||
ghosts: [],
|
||||
};
|
||||
/** @type {Record<number, number>} 顾客出生时刻(走位动画)。 */
|
||||
const customerBornMs = {};
|
||||
|
||||
/* ── 度量(checklist 自动子项的数据源)── */
|
||||
const measures = {
|
||||
frames: 0,
|
||||
taps: 0,
|
||||
tapFeedbackMaxFrames: 0,
|
||||
comboUiShown: 0,
|
||||
hitStopCount: 0,
|
||||
rushPaletteSeen: false,
|
||||
bgmCrossfadeSeen: false,
|
||||
linesShown: 0,
|
||||
bufferedServeReplays: 0,
|
||||
};
|
||||
/** @type {Array<object>} 最近逻辑事件环形缓冲(取证)。 */
|
||||
const recentEvents = [];
|
||||
function pushRecent(e) {
|
||||
recentEvents.push(e);
|
||||
if (recentEvents.length > 240) recentEvents.shift();
|
||||
}
|
||||
|
||||
/* ── 台词气泡 ── */
|
||||
/**
|
||||
* 店主说一句(场景取词;普通优先级 700ms 内不顶替)。
|
||||
* @param {string} sceneKey @param {boolean} [force] 高优先级强插
|
||||
*/
|
||||
function say(sceneKey, force) {
|
||||
if (!force && mockNowMs - ui.bubbleShownAt < 700 && ui.bubbleUntil > mockNowMs) return;
|
||||
const text = pickLine(sceneKey);
|
||||
if (!text) return;
|
||||
ui.bubbleText = text;
|
||||
ui.bubbleUntil = mockNowMs + 2600;
|
||||
ui.bubbleShownAt = mockNowMs;
|
||||
ui.tickerText = text;
|
||||
measures.linesShown += 1;
|
||||
fx.springKick('bubble', 2.2);
|
||||
}
|
||||
|
||||
/** 横幅。 */
|
||||
function banner(text, color, durMs) {
|
||||
ui.banner = { text, color, dur: durMs || 1500, until: mockNowMs + (durMs || 1500) };
|
||||
}
|
||||
|
||||
function hasSave() {
|
||||
return !!save.get('profile');
|
||||
}
|
||||
|
||||
/* ── 命中区(视觉同源几何;collision broadphase + 窄相点测)── */
|
||||
/** @type {Array<any>} 当前帧命中区。 */
|
||||
let zones = [];
|
||||
function rebuildZones() {
|
||||
zones = computeZones(core, { hasSave: hasSave() });
|
||||
collision.spatial.clear();
|
||||
for (const z of zones) collision.spatial.insert(z.id, { x: z.x, y: z.y, hw: z.hw, hh: z.hh });
|
||||
}
|
||||
/** 点测:broadphase 粗筛 → aabbVsAabb 窄相 → 取 z 最高者。 */
|
||||
function hitZone(x, y) {
|
||||
const candidates = collision.spatial.queryPoint(x, y);
|
||||
let best = null;
|
||||
for (const c of candidates) {
|
||||
const z = zones.find((zz) => zz.id === c.id);
|
||||
if (!z) continue;
|
||||
if (!aabbVsAabb({ x, y, hw: 0, hh: 0 }, { x: z.x, y: z.y, hw: z.hw, hh: z.hh }).hit) continue;
|
||||
if (!best || z.z > best.z) best = z;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/* ── 接待缓冲重放(P4 输入缓冲:冷却期点击不丢,窗口内自动补打)── */
|
||||
/** @type {{productId:string, tMs:number}|null} */
|
||||
let pendingServe = null;
|
||||
function maybeReplayBufferedServe() {
|
||||
if (!pendingServe || core.state.phase !== 'open' || core.state.serveCooldown > 0) return;
|
||||
const within = mockNowMs - pendingServe.tMs <= 260;
|
||||
const gamefeel = plugins.gamefeel;
|
||||
if (within && gamefeel.inputBuffer && gamefeel.inputBuffer.consume('pointerdown', mockNowMs)) {
|
||||
const pid = pendingServe.productId;
|
||||
pendingServe = null;
|
||||
const r = core.serve(pid);
|
||||
if (r.ok) measures.bufferedServeReplays += 1;
|
||||
} else if (!within) {
|
||||
pendingServe = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 点击路由 ── */
|
||||
let sliderDrag = null; // {track}
|
||||
/** 滑条 x → 定价。 */
|
||||
function priceFromX(x, track) {
|
||||
const v = Math.max(0, Math.min(1, (x - track.x) / track.w));
|
||||
core.setPrice(ECON.priceMin + v * (ECON.priceMax - ECON.priceMin));
|
||||
}
|
||||
|
||||
function onTap(e) {
|
||||
measures.taps += 1;
|
||||
const tapFrame = bundle.frameCount();
|
||||
fx.addTapRing(e.x, e.y); // 当帧反馈(≤1 渲染帧可见)
|
||||
const z = hitZone(e.x, e.y);
|
||||
if (!z) {
|
||||
// 空点:极轻 tick(仍有听觉回执),不打扰。
|
||||
if (audioUnlocked) audio.sfx('tick');
|
||||
noteFeedback(tapFrame);
|
||||
return;
|
||||
}
|
||||
fx.springKick('z:' + z.id, -2.6); // 按压脉冲(render 侧按钮读同名弹簧 'z:<zoneId>')
|
||||
switch (z.kind) {
|
||||
case 'serve': {
|
||||
const pid = z.payload.productId;
|
||||
fx.springKick('slot-' + pid, -3.2);
|
||||
const r = core.serve(pid);
|
||||
if (!r.ok) {
|
||||
if (r.reason === 'cooldown') {
|
||||
// 【mustFix #2】tMs 取**本次输入事件自带的受控时间戳 e.tMs**(= inputBridge._emit 一次性盖、
|
||||
// 并与 gamefeel InputBuffer 同次 _emit 记录的 tMs 完全同值)→ 补打窗口判(now−tMs<=260)与
|
||||
// InputBuffer.consume 的 windowMs 判读同一钟同一刻,真实生效(取代原读私有 mockNowMs 致的双钟错位)。
|
||||
pendingServe = { productId: pid, tMs: e.tMs }; // 输入缓冲补打
|
||||
} else if (r.reason === 'no_stock') {
|
||||
audio.sfx('empty');
|
||||
const rect = shelfSlotRect(core.state.unlocked.indexOf(pid));
|
||||
fx.addFloater('缺货…', rect.x + rect.w / 2, rect.y, 'danger', 13);
|
||||
} else if (r.reason === 'no_customer') {
|
||||
audio.sfx('tick');
|
||||
const rect = shelfSlotRect(core.state.unlocked.indexOf(pid));
|
||||
fx.addFloater('没人要这个', rect.x + rect.w / 2, rect.y, 'inkSoft', 12);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'stockInc': {
|
||||
const r = core.buyStock(z.payload.productId, 1);
|
||||
if (r.ok) audio.sfx('tick');
|
||||
else {
|
||||
audio.sfx('empty');
|
||||
fx.addFloater(r.reason === 'no_cash' ? '钱不够了' : '货架满了', z.x, z.y - 22, 'danger', 13);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'stockDec': {
|
||||
const r = core.refundStock(z.payload.productId, 1);
|
||||
audio.sfx(r.ok ? 'tick' : 'empty');
|
||||
break;
|
||||
}
|
||||
case 'slider':
|
||||
sliderDrag = { track: z.payload.track };
|
||||
priceFromX(e.x, z.payload.track);
|
||||
audio.sfx('tick');
|
||||
break;
|
||||
case 'upgrade': {
|
||||
const r = core.buyUpgrade(z.payload.upgradeId);
|
||||
if (!r.ok && r.reason === 'no_cash') {
|
||||
audio.sfx('empty');
|
||||
fx.addFloater('钱不够了', z.x, z.y - 24, 'danger', 13);
|
||||
} else if (!r.ok && r.reason === 'requires') {
|
||||
audio.sfx('empty');
|
||||
fx.addFloater('要先买前置', z.x, z.y - 24, 'inkSoft', 12);
|
||||
} else if (!r.ok && r.reason === 'owned') {
|
||||
audio.sfx('tick');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'btn':
|
||||
audio.sfx('tick');
|
||||
handleAction(z.payload.action);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
noteFeedback(tapFrame);
|
||||
}
|
||||
|
||||
/** 反馈延迟度量:反馈在 tap 同帧注册,下一渲染帧可见 ⇒ 帧延迟 ≤1。 */
|
||||
function noteFeedback(tapFrame) {
|
||||
const delay = Math.max(bundle.frameCount() - tapFrame, 0) + 1; // +1=下一渲染帧可见的保守口径
|
||||
measures.tapFeedbackMaxFrames = Math.max(measures.tapFeedbackMaxFrames, delay);
|
||||
}
|
||||
|
||||
/** 按钮动作。 */
|
||||
function handleAction(action) {
|
||||
switch (action) {
|
||||
case 'newGame':
|
||||
core.startGame(null);
|
||||
break;
|
||||
case 'continue':
|
||||
core.startGame(save.get('profile'));
|
||||
break;
|
||||
case 'openShop':
|
||||
core.startDay();
|
||||
break;
|
||||
case 'confirmSettle':
|
||||
core.confirmSettle();
|
||||
break;
|
||||
case 'confirmSummary':
|
||||
core.confirmSummary();
|
||||
break;
|
||||
case 'restart':
|
||||
core.startGame(null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 逻辑事件 → 表现(粒子/音效/台词/横幅/存档)── */
|
||||
/** 槽位中心坐标。 */
|
||||
function slotPos(slot) {
|
||||
const s = LAYOUT.queue[slot] || LAYOUT.queue[0];
|
||||
return { x: s.x, y: s.y };
|
||||
}
|
||||
|
||||
function applyEvents(events) {
|
||||
for (const e of events) {
|
||||
pushRecent(e);
|
||||
switch (e.type) {
|
||||
case 'customer_arrive': {
|
||||
customerBornMs[e.id] = mockNowMs;
|
||||
audio.sfx('arrive');
|
||||
break;
|
||||
}
|
||||
case 'serve_ok': {
|
||||
const from = shelfSlotRect(core.state.unlocked.indexOf(e.productId));
|
||||
const to = slotPos(e.slot);
|
||||
delete customerBornMs[e.id];
|
||||
// 满意幽灵(顾客拿到货的余韵)。
|
||||
ui.ghosts.push({ slot: e.slot, avatar: e.avatar, emote: e.rescued ? '!' : '♪', until: mockNowMs + 460 });
|
||||
// 商品飞行:货架 → 顾客(非线性缓动+弧线),到达再收钱爆金光。
|
||||
fx.addFlight({
|
||||
fx: from.x + 28,
|
||||
fy: from.y + 28,
|
||||
tx: to.x,
|
||||
ty: to.y,
|
||||
dur: 0.28,
|
||||
imageKey: PRODUCT_IMAGE_KEY[e.productId],
|
||||
label: (productOf(e.productId) || { name: '?' }).name[0],
|
||||
onArrive: () => {
|
||||
fx.burstCoin(to.x, to.y, e.big);
|
||||
fx.addFloater(`+¥${fmtYuan(e.revenueJiao)}`, to.x, to.y - 30, 'gold', e.big ? 19 : 15);
|
||||
audio.sfx('coin', { combo: e.combo });
|
||||
fx.springKick('hud-cash', 1.8);
|
||||
if (e.rescued) {
|
||||
fx.burstSparkle(to.x, to.y - 10, 14);
|
||||
audio.sfx('rescue');
|
||||
}
|
||||
},
|
||||
});
|
||||
if (e.combo >= 2) {
|
||||
fx.springKick('combo', 2.4);
|
||||
if (e.combo === 3) measures.comboUiShown += 1;
|
||||
}
|
||||
if (e.big) {
|
||||
// 大额成交:hit-stop + 屏震 + 闪白(脆的打击感三件套)。
|
||||
juice.hitStop(0.09, 0.12);
|
||||
juice.shakeScreen(0.22, 5, 26);
|
||||
juice.flashScreen(0.12, 0.18);
|
||||
measures.hitStopCount += 1;
|
||||
audio.sfx('bigsale');
|
||||
ui.keeperWaveUntil = mockNowMs + 1300;
|
||||
say('big_sale', true);
|
||||
} else if (e.rescued) {
|
||||
say('rescue', true);
|
||||
} else if (e.combo >= 3 && e.combo % 3 === 0) {
|
||||
say('combo_hot');
|
||||
} else if (e.combo === 1 && core.state.today.served % 4 === 1) {
|
||||
say('serve_ok');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'angry_leave': {
|
||||
const p = slotPos(e.slot);
|
||||
delete customerBornMs[e.id];
|
||||
ui.ghosts.push({ slot: e.slot, avatar: e.avatar, emote: '怒', until: mockNowMs + 520 });
|
||||
fx.burstSmoke(p.x, p.y);
|
||||
fx.addFloater('差评!', p.x, p.y - 36, 'danger', 15);
|
||||
juice.shakeScreen(0.16, 3, 24);
|
||||
audio.sfx('bad');
|
||||
say('angry', true);
|
||||
break;
|
||||
}
|
||||
case 'stockout_leave': {
|
||||
const p = slotPos(e.slot);
|
||||
delete customerBornMs[e.id];
|
||||
ui.ghosts.push({ slot: e.slot, avatar: e.avatar, emote: '…', until: mockNowMs + 480 });
|
||||
fx.burstSmoke(p.x, p.y);
|
||||
fx.addFloater('白跑一趟…', p.x, p.y - 36, 'danger', 13);
|
||||
audio.sfx('empty');
|
||||
if (core.state.today.stockout % 3 === 1) say('stockout');
|
||||
break;
|
||||
}
|
||||
case 'customer_grace':
|
||||
audio.sfx('tick');
|
||||
break;
|
||||
case 'walkaway_full':
|
||||
fx.addFloater('店里挤不下…', 372, 420, 'inkSoft', 12);
|
||||
break;
|
||||
case 'rush_start':
|
||||
banner('高峰来了!', 'brick', 1600);
|
||||
say('rush_start', true);
|
||||
break;
|
||||
case 'rush_end':
|
||||
banner('缓过来了~', 'leaf', 1300);
|
||||
break;
|
||||
case 'closing':
|
||||
banner('打烊咯~', 'wood', 1500);
|
||||
break;
|
||||
case 'day_start': {
|
||||
say('day_start', true);
|
||||
ui.keeperWaveUntil = mockNowMs + 1200;
|
||||
// 进货上架弹道(P2 Projectile):开门瞬间把备货抛上架,落架压扁+闷响。
|
||||
let delay = 0;
|
||||
for (let i = 0; i < core.state.unlocked.length; i++) {
|
||||
const pid = core.state.unlocked[i];
|
||||
const n = Math.min(core.state.stock[pid] || 0, 3);
|
||||
const rect = shelfSlotRect(i);
|
||||
for (let k = 0; k < n; k++) {
|
||||
const d = delay;
|
||||
fx.addTween(Math.max(d, 0.001), () => {}, {
|
||||
onDone: () =>
|
||||
fx.addToss({
|
||||
fx: -24,
|
||||
fy: 600,
|
||||
tx: rect.x + 28,
|
||||
ty: rect.y + 28,
|
||||
dur: 0.42,
|
||||
imageKey: PRODUCT_IMAGE_KEY[pid],
|
||||
onLand: () => {
|
||||
fx.springKick('slot-' + pid, -3.4);
|
||||
audio.sfx('shelve');
|
||||
},
|
||||
}),
|
||||
});
|
||||
delay += 0.07;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'day_end': {
|
||||
ui.settleAnim = 0;
|
||||
fx.addTween(1.1, (v) => {
|
||||
ui.settleAnim = v;
|
||||
});
|
||||
say(e.settle.netJiao >= 0 ? 'settle_win' : 'settle_lose', true);
|
||||
break;
|
||||
}
|
||||
case 'fined':
|
||||
audio.sfx('bad');
|
||||
break;
|
||||
case 'upgrade_bought': {
|
||||
fx.burstSparkle(VIEW_W / 2, 430, e.first ? 22 : 14);
|
||||
fx.addFloater('升级!', VIEW_W / 2, 410, 'gold', 18);
|
||||
audio.sfx('upgrade');
|
||||
ui.keeperWaveUntil = mockNowMs + 1300;
|
||||
say('upgrade', true);
|
||||
break;
|
||||
}
|
||||
case 'price_changed':
|
||||
break;
|
||||
case 'phase_changed': {
|
||||
if (e.to === 'prep') {
|
||||
ui.panelSlide = 0;
|
||||
fx.addTween(0.45, (v) => {
|
||||
ui.panelSlide = v;
|
||||
});
|
||||
if (e.from === 'title' || e.from === 'gameover') say('open_shop', true);
|
||||
}
|
||||
ui.ghosts.length = 0;
|
||||
break;
|
||||
}
|
||||
case 'gameover':
|
||||
save.remove('profile'); // 失败清档(重开新局)
|
||||
audio.sfx('bad');
|
||||
say('gameover', true);
|
||||
break;
|
||||
case 'save_point':
|
||||
save.set('profile', core.snapshotForSave());
|
||||
break;
|
||||
case 'segment_end':
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 闲时台词节拍器 ── */
|
||||
let idleChatTimer = 7;
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* 装载契约 GameInstance 实现
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/**
|
||||
* init:建游戏对象 + 装资产 + 订阅受控输入(boot.ctx.getInput)。异步(loadAssets 异步)。
|
||||
* @param {import('../../../src/core/game-host.d.ts').GameHostBootContext} b boot 上下文
|
||||
*/
|
||||
async function init(b) {
|
||||
boot = b;
|
||||
ctx = b.ctx;
|
||||
seed = b.seed;
|
||||
|
||||
/* ── 游戏对象(逻辑随机=派生流;台词随机=另一派生流——互不推演)── */
|
||||
core = createShopCore({ random: bundle.deriveContextFor('shop-core', 'shop-core').random });
|
||||
pickLine = createLinePicker(bundle.deriveContextFor('shop-lines', 'shop-lines').random);
|
||||
pal = createPalette();
|
||||
fx = createHostFx({ bundle, juice });
|
||||
audio = createAudioDirector({ getAudio: () => getRealAudioCtx(), p6: audioMusic });
|
||||
|
||||
/* ── 资产装载(容错:缺失走程序化回退,照常启动)── */
|
||||
assets = await loadAssets('./assets/');
|
||||
|
||||
/* ── boot 期纯逻辑证明(闭环/失败态/定价两头输——挂 forensics 供 e2e 读)── */
|
||||
loopProof = proveCoreLoop();
|
||||
failureProof = proveFailure();
|
||||
priceProof = (() => {
|
||||
const lo = estimateDayOutcome({ priceMul: ECON.priceMin });
|
||||
const mid = estimateDayOutcome({ priceMul: 1.0 });
|
||||
const hi = estimateDayOutcome({ priceMul: ECON.priceMax });
|
||||
return { loProfit: lo.profitJiao, midProfit: mid.profitJiao, hiProfit: hi.profitJiao, twoSidedLoss: lo.profitJiao < 0 && hi.profitJiao < 0 && mid.profitJiao > 0 };
|
||||
})();
|
||||
|
||||
/* ── 游戏侧输入订阅:经受控输入面(与插件同一面,boot.ctx.getInput)── */
|
||||
const input = ctx.getInput();
|
||||
subDown = input.on('pointerdown', (e) => onTap(e));
|
||||
subMove = input.on('pointermove', (e) => {
|
||||
if (sliderDrag) priceFromX(e.x, sliderDrag.track);
|
||||
});
|
||||
subUp = input.on('pointerup', () => {
|
||||
sliderDrag = null;
|
||||
});
|
||||
|
||||
inited = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* update:逻辑步(引擎 gameUpdate 每逻辑步驱动;dt 恒定)。
|
||||
* @param {number} dt 逻辑步秒数
|
||||
*/
|
||||
function update(dt) {
|
||||
if (!inited) return;
|
||||
// 【mustFix #2】刷新本帧逻辑钟快照=host 唯一推进时钟(host 已在调本 update 前自增 mockNowMs,故此读即本帧值)。
|
||||
// 取代原私有 `mockNowMs += dt*1000` 自增(双钟分裂根因);本流(update/applyEvents/render)逐行读此快照,
|
||||
// 与原「update 首行自增后逐行读」语义逐行等价(确定性测状态终点不变)。
|
||||
mockNowMs = ctx.time.nowMs();
|
||||
const ts = juice.getTimeScale();
|
||||
const dtWorld = dt * ts; // hit-stop 冻结世界(手感锚点)
|
||||
core.update(dtWorld);
|
||||
bundle.tick(dt); // 驱动插件 onFrame(粒子 autoStep 等,按 PLUGIN.md 约定吃真实 dt;引擎粒子自渲与此解耦)
|
||||
applyEvents(core.drainEvents());
|
||||
maybeReplayBufferedServe();
|
||||
fx.step(dt, dtWorld);
|
||||
|
||||
// 高峰 → 色板/暗角/BGM 三联动。
|
||||
const rush = core.state.phase === 'open' ? core.state.rushFactor : 0;
|
||||
pal.setRush(rush);
|
||||
if (rush > 0.9) measures.rushPaletteSeen = true;
|
||||
palettePost.setPost({ vignette: { enabled: true, strength: 0.22 + rush * 0.24 } });
|
||||
audio.step(dt, rush, core.state.phase === 'open');
|
||||
const ap = audio.probe();
|
||||
if (ap.bgm.ready && ap.bgm.idleLevel > 0.02 && ap.bgm.rushLevel > 0.02) measures.bgmCrossfadeSeen = true;
|
||||
|
||||
// 闲时随口台词(营业中、不忙的时候偶尔来一句)。
|
||||
if (core.state.phase === 'open') {
|
||||
idleChatTimer -= dt;
|
||||
if (idleChatTimer <= 0) {
|
||||
idleChatTimer = 8 + (rush > 0.5 ? 4 : 0);
|
||||
if (ui.bubbleUntil < mockNowMs) say(rush > 0.5 ? 'rush_start' : 'idle_chat');
|
||||
}
|
||||
}
|
||||
// 幽灵过期清理。
|
||||
for (let i = ui.ghosts.length - 1; i >= 0; i--) if (ui.ghosts[i].until <= mockNowMs) ui.ghosts.splice(i, 1);
|
||||
|
||||
rebuildZones();
|
||||
measures.frames += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* render:绘制段(引擎 gameRender 清屏后驱动;g=host 注入的绘制面)。
|
||||
* 注:游戏美术与引擎粒子同屏靠「g=引擎 mainContext」(real,host 注入);stub 下 g=#game ctx。
|
||||
* DPR 变换补偿由 host 在调 render 前于外层处理(real 引擎 mainContext 无 DPR 变换,host save+setTransform;
|
||||
* stub #game ctx 自带 DPR 变换,host 不补)——本工厂只按逻辑像素绘制,不感知通道差异。
|
||||
* @param {CanvasRenderingContext2D} g 绘制面(=boot.mainContext 的同一面 / stub=#game ctx)
|
||||
*/
|
||||
function render(g) {
|
||||
if (!inited) return;
|
||||
drawFrame(g, {
|
||||
core,
|
||||
pal,
|
||||
assets,
|
||||
fx,
|
||||
ui,
|
||||
nowMs: mockNowMs,
|
||||
customerBornMs,
|
||||
hasSave: hasSave(),
|
||||
});
|
||||
palettePost.renderPost(g); // 暗角收口(绘制末尾叠加)
|
||||
}
|
||||
|
||||
/**
|
||||
* destroy:卸载卫生(P1 最小实现)。取消输入订阅(幂等);游戏对象交由 host dispose 插件/bundle 统一回收。
|
||||
* P2 装载多游戏复位时此处补「重置 UI 态/清 fx 层」;P1 ref 单游戏不复用实例,最小即可(须声明,非空 no-op)。
|
||||
*/
|
||||
function destroy() {
|
||||
try { if (subDown && subDown.cancel) subDown.cancel(); } catch (_) { /* 幂等 */ }
|
||||
try { if (subMove && subMove.cancel) subMove.cancel(); } catch (_) { /* 幂等 */ }
|
||||
try { if (subUp && subUp.cancel) subUp.cancel(); } catch (_) { /* 幂等 */ }
|
||||
subDown = subMove = subUp = null;
|
||||
inited = false;
|
||||
}
|
||||
|
||||
/* ── 取证面(host 经此挂 window.__refForensics/__refChecklist;非装载契约成员,host wiring)── */
|
||||
/**
|
||||
* 取证导出(host 拿去挂 window.__refForensics)。
|
||||
* @returns {object}
|
||||
*/
|
||||
function forensicsView() {
|
||||
return {
|
||||
seed,
|
||||
/** 可交互几何(当前帧;与视觉同源)。 */
|
||||
geometry: () => zones.map((z) => ({ id: z.id, kind: z.kind, x: z.x, y: z.y, hw: z.hw, hh: z.hh, z: z.z })),
|
||||
loopProof,
|
||||
failureProof,
|
||||
priceProof,
|
||||
/** 游戏状态快照(JSON 安全)。 */
|
||||
state: () => JSON.parse(JSON.stringify({
|
||||
phase: core.state.phase,
|
||||
day: core.state.day,
|
||||
cash: core.state.cash,
|
||||
reputation: core.reputation(),
|
||||
priceMul: core.state.priceMul,
|
||||
stock: core.state.stock,
|
||||
unlocked: core.state.unlocked,
|
||||
upgrades: core.state.upgrades,
|
||||
customers: core.state.customers.length,
|
||||
combo: core.state.comboCount,
|
||||
today: core.state.today,
|
||||
lifetime: core.state.lifetime,
|
||||
gameoverReason: core.state.gameoverReason,
|
||||
})),
|
||||
/** 最近逻辑事件(环形 240 条)。 */
|
||||
recentEvents: () => recentEvents.slice(),
|
||||
/** 实时度量。 */
|
||||
measures: () => Object.assign({ particle: fx.particleProbe(), audio: audio.probe(), assetsMissing: assets.missing }, measures),
|
||||
/** 资产装载进度(host 取证用)。 */
|
||||
assetsLoaded: () => ({ loaded: assets.loaded, total: assets.total }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* checklist 刷新(host 逐帧调,刷新 window.__refChecklist 的数据源)。
|
||||
* @param {object} checklist host 持有的 checklist 对象(本函数原地填)
|
||||
*/
|
||||
function refreshChecklist(checklist) {
|
||||
if (!inited) return;
|
||||
const ap = audio.probe();
|
||||
checklist.shougan = {
|
||||
tapFeedbackMaxFrames: measures.tapFeedbackMaxFrames, // 目标 ≤3
|
||||
tapFeedbackWithin3: measures.taps === 0 || measures.tapFeedbackMaxFrames <= 3,
|
||||
everyTapScalePulse: true, // 构造保证:onTap 必加涟漪+弹簧(见 onTap)
|
||||
comboUiShown: measures.comboUiShown > 0 || core.state.today.maxCombo >= 2,
|
||||
flightEasingUsed: true, // serve_ok 飞行走 easing.quadOut+弧线(构造保证)
|
||||
hitStopTriggered: measures.hitStopCount > 0,
|
||||
bufferedServeReplays: measures.bufferedServeReplays,
|
||||
};
|
||||
checklist.meishu = {
|
||||
allDrawsViaPalette: true, // 构造保证:render.js 一切取色经 pal.c()
|
||||
vignetteEnabled: palettePost.getPost().vignette.enabled,
|
||||
rushPaletteSeen: measures.rushPaletteSeen,
|
||||
assetsLoaded: `${assets.loaded}/${assets.total}`,
|
||||
noPrimaryPlaceholderRects: true, // 构造保证:回退块也走调色(amber/sky 族)
|
||||
};
|
||||
checklist.yinyue = {
|
||||
audioUnlocked,
|
||||
bgmReady: ap.bgm.ready,
|
||||
bgmCrossfadeSeen: measures.bgmCrossfadeSeen,
|
||||
sfxKindCount: ap.sfxKinds.length, // 目标 ≥5
|
||||
sfxKinds: ap.sfxKinds,
|
||||
};
|
||||
checklist.shendu = {
|
||||
loopReachable: loopProof.ok,
|
||||
loopOps: loopProof.opCount,
|
||||
failureReachable: failureProof.ok,
|
||||
failureReason: failureProof.reason,
|
||||
priceTwoSidedLoss: priceProof.twoSidedLoss,
|
||||
upgradeTreeSize: 8,
|
||||
firstUpgradeDay: loopProof.day,
|
||||
};
|
||||
checklist.juese = {
|
||||
keeperSpriteResident: true, // 构造保证:drawKeeper 每帧绘制
|
||||
lineGroups: Object.keys(LINES).length,
|
||||
lineTotal: Object.values(LINES).reduce((s, a) => s + a.length, 0),
|
||||
linesShownLive: measures.linesShown,
|
||||
};
|
||||
}
|
||||
|
||||
/* ── GameInstance + host wiring 扩展面(forensicsView/refreshChecklist/handleAction 给 host 取证与 __refHost.do 用)── */
|
||||
return {
|
||||
// ── 装载契约 GameInstance 五件 ──
|
||||
init,
|
||||
update,
|
||||
render,
|
||||
destroy,
|
||||
// ── host wiring 扩展(非契约成员;host 经此挂取证 + __refHost 驱动钩子)──
|
||||
/** @internal host 取证:返回 forensics 视图。 */
|
||||
_forensicsView: forensicsView,
|
||||
/** @internal host 取证:原地刷新 checklist 对象。 */
|
||||
_refreshChecklist: refreshChecklist,
|
||||
/** @internal host wiring:__refHost.do(action) 透传到游戏逻辑公开 API。 */
|
||||
_handleAction: handleAction,
|
||||
/** @internal host wiring:取当前逻辑帧钟读数(real 取证旁证)。 */
|
||||
_nowMs: () => mockNowMs,
|
||||
/**
|
||||
* @internal host wiring:首个用户手势解锁音频后,host 调本钩子通知本游戏(host 已建/恢复 AudioContext)。
|
||||
* 本游戏据此置 audioUnlocked + 起 BGM(BGM 行为是游戏域,AudioContext 解锁时机是 host 域,一职一路)。幂等。
|
||||
*/
|
||||
_onAudioUnlock: () => {
|
||||
if (audioUnlocked) return;
|
||||
audioUnlocked = true;
|
||||
if (audio && typeof audio.startBgm === 'function') audio.startBgm();
|
||||
},
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,12 +3,19 @@
|
||||
*
|
||||
* 【所有权】主会话维护(HJ-T1B-A-EXEC-001 §4:评估执行主体非开发 agent 所有)。
|
||||
* 【运行面】仅 mini-desktop(连本地 headless chrome via CDP,样板=integration-run.cjs 同形);
|
||||
* 前置:static-serve 起在 4320、chrome 起在 9222(--headless=new --no-sandbox --no-zygote --remote-allow-origins=*)。
|
||||
* 【四件套】输入轨迹 JSONL / 截图+canvas 哈希 / probe JSONL / checklist JSON → evidence/round-1/。
|
||||
* 【判读口径】硬门=boot/probe 链/真输入闭环(开局→首日结算→次日升级)/接待数>0/菜单帧哈希双开一致;
|
||||
* live 软项(rushPalette/bgmCrossfade/sfxKinds/hitStop)=如实记录不硬挡(亲玩轮校准类,见 FORENSICS §2)。
|
||||
* 前置:static-serve 起在 4320、chrome 起在 9222(--headless=new --no-sandbox --no-zygote --remote-allow-origins=* --autoplay-policy=no-user-gesture-required)。
|
||||
* 【四件套】输入轨迹 JSONL / 截图+canvas 哈希 / probe JSONL / checklist JSON → evidence/round-N/。
|
||||
*
|
||||
* 【双通道·P1】
|
||||
* - ENGINE=stub(缺省):原轮 1 取证主体逐行不变(mode=evidence + __refHost.stepFrames 手摇定量推帧 + 逐像素双开哈希)。
|
||||
* - ENGINE=real:引擎掌帧通道(M1/M2/M3 范式切换·非增量)。判读口径:
|
||||
* 门①掌帧(window.__engine.snapshot() 帧增量∈[20,45]/500ms) / 门②同屏像素(#game-engine 上游戏暖色 warmHits>N ∧ 引擎粒子 brightHits>0∧maxCh>80) /
|
||||
* 门③真接线(__engineCalls 含 particles.spawnEmitter call-ID) / 门④CDP 真玩闭环(真触摸+轮询·禁 stepFrames 手摇,否则双掌帧违铁律) / 门⑦console 零未捕获。
|
||||
* real 等 __refBootedReal 再驱动;坐标 rect 切 #game-engine。
|
||||
*
|
||||
* 用法(cwd=game-runtime):node games/wanglanmei-ref/test/e2e-input.cdp.cjs [baseUrl] [outDir]
|
||||
* 缺省 baseUrl=http://127.0.0.1:4320 outDir=games/wanglanmei-ref/evidence/round-1
|
||||
* 通道:环境变量 ENGINE=real|stub(缺省 stub)。real 时 baseUrl 后自动拼 &engine=real。
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
@ -22,7 +29,19 @@ const BASE = process.argv[2] || 'http://127.0.0.1:4320';
|
||||
const OUT_DIR = path.resolve(process.argv[3] || 'games/wanglanmei-ref/evidence/round-1');
|
||||
const CDP_HTTP = process.env.CDP_HTTP || H.DEFAULT_CDP_HTTP || 'http://127.0.0.1:9222';
|
||||
const SEED = 305441741; // BUILD.md 取证缺省种子,与轮 1 全部证据统一
|
||||
const URL_EVIDENCE = `${BASE}/?mode=evidence&seed=${SEED}`;
|
||||
// 【P1】通道选择:ENGINE=real → 引擎掌帧通道;缺省 stub → 原逐像素确定性取证主体(逐行不变)。
|
||||
const ENGINE = (process.env.ENGINE || 'stub').toLowerCase() === 'real' ? 'real' : 'stub';
|
||||
const IS_REAL = ENGINE === 'real';
|
||||
// stub:原 URL(mode=evidence + 手摇)。real:去掉 mode=evidence(真触摸+轮询,不靠 stepFrames),拼 &engine=real。
|
||||
const URL_EVIDENCE = IS_REAL
|
||||
? `${BASE}/?engine=real&seed=${SEED}`
|
||||
: `${BASE}/?mode=evidence&seed=${SEED}`;
|
||||
// real 通道引擎自建可见画布选择器(host.js:830 同口径);stub 用 #game。坐标 rect 与像素扫描均切此画布(M3)。
|
||||
const CANVAS_SEL = IS_REAL ? '#game-engine' : '#game';
|
||||
// 门① 引擎掌帧帧增量区间(@60fps ~30;排单步假推进+失控空转,复用 gate0 G0-2/a6 A6-1 口径)。
|
||||
const FRAME_WINDOW_MS = 500;
|
||||
const FRAME_DELTA_MIN = 20;
|
||||
const FRAME_DELTA_MAX = 45;
|
||||
|
||||
/* ───────────────────────── 基础设施(自带连接段) ───────────────────────── */
|
||||
|
||||
@ -68,15 +87,19 @@ async function connectRef(url) {
|
||||
return cdp;
|
||||
}
|
||||
|
||||
/** 等参考件引导完成(__refBooted/__refHost;boot 含 13 图装载+2 首 mp3 解码,给 25s 裕量)。 */
|
||||
/** 等参考件引导完成(boot 含 13 图装载+2 首 mp3 解码,给 25s 裕量)。
|
||||
* 【M1】real 通道等 __refBootedReal(只在 engineInit().then 内 game.init 完成后置,仿 host-dev __hostBootedReal);
|
||||
* stub 等 __refBooted(bootRef().then 置)。real 据 __refBooted 驱动会打空/读 undefined → 假失败或间歇绿,故分流。 */
|
||||
async function waitRefBoot(session) {
|
||||
const deadline = Date.now() + 25000;
|
||||
await delay(900);
|
||||
for (;;) {
|
||||
const st = await session.evaluate('({ b: !!window.__refBooted, e: window.__refBootError || null, h: !!window.__refHost })');
|
||||
const st = IS_REAL
|
||||
? await session.evaluate('({ b: !!window.__refBootedReal, e: window.__refBootError || null, h: !!window.__refHost })')
|
||||
: await session.evaluate('({ b: !!window.__refBooted, e: window.__refBootError || null, h: !!window.__refHost })');
|
||||
if (st && st.e) throw new Error('参考件引导失败:' + st.e);
|
||||
if (st && st.b && st.h) return;
|
||||
if (Date.now() > deadline) throw new Error('等 __refBooted 超时');
|
||||
if (Date.now() > deadline) throw new Error(`等 ${IS_REAL ? '__refBootedReal' : '__refBooted'} 超时`);
|
||||
await delay(300);
|
||||
}
|
||||
}
|
||||
@ -123,13 +146,16 @@ async function listZones(phaseTag) {
|
||||
首跑实翻教训:页面壳曾把 canvas 下推 ≈29px,zone 坐标直发全部偏移 → 一律经 rect 映射换算。 */
|
||||
let MAP = { l: 0, t: 0, sx: 1, sy: 1 };
|
||||
async function initMapping() {
|
||||
// 【M3】坐标 rect 选择器按通道切:real=#game-engine(引擎自建可见画布,host.js:830;real 下 #game 不是可见画布,取错画布/null→坐标全错);stub=#game。
|
||||
const m = await cdp.evaluate(`(function(){
|
||||
var c = document.getElementById('game'); var r = c.getBoundingClientRect();
|
||||
var c = document.querySelector(${JSON.stringify(CANVAS_SEL)}); if(!c) return { err: 'canvas not found: ' + ${JSON.stringify(CANVAS_SEL)} };
|
||||
var r = c.getBoundingClientRect();
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
return { l: r.left, t: r.top, w: r.width, h: r.height, vw: c.width / dpr, vh: c.height / dpr };
|
||||
})()`);
|
||||
if (m && m.err) throw new Error('坐标映射失败:' + m.err);
|
||||
MAP = { l: m.l, t: m.t, sx: m.w / m.vw, sy: m.h / m.vh };
|
||||
evidence.meta.coordMap = { rect: m, map: MAP };
|
||||
evidence.meta.coordMap = { rect: m, map: MAP, canvas: CANVAS_SEL };
|
||||
console.log(' · 坐标映射:', JSON.stringify(evidence.meta.coordMap));
|
||||
}
|
||||
const toClient = (x, y) => ({ x: MAP.l + x * MAP.sx, y: MAP.t + y * MAP.sy });
|
||||
@ -180,9 +206,209 @@ async function shot(name, withHash) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ═══════════════════ real 通道专用驱动(M2 范式切换:真触摸+轮询,禁 stepFrames 手摇) ═══════════════════ */
|
||||
|
||||
/** real 真触摸点按(touchStart→touchEnd 成对;**不 stepFrames**——引擎内部 RAF 已掌帧,手摇=双掌帧违铁律,M2)。
|
||||
* 触后 delay 等引擎自然推帧落地;调用方再轮询 __refForensics 判相位。 */
|
||||
async function realTapAt(x, y, zoneId, settleMs) {
|
||||
const c = toClient(x, y);
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: c.x, y: c.y }] });
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
||||
trajectory.push({ seq: trajectory.length, wallMs: Date.now(), kind: 'tap', zoneId: zoneId || null, x, y, clientX: c.x, clientY: c.y });
|
||||
await delay(typeof settleMs === 'number' ? settleMs : 120); // 等真事件经浏览器→输入桥→引擎下一拍消化(不手摇推帧)
|
||||
}
|
||||
/** real 按 zone id 真点(找不到=返回 false 不抛)。 */
|
||||
async function realTapZone(id, settleMs) {
|
||||
const zs = await listZones(null);
|
||||
const z = zs.find((q) => q.id === id);
|
||||
if (!z) return false;
|
||||
await realTapAt(z.x, z.y, id, settleMs);
|
||||
return true;
|
||||
}
|
||||
/** 轮询 __refForensics.state() 直到 pred(state)===true 或超时(real 判相位推进的唯一手段,替代手摇定量推帧)。 */
|
||||
async function pollState(pred, timeoutMs, stepMs) {
|
||||
const deadline = Date.now() + (timeoutMs || 8000);
|
||||
for (;;) {
|
||||
const s = await F('FZ.state()');
|
||||
if (pred(s)) return s;
|
||||
if (Date.now() > deadline) return s; // 超时返最后一次(调用方据此 check)
|
||||
await delay(stepMs || 200);
|
||||
}
|
||||
}
|
||||
/** 抓 CANVAS_SEL 像素并算「游戏暖色 warmHits + 引擎粒子亮色 brightHits/maxCh」(门②同屏判据;抽样网格步进 4*97 同 a6)。
|
||||
* 暖色判据(开工前从 palette.js BASE 坐实,§门②(a)):warmHit ⟺ r>140 ∧ r-b>85 ∧ g<r(命中 wood/brick/gold/amber/danger,排 paper/paperHi/sky/cream)。 */
|
||||
async function scanSameScreen(selector) {
|
||||
const img = await H.captureImageData(cdp, selector);
|
||||
const px = img && img.data;
|
||||
const nonEmpty = !!(img && img.width > 0 && img.height > 0 && px && px.length > 0);
|
||||
let warmHits = 0, brightHits = 0, maxCh = 0;
|
||||
if (nonEmpty) {
|
||||
for (let i = 0; i + 3 < px.length; i += 4 * 97) {
|
||||
const r = px[i], g = px[i + 1], b = px[i + 2];
|
||||
const m = Math.max(r, g, b);
|
||||
if (m > maxCh) maxCh = m;
|
||||
if (m > 80) brightHits++; // 引擎粒子(burst 白/任意亮色)——非全黑底
|
||||
if (r > 140 && (r - b) > 85 && g < r) warmHits++; // 游戏美术店面暖木/砖/金色域(排纸底/天光/近白,§门②(a))
|
||||
}
|
||||
}
|
||||
return { nonEmpty, width: img && img.width, height: img && img.height, warmHits, brightHits, maxCh };
|
||||
}
|
||||
|
||||
/**
|
||||
* real 通道主流程(门①掌帧/②同屏像素/③call-ID/④真玩闭环+四件套/⑦console)。
|
||||
* 不跑 stub 的逐像素双开哈希(门⑤交 stub 通道独占,harness §5-坑6)。
|
||||
*/
|
||||
async function mainReal() {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
console.log('=== 参考件 e2e(④ 评估·engine=real)开始 ===', URL_EVIDENCE);
|
||||
evidence.meta.engineMode = 'real';
|
||||
|
||||
cdp = await connectRef(URL_EVIDENCE); // 内部 waitRefBoot 已按 IS_REAL 等 __refBootedReal
|
||||
try {
|
||||
await initMapping(); // rect 切 #game-engine(M3)
|
||||
|
||||
/* 门⑦ console + probe 链:real 引导后零未捕获 + 六锚链成立 */
|
||||
const probe = await F('FZ.probe()');
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'probe.jsonl'), (probe.jsonl || '') + '\n');
|
||||
check(probe.verify && probe.verify.ok === true, `probe 链 verify.ok(实际:${JSON.stringify(probe.verify)})`);
|
||||
check(Array.isArray(probe.uncaught) && probe.uncaught.length === 0, `门⑦ console 零未捕获(实际:${JSON.stringify(probe.uncaught)})`);
|
||||
evidence.boot = { anchors: probe.anchors, initOk: probe.initOk, registeredPlugins: probe.registeredPlugins };
|
||||
|
||||
/* 门① 掌帧:window.__engine.snapshot() 两采样间隔 500ms,frame 增量∈[20,45](排单步假推进+失控空转) */
|
||||
const eng0 = await cdp.evaluate('(window.__engine ? window.__engine.snapshot() : null)');
|
||||
await delay(FRAME_WINDOW_MS);
|
||||
const eng1 = await cdp.evaluate('(window.__engine ? window.__engine.snapshot() : null)');
|
||||
const dFrame = (eng0 && eng1) ? (eng1.frame - eng0.frame) : -1;
|
||||
check(eng0 && eng1 && dFrame >= FRAME_DELTA_MIN && dFrame <= FRAME_DELTA_MAX,
|
||||
`门① 引擎掌帧 frame 增量∈[${FRAME_DELTA_MIN},${FRAME_DELTA_MAX}](实际 dFrame=${dFrame})`);
|
||||
evidence.gates.engineFrameDrive = { dFrame, range: [FRAME_DELTA_MIN, FRAME_DELTA_MAX], s0: eng0, s1: eng1 };
|
||||
|
||||
/* 门③ 真接线:经游戏内真粒子(开店→营业→serve_ok 飞行 burstCoin)触发后,__engineCalls 含 particles.spawnEmitter call-ID。
|
||||
先开局营业造 serve_ok(真粒子来源),再读 __engineCalls。先清空记录隔离引导期残留。 */
|
||||
await cdp.evaluate('(function(){ try{ window.__engineCalls=[]; }catch(e){} return 1; })()');
|
||||
|
||||
/* 门④ 真玩闭环:开店→进货→营业→结算→升级(真触摸+轮询,禁 stepFrames) */
|
||||
// 菜单态截图(real:引擎已在渲,直接截 #game-engine)
|
||||
await waitAssets(cdp);
|
||||
await shot('real-menu', false);
|
||||
await snapState('real-menu');
|
||||
|
||||
check(await realTapZone('newGame'), '门④ 真点 newGame');
|
||||
let st = await pollState((s) => s.phase === 'prep', 4000);
|
||||
check(st.phase === 'prep', `门④ phase=prep(实际:${st.phase})`);
|
||||
await listZones('real-prep-day1');
|
||||
await shot('real-prep-day1', false);
|
||||
|
||||
// 备货:对全部 inc-* 各点 6 次(买空/钱尽=无代价 no-op)
|
||||
const incZones = (await listZones(null)).filter((z) => /^inc-/.test(z.id));
|
||||
for (const z of incZones) for (let k = 0; k < 6; k++) await realTapAt(z.x, z.y, z.id, 70);
|
||||
st = await snapState('real-after-stock-day1');
|
||||
|
||||
check(await realTapZone('openShop'), '门④ 真点 openShop');
|
||||
st = await pollState((s) => s.phase === 'open', 4000);
|
||||
check(st.phase === 'open', `门④ 营业态 phase=open(实际:${st.phase})`);
|
||||
|
||||
// 营业接待循环:每拍真点全部 serve-*,delay 等引擎自然推帧,轮询直到离开 open(自适应,禁 stepFrames)
|
||||
let guard = 0, serveOkSeen = 0;
|
||||
for (;;) {
|
||||
guard++;
|
||||
if (guard > 400) { check(false, '门④ 营业循环护栏触发(400 轮未离开 open)'); break; }
|
||||
const zs = await listZones(null);
|
||||
for (const z of zs.filter((q) => /^serve-/.test(q.id))) await realTapAt(z.x, z.y, z.id, 60);
|
||||
await delay(180); // 等引擎推帧 + 收银冷却滚动
|
||||
if (!serveOkSeen) serveOkSeen = await F('FZ.recentEvents().filter(function(e){return e.type==="serve_ok"}).length');
|
||||
const ph = (await F('FZ.state()')).phase;
|
||||
if (ph !== 'open') break;
|
||||
}
|
||||
st = await snapState('real-after-serving-day1');
|
||||
check(st.today && st.today.served > 0, `门④ 首日接待数>0(实际:${st.today && st.today.served})`);
|
||||
check(serveOkSeen > 0, `门④ 循环内追踪到 serve_ok 事件(${serveOkSeen} 条)`);
|
||||
|
||||
/* 门③ 读 call-ID(serve_ok 飞行 onArrive 触 burstCoin→particles-juice.spawnEmitter→getEngine().particles.spawnEmitter) */
|
||||
const calls = await cdp.evaluate('JSON.stringify(window.__engineCalls||[])');
|
||||
let callList = []; try { callList = JSON.parse(calls) || []; } catch (_) { callList = []; }
|
||||
const hasSpawn = callList.some((c) => typeof c === 'string' && c.indexOf('particles.spawnEmitter') === 0);
|
||||
check(hasSpawn, `门③ __engineCalls 含 particles.spawnEmitter call-ID(真接线;实际样本:${JSON.stringify(callList.slice(0, 8))})`);
|
||||
evidence.gates.engineWiringCallId = { hasSpawn, sample: callList.slice(0, 20) };
|
||||
|
||||
/* 门② 同屏像素:截 #game-engine,游戏暖色 warmHits>200 ∧ 引擎粒子 brightHits>0∧maxCh>80(两类同命中⟺同一张画布合并) */
|
||||
// 触一发真粒子(空点/serve 后引擎下一拍渲),再扫(burst 寿命短,扫前再点一发保粒子在屏)
|
||||
const serveZs = (await listZones(null)).filter((z) => /^serve-/.test(z.id));
|
||||
if (serveZs.length) { await realTapAt(serveZs[0].x, serveZs[0].y, serveZs[0].id, 40); }
|
||||
// 【shouldFix】扫像素前先等引擎 frame 真增量 >= SCAN_FRAME_DELTA_MIN(读 window.__engine.snapshot().frame 确认已推进)。
|
||||
// 起因:headless chrome 的 RAF 推帧偶发偏慢,固定 delay(80) 不保证 burst 粒子已渲到当前帧 → 粒子相位错开、
|
||||
// brightHits 偶发 0 假阴。改为「轮询引擎帧增量达标」再扫,把扫描钉在引擎确已推进若干帧之后(自适应,非死等)。
|
||||
const SCAN_FRAME_DELTA_MIN = 2; // burst 粒子寿命短,等 ≥2 帧足够其渲到屏;不宜过大(粒子可能已 fade)
|
||||
{
|
||||
const f0 = await cdp.evaluate('(window.__engine ? window.__engine.snapshot().frame : -1)');
|
||||
const fDeadline = Date.now() + 2000;
|
||||
for (;;) {
|
||||
const f1 = await cdp.evaluate('(window.__engine ? window.__engine.snapshot().frame : -1)');
|
||||
if (f0 >= 0 && f1 >= 0 && f1 - f0 >= SCAN_FRAME_DELTA_MIN) break;
|
||||
if (Date.now() > fDeadline) break; // 超时如实续扫(下方 check 会据实际 hit 数判读)
|
||||
await delay(40);
|
||||
}
|
||||
}
|
||||
const scan = await scanSameScreen(CANVAS_SEL);
|
||||
await shot('real-sameScreen', false);
|
||||
// 【shouldFix】warmHits>200 阈值:real 首跑请打印 scan.warmHits 实际值校准 N
|
||||
// (200 是开工前的保守估计,首次真机取证后据「游戏暖色域在 #game-engine 实测命中数」回填精确阈值,
|
||||
// 避免误报;色域不等式 r>140∧r-b>85∧g<r 已经 palette.js 坐实,不动)。
|
||||
const warmOk = scan.warmHits > 200;
|
||||
const partOk = scan.brightHits > 0 && scan.maxCh > 80;
|
||||
check(scan.nonEmpty && warmOk && partOk,
|
||||
`门② 同屏(#game-engine 游戏暖色 warmHits>200 且 引擎粒子 brightHits>0∧maxCh>80;实际 warmHits=${scan.warmHits} brightHits=${scan.brightHits} maxCh=${scan.maxCh})`);
|
||||
evidence.gates.sameScreenPixels = scan;
|
||||
|
||||
/* 门 E 续:日结→确认→升级(real 真玩闭环复证) */
|
||||
if (st.phase === 'settle' || st.phase === 'summary') {
|
||||
await shot('real-settle-day1', false);
|
||||
if (!(await realTapZone('confirmSettle'))) await cdp.evaluate(`window.__refHost.do('confirmSettle')`);
|
||||
let st2 = await pollState((s) => s.phase === 'summary' || s.phase === 'prep', 4000);
|
||||
if (st2.phase === 'summary') {
|
||||
if (!(await realTapZone('confirmSummary'))) await cdp.evaluate(`window.__refHost.do('confirmSummary')`);
|
||||
st2 = await pollState((s) => s.phase === 'prep', 4000);
|
||||
}
|
||||
await listZones('real-day2-prep');
|
||||
const upOk = await realTapZone('up-speed1');
|
||||
st2 = await pollState((s) => s.upgrades && s.upgrades.speed1 === true, 3000);
|
||||
check(upOk && st2.upgrades && st2.upgrades.speed1 === true, `门④ 真点 up-speed1 后 upgrades.speed1=true(实际:${st2.upgrades && st2.upgrades.speed1})`);
|
||||
await shot('real-day2-upgraded', false);
|
||||
} else {
|
||||
check(false, `门④ 首日到达结算(实际:${st.phase})`);
|
||||
}
|
||||
|
||||
/* 终采:checklist + measures 全量落盘 */
|
||||
const checklist = await cdp.evaluate('window.__refChecklist', 30000);
|
||||
const measures = await F('FZ.measures()');
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'checklist.json'), JSON.stringify({ checklist, measures }, null, 2));
|
||||
evidence.checklist = checklist; evidence.measures = measures;
|
||||
evidence.softLive = {
|
||||
rushPaletteSeen: !!measures.rushPaletteSeen, bgmCrossfadeSeen: !!measures.bgmCrossfadeSeen,
|
||||
sfxKindCount: (checklist.yinyue && checklist.yinyue.sfxKindCount) || 0,
|
||||
hitStopTriggered: (measures.hitStopCount || 0) > 0, audioUnlocked: !!(checklist.yinyue && checklist.yinyue.audioUnlocked),
|
||||
};
|
||||
console.log(' · live 软项:', JSON.stringify(evidence.softLive));
|
||||
} finally {
|
||||
try { cdp.close(); await closeTarget(cdp.targetId); } catch { /* 收尾容错 */ }
|
||||
}
|
||||
|
||||
/* 落盘四件套汇总 */
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'input-trajectory.jsonl'), trajectory.map((t) => JSON.stringify(t)).join('\n') + '\n');
|
||||
evidence.gates.closedLoopRealInput = !evidence.failures.some((f) => /门④/.test(f));
|
||||
evidence.gates.engineTakeover = !evidence.failures.some((f) => /门①|门②|门③/.test(f));
|
||||
evidence.pass = evidence.failures.length === 0;
|
||||
evidence.meta.endedAt = new Date().toISOString();
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'e2e-evidence.json'), JSON.stringify(evidence, null, 2));
|
||||
console.log(`=== REF_E2E(real): ${evidence.pass ? 'PASS' : 'FAIL'} ===(failures=${evidence.failures.length})`);
|
||||
evidence.failures.forEach((f) => console.log(' FAIL · ' + f));
|
||||
process.exit(evidence.pass ? 0 : 1);
|
||||
}
|
||||
|
||||
/* ───────────────────────── 主流程 ───────────────────────── */
|
||||
|
||||
(async function main() {
|
||||
if (IS_REAL) { await mainReal(); return; } // 【P1】real 通道走范式切换主体(真触摸+轮询);stub 通道下方逐行不变
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
console.log('=== 参考件 e2e(④ 评估)开始 ===', URL_EVIDENCE);
|
||||
|
||||
|
||||
@ -165,3 +165,34 @@ test('无浏览器确定性:同 seed 同驱动脚本 → 状态终点一致',
|
||||
const b = await run();
|
||||
assert.equal(a, b, '同 seed 同脚本应逐字段一致(受控时间/随机生效)');
|
||||
});
|
||||
|
||||
test('P4 输入缓冲补打恢复:冷却期点击在窗口内自动补打(mustFix #2 回归守门)', async () => {
|
||||
// 【守门意图】本测专钉「双 mockNowMs 致 bundle.clock 冻死=0 → 补打窗口判恒假」缺陷的回归:
|
||||
// 修前 host 喂 bundle.clock 的钟恒 0 → 输入事件 tMs=0 → InputBuffer 记 0 → game-shop maybeReplayBufferedServe
|
||||
// 用推进的游戏钟减 0 判窗口恒假 → measures.bufferedServeReplays 恒 0。
|
||||
// 修后 host 唯一推进时钟既喂 bundle.clock 又(经 ctx.time)供游戏逻辑读 → 同钟同刻 → 窗口判真实生效。
|
||||
// 驱动构造(确定性,seed=1 实测 replays=1):serve 一发起冷却 → 推进到冷却尾段(剩余<260ms 进窗)补一发同
|
||||
// product(冷却中→buffer)→ 静候冷却清(不点别的)→ 该帧 maybeReplayBufferedServe 用同钟补打成功。
|
||||
const host = await bootHeadless('?mode=evidence&seed=1');
|
||||
const F = globalThis.window.__refForensics;
|
||||
const z = (id) => F.geometry().find((zz) => zz.id === id);
|
||||
const tap = (id) => { const g = z(id); if (g) host.tap(g.x, g.y); return !!g; };
|
||||
|
||||
tap('newGame'); host.stepFrames(2);
|
||||
for (const pid of ['soda', 'snack', 'bread']) for (let i = 0; i < 9; i++) { if (tap('inc-' + pid)) host.stepFrames(1); }
|
||||
tap('openShop'); host.stepFrames(2);
|
||||
assert.equal(F.state().phase, 'open', '应入营业');
|
||||
|
||||
let guard = 0;
|
||||
while (F.state().phase === 'open' && guard++ < 300) {
|
||||
for (const pid of F.state().unlocked) {
|
||||
const g = z('serve-' + pid);
|
||||
if (!g) continue;
|
||||
host.tap(g.x, g.y); host.stepFrames(20); // 第一发 serve→冷却;推进到冷却尾段
|
||||
host.tap(g.x, g.y); host.stepFrames(18); // 第二发冷却中→buffer;静候冷却清→补打
|
||||
}
|
||||
host.stepFrames(4);
|
||||
}
|
||||
const replays = F.measures().bufferedServeReplays;
|
||||
assert.ok(replays > 0, `接待缓冲补打应触发(bufferedServeReplays=${replays};恒 0 即双钟分裂回归)`);
|
||||
});
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# ref-e2e-real-runbook.sh — 参考件 ?engine=real 引擎掌帧 e2e 编排(仅 mini-desktop;主会话经 ssh bash -s 投递)
|
||||
# 【P1】沿 ref-e2e-runbook.sh 的 git-clone-scratch 口径(A4,非 tar-pipe);real 版三处差异:
|
||||
# ① 依赖安装改 `npm i`(装 deps+devDeps=引擎 littlejsengine + esbuild)再 `npm i ws`——
|
||||
# **禁 `npm init -y`**(会覆盖含引擎依赖的 package.json 丢 littlejsengine,real 构建即失败;harness §5-坑2);
|
||||
# ② 构建后 `grep -q ParticleEmitter dist/ref-bundle.js` 核「引擎真入包」(§8/A4);
|
||||
# ③ driver 经 ENGINE=real 环境变量走范式切换主体(真触摸+轮询,禁 stepFrames;URL 自动拼 &engine=real)。
|
||||
# 形状沿 ref-e2e-runbook.sh:不跑 -e 全局(探活/清理容错),关键门各自判退出码;长驻进程一律 systemd-run。
|
||||
set -u
|
||||
SHA="${1:?需要精确 commit SHA}"
|
||||
SCRATCH="$HOME/t1bb-ref-real"
|
||||
RT="$SCRATCH/game-runtime"
|
||||
EV="$RT/games/wanglanmei-ref/evidence/real"
|
||||
PORT=4320
|
||||
CDP_PORT=9222
|
||||
U_SERVE=t1bb-ref-real-serve
|
||||
U_CHROME=t1bb-ref-real-chrome
|
||||
|
||||
echo "=== [0] 环境与净场 ==="
|
||||
hostname; node --version
|
||||
systemctl stop "$U_SERVE" "$U_CHROME" 2>/dev/null; systemctl reset-failed "$U_SERVE" "$U_CHROME" 2>/dev/null
|
||||
rm -rf "$SCRATCH"
|
||||
|
||||
echo "=== [1] scratch 检出精确 SHA ==="
|
||||
git clone -q --depth 50 --branch dev/2.0.0 ssh://git@101.200.34.71:2222/zizi-al/games-development-ai.git "$SCRATCH" || { echo "GATE: CLONE FAIL"; exit 1; }
|
||||
git -C "$SCRATCH" checkout -q "$SHA" || { echo "GATE: SHA FAIL"; exit 1; }
|
||||
echo "checkout=$(git -C "$SCRATCH" rev-parse --short HEAD)"
|
||||
|
||||
echo "=== [2] 依赖+构建(esbuild 锁参;引擎真入包) ==="
|
||||
cd "$RT" || exit 1
|
||||
# 【关键·harness §5-坑2】直接 npm i 装 deps+devDeps(含 littlejsengine + esbuild),禁 npm init -y(会丢引擎依赖)。
|
||||
npm i --no-audit --no-fund >/dev/null 2>&1 || { echo "GATE: NPM INSTALL FAIL"; exit 1; }
|
||||
npm i --no-audit --no-fund ws >/dev/null 2>&1 || { echo "GATE: NPM WS FAIL"; exit 1; }
|
||||
node scripts/build.mjs games/wanglanmei-ref/entry.js games/wanglanmei-ref/dist/ref-bundle.js --global-name=WanglanmeiRef || { echo "GATE: BUILD FAIL"; exit 1; }
|
||||
ls -l games/wanglanmei-ref/dist/ref-bundle.js
|
||||
# 核「引擎真入包」(§8/A4):grep ParticleEmitter;并打印 raw/gz 量级备查。
|
||||
grep -q ParticleEmitter games/wanglanmei-ref/dist/ref-bundle.js || { echo "GATE: ENGINE NOT IN BUNDLE(ParticleEmitter 缺)"; exit 1; }
|
||||
node -e "const z=require('zlib'),fs=require('fs');const b=fs.readFileSync('games/wanglanmei-ref/dist/ref-bundle.js');console.log('ref-bundle raw='+b.length+' gz='+z.gzipSync(b,{level:9}).length);"
|
||||
echo "GATE: BUILD OK(引擎真入包)"
|
||||
|
||||
echo "=== [3] IP gate(产物+对外文本,码位构造防自触发) ==="
|
||||
node -e "
|
||||
const fs=require('fs');const b=String.fromCharCode(0x738b,0x84dd,0x8393);let bad=false;
|
||||
for(const f of ['games/wanglanmei-ref/dist/ref-bundle.js','games/wanglanmei-ref/index.html','games/wanglanmei-ref/assets/manifest.json']){
|
||||
const hit=fs.readFileSync(f,'utf8').includes(b);console.log(f, hit?'VIOLATION':'clean'); if(hit) bad=true;
|
||||
}
|
||||
process.exit(bad?1:0)" || { echo "GATE: IPGATE FAIL"; exit 1; }
|
||||
echo "GATE: IPGATE OK"
|
||||
|
||||
echo "=== [4] 起 serve+chrome(systemd-run durable 单元) ==="
|
||||
mkdir -p "$EV"
|
||||
systemd-run --unit="$U_SERVE" --setenv=NODE_NO_WARNINGS=1 \
|
||||
/usr/bin/env node "$RT/test/harness/static-serve.cjs" "$RT/games/wanglanmei-ref" "$PORT" >/dev/null 2>&1
|
||||
CHROME="$(command -v google-chrome google-chrome-stable chromium chromium-browser 2>/dev/null | head -1)"
|
||||
[ -n "$CHROME" ] || { echo "GATE: NO CHROME"; exit 1; }
|
||||
systemd-run --unit="$U_CHROME" "$CHROME" --headless=new --no-sandbox --no-zygote \
|
||||
--remote-debugging-port=$CDP_PORT --remote-allow-origins='*' --autoplay-policy=no-user-gesture-required \
|
||||
about:blank >/dev/null 2>&1
|
||||
echo "units: $U_SERVE / $U_CHROME ($CHROME)"
|
||||
|
||||
echo "=== [5] 探活门(就绪等待→serve 5×6s 持续在线 + CDP 就绪) ==="
|
||||
READY=0
|
||||
for i in $(seq 1 10); do curl -sf "http://127.0.0.1:$PORT/" >/dev/null && { READY=1; break; }; sleep 0.5; done
|
||||
[ "$READY" = 1 ] || { echo "GATE: SERVE NEVER READY(5s)"; journalctl -u "$U_SERVE" --no-pager | tail -10; systemctl stop "$U_SERVE" "$U_CHROME" 2>/dev/null; exit 1; }
|
||||
ALIVE=1
|
||||
for i in 1 2 3 4 5; do
|
||||
curl -sf "http://127.0.0.1:$PORT/" >/dev/null || { ALIVE=0; echo "probe $i: serve DOWN"; break; }
|
||||
echo "probe $i: serve OK"; [ "$i" -lt 5 ] && sleep 6
|
||||
done
|
||||
[ "$ALIVE" = 1 ] || { echo "GATE: SERVE FAIL"; systemctl stop "$U_SERVE" "$U_CHROME" 2>/dev/null; exit 1; }
|
||||
CDPOK=0
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do curl -sf "http://127.0.0.1:$CDP_PORT/json/version" >/dev/null && { CDPOK=1; break; }; sleep 2; done
|
||||
[ "$CDPOK" = 1 ] || { echo "GATE: CDP FAIL"; systemctl stop "$U_SERVE" "$U_CHROME" 2>/dev/null; exit 1; }
|
||||
echo "GATE: SERVE+CDP OK"
|
||||
|
||||
echo "=== [6] e2e driver(engine=real;真触摸+轮询·门①②③④⑦) ==="
|
||||
ENGINE=real node games/wanglanmei-ref/test/e2e-input.cdp.cjs "http://127.0.0.1:$PORT" "$EV" 2>&1 | tee "$EV/driver-console.log"
|
||||
RC=${PIPESTATUS[0]}
|
||||
echo "driver rc=$RC"
|
||||
|
||||
echo "=== [7] 轮终收尾(停 unit+日志归档,不留孤儿) ==="
|
||||
journalctl -u "$U_SERVE" -u "$U_CHROME" --no-pager 2>/dev/null | tail -120 > "$EV/serve-units.log"
|
||||
systemctl stop "$U_SERVE" "$U_CHROME" 2>/dev/null
|
||||
systemctl reset-failed "$U_SERVE" "$U_CHROME" 2>/dev/null
|
||||
ls -la "$EV"
|
||||
echo "=== RUNBOOK(real) DONE rc=$RC ==="
|
||||
exit "$RC"
|
||||
166
game-runtime/host-dev/engine-caps.js
Normal file
166
game-runtime/host-dev/engine-caps.js
Normal file
@ -0,0 +1,166 @@
|
||||
/**
|
||||
* host-dev/engine-caps.js — 引擎能力背书工厂(EngineCapabilities 三面:math/particles/audio.synth)
|
||||
* owner:Runner v2 P1(提升自 host.js:197-318 的 makeEngineCaps 闭包)| 消费方:host-dev/host.js + games/wanglanmei-ref/src/main.js(real 通道)
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【为什么单列本模块(P1 提升·搬运非新造)】
|
||||
* host-dev 与 wanglanmei-ref real 通道都要把「插件能力→引擎」薄包装层接进受控面 EngineCapabilities。
|
||||
* 原实现是 host.js bootHostDev 内的闭包 makeEngineCaps(host.js:197-318,已包装 particles/audio.synth/math 三面)。
|
||||
* P1 把它提升为「吃 {LJS, engineMath, engineMode, recHook} 的纯工厂函数」,host-dev / ref 共用同一份——
|
||||
* 这是「模块化已有件」(搬运,语义零变),非从零造。引擎 import 仍只活在调用方(host.js / entry.js / ref entry.js),
|
||||
* 本模块不 import 引擎(引擎名字空间经入参 LJS 注入),故 node 可 import 它(规避 F7:顶层 import 引擎在 node 抛 window)。
|
||||
*
|
||||
* 【逐行保语义(回归基线=门0 8/8 + real 门 6/6,绝不可破)】
|
||||
* - 首行 engineMode!=='real' → return null:保 null-engine 回归短路(stub/node 不背书引擎,host.js:198 原语义)。
|
||||
* - recHook 默认 no-op:原闭包内 `window.__engineCalls.push` 由调用方经 recHook 注入(取证路径才注,渠道产物不注 → 探针无副作用)。
|
||||
* - mathRaw = engineMath(调用方传入 buildEngineMath(LJS) 的结果,host.js:204 同口径)。
|
||||
* - particles/audio.synth 三面包装体逐字搬自 host.js:214-316,引擎引用经入参 LJS.*。
|
||||
*
|
||||
* 【引擎↔插件 边界(创始人 2026-06-13 拍)】引擎有 ParticleEmitter/zzfxG/zzfxM → 此处薄包装;插件侧删自研 sim/vendored 合成核,一职一路。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 【P1 提升】构造引擎能力背书工厂(EngineCapabilities 三面:math 含 easing 门面 / particles / audio.synth)。
|
||||
* - 仅 real 通道返真能力面;stub/node 通道短路返 null(保 null-engine 回归战场 + 规避 F7 引擎 import 在 node 抛错)。
|
||||
* real/stub 都经此工厂(buildBundle/createHostDevContext 注入),若无条件背书引擎,stub 通道也会拿到引擎能力 → 破 null 回归;
|
||||
* 故首行按 engineMode 短路:stub→返 null(等价「未注入」,触发 plugin.js getEngineShared 的 null 分支:告警一次 + getEngine() 返 null)。
|
||||
* - 引擎 import 唯一活点=调用方(host.js / entry.js LJS@顶部 import);插件零直接 import,一律经 ctx.getEngine().*(Q4 铁律)。
|
||||
* - math/easing=纯数学族特例(政策 §1.3):门面逐字节/ULP 等价引擎 LJS.lerp/smoothStep/Ease;gamefeel 侧保内置实现作 null-engine fallback(非有意替代)。
|
||||
* easing 门面**只暴 11 条等价曲线**——backInOut/elasticInOut 不入面(引擎 IN_OUT 拼接是另一条曲线,差 6.6%/17%;gamefeel 对其保内置=补层)。
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {any} opts.LJS 引擎名字空间(含 ParticleEmitter/zzfxG/zzfxM/screenToWorld/vec2/rgb/cameraScale 等)。
|
||||
* @param {import('../src/core/api.d.ts').EngineMath} opts.engineMath 已构造的引擎数学门面(buildEngineMath(LJS) 的结果,调用方传入)。
|
||||
* @param {string} opts.engineMode 通道模式('real' 才背书引擎;其余短路返 null)。
|
||||
* @param {((id:string)=>void)} [opts.recHook] 真接线门②取证钩子:每个引擎能力叶方法被调时记一条 call-ID。
|
||||
* **默认 no-op**(不传或传 undefined → 探针无副作用、不挂 window.__engineCalls,渠道产物物理保证)。
|
||||
* @returns {import('../src/core/api.d.ts').EngineCapabilities|null} 引擎能力三面;非 real 返 null。
|
||||
*/
|
||||
export function createEngineCaps(opts) {
|
||||
const LJS = opts.LJS;
|
||||
const engineMath = opts.engineMath;
|
||||
const engineMode = opts.engineMode;
|
||||
// recHook 默认 no-op:取证路径由调用方注入(id => window.__engineCalls.push(id));渠道产物不注 → 探针零副作用。
|
||||
const rec = opts.recHook || (() => { /* 默认 no-op:探针绝不入渠道产物 */ });
|
||||
|
||||
if (engineMode !== 'real') return null; // stub/node:不背书引擎(保 null-engine 回归;F7:node 本就 import 不了引擎)
|
||||
|
||||
// math 门面(buildEngineMath 真活包装引擎,调用方传入)外再裹一层 rec:lerp/smoothStep 直裹,easing 子面逐曲线裹(保全部键与口径不变)。
|
||||
const mathRaw = engineMath;
|
||||
const math = {
|
||||
lerp: (a, b, p) => { rec('math.lerp'); return mathRaw.lerp(a, b, p); },
|
||||
smoothStep: (p) => { rec('math.smoothStep'); return mathRaw.smoothStep(p); },
|
||||
easing: Object.fromEntries(Object.keys(mathRaw.easing).map((k) => [k, (t) => { rec('math.easing.' + k); return mathRaw.easing[k](t); }])),
|
||||
};
|
||||
return {
|
||||
// ── math(含 easing 门面;外层 rec 记调用,内层 mathRaw 真包装引擎 lerp/smoothStep/Ease)──
|
||||
math,
|
||||
// ── particles ──薄包装引擎 ParticleEmitter:EngineEmitterSpec(像素口径)→ 引擎世界口径,返封装句柄。
|
||||
particles: {
|
||||
/**
|
||||
* 受控面 spawnEmitter:EngineEmitterSpec(像素口径)→ 引擎 ParticleEmitter(世界口径),返封装句柄(禁泄漏引擎对象)。
|
||||
* 边界模型(创始人 2026-06-13 拍):引擎有 ParticleEmitter → 此处薄包装;插件侧删自研 sim 积分循环,一职一路。
|
||||
* 引擎自渲:new ParticleEmitter 经 EngineObject 构造自入 engineObjects(esm.js:3580),引擎主循环每帧自调
|
||||
* update(发射+积分,esm.js:8348)+ render(drawTile 落 mainContext,esm.js:8654/8658)→ host 无需手动 add/render。
|
||||
* @param {import('../src/core/api.d.ts').EngineEmitterSpec} spec 归一化发射参数(像素/中性形态)
|
||||
* @returns {import('../src/core/api.d.ts').EngineEmitterHandle} 封装句柄(仅 isActive/stop,不见引擎对象)
|
||||
*/
|
||||
spawnEmitter(spec) {
|
||||
rec('particles.spawnEmitter'); // 真接线门②:记一次「插件经 ctx.getEngine().particles 真调引擎粒子构造」
|
||||
// 1) 像素↔世界阻抗换算(cameraScale 默认 32,esm.js:2785;host 未 setCameraScale 故用引擎 live binding 默认值,I3/OQ-2)。
|
||||
const worldPos = LJS.screenToWorld(LJS.vec2(spec.pos.x, spec.pos.y)); // 像素→世界(esm.js:5067/screenToWorld)
|
||||
const SCALE = LJS.cameraScale; // 世界单位/像素(用 live binding,随引擎相机变)
|
||||
// 2) burst/continuous → emitTime/emitRate(I1:引擎无「spawn 恰好 N」原语,burst 用「1 逻辑帧窗 × count×60 速率」近似 ≈N 颗)。
|
||||
const emitTime = spec.emitTime != null ? spec.emitTime
|
||||
: (spec.count != null ? 1 / 60 : 0); // burst=1帧窗(满即自停) / continuous=0(forever)
|
||||
const emitRate = spec.emitRate != null ? spec.emitRate
|
||||
: (spec.count != null ? spec.count * 60 : 100); // burst: count×60 颗/秒(该窗内恰发 ≈count 颗)
|
||||
// 3) speed:像素/秒 → 世界/帧@60(引擎 speed 单位来源 esm.js:8253,无再乘 dt)。
|
||||
const engSpeed = (spec.speed != null ? spec.speed : 0) / SCALE / 60;
|
||||
// 4) damping:每帧速度乘子(spec 已由插件把 drag/秒折算为每帧乘子,这里直透;缺省 1=无阻)。
|
||||
const engDamping = spec.damping != null ? spec.damping : 1;
|
||||
// 5) 颜色:归一化 {r,g,b,a} → 引擎 Color(esm.js:1547 rgb)。缺省不透明白→透明,保 real 像素门「有色像素命中>0」(I2)。
|
||||
const cs = spec.colorStart || { r: 1, g: 1, b: 1, a: 1 };
|
||||
const ce = spec.colorEnd || { r: 1, g: 1, b: 1, a: 0 };
|
||||
const colorStart = LJS.rgb(cs.r, cs.g, cs.b, cs.a);
|
||||
const colorEnd = LJS.rgb(ce.r, ce.g, ce.b, ce.a);
|
||||
// 6) 构造引擎发射器(27 参,按 d.ts:3232 构造签名逐位映射)。
|
||||
const em = new LJS.ParticleEmitter(
|
||||
worldPos, // pos(世界)
|
||||
spec.angle != null ? spec.angle : 0, // angle
|
||||
(spec.emitSize != null ? spec.emitSize : 0) / SCALE, // emitSize 像素→世界
|
||||
emitTime, // emitTime(0=forever)
|
||||
emitRate, // emitRate
|
||||
spec.coneAngle != null ? spec.coneAngle : Math.PI, // emitConeAngle(半锥角)
|
||||
undefined, // tileInfo: untextured(2D fillRect 路)
|
||||
colorStart, colorStart, colorEnd, colorEnd, // colorStartA/B + colorEndA/B(A=B → 固定色不随机)
|
||||
spec.particleTime != null ? spec.particleTime : 0.5, // particleTime
|
||||
(spec.sizeStart != null ? spec.sizeStart : 6) / SCALE, // sizeStart 像素→世界
|
||||
(spec.sizeEnd != null ? spec.sizeEnd : 0) / SCALE, // sizeEnd 像素→世界
|
||||
engSpeed, // speed(世界/帧@60)
|
||||
0, // angleSpeed: 0(无玩法语义,不旋转)
|
||||
engDamping, // damping
|
||||
1, // angleDamping: 1(无阻)
|
||||
spec.gravityScale != null ? spec.gravityScale : 0, // gravityScale(host 未 setGravity,全局 gravity=0 故暂不生效,OQ-4 follow-up)
|
||||
Math.PI, // particleConeAngle(粒子自转锥,留默认)
|
||||
0.1, // fadeRate(默认)
|
||||
0, // randomness: 0(spec 已显式给参,关引擎默认 .2 额外抖动求确定)
|
||||
false, // collideTiles: false
|
||||
spec.additive != null ? spec.additive : false, // additive
|
||||
true // randomColorLinear(A=B 时无影响)
|
||||
);
|
||||
// 7) 封装句柄(禁泄漏引擎对象;isActive/stop 转发,stop 幂等销毁)。
|
||||
return {
|
||||
isActive() { return em.isActive(); }, // esm.js:8482
|
||||
stop() { if (!em.destroyed) em.destroy(true); }, // 幂等(已 destroyed 直返,不二次销毁)
|
||||
};
|
||||
},
|
||||
},
|
||||
// ── audio.synth ──薄包装引擎 zzfxG/zzfxM:参数包/曲谱 → PCM 样本(不播放)。
|
||||
// 边界模型(创始人 2026-06-13 拍):引擎有 zzfxG/zzfxM → 此处薄包装;插件侧删 vendored 合成核,一职一路。
|
||||
// 引擎 zzfxG **不烤主音量 ×0.3**(样本=s*volume,esm.js:7391;×0.3 在引擎 gain 层 soundVolume=.3,esm.js:3121/6777)
|
||||
// → 返回的样本是「未乘 0.3 的 PCM」,与 api.d.ts EngineAudioSynth 口径一致;×0.3 由插件播放层补(audio-music/impl.js playChannels)。
|
||||
// 合成异常/失败返 null(插件侧 → 发声 no-op,不回退 vendored)。
|
||||
audio: {
|
||||
synth: {
|
||||
/**
|
||||
* ZzFX 单音效合成核:参数包 → PCM 单声道样本数组(未乘 0.3,不播放)。薄包装引擎 LJS.zzfxG。
|
||||
* @param {number[]} params ZzFX 参数包
|
||||
* @returns {number[]|null} PCM 单声道样本;合成异常返 null
|
||||
*/
|
||||
synthSfx: (params) => {
|
||||
rec('audio.synth.synthSfx'); // 真接线门②:记一次「插件经 ctx.getEngine().audio.synth 真调引擎 zzfxG」
|
||||
try {
|
||||
// 引擎 zzfxG 收 positional 参数(与 vendored 同签名,esm.js:7326)→ 展开参数包。
|
||||
return LJS.zzfxG(...params);
|
||||
} catch (e) {
|
||||
// 合成异常降级为 null(不连坐游戏,记 stderr);插件侧据 null 走发声 no-op。
|
||||
console.error('[host][engine-audio] zzfxG 合成异常,返 null:' + (e && e.message ? e.message : e));
|
||||
return null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* ZzFXM 曲谱合成核:乐器/模式/序列/BPM → [L,R] 双声道样本(未乘 0.3,不播放)。薄包装引擎 LJS.zzfxM。
|
||||
* @param {number[][]} instruments 乐器表
|
||||
* @param {number[][][]} patterns 模式表
|
||||
* @param {number[]} sequence 序列
|
||||
* @param {number} [bpm] 节拍(缺省引擎默 125)
|
||||
* @returns {number[][]|null} [L,R] 双声道样本;合成异常返 null
|
||||
*/
|
||||
synthSong: (instruments, patterns, sequence, bpm) => {
|
||||
rec('audio.synth.synthSong'); // 真接线门②:记一次「插件经 ctx.getEngine().audio.synth 真调引擎 zzfxM」
|
||||
try {
|
||||
// 引擎 zzfxM 签名与 vendored 逐字一致(esm.js:10837 `zzfxM(instruments, patterns, sequence, BPM=125)`)。
|
||||
return LJS.zzfxM(instruments, patterns, sequence, bpm);
|
||||
} catch (e) {
|
||||
console.error('[host][engine-audio] zzfxM 合成异常,返 null:' + (e && e.message ? e.message : e));
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -42,9 +42,12 @@ import {
|
||||
// setCanvasClearColor / BLACK / vec2 / mainCanvas / mainContext / frame / time / timeReal /
|
||||
// paused / timeScale 等全经 LJS.* 触达。
|
||||
import * as LJS from 'littlejsengine';
|
||||
// 【A1】引擎数学门面构造(纯函数,零引擎 import):real 通道工厂 makeEngineCaps 用它包装引擎 lerp/smoothStep/Ease。
|
||||
// 【A1】引擎数学门面构造(纯函数,零引擎 import):real 通道工厂用它包装引擎 lerp/smoothStep/Ease。
|
||||
// 单列在 engine-math.js(不 import 引擎)→ node 单测可 import 它注入 mock ljs 验门面,规避 F7(host.js 顶层 import 引擎在 node 抛 window)。
|
||||
import { buildEngineMath } from './engine-math.js';
|
||||
// 【P1 提升】引擎能力背书工厂(EngineCapabilities 三面:math/particles/audio.synth)已从本文件闭包提升为可 import 模块,
|
||||
// host-dev / wanglanmei-ref real 通道共用同一份(搬运·语义零变)。本模块不 import 引擎(经入参 LJS 注入)。
|
||||
import { createEngineCaps } from './engine-caps.js';
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────────────
|
||||
* 取证固定环境常量(与 test/harness/browser-evidence.cjs 的 FIXED_ENV 对齐)
|
||||
@ -182,140 +185,10 @@ export function bootHostDev(opts) {
|
||||
return realAudioCtx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 【A1】引擎能力背书工厂:构造 EngineCapabilities 三面(math 含 easing 门面 / particles / audio.synth)。
|
||||
* - 仅 real 通道返真能力面;stub/node 通道短路返 null(保 null-engine 回归战场 + 规避 F7 引擎 import 在 node 抛错)。
|
||||
* real/stub 都经 buildBundle(setupAfterEngine 调),若无条件背书引擎,stub 通道也会拿到引擎能力 → 破 null 回归;
|
||||
* 故首行按 engineMode 短路:stub→返 null(等价「未注入」,触发 plugin.js getEngineShared 的 null 分支:告警一次 + getEngine() 返 null)。
|
||||
* - 引擎 import 唯一活点=本 host(LJS@顶部 import);插件零直接 import,一律经 ctx.getEngine().*(Q4 铁律)。
|
||||
* - math/easing=纯数学族特例(政策 §1.3):门面逐字节/ULP 等价引擎 LJS.lerp/smoothStep/Ease;
|
||||
* gamefeel 侧保内置实现作 null-engine fallback(非有意替代)。easing 门面**只暴 11 条等价曲线**——
|
||||
* backInOut/elasticInOut 不入面(引擎 IN_OUT 拼接是另一条曲线,差 6.6%/17%;gamefeel 对其保内置=补层)。
|
||||
* - particles/audio.synth=A1 留空骨架占位,A2/A3 在同一工厂内**替换填体**(串行落,避免本函数合并冲突)。
|
||||
* @returns {import('../src/core/api.d.ts').EngineCapabilities|null}
|
||||
*/
|
||||
function makeEngineCaps() {
|
||||
if (engineMode !== 'real') return null; // stub/node:不背书引擎(保 null-engine 回归;F7:node 本就 import 不了引擎)
|
||||
// 【真接线门②·host-dev 取证探针,绝不入渠道产物】每个引擎能力叶方法被调时记一条 call-ID 到 window.__engineCalls,
|
||||
// 供 mini-desktop CDP harness 断言「插件运行时真的经 ctx.getEngine() 调到了引擎」(动态运行证据,区别于门①的静态 import)。
|
||||
// rec 失败静默(取证副作用绝不连坐运行时);本函数在 engineMode!=='real' 已 return null,故探针只活 real 通道。
|
||||
const rec = (id) => { try { (window.__engineCalls || (window.__engineCalls = [])).push(id); } catch (_) { /* 取证副作用,忽略 */ } };
|
||||
// math 门面(A1 buildEngineMath 真活包装引擎)外再裹一层 rec:lerp/smoothStep 直裹,easing 子面逐曲线裹(保全部键与口径不变)。
|
||||
const mathRaw = buildEngineMath(LJS);
|
||||
const math = {
|
||||
lerp: (a, b, p) => { rec('math.lerp'); return mathRaw.lerp(a, b, p); },
|
||||
smoothStep: (p) => { rec('math.smoothStep'); return mathRaw.smoothStep(p); },
|
||||
easing: Object.fromEntries(Object.keys(mathRaw.easing).map((k) => [k, (t) => { rec('math.easing.' + k); return mathRaw.easing[k](t); }])),
|
||||
};
|
||||
return {
|
||||
// ── math(A1 落实现,含 easing 门面;外层 rec 记调用,内层 mathRaw 真包装引擎 lerp/smoothStep/Ease)──
|
||||
math,
|
||||
// ── particles(A2 填体)──薄包装引擎 ParticleEmitter:EngineEmitterSpec(像素口径)→ 引擎世界口径,返封装句柄。
|
||||
particles: {
|
||||
/**
|
||||
* 受控面 spawnEmitter:EngineEmitterSpec(像素口径)→ 引擎 ParticleEmitter(世界口径),返封装句柄(禁泄漏引擎对象)。
|
||||
* 边界模型(创始人 2026-06-13 拍):引擎有 ParticleEmitter → 此处薄包装;插件侧删自研 sim 积分循环,一职一路。
|
||||
* 引擎自渲:new ParticleEmitter 经 EngineObject 构造自入 engineObjects(esm.js:3580),引擎主循环每帧自调
|
||||
* update(发射+积分,esm.js:8348)+ render(drawTile 落 mainContext,esm.js:8654/8658)→ host 无需手动 add/render。
|
||||
* @param {import('../src/core/api.d.ts').EngineEmitterSpec} spec 归一化发射参数(像素/中性形态)
|
||||
* @returns {import('../src/core/api.d.ts').EngineEmitterHandle} 封装句柄(仅 isActive/stop,不见引擎对象)
|
||||
*/
|
||||
spawnEmitter(spec) {
|
||||
rec('particles.spawnEmitter'); // 真接线门②:记一次「插件经 ctx.getEngine().particles 真调引擎粒子构造」
|
||||
// 1) 像素↔世界阻抗换算(cameraScale 默认 32,esm.js:2785;host 未 setCameraScale 故用引擎 live binding 默认值,I3/OQ-2)。
|
||||
const worldPos = LJS.screenToWorld(LJS.vec2(spec.pos.x, spec.pos.y)); // 像素→世界(esm.js:5067/screenToWorld)
|
||||
const SCALE = LJS.cameraScale; // 世界单位/像素(用 live binding,随引擎相机变)
|
||||
// 2) burst/continuous → emitTime/emitRate(I1:引擎无「spawn 恰好 N」原语,burst 用「1 逻辑帧窗 × count×60 速率」近似 ≈N 颗)。
|
||||
const emitTime = spec.emitTime != null ? spec.emitTime
|
||||
: (spec.count != null ? 1 / 60 : 0); // burst=1帧窗(满即自停) / continuous=0(forever)
|
||||
const emitRate = spec.emitRate != null ? spec.emitRate
|
||||
: (spec.count != null ? spec.count * 60 : 100); // burst: count×60 颗/秒(该窗内恰发 ≈count 颗)
|
||||
// 3) speed:像素/秒 → 世界/帧@60(引擎 speed 单位来源 esm.js:8253,无再乘 dt)。
|
||||
const engSpeed = (spec.speed != null ? spec.speed : 0) / SCALE / 60;
|
||||
// 4) damping:每帧速度乘子(spec 已由插件把 drag/秒折算为每帧乘子,这里直透;缺省 1=无阻)。
|
||||
const engDamping = spec.damping != null ? spec.damping : 1;
|
||||
// 5) 颜色:归一化 {r,g,b,a} → 引擎 Color(esm.js:1547 rgb)。缺省不透明白→透明,保 real 像素门「有色像素命中>0」(I2)。
|
||||
const cs = spec.colorStart || { r: 1, g: 1, b: 1, a: 1 };
|
||||
const ce = spec.colorEnd || { r: 1, g: 1, b: 1, a: 0 };
|
||||
const colorStart = LJS.rgb(cs.r, cs.g, cs.b, cs.a);
|
||||
const colorEnd = LJS.rgb(ce.r, ce.g, ce.b, ce.a);
|
||||
// 6) 构造引擎发射器(27 参,按 d.ts:3232 构造签名逐位映射)。
|
||||
const em = new LJS.ParticleEmitter(
|
||||
worldPos, // pos(世界)
|
||||
spec.angle != null ? spec.angle : 0, // angle
|
||||
(spec.emitSize != null ? spec.emitSize : 0) / SCALE, // emitSize 像素→世界
|
||||
emitTime, // emitTime(0=forever)
|
||||
emitRate, // emitRate
|
||||
spec.coneAngle != null ? spec.coneAngle : Math.PI, // emitConeAngle(半锥角)
|
||||
undefined, // tileInfo: untextured(2D fillRect 路)
|
||||
colorStart, colorStart, colorEnd, colorEnd, // colorStartA/B + colorEndA/B(A=B → 固定色不随机)
|
||||
spec.particleTime != null ? spec.particleTime : 0.5, // particleTime
|
||||
(spec.sizeStart != null ? spec.sizeStart : 6) / SCALE, // sizeStart 像素→世界
|
||||
(spec.sizeEnd != null ? spec.sizeEnd : 0) / SCALE, // sizeEnd 像素→世界
|
||||
engSpeed, // speed(世界/帧@60)
|
||||
0, // angleSpeed: 0(无玩法语义,不旋转)
|
||||
engDamping, // damping
|
||||
1, // angleDamping: 1(无阻)
|
||||
spec.gravityScale != null ? spec.gravityScale : 0, // gravityScale(host 未 setGravity,全局 gravity=0 故暂不生效,OQ-4 follow-up)
|
||||
Math.PI, // particleConeAngle(粒子自转锥,留默认)
|
||||
0.1, // fadeRate(默认)
|
||||
0, // randomness: 0(spec 已显式给参,关引擎默认 .2 额外抖动求确定)
|
||||
false, // collideTiles: false
|
||||
spec.additive != null ? spec.additive : false, // additive
|
||||
true // randomColorLinear(A=B 时无影响)
|
||||
);
|
||||
// 7) 封装句柄(禁泄漏引擎对象;isActive/stop 转发,stop 幂等销毁)。
|
||||
return {
|
||||
isActive() { return em.isActive(); }, // esm.js:8482
|
||||
stop() { if (!em.destroyed) em.destroy(true); }, // 幂等(已 destroyed 直返,不二次销毁)
|
||||
};
|
||||
},
|
||||
},
|
||||
// ── audio.synth(A3 填体)──薄包装引擎 zzfxG/zzfxM:参数包/曲谱 → PCM 样本(不播放)。
|
||||
// 边界模型(创始人 2026-06-13 拍):引擎有 zzfxG/zzfxM → 此处薄包装;插件侧删 vendored 合成核,一职一路。
|
||||
// 引擎 zzfxG **不烤主音量 ×0.3**(样本=s*volume,esm.js:7391;×0.3 在引擎 gain 层 soundVolume=.3,esm.js:3121/6777)
|
||||
// → 返回的样本是「未乘 0.3 的 PCM」,与 api.d.ts EngineAudioSynth 口径一致;×0.3 由插件播放层补(audio-music/impl.js playChannels)。
|
||||
// 合成异常/失败返 null(插件侧 → 发声 no-op,不回退 vendored)。
|
||||
audio: {
|
||||
synth: {
|
||||
/**
|
||||
* ZzFX 单音效合成核:参数包 → PCM 单声道样本数组(未乘 0.3,不播放)。薄包装引擎 LJS.zzfxG。
|
||||
* @param {number[]} params ZzFX 参数包
|
||||
* @returns {number[]|null} PCM 单声道样本;合成异常返 null
|
||||
*/
|
||||
synthSfx: (params) => {
|
||||
rec('audio.synth.synthSfx'); // 真接线门②:记一次「插件经 ctx.getEngine().audio.synth 真调引擎 zzfxG」
|
||||
try {
|
||||
// 引擎 zzfxG 收 positional 参数(与 vendored 同签名,esm.js:7326)→ 展开参数包。
|
||||
return LJS.zzfxG(...params);
|
||||
} catch (e) {
|
||||
// 合成异常降级为 null(不连坐游戏,记 stderr);插件侧据 null 走发声 no-op。
|
||||
console.error('[host][engine-audio] zzfxG 合成异常,返 null:' + (e && e.message ? e.message : e));
|
||||
return null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* ZzFXM 曲谱合成核:乐器/模式/序列/BPM → [L,R] 双声道样本(未乘 0.3,不播放)。薄包装引擎 LJS.zzfxM。
|
||||
* @param {number[][]} instruments 乐器表
|
||||
* @param {number[][][]} patterns 模式表
|
||||
* @param {number[]} sequence 序列
|
||||
* @param {number} [bpm] 节拍(缺省引擎默 125)
|
||||
* @returns {number[][]|null} [L,R] 双声道样本;合成异常返 null
|
||||
*/
|
||||
synthSong: (instruments, patterns, sequence, bpm) => {
|
||||
rec('audio.synth.synthSong'); // 真接线门②:记一次「插件经 ctx.getEngine().audio.synth 真调引擎 zzfxM」
|
||||
try {
|
||||
// 引擎 zzfxM 签名与 vendored 逐字一致(esm.js:10837 `zzfxM(instruments, patterns, sequence, BPM=125)`)。
|
||||
return LJS.zzfxM(instruments, patterns, sequence, bpm);
|
||||
} catch (e) {
|
||||
console.error('[host][engine-audio] zzfxM 合成异常,返 null:' + (e && e.message ? e.message : e));
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
// 【P1 提升】引擎能力背书工厂 makeEngineCaps 已从此处闭包提升为可 import 模块 host-dev/engine-caps.js
|
||||
// 的 createEngineCaps({LJS, engineMath, engineMode, recHook})(搬运·语义零变;host-dev / ref real 通道共用)。
|
||||
// buildBundle 处以闭包 makeEngineCaps 桥接(保 engineFactory 注入口径不变 + host-dev 探针 rec 经 recHook 注入)。
|
||||
// 逐行保语义回归基线=门0 8/8 + real 门 6/6(提升不得改任何判据数值)。
|
||||
|
||||
// 创建 host-dev 受控上下文:注入「渲染面 / 主种子 / 音频工厂」。
|
||||
// 【A0 改点】受控面 getContext2d 的 backing:real 模式=引擎 engineInit 自建的 mainContext(纯 2D,
|
||||
@ -356,7 +229,15 @@ export function bootHostDev(opts) {
|
||||
seed,
|
||||
audioFactory,
|
||||
clock: () => mockNowMs,
|
||||
engineFactory: makeEngineCaps, // 【A1】注入引擎能力工厂;工厂内按 engineMode 短路(stub→null,保 null-engine 回归)
|
||||
// 【P1 提升】注入引擎能力工厂;工厂内按 engineMode 短路(stub→null,保 null-engine 回归)。
|
||||
// host-dev 仍注入 recHook(门② 探针只在 host-dev 取证产物里,host-dev 非渠道产物,符合现状 host.js 原 rec 语义)。
|
||||
// recHook 失败静默(取证副作用绝不连坐运行时);createEngineCaps 在 engineMode!=='real' 已 return null,故探针只活 real 通道。
|
||||
engineFactory: () => createEngineCaps({
|
||||
LJS,
|
||||
engineMath: buildEngineMath(LJS),
|
||||
engineMode,
|
||||
recHook: (id) => { try { (window.__engineCalls || (window.__engineCalls = [])).push(id); } catch (_) { /* 取证副作用,忽略 */ } },
|
||||
}),
|
||||
});
|
||||
// 合法包裹 bundle.deriveContextFor(host 拥有 bundle):注册器为每插件派生上下文时,记下该插件的
|
||||
// 派生 random 实例引用。这样 host 取证时能复位「插件那一份」随机(见 derivedRandoms 注释)。
|
||||
|
||||
59
game-runtime/src/core/game-host.d.ts
vendored
Normal file
59
game-runtime/src/core/game-host.d.ts
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
// game-runtime/src/core/game-host.d.ts —— 游戏宿主装载契约(第 9 类契约 · additive · game-runtime 内部)
|
||||
// owner:Runner v2 P1 | 消费方:集成段 host(boot 游戏)+ 生成 agent(产出靶)
|
||||
//
|
||||
// 【为什么新立、不折进 api.d.ts】
|
||||
// api.d.ts 的 PluginContext 是「插件↔引擎」面(getEngine() 等,api.d.ts:303,已冻结);
|
||||
// 本契约是「游戏整体↔引擎宿主」面——游戏的 update/render 主体不是插件、不走 PluginContext。
|
||||
// 两者正交(边界模型「同一职不留两条并行路」):插件经 getEngine() 拿能力,游戏经本契约挂宿主。
|
||||
// 引擎 import 只活集成段 host(Q4 铁律),游戏工厂零引擎 import。
|
||||
// 【为什么落 game-runtime/src 而非 contracts/】
|
||||
// contracts/ 是跨三仓五工位的业务契约 SSOT(API/DB/SDK/event…,contracts/README.md §1);
|
||||
// 本契约是 game-runtime 内部「host↔游戏工厂」接线面,消费方只在 game-runtime,
|
||||
// 故与 api.d.ts 同目录、import 方向顺;落 contracts 会让跨仓 SSOT 反向相对 import ../game-runtime/src(拆仓即断)。
|
||||
|
||||
import type { PluginContext } from './api.d.ts';
|
||||
|
||||
/**
|
||||
* 游戏挂宿主时拿到的 boot 上下文。host 在游戏 init 时一次性注入。
|
||||
* - ctx:下层已冻结受控面(PluginContext)。游戏经 ctx.getEngine() 拿插件能力(粒子/音频/数学),
|
||||
* 经 ctx.getInput() 拿受控输入,经 ctx.random/time 拿受控随机/时钟。**不得绕过 ctx 触碰裸引擎/DOM。**
|
||||
* - mainContext:引擎 mainContext(setGLEnable(false) 后的纯 2D 绘制面)。游戏 render(g) 的 g 即它;
|
||||
* 游戏美术画在此面 → 与引擎粒子(引擎自渲落同一 mainContext)同屏。**此字段是装载契约的核心**
|
||||
* (游戏取引擎绘制面的唯一约定,不借 PluginContext.getContext2d——那是插件面)。
|
||||
* - seed:主随机种子(确定性);canvas:可见画布引用(输入 rect 映射用,real=引擎 mainCanvas)。
|
||||
*/
|
||||
export interface GameHostBootContext {
|
||||
ctx: PluginContext; // 下层已冻结受控面(复用)
|
||||
mainContext: CanvasRenderingContext2D; // 引擎 mainContext(boot 注入,render 绘制面)
|
||||
canvas: HTMLCanvasElement; // 可见画布(real=引擎 mainCanvas,输入 rect 映射用)
|
||||
seed: number; // 主随机种子
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏实例:host 经引擎五回调驱动的稳定生命周期。
|
||||
* 生命周期对齐 engineInit 五回调(host.js:883-887 engGameInit/Update/Render):
|
||||
* · init ← gameInit(引擎首帧前一次性):建游戏对象、订阅 ctx.getInput()、装资产。
|
||||
* · update(dt) ← gameUpdate(引擎每逻辑步,dt=FIXED_DT 恒定):core.update/事件/特效计时。
|
||||
* · render(g) ← gameRender(引擎清屏后,g=mainContext):画游戏美术;引擎粒子已自渲落同一 g。
|
||||
* · onInput(ev)(可选):归一化输入事件回调。**注**:ref 经 ctx.getInput().on 订阅即可,
|
||||
* onInput 是「不便用 ctx.getInput 的极简游戏」的备选入口;二者择一,ref 用前者。
|
||||
* · destroy():卸载卫生(P2 装载多游戏复位用;P1 ref 单游戏可空实现但须声明)。
|
||||
*/
|
||||
export interface GameInstance {
|
||||
init(boot: GameHostBootContext): void | Promise<void>; // 异步:装资产(ref loadAssets 异步)
|
||||
update(dt: number): void; // 逻辑步(dt 秒,引擎恒定步长)
|
||||
render(g: CanvasRenderingContext2D): void; // g=mainContext(boot 注入的同一面)
|
||||
onInput?(ev: { type: string; x?: number; y?: number; key?: string }): void; // 可选输入入口
|
||||
destroy(): void; // 卸载(幂等)
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏工厂:纯模块导出(零引擎 import)。host 调它建 GameInstance。
|
||||
* 生成 agent 的唯一产出靶 = 实现并默认导出一个 GameHostFactory。
|
||||
* 【硬约束·§12-A3】必须是「独立、可被 P3 生成 agent 复制的工厂模块」:
|
||||
* · 独立文件默认导出(ref=games/wanglanmei-ref/src/game-shop.js 的 `export default createShopGame`);
|
||||
* · 零引擎 import(引擎能力一律经 boot.ctx.getEngine());
|
||||
* · 禁在 main.js 内用内联闭包「形似实现」糊过——否则 P3 缺真实参照实现,违 P1「spike 装载契约到可被 P3 复制」目标。
|
||||
* @param opts 工厂选项(无玩法语义;预留 difficulty/variant 等 additive 扩展位)。
|
||||
*/
|
||||
export type GameHostFactory = (opts?: Record<string, unknown>) => GameInstance;
|
||||
Loading…
x
Reference in New Issue
Block a user