merge(wg1): 并入 L1 scale-20 放量 + 宿主键盘桥 fix(d754b71·关键盘输入缺口)
独立验证(不信自述)后并入 fix/host-keyboard-bridge(=wg1/scale-20 超集,merge-base d79f2c3):
✅ 宿主键盘桥 fix(d754b71·5行纯增量):boot-game-host.js attachInput() 补 window keydown/keyup
桥接(ctx.getInput 键盘通道原为空→方向键/空格游戏输入死),对齐 host-dev/host.js:427-428 既有范式,
inputBridge _assertType 白名单本已含 keydown/keyup=零兼容风险。独立验:tetris verdict 修后
pass=True/9门/0挂(翻绿铁证)、tictactoe pass=True。
L1 scale-20 14 款放量(briefs/results/calibration/m3_player_test.py + tictactoe/tetris 正样本):
主力 7/8 真可玩;spec 预判门面缺口几乎全推翻(CCD/文本HUD/网格/match-finding 便宜模型自写对),
真·L0 门面缺口收敛为唯一一处=宿主键盘桥(本 fix 已补)。trial merge 干净(仅 .agents skill
auto-merge),我 3b-B 文件(service.py/CallbackSignatureVerifier)完好。
校正:我 §5『点击坑=moat 真壁垒』系单样本(3b-B)过度泛化,与 L1 §9 scale-20 实证矛盾→已改为
承认便宜模型相当能干、真缺口=键盘桥(已修)+driver-coverage(harness),非模型能力。
重大上修:moat 生成质量远好于 3b-B 单样本所示;失败主因=宿主门面(已补)+harness driver 覆盖,非模型。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ -37,8 +37,8 @@ brief 题面 → 便宜模型写 `generated-factory.js`(一个 `GameHostFactor
|
||||
## 5. 边界与未做
|
||||
|
||||
- 只消费冻结契约(`game-host.d.ts` / 6 受控面 / `__GameBundle` / `bootGameHost`)**零修改**;只在 lane 分支,不推 dev/2.0.0。
|
||||
- **✅ 生产接线已成(3b-B 派发面,`c99014c`)**:`AigcGenerateExecutor.dispatchGeneric`→真 worker `service.py`(包 `run_studio`)→HMAC 回调→落库入 feed 全链真证闭合(见 [`ai-generation-pipeline.md`](./ai-generation-pipeline.md) §⓪′)。**但 3b-B e2e 确认「点击坑」是 moat 真壁垒**:一句话「点击小怪物」三款真生成全 `nine_gate_failed`——截图证真出 on-theme 可渲可动游戏(Score/3s/monsters),仅点击→计分坏(Score 恒 0)→**九门正确拦截不可玩**(正是 §3 点击坑/harness §7 G-H 门逮的),`run_studio` 自修 `max_repairs` 未救回。**铁律重申**:plumbing 易、**便宜模型把「点击命中→计分」写对=真难点**;质量门拒发不可玩=系统正确履职(可玩性地板),非故障。
|
||||
- **未做**(创始人并行 / 后续波次):全 20 款 bake-off、四模型横比(过门率/重试/¥每过门)、碰撞穿透门 J、**落一个过九门的正样本**(v5 挡板太慢挂 H/I;3b-B「点击小怪物」挂点击命中——均需 prompt 给具体参数[挡板速度/命中半径] + driver 持续/精准出招 + 阈值校准)——规划见 [`2026-06-14-WG1-bakeoff天花板与Claude-free评估门-review.md`](../../docs/agent-specs/2026-06-14-WG1-bakeoff天花板与Claude-free评估门-review.md)。
|
||||
- **✅ 生产接线已成(3b-B 派发面,`c99014c`)**:`AigcGenerateExecutor.dispatchGeneric`→真 worker `service.py`(包 `run_studio`)→HMAC 回调→落库入 feed 全链真证闭合(见 [`ai-generation-pipeline.md`](./ai-generation-pipeline.md) §⓪′)。3b-B 单款一句话「点击小怪物」真生成卡九门(Score 恒 0→**质量门正确拒发不可玩=系统履职非故障**),`run_studio` 自修 `max_repairs` 未救回。**⚠️ 但单点失败≠模型壁垒**:L1 scale-20 14 款实证(§9)便宜模型实际相当能干(主力 7/8 可玩、自写 CCD/文本HUD/网格/match 全对),多数失败是**宿主键盘桥 host bug(已修 `d754b71`)+ driver-coverage(harness 太朴素),非模型**;3b-B 那款真因(driver-coverage 或该款 hit-detection)续查并入 §9 画像。**真难点不是模型能力,是 harness driver 覆盖 + 宿主门面完备**(§9)。
|
||||
- **未做/续(创始人并行)**:20 款补全(scale-20 已 14 款,见 §9)、四模型横比(过门率/重试/¥每过门)、6 个 driver-coverage 失败款的 driver 增强(flappy/multiball/愤怒小鸟等)、键盘款修后批量重跑、calibration 人锚补全——规划见 [`2026-06-14-WG1-bakeoff天花板与Claude-free评估门-review.md`](../../docs/agent-specs/2026-06-14-WG1-bakeoff天花板与Claude-free评估门-review.md)。
|
||||
|
||||
## 6. 官方 latch 终态契约 + 套壳(P3 对齐,2026-06-14)
|
||||
|
||||
@ -66,3 +66,13 @@ P3 主线把 L1 产出靶定死为**官方通用装载壳**(`game-runtime/game
|
||||
- **gate-H 盲打脚本会假阴性**:固定坐标 taps 打不动技巧游戏→"0 进展"≠空心(创始人把被判空心的 v2 排第 2)。解法 = **适配性真玩 driver**(读 state 接球,harness §7)。
|
||||
- **founder/人锚不可替代**(至少现阶段):没有它会把碰撞有问题的 pro 当"最佳"发出去。**校准 harness** = `wg1/gen-worker/worker/agent_loop/calibrate.py`(吃 `calibration/labels.json` 人锚,算 player vs 人锚 MAE/吻合/反样本否决)。
|
||||
- **路线**:确定性门做"地板"(能跑/真接线/有进展/latch/跟手),手感尽量拆成确定性检查(碰撞穿透门 J 规划中);拆不掉的主观手感留**创始人抽检**。
|
||||
|
||||
## 9. scale-20 放量实证:模型能力画像 + spec 预判推翻(W-G1 lane v3,2026-06-14)
|
||||
|
||||
flash 打底放量 14 款(主力 8 + 压测 6),Opus 对抗审计核实(2 个 ultracode workflow):
|
||||
|
||||
- **主力 7/8 真可玩**(0 假绿):井字棋/见缝插针/打砖块/跑酷/太空侵略者/Doodle/扫雷(扫雷=borderline——本体可玩,但门弱断言经"踩雷 lose"路径满足、证据薄)。flappy 唯一未过=**driver-coverage**(升 pro 仍挂,但源码物理合理、理想 AI 得分→是 flap-to-gap driver 太朴素,**非模型天花板**)。
|
||||
- **压测**:match3 ✅;**2048/Tetris/Asteroids 三款键盘游戏全挂于宿主缺键盘桥**(非模型,见 [`game-e2e-cdp-harness.md`](./game-e2e-cdp-harness.md) §8);multiball/愤怒小鸟=driver-coverage。
|
||||
- **★ 模型能力画像(flash)**:文本 HUD / 网格状态机(井字棋胜负+AI、扫雷洪泛)/ 子步 swept CCD(asteroids/multiball)**均自写正确**;计时/物理精度(Flappy)需**题面 pin 死数值 + 收紧观测契约**(Doodle 实证:收紧计分 + seek-x driver → regen 首发过)。栽点主要在**静态门可拦的纪律滑坡**(Date.now/Math.random/缺 render·destroy/没调 getEngine),零 CDP 成本拦下、回喂即修。
|
||||
- **★ spec 预判的门面缺口几乎全推翻**:CCD / 文本HUD / 网格 / match-finding / 手势均**非缺口**(便宜模型自带或自写;故"碰撞穿透门 J"已非急需)。**真·L0 门面缺口收敛为唯一一处=宿主键盘桥**(已 spawn_task 交 L0)。完整数据见 [`2026-06-14-WG1-scale20-短板量化报告.md`](../../docs/agent-specs/2026-06-14-WG1-scale20-短板量化报告.md)。
|
||||
- **M3 视觉 player 当生产过滤器**(`calibration/m3_test.json`):与门 8/9 一致、抓显性空心(flappy fun1)、漏截图不可见失败(键盘死/弱证据,恰被确定性门兜住)→ **三层互补**(门=地板 / M3=主观过滤 + 便宜 fun 信号 / 创始人=人锚)。MAE 压低待创始人对新款补锚(框架沿用 `calibrate.py`)。
|
||||
|
||||
@ -73,3 +73,24 @@
|
||||
**G 门 = 本 lane 关键发明,专逮"假绿"**:Pong 这类带**自走动画**的游戏,输入全失灵也"能动"、能骗过 C/D/E;G 门用同帧号 A/B 哈希对照隔离出"输入到底改没改变走向"。实证逮住一处**输入坐标契约写错**(生成 prompt 误称归一坐标、实为逻辑像素 `boot-game-host.js:295`)致"能编译能渲染能动但点击全失灵"的假绿。
|
||||
|
||||
**CDP 触摸下发坑**(承 §2 律3,生成游戏侧细化):tap 须**镜像 ref `realTapAt`**——裸 `{x,y}`(逻辑像素,经 §2 律2 rect 映射)、`touchStart`/`touchEnd` **背靠背**(无 radius/force/长按),否则触摸不达 `getInput()`。坐标契约 = **逻辑像素 [0..390]/[0..844],非归一**(便宜模型与人皆易错)。
|
||||
|
||||
## 8. scale-20 放量实证:driver 库泛化 + 门承重件 + 对抗审计硬化(W-G1 lane v3,2026-06-14)
|
||||
|
||||
**适配 driver 库 1→6 型 + drag 输入事件**(`play.cdp.cjs`,`spec.driver.type` 分发;盲打打不动技巧/实时游戏=H 假阴根因,故按机制族建 driver):
|
||||
|
||||
| driver | 机制族 | 读什么自动出招 |
|
||||
|---|---|---|
|
||||
| `paddle-intercept` | 挡板接球 | `ball.x` → 挡板对齐 |
|
||||
| `tap-targets` | 点击离散目标 | `state.targets[{x,y,occupied}]` → 点未占用(井字棋/扫雷/打地鼠/Simon/invaders…) |
|
||||
| `flap-to-gap` | 重力穿缝 | `bird.y` vs `nextGap.centerY` → 偏低拍翅 |
|
||||
| `seek-x` | 横向趋近接台 | `bird.x` vs `nextPlatform.x` → 点左/右半 |
|
||||
| `tap-pairs` | 相邻交换(三消) | `targets[k]`/`[k+1]` 成对点 |
|
||||
| `key-cycle` | 纯按键 | 循环按 `keys[]` 到 gameover(**依赖宿主键盘桥,见下** ) |
|
||||
|
||||
固定输入新增 `{t:'drag',from,to,ms}`(touchStart→插值 touchMove→touchEnd,合成 swipe/拖拽)。**driver 覆盖度 = 门能否兜住的前提**:shooter/填充型 latch 需足够步数驱动到终局(invaders/tetris/multiball 的 latch 假阴皆此因,非游戏坏;invaders 步数 30→50 即翻绿,实证)。
|
||||
|
||||
**★ 地板真承重件 = `expectLatch` + `G_input`,不是 progress 计数**(对抗审计实证):`H_progress.pass = progressPass && latchPass`;单条 `assertAfterPlay`(score/revealed/moves +1)鉴别力弱——saolei 踩雷也 revealed++、runner score 计时自增,可被"漂亮空壳"蹭过;真隔离空壳的是 latch(真到 gameover+600ms 驻留)+ G_input(同帧 A/B 哈希证输入真生效)。**新游戏务必让 progress 断言带终局语义**(全清/三连/真死),否则靠 latch+G 兜。
|
||||
|
||||
**★ 宿主键盘桥缺失(L0 硬缺口,实证)**:`boot-game-host.js attachInput()` 只桥 pointer 三态、**不桥 keydown/keyup** → `ctx.getInput()` 键盘通道空,所有方向键游戏(2048/Tetris/Asteroids)输入死。**失败严格沿"键盘 vs 指针"分界=最强系统性证据**(指针款全过、键盘款全挂)。修法对齐 `host-dev/host.js:427-428`(已 spawn_task 交 L0)。→ **移动端轻游戏优先 pointer/tap/swipe,键盘款待宿主补桥**;配套 `play.cdp.cjs` key() 对 Arrow 须传 `code===key`(原 undefined 致 event.key 空)。
|
||||
|
||||
**★ 对抗审计硬化门(Opus build-then-exit,ultracode workflow)**:脚手架期用 Opus 多 agent 对抗审计九门判定(反驳每个 PASS 找假绿 + 归因每个 FAIL)——实证抓出 saolei 弱断言(门+M3 都没抓到)、把 flappy 错归"模型天花板"纠正为"driver 缺陷"、定位宿主键盘根因。**生产无 Claude:门(地板)+ M3 便宜 player(视觉过滤:抓显性空心、漏截图不可见失败)+ 创始人(人锚)三层互补**;Opus 审完去硬化门/driver 即退出。
|
||||
|
||||
148
docs/agent-specs/2026-06-14-WG1-scale20-短板量化报告.md
Normal file
@ -0,0 +1,148 @@
|
||||
# 《W-G1 · 20 经典轻游戏放量 — 短板量化报告》
|
||||
|
||||
> 状态:**进行中**(主力批 8 款已终;压测批 6 款 + flappy@pro 后台跑中;player 校准待做)· 2026-06-14
|
||||
> lane:`wg1/scale-20`(基于 `dev/2.0.0 @d79f2c3`)· 模型:DeepSeek-V4-flash 打底、耗尽升 pro(经 new-api 网关 `100.64.0.8:3000`,无 Claude)
|
||||
> 口径:每款过**九门**(A-G 假绿守卫 + H 机制/latch + I 控制手感),`results/<id>.json` 为原始证据。靶集见 [20 经典轻游戏基准 review](2026-06-13-WG1-20经典轻游戏基准-review.md)。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论先行(给创始人/L0 的三句话)
|
||||
|
||||
1. **便宜模型(flash)+ 九门 harness 能产可玩轻游戏**:主力批 8 款 flash 实过 **7/8**(井字棋/见缝插针/打砖块/跑酷/太空侵略者/Doodle 经 Opus 对抗审计**确认真可玩、0 假绿**;扫雷=**borderline**,本体可玩但门证据过薄,见 §七)。多数款 1 次重试内过,**失败基本被静态门零 CDP 成本拦下、回喂即修**。**唯 Flappy 主力批未过**——经审计:**非模型天花板**(升 pro 仍挂、但理想瞄准 AI 可得分),根因是适配 driver 太朴素(driver-coverage,见 §七)。
|
||||
2. **⚠️ 推翻 spec 预判**:基准 review 预测最痛缺口是「文本 HUD / 网格状态」。**实测 flash 这档能自写文本+网格**(井字棋、扫雷均过,grid 状态机/洪水填充/AI/计分自管正确)→ **这两类门面优先级应下调**。
|
||||
3. **真天花板 = 计时/物理精度类的生成质量 + harness 适配 driver 的覆盖**:Flappy(物理太硬秒死、缝隙观测漏出)flash 2×2 次全挂;Doodle 靠收紧计分语义 + 适配 driver 才救回;太空侵略者靠调 driver 步数才验到 latch。**门兜底有效的前提是 harness 有对的适配 driver**——本批把 driver 库从 1 种扩到 6 种即为此。
|
||||
|
||||
---
|
||||
|
||||
## 二、方法与口径
|
||||
|
||||
- **生成侧**:`wg1/gen-worker` worker(裸 openai client → new-api 网关),`briefs/<id>.json` = 题面 + play-spec(九门 driver 配置);闭环 generate→静态门→esbuild→九门真玩→失败回喂重试 ≤2。
|
||||
- **真玩侧**:`game-runtime/games/_wg1-gen/_shared/play.cdp.cjs`(macOS 本机 Chrome+CDP)。**H/I 门靠"适配性真玩 driver"**:读 `_forensicsView().state()` 实体位/状态自动出招(盲打固定坐标对技巧游戏必假阴)。
|
||||
- **成本**:每款 token 见下表;¥ 折算(cost.py / new-api quota)待最终补;闸 ¥0.15/款。
|
||||
|
||||
---
|
||||
|
||||
## 三、主力批 8 款结果(flash 打底)
|
||||
|
||||
| # | 款 | 结果 | 重试 | tokens(in/out) | driver | 首次失败 / 关键 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | 井字棋 tictactoe | ✅ | 1 | 8354/10055 | tap-targets | 静态门:`Date.now` 违禁 → 回喂修 |
|
||||
| 7 | 扫雷 saolei | ⚠️ ✅borderline | 1 | 8214/13932 | tap-targets | 本体源码可玩,但该 verdict 的 H 仅 `revealed 0→1` 且经【踩雷 lose 路径】满足(盲打首 tap 即踩雷、核心玩法零暴露)→ Opus 深审判 borderline,见 §七 |
|
||||
| 18 | 见缝插针 jianfeng | ✅ | 0 | 3943/7005 | 固定输入 | **首发全过** |
|
||||
| 5 | 打砖块 breakout | ✅ | 1 | 8115/9646 | paddle-intercept | 静态门:缺 render/destroy + 没调 getEngine + `Math.random` |
|
||||
| 16 | 跑酷 runner | ✅ | 1 | 7908/16124 | 固定输入 | 静态门:`Date.now`;score 自增稳过,G 验跳跃有效、latch 验真撞死 |
|
||||
| 10 | 太空侵略者 invaders | ✅ | 2 + 步数调 | 12211/25573 | tap-targets | **游戏本身好**(score 0→12 全清);仅 driver 步数 30→50 才驱动到 latch |
|
||||
| 14 | 跳跃者 doodle | ✅ | 0(regen) | 4021/10197 | seek-x | 首版计分语义错(score 卡 118);收紧题面 + seek-x → regen 首发过(score 259→1003) |
|
||||
| 4 | Flappy flappy | ❌(待 pro) | 2×2 | — | flap-to-gap | 生成物理太硬(`vy≈626` 秒坠死)+ `nextGap` 开局漏出;flash 天花板,升 pro 中 |
|
||||
|
||||
**主力批:flash 实过 7/8。** flappy@pro 结果待填。
|
||||
|
||||
---
|
||||
|
||||
## 四、短板发现(证据支撑)
|
||||
|
||||
1. **便宜模型主要栽点 = 静态门可拦的契约纪律滑坡**。高频违规:`Date.now`/`Math.random`(应走受控 `time`/`random`)、缺 `render`/`destroy` 方法、忘调 `getEngine()`(真接线门无信号)。**全部静态门零 CDP 成本拦下,回喂一次即修**——印证"门兜底"是便宜模型路线的命门,且大头开销不在 CDP 而在生成。
|
||||
2. **文本 HUD / 网格状态:flash 能自写,非最痛缺口(推翻 spec 预判)**。井字棋(3×3 状态机+胜负+AI+文本)、扫雷(洪水填充+雷数文本)均过;仅扫雷的进展观测量 `revealed` 首发没接对、retry 修。→ **text/grid 门面优先级下调**;若补,价值在"省 token/一致性",非"造不出"。
|
||||
3. **计时/物理精度类:生成质量 + driver 覆盖双重挑战(原"模型天花板"判断已被审计纠正)**。Doodle 首版计分语义错(score 卡 118)→ **收紧题面计分契约 + seek-x driver** regen 首发过(score 259→1003),证**生成质量可经题面收紧 + 适配 driver 救回**。Flappy 经 Opus 审计:**非模型天花板**——升 pro 三次仍挂、但源码物理参数合理(GRAVITY420/FLAP-260/GAP220),"理想瞄准 AI"多种子可得 5-15 分 → 根因是 **flap-to-gap driver 太朴素(无前瞻预测落点),对极端高位缝追不上**(driver-coverage)。→ 对策:**收紧物理/观测题面 + 给适配 driver 加前瞻**,而非简单升档。
|
||||
4. **harness 适配 driver 的覆盖 = 门能否兜住的前提**。技巧/实时类盲打固定坐标必假阴(H 假阴);shooter 类 latch 需足够 driver 步数(invaders 实证:步数不够≠游戏坏)。本批据此扩 driver 库(见下)。
|
||||
|
||||
---
|
||||
|
||||
## 五、harness 产出:适配 driver 库(1 → 6 种 + drag 事件)
|
||||
|
||||
> gate-H 推广在实践中落地:把"读 state 自动真玩"覆盖到六类机制族。play.cdp.cjs。
|
||||
|
||||
| driver | 机制族 | 覆盖款 | 状态 |
|
||||
|---|---|---|---|
|
||||
| `paddle-intercept` | 挡板接球 | 打砖块 / 多球弹球 | 既有 |
|
||||
| `tap-targets` | 点击离散目标 | 井字棋 / 扫雷 / 太空侵略者(+翻牌/Simon…) | 本批新建,3 款验证 |
|
||||
| `flap-to-gap` | 重力穿缝 | Flappy | 本批新建,验证中 |
|
||||
| `seek-x` | 横向趋近接台 | Doodle | 本批新建,✅ Doodle |
|
||||
| `tap-pairs` | 相邻交换 | Match-3 | 本批新建,压测中 |
|
||||
| `key-cycle` | 纯按键循环 | Tetris / 2048 / Asteroids | 本批新建,压测中 |
|
||||
| `drag`(输入事件) | 拖拽手势 | 愤怒小鸟 | 本批新建,压测中 |
|
||||
|
||||
---
|
||||
|
||||
## 六、压测批 6 款结果(测上限/暴露缺口)— 已完成
|
||||
|
||||
> 经第二轮对抗诊断 workflow(7 agents)逐款归因。**重大发现:6 款失败严格沿「键盘 vs 指针」分界**——所有 key-cycle 键盘款(t2048/tetris/asteroids)集体挂、所有 pointer/tap 款全过,锁定根因 = **宿主键盘门面缺失**(见 §八),非模型逐个写坏。
|
||||
|
||||
| # | 款 | 结果 | driver | 根因 | 真·门面缺口? |
|
||||
|---|---|---|---|---|---|
|
||||
| 13 | Match-3 | ✅ r0 | tap-pairs | **真可玩**(score 0→40 真消除、latch 驻留;盲态交换凑出三连) | 无(swipe/match-finding 预判**未成立**) |
|
||||
| 20 | 多球弹球 | ❌ latch | paddle-intercept | driver-coverage(游戏含**子步 swept CCD**、本体正确;9s 既清不完砖又护住末球→终态不可达) | 无;driver 加"建进展→弃守排空"相位即翻绿 |
|
||||
| 11 | Tetris | ❌ latch | key-cycle | **宿主键盘门面缺失**(piecesLocked 0→1 实为重力自动锁定、非键盘;progress 假性过) | **是:键盘桥** |
|
||||
| 2 | 2048 | ❌ score0 | key-cycle | **宿主键盘门面缺失**(50 次按键棋盘零变化;move/合并/终态本体正确) | **是:键盘桥** |
|
||||
| 12 | Asteroids | ❌ score0 + F_wiring0 | key-cycle | **宿主键盘门面缺失**(键盘没到→不开火→不调引擎;子弹**已是 swept CCD**,CCD 预判未成立) | **是:键盘桥** |
|
||||
| 15 | 愤怒小鸟 | ❌ score0(latch 过) | 固定 drag | drag-aiming(固定盲拖弹道最近距猪~218px;本体正确、F_wiring 过) | 无;driver 加第 7 型 drag-aiming(读目标反解拖拽矢量)+ 放宽关卡 tuning |
|
||||
|
||||
**压测批结论**:6 款里 **0 个是"便宜模型造不出"**——match3 真可玩;3 款键盘款挂在**同一处宿主缺键盘桥**;multiball/愤怒小鸟挂在 harness driver 覆盖。**spec 预判的 CCD/match-finding/手势/文本 HUD 缺口全部未成立**(详 §八)。
|
||||
|
||||
> **★ host 修复已验证(`fix/host-keyboard-bridge` 分支 · commit d754b71 · 2026-06-14,经创始人授权隔离执行)**:补宿主键盘桥 + 重打包 3 款重跑九门——**键盘输入全部接通**:**tetris 整盘翻绿**(piecesLocked 0→10 + latch✓,修前 0→1 是重力假性)、t2048 **score 0→4 真合并**(修前 0→0)、asteroids **F_wiring 0→27 + latch✓**(船真动真开火,修前全冻 0)。残留:t2048 latch(2048 盲态几乎不死锁)、asteroids score(盲射没瞄准)= driver-coverage 跟进,**非键盘非模型**。→ host 键盘桥根因**坐实**,供 L0 cherry-pick `d754b71` 到 dev/2.0.0。
|
||||
|
||||
---
|
||||
|
||||
## 七、三方对照:确定性门 vs M3 便宜 player vs Opus 教师审计(核心实证)
|
||||
|
||||
对主力批 9 款(7 pass + flappy/tetris fail)做三通道交叉验证。
|
||||
|
||||
### 7.1 M3 视觉 player(生产中间层,Claude-free)
|
||||
MiniMax-M3 看首帧/玩后截图 + 九门数据判可玩/好玩(production-faithful,`roles.player_system` 人格)。结果 `wg1/gen-worker/calibration/m3_test.json`:
|
||||
- **与门 8/9 一致**;独立判出 flappy「空心、开局即死、零得分」(fun=1, verdict=fix)——**便宜 player 能抓明显差的**。
|
||||
- **fun 压缩**(可玩款都给 3、doodle 给 4):**细排不行(需创始人锚),但粗筛 hollow(1)↔可玩(3-4)能分开** = 过滤器能力成立。
|
||||
- **盲点**:saolei 给 pass(fun 3)——M3 凭截图+常识判"扫雷骨架完整可玩",**没识破该次 run 只踩雷、核心玩法零暴露的弱证据**(见 7.3)。
|
||||
→ **M3 作生产 player 过滤层:抓明显坏可信;审"门证据质量"不行**(那是教师层的事)。
|
||||
|
||||
### 7.2 Opus 教师审计(脚手架期,10 agents 对抗反驳)
|
||||
每款派对抗审计员读源码+截图+state 尽力反驳"假绿"/归因失败:
|
||||
- **0 假绿(危险侧无失守)**:7 个 claimed-pass 中 **6 真可玩**(tictactoe/jianfeng/breakout/runner/invaders/doodle,refutation 全失败,含独立 node 复算棋局、碰撞数学、分数防灌源码证伪)——**没有一个坏游戏被当好游戏放过**。
|
||||
- **saolei = borderline(关键告警)**:游戏本体源码可玩(布雷/邻数/数字渲染/0格洪泛/win 判定全对),但该 verdict 的 H 仅 `revealed 0→1` 且经【踩雷 lose】满足 → 核心玩法在门证据里零暴露。**门没误判"游戏不存在",是 driver 盲打无避雷致证据过薄;门+M3 都没抓到,只有 Opus 深审抓到。**
|
||||
- **2 fail 全是 driver-coverage,零 model-gap / 零 hard-ceiling**:
|
||||
- **flappy**:8 门绿、仅 progress(score 0→0)挂。flap-to-gap driver 太朴素(无前瞻),对极端高位缝(centerY174/鸟559,单拍仅升 80px)追不上→撞管。**源码物理合理,理想瞄准 AI 多种子得 5-15 分→游戏可玩**。**升 pro 三次仍同门挂 → 证 driver 缺陷、非模型档位**。
|
||||
- **tetris**:8 门绿、仅 latch 挂(piecesLocked 0→1 过但没堆到顶)。key-cycle 盲打 50 步 + clearLines 持续消行→永堆不到 spawn 行→gameover 不可达。**源码 latch 契约忠实、对人类可达**。换"读盘单列堆顶"driver 或大幅加步即翻绿。
|
||||
|
||||
### 7.3 地板的真实承重件 = latch + G_input,不是 progress 计数
|
||||
审计核实 `play.cdp.cjs:516` `H_progress.pass = progressPass && latchPass`(硬 AND)。7 款 `assertAfterPlay` 全是单条弱计数(moves/revealed/inserted/score/remaining 动 1 步即过),其中 **saolei(踩雷也 ++)/ runner(score 计时自增、spec 自承"稳过")/ doodle(理论纯自弹也可能蹭高度)** 的 progress 子句鉴别力弱。**真正隔离"漂亮空壳"的是 expectLatch(真到 gameover + 600ms 驻留、堵自动重开)+ G_input(同帧 A/B 哈希证输入真生效)**;tictactoe/invaders/breakout 因终局语义(全清/三连/真掉球)progress 才较强。**门有鉴别力非橡皮图章**:flappy 卡 progress、tetris 卡 latch、t2048 连 E/G 都红——失败分布在不同门。
|
||||
|
||||
### 7.4 三方结论
|
||||
- **确定性门 = 地板**:危险侧 **0 假绿**、有鉴别力;但**地板高度由 driver 覆盖度封顶**——driver 太弱则塌成 false-fail(flappy/tetris/saolei 同源),弱 progress 计数靠 latch+G_input 兜。
|
||||
- **M3 便宜 player = 过滤器**:抓明显坏(flappy)、粗筛可玩,**担生产中间层可信**;审不出弱证据(saolei),细排靠创始人。**这正是三层 reframe 的中间层落地。**
|
||||
- **Opus 教师 = build-then-exit 的硬化工具**:抓到 saolei 弱断言 + 把 flappy 错归因"模型天花板"纠正为"driver 缺陷"——**审完去硬化门/driver,生产跑 门+M3+创始人(无 Claude)**。
|
||||
|
||||
> 方法学诚实标注【inferred/静态】:Opus 审计系**源码 + 落盘证据静态交叉验证**(本机 serve/CDP 已停,saolei-borderline、flappy@pro-fail、invaders-replay 等部分款未现场重跑);结论可复核但非全量现场实测。
|
||||
|
||||
### 7.5 player-as-filter 量化(Task #6 自主部分)
|
||||
以 Opus 审计的 playableTruth 为代理真值(创始人锚 MAE 需创始人补,框架沿用 `calibrate.py`),量化 M3 当过滤器(数据 `calibration/m3_test.json`):
|
||||
- **M3 vs 确定性门:8/9 一致**(唯 tetris 分歧:门判 fail/latch、M3 判 pass——**门更严、抓到 M3 看不见的机制失败**)。
|
||||
- **M3 vs 审计真值:7/9**。M3 正确:6 款 confirmed-playable 全 pass(**好游戏 0 误杀**)+ flappy 正确判 hollow(fun1)。漏判 2 款:**saolei(审计 borderline)、tetris(审计 fail/键盘死)——共性="截图里看着没问题"**(saolei 踩雷 lose 屏、tetris 有 1 块落定),M3 凭视觉+常识判可玩,看不出"该次 run 核心玩法没真跑"。
|
||||
- **过滤器结论**:M3 **抓视觉显性空心**(flappy:鸟死/零分/空屏)有效;**漏截图不可见的失败**(键盘死、弱证据)——但这些**恰好确定性门已兜住**(tetris latch 挂、键盘款 score0 挂、saolei 弱断言已列整改)。→ **生产三层互补非冗余**:门抓机制失败(地板)、M3 抓视觉空心 + 给便宜 fun 信号(过滤 + 主观层)、创始人抓截图不可见的手感/弱证据(人锚)。**M3 边际价值不在"抓门漏的坏"(门更强),而在门通过后的便宜主观 fun 信号 + 减创始人对显性差件的抽检量。**
|
||||
- **压 MAE / 查准下一步**:需创始人对这 9+ 新款亲玩给锚(我产不了)→ 入 `labels.json` 跑 `calibrate.py` 出 MAE/查准查全数值;框架就绪、待人锚。
|
||||
|
||||
---
|
||||
|
||||
## 八、门面优先级建议(定稿,经两轮对抗诊断 workflow)
|
||||
|
||||
> 经压测批实测 + 源码核验,**spec 预判的功能门面缺口几乎全部被推翻**;真·L0 门面缺口高度收敛为**唯一一处**。
|
||||
|
||||
### P0 — 唯一真·L0 硬门面缺口(系统性命中 3 款,最高优先)
|
||||
**宿主键盘事件门面缺失** —— `game-runtime/src/host/boot-game-host.js` 的 `attachInput()` 只桥 `pointerdown/move/up`,**完全不桥 `keydown/keyup`** → `ctx.getInput()` 键盘通道空。`getInput` 契约声明「pointer 三态 + key 两态」,实现漏了 key 两态。
|
||||
- **证据**:失败严格沿「键盘 vs 指针」分界(3 键盘款全挂、所有指针款全过;同射击品类 invaders 走 pointer 即过);bundle 内 `_emit` 仅 pointer×5、`keydown` emit 0 命中;桥白名单已含 keydown/keyup 却无人 emit。
|
||||
- **修法(L0,2 行,零兼容风险)**:`attachInput()` 补 `window.addEventListener('keydown', ev => { unlockAudioOnce(); inputBridge._emit('keydown',{key:ev.key}) })` + keyup,对齐同仓 `host-dev/host.js:427-428` 已有正确范式。一次解锁 t2048 / asteroids / tetris 三款。**已 spawn_task 交 L0**。
|
||||
- **配套(我域,已修)**:`play.cdp.cjs:159` key() 对 Arrow 键发 `key=undefined` → 改为 `code===key` 直传(否则桥了 ev.key 游戏仍读空)。
|
||||
|
||||
### 实测未成立的预判(逐条澄清,勿再投入门面)
|
||||
- **CCD swept**:asteroids(`lineCircleIntersect` 线段vs圆)、multiball(`SUB_STEP` 子步 swept)便宜模型**已自带连续碰撞**,无 tunneling。**不是缺口**。
|
||||
- **文本 HUD**:6 款 `_forensicsView` + HUD 渲染齐全,D_render/E_live 全过。**不是缺口**。
|
||||
- **网格状态**:井字棋/扫雷/2048/match3 网格逻辑 flash **自写正确**。**不是缺口**。
|
||||
- **match-finding / swipe**:match3 盲态 tap-pairs 已凑出三连真过。**非 L0 门面**(要确定性可在 driver 侧加 grid-swap-finding)。
|
||||
- **手势(drag-to-aim)**:愤怒小鸟缺的是 harness driver 第 7 型,**driver 域、非门面**。
|
||||
|
||||
### driver-coverage(harness 我域,非门面;是门面定优先级的前置净化步)
|
||||
**"driver-coverage 可修翻绿"已双证**:invaders(步数 30→50 翻绿)、**tetris(host 键盘修复后整盘翻绿)**。其余残留经 driver 调参实测(flap 加速度前瞻 / paddle 弃守排空相位 / 2048 双向堆叠)**未轻易翻绿——因玩法特性、收益递减**,如实记档(均**非门面、非模型**,实验已 revert):
|
||||
- **multiball**:终局可达,但盲态弃守排空时挡板被钳在屏内、角落仍接球,末球不漏 → latch 难达;需"反接球振荡排空"driver(未做)。
|
||||
- **t2048**:2048 盲态**无强制终局**(随机走总有合法移动,实测双向也只死 1 次合并)→ latch 非盲态可达;需"填库死锁"driver 或 progress-only 门(玩法特性,非缺陷)。
|
||||
- **flappy**:flap-to-gap 加速度前瞻仍救不回**该 pro 生成件**(物理太硬 + 缝隙观测早期 None)→ 需近理想控制 driver 或 pin-易物理重生成。
|
||||
- **愤怒小鸟**:需 drag-aiming driver(读目标反解拖拽矢量)+ 游戏导出目标位;未做。
|
||||
|
||||
两轮诊断证实:把这些 false-fail 误记为门面缺口会污染优先级,故先净化 driver 覆盖再定门面。论题已立,残留属"调参/重生成"打磨项,非阻塞。
|
||||
@ -156,7 +156,7 @@ async function tap(cdp, xLogical, yLogical) {
|
||||
async function key(cdp, code, downMs) {
|
||||
const map = { ArrowLeft: 37, ArrowRight: 39, ArrowUp: 38, ArrowDown: 40, Space: 32, Enter: 13 };
|
||||
const keyCode = map[code] || 0;
|
||||
const k = code === 'Space' ? ' ' : (code && code.startsWith('Arrow')) ? undefined : code;
|
||||
const k = code === 'Space' ? ' ' : code; // 方向键 DOM code===key,直接传;原对 Arrow 发 undefined 致 event.key 空、键盘游戏读 e.key 拿空被忽略(压测批诊断纠)
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', code, key: k, windowsVirtualKeyCode: keyCode, nativeVirtualKeyCode: keyCode });
|
||||
await delay(downMs || 200);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', code, key: k, windowsVirtualKeyCode: keyCode, nativeVirtualKeyCode: keyCode });
|
||||
@ -200,9 +200,145 @@ function getPath(obj, p) {
|
||||
return String(p).split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
|
||||
}
|
||||
|
||||
/** 适配性真玩驱动:读 state 自适应出招,解「盲打固定坐标打不动技巧游戏」的假阴性。
|
||||
* type='paddle-intercept':每步读球 x、tap 到球正下方把挡板移过去接(真玩到清砖);到 gameover 即停。 */
|
||||
/** 适配性真玩驱动:读 state 自适应出招,解「盲打固定坐标打不动技巧游戏」的假阴性。按 driver.type 分发,缺省=paddle-intercept(向后兼容)。 */
|
||||
async function runDriver(cdp, driver, hashes) {
|
||||
if (driver.type === 'tap-targets') return runTapTargets(cdp, driver, hashes);
|
||||
if (driver.type === 'flap-to-gap') return runFlapToGap(cdp, driver, hashes);
|
||||
if (driver.type === 'seek-x') return runSeekX(cdp, driver, hashes);
|
||||
if (driver.type === 'tap-pairs') return runTapPairs(cdp, driver, hashes);
|
||||
if (driver.type === 'key-cycle') return runKeyCycle(cdp, driver, hashes);
|
||||
return runPaddleIntercept(cdp, driver, hashes);
|
||||
}
|
||||
|
||||
/** type='tap-targets':读 state().<targetsPath> 的离散可点目标 {x,y,occupied},每步点一个未占用目标,到 gameover 即停(交 latch 校验)。
|
||||
* 复用于井字棋/扫雷/翻牌/打地鼠/Simon/见缝插针/节奏等「点击离散目标」类游戏。 */
|
||||
async function runTapTargets(cdp, driver, hashes) {
|
||||
const targetsPath = driver.targetsPath || 'targets';
|
||||
const steps = driver.steps || 12;
|
||||
const stepMs = driver.stepMs != null ? driver.stepMs : 320;
|
||||
let drove = 0, cyc = 0;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const s = await readGameState(cdp);
|
||||
if (s && s.phase === 'gameover') break; // 终局即停(交 latch 校验)
|
||||
const targets = getPath(s, targetsPath);
|
||||
if (Array.isArray(targets) && targets.length) {
|
||||
let t = targets.find((g) => g && g.occupied !== true && typeof g.x === 'number');
|
||||
if (!t) { t = targets[cyc % targets.length]; } // 全占用/未导出 occupied → 按序轮点
|
||||
cyc++;
|
||||
if (t && typeof t.x === 'number') { await tap(cdp, Math.max(4, Math.min(386, t.x)), Math.max(4, Math.min(840, t.y))); drove++; }
|
||||
else { await delay(stepMs); }
|
||||
} else { await delay(stepMs); } // 没导出 targets → 空转(H/G 门会判红线)
|
||||
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
|
||||
await delay(stepMs);
|
||||
}
|
||||
return { drove, steps };
|
||||
}
|
||||
|
||||
/** type='flap-to-gap':读控制体 y 与目标缝隙中心 y——体在缝下方(y 更大=更低)就拍翅上升,否则任重力下落。驱动 Flappy 类穿缝。 */
|
||||
async function runFlapToGap(cdp, driver, hashes) {
|
||||
const yPath = driver.entityPath || 'bird.y';
|
||||
const gapPath = driver.gapPath || 'nextGap.centerY';
|
||||
const tapX = driver.tapX != null ? driver.tapX : 195;
|
||||
const steps = driver.steps || 60;
|
||||
const stepMs = driver.stepMs != null ? driver.stepMs : 110;
|
||||
const margin = driver.margin != null ? driver.margin : 16;
|
||||
let drove = 0;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const s = await readGameState(cdp);
|
||||
if (s && s.phase === 'gameover') break; // 已结束(撞死/通关)→停,交 latch 校验
|
||||
const by = getPath(s, yPath);
|
||||
let gy = getPath(s, gapPath);
|
||||
if (typeof gy !== 'number') gy = 422; // 无缝隙信息→维持中线高度
|
||||
if (typeof by === 'number' && by > gy + margin) { await tap(cdp, tapX, 420); drove++; } // 偏低→拍翅上升
|
||||
else { await delay(20); } // 偏高/已对齐→不拍,让重力下落
|
||||
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
|
||||
await delay(stepMs);
|
||||
}
|
||||
return { drove, steps };
|
||||
}
|
||||
|
||||
/** type='seek-x':读控制体 x 与下一平台 x——点屏左/右半把体水平移向平台,驱动 Doodle 类踩台上升。 */
|
||||
async function runSeekX(cdp, driver, hashes) {
|
||||
const xPath = driver.entityPath || 'bird.x';
|
||||
const targetPath = driver.targetPath || 'nextPlatform.x';
|
||||
const leftX = driver.leftX != null ? driver.leftX : 70;
|
||||
const rightX = driver.rightX != null ? driver.rightX : 320;
|
||||
const steps = driver.steps || 60;
|
||||
const stepMs = driver.stepMs != null ? driver.stepMs : 120;
|
||||
const dead = driver.deadzone != null ? driver.deadzone : 18;
|
||||
let drove = 0;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const s = await readGameState(cdp);
|
||||
if (s && s.phase === 'gameover') break; // 已结束(掉落)→停,交 latch 校验
|
||||
const bx = getPath(s, xPath);
|
||||
const tx = getPath(s, targetPath);
|
||||
if (typeof bx === 'number' && typeof tx === 'number') {
|
||||
if (tx < bx - dead) { await tap(cdp, leftX, 420); drove++; } // 平台在左→点左半左移
|
||||
else if (tx > bx + dead) { await tap(cdp, rightX, 420); drove++; } // 平台在右→点右半右移
|
||||
else { await delay(20); } // 已对齐→不动
|
||||
} else { await delay(stepMs); }
|
||||
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
|
||||
await delay(stepMs);
|
||||
}
|
||||
return { drove, steps };
|
||||
}
|
||||
|
||||
/** 触屏拖拽:touchStart(from) → 多帧 touchMove 插值到 to → touchEnd。受控面只给原始 pointer,swipe/拖拽手势须自合成(愤怒小鸟蓄力等)。 */
|
||||
async function drag(cdp, from, to, ms) {
|
||||
const f = from || { x: 195, y: 600 }, t = to || { x: 195, y: 700 };
|
||||
const n = 8, dur = ms || 400;
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: f.x, y: f.y }] });
|
||||
for (let i = 1; i <= n; i++) {
|
||||
const k = i / n, x = f.x + (t.x - f.x) * k, y = f.y + (t.y - f.y) * k;
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y }] });
|
||||
await delay(Math.max(8, Math.floor(dur / n)));
|
||||
}
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
||||
await delay(120);
|
||||
}
|
||||
|
||||
/** type='tap-pairs':读 state().<targetsPath> 离散目标,每步点【相邻两个】(点A→点B=交换),到 gameover 即停。
|
||||
* 驱动 Match-3 类「交换相邻消除」——盲态轮换交换,验便宜模型能否产可消三连(多半暴露需「读盘找可消」的更强 driver/swipe 门面)。 */
|
||||
async function runTapPairs(cdp, driver, hashes) {
|
||||
const targetsPath = driver.targetsPath || 'targets';
|
||||
const steps = driver.steps || 30;
|
||||
const stepMs = driver.stepMs != null ? driver.stepMs : 280;
|
||||
let drove = 0, k = 0;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const s = await readGameState(cdp);
|
||||
if (s && s.phase === 'gameover') break; // 终局即停(交 latch 校验)
|
||||
const targets = getPath(s, targetsPath);
|
||||
if (Array.isArray(targets) && targets.length >= 2) {
|
||||
const a = targets[k % targets.length], b = targets[(k + 1) % targets.length];
|
||||
k++;
|
||||
if (a && b && typeof a.x === 'number' && typeof b.x === 'number') {
|
||||
await tap(cdp, a.x, a.y); await delay(90); await tap(cdp, b.x, b.y); drove++;
|
||||
} else { await delay(stepMs); }
|
||||
} else { await delay(stepMs); }
|
||||
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
|
||||
await delay(stepMs);
|
||||
}
|
||||
return { drove, steps };
|
||||
}
|
||||
|
||||
/** type='key-cycle':循环按下 driver.keys 列表里的键,到 gameover 即停。驱动 Tetris/2048/Asteroids 等纯按键游戏(盲态铺满/合并/射击→暴露上限)。 */
|
||||
async function runKeyCycle(cdp, driver, hashes) {
|
||||
const keys = (driver.keys && driver.keys.length) ? driver.keys : ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'Space'];
|
||||
const steps = driver.steps || 40;
|
||||
const stepMs = driver.stepMs != null ? driver.stepMs : 200;
|
||||
let drove = 0;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const s = await readGameState(cdp);
|
||||
if (s && s.phase === 'gameover') break; // 终局即停(交 latch 校验)
|
||||
await key(cdp, keys[i % keys.length], driver.downMs || 60); drove++;
|
||||
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
|
||||
await delay(stepMs);
|
||||
}
|
||||
return { drove, steps };
|
||||
}
|
||||
|
||||
/** type='paddle-intercept':每步读球 x、tap 到球正下方把挡板移过去接(真玩到清砖);到 gameover 即停。 */
|
||||
async function runPaddleIntercept(cdp, driver, hashes) {
|
||||
const ballPath = driver.ballPath || 'ball.x';
|
||||
const paddleY = driver.paddleY != null ? driver.paddleY : 800;
|
||||
const steps = driver.steps || 60;
|
||||
@ -291,6 +427,7 @@ async function main() {
|
||||
for (const ev of (spec.inputs || [])) {
|
||||
if (ev.t === 'tap') await tap(cdp, ev.x, ev.y);
|
||||
else if (ev.t === 'key') await key(cdp, ev.code, ev.downMs);
|
||||
else if (ev.t === 'drag') await drag(cdp, ev.from, ev.to, ev.ms);
|
||||
else if (ev.t === 'wait') await delay(ev.ms || 200);
|
||||
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
|
||||
await delay(50);
|
||||
|
||||
34
game-runtime/games/_wg1-gen/angrybirds/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/angrybirds/generated-factory.js":{"bytes":13827,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/angrybirds/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/angrybirds/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/angrybirds/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/angrybirds/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164190},"games/_wg1-gen/angrybirds/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5364},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4161},"src/plugins/palette-post/impl.js":{"bytesInOutput":4216},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/angrybirds/generated-factory.js":{"bytesInOutput":5752}},"bytes":213523}}}
|
||||
77
game-runtime/games/_wg1-gen/angrybirds/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/angrybirds/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
game-runtime/games/_wg1-gen/angrybirds/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
121
game-runtime/games/_wg1-gen/angrybirds/evidence/verdict.json
Normal file
@ -0,0 +1,121 @@
|
||||
{
|
||||
"gameId": "angrybirds",
|
||||
"url": "http://localhost:4320/games/_wg1-gen/angrybirds/index.html",
|
||||
"ts": null,
|
||||
"guards": {
|
||||
"A_boot": {
|
||||
"pass": true
|
||||
},
|
||||
"C_frame": {
|
||||
"pass": true,
|
||||
"f0": 48,
|
||||
"f1": 78,
|
||||
"delta": 30
|
||||
},
|
||||
"I_control": {
|
||||
"pass": true,
|
||||
"skipped": "未声明 controlCheck(向后兼容)"
|
||||
},
|
||||
"D_render": {
|
||||
"pass": true,
|
||||
"bright": 34719,
|
||||
"maxCh": 255
|
||||
},
|
||||
"E_live": {
|
||||
"pass": true,
|
||||
"distinctStates": 10,
|
||||
"samples": 15
|
||||
},
|
||||
"B_uncaught": {
|
||||
"pass": true,
|
||||
"uncaught": []
|
||||
},
|
||||
"F_wiring": {
|
||||
"pass": true,
|
||||
"callCount": 18,
|
||||
"sample": [
|
||||
"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",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx"
|
||||
],
|
||||
"expected": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
]
|
||||
},
|
||||
"G_input": {
|
||||
"pass": true,
|
||||
"skipped": "无 tap/key 输入"
|
||||
},
|
||||
"H_progress": {
|
||||
"pass": false,
|
||||
"checks": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"before": 0,
|
||||
"after": 0,
|
||||
"pass": false,
|
||||
"why": "盲态固定拖拽发射→偶中目标得分;若固定角度打不中(需读目标算拖拽矢量)则不升=暴露拖拽瞄准缺口"
|
||||
}
|
||||
],
|
||||
"latch": {
|
||||
"pass": true,
|
||||
"dwelledMs": 600,
|
||||
"after": "gameover",
|
||||
"reason": ""
|
||||
},
|
||||
"state0": {
|
||||
"phase": "playing",
|
||||
"result": null,
|
||||
"score": 0,
|
||||
"birdsLeft": 4,
|
||||
"targets": [
|
||||
{
|
||||
"x": 290,
|
||||
"y": 560
|
||||
},
|
||||
{
|
||||
"x": 310,
|
||||
"y": 520
|
||||
},
|
||||
{
|
||||
"x": 280,
|
||||
"y": 480
|
||||
}
|
||||
]
|
||||
},
|
||||
"state1": {
|
||||
"phase": "gameover",
|
||||
"result": "lose",
|
||||
"score": 0,
|
||||
"birdsLeft": 0,
|
||||
"targets": [
|
||||
{
|
||||
"x": 290,
|
||||
"y": 560
|
||||
},
|
||||
{
|
||||
"x": 310,
|
||||
"y": 520
|
||||
},
|
||||
{
|
||||
"x": 280,
|
||||
"y": 480
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": false,
|
||||
"notes": []
|
||||
}
|
||||
493
game-runtime/games/_wg1-gen/angrybirds/generated-factory.js
Normal file
@ -0,0 +1,493 @@
|
||||
'use strict';
|
||||
/**
|
||||
* 简版愤怒小鸟 —— GameHostFactory 模块
|
||||
* 竖屏 390×844,弹弓发射,重力弹道,砖块结构 + 猪,5只鸟,拖拽瞄准蓄力,粒子+音效反馈。
|
||||
*/
|
||||
export default function createGame(opts) {
|
||||
const o = opts || {};
|
||||
let ctx = null;
|
||||
let seed = null;
|
||||
let now = 0;
|
||||
let t = 0;
|
||||
let subPointers = null; // 订阅句柄
|
||||
let subPointerup = null;
|
||||
let subPointermove = null;
|
||||
// 游戏状态
|
||||
let phase = 'booting'; // 'playing' | 'gameover'
|
||||
let result = '';
|
||||
let score = 0;
|
||||
let birdsLeft = 5;
|
||||
let birdIndex = 0; // 当前正在用第几只 (0~4)
|
||||
let bird = null; // 当前小鸟 { x, y, vx, vy, state: 'ready'|'flying'|'hit' }
|
||||
let bricks = [];
|
||||
let pigs = [];
|
||||
// 拖拽状态
|
||||
let dragging = false;
|
||||
let dragStart = null; // { x, y } 弹弓口
|
||||
let dragEnd = null; // 当前拖拽点
|
||||
// 特效
|
||||
let explosions = []; // { x, y, born, duration }
|
||||
let texts = []; // 飘字 +score
|
||||
|
||||
const GRAVITY = 980; // px/s^2
|
||||
const LAUNCH_FACTOR = 0.5; // 拖拽距离 -> 速度乘数
|
||||
const BIRD_RADIUS = 8;
|
||||
const PIG_RADIUS = 16;
|
||||
const SLING_X = 70;
|
||||
const SLING_Y = 580;
|
||||
|
||||
// ========== 内部辅助 ==========
|
||||
function resetGameData() {
|
||||
phase = 'playing';
|
||||
result = '';
|
||||
score = 0;
|
||||
birdsLeft = 5;
|
||||
birdIndex = 0;
|
||||
bird = null;
|
||||
bricks = [];
|
||||
pigs = [];
|
||||
dragging = false;
|
||||
dragStart = null;
|
||||
dragEnd = null;
|
||||
explosions = [];
|
||||
texts = [];
|
||||
loadLevel();
|
||||
spawnBird();
|
||||
}
|
||||
|
||||
function loadLevel() {
|
||||
// 固定布局:三个猪被砖块包围
|
||||
// 猪
|
||||
const pigPositions = [
|
||||
{ x: 290, y: 560 },
|
||||
{ x: 310, y: 520 },
|
||||
{ x: 280, y: 480 },
|
||||
];
|
||||
for (const p of pigPositions) {
|
||||
pigs.push({ x: p.x, y: p.y, r: PIG_RADIUS, active: true });
|
||||
}
|
||||
// 砖块 (矩形, 活跃)
|
||||
const brickDefs = [
|
||||
// 底部支撑
|
||||
{ x: 270, y: 576, w: 24, h: 10 },
|
||||
{ x: 306, y: 576, w: 24, h: 10 },
|
||||
// 左竖墙
|
||||
{ x: 262, y: 550, w: 10, h: 24 },
|
||||
// 右竖墙
|
||||
{ x: 322, y: 550, w: 10, h: 24 },
|
||||
// 中层
|
||||
{ x: 290, y: 546, w: 30, h: 10 },
|
||||
{ x: 290, y: 534, w: 10, h: 12 }, // 垂直分隔
|
||||
// 上层
|
||||
{ x: 270, y: 506, w: 24, h: 10 },
|
||||
{ x: 306, y: 506, w: 24, h: 10 },
|
||||
{ x: 280, y: 490, w: 10, h: 16 },
|
||||
{ x: 310, y: 490, w: 10, h: 16 },
|
||||
// 顶保护
|
||||
{ x: 280, y: 470, w: 30, h: 8 },
|
||||
];
|
||||
for (const b of brickDefs) {
|
||||
bricks.push({ x: b.x, y: b.y, w: b.w, h: b.h, active: true });
|
||||
}
|
||||
}
|
||||
|
||||
function spawnBird() {
|
||||
if (birdIndex >= 5) return; // 鸟用完了
|
||||
bird = {
|
||||
x: SLING_X,
|
||||
y: SLING_Y - 10,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
state: 'ready', // 'ready' | 'flying' | 'hit'
|
||||
alive: true,
|
||||
};
|
||||
birdIndex++;
|
||||
birdsLeft = 5 - birdIndex;
|
||||
}
|
||||
|
||||
/** 发射小鸟 */
|
||||
function launchBird(px, py) {
|
||||
if (!bird || bird.state !== 'ready') return;
|
||||
// 从弹弓口到拖拽点的反方向
|
||||
const dx = SLING_X - px;
|
||||
const dy = (SLING_Y - 10) - py;
|
||||
const vx = dx * LAUNCH_FACTOR;
|
||||
const vy = dy * LAUNCH_FACTOR;
|
||||
bird.vx = vx;
|
||||
bird.vy = vy;
|
||||
bird.state = 'flying';
|
||||
dragging = false;
|
||||
dragStart = null;
|
||||
dragEnd = null;
|
||||
// 发射粒子 + 音效
|
||||
spawnParticles(SLING_X, SLING_Y - 10, 30, { r: 1, g: 0.9, b: 0.3, a: 1 }, 0.6);
|
||||
playSfx([1, 0.08, 600, , 0.1, 0.5, 0.5, 1.2]);
|
||||
}
|
||||
|
||||
/** 碰撞检测与物理更新 */
|
||||
function updatePhysics(dt) {
|
||||
if (!bird || bird.state !== 'flying') return;
|
||||
const b = bird;
|
||||
// 重力
|
||||
b.vy += GRAVITY * dt;
|
||||
b.x += b.vx * dt;
|
||||
b.y += b.vy * dt;
|
||||
|
||||
// 出界检测
|
||||
if (b.y > 844 || b.x < -20 || b.x > 410 || b.y < -20) {
|
||||
hitObstacle(b.x, b.y);
|
||||
return;
|
||||
}
|
||||
|
||||
// 碰撞检测: 猪
|
||||
for (const pig of pigs) {
|
||||
if (!pig.active) continue;
|
||||
const dx = b.x - pig.x;
|
||||
const dy = b.y - pig.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < BIRD_RADIUS + pig.r) {
|
||||
pig.active = false;
|
||||
score += 100;
|
||||
// 爆炸粒子 + 音效
|
||||
spawnParticles(pig.x, pig.y, 40, { r: 1, g: 0.4, b: 0.1, a: 1 }, 0.8);
|
||||
playSfx([1, 0.9, 100, , 0.1, 0.4, 0.8, 1.5]);
|
||||
addFloatingText(pig.x, pig.y, '+100');
|
||||
hitObstacle(b.x, b.y);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 碰撞检测: 砖块
|
||||
for (const brick of bricks) {
|
||||
if (!brick.active) continue;
|
||||
if (b.x > brick.x && b.x < brick.x + brick.w &&
|
||||
b.y > brick.y && b.y < brick.y + brick.h) {
|
||||
brick.active = false;
|
||||
score += 10;
|
||||
spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, 20, { r: 0.8, g: 0.7, b: 0.5, a: 1 }, 0.5);
|
||||
playSfx([1, 0.2, 400, , 0.1, 0.3, 0.6, 1.3]);
|
||||
addFloatingText(brick.x + brick.w / 2, brick.y, '+10');
|
||||
hitObstacle(b.x, b.y);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果鸟落地上或太低,也视为落败
|
||||
if (b.y > 800) {
|
||||
hitObstacle(b.x, b.y);
|
||||
}
|
||||
}
|
||||
|
||||
/** 鸟撞击/消失处理 */
|
||||
function hitObstacle(x, y) {
|
||||
if (!bird || bird.state !== 'flying') return;
|
||||
bird.state = 'hit';
|
||||
bird.alive = false;
|
||||
// 碰撞粒子
|
||||
spawnParticles(x, y, 25, { r: 0.6, g: 0.8, b: 1, a: 1 }, 0.4);
|
||||
playSfx([1, 0.05, 300, , 0.1, 0.2, 0.5, 1.0]);
|
||||
// 延迟切换到下一只鸟
|
||||
setTimeout(() => {
|
||||
if (phase === 'gameover') return;
|
||||
if (allPigsCleared()) {
|
||||
phase = 'gameover';
|
||||
result = 'win';
|
||||
showResult();
|
||||
return;
|
||||
}
|
||||
if (birdIndex >= 5) {
|
||||
phase = 'gameover';
|
||||
result = 'lose';
|
||||
showResult();
|
||||
return;
|
||||
}
|
||||
spawnBird();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function allPigsCleared() {
|
||||
return pigs.every(p => !p.active);
|
||||
}
|
||||
|
||||
function showResult() {
|
||||
spawnParticles(195, 422, 50, { r: 1, g: 0.9, b: 0.2, a: 1 }, 1.5);
|
||||
playSfx([1, 0.9, 800, , 0.1, 0.6, 0.9, 1.8]);
|
||||
}
|
||||
|
||||
// ========== 输入处理 ==========
|
||||
function onPointerDown(e) {
|
||||
if (phase === 'gameover') {
|
||||
// 检查是否点击重玩区域(固定: 中间偏下 "Tap to Restart")
|
||||
if (e.x > 100 && e.x < 290 && e.y > 600 && e.y < 680) {
|
||||
resetGameData();
|
||||
phase = 'playing';
|
||||
// 清除任何残留特效
|
||||
explosions = [];
|
||||
texts = [];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!bird || bird.state !== 'ready') return;
|
||||
// 检测点击是否在弹弓附近
|
||||
const dx = e.x - SLING_X;
|
||||
const dy = e.y - (SLING_Y - 10);
|
||||
if (Math.sqrt(dx * dx + dy * dy) < 80) {
|
||||
dragging = true;
|
||||
dragStart = { x: SLING_X, y: SLING_Y - 10 };
|
||||
dragEnd = { x: e.x, y: e.y };
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerMove(e) {
|
||||
if (!dragging) return;
|
||||
dragEnd = { x: e.x, y: e.y };
|
||||
}
|
||||
|
||||
function onPointerUp(e) {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
if (dragEnd) {
|
||||
launchBird(dragEnd.x, dragEnd.y);
|
||||
}
|
||||
dragStart = null;
|
||||
dragEnd = null;
|
||||
}
|
||||
|
||||
// ========== 特效辅助 ==========
|
||||
function spawnParticles(x, y, count, colorStart, life) {
|
||||
const eng = ctx && ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x, y },
|
||||
count: count,
|
||||
speed: 100 + count * 2,
|
||||
particleTime: life || 0.6,
|
||||
colorStart: colorStart || { r: 1, g: 0.5, b: 0, a: 1 },
|
||||
colorEnd: { r: 1, g: 0.2, b: 0, a: 0 },
|
||||
sizeStart: 6,
|
||||
sizeEnd: 2,
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function playSfx(params) {
|
||||
const eng = ctx && ctx.getEngine();
|
||||
if (eng && eng.audio && eng.audio.synth) {
|
||||
try {
|
||||
eng.audio.synth.synthSfx(params);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function addFloatingText(x, y, text) {
|
||||
texts.push({ x, y, text, born: now, duration: 800 });
|
||||
}
|
||||
|
||||
// ========== 生命周期 ==========
|
||||
function init(boot) {
|
||||
ctx = boot.ctx;
|
||||
seed = boot.seed || 0;
|
||||
// 订阅输入
|
||||
subPointers = ctx.getInput().on('pointerdown', onPointerDown);
|
||||
subPointermove = ctx.getInput().on('pointermove', onPointerMove);
|
||||
subPointerup = ctx.getInput().on('pointerup', onPointerUp);
|
||||
// 先确保种子(可选)
|
||||
if (ctx.random) {
|
||||
ctx.random.reseed(seed);
|
||||
}
|
||||
// 初始化关卡
|
||||
resetGameData();
|
||||
phase = 'playing';
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
now = ctx.time.nowMs();
|
||||
t = now / 1000;
|
||||
if (phase !== 'playing') return;
|
||||
// 更新物理
|
||||
updatePhysics(dt);
|
||||
// 检查是否所有猪消灭 -> win
|
||||
if (allPigsCleared() && bird && bird.state !== 'flying') {
|
||||
phase = 'gameover';
|
||||
result = 'win';
|
||||
showResult();
|
||||
}
|
||||
// 鸟用完且没有飞行中的鸟 => lose
|
||||
if (birdIndex >= 5 && (!bird || bird.state === 'hit' || bird.state === 'ready')) {
|
||||
phase = 'gameover';
|
||||
result = 'lose';
|
||||
showResult();
|
||||
}
|
||||
// 清理过期文字
|
||||
texts = texts.filter(txt => now - txt.born < txt.duration);
|
||||
// 清理过期爆炸效果(本示例仅依靠引擎粒子,此处保留一个简单列表用于自绘圈)
|
||||
// 无额外自绘圈,而是依赖引擎粒子和HUD
|
||||
}
|
||||
|
||||
function render(g) {
|
||||
// 背景
|
||||
g.fillStyle = '#1a2b3c';
|
||||
g.fillRect(0, 0, 390, 844);
|
||||
|
||||
// 绘制砖块
|
||||
g.fillStyle = '#c8913e';
|
||||
for (const brick of bricks) {
|
||||
if (!brick.active) continue;
|
||||
g.fillRect(brick.x, brick.y, brick.w, brick.h);
|
||||
g.strokeStyle = '#5f3a1a';
|
||||
g.lineWidth = 1;
|
||||
g.strokeRect(brick.x, brick.y, brick.w, brick.h);
|
||||
}
|
||||
|
||||
// 绘制猪
|
||||
for (const pig of pigs) {
|
||||
if (!pig.active) continue;
|
||||
g.fillStyle = '#4CAF50';
|
||||
g.beginPath();
|
||||
g.arc(pig.x, pig.y, pig.r, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
g.strokeStyle = '#2e7d32';
|
||||
g.lineWidth = 2;
|
||||
g.stroke();
|
||||
// 鼻子
|
||||
g.fillStyle = '#e2a0a0';
|
||||
g.beginPath();
|
||||
g.arc(pig.x - 4, pig.y - 2, 3, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
}
|
||||
|
||||
// 绘制弹弓
|
||||
g.strokeStyle = '#5c3a1e';
|
||||
g.lineWidth = 6;
|
||||
g.beginPath();
|
||||
g.moveTo(60, 590);
|
||||
g.lineTo(70, 540);
|
||||
g.lineTo(80, 590);
|
||||
g.stroke();
|
||||
g.beginPath();
|
||||
g.moveTo(70, 540);
|
||||
g.lineTo(74, 600);
|
||||
g.stroke();
|
||||
|
||||
// 绘制小鸟(如果 ready 或 flying)
|
||||
if (bird && (bird.state === 'ready' || bird.state === 'flying')) {
|
||||
g.fillStyle = '#f44336';
|
||||
g.beginPath();
|
||||
g.arc(bird.x, bird.y, BIRD_RADIUS, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
g.fillStyle = '#ffcdd2';
|
||||
g.beginPath();
|
||||
g.arc(bird.x - 3, bird.y - 3, 3, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
// 嘴
|
||||
g.fillStyle = '#ff9800';
|
||||
g.beginPath();
|
||||
g.moveTo(bird.x + 6, bird.y - 1);
|
||||
g.lineTo(bird.x + 12, bird.y);
|
||||
g.lineTo(bird.x + 6, bird.y + 2);
|
||||
g.fill();
|
||||
}
|
||||
|
||||
// 绘制拖拽线
|
||||
if (dragging && dragStart && dragEnd) {
|
||||
g.strokeStyle = 'rgba(255,255,255,0.5)';
|
||||
g.lineWidth = 2;
|
||||
g.setLineDash([6, 4]);
|
||||
g.beginPath();
|
||||
g.moveTo(dragStart.x, dragStart.y);
|
||||
g.lineTo(dragEnd.x, dragEnd.y);
|
||||
g.stroke();
|
||||
g.setLineDash([]);
|
||||
// 箭头指示方向反向
|
||||
const dx = dragStart.x - dragEnd.x;
|
||||
const dy = dragStart.y - dragEnd.y;
|
||||
const len = Math.sqrt(dx*dx + dy*dy);
|
||||
if (len > 5) {
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const arrowLen = 12;
|
||||
g.fillStyle = '#fff';
|
||||
g.beginPath();
|
||||
g.moveTo(dragStart.x + dx/4, dragStart.y + dy/4);
|
||||
g.lineTo(dragStart.x + dx/4 - arrowLen * Math.cos(angle - 0.4), dragStart.y + dy/4 - arrowLen * Math.sin(angle - 0.4));
|
||||
g.lineTo(dragStart.x + dx/4 - arrowLen * Math.cos(angle + 0.4), dragStart.y + dy/4 - arrowLen * Math.sin(angle + 0.4));
|
||||
g.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// HUD 文字 (顶部)
|
||||
g.fillStyle = 'rgba(255,255,255,0.8)';
|
||||
g.font = 'bold 20px monospace';
|
||||
g.textAlign = 'left';
|
||||
g.fillText('🐦 ×' + birdsLeft, 10, 30);
|
||||
g.textAlign = 'right';
|
||||
g.fillText('🏆 ' + score, 380, 30);
|
||||
g.textAlign = 'center';
|
||||
g.fillStyle = '#9f9';
|
||||
g.font = '16px monospace';
|
||||
const status = phase === 'gameover' ? (result === 'win' ? '✨ 全灭!' : '💀 失败') : (bird && bird.state === 'flying' ? '🎯 飞行中' : '📐 拖拽蓄力');
|
||||
g.fillText(status, 195, 55);
|
||||
|
||||
// 飘字
|
||||
for (const txt of texts) {
|
||||
const age = (now - txt.born) / txt.duration;
|
||||
const alpha = 1 - age;
|
||||
g.globalAlpha = alpha;
|
||||
g.fillStyle = '#ffdd44';
|
||||
g.font = 'bold 18px monospace';
|
||||
g.textAlign = 'center';
|
||||
g.fillText(txt.text, txt.x, txt.y - 20 * age);
|
||||
g.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// 游戏结束重玩提示
|
||||
if (phase === 'gameover') {
|
||||
g.fillStyle = 'rgba(0,0,0,0.4)';
|
||||
g.fillRect(100, 600, 190, 60);
|
||||
g.strokeStyle = '#fff';
|
||||
g.lineWidth = 2;
|
||||
g.strokeRect(100, 600, 190, 60);
|
||||
g.fillStyle = '#fff';
|
||||
g.font = 'bold 22px monospace';
|
||||
g.textAlign = 'center';
|
||||
g.fillText('点击重玩', 195, 638);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
try { subPointers && subPointers.cancel(); } catch(_) {}
|
||||
try { subPointermove && subPointermove.cancel(); } catch(_) {}
|
||||
try { subPointerup && subPointerup.cancel(); } catch(_) {}
|
||||
subPointers = subPointermove = subPointerup = null;
|
||||
explosions = [];
|
||||
texts = [];
|
||||
bricks = [];
|
||||
pigs = [];
|
||||
bird = null;
|
||||
}
|
||||
|
||||
function _forensicsView() {
|
||||
return {
|
||||
seed: seed,
|
||||
state: () => {
|
||||
const remainingPigs = pigs.filter(p => p.active).map(p => ({ x: p.x, y: p.y }));
|
||||
return {
|
||||
phase: phase,
|
||||
result: (phase === 'gameover' ? result : null),
|
||||
score: score,
|
||||
birdsLeft: birdsLeft,
|
||||
targets: remainingPigs,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
update,
|
||||
render,
|
||||
destroy,
|
||||
_forensicsView
|
||||
};
|
||||
}
|
||||
89
game-runtime/games/_wg1-gen/angrybirds/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 uncaught[])。
|
||||
布局收编(对齐 ref 教训):canvas 顶在 (0,0) 满幅 390×844;状态行 absolute overlay + pointer-events:none,
|
||||
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 固定取证视口:390×844 移动竖屏基准;user-scalable=no 防缩放扰动像素哈希。 -->
|
||||
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
|
||||
<title>W-G1 generated game</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
|
||||
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
|
||||
#wrap { position: relative; width: 390px; margin: 0 auto; }
|
||||
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
|
||||
/* 状态行默认隐藏:boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
|
||||
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
|
||||
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
|
||||
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
|
||||
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
|
||||
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div id="status">booting…</div>
|
||||
<!-- 受控画布(opts.canvas 占位/输入兜底);real 引擎自建 #game-engine 叠加。 -->
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- esbuild 产物:iife 全局 __GameBundle(含 GameHostFactory + bootGameHost)。 -->
|
||||
<script src="./bundle.iife.js"></script>
|
||||
<script>
|
||||
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
|
||||
(function () {
|
||||
'use strict';
|
||||
var statusEl = document.getElementById('status');
|
||||
var canvas = document.getElementById('game');
|
||||
// 假绿守卫C·真接线探针:每次引擎能力被调,recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
|
||||
window.__engineCalls = [];
|
||||
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
|
||||
// 就绪/错误标记初值。
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = null;
|
||||
try {
|
||||
var G = window.__GameBundle;
|
||||
if (!G || typeof G.bootGameHost !== 'function') {
|
||||
throw new Error('__GameBundle.bootGameHost 未找到(bundle.iife.js 未正确加载或 globalName 不符)');
|
||||
}
|
||||
G.bootGameHost({
|
||||
canvas: canvas,
|
||||
statusEl: statusEl,
|
||||
recHook: recHook,
|
||||
// onReady:收集 internals(uncaught[] 等)供假绿守卫B 读。
|
||||
onReady: function (host, internals) {
|
||||
window.__genHost = host;
|
||||
window.__genInternals = internals || null;
|
||||
}
|
||||
})
|
||||
.then(function (host) {
|
||||
window.__genHost = window.__genHost || host;
|
||||
// 可测性别名:守卫H 经此读游戏可观测状态(host.state() 透传 game._forensicsView().state;游戏未实现则返回 {phase:'booting'})。
|
||||
// 宿主侧挂载,工厂仍零 window;与 ref 在 onReady 挂 __refForensics 同范式。
|
||||
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
|
||||
window.__genBooted = true; // CDP 探活标记(区分「脚本没加载」与「boot 抛错」)
|
||||
})
|
||||
.catch(function (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
|
||||
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
|
||||
});
|
||||
} catch (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
116
game-runtime/games/_wg1-gen/angrybirds/play-spec.json
Normal file
@ -0,0 +1,116 @@
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 700
|
||||
},
|
||||
{
|
||||
"t": "drag",
|
||||
"from": {
|
||||
"x": 80,
|
||||
"y": 600
|
||||
},
|
||||
"to": {
|
||||
"x": 44,
|
||||
"y": 668
|
||||
},
|
||||
"ms": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 1700
|
||||
},
|
||||
{
|
||||
"t": "drag",
|
||||
"from": {
|
||||
"x": 80,
|
||||
"y": 600
|
||||
},
|
||||
"to": {
|
||||
"x": 40,
|
||||
"y": 690
|
||||
},
|
||||
"ms": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 1700
|
||||
},
|
||||
{
|
||||
"t": "drag",
|
||||
"from": {
|
||||
"x": 80,
|
||||
"y": 600
|
||||
},
|
||||
"to": {
|
||||
"x": 50,
|
||||
"y": 655
|
||||
},
|
||||
"ms": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 1700
|
||||
},
|
||||
{
|
||||
"t": "drag",
|
||||
"from": {
|
||||
"x": 80,
|
||||
"y": 600
|
||||
},
|
||||
"to": {
|
||||
"x": 38,
|
||||
"y": 700
|
||||
},
|
||||
"ms": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 1700
|
||||
},
|
||||
{
|
||||
"t": "drag",
|
||||
"from": {
|
||||
"x": 80,
|
||||
"y": 600
|
||||
},
|
||||
"to": {
|
||||
"x": 54,
|
||||
"y": 648
|
||||
},
|
||||
"ms": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 1700
|
||||
},
|
||||
{
|
||||
"t": "drag",
|
||||
"from": {
|
||||
"x": 80,
|
||||
"y": 600
|
||||
},
|
||||
"to": {
|
||||
"x": 42,
|
||||
"y": 680
|
||||
},
|
||||
"ms": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 1700
|
||||
}
|
||||
],
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
],
|
||||
"assertAfterPlay": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"why": "盲态固定拖拽发射→偶中目标得分;若固定角度打不中(需读目标算拖拽矢量)则不升=暴露拖拽瞄准缺口"
|
||||
}
|
||||
],
|
||||
"expectLatch": true
|
||||
}
|
||||
34
game-runtime/games/_wg1-gen/asteroids/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":28239,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/asteroids/generated-factory.js":{"bytes":9418,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/asteroids/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/asteroids/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/asteroids/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/asteroids/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164254},"games/_wg1-gen/asteroids/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5505},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4216},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/asteroids/generated-factory.js":{"bytesInOutput":4408}},"bytes":212389}}}
|
||||
77
game-runtime/games/_wg1-gen/asteroids/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/asteroids/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
game-runtime/games/_wg1-gen/asteroids/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
106
game-runtime/games/_wg1-gen/asteroids/evidence/verdict.json
Normal file
@ -0,0 +1,106 @@
|
||||
{
|
||||
"gameId": "asteroids",
|
||||
"url": "http://localhost:4320/games/_wg1-gen/asteroids/index.html",
|
||||
"ts": null,
|
||||
"guards": {
|
||||
"A_boot": {
|
||||
"pass": true
|
||||
},
|
||||
"C_frame": {
|
||||
"pass": true,
|
||||
"f0": 48,
|
||||
"f1": 78,
|
||||
"delta": 30
|
||||
},
|
||||
"I_control": {
|
||||
"pass": true,
|
||||
"skipped": "未声明 controlCheck(向后兼容)"
|
||||
},
|
||||
"D_render": {
|
||||
"pass": true,
|
||||
"bright": 66491,
|
||||
"maxCh": 255
|
||||
},
|
||||
"E_live": {
|
||||
"pass": true,
|
||||
"distinctStates": 37,
|
||||
"samples": 37
|
||||
},
|
||||
"B_uncaught": {
|
||||
"pass": true,
|
||||
"uncaught": []
|
||||
},
|
||||
"F_wiring": {
|
||||
"pass": true,
|
||||
"callCount": 27,
|
||||
"sample": [
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter"
|
||||
],
|
||||
"expected": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
]
|
||||
},
|
||||
"G_input": {
|
||||
"pass": true,
|
||||
"inputHash": "d2d3585d",
|
||||
"controlHash": "f13eb61f",
|
||||
"atFrame": 901,
|
||||
"ctrlFrame": 905
|
||||
},
|
||||
"H_progress": {
|
||||
"pass": false,
|
||||
"checks": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"before": 0,
|
||||
"after": 0,
|
||||
"pass": false,
|
||||
"why": "循环旋转+推进+开火→击碎陨石得分上升;若高速子弹穿透(CCD缺)或打不中则不升=暴露 CCD 缺口"
|
||||
}
|
||||
],
|
||||
"latch": {
|
||||
"pass": true,
|
||||
"dwelledMs": 600,
|
||||
"after": "gameover",
|
||||
"reason": ""
|
||||
},
|
||||
"state0": {
|
||||
"phase": "playing",
|
||||
"result": null,
|
||||
"score": 0,
|
||||
"asteroidsRemaining": 5,
|
||||
"ship": {
|
||||
"x": 195,
|
||||
"y": 422,
|
||||
"angle": -1.5707963267948966
|
||||
}
|
||||
},
|
||||
"state1": {
|
||||
"phase": "gameover",
|
||||
"result": "lose",
|
||||
"score": 0,
|
||||
"asteroidsRemaining": 5,
|
||||
"ship": {
|
||||
"x": 152.8208837988882,
|
||||
"y": 301.88159315392096,
|
||||
"angle": -1.7707963267948967
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": false,
|
||||
"notes": []
|
||||
}
|
||||
354
game-runtime/games/_wg1-gen/asteroids/generated-factory.js
Normal file
@ -0,0 +1,354 @@
|
||||
'use strict';
|
||||
export default function createAsteroidsGame(opts) {
|
||||
const o = opts || {};
|
||||
let ctx = null;
|
||||
let subHandlers = [];
|
||||
|
||||
// game state
|
||||
let phase = 'booting';
|
||||
let result = null;
|
||||
let score = 0;
|
||||
let ship = { x:0, y:0, vx:0, vy:0, angle:0, radius:15 };
|
||||
let asteroids = [];
|
||||
let bullets = [];
|
||||
let lastFireTime = 0;
|
||||
let keyState = {};
|
||||
|
||||
const FIRE_COOLDOWN = 200;
|
||||
const BULLET_SPEED = 600;
|
||||
const BULLET_LIFETIME = 2000;
|
||||
const FRICTION = 0.99;
|
||||
const SHIP_RADIUS = 15;
|
||||
const BIG_ASTEROID_RADIUS = 30;
|
||||
const SMALL_ASTEROID_RADIUS = 15;
|
||||
const ASTEROID_SPEED_MIN = 40;
|
||||
const ASTEROID_SPEED_MAX = 80;
|
||||
const INITIAL_ASTEROIDS = 5;
|
||||
const THRUST = 200;
|
||||
const ROTATION_SPEED = 3.0;
|
||||
|
||||
// helpers
|
||||
function wrapPos(p) {
|
||||
if (p.x < 0) p.x += 390;
|
||||
else if (p.x > 390) p.x -= 390;
|
||||
if (p.y < 0) p.y += 844;
|
||||
else if (p.y > 844) p.y -= 844;
|
||||
}
|
||||
|
||||
function lineCircleIntersect(p1, p2, c, r) {
|
||||
const dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||
const fx = p1.x - c.x, fy = p1.y - c.y;
|
||||
const a = dx*dx + dy*dy;
|
||||
const b = 2*(fx*dx + fy*dy);
|
||||
const c_ = fx*fx + fy*fy - r*r;
|
||||
let disc = b*b - 4*a*c_;
|
||||
if (disc < 0) return false;
|
||||
disc = Math.sqrt(disc);
|
||||
const t1 = (-b - disc) / (2*a);
|
||||
const t2 = (-b + disc) / (2*a);
|
||||
return (t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1);
|
||||
}
|
||||
|
||||
function distance(ax, ay, bx, by) {
|
||||
const dx = ax - bx, dy = ay - by;
|
||||
return Math.sqrt(dx*dx + dy*dy);
|
||||
}
|
||||
|
||||
function createAsteroid(x, y, radius, vx, vy) {
|
||||
return { x, y, vx, vy, radius, isBig: radius >= BIG_ASTEROID_RADIUS };
|
||||
}
|
||||
|
||||
function spawnInitialAsteroids() {
|
||||
asteroids = [];
|
||||
const rand = ctx.random;
|
||||
for (let i = 0; i < INITIAL_ASTEROIDS; i++) {
|
||||
let x, y;
|
||||
do {
|
||||
x = rand.range(0, 390);
|
||||
y = rand.range(0, 844);
|
||||
} while (distance(x,y,195,422) < 100);
|
||||
const angle = rand.range(0, Math.PI*2);
|
||||
const speed = rand.range(ASTEROID_SPEED_MIN, ASTEROID_SPEED_MAX);
|
||||
const vx = Math.cos(angle)*speed;
|
||||
const vy = Math.sin(angle)*speed;
|
||||
asteroids.push(createAsteroid(x, y, BIG_ASTEROID_RADIUS, vx, vy));
|
||||
}
|
||||
}
|
||||
|
||||
function splitAsteroid(ast) {
|
||||
const rand = ctx.random;
|
||||
const angle1 = rand.range(0, Math.PI*2);
|
||||
const angle2 = angle1 + Math.PI*2/3;
|
||||
const speed = rand.range(ASTEROID_SPEED_MIN, ASTEROID_SPEED_MAX) * 1.2;
|
||||
const vx1 = Math.cos(angle1)*speed;
|
||||
const vy1 = Math.sin(angle1)*speed;
|
||||
const vx2 = Math.cos(angle2)*speed;
|
||||
const vy2 = Math.sin(angle2)*speed;
|
||||
const a1 = createAsteroid(ast.x, ast.y, SMALL_ASTEROID_RADIUS, vx1, vy1);
|
||||
const a2 = createAsteroid(ast.x, ast.y, SMALL_ASTEROID_RADIUS, vx2, vy2);
|
||||
asteroids.push(a1, a2);
|
||||
}
|
||||
|
||||
function spawnParticles(x, y, colorStart, colorEnd, count) {
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x, y },
|
||||
count: count || 20,
|
||||
speed: 80,
|
||||
particleTime: 0.4,
|
||||
sizeStart: 6,
|
||||
sizeEnd: 2,
|
||||
colorStart: colorStart || { r:1, g:1, b:1, a:1 },
|
||||
colorEnd: colorEnd || { r:0.5, g:0.5, b:0.5, a:0 }
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function playSfx(params) {
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.audio && eng.audio.synth) {
|
||||
try { eng.audio.synth.synthSfx(params); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function fireBullet() {
|
||||
const now = ctx.time.nowMs();
|
||||
if (now - lastFireTime < FIRE_COOLDOWN) return;
|
||||
lastFireTime = now;
|
||||
const bx = ship.x + Math.cos(ship.angle) * (ship.radius + 5);
|
||||
const by = ship.y + Math.sin(ship.angle) * (ship.radius + 5);
|
||||
const bvx = Math.cos(ship.angle) * BULLET_SPEED + ship.vx;
|
||||
const bvy = Math.sin(ship.angle) * BULLET_SPEED + ship.vy;
|
||||
bullets.push({ x: bx, y: by, vx: bvx, vy: bvy, born: now, radius: 3 });
|
||||
playSfx([1,0.02,600,,,0.1,,0.6]);
|
||||
}
|
||||
|
||||
function resetGameState() {
|
||||
ship.x = 195;
|
||||
ship.y = 422;
|
||||
ship.vx = 0;
|
||||
ship.vy = 0;
|
||||
ship.angle = -Math.PI/2;
|
||||
spawnInitialAsteroids();
|
||||
bullets = [];
|
||||
score = 0;
|
||||
lastFireTime = 0;
|
||||
keyState = {};
|
||||
}
|
||||
|
||||
function resetGame() {
|
||||
resetGameState();
|
||||
phase = 'playing';
|
||||
result = null;
|
||||
}
|
||||
|
||||
// lifecycle
|
||||
function init(boot) {
|
||||
ctx = boot.ctx;
|
||||
const input = ctx.getInput();
|
||||
|
||||
const keydownSub = input.on('keydown', (e) => {
|
||||
if (e.key) keyState[e.key] = true;
|
||||
});
|
||||
subHandlers.push(keydownSub);
|
||||
|
||||
const keyupSub = input.on('keyup', (e) => {
|
||||
if (e.key) keyState[e.key] = false;
|
||||
});
|
||||
subHandlers.push(keyupSub);
|
||||
|
||||
const pointerSub = input.on('pointerdown', () => {
|
||||
if (phase === 'gameover') resetGame();
|
||||
});
|
||||
subHandlers.push(pointerSub);
|
||||
|
||||
resetGameState();
|
||||
phase = 'playing';
|
||||
result = null;
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
if (phase === 'gameover') return;
|
||||
const now = ctx.time.nowMs();
|
||||
|
||||
// ship rotation
|
||||
if (keyState['ArrowLeft']) ship.angle -= ROTATION_SPEED * dt;
|
||||
if (keyState['ArrowRight']) ship.angle += ROTATION_SPEED * dt;
|
||||
|
||||
// thrust
|
||||
if (keyState['ArrowUp']) {
|
||||
ship.vx += Math.cos(ship.angle) * THRUST * dt;
|
||||
ship.vy += Math.sin(ship.angle) * THRUST * dt;
|
||||
spawnParticles(
|
||||
ship.x - Math.cos(ship.angle)*20,
|
||||
ship.y - Math.sin(ship.angle)*20,
|
||||
{ r:1, g:0.8, b:0.2, a:1 },
|
||||
{ r:1, g:0.4, b:0, a:0 },
|
||||
5
|
||||
);
|
||||
}
|
||||
|
||||
// fire
|
||||
if (keyState['Space']) fireBullet();
|
||||
|
||||
// friction
|
||||
ship.vx *= FRICTION;
|
||||
ship.vy *= FRICTION;
|
||||
|
||||
// move ship
|
||||
ship.x += ship.vx * dt;
|
||||
ship.y += ship.vy * dt;
|
||||
wrapPos(ship);
|
||||
|
||||
// move asteroids
|
||||
for (const a of asteroids) {
|
||||
a.x += a.vx * dt;
|
||||
a.y += a.vy * dt;
|
||||
wrapPos(a);
|
||||
}
|
||||
|
||||
// update bullets & collision with asteroids (CCD)
|
||||
for (let i = bullets.length-1; i >= 0; i--) {
|
||||
const b = bullets[i];
|
||||
const oldX = b.x, oldY = b.y;
|
||||
b.x += b.vx * dt;
|
||||
b.y += b.vy * dt;
|
||||
|
||||
// lifespan
|
||||
if (now - b.born > BULLET_LIFETIME) {
|
||||
bullets.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// off-screen remove (no wrap for simplicity)
|
||||
if (b.x < 0 || b.x > 390 || b.y < 0 || b.y > 844) {
|
||||
bullets.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
let hit = false;
|
||||
for (let j = asteroids.length-1; j >= 0; j--) {
|
||||
const a = asteroids[j];
|
||||
if (lineCircleIntersect({x:oldX, y:oldY}, {x:b.x, y:b.y}, {x:a.x, y:a.y}, a.radius)) {
|
||||
hit = true;
|
||||
score += 1;
|
||||
spawnParticles(b.x, b.y, {r:1,g:0.9,b:0.5,a:1}, {r:1,g:0.3,b:0,a:0}, 30);
|
||||
playSfx([1,0.1,880,,,0.2,,0.4]);
|
||||
if (a.isBig) splitAsteroid(a);
|
||||
asteroids.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hit) {
|
||||
bullets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ship vs asteroids
|
||||
for (const a of asteroids) {
|
||||
const dx = ship.x - a.x, dy = ship.y - a.y;
|
||||
if (dx*dx + dy*dy < (ship.radius + a.radius) * (ship.radius + a.radius)) {
|
||||
phase = 'gameover';
|
||||
result = 'lose';
|
||||
spawnParticles(ship.x, ship.y, {r:1,g:0.2,b:0.2,a:1}, {r:0.5,g:0,b:0,a:0}, 40);
|
||||
playSfx([1,0.2,100,,,0.5,,1.5]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// win condition
|
||||
if (asteroids.length === 0) {
|
||||
phase = 'gameover';
|
||||
result = 'win';
|
||||
}
|
||||
}
|
||||
|
||||
function render(g) {
|
||||
// background
|
||||
g.fillStyle = '#0a0a1a';
|
||||
g.fillRect(0, 0, 390, 844);
|
||||
|
||||
// asteroids
|
||||
for (const a of asteroids) {
|
||||
g.fillStyle = a.isBig ? '#8b5e3c' : '#a0744a';
|
||||
g.beginPath();
|
||||
g.arc(a.x, a.y, a.radius, 0, Math.PI*2);
|
||||
g.fill();
|
||||
g.strokeStyle = '#d4a76a';
|
||||
g.lineWidth = 2;
|
||||
g.stroke();
|
||||
}
|
||||
|
||||
// bullets
|
||||
for (const b of bullets) {
|
||||
g.fillStyle = '#ffff00';
|
||||
g.beginPath();
|
||||
g.arc(b.x, b.y, b.radius, 0, Math.PI*2);
|
||||
g.fill();
|
||||
}
|
||||
|
||||
// ship
|
||||
g.save();
|
||||
g.translate(ship.x, ship.y);
|
||||
g.rotate(ship.angle);
|
||||
g.fillStyle = phase === 'gameover' ? '#ff4444' : '#44aaff';
|
||||
g.beginPath();
|
||||
g.moveTo(20, 0);
|
||||
g.lineTo(-15, -12);
|
||||
g.lineTo(-10, 0);
|
||||
g.lineTo(-15, 12);
|
||||
g.closePath();
|
||||
g.fill();
|
||||
g.strokeStyle = '#ffffff';
|
||||
g.lineWidth = 1.5;
|
||||
g.stroke();
|
||||
g.restore();
|
||||
|
||||
// HUD
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = 'bold 16px monospace';
|
||||
g.fillText('Score: ' + score, 10, 30);
|
||||
g.fillText('Asteroids: ' + asteroids.length, 10, 50);
|
||||
g.fillText('Phase: ' + phase, 10, 70);
|
||||
|
||||
if (phase === 'gameover') {
|
||||
g.fillStyle = result === 'win' ? '#00ff00' : '#ff0000';
|
||||
g.font = 'bold 28px monospace';
|
||||
g.textAlign = 'center';
|
||||
g.fillText(result === 'win' ? 'YOU WIN!' : 'GAME OVER', 195, 400);
|
||||
g.font = '16px monospace';
|
||||
g.fillStyle = '#ffffff';
|
||||
g.fillText('Tap to restart', 195, 430);
|
||||
g.textAlign = 'start';
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
for (const sub of subHandlers) {
|
||||
try { sub.cancel(); } catch(_) {}
|
||||
}
|
||||
subHandlers = [];
|
||||
asteroids = [];
|
||||
bullets = [];
|
||||
phase = 'booting';
|
||||
result = null;
|
||||
}
|
||||
|
||||
function _forensicsView() {
|
||||
return {
|
||||
seed: ctx && ctx.random ? ctx.random.seed : null,
|
||||
state: () => ({
|
||||
phase,
|
||||
result,
|
||||
score,
|
||||
asteroidsRemaining: asteroids.length,
|
||||
ship: { x: ship.x, y: ship.y, angle: ship.angle }
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return { init, update, render, destroy, _forensicsView };
|
||||
}
|
||||
89
game-runtime/games/_wg1-gen/asteroids/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 uncaught[])。
|
||||
布局收编(对齐 ref 教训):canvas 顶在 (0,0) 满幅 390×844;状态行 absolute overlay + pointer-events:none,
|
||||
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 固定取证视口:390×844 移动竖屏基准;user-scalable=no 防缩放扰动像素哈希。 -->
|
||||
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
|
||||
<title>W-G1 generated game</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
|
||||
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
|
||||
#wrap { position: relative; width: 390px; margin: 0 auto; }
|
||||
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
|
||||
/* 状态行默认隐藏:boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
|
||||
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
|
||||
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
|
||||
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
|
||||
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
|
||||
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div id="status">booting…</div>
|
||||
<!-- 受控画布(opts.canvas 占位/输入兜底);real 引擎自建 #game-engine 叠加。 -->
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- esbuild 产物:iife 全局 __GameBundle(含 GameHostFactory + bootGameHost)。 -->
|
||||
<script src="./bundle.iife.js"></script>
|
||||
<script>
|
||||
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
|
||||
(function () {
|
||||
'use strict';
|
||||
var statusEl = document.getElementById('status');
|
||||
var canvas = document.getElementById('game');
|
||||
// 假绿守卫C·真接线探针:每次引擎能力被调,recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
|
||||
window.__engineCalls = [];
|
||||
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
|
||||
// 就绪/错误标记初值。
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = null;
|
||||
try {
|
||||
var G = window.__GameBundle;
|
||||
if (!G || typeof G.bootGameHost !== 'function') {
|
||||
throw new Error('__GameBundle.bootGameHost 未找到(bundle.iife.js 未正确加载或 globalName 不符)');
|
||||
}
|
||||
G.bootGameHost({
|
||||
canvas: canvas,
|
||||
statusEl: statusEl,
|
||||
recHook: recHook,
|
||||
// onReady:收集 internals(uncaught[] 等)供假绿守卫B 读。
|
||||
onReady: function (host, internals) {
|
||||
window.__genHost = host;
|
||||
window.__genInternals = internals || null;
|
||||
}
|
||||
})
|
||||
.then(function (host) {
|
||||
window.__genHost = window.__genHost || host;
|
||||
// 可测性别名:守卫H 经此读游戏可观测状态(host.state() 透传 game._forensicsView().state;游戏未实现则返回 {phase:'booting'})。
|
||||
// 宿主侧挂载,工厂仍零 window;与 ref 在 onReady 挂 __refForensics 同范式。
|
||||
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
|
||||
window.__genBooted = true; // CDP 探活标记(区分「脚本没加载」与「boot 抛错」)
|
||||
})
|
||||
.catch(function (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
|
||||
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
|
||||
});
|
||||
} catch (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
27
game-runtime/games/_wg1-gen/asteroids/play-spec.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"driver": {
|
||||
"type": "key-cycle",
|
||||
"keys": [
|
||||
"ArrowLeft",
|
||||
"ArrowUp",
|
||||
"Space",
|
||||
"ArrowRight",
|
||||
"Space",
|
||||
"Space"
|
||||
],
|
||||
"steps": 50,
|
||||
"stepMs": 200
|
||||
},
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
],
|
||||
"assertAfterPlay": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"why": "循环旋转+推进+开火→击碎陨石得分上升;若高速子弹穿透(CCD缺)或打不中则不升=暴露 CCD 缺口"
|
||||
}
|
||||
],
|
||||
"expectLatch": true
|
||||
}
|
||||
34
game-runtime/games/_wg1-gen/breakout/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/breakout/generated-factory.js":{"bytes":10687,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/breakout/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/breakout/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/breakout/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/breakout/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164172},"games/_wg1-gen/breakout/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5359},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4218},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/breakout/generated-factory.js":{"bytesInOutput":4386}},"bytes":212141}}}
|
||||
77
game-runtime/games/_wg1-gen/breakout/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/breakout/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
game-runtime/games/_wg1-gen/breakout/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
138
game-runtime/games/_wg1-gen/breakout/evidence/verdict.json
Normal file
@ -0,0 +1,138 @@
|
||||
{
|
||||
"gameId": "breakout",
|
||||
"url": "http://localhost:4320/games/_wg1-gen/breakout/index.html",
|
||||
"ts": null,
|
||||
"guards": {
|
||||
"A_boot": {
|
||||
"pass": true
|
||||
},
|
||||
"C_frame": {
|
||||
"pass": true,
|
||||
"f0": 50,
|
||||
"f1": 80,
|
||||
"delta": 30
|
||||
},
|
||||
"I_control": {
|
||||
"pass": true,
|
||||
"paddlePath": "paddle.x",
|
||||
"results": [
|
||||
{
|
||||
"tapX": 50,
|
||||
"before": 195,
|
||||
"after": 50.00000000001471,
|
||||
"moved": -144.99999999998528,
|
||||
"distToTarget": 1.4708234630234074e-11,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"tapX": 340,
|
||||
"before": 50.00000000001471,
|
||||
"after": 338.5467236151453,
|
||||
"moved": 288.5467236151306,
|
||||
"distToTarget": 1.4532763848546892,
|
||||
"pass": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"D_render": {
|
||||
"pass": true,
|
||||
"bright": 154309,
|
||||
"maxCh": 255
|
||||
},
|
||||
"E_live": {
|
||||
"pass": true,
|
||||
"distinctStates": 24,
|
||||
"samples": 24
|
||||
},
|
||||
"B_uncaught": {
|
||||
"pass": true,
|
||||
"uncaught": []
|
||||
},
|
||||
"F_wiring": {
|
||||
"pass": true,
|
||||
"callCount": 21,
|
||||
"sample": [
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx"
|
||||
],
|
||||
"expected": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
]
|
||||
},
|
||||
"G_input": {
|
||||
"pass": true,
|
||||
"inputHash": "0fd7f8bc",
|
||||
"controlHash": "7dcc3da8",
|
||||
"atFrame": 836,
|
||||
"ctrlFrame": 838
|
||||
},
|
||||
"H_progress": {
|
||||
"pass": true,
|
||||
"checks": [
|
||||
{
|
||||
"path": "remaining",
|
||||
"op": "decreased",
|
||||
"before": 30,
|
||||
"after": 29,
|
||||
"pass": true,
|
||||
"why": "适配性真玩(挡板自动接球)下击碎砖块→剩余下降;接不住/碰撞失效则不降"
|
||||
}
|
||||
],
|
||||
"latch": {
|
||||
"pass": true,
|
||||
"dwelledMs": 600,
|
||||
"after": "gameover",
|
||||
"reason": ""
|
||||
},
|
||||
"state0": {
|
||||
"phase": "playing",
|
||||
"score": 0,
|
||||
"remaining": 30,
|
||||
"progress": 0,
|
||||
"gameoverReason": "",
|
||||
"ball": {
|
||||
"x": 361.6493303574015,
|
||||
"y": 530.5824887640108,
|
||||
"vx": 199.9791964288825,
|
||||
"vy": -311.3010134831855
|
||||
},
|
||||
"paddle": {
|
||||
"x": 195,
|
||||
"y": 810,
|
||||
"w": 70
|
||||
}
|
||||
},
|
||||
"state1": {
|
||||
"phase": "gameover",
|
||||
"score": 1,
|
||||
"remaining": 29,
|
||||
"progress": 0.033333333333333326,
|
||||
"gameoverReason": "ball_lost",
|
||||
"ball": {
|
||||
"x": 352.5509849051085,
|
||||
"y": 839.7168663631734,
|
||||
"vx": 272.80340913561224,
|
||||
"vy": 249.9565961601889
|
||||
},
|
||||
"paddle": {
|
||||
"x": 288.28229534102184,
|
||||
"y": 810,
|
||||
"w": 70
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": true,
|
||||
"notes": []
|
||||
}
|
||||
385
game-runtime/games/_wg1-gen/breakout/generated-factory.js
Normal file
@ -0,0 +1,385 @@
|
||||
'use strict';
|
||||
|
||||
export default function createGame(opts) {
|
||||
const o = opts || {};
|
||||
let ctx = null;
|
||||
let boot = null;
|
||||
let gameState = null; // { phase, score, remaining, ball, paddle, bricks, targetX, ... }
|
||||
let inputSubs = [];
|
||||
let nowMs = 0;
|
||||
|
||||
// 砖块配置
|
||||
const COLS = 6;
|
||||
const ROWS = 5;
|
||||
const BRICK_W = 62;
|
||||
const BRICK_H = 20;
|
||||
const BRICK_GAP = 3;
|
||||
const BOARD_Y = 50;
|
||||
const PADDLE_W = 70;
|
||||
const PADDLE_H = 16;
|
||||
const PADDLE_Y = 810;
|
||||
const BALL_R = 7;
|
||||
const BALL_SPEED = 370; // px/s
|
||||
const MAX_DEFLECT_ANGLE = 70 * Math.PI / 180;
|
||||
|
||||
function init(bootData) {
|
||||
boot = bootData;
|
||||
ctx = boot.ctx;
|
||||
nowMs = ctx.time.nowMs();
|
||||
|
||||
// 初始化砖块
|
||||
const bricks = [];
|
||||
for (let row = 0; row < ROWS; row++) {
|
||||
for (let col = 0; col < COLS; col++) {
|
||||
const health = ctx.random.range(0, 1) > 0.7 ? 2 : 1; // 30% 需要2次
|
||||
bricks.push({
|
||||
x: col * (BRICK_W + BRICK_GAP) + 4,
|
||||
y: BOARD_Y + row * (BRICK_H + BRICK_GAP),
|
||||
w: BRICK_W,
|
||||
h: BRICK_H,
|
||||
health: health,
|
||||
maxHealth: health
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 挡板(坐标为中心)
|
||||
const paddle = {
|
||||
x: 195,
|
||||
y: PADDLE_Y,
|
||||
w: PADDLE_W,
|
||||
h: PADDLE_H
|
||||
};
|
||||
|
||||
// 球(中心)
|
||||
const ball = {
|
||||
x: 195,
|
||||
y: PADDLE_Y - 20,
|
||||
vx: 0,
|
||||
vy: 0
|
||||
};
|
||||
|
||||
// 初始球方向随机(向上偏)
|
||||
const angle = ctx.random.range(-45, 45) * Math.PI / 180;
|
||||
ball.vx = BALL_SPEED * Math.sin(angle);
|
||||
ball.vy = -BALL_SPEED * Math.cos(angle);
|
||||
|
||||
gameState = {
|
||||
phase: 'playing',
|
||||
score: 0,
|
||||
remaining: bricks.length,
|
||||
bricks: bricks,
|
||||
paddle: paddle,
|
||||
ball: ball,
|
||||
targetX: 195,
|
||||
tapToRestart: false,
|
||||
gameoverReason: ''
|
||||
};
|
||||
|
||||
// 输入订阅
|
||||
const input = ctx.getInput();
|
||||
if (input) {
|
||||
const subDown = input.on('pointerdown', onPointer);
|
||||
const subMove = input.on('pointermove', onPointer);
|
||||
const subKey = input.on('keydown', onKey);
|
||||
inputSubs = [subDown, subMove, subKey];
|
||||
}
|
||||
|
||||
// 尝试首次触发引擎能力(常驻粒子)
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: 195, y: 300 },
|
||||
emitRate: 15,
|
||||
emitTime: 0,
|
||||
particleTime: 1.0,
|
||||
speed: 60,
|
||||
colorStart: { r: 1, g: 0.8, b: 0.2, a: 1 },
|
||||
colorEnd: { r: 1, g: 0.2, b: 0, a: 0 }
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function onPointer(e) {
|
||||
if (!gameState) return;
|
||||
if (e.type === 'pointerdown') {
|
||||
// 检查是否gameover后点击重玩
|
||||
if (gameState.phase === 'gameover') {
|
||||
restartGame();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 更新目标位置(逻辑像素坐标)
|
||||
gameState.targetX = Math.max(0, Math.min(390, e.x || 0));
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
if (!gameState) return;
|
||||
if (e.type === 'keydown' && e.key === ' ') {
|
||||
if (gameState.phase === 'gameover') {
|
||||
restartGame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
// 销毁当前订阅
|
||||
destroySubs();
|
||||
// 重新初始化
|
||||
init(boot);
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
// 每个逻辑步刷新时间
|
||||
nowMs = ctx.time.nowMs();
|
||||
|
||||
if (!gameState || gameState.phase !== 'playing') return;
|
||||
|
||||
const paddle = gameState.paddle;
|
||||
const ball = gameState.ball;
|
||||
const bricks = gameState.bricks;
|
||||
|
||||
// 挡板平滑跟随目标
|
||||
paddle.x += (gameState.targetX - paddle.x) * 0.25;
|
||||
paddle.x = Math.max(paddle.w / 2, Math.min(390 - paddle.w / 2, paddle.x));
|
||||
|
||||
// 球移动
|
||||
ball.x += ball.vx * dt;
|
||||
ball.y += ball.vy * dt;
|
||||
|
||||
// 左右墙反弹(考虑球半径)
|
||||
if (ball.x - BALL_R < 0) {
|
||||
ball.x = BALL_R;
|
||||
ball.vx = -ball.vx;
|
||||
playHitSound();
|
||||
}
|
||||
if (ball.x + BALL_R > 390) {
|
||||
ball.x = 390 - BALL_R;
|
||||
ball.vx = -ball.vx;
|
||||
playHitSound();
|
||||
}
|
||||
|
||||
// 顶墙反弹
|
||||
if (ball.y - BALL_R < 0) {
|
||||
ball.y = BALL_R;
|
||||
ball.vy = -ball.vy;
|
||||
playHitSound();
|
||||
}
|
||||
|
||||
// 底部失败
|
||||
if (ball.y + BALL_R > 844) {
|
||||
gameState.phase = 'gameover';
|
||||
gameState.gameoverReason = 'ball_lost';
|
||||
// 爆炸粒子
|
||||
spawnBurst(ball.x, ball.y, { r: 1, g: 0.2, b: 0.2, a: 1 });
|
||||
playHitSound();
|
||||
return;
|
||||
}
|
||||
|
||||
// 砖块碰撞(按顺序,每块一次)
|
||||
for (let i = bricks.length - 1; i >= 0; i--) {
|
||||
const b = bricks[i];
|
||||
// 检测矩形与圆的碰撞(简化为AABB + 圆球)
|
||||
const closestX = Math.max(b.x, Math.min(ball.x, b.x + b.w));
|
||||
const closestY = Math.max(b.y, Math.min(ball.y, b.y + b.h));
|
||||
const dx = ball.x - closestX;
|
||||
const dy = ball.y - closestY;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < BALL_R) {
|
||||
// 确定碰撞方向(主要从哪面进入)
|
||||
const overlapX = (b.w / 2 + BALL_R) - Math.abs(ball.x - (b.x + b.w/2));
|
||||
const overlapY = (b.h / 2 + BALL_R) - Math.abs(ball.y - (b.y + b.h/2));
|
||||
if (overlapX < overlapY) {
|
||||
ball.vx = -ball.vx;
|
||||
} else {
|
||||
ball.vy = -ball.vy;
|
||||
}
|
||||
// 将球推出砖块
|
||||
if (overlapX < overlapY) {
|
||||
if (ball.x < b.x + b.w/2) ball.x = b.x - BALL_R;
|
||||
else ball.x = b.x + b.w + BALL_R;
|
||||
} else {
|
||||
if (ball.y < b.y + b.h/2) ball.y = b.y - BALL_R;
|
||||
else ball.y = b.y + b.h + BALL_R;
|
||||
}
|
||||
|
||||
// 砖块耐久减1
|
||||
b.health--;
|
||||
if (b.health <= 0) {
|
||||
bricks.splice(i, 1);
|
||||
gameState.score++;
|
||||
gameState.remaining = bricks.length;
|
||||
// 粒子爆发
|
||||
spawnBurst(b.x + b.w/2, b.y + b.h/2, { r: 0.2, g: 0.9, b: 1, a: 1 });
|
||||
// 音效
|
||||
playScoreSound();
|
||||
} else {
|
||||
// 击中但未碎,给个小型粒子
|
||||
spawnBurst(b.x + b.w/2, b.y + b.h/2, { r: 0.9, g: 0.6, b: 0.2, a: 0.8 }, 8);
|
||||
playHitSound();
|
||||
}
|
||||
break; // 每帧只处理一个砖块碰撞,避免多个同时
|
||||
}
|
||||
}
|
||||
|
||||
// 挡板碰撞
|
||||
const paddleLeft = paddle.x - paddle.w/2;
|
||||
const paddleRight = paddle.x + paddle.w/2;
|
||||
const paddleTop = paddle.y - paddle.h/2;
|
||||
const paddleBottom = paddle.y + paddle.h/2;
|
||||
if (ball.y + BALL_R >= paddleTop && ball.y - BALL_R <= paddleBottom &&
|
||||
ball.x + BALL_R >= paddleLeft && ball.x - BALL_R <= paddleRight) {
|
||||
// 击中挡板,计算反弹角度
|
||||
const offset = (ball.x - paddle.x) / (paddle.w/2);
|
||||
const clampedOffset = Math.max(-1, Math.min(1, offset)); // -1~1
|
||||
const angle = clampedOffset * MAX_DEFLECT_ANGLE;
|
||||
ball.vx = BALL_SPEED * Math.sin(angle);
|
||||
ball.vy = -BALL_SPEED * Math.cos(angle);
|
||||
// 将球置于挡板上沿
|
||||
ball.y = paddleTop - BALL_R;
|
||||
playHitSound();
|
||||
// 粒子
|
||||
spawnBurst(ball.x, ball.y, { r: 0.3, g: 0.8, b: 0.3, a: 1 }, 12);
|
||||
}
|
||||
|
||||
// 检查通关
|
||||
if (gameState.bricks.length === 0) {
|
||||
gameState.phase = 'gameover';
|
||||
gameState.gameoverReason = 'cleared_all';
|
||||
// 胜利粒子
|
||||
spawnBurst(195, 400, { r: 1, g: 1, b: 0.2, a: 1 }, 30);
|
||||
playScoreSound();
|
||||
}
|
||||
}
|
||||
|
||||
function spawnBurst(x, y, color, count) {
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x, y },
|
||||
count: count || 22,
|
||||
speed: 80,
|
||||
particleTime: 0.5,
|
||||
sizeStart: 10,
|
||||
sizeEnd: 2,
|
||||
colorStart: color || { r: 1, g: 0.8, b: 0.2, a: 1 },
|
||||
colorEnd: { r: 0.2, g: 0.5, b: 1, a: 0 }
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function playHitSound() {
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.audio && eng.audio.synth) {
|
||||
try {
|
||||
eng.audio.synth.synthSfx([0.5, 0.05, 440, , , 0.1, , 1.2]);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function playScoreSound() {
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.audio && eng.audio.synth) {
|
||||
try {
|
||||
eng.audio.synth.synthSfx([1, 0.1, 880, , , 0.15, , 1.8]);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function render(g) {
|
||||
if (!gameState) return;
|
||||
const state = gameState;
|
||||
const paddle = state.paddle;
|
||||
const ball = state.ball;
|
||||
const bricks = state.bricks;
|
||||
|
||||
// 背景
|
||||
g.fillStyle = '#121212';
|
||||
g.fillRect(0, 0, 390, 844);
|
||||
|
||||
// 砖块
|
||||
for (const b of bricks) {
|
||||
const hpRatio = b.health / b.maxHealth;
|
||||
let color;
|
||||
if (hpRatio > 0.66) color = '#00cc66';
|
||||
else if (hpRatio > 0.33) color = '#ffaa00';
|
||||
else color = '#ff3333';
|
||||
g.fillStyle = color;
|
||||
g.fillRect(b.x, b.y, b.w, b.h);
|
||||
g.strokeStyle = '#222';
|
||||
g.lineWidth = 1;
|
||||
g.strokeRect(b.x, b.y, b.w, b.h);
|
||||
}
|
||||
|
||||
// 挡板
|
||||
g.fillStyle = '#43c6e6';
|
||||
g.fillRect(paddle.x - paddle.w/2, paddle.y - paddle.h/2, paddle.w, paddle.h);
|
||||
|
||||
// 球
|
||||
g.fillStyle = '#ffcc00';
|
||||
g.beginPath();
|
||||
g.arc(ball.x, ball.y, BALL_R, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
|
||||
// HUD
|
||||
g.fillStyle = '#cccccc';
|
||||
g.font = '18px monospace';
|
||||
g.fillText('Score: ' + state.score + ' Remaining: ' + state.remaining, 10, 30);
|
||||
|
||||
// 游戏结束覆盖
|
||||
if (state.phase === 'gameover') {
|
||||
g.fillStyle = 'rgba(0,0,0,0.6)';
|
||||
g.fillRect(0, 0, 390, 844);
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = 'bold 36px sans-serif';
|
||||
g.textAlign = 'center';
|
||||
if (state.gameoverReason === 'cleared_all') {
|
||||
g.fillText('YOU WIN!', 195, 380);
|
||||
} else {
|
||||
g.fillText('GAME OVER', 195, 380);
|
||||
}
|
||||
g.font = '18px sans-serif';
|
||||
g.fillText('Tap to restart', 195, 440);
|
||||
g.textAlign = 'start';
|
||||
}
|
||||
}
|
||||
|
||||
function destroySubs() {
|
||||
inputSubs.forEach(sub => {
|
||||
try { sub.cancel(); } catch(_) {}
|
||||
});
|
||||
inputSubs = [];
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
destroySubs();
|
||||
gameState = null;
|
||||
boot = null;
|
||||
ctx = null;
|
||||
}
|
||||
|
||||
function _forensicsView() {
|
||||
return {
|
||||
seed: o.seed || 0,
|
||||
state: function() {
|
||||
const state = gameState;
|
||||
if (!state) return { phase: 'unknown', score: 0, remaining: 0, ball: {x:0,y:0,vx:0,vy:0}, paddle: {x:0,y:0,w:0} };
|
||||
return {
|
||||
phase: state.phase,
|
||||
score: state.score,
|
||||
remaining: state.remaining,
|
||||
progress: state.remaining === 0 ? 1 : (1 - state.remaining / (COLS*ROWS)),
|
||||
gameoverReason: state.gameoverReason || '',
|
||||
ball: { x: state.ball.x, y: state.ball.y, vx: state.ball.vx, vy: state.ball.vy },
|
||||
paddle: { x: state.paddle.x, y: state.paddle.y, w: state.paddle.w }
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { init, update, render, destroy, _forensicsView };
|
||||
}
|
||||
89
game-runtime/games/_wg1-gen/breakout/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 uncaught[])。
|
||||
布局收编(对齐 ref 教训):canvas 顶在 (0,0) 满幅 390×844;状态行 absolute overlay + pointer-events:none,
|
||||
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 固定取证视口:390×844 移动竖屏基准;user-scalable=no 防缩放扰动像素哈希。 -->
|
||||
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
|
||||
<title>W-G1 generated game</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
|
||||
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
|
||||
#wrap { position: relative; width: 390px; margin: 0 auto; }
|
||||
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
|
||||
/* 状态行默认隐藏:boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
|
||||
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
|
||||
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
|
||||
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
|
||||
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
|
||||
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div id="status">booting…</div>
|
||||
<!-- 受控画布(opts.canvas 占位/输入兜底);real 引擎自建 #game-engine 叠加。 -->
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- esbuild 产物:iife 全局 __GameBundle(含 GameHostFactory + bootGameHost)。 -->
|
||||
<script src="./bundle.iife.js"></script>
|
||||
<script>
|
||||
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
|
||||
(function () {
|
||||
'use strict';
|
||||
var statusEl = document.getElementById('status');
|
||||
var canvas = document.getElementById('game');
|
||||
// 假绿守卫C·真接线探针:每次引擎能力被调,recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
|
||||
window.__engineCalls = [];
|
||||
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
|
||||
// 就绪/错误标记初值。
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = null;
|
||||
try {
|
||||
var G = window.__GameBundle;
|
||||
if (!G || typeof G.bootGameHost !== 'function') {
|
||||
throw new Error('__GameBundle.bootGameHost 未找到(bundle.iife.js 未正确加载或 globalName 不符)');
|
||||
}
|
||||
G.bootGameHost({
|
||||
canvas: canvas,
|
||||
statusEl: statusEl,
|
||||
recHook: recHook,
|
||||
// onReady:收集 internals(uncaught[] 等)供假绿守卫B 读。
|
||||
onReady: function (host, internals) {
|
||||
window.__genHost = host;
|
||||
window.__genInternals = internals || null;
|
||||
}
|
||||
})
|
||||
.then(function (host) {
|
||||
window.__genHost = window.__genHost || host;
|
||||
// 可测性别名:守卫H 经此读游戏可观测状态(host.state() 透传 game._forensicsView().state;游戏未实现则返回 {phase:'booting'})。
|
||||
// 宿主侧挂载,工厂仍零 window;与 ref 在 onReady 挂 __refForensics 同范式。
|
||||
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
|
||||
window.__genBooted = true; // CDP 探活标记(区分「脚本没加载」与「boot 抛错」)
|
||||
})
|
||||
.catch(function (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
|
||||
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
|
||||
});
|
||||
} catch (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
33
game-runtime/games/_wg1-gen/breakout/play-spec.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"controlCheck": {
|
||||
"paddlePath": "paddle.x",
|
||||
"paddleY": 800,
|
||||
"tapXs": [
|
||||
50,
|
||||
340
|
||||
],
|
||||
"tapsPerTarget": 6,
|
||||
"moveMin": 25,
|
||||
"tolerance": 70,
|
||||
"why": "挡板须平滑逼近触点 x(逮「一格一跳」/卡死:连点同一 x 后挡板应明显移动且最终靠近该 x)"
|
||||
},
|
||||
"driver": {
|
||||
"type": "paddle-intercept",
|
||||
"ballPath": "ball.x",
|
||||
"paddleY": 800,
|
||||
"steps": 60,
|
||||
"stepMs": 130
|
||||
},
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
],
|
||||
"assertAfterPlay": [
|
||||
{
|
||||
"path": "remaining",
|
||||
"op": "decreased",
|
||||
"why": "适配性真玩(挡板自动接球)下击碎砖块→剩余下降;接不住/碰撞失效则不降"
|
||||
}
|
||||
],
|
||||
"expectLatch": true
|
||||
}
|
||||
34
game-runtime/games/_wg1-gen/doodle/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/doodle/generated-factory.js":{"bytes":11886,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/doodle/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/doodle/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/doodle/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/doodle/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164205},"games/_wg1-gen/doodle/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5359},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4156},"src/plugins/palette-post/impl.js":{"bytesInOutput":4218},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/doodle/generated-factory.js":{"bytesInOutput":3869}},"bytes":211647}}}
|
||||
77
game-runtime/games/_wg1-gen/doodle/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/doodle/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
game-runtime/games/_wg1-gen/doodle/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
112
game-runtime/games/_wg1-gen/doodle/evidence/verdict.json
Normal file
@ -0,0 +1,112 @@
|
||||
{
|
||||
"gameId": "doodle",
|
||||
"url": "http://localhost:4320/games/_wg1-gen/doodle/index.html",
|
||||
"ts": null,
|
||||
"guards": {
|
||||
"A_boot": {
|
||||
"pass": true
|
||||
},
|
||||
"C_frame": {
|
||||
"pass": true,
|
||||
"f0": 47,
|
||||
"f1": 77,
|
||||
"delta": 30
|
||||
},
|
||||
"I_control": {
|
||||
"pass": true,
|
||||
"skipped": "未声明 controlCheck(向后兼容)"
|
||||
},
|
||||
"D_render": {
|
||||
"pass": true,
|
||||
"bright": 65074,
|
||||
"maxCh": 255
|
||||
},
|
||||
"E_live": {
|
||||
"pass": true,
|
||||
"distinctStates": 13,
|
||||
"samples": 13
|
||||
},
|
||||
"B_uncaught": {
|
||||
"pass": true,
|
||||
"uncaught": []
|
||||
},
|
||||
"F_wiring": {
|
||||
"pass": true,
|
||||
"callCount": 13,
|
||||
"sample": [
|
||||
"particles.spawnEmitter",
|
||||
"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",
|
||||
"particles.spawnEmitter"
|
||||
],
|
||||
"expected": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
]
|
||||
},
|
||||
"G_input": {
|
||||
"pass": true,
|
||||
"inputHash": "229f8947",
|
||||
"controlHash": "ebd062df",
|
||||
"atFrame": 350,
|
||||
"ctrlFrame": 355
|
||||
},
|
||||
"H_progress": {
|
||||
"pass": true,
|
||||
"checks": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"before": 259,
|
||||
"after": 1003,
|
||||
"pass": true,
|
||||
"why": "适配性真玩(读 bird.x/nextPlatform.x 左右趋近接台)→高度分上升;落不住或计分语义错则不升"
|
||||
}
|
||||
],
|
||||
"latch": {
|
||||
"pass": true,
|
||||
"dwelledMs": 600,
|
||||
"after": "gameover",
|
||||
"reason": ""
|
||||
},
|
||||
"state0": {
|
||||
"phase": "playing",
|
||||
"result": null,
|
||||
"score": 259,
|
||||
"bird": {
|
||||
"x": 195,
|
||||
"y": 657,
|
||||
"vy": 120
|
||||
},
|
||||
"nextPlatform": {
|
||||
"x": 38.45870539546013,
|
||||
"y": 622.021542112343
|
||||
}
|
||||
},
|
||||
"state1": {
|
||||
"phase": "gameover",
|
||||
"result": "lose",
|
||||
"score": 1003,
|
||||
"bird": {
|
||||
"x": 174.1666666666667,
|
||||
"y": 837,
|
||||
"vy": 660
|
||||
},
|
||||
"nextPlatform": {
|
||||
"x": 315.5195666849613,
|
||||
"y": 835.9829828934744
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": true,
|
||||
"notes": []
|
||||
}
|
||||
388
game-runtime/games/_wg1-gen/doodle/generated-factory.js
Normal file
@ -0,0 +1,388 @@
|
||||
'use strict';
|
||||
|
||||
export default function createDoodleJumpGame(opts) {
|
||||
const o = opts || {};
|
||||
let ctx = null;
|
||||
let subPointerDown = null;
|
||||
let subPointerUp = null;
|
||||
let subPointerMove = null;
|
||||
let leftPressed = false;
|
||||
let rightPressed = false;
|
||||
|
||||
// 游戏常数
|
||||
const SCREEN_W = 390;
|
||||
const SCREEN_H = 844;
|
||||
const GRAVITY = 1200;
|
||||
const JUMP_VY = -800;
|
||||
const HORIZONTAL_SPEED = 250;
|
||||
const ROLE_RADIUS = 10;
|
||||
const PLATFORM_W = 70;
|
||||
const PLATFORM_H = 10;
|
||||
const CAMERA_FIXED_Y = 650;
|
||||
const PLATFORM_SPACING_MIN = 80;
|
||||
const PLATFORM_SPACING_MAX = 130;
|
||||
const PLATFORM_X_OFFSET_MAX = 150;
|
||||
|
||||
// 游戏状态
|
||||
let phase = 'booting';
|
||||
let result = null;
|
||||
let score = 0;
|
||||
let role = null;
|
||||
let platforms = [];
|
||||
let cameraY = 0;
|
||||
let highestWorldY = 0;
|
||||
let initialWorldY = 0;
|
||||
const FIXED_STEP = 1 / 60; // 固定 dt
|
||||
let nowMs = 0;
|
||||
|
||||
// ---------- 内部重置函数 ----------
|
||||
function resetGame() {
|
||||
const spawnFirst = !role; // 首次调用时还没创建 role
|
||||
if (!role) {
|
||||
role = {
|
||||
worldX: SCREEN_W / 2,
|
||||
worldY: 0,
|
||||
vy: 0,
|
||||
vx: 0,
|
||||
};
|
||||
}
|
||||
// 重置角色位置:初始站在第一个平台上
|
||||
role.worldX = SCREEN_W / 2;
|
||||
role.worldY = SCREEN_H - 200; // 初始靠近底部,但下方留空间
|
||||
role.vy = 0;
|
||||
role.vx = 0;
|
||||
initialWorldY = role.worldY;
|
||||
highestWorldY = role.worldY;
|
||||
cameraY = CAMERA_FIXED_Y - role.worldY; // 使得角色屏幕 y = CAMERA_FIXED_Y
|
||||
platforms = [];
|
||||
leftPressed = false;
|
||||
rightPressed = false;
|
||||
score = 0;
|
||||
phase = 'playing';
|
||||
result = null;
|
||||
|
||||
// 生成初始平台:角色脚下平台 + 上方若干平台
|
||||
// 第一个平台:位于角色脚下(角色底部刚好踩到平台顶部)
|
||||
const footY = role.worldY + ROLE_RADIUS;
|
||||
const firstPlat = {
|
||||
x: role.worldX - PLATFORM_W / 2,
|
||||
y: footY,
|
||||
w: PLATFORM_W,
|
||||
h: PLATFORM_H,
|
||||
};
|
||||
platforms.push(firstPlat);
|
||||
|
||||
// 向上生成 6 个平台
|
||||
let curY = footY;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const spacing = ctx.random.range(PLATFORM_SPACING_MIN, PLATFORM_SPACING_MAX);
|
||||
curY -= spacing;
|
||||
if (curY < 0) curY = 0; // 防止生成到不可到达的顶端
|
||||
const nextX = ctx.random.range(0, SCREEN_W - PLATFORM_W);
|
||||
platforms.push({
|
||||
x: nextX,
|
||||
y: curY,
|
||||
w: PLATFORM_W,
|
||||
h: PLATFORM_H,
|
||||
});
|
||||
}
|
||||
// 按 y 排序(从高到低,y 小在上)
|
||||
platforms.sort((a, b) => a.y - b.y);
|
||||
}
|
||||
|
||||
// ---------- 输入处理 ----------
|
||||
function onPointerDown(e) {
|
||||
if (phase === 'gameover') {
|
||||
// 只处理重玩按钮区域:屏幕中央下方约 (195, 600~700)
|
||||
const bx = 195, by = 650, bw = 160, bh = 50;
|
||||
if (e.x >= bx - bw / 2 && e.x <= bx + bw / 2 &&
|
||||
e.y >= by - bh / 2 && e.y <= by + bh / 2) {
|
||||
resetGame();
|
||||
}
|
||||
return;
|
||||
}
|
||||
leftPressed = e.x < SCREEN_W / 2;
|
||||
rightPressed = e.x >= SCREEN_W / 2;
|
||||
}
|
||||
|
||||
function onPointerUp(e) {
|
||||
if (phase === 'gameover') return;
|
||||
// 松开时清除对应方向(简单起见,松开任意手指清除所有方向)
|
||||
leftPressed = false;
|
||||
rightPressed = false;
|
||||
}
|
||||
|
||||
function onPointerMove(e) {
|
||||
if (phase === 'gameover') return;
|
||||
leftPressed = e.x < SCREEN_W / 2;
|
||||
rightPressed = e.x >= SCREEN_W / 2;
|
||||
}
|
||||
|
||||
// ---------- 生成平台(按需向上补充) ----------
|
||||
function ensurePlatformsAbove() {
|
||||
// 确保上方有足够覆盖到角色上方一个屏幕高度的平台
|
||||
const topScreenWorld = role.worldY - SCREEN_H; // 屏幕上边缘对应的世界 y
|
||||
// 找出当前最高平台(y 最小)
|
||||
let smallestY = 0;
|
||||
if (platforms.length > 0) {
|
||||
smallestY = platforms[0].y;
|
||||
for (let i = 1; i < platforms.length; i++) {
|
||||
if (platforms[i].y < smallestY) smallestY = platforms[i].y;
|
||||
}
|
||||
}
|
||||
// 如果最高平台仍不够高(大于目标顶部),继续生成
|
||||
while (smallestY > topScreenWorld - 200) {
|
||||
const spacing = ctx.random.range(PLATFORM_SPACING_MIN, PLATFORM_SPACING_MAX);
|
||||
const newY = smallestY - spacing;
|
||||
const newX = ctx.random.range(0, SCREEN_W - PLATFORM_W);
|
||||
platforms.push({
|
||||
x: newX,
|
||||
y: newY,
|
||||
w: PLATFORM_W,
|
||||
h: PLATFORM_H,
|
||||
});
|
||||
smallestY = newY;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 移除屏幕下的平台 ----------
|
||||
function removePlatformsBelow() {
|
||||
const bottomWorld = role.worldY + SCREEN_H + 50;
|
||||
platforms = platforms.filter(p => p.y < bottomWorld);
|
||||
}
|
||||
|
||||
// ---------- 碰撞检测与反弹 ----------
|
||||
function checkPlatformCollision(dt) {
|
||||
if (role.vy <= 0) return false; // 只有下落才可能碰撞
|
||||
const oldBottomY = role.worldY - role.vy * dt + ROLE_RADIUS;
|
||||
let hit = false;
|
||||
for (const p of platforms) {
|
||||
// 粗略过滤:角色底部在平台上方或内部
|
||||
const roleBottom = role.worldY + ROLE_RADIUS;
|
||||
if (roleBottom >= p.y && roleBottom <= p.y + p.h + 30) {
|
||||
// 水平检测
|
||||
if (role.worldX + ROLE_RADIUS > p.x && role.worldX - ROLE_RADIUS < p.x + p.w) {
|
||||
// 上一帧底部在平台顶部以上
|
||||
if (oldBottomY <= p.y && roleBottom >= p.y) {
|
||||
// 反弹
|
||||
role.vy = JUMP_VY;
|
||||
// 将角色位置置于平台顶部(避免卡入)
|
||||
role.worldY = p.y - ROLE_RADIUS;
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
// ---------- 暴露给宿主的生命周期 ----------
|
||||
function init(boot) {
|
||||
ctx = boot.ctx;
|
||||
const input = ctx.getInput();
|
||||
subPointerDown = input.on('pointerdown', onPointerDown);
|
||||
subPointerUp = input.on('pointerup', onPointerUp);
|
||||
subPointerMove = input.on('pointermove', onPointerMove);
|
||||
resetGame(); // 初始化状态
|
||||
// 引擎预热:发射一个常驻粒子(软启动)
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: 195, y: 300 },
|
||||
emitRate: 20,
|
||||
emitTime: 0,
|
||||
particleTime: 0.6,
|
||||
speed: 50,
|
||||
colorStart: { r: 1, g: 0.8, b: 0.3, a: 1 },
|
||||
colorEnd: { r: 1, g: 0.2, b: 0, a: 0 },
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
nowMs = ctx.time.nowMs();
|
||||
if (phase !== 'playing') return; // 只有在游戏进行中才更新逻辑
|
||||
// 检查失败
|
||||
const screenY = role.worldY + cameraY;
|
||||
if (screenY + ROLE_RADIUS > SCREEN_H) {
|
||||
phase = 'gameover';
|
||||
result = 'lose';
|
||||
// 失败粒子爆发 + 音效
|
||||
const eng = ctx.getEngine();
|
||||
if (eng) {
|
||||
if (eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: role.worldX, y: role.worldY },
|
||||
count: 40,
|
||||
speed: 200,
|
||||
particleTime: 0.7,
|
||||
colorStart: { r: 1, g: 0.2, b: 0.2, a: 1 },
|
||||
colorEnd: { r: 0.8, g: 0, b: 0, a: 0 },
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
if (eng.audio && eng.audio.synth) {
|
||||
try {
|
||||
eng.audio.synth.synthSfx([1, 0.1, 150, , , 0.2, , 1.5, , , , , 0.5]);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 水平移动
|
||||
if (leftPressed) {
|
||||
role.vx = -HORIZONTAL_SPEED;
|
||||
} else if (rightPressed) {
|
||||
role.vx = HORIZONTAL_SPEED;
|
||||
} else {
|
||||
role.vx = 0;
|
||||
}
|
||||
// 边界环绕
|
||||
role.worldX += role.vx * dt;
|
||||
if (role.worldX < -ROLE_RADIUS) role.worldX = SCREEN_W + ROLE_RADIUS;
|
||||
if (role.worldX > SCREEN_W + ROLE_RADIUS) role.worldX = -ROLE_RADIUS;
|
||||
// 重力
|
||||
role.vy += GRAVITY * dt;
|
||||
// 垂直移动
|
||||
role.worldY += role.vy * dt;
|
||||
// 碰撞检测
|
||||
const hit = checkPlatformCollision(dt);
|
||||
if (hit) {
|
||||
// 踩中平台,播放反馈
|
||||
const eng = ctx.getEngine();
|
||||
if (eng) {
|
||||
if (eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: role.worldX, y: role.worldY + ROLE_RADIUS },
|
||||
count: 20,
|
||||
speed: 100,
|
||||
particleTime: 0.4,
|
||||
colorStart: { r: 0.2, g: 1, b: 0.5, a: 1 },
|
||||
colorEnd: { r: 0, g: 0.7, b: 0.2, a: 0 },
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
if (eng.audio && eng.audio.synth) {
|
||||
try {
|
||||
eng.audio.synth.synthSfx([1, 0.05, 300, , , 0.15, , 1.3]);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 更新相机(只上移不下移)
|
||||
cameraY = Math.max(cameraY, CAMERA_FIXED_Y - role.worldY);
|
||||
// 更新最高点与分数
|
||||
if (role.worldY < highestWorldY) {
|
||||
highestWorldY = role.worldY;
|
||||
score = Math.floor(initialWorldY - highestWorldY);
|
||||
}
|
||||
// 平台管理
|
||||
ensurePlatformsAbove();
|
||||
removePlatformsBelow();
|
||||
}
|
||||
|
||||
function render(g) {
|
||||
// 清空背景
|
||||
g.fillStyle = '#0d1b2a';
|
||||
g.fillRect(0, 0, SCREEN_W, SCREEN_H);
|
||||
// 绘制平台(所有平台的世界坐标 -> 屏幕坐标 = world + cameraY)
|
||||
for (const p of platforms) {
|
||||
const sx = p.x;
|
||||
const sy = p.y + cameraY;
|
||||
if (sy + p.h < 0 || sy > SCREEN_H) continue; // 裁剪
|
||||
g.fillStyle = '#e0e1dd';
|
||||
g.fillRect(sx, sy, p.w, p.h);
|
||||
// 平台高光线
|
||||
g.fillStyle = '#778da9';
|
||||
g.fillRect(sx, sy, p.w, 2);
|
||||
}
|
||||
// 绘制角色
|
||||
const roleScreenX = role ? role.worldX : SCREEN_W / 2;
|
||||
const roleScreenY = role ? role.worldY + cameraY : CAMERA_FIXED_Y;
|
||||
if (role) {
|
||||
g.fillStyle = '#f4a261';
|
||||
g.beginPath();
|
||||
g.arc(roleScreenX, roleScreenY, ROLE_RADIUS, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
}
|
||||
// 绘制 HUD(顶部文字)
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = 'bold 16px monospace';
|
||||
g.textAlign = 'left';
|
||||
g.fillText('Height: ' + score, 12, 30);
|
||||
if (phase === 'gameover') {
|
||||
g.textAlign = 'center';
|
||||
g.font = 'bold 28px monospace';
|
||||
g.fillStyle = '#e63946';
|
||||
g.fillText('GAME OVER', SCREEN_W / 2, 400);
|
||||
g.font = 'bold 18px monospace';
|
||||
g.fillStyle = '#a8dadc';
|
||||
g.fillText('Score: ' + score, SCREEN_W / 2, 460);
|
||||
g.font = '20px monospace';
|
||||
g.fillStyle = '#f1faee';
|
||||
// 重玩按钮
|
||||
const bx = 195, by = 650, bw = 160, bh = 50;
|
||||
g.fillStyle = '#457b9d';
|
||||
g.fillRect(bx - bw / 2, by - bh / 2, bw, bh);
|
||||
g.fillStyle = '#1d3557';
|
||||
g.font = 'bold 18px monospace';
|
||||
g.fillText('Tap to Retry', bx, by + 6);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
try { subPointerDown && subPointerDown.cancel(); } catch (_) {}
|
||||
try { subPointerUp && subPointerUp.cancel(); } catch (_) {}
|
||||
try { subPointerMove && subPointerMove.cancel(); } catch (_) {}
|
||||
subPointerDown = null;
|
||||
subPointerUp = null;
|
||||
subPointerMove = null;
|
||||
platforms = [];
|
||||
role = null;
|
||||
}
|
||||
|
||||
// ---------- 可测性视图 ----------
|
||||
function _forensicsView() {
|
||||
return {
|
||||
seed: o.seed || 0,
|
||||
state: () => {
|
||||
let nextPlatform = null;
|
||||
if (role && platforms.length > 0) {
|
||||
// 寻找角色正上方最近(y 小于角色 worldY 且最小 y)的平台
|
||||
let best = null;
|
||||
let bestY = -Infinity;
|
||||
for (const p of platforms) {
|
||||
if (p.y < role.worldY && p.y > bestY) {
|
||||
best = p;
|
||||
bestY = p.y;
|
||||
}
|
||||
}
|
||||
if (best) {
|
||||
nextPlatform = {
|
||||
x: best.x + best.w / 2,
|
||||
y: best.y + cameraY,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
phase,
|
||||
result,
|
||||
score,
|
||||
bird: role ? {
|
||||
x: role.worldX,
|
||||
y: role.worldY + cameraY,
|
||||
vy: role.vy,
|
||||
} : { x: 0, y: 0, vy: 0 },
|
||||
nextPlatform: nextPlatform || { x: 0, y: 0 },
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { init, update, render, destroy, _forensicsView };
|
||||
}
|
||||
89
game-runtime/games/_wg1-gen/doodle/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 uncaught[])。
|
||||
布局收编(对齐 ref 教训):canvas 顶在 (0,0) 满幅 390×844;状态行 absolute overlay + pointer-events:none,
|
||||
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 固定取证视口:390×844 移动竖屏基准;user-scalable=no 防缩放扰动像素哈希。 -->
|
||||
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
|
||||
<title>W-G1 generated game</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
|
||||
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
|
||||
#wrap { position: relative; width: 390px; margin: 0 auto; }
|
||||
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
|
||||
/* 状态行默认隐藏:boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
|
||||
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
|
||||
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
|
||||
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
|
||||
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
|
||||
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div id="status">booting…</div>
|
||||
<!-- 受控画布(opts.canvas 占位/输入兜底);real 引擎自建 #game-engine 叠加。 -->
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- esbuild 产物:iife 全局 __GameBundle(含 GameHostFactory + bootGameHost)。 -->
|
||||
<script src="./bundle.iife.js"></script>
|
||||
<script>
|
||||
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
|
||||
(function () {
|
||||
'use strict';
|
||||
var statusEl = document.getElementById('status');
|
||||
var canvas = document.getElementById('game');
|
||||
// 假绿守卫C·真接线探针:每次引擎能力被调,recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
|
||||
window.__engineCalls = [];
|
||||
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
|
||||
// 就绪/错误标记初值。
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = null;
|
||||
try {
|
||||
var G = window.__GameBundle;
|
||||
if (!G || typeof G.bootGameHost !== 'function') {
|
||||
throw new Error('__GameBundle.bootGameHost 未找到(bundle.iife.js 未正确加载或 globalName 不符)');
|
||||
}
|
||||
G.bootGameHost({
|
||||
canvas: canvas,
|
||||
statusEl: statusEl,
|
||||
recHook: recHook,
|
||||
// onReady:收集 internals(uncaught[] 等)供假绿守卫B 读。
|
||||
onReady: function (host, internals) {
|
||||
window.__genHost = host;
|
||||
window.__genInternals = internals || null;
|
||||
}
|
||||
})
|
||||
.then(function (host) {
|
||||
window.__genHost = window.__genHost || host;
|
||||
// 可测性别名:守卫H 经此读游戏可观测状态(host.state() 透传 game._forensicsView().state;游戏未实现则返回 {phase:'booting'})。
|
||||
// 宿主侧挂载,工厂仍零 window;与 ref 在 onReady 挂 __refForensics 同范式。
|
||||
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
|
||||
window.__genBooted = true; // CDP 探活标记(区分「脚本没加载」与「boot 抛错」)
|
||||
})
|
||||
.catch(function (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
|
||||
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
|
||||
});
|
||||
} catch (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
20
game-runtime/games/_wg1-gen/doodle/play-spec.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"driver": {
|
||||
"type": "seek-x",
|
||||
"steps": 70,
|
||||
"stepMs": 120,
|
||||
"deadzone": 16
|
||||
},
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
],
|
||||
"assertAfterPlay": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"why": "适配性真玩(读 bird.x/nextPlatform.x 左右趋近接台)→高度分上升;落不住或计分语义错则不升"
|
||||
}
|
||||
],
|
||||
"expectLatch": true
|
||||
}
|
||||
34
game-runtime/games/_wg1-gen/flappy/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/flappy/generated-factory.js":{"bytes":9192,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/flappy/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/flappy/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/flappy/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/flappy/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164173},"games/_wg1-gen/flappy/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5363},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4218},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/flappy/generated-factory.js":{"bytesInOutput":3299}},"bytes":211059}}}
|
||||
77
game-runtime/games/_wg1-gen/flappy/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/flappy/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
game-runtime/games/_wg1-gen/flappy/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
110
game-runtime/games/_wg1-gen/flappy/evidence/verdict.json
Normal file
@ -0,0 +1,110 @@
|
||||
{
|
||||
"gameId": "flappy",
|
||||
"url": "http://localhost:4320/games/_wg1-gen/flappy/index.html",
|
||||
"ts": null,
|
||||
"guards": {
|
||||
"A_boot": {
|
||||
"pass": true
|
||||
},
|
||||
"C_frame": {
|
||||
"pass": true,
|
||||
"f0": 48,
|
||||
"f1": 78,
|
||||
"delta": 30
|
||||
},
|
||||
"I_control": {
|
||||
"pass": true,
|
||||
"skipped": "未声明 controlCheck(向后兼容)"
|
||||
},
|
||||
"D_render": {
|
||||
"pass": true,
|
||||
"bright": 178920,
|
||||
"maxCh": 255
|
||||
},
|
||||
"E_live": {
|
||||
"pass": true,
|
||||
"distinctStates": 6,
|
||||
"samples": 6
|
||||
},
|
||||
"B_uncaught": {
|
||||
"pass": true,
|
||||
"uncaught": []
|
||||
},
|
||||
"F_wiring": {
|
||||
"pass": true,
|
||||
"callCount": 8,
|
||||
"sample": [
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx"
|
||||
],
|
||||
"expected": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
]
|
||||
},
|
||||
"G_input": {
|
||||
"pass": true,
|
||||
"inputHash": "0e133ce8",
|
||||
"controlHash": "4fb72d48",
|
||||
"atFrame": 214,
|
||||
"ctrlFrame": 218
|
||||
},
|
||||
"H_progress": {
|
||||
"pass": false,
|
||||
"checks": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"before": 0,
|
||||
"after": 0,
|
||||
"pass": false,
|
||||
"why": "适配性真玩(读 bird.y/nextGap.centerY 拍翅穿缝)→得分上升;物理太硬秒死或穿不过则 0"
|
||||
}
|
||||
],
|
||||
"latch": {
|
||||
"pass": true,
|
||||
"dwelledMs": 600,
|
||||
"after": "gameover",
|
||||
"reason": ""
|
||||
},
|
||||
"state0": {
|
||||
"phase": "playing",
|
||||
"result": null,
|
||||
"score": 0,
|
||||
"bird": {
|
||||
"x": 110,
|
||||
"y": 559.1999999999999,
|
||||
"vy": 336
|
||||
},
|
||||
"nextGap": {
|
||||
"x": 600,
|
||||
"centerY": 174.12938179075718
|
||||
},
|
||||
"gameoverReason": ""
|
||||
},
|
||||
"state1": {
|
||||
"phase": "gameover",
|
||||
"result": "lose",
|
||||
"score": 0,
|
||||
"bird": {
|
||||
"x": 110,
|
||||
"y": 523.0999999999999,
|
||||
"vy": -169
|
||||
},
|
||||
"nextGap": {
|
||||
"x": 600,
|
||||
"centerY": 174.12938179075718
|
||||
},
|
||||
"gameoverReason": "pipe_collision"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": false,
|
||||
"notes": []
|
||||
}
|
||||
310
game-runtime/games/_wg1-gen/flappy/generated-factory.js
Normal file
@ -0,0 +1,310 @@
|
||||
'use strict';
|
||||
export default function createFlappyBirdGame(opts) {
|
||||
const o = opts || {};
|
||||
let ctx = null;
|
||||
let nowMs = 0;
|
||||
let subPointer = null;
|
||||
|
||||
// 游戏状态
|
||||
let bird = { x: 110, y: 422, vy: 0, r: 12 }; // r 用于碰撞和渲染
|
||||
const GRAVITY = 420; // 像素/秒²
|
||||
const FLAP_VEL = -260; // 拍翅瞬速(负=上)
|
||||
const PIPE_SPEED = 150; // 像素/秒
|
||||
const PIPE_WIDTH = 60;
|
||||
const GAP_HEIGHT = 220; // ≥ 0.24 * 844 = 202.56
|
||||
const PIPE_SPACING = 280; // 管道中心水平间隔,保证反应时间
|
||||
const MIN_PIPE_CENTER_Y = GAP_HEIGHT / 2;
|
||||
const MAX_PIPE_CENTER_Y = 844 - GAP_HEIGHT / 2;
|
||||
const BIRD_HALF = 12; // 鸟的显示/碰撞半径(实际用矩形 24x24)
|
||||
|
||||
let pipes = []; // { x, centerY, scored }
|
||||
let score = 0;
|
||||
let phase = 'booting'; // 'booting' | 'playing' | 'gameover'
|
||||
let gameoverReason = '';
|
||||
let result = null;
|
||||
|
||||
// 粒子/音效触发标记(帧内)
|
||||
let pendingFlapEffect = false;
|
||||
let pendingScoreEffect = false;
|
||||
let pendingCrashEffect = false;
|
||||
let crashPos = { x: 0, y: 0 };
|
||||
|
||||
function resetGame() {
|
||||
bird.y = 422;
|
||||
bird.vy = 0;
|
||||
pipes = [];
|
||||
score = 0;
|
||||
phase = 'playing';
|
||||
gameoverReason = '';
|
||||
result = null;
|
||||
pendingFlapEffect = false;
|
||||
pendingScoreEffect = false;
|
||||
pendingCrashEffect = false;
|
||||
// 第一对管道初始位置:保证 ≥1.5 秒后与鸟相遇
|
||||
const initialPipeX = 390 + PIPE_WIDTH / 2 + 180; // 速度150,到达鸟时间≈(570-110)/150≈3.07s > 1.5s
|
||||
pipes.push({
|
||||
x: initialPipeX,
|
||||
centerY: randomPipeCenterY(),
|
||||
scored: false
|
||||
});
|
||||
}
|
||||
|
||||
function randomPipeCenterY() {
|
||||
// ctx.random.range 防御
|
||||
if (ctx && ctx.random && ctx.random.range) {
|
||||
return ctx.random.range(MIN_PIPE_CENTER_Y, MAX_PIPE_CENTER_Y);
|
||||
}
|
||||
// 极低概率缺省时用固定值
|
||||
return 422;
|
||||
}
|
||||
|
||||
/** 触发引擎粒子 + 音效 */
|
||||
function spawnEffects(pos, type) {
|
||||
const eng = ctx.getEngine();
|
||||
if (!eng) return;
|
||||
if (eng.particles) {
|
||||
try {
|
||||
const emitter = eng.particles.spawnEmitter({
|
||||
pos: { x: pos.x, y: pos.y },
|
||||
count: type === 'crash' ? 35 : 22,
|
||||
emitRate: 0,
|
||||
particleTime: 0.6,
|
||||
speed: type === 'crash' ? 160 : 110,
|
||||
coneAngle: type === 'crash' ? Math.PI : 0.5,
|
||||
sizeStart: type === 'crash' ? 16 : 12,
|
||||
sizeEnd: 2,
|
||||
colorStart: type === 'crash' ? { r: 1, g: 0.1, b: 0.1, a: 1 }
|
||||
: type === 'score' ? { r: 0.2, g: 1, b: 0.3, a: 1 }
|
||||
: { r: 0.3, g: 0.7, b: 1, a: 1 },
|
||||
colorEnd: type === 'crash' ? { r: 1, g: 0.3, b: 0, a: 0 }
|
||||
: { r: 0.1, g: 0.5, b: 1, a: 0 }
|
||||
});
|
||||
} catch (_) { }
|
||||
}
|
||||
if (eng.audio && eng.audio.synth) {
|
||||
try {
|
||||
if (type === 'flap') {
|
||||
eng.audio.synth.synthSfx([1, 0.05, 340, , , 0.12, , 1.2, , , , , , , , 0.1]);
|
||||
} else if (type === 'score') {
|
||||
eng.audio.synth.synthSfx([1, 0.05, 880, , , 0.22, , 1.9, , , , , , , , 0.4]);
|
||||
} else if (type === 'crash') {
|
||||
eng.audio.synth.synthSfx([2, 0.5, 90, , , 0.5, , 0.3, , , , , , , , 0.1]);
|
||||
}
|
||||
} catch (_) { }
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击处理:拍翅(playing)或重玩(gameover) */
|
||||
function onPointer(e) {
|
||||
if (phase === 'playing') {
|
||||
// 拍翅
|
||||
bird.vy = FLAP_VEL;
|
||||
pendingFlapEffect = true;
|
||||
} else if (phase === 'gameover') {
|
||||
// 检测重玩按钮区域(居中偏下)
|
||||
const bx = 115, by = 620, bw = 160, bh = 60;
|
||||
const px = e.x || 0, py = e.y || 0;
|
||||
if (px >= bx && px <= bx + bw && py >= by && py <= by + bh) {
|
||||
resetGame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init(boot) {
|
||||
ctx = boot.ctx;
|
||||
// 订阅输入
|
||||
subPointer = ctx.getInput().on('pointerdown', onPointer);
|
||||
// 初始随机种子
|
||||
resetGame();
|
||||
phase = 'playing';
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
nowMs = ctx.time.nowMs(); // 控制钟
|
||||
if (phase !== 'playing') return; // 终态驻留,不更新物理
|
||||
|
||||
// 物理更新(dt 固定 1/60)
|
||||
const grav = GRAVITY;
|
||||
bird.vy += grav * dt;
|
||||
bird.y += bird.vy * dt;
|
||||
|
||||
// 触顶触底判定
|
||||
if (bird.y - BIRD_HALF <= 0 || bird.y + BIRD_HALF >= 844) {
|
||||
gameOver('boundary');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新管道位置
|
||||
for (const pipe of pipes) {
|
||||
pipe.x -= PIPE_SPEED * dt;
|
||||
}
|
||||
|
||||
// 移除屏幕外的管道
|
||||
pipes = pipes.filter(p => p.x > -PIPE_WIDTH / 2 - 10);
|
||||
|
||||
// 生成新管道:当最右的管道 x 小于阈值时,生成新管道
|
||||
const lastPipe = pipes.length > 0 ? pipes[pipes.length - 1] : null;
|
||||
if (!lastPipe || lastPipe.x <= 390 + PIPE_WIDTH / 2 - PIPE_SPACING) {
|
||||
const newX = lastPipe ? lastPipe.x + PIPE_SPACING : 390 + PIPE_WIDTH / 2 + 180;
|
||||
pipes.push({
|
||||
x: newX,
|
||||
centerY: randomPipeCenterY(),
|
||||
scored: false
|
||||
});
|
||||
}
|
||||
|
||||
// 碰撞检测 & 得分
|
||||
const birdRect = {
|
||||
x: bird.x - BIRD_HALF,
|
||||
y: bird.y - BIRD_HALF,
|
||||
w: BIRD_HALF * 2,
|
||||
h: BIRD_HALF * 2
|
||||
};
|
||||
for (const pipe of pipes) {
|
||||
const topH = pipe.centerY - GAP_HEIGHT / 2;
|
||||
const bottomY = pipe.centerY + GAP_HEIGHT / 2;
|
||||
const pipeRect = {
|
||||
x: pipe.x - PIPE_WIDTH / 2,
|
||||
y: 0,
|
||||
w: PIPE_WIDTH,
|
||||
h: topH
|
||||
};
|
||||
const pipeRect2 = {
|
||||
x: pipe.x - PIPE_WIDTH / 2,
|
||||
y: bottomY,
|
||||
w: PIPE_WIDTH,
|
||||
h: 844 - bottomY
|
||||
};
|
||||
// 碰撞(矩形相交)
|
||||
const collides = (a, b) => {
|
||||
return a.x < b.x + b.w && a.x + a.w > b.x &&
|
||||
a.y < b.y + b.h && a.y + a.h > b.y;
|
||||
};
|
||||
if (collides(birdRect, pipeRect) || collides(birdRect, pipeRect2)) {
|
||||
gameOver('pipe_collision');
|
||||
return;
|
||||
}
|
||||
// 得分:管道完全通过鸟左边缘
|
||||
if (!pipe.scored && pipe.x + PIPE_WIDTH / 2 < bird.x - BIRD_HALF) {
|
||||
pipe.scored = true;
|
||||
score++;
|
||||
pendingScoreEffect = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理本帧累积的特效
|
||||
if (pendingFlapEffect) {
|
||||
spawnEffects({ x: bird.x, y: bird.y }, 'flap');
|
||||
pendingFlapEffect = false;
|
||||
}
|
||||
if (pendingScoreEffect) {
|
||||
spawnEffects({ x: bird.x, y: bird.y }, 'score');
|
||||
pendingScoreEffect = false;
|
||||
}
|
||||
if (pendingCrashEffect) {
|
||||
spawnEffects(crashPos, 'crash');
|
||||
pendingCrashEffect = false;
|
||||
}
|
||||
}
|
||||
|
||||
function gameOver(reason) {
|
||||
phase = 'gameover';
|
||||
gameoverReason = reason;
|
||||
result = 'lose';
|
||||
crashPos = { x: bird.x, y: bird.y };
|
||||
pendingCrashEffect = true;
|
||||
}
|
||||
|
||||
function render(g) {
|
||||
// 背景
|
||||
g.fillStyle = '#0a1122';
|
||||
g.fillRect(0, 0, 390, 844);
|
||||
|
||||
if (phase === 'gameover' || phase === 'playing') {
|
||||
// 绘制管道
|
||||
g.fillStyle = '#3a5';
|
||||
for (const pipe of pipes) {
|
||||
const topH = pipe.centerY - GAP_HEIGHT / 2;
|
||||
const bottomY = pipe.centerY + GAP_HEIGHT / 2;
|
||||
// 顶管道
|
||||
g.fillRect(pipe.x - PIPE_WIDTH / 2, 0, PIPE_WIDTH, topH);
|
||||
// 底管道
|
||||
g.fillRect(pipe.x - PIPE_WIDTH / 2, bottomY, PIPE_WIDTH, 844 - bottomY);
|
||||
// 管道帽
|
||||
g.fillStyle = '#2c4';
|
||||
g.fillRect(pipe.x - PIPE_WIDTH / 2 - 4, topH - 10, PIPE_WIDTH + 8, 10);
|
||||
g.fillRect(pipe.x - PIPE_WIDTH / 2 - 4, bottomY, PIPE_WIDTH + 8, 10);
|
||||
g.fillStyle = '#3a5';
|
||||
}
|
||||
|
||||
// 绘制小鸟(亮黄色方块+眼睛)
|
||||
g.fillStyle = '#ffcc00';
|
||||
g.fillRect(bird.x - BIRD_HALF, bird.y - BIRD_HALF, BIRD_HALF * 2, BIRD_HALF * 2);
|
||||
g.fillStyle = '#000';
|
||||
g.fillRect(bird.x + 4, bird.y - 6, 5, 5);
|
||||
}
|
||||
|
||||
// HUD
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '20px monospace';
|
||||
g.fillText('得分: ' + score, 20, 40);
|
||||
|
||||
if (phase === 'gameover') {
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '26px monospace';
|
||||
g.fillText('失败!', 130, 300);
|
||||
// 重玩按钮
|
||||
const bx = 115, by = 620, bw = 160, bh = 60;
|
||||
g.fillStyle = '#2255aa';
|
||||
g.fillRect(bx, by, bw, bh);
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '22px monospace';
|
||||
g.fillText('点击重玩', bx + 10, by + 38);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
try { subPointer && subPointer.cancel(); } catch (_) {}
|
||||
subPointer = null;
|
||||
pipes = [];
|
||||
bird = { x: 110, y: 422, vy: 0, r: 12 };
|
||||
}
|
||||
|
||||
/** 可测性视图 */
|
||||
function _forensicsView() {
|
||||
const nextGap = (() => {
|
||||
// 找到小鸟前方最近的管道
|
||||
if (phase !== 'playing') return null;
|
||||
let candidate = null;
|
||||
for (const pipe of pipes) {
|
||||
if (pipe.x > bird.x - BIRD_HALF) {
|
||||
if (!candidate || pipe.x < candidate.x) {
|
||||
candidate = pipe;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!candidate) return null;
|
||||
return { x: candidate.x, centerY: candidate.centerY };
|
||||
})();
|
||||
|
||||
return {
|
||||
seed: opts && opts.seed !== undefined ? opts.seed : undefined,
|
||||
state: () => ({
|
||||
phase,
|
||||
result,
|
||||
score,
|
||||
bird: { x: bird.x, y: bird.y, vy: bird.vy },
|
||||
nextGap,
|
||||
gameoverReason
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
update,
|
||||
render,
|
||||
destroy,
|
||||
_forensicsView
|
||||
};
|
||||
}
|
||||
89
game-runtime/games/_wg1-gen/flappy/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 uncaught[])。
|
||||
布局收编(对齐 ref 教训):canvas 顶在 (0,0) 满幅 390×844;状态行 absolute overlay + pointer-events:none,
|
||||
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 固定取证视口:390×844 移动竖屏基准;user-scalable=no 防缩放扰动像素哈希。 -->
|
||||
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
|
||||
<title>W-G1 generated game</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
|
||||
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
|
||||
#wrap { position: relative; width: 390px; margin: 0 auto; }
|
||||
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
|
||||
/* 状态行默认隐藏:boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
|
||||
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
|
||||
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
|
||||
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
|
||||
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
|
||||
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div id="status">booting…</div>
|
||||
<!-- 受控画布(opts.canvas 占位/输入兜底);real 引擎自建 #game-engine 叠加。 -->
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- esbuild 产物:iife 全局 __GameBundle(含 GameHostFactory + bootGameHost)。 -->
|
||||
<script src="./bundle.iife.js"></script>
|
||||
<script>
|
||||
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
|
||||
(function () {
|
||||
'use strict';
|
||||
var statusEl = document.getElementById('status');
|
||||
var canvas = document.getElementById('game');
|
||||
// 假绿守卫C·真接线探针:每次引擎能力被调,recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
|
||||
window.__engineCalls = [];
|
||||
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
|
||||
// 就绪/错误标记初值。
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = null;
|
||||
try {
|
||||
var G = window.__GameBundle;
|
||||
if (!G || typeof G.bootGameHost !== 'function') {
|
||||
throw new Error('__GameBundle.bootGameHost 未找到(bundle.iife.js 未正确加载或 globalName 不符)');
|
||||
}
|
||||
G.bootGameHost({
|
||||
canvas: canvas,
|
||||
statusEl: statusEl,
|
||||
recHook: recHook,
|
||||
// onReady:收集 internals(uncaught[] 等)供假绿守卫B 读。
|
||||
onReady: function (host, internals) {
|
||||
window.__genHost = host;
|
||||
window.__genInternals = internals || null;
|
||||
}
|
||||
})
|
||||
.then(function (host) {
|
||||
window.__genHost = window.__genHost || host;
|
||||
// 可测性别名:守卫H 经此读游戏可观测状态(host.state() 透传 game._forensicsView().state;游戏未实现则返回 {phase:'booting'})。
|
||||
// 宿主侧挂载,工厂仍零 window;与 ref 在 onReady 挂 __refForensics 同范式。
|
||||
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
|
||||
window.__genBooted = true; // CDP 探活标记(区分「脚本没加载」与「boot 抛错」)
|
||||
})
|
||||
.catch(function (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
|
||||
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
|
||||
});
|
||||
} catch (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
20
game-runtime/games/_wg1-gen/flappy/play-spec.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"driver": {
|
||||
"type": "flap-to-gap",
|
||||
"steps": 70,
|
||||
"stepMs": 110,
|
||||
"margin": 14
|
||||
},
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
],
|
||||
"assertAfterPlay": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"why": "适配性真玩(读 bird.y/nextGap.centerY 拍翅穿缝)→得分上升;物理太硬秒死或穿不过则 0"
|
||||
}
|
||||
],
|
||||
"expectLatch": true
|
||||
}
|
||||
34
game-runtime/games/_wg1-gen/invaders/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/invaders/generated-factory.js":{"bytes":8364,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/invaders/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/invaders/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/invaders/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/invaders/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164199},"games/_wg1-gen/invaders/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5346},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4159},"src/plugins/palette-post/impl.js":{"bytesInOutput":4216},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/invaders/generated-factory.js":{"bytesInOutput":3243}},"bytes":211003}}}
|
||||
77
game-runtime/games/_wg1-gen/invaders/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/invaders/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
game-runtime/games/_wg1-gen/invaders/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
171
game-runtime/games/_wg1-gen/invaders/evidence/verdict.json
Normal file
@ -0,0 +1,171 @@
|
||||
{
|
||||
"gameId": "invaders",
|
||||
"url": "http://localhost:4320/games/_wg1-gen/invaders/index.html",
|
||||
"ts": null,
|
||||
"guards": {
|
||||
"A_boot": {
|
||||
"pass": true
|
||||
},
|
||||
"C_frame": {
|
||||
"pass": true,
|
||||
"f0": 47,
|
||||
"f1": 80,
|
||||
"delta": 33
|
||||
},
|
||||
"I_control": {
|
||||
"pass": true,
|
||||
"skipped": "未声明 controlCheck(向后兼容)"
|
||||
},
|
||||
"D_render": {
|
||||
"pass": true,
|
||||
"bright": 51625,
|
||||
"maxCh": 255
|
||||
},
|
||||
"E_live": {
|
||||
"pass": true,
|
||||
"distinctStates": 47,
|
||||
"samples": 47
|
||||
},
|
||||
"B_uncaught": {
|
||||
"pass": true,
|
||||
"uncaught": []
|
||||
},
|
||||
"F_wiring": {
|
||||
"pass": true,
|
||||
"callCount": 114,
|
||||
"sample": [
|
||||
"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",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx"
|
||||
],
|
||||
"expected": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
]
|
||||
},
|
||||
"G_input": {
|
||||
"pass": true,
|
||||
"inputHash": "9dc71645",
|
||||
"controlHash": "27ebc378",
|
||||
"atFrame": 1544,
|
||||
"ctrlFrame": -1
|
||||
},
|
||||
"H_progress": {
|
||||
"pass": true,
|
||||
"checks": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"before": 0,
|
||||
"after": 12,
|
||||
"pass": true,
|
||||
"why": "适配性真玩(炮台移到敌人 x 开火)下击毁敌人→得分上升;打不中/无碰撞则不升"
|
||||
}
|
||||
],
|
||||
"latch": {
|
||||
"pass": true,
|
||||
"dwelledMs": 600,
|
||||
"after": "gameover",
|
||||
"reason": ""
|
||||
},
|
||||
"state0": {
|
||||
"phase": "playing",
|
||||
"result": null,
|
||||
"score": 0,
|
||||
"enemiesRemaining": 12,
|
||||
"targets": [
|
||||
{
|
||||
"x": 151.33333333333306,
|
||||
"y": 95,
|
||||
"idx": 0,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 201.3333333333329,
|
||||
"y": 95,
|
||||
"idx": 1,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 251.3333333333329,
|
||||
"y": 95,
|
||||
"idx": 2,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 301.3333333333342,
|
||||
"y": 95,
|
||||
"idx": 3,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 151.33333333333306,
|
||||
"y": 135,
|
||||
"idx": 4,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 201.3333333333329,
|
||||
"y": 135,
|
||||
"idx": 5,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 251.3333333333329,
|
||||
"y": 135,
|
||||
"idx": 6,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 301.3333333333342,
|
||||
"y": 135,
|
||||
"idx": 7,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 151.33333333333306,
|
||||
"y": 175,
|
||||
"idx": 8,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 201.3333333333329,
|
||||
"y": 175,
|
||||
"idx": 9,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 251.3333333333329,
|
||||
"y": 175,
|
||||
"idx": 10,
|
||||
"occupied": false
|
||||
},
|
||||
{
|
||||
"x": 301.3333333333342,
|
||||
"y": 175,
|
||||
"idx": 11,
|
||||
"occupied": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"state1": {
|
||||
"phase": "gameover",
|
||||
"result": "win",
|
||||
"score": 12,
|
||||
"enemiesRemaining": 0,
|
||||
"targets": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": true,
|
||||
"notes": []
|
||||
}
|
||||
310
game-runtime/games/_wg1-gen/invaders/generated-factory.js
Normal file
@ -0,0 +1,310 @@
|
||||
'use strict';
|
||||
|
||||
export default function createGame(opts) {
|
||||
const o = opts || {};
|
||||
let ctx;
|
||||
let phase; // 'playing' | 'gameover'
|
||||
let result; // null | 'win' | 'lose'
|
||||
let score;
|
||||
let enemies; // { idx, x, y, initX, initY, alive }
|
||||
let enemyDir; // 1 or -1
|
||||
let downTimer; // seconds
|
||||
let paddleX; // 炮台中心 x
|
||||
let bullets; // { x, y, active, w, h }
|
||||
let pointerSub;
|
||||
|
||||
const ENEMY_DOWN_INTERVAL = 1.5;
|
||||
const ENEMY_SPEED_HOR = 40; // px/s
|
||||
const BULLET_SPEED = 400; // px/s
|
||||
const STEP_DOWN = 30; // 下压步长
|
||||
const PADDLE_TOP = 844 - 20; // 炮台矩形顶部 y
|
||||
const RESTART_RECT = { x: 95, y: 400, w: 200, h: 50 };
|
||||
|
||||
// ---------- 事件处理 ----------
|
||||
function handlePointerDown(e) {
|
||||
const px = e.x;
|
||||
const py = e.y;
|
||||
|
||||
if (phase === 'gameover') {
|
||||
if (px >= RESTART_RECT.x && px <= RESTART_RECT.x + RESTART_RECT.w &&
|
||||
py >= RESTART_RECT.y && py <= RESTART_RECT.y + RESTART_RECT.h) {
|
||||
resetGame();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// playing → 移动炮台 + 发射子弹
|
||||
paddleX = Math.max(20, Math.min(370, px)); // 炮台宽 40 → 半宽 20
|
||||
|
||||
const bulletX = paddleX;
|
||||
bullets.push({ x: bulletX, y: PADDLE_TOP, active: true, w: 6, h: 12 });
|
||||
|
||||
// 粒子(开火)
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: bulletX, y: PADDLE_TOP },
|
||||
count: 10,
|
||||
speed: 60,
|
||||
particleTime: 0.3,
|
||||
colorStart: { r: 0.2, g: 0.8, b: 1, a: 1 },
|
||||
colorEnd: { r: 0, g: 0.3, b: 0.6, a: 0 }
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
if (eng && eng.audio && eng.audio.synth) {
|
||||
try { eng.audio.synth.synthSfx([1, 0.05, 440, , , 0.1, , 1.4]); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 游戏重置 ----------
|
||||
function resetGame() {
|
||||
phase = 'playing';
|
||||
result = null;
|
||||
score = 0;
|
||||
bullets = [];
|
||||
paddleX = 390 / 2;
|
||||
enemyDir = 1;
|
||||
downTimer = ENEMY_DOWN_INTERVAL;
|
||||
|
||||
enemies.forEach(e => {
|
||||
e.x = e.initX;
|
||||
e.y = e.initY;
|
||||
e.alive = true;
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 敌人下移 ----------
|
||||
function shiftDown() {
|
||||
enemies.forEach(e => {
|
||||
if (e.alive) e.y += STEP_DOWN;
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 生命周期 ----------
|
||||
function init(boot) {
|
||||
ctx = boot.ctx;
|
||||
phase = 'playing';
|
||||
result = null;
|
||||
score = 0;
|
||||
bullets = [];
|
||||
paddleX = 390 / 2;
|
||||
enemyDir = 1;
|
||||
downTimer = ENEMY_DOWN_INTERVAL;
|
||||
|
||||
// 初始化敌人(3行×4列)
|
||||
enemies = [];
|
||||
const startY = 80;
|
||||
const rows = 3;
|
||||
const cols = 4;
|
||||
const enemyW = 30;
|
||||
const colGap = 50;
|
||||
const rowGap = 40;
|
||||
const totalW = (cols - 1) * colGap + enemyW;
|
||||
const offsetLeft = (390 - totalW) / 2 + enemyW / 2;
|
||||
const offsetTop = startY + enemyW / 2; // 中心 Y 偏移
|
||||
|
||||
let idx = 0;
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const ex = offsetLeft + c * colGap;
|
||||
const ey = offsetTop + r * rowGap;
|
||||
enemies.push({
|
||||
idx,
|
||||
x: ex, y: ey,
|
||||
initX: ex, initY: ey,
|
||||
alive: true
|
||||
});
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
pointerSub = ctx.getInput().on('pointerdown', handlePointerDown);
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
if (phase !== 'playing') return;
|
||||
|
||||
// ---------- 敌人逻辑 ----------
|
||||
const aliveEnemies = enemies.filter(e => e.alive);
|
||||
if (aliveEnemies.length > 0) {
|
||||
// 水平移动
|
||||
const dx = ENEMY_SPEED_HOR * dt * enemyDir;
|
||||
aliveEnemies.forEach(e => e.x += dx);
|
||||
|
||||
// 边界检测 → 反转 + 下移 + 重置计时器
|
||||
let minX = Infinity, maxX = -Infinity;
|
||||
aliveEnemies.forEach(e => {
|
||||
if (e.x < minX) minX = e.x;
|
||||
if (e.x > maxX) maxX = e.x;
|
||||
});
|
||||
if (minX - 15 <= 0) {
|
||||
enemyDir = 1;
|
||||
shiftDown();
|
||||
downTimer = ENEMY_DOWN_INTERVAL;
|
||||
} else if (maxX + 15 >= 390) {
|
||||
enemyDir = -1;
|
||||
shiftDown();
|
||||
downTimer = ENEMY_DOWN_INTERVAL;
|
||||
}
|
||||
|
||||
// 定时下压
|
||||
downTimer -= dt;
|
||||
if (downTimer <= 0) {
|
||||
downTimer += ENEMY_DOWN_INTERVAL;
|
||||
shiftDown();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 子弹逻辑 ----------
|
||||
bullets.forEach(b => {
|
||||
if (!b.active) return;
|
||||
b.y -= BULLET_SPEED * dt;
|
||||
if (b.y + b.h / 2 < 0) b.active = false;
|
||||
});
|
||||
|
||||
// 子弹与敌人碰撞
|
||||
for (const b of bullets) {
|
||||
if (!b.active) continue;
|
||||
for (const e of enemies) {
|
||||
if (!e.alive) continue;
|
||||
// 矩形相交检测
|
||||
const bLeft = b.x - b.w / 2;
|
||||
const bRight = b.x + b.w / 2;
|
||||
const bTop = b.y - b.h / 2;
|
||||
const bBottom = b.y + b.h / 2;
|
||||
const eLeft = e.x - 15;
|
||||
const eRight = e.x + 15;
|
||||
const eTop = e.y - 15;
|
||||
const eBottom = e.y + 15;
|
||||
if (bRight >= eLeft && bLeft <= eRight &&
|
||||
bBottom >= eTop && bTop <= eBottom) {
|
||||
e.alive = false;
|
||||
b.active = false;
|
||||
score++;
|
||||
|
||||
// 命中粒子
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: e.x, y: e.y },
|
||||
count: 20,
|
||||
speed: 100,
|
||||
particleTime: 0.4,
|
||||
colorStart: { r: 1, g: 0.7, b: 0.2, a: 1 },
|
||||
colorEnd: { r: 0.8, g: 0.2, b: 0, a: 0 }
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
if (eng && eng.audio && eng.audio.synth) {
|
||||
try { eng.audio.synth.synthSfx([1, 0.05, 600, , , 0.1, , 1.2]); } catch (_) {}
|
||||
}
|
||||
break; // 一颗子弹只打一个敌人
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清除失效子弹
|
||||
bullets = bullets.filter(b => b.active);
|
||||
|
||||
// ---------- 胜负判定 ----------
|
||||
const aliveCount = enemies.filter(e => e.alive).length;
|
||||
if (aliveCount === 0) {
|
||||
phase = 'gameover';
|
||||
result = 'win';
|
||||
} else {
|
||||
let maxY = -Infinity;
|
||||
enemies.forEach(e => { if (e.alive && e.y > maxY) maxY = e.y; });
|
||||
if (maxY + 15 >= PADDLE_TOP) {
|
||||
phase = 'gameover';
|
||||
result = 'lose';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function render(g) {
|
||||
// 背景
|
||||
g.fillStyle = '#0a0a1a';
|
||||
g.fillRect(0, 0, 390, 844);
|
||||
|
||||
// 敌人(存活)
|
||||
g.fillStyle = '#33ff66';
|
||||
enemies.forEach(e => {
|
||||
if (!e.alive) return;
|
||||
g.fillRect(e.x - 15, e.y - 15, 30, 30);
|
||||
});
|
||||
|
||||
// 子弹
|
||||
g.fillStyle = '#ffcc00';
|
||||
bullets.forEach(b => {
|
||||
g.fillRect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h);
|
||||
});
|
||||
|
||||
// 炮台
|
||||
g.fillStyle = '#66aaff';
|
||||
g.fillRect(paddleX - 20, 844 - 20, 40, 20);
|
||||
|
||||
// HUD
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '16px monospace';
|
||||
g.textAlign = 'left';
|
||||
g.fillText('得分: ' + score, 10, 25);
|
||||
const aliveCount = enemies.filter(e => e.alive).length;
|
||||
g.fillText('剩余: ' + aliveCount, 150, 25);
|
||||
if (phase === 'playing') g.fillText('状态: 战斗', 290, 25);
|
||||
|
||||
// 游戏结束界面
|
||||
if (phase === 'gameover') {
|
||||
g.fillStyle = 'rgba(0,0,0,0.5)';
|
||||
g.fillRect(0, 0, 390, 844);
|
||||
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = 'bold 32px monospace';
|
||||
g.textAlign = 'center';
|
||||
if (result === 'win') g.fillText('你赢了!', 195, 300);
|
||||
else if (result === 'lose') g.fillText('你输了!', 195, 300);
|
||||
|
||||
g.fillStyle = '#228833';
|
||||
g.fillRect(RESTART_RECT.x, RESTART_RECT.y, RESTART_RECT.w, RESTART_RECT.h);
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '20px monospace';
|
||||
g.fillText('点击重玩', RESTART_RECT.x + RESTART_RECT.w / 2, RESTART_RECT.y + RESTART_RECT.h / 2 + 6);
|
||||
}
|
||||
|
||||
g.textAlign = 'left';
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
try { pointerSub && pointerSub.cancel(); } catch (_) {}
|
||||
pointerSub = null;
|
||||
enemies = [];
|
||||
bullets = [];
|
||||
}
|
||||
|
||||
// ---------- 可测性视图 ----------
|
||||
function state() {
|
||||
const alive = enemies.filter(e => e.alive).map(e => ({
|
||||
x: e.x,
|
||||
y: e.y,
|
||||
idx: e.idx,
|
||||
occupied: false
|
||||
}));
|
||||
return {
|
||||
phase,
|
||||
result,
|
||||
score,
|
||||
enemiesRemaining: alive.length,
|
||||
targets: alive
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
update,
|
||||
render,
|
||||
destroy,
|
||||
_forensicsView: () => ({ seed: o.seed || 0, state })
|
||||
};
|
||||
}
|
||||
89
game-runtime/games/_wg1-gen/invaders/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 uncaught[])。
|
||||
布局收编(对齐 ref 教训):canvas 顶在 (0,0) 满幅 390×844;状态行 absolute overlay + pointer-events:none,
|
||||
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 固定取证视口:390×844 移动竖屏基准;user-scalable=no 防缩放扰动像素哈希。 -->
|
||||
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
|
||||
<title>W-G1 generated game</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
|
||||
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
|
||||
#wrap { position: relative; width: 390px; margin: 0 auto; }
|
||||
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
|
||||
/* 状态行默认隐藏:boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
|
||||
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
|
||||
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
|
||||
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
|
||||
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
|
||||
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div id="status">booting…</div>
|
||||
<!-- 受控画布(opts.canvas 占位/输入兜底);real 引擎自建 #game-engine 叠加。 -->
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- esbuild 产物:iife 全局 __GameBundle(含 GameHostFactory + bootGameHost)。 -->
|
||||
<script src="./bundle.iife.js"></script>
|
||||
<script>
|
||||
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
|
||||
(function () {
|
||||
'use strict';
|
||||
var statusEl = document.getElementById('status');
|
||||
var canvas = document.getElementById('game');
|
||||
// 假绿守卫C·真接线探针:每次引擎能力被调,recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
|
||||
window.__engineCalls = [];
|
||||
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
|
||||
// 就绪/错误标记初值。
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = null;
|
||||
try {
|
||||
var G = window.__GameBundle;
|
||||
if (!G || typeof G.bootGameHost !== 'function') {
|
||||
throw new Error('__GameBundle.bootGameHost 未找到(bundle.iife.js 未正确加载或 globalName 不符)');
|
||||
}
|
||||
G.bootGameHost({
|
||||
canvas: canvas,
|
||||
statusEl: statusEl,
|
||||
recHook: recHook,
|
||||
// onReady:收集 internals(uncaught[] 等)供假绿守卫B 读。
|
||||
onReady: function (host, internals) {
|
||||
window.__genHost = host;
|
||||
window.__genInternals = internals || null;
|
||||
}
|
||||
})
|
||||
.then(function (host) {
|
||||
window.__genHost = window.__genHost || host;
|
||||
// 可测性别名:守卫H 经此读游戏可观测状态(host.state() 透传 game._forensicsView().state;游戏未实现则返回 {phase:'booting'})。
|
||||
// 宿主侧挂载,工厂仍零 window;与 ref 在 onReady 挂 __refForensics 同范式。
|
||||
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
|
||||
window.__genBooted = true; // CDP 探活标记(区分「脚本没加载」与「boot 抛错」)
|
||||
})
|
||||
.catch(function (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
|
||||
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
|
||||
});
|
||||
} catch (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
19
game-runtime/games/_wg1-gen/invaders/play-spec.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"driver": {
|
||||
"type": "tap-targets",
|
||||
"steps": 50,
|
||||
"stepMs": 260
|
||||
},
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
],
|
||||
"assertAfterPlay": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"why": "适配性真玩(炮台移到敌人 x 开火)下击毁敌人→得分上升;打不中/无碰撞则不升"
|
||||
}
|
||||
],
|
||||
"expectLatch": true
|
||||
}
|
||||
34
game-runtime/games/_wg1-gen/jianfeng/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/jianfeng/generated-factory.js":{"bytes":8875,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/jianfeng/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/jianfeng/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/jianfeng/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/jianfeng/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164141},"games/_wg1-gen/jianfeng/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5357},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4159},"src/plugins/palette-post/impl.js":{"bytesInOutput":4218},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/jianfeng/generated-factory.js":{"bytesInOutput":3420}},"bytes":211135}}}
|
||||
77
game-runtime/games/_wg1-gen/jianfeng/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/jianfeng/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
game-runtime/games/_wg1-gen/jianfeng/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
106
game-runtime/games/_wg1-gen/jianfeng/evidence/verdict.json
Normal file
@ -0,0 +1,106 @@
|
||||
{
|
||||
"gameId": "jianfeng",
|
||||
"url": "http://localhost:4320/games/_wg1-gen/jianfeng/index.html",
|
||||
"ts": null,
|
||||
"guards": {
|
||||
"A_boot": {
|
||||
"pass": true
|
||||
},
|
||||
"C_frame": {
|
||||
"pass": true,
|
||||
"f0": 56,
|
||||
"f1": 87,
|
||||
"delta": 31
|
||||
},
|
||||
"I_control": {
|
||||
"pass": true,
|
||||
"skipped": "未声明 controlCheck(向后兼容)"
|
||||
},
|
||||
"D_render": {
|
||||
"pass": true,
|
||||
"bright": 55058,
|
||||
"maxCh": 255
|
||||
},
|
||||
"E_live": {
|
||||
"pass": true,
|
||||
"distinctStates": 29,
|
||||
"samples": 29
|
||||
},
|
||||
"B_uncaught": {
|
||||
"pass": true,
|
||||
"uncaught": []
|
||||
},
|
||||
"F_wiring": {
|
||||
"pass": true,
|
||||
"callCount": 974,
|
||||
"sample": [
|
||||
"particles.spawnEmitter",
|
||||
"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",
|
||||
"particles.spawnEmitter"
|
||||
],
|
||||
"expected": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
]
|
||||
},
|
||||
"G_input": {
|
||||
"pass": true,
|
||||
"inputHash": "a866eaf1",
|
||||
"controlHash": "2cc7cc1e",
|
||||
"atFrame": 951,
|
||||
"ctrlFrame": 957
|
||||
},
|
||||
"H_progress": {
|
||||
"pass": true,
|
||||
"checks": [
|
||||
{
|
||||
"path": "inserted",
|
||||
"op": "increased",
|
||||
"before": 0,
|
||||
"after": 12,
|
||||
"pass": true,
|
||||
"why": "定时连发射针→已插入数上升;点击失灵/不响应则不升"
|
||||
}
|
||||
],
|
||||
"latch": {
|
||||
"pass": true,
|
||||
"dwelledMs": 600,
|
||||
"after": "gameover",
|
||||
"reason": ""
|
||||
},
|
||||
"state0": {
|
||||
"phase": "playing",
|
||||
"result": null,
|
||||
"inserted": 0,
|
||||
"target": 12,
|
||||
"score": 0,
|
||||
"remaining": 12,
|
||||
"progress": 0,
|
||||
"gameoverReason": "",
|
||||
"diskAngle": 0.5600000000000003
|
||||
},
|
||||
"state1": {
|
||||
"phase": "gameover",
|
||||
"result": "win",
|
||||
"inserted": 12,
|
||||
"target": 12,
|
||||
"score": 12,
|
||||
"remaining": 0,
|
||||
"progress": 1,
|
||||
"gameoverReason": "cleared_all",
|
||||
"diskAngle": 1.7968146928203255
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": true,
|
||||
"notes": []
|
||||
}
|
||||
326
game-runtime/games/_wg1-gen/jianfeng/generated-factory.js
Normal file
@ -0,0 +1,326 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 见缝插针游戏工厂模块
|
||||
* 符合 GameHostFactory 契约,支持粒子/音效/受控时钟与随机。
|
||||
* 可测性:_forensicsView 提供实时状态快照。
|
||||
*/
|
||||
export default function createGame(opts) {
|
||||
const o = opts || {};
|
||||
let ctx = null;
|
||||
let seed = 0;
|
||||
|
||||
// 游戏配置
|
||||
const TARGET = o.target || 12;
|
||||
const R = 100; // 圆盘半径
|
||||
const CX = 195; // 圆心x
|
||||
const CY = 350; // 圆心y
|
||||
const ANG_VEL = 0.6; // 弧度/秒
|
||||
const COLL_THRESH = Math.PI / 18; // 10度
|
||||
|
||||
// 游戏状态
|
||||
let phase = 'booting'; // 'booting' -> 'playing' -> 'gameover'
|
||||
let result = null; // null | 'win' | 'lose'
|
||||
let inserted = 0;
|
||||
let needles = []; // { angle: number }[]
|
||||
let needleReady = true; // 底部有待发射针
|
||||
let diskAngle = 0;
|
||||
let gameoverReason = '';
|
||||
let subscriptions = [];
|
||||
|
||||
// 点击订阅句柄
|
||||
let pointerSub = null;
|
||||
|
||||
// 重置函数(用于重玩)
|
||||
function reset() {
|
||||
phase = 'playing';
|
||||
result = null;
|
||||
inserted = 0;
|
||||
needles = [];
|
||||
needleReady = true;
|
||||
diskAngle = 0;
|
||||
gameoverReason = '';
|
||||
|
||||
// 预置 0 或 1 根针
|
||||
const preCount = ctx.random.range(0, 1) < 0.5 ? 0 : 1;
|
||||
for (let i = 0; i < preCount; i++) {
|
||||
const a = ctx.random.range(0, 2 * Math.PI);
|
||||
needles.push({ angle: a });
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
function init(boot) {
|
||||
ctx = boot.ctx;
|
||||
seed = boot.seed || 0;
|
||||
|
||||
// 订阅 pointerdown
|
||||
pointerSub = ctx.getInput().on('pointerdown', onPointerDown);
|
||||
subscriptions.push(pointerSub);
|
||||
|
||||
// 初始状态
|
||||
reset();
|
||||
|
||||
// 尝试触发一次引擎能力(开场的粒子没啥用,但合规)
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: CX, y: CY },
|
||||
emitRate: 10,
|
||||
particleTime: 0.5,
|
||||
speed: 50,
|
||||
colorStart: { r: 0.3, g: 0.7, b: 1, a: 1 },
|
||||
colorEnd: { r: 0, g: 0.2, b: 0.6, a: 0 },
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerDown(e) {
|
||||
const now = ctx.time.nowMs();
|
||||
const px = e.x;
|
||||
const py = e.y;
|
||||
|
||||
if (phase === 'gameover') {
|
||||
// 检查是否点击“重玩”按钮区域(底部中央一个矩形区域)
|
||||
const btnX = CX - 60;
|
||||
const btnY = 600;
|
||||
const btnW = 120;
|
||||
const btnH = 50;
|
||||
if (px >= btnX && px <= btnX + btnW && py >= btnY && py <= btnY + btnH) {
|
||||
reset();
|
||||
// 重新随机预置(reset中已经做)
|
||||
// 触发一次重置粒子
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: CX, y: CY },
|
||||
count: 15,
|
||||
speed: 80,
|
||||
particleTime: 0.4,
|
||||
colorStart: { r: 0.2, g: 1, b: 0.3, a: 1 },
|
||||
colorEnd: { r: 0, g: 0.5, b: 0, a: 0 },
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有 playing 且 needleReady 才发射
|
||||
if (phase !== 'playing' || !needleReady) return;
|
||||
|
||||
const newAngle = diskAngle; // 插入角度 = 当前圆盘角度
|
||||
|
||||
// 碰撞检测(与已插入针)
|
||||
let collision = false;
|
||||
for (let i = 0; i < needles.length; i++) {
|
||||
let diff = Math.abs(newAngle - needles[i].angle);
|
||||
if (diff > Math.PI) diff = 2 * Math.PI - diff;
|
||||
if (diff < COLL_THRESH) {
|
||||
collision = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (collision) {
|
||||
// 失败
|
||||
phase = 'gameover';
|
||||
result = 'lose';
|
||||
gameoverReason = 'collision';
|
||||
needleReady = false; // 待发射针消失
|
||||
|
||||
// 触发失败粒子(在圆盘中心爆发红色粒子)
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: CX, y: CY },
|
||||
count: 30,
|
||||
speed: 120,
|
||||
particleTime: 0.7,
|
||||
colorStart: { r: 1, g: 0.2, b: 0.2, a: 1 },
|
||||
colorEnd: { r: 0.6, g: 0, b: 0, a: 0 },
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
if (eng && eng.audio && eng.audio.synth) {
|
||||
try {
|
||||
eng.audio.synth.synthSfx([1, 0.08, 150, , , 0.3, , 1.0]);
|
||||
} catch (_) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 成功插入
|
||||
needles.push({ angle: newAngle });
|
||||
inserted++;
|
||||
needleReady = true; // 底部生成新针
|
||||
|
||||
// 检查胜利
|
||||
if (inserted >= TARGET) {
|
||||
phase = 'gameover';
|
||||
result = 'win';
|
||||
gameoverReason = 'cleared_all';
|
||||
needleReady = false;
|
||||
}
|
||||
|
||||
// 成功粒子(在针头位置)
|
||||
const headX = CX + (R + 8) * Math.cos(newAngle);
|
||||
const headY = CY + (R + 8) * Math.sin(newAngle);
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: headX, y: headY },
|
||||
count: 25,
|
||||
speed: 90,
|
||||
particleTime: 0.5,
|
||||
colorStart: { r: 1, g: 0.8, b: 0.2, a: 1 },
|
||||
colorEnd: { r: 0.8, g: 0.4, b: 0, a: 0 },
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
if (eng && eng.audio && eng.audio.synth) {
|
||||
try {
|
||||
eng.audio.synth.synthSfx([1, 0.05, 440, , , 0.15, , 0.8]);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
const now = ctx.time.nowMs();
|
||||
|
||||
// 只有 playing 时旋转圆盘
|
||||
if (phase === 'playing') {
|
||||
diskAngle = (diskAngle + ANG_VEL * dt) % (2 * Math.PI);
|
||||
}
|
||||
|
||||
// 每帧触发 math 调用(合规)
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.math) {
|
||||
try {
|
||||
const t = (now / 1000) % 1;
|
||||
eng.math.lerp(0, 1, t);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function render(g) {
|
||||
// 深色背景满幅
|
||||
g.fillStyle = '#0d0d1a';
|
||||
g.fillRect(0, 0, 390, 844);
|
||||
|
||||
// 绘制 HUD(顶部)
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '18px monospace';
|
||||
g.textAlign = 'left';
|
||||
g.fillText('针: ' + inserted + '/' + TARGET, 20, 30);
|
||||
if (phase === 'gameover') {
|
||||
g.fillStyle = result === 'win' ? '#4cff4c' : '#ff4c4c';
|
||||
g.fillText(result === 'win' ? '胜利!' : '失败!', 20, 55);
|
||||
}
|
||||
|
||||
// 绘制圆盘
|
||||
g.save();
|
||||
g.translate(CX, CY);
|
||||
g.beginPath();
|
||||
g.arc(0, 0, R, 0, 2 * Math.PI);
|
||||
g.fillStyle = '#2a2a3e';
|
||||
g.fill();
|
||||
g.strokeStyle = '#88aacc';
|
||||
g.lineWidth = 3;
|
||||
g.stroke();
|
||||
|
||||
// 绘制已插入的针
|
||||
for (const nd of needles) {
|
||||
const angle = nd.angle;
|
||||
const x0 = 0;
|
||||
const y0 = 0;
|
||||
const x1 = (R + 20) * Math.cos(angle);
|
||||
const y1 = (R + 20) * Math.sin(angle);
|
||||
g.beginPath();
|
||||
g.moveTo(x0, y0);
|
||||
g.lineTo(x1, y1);
|
||||
g.strokeStyle = '#ffcc33';
|
||||
g.lineWidth = 3;
|
||||
g.stroke();
|
||||
// 针头小圆点
|
||||
g.beginPath();
|
||||
g.arc(x1, y1, 3, 0, 2 * Math.PI);
|
||||
g.fillStyle = '#ffaa00';
|
||||
g.fill();
|
||||
}
|
||||
|
||||
// 绘制待发射针(如果 needleReady 且 playing)
|
||||
if (needleReady && phase === 'playing') {
|
||||
// 从底部指向圆心
|
||||
const bx = 0;
|
||||
const by = 430; // 相对于圆心偏移 (CY=350, 底部y约780 -> 偏移430)
|
||||
const ex = 0;
|
||||
const ey = 0; // 圆心
|
||||
g.beginPath();
|
||||
g.moveTo(bx, by);
|
||||
g.lineTo(ex, ey);
|
||||
g.strokeStyle = '#66ff66';
|
||||
g.lineWidth = 4;
|
||||
g.stroke();
|
||||
// 针尾小圆
|
||||
g.beginPath();
|
||||
g.arc(bx, by, 4, 0, 2 * Math.PI);
|
||||
g.fillStyle = '#66ff66';
|
||||
g.fill();
|
||||
}
|
||||
|
||||
// 绘制重玩按钮(gameover时)
|
||||
if (phase === 'gameover') {
|
||||
g.translate(-CX, -CY); // 先平移回原点
|
||||
const btnX = CX - 60;
|
||||
const btnY = 600;
|
||||
const btnW = 120;
|
||||
const btnH = 50;
|
||||
g.fillStyle = '#335566';
|
||||
g.fillRect(btnX, btnY, btnW, btnH);
|
||||
g.strokeStyle = '#aaccdd';
|
||||
g.lineWidth = 2;
|
||||
g.strokeRect(btnX, btnY, btnW, btnH);
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '20px monospace';
|
||||
g.textAlign = 'center';
|
||||
g.fillText('重玩', CX, btnY + 33);
|
||||
} else {
|
||||
g.restore(); // 只 restore 一次?注意上面有多次 save/restore,这里简单处理:直接重置变换
|
||||
g.setTransform(1, 0, 0, 1, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
for (const sub of subscriptions) {
|
||||
try { sub.cancel(); } catch (_) {}
|
||||
}
|
||||
subscriptions = [];
|
||||
pointerSub = null;
|
||||
needles = [];
|
||||
}
|
||||
|
||||
function _forensicsView() {
|
||||
return {
|
||||
seed: seed,
|
||||
state: () => ({
|
||||
phase: phase,
|
||||
result: result,
|
||||
inserted: inserted,
|
||||
target: TARGET,
|
||||
score: inserted, // 通用字段
|
||||
remaining: TARGET - inserted,
|
||||
progress: TARGET > 0 ? inserted / TARGET : 0,
|
||||
gameoverReason: gameoverReason,
|
||||
diskAngle: diskAngle,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return { init, update, render, destroy, _forensicsView };
|
||||
}
|
||||
89
game-runtime/games/_wg1-gen/jianfeng/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 uncaught[])。
|
||||
布局收编(对齐 ref 教训):canvas 顶在 (0,0) 满幅 390×844;状态行 absolute overlay + pointer-events:none,
|
||||
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 固定取证视口:390×844 移动竖屏基准;user-scalable=no 防缩放扰动像素哈希。 -->
|
||||
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
|
||||
<title>W-G1 generated game</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
|
||||
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
|
||||
#wrap { position: relative; width: 390px; margin: 0 auto; }
|
||||
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
|
||||
/* 状态行默认隐藏:boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
|
||||
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
|
||||
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
|
||||
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
|
||||
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
|
||||
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div id="status">booting…</div>
|
||||
<!-- 受控画布(opts.canvas 占位/输入兜底);real 引擎自建 #game-engine 叠加。 -->
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- esbuild 产物:iife 全局 __GameBundle(含 GameHostFactory + bootGameHost)。 -->
|
||||
<script src="./bundle.iife.js"></script>
|
||||
<script>
|
||||
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
|
||||
(function () {
|
||||
'use strict';
|
||||
var statusEl = document.getElementById('status');
|
||||
var canvas = document.getElementById('game');
|
||||
// 假绿守卫C·真接线探针:每次引擎能力被调,recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
|
||||
window.__engineCalls = [];
|
||||
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
|
||||
// 就绪/错误标记初值。
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = null;
|
||||
try {
|
||||
var G = window.__GameBundle;
|
||||
if (!G || typeof G.bootGameHost !== 'function') {
|
||||
throw new Error('__GameBundle.bootGameHost 未找到(bundle.iife.js 未正确加载或 globalName 不符)');
|
||||
}
|
||||
G.bootGameHost({
|
||||
canvas: canvas,
|
||||
statusEl: statusEl,
|
||||
recHook: recHook,
|
||||
// onReady:收集 internals(uncaught[] 等)供假绿守卫B 读。
|
||||
onReady: function (host, internals) {
|
||||
window.__genHost = host;
|
||||
window.__genInternals = internals || null;
|
||||
}
|
||||
})
|
||||
.then(function (host) {
|
||||
window.__genHost = window.__genHost || host;
|
||||
// 可测性别名:守卫H 经此读游戏可观测状态(host.state() 透传 game._forensicsView().state;游戏未实现则返回 {phase:'booting'})。
|
||||
// 宿主侧挂载,工厂仍零 window;与 ref 在 onReady 挂 __refForensics 同范式。
|
||||
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
|
||||
window.__genBooted = true; // CDP 探活标记(区分「脚本没加载」与「boot 抛错」)
|
||||
})
|
||||
.catch(function (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
|
||||
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
|
||||
});
|
||||
} catch (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
137
game-runtime/games/_wg1-gen/jianfeng/play-spec.json
Normal file
@ -0,0 +1,137 @@
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 700
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 760
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 520
|
||||
}
|
||||
],
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
],
|
||||
"assertAfterPlay": [
|
||||
{
|
||||
"path": "inserted",
|
||||
"op": "increased",
|
||||
"why": "定时连发射针→已插入数上升;点击失灵/不响应则不升"
|
||||
}
|
||||
],
|
||||
"expectLatch": true
|
||||
}
|
||||
34
game-runtime/games/_wg1-gen/match3/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/match3/generated-factory.js":{"bytes":8400,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/match3/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/match3/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/match3/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/match3/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164219},"games/_wg1-gen/match3/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5351},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4214},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/match3/generated-factory.js":{"bytesInOutput":3296}},"bytes":211086}}}
|
||||
77
game-runtime/games/_wg1-gen/match3/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/match3/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
game-runtime/games/_wg1-gen/match3/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
1025
game-runtime/games/_wg1-gen/match3/evidence/verdict.json
Normal file
306
game-runtime/games/_wg1-gen/match3/generated-factory.js
Normal file
@ -0,0 +1,306 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Match-3 三消游戏工厂模块
|
||||
* 竖屏 390×844,8×8 宝石网格,6 种颜色,限 20 步
|
||||
*/
|
||||
export default function createGame(opts) {
|
||||
const o = opts || {};
|
||||
let ctx = null;
|
||||
let engine = null;
|
||||
let nowMs = 0;
|
||||
let sub = null;
|
||||
const ROWS = 8, COLS = 8;
|
||||
const CELL_SIZE = 40;
|
||||
const BOARD_LEFT = 35;
|
||||
const BOARD_TOP = 120;
|
||||
const COLORS = 6;
|
||||
const COLOR_STYLES = [
|
||||
'#FF5733', '#33FF57', '#3357FF', '#F333FF', '#33FFF3', '#FFD733'
|
||||
];
|
||||
|
||||
let board = []; // [row][col] = 0..5
|
||||
let score = 0;
|
||||
let movesLeft = 20;
|
||||
let phase = 'booting';
|
||||
let selected = null; // {row, col} | null
|
||||
let gameoverReason = '';
|
||||
let targets = [];
|
||||
|
||||
function cellCenter(row, col) {
|
||||
return {
|
||||
x: BOARD_LEFT + col * CELL_SIZE + CELL_SIZE / 2,
|
||||
y: BOARD_TOP + row * CELL_SIZE + CELL_SIZE / 2
|
||||
};
|
||||
}
|
||||
|
||||
function updateTargets() {
|
||||
targets = [];
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const cc = cellCenter(r, c);
|
||||
targets.push({ x: cc.x, y: cc.y, idx: r * COLS + c, occupied: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------- 棋盘初始化(无三连) ---------------------
|
||||
function initBoard() {
|
||||
board = [];
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
board[r] = [];
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
let color, tries = 0;
|
||||
do {
|
||||
color = Math.floor(ctx.random.range(0, COLORS));
|
||||
tries++;
|
||||
if (tries > 100) break;
|
||||
} while (
|
||||
(c >= 2 && board[r][c-1] === color && board[r][c-2] === color) ||
|
||||
(r >= 2 && board[r-1][c] === color && board[r-2][c] === color)
|
||||
);
|
||||
board[r][c] = color;
|
||||
}
|
||||
}
|
||||
updateTargets();
|
||||
}
|
||||
|
||||
// --------------------- 消除检测 ---------------------
|
||||
function findMatches() {
|
||||
const set = new Set();
|
||||
// 水平
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
let c = 0;
|
||||
while (c < COLS) {
|
||||
const col = board[r][c];
|
||||
let end = c + 1;
|
||||
while (end < COLS && board[r][end] === col) end++;
|
||||
if (end - c >= 3) {
|
||||
for (let i = c; i < end; i++) set.add(r * COLS + i);
|
||||
}
|
||||
c = end;
|
||||
}
|
||||
}
|
||||
// 垂直
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
let r = 0;
|
||||
while (r < ROWS) {
|
||||
const col = board[r][c];
|
||||
let end = r + 1;
|
||||
while (end < ROWS && board[end][c] === col) end++;
|
||||
if (end - r >= 3) {
|
||||
for (let i = r; i < end; i++) set.add(i * COLS + c);
|
||||
}
|
||||
r = end;
|
||||
}
|
||||
}
|
||||
const out = [];
|
||||
for (const idx of set) out.push({ row: Math.floor(idx / COLS), col: idx % COLS });
|
||||
return out;
|
||||
}
|
||||
|
||||
// 重力下落 + 顶部填充随机(不预防三连)
|
||||
function gravityAndFill() {
|
||||
let changed = false;
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
let write = ROWS - 1;
|
||||
for (let r = ROWS - 1; r >= 0; r--) {
|
||||
if (board[r][c] !== -1) {
|
||||
if (r !== write) {
|
||||
board[write][c] = board[r][c];
|
||||
board[r][c] = -1;
|
||||
changed = true;
|
||||
}
|
||||
write--;
|
||||
}
|
||||
}
|
||||
for (let r = write; r >= 0; r--) {
|
||||
board[r][c] = Math.floor(ctx.random.range(0, COLORS));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// 在指定位置爆发粒子的辅助函数
|
||||
function spawnParticlesAt(x, y) {
|
||||
if (!engine || !engine.particles) return;
|
||||
try {
|
||||
engine.particles.spawnEmitter({
|
||||
pos: { x, y },
|
||||
count: 20,
|
||||
speed: 80,
|
||||
particleTime: 0.5,
|
||||
sizeStart: 10,
|
||||
sizeEnd: 2,
|
||||
colorStart: { r: 1, g: 0.9, b: 0.2, a: 1 },
|
||||
colorEnd: { r: 1, g: 0.4, b: 0, a: 0 }
|
||||
});
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
function playMatchSound() {
|
||||
if (!engine || !engine.audio || !engine.audio.synth) return;
|
||||
try {
|
||||
engine.audio.synth.synthSfx([1, 0.02, 440, 0, 0, 0.12, 0, 1.4]);
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
// 执行消除闭环(尝试交换,若有消除则连锁消除;否则换回)
|
||||
// 返回是否发生了消除
|
||||
function trySwap(r1, c1, r2, c2) {
|
||||
// 交换
|
||||
[board[r1][c1], board[r2][c2]] = [board[r2][c2], board[r1][c1]];
|
||||
let matches = findMatches();
|
||||
if (matches.length === 0) {
|
||||
// 换回
|
||||
[board[r1][c1], board[r2][c2]] = [board[r2][c2], board[r1][c1]];
|
||||
return false;
|
||||
}
|
||||
// 有消除 → 进入连锁循环
|
||||
while (matches.length > 0) {
|
||||
// 粒子 + 音效
|
||||
for (const m of matches) {
|
||||
const cc = cellCenter(m.row, m.col);
|
||||
spawnParticlesAt(cc.x, cc.y);
|
||||
}
|
||||
playMatchSound();
|
||||
// 计分(每块 10 分)
|
||||
score += matches.length * 10;
|
||||
// 标记移除
|
||||
for (const m of matches) board[m.row][m.col] = -1;
|
||||
// 下落填充
|
||||
gravityAndFill();
|
||||
// 再次检测
|
||||
matches = findMatches();
|
||||
}
|
||||
updateTargets();
|
||||
return true;
|
||||
}
|
||||
|
||||
// --------------------- 输入处理 ---------------------
|
||||
function onPointerDown(e) {
|
||||
if (phase !== 'playing') return;
|
||||
const col = Math.floor((e.x - BOARD_LEFT) / CELL_SIZE);
|
||||
const row = Math.floor((e.y - BOARD_TOP) / CELL_SIZE);
|
||||
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
|
||||
selected = null;
|
||||
return;
|
||||
}
|
||||
if (selected === null) {
|
||||
selected = { row, col };
|
||||
return;
|
||||
}
|
||||
const sr = selected.row, sc = selected.col;
|
||||
selected = null;
|
||||
// 检查相邻
|
||||
const dr = Math.abs(row - sr), dc = Math.abs(col - sc);
|
||||
if (!((dr === 1 && dc === 0) || (dr === 0 && dc === 1))) {
|
||||
// 非相邻 -> 选中新格子
|
||||
selected = { row, col };
|
||||
return;
|
||||
}
|
||||
// 消耗一步
|
||||
movesLeft--;
|
||||
// 尝试交换
|
||||
const didMatch = trySwap(sr, sc, row, col);
|
||||
if (!didMatch) {
|
||||
// 无效交换,棋盘已恢复
|
||||
}
|
||||
// 检查步数是否耗尽
|
||||
if (movesLeft <= 0) {
|
||||
phase = 'gameover';
|
||||
gameoverReason = 'no_moves';
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------- 生命周期 ---------------------
|
||||
function init(boot) {
|
||||
ctx = boot.ctx;
|
||||
engine = ctx.getEngine(); // 可能 null
|
||||
phase = 'playing';
|
||||
score = 0;
|
||||
movesLeft = 20;
|
||||
selected = null;
|
||||
gameoverReason = '';
|
||||
initBoard();
|
||||
sub = ctx.getInput().on('pointerdown', onPointerDown);
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
nowMs = ctx.time.nowMs();
|
||||
}
|
||||
|
||||
function render(g) {
|
||||
// 背景
|
||||
g.fillStyle = '#1a1a2e';
|
||||
g.fillRect(0, 0, 390, 844);
|
||||
|
||||
// HUD
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '20px monospace';
|
||||
g.fillText('Score: ' + score, 20, 40);
|
||||
g.fillText('Moves: ' + movesLeft, 20, 70);
|
||||
|
||||
if (phase === 'gameover') {
|
||||
g.fillStyle = '#ff3333';
|
||||
g.font = '28px monospace';
|
||||
g.fillText('GAME OVER', 80, 420);
|
||||
g.fillStyle = '#aaaaaa';
|
||||
g.font = '16px monospace';
|
||||
g.fillText('Tap to restart (not available)', 60, 450);
|
||||
}
|
||||
|
||||
// 绘制宝石
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const val = board[r][c];
|
||||
if (val === -1) continue;
|
||||
const cx = BOARD_LEFT + c * CELL_SIZE + CELL_SIZE / 2;
|
||||
const cy = BOARD_TOP + r * CELL_SIZE + CELL_SIZE / 2;
|
||||
const radius = CELL_SIZE / 2 - 2;
|
||||
// 圆形宝石
|
||||
g.fillStyle = COLOR_STYLES[val];
|
||||
g.beginPath();
|
||||
g.arc(cx, cy, radius, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
// 高光
|
||||
g.fillStyle = 'rgba(255,255,255,0.25)';
|
||||
g.beginPath();
|
||||
g.arc(cx - 4, cy - 4, 5, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
// 选中边框
|
||||
if (selected && selected.row === r && selected.col === c) {
|
||||
g.strokeStyle = '#ffffff';
|
||||
g.lineWidth = 3;
|
||||
g.beginPath();
|
||||
g.arc(cx, cy, radius, 0, Math.PI * 2);
|
||||
g.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
try { if (sub) sub.cancel(); } catch (_) {}
|
||||
sub = null;
|
||||
board = [];
|
||||
selected = null;
|
||||
}
|
||||
|
||||
// --------------------- 可测性视图 ---------------------
|
||||
function _forensicsView() {
|
||||
return {
|
||||
seed: 0, // 宿主可忽略
|
||||
state: () => ({
|
||||
phase,
|
||||
result: phase === 'gameover' ? 'done' : null,
|
||||
score,
|
||||
movesLeft,
|
||||
board: board.map(row => row.slice()), // 深拷贝
|
||||
targets: targets.map(t => ({ ...t }))
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return { init, update, render, destroy, _forensicsView };
|
||||
}
|
||||
89
game-runtime/games/_wg1-gen/match3/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 uncaught[])。
|
||||
布局收编(对齐 ref 教训):canvas 顶在 (0,0) 满幅 390×844;状态行 absolute overlay + pointer-events:none,
|
||||
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 固定取证视口:390×844 移动竖屏基准;user-scalable=no 防缩放扰动像素哈希。 -->
|
||||
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
|
||||
<title>W-G1 generated game</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
|
||||
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
|
||||
#wrap { position: relative; width: 390px; margin: 0 auto; }
|
||||
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
|
||||
/* 状态行默认隐藏:boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
|
||||
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
|
||||
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
|
||||
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
|
||||
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
|
||||
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div id="status">booting…</div>
|
||||
<!-- 受控画布(opts.canvas 占位/输入兜底);real 引擎自建 #game-engine 叠加。 -->
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- esbuild 产物:iife 全局 __GameBundle(含 GameHostFactory + bootGameHost)。 -->
|
||||
<script src="./bundle.iife.js"></script>
|
||||
<script>
|
||||
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
|
||||
(function () {
|
||||
'use strict';
|
||||
var statusEl = document.getElementById('status');
|
||||
var canvas = document.getElementById('game');
|
||||
// 假绿守卫C·真接线探针:每次引擎能力被调,recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
|
||||
window.__engineCalls = [];
|
||||
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
|
||||
// 就绪/错误标记初值。
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = null;
|
||||
try {
|
||||
var G = window.__GameBundle;
|
||||
if (!G || typeof G.bootGameHost !== 'function') {
|
||||
throw new Error('__GameBundle.bootGameHost 未找到(bundle.iife.js 未正确加载或 globalName 不符)');
|
||||
}
|
||||
G.bootGameHost({
|
||||
canvas: canvas,
|
||||
statusEl: statusEl,
|
||||
recHook: recHook,
|
||||
// onReady:收集 internals(uncaught[] 等)供假绿守卫B 读。
|
||||
onReady: function (host, internals) {
|
||||
window.__genHost = host;
|
||||
window.__genInternals = internals || null;
|
||||
}
|
||||
})
|
||||
.then(function (host) {
|
||||
window.__genHost = window.__genHost || host;
|
||||
// 可测性别名:守卫H 经此读游戏可观测状态(host.state() 透传 game._forensicsView().state;游戏未实现则返回 {phase:'booting'})。
|
||||
// 宿主侧挂载,工厂仍零 window;与 ref 在 onReady 挂 __refForensics 同范式。
|
||||
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
|
||||
window.__genBooted = true; // CDP 探活标记(区分「脚本没加载」与「boot 抛错」)
|
||||
})
|
||||
.catch(function (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
|
||||
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
|
||||
});
|
||||
} catch (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
19
game-runtime/games/_wg1-gen/match3/play-spec.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"driver": {
|
||||
"type": "tap-pairs",
|
||||
"steps": 24,
|
||||
"stepMs": 300
|
||||
},
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
],
|
||||
"assertAfterPlay": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"why": "盲态交换相邻宝石→偶成三连得分上升;若全程凑不出三连(需读盘找可消)则不升=暴露 swipe/match 缺口"
|
||||
}
|
||||
],
|
||||
"expectLatch": true
|
||||
}
|
||||
34
game-runtime/games/_wg1-gen/multiball/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/multiball/generated-factory.js":{"bytes":12098,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/multiball/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/multiball/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/multiball/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/multiball/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164197},"games/_wg1-gen/multiball/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5355},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4218},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/multiball/generated-factory.js":{"bytesInOutput":4945}},"bytes":212721}}}
|
||||
77
game-runtime/games/_wg1-gen/multiball/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/multiball/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
game-runtime/games/_wg1-gen/multiball/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
167
game-runtime/games/_wg1-gen/multiball/evidence/verdict.json
Normal file
@ -0,0 +1,167 @@
|
||||
{
|
||||
"gameId": "multiball",
|
||||
"url": "http://localhost:4320/games/_wg1-gen/multiball/index.html",
|
||||
"ts": null,
|
||||
"guards": {
|
||||
"A_boot": {
|
||||
"pass": true
|
||||
},
|
||||
"C_frame": {
|
||||
"pass": true,
|
||||
"f0": 46,
|
||||
"f1": 75,
|
||||
"delta": 29
|
||||
},
|
||||
"I_control": {
|
||||
"pass": true,
|
||||
"paddlePath": "paddle.x",
|
||||
"results": [
|
||||
{
|
||||
"tapX": 50,
|
||||
"before": 194.99999999990354,
|
||||
"after": 50,
|
||||
"moved": -144.99999999990354,
|
||||
"distToTarget": 0,
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"tapX": 340,
|
||||
"before": 50,
|
||||
"after": 310,
|
||||
"moved": 260,
|
||||
"distToTarget": 30,
|
||||
"pass": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"D_render": {
|
||||
"pass": true,
|
||||
"bright": 146201,
|
||||
"maxCh": 255
|
||||
},
|
||||
"E_live": {
|
||||
"pass": true,
|
||||
"distinctStates": 64,
|
||||
"samples": 72
|
||||
},
|
||||
"B_uncaught": {
|
||||
"pass": true,
|
||||
"uncaught": []
|
||||
},
|
||||
"F_wiring": {
|
||||
"pass": true,
|
||||
"callCount": 15,
|
||||
"sample": [
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"audio.synth.synthSfx",
|
||||
"audio.synth.synthSfx",
|
||||
"particles.spawnEmitter",
|
||||
"particles.spawnEmitter"
|
||||
],
|
||||
"expected": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
]
|
||||
},
|
||||
"G_input": {
|
||||
"pass": true,
|
||||
"inputHash": "9cd794ce",
|
||||
"controlHash": "6afc48cc",
|
||||
"atFrame": 1984,
|
||||
"ctrlFrame": -1
|
||||
},
|
||||
"H_progress": {
|
||||
"pass": false,
|
||||
"checks": [
|
||||
{
|
||||
"path": "remaining",
|
||||
"op": "decreased",
|
||||
"before": 40,
|
||||
"after": 36,
|
||||
"pass": true,
|
||||
"why": "多球+挡板接球击碎砖→剩余下降;碰撞失效/高速穿透(CCD缺)则不降"
|
||||
}
|
||||
],
|
||||
"latch": {
|
||||
"pass": false,
|
||||
"reason": "真玩未到达 gameover 终态(终态不可达/无失败态/play-spec 未驱动到结束)",
|
||||
"phaseNow": "playing"
|
||||
},
|
||||
"state0": {
|
||||
"phase": "playing",
|
||||
"result": null,
|
||||
"score": 0,
|
||||
"remaining": 40,
|
||||
"progress": 0,
|
||||
"gameoverReason": "",
|
||||
"ball": {
|
||||
"x": 171,
|
||||
"y": 793,
|
||||
"vx": -103.2459330675854,
|
||||
"vy": 317.75830848240577
|
||||
},
|
||||
"paddle": {
|
||||
"x": 194.99999700380667,
|
||||
"y": 804,
|
||||
"w": 80
|
||||
},
|
||||
"balls": [
|
||||
{
|
||||
"x": 171,
|
||||
"y": 793
|
||||
},
|
||||
{
|
||||
"x": 187,
|
||||
"y": 793
|
||||
},
|
||||
{
|
||||
"x": 203,
|
||||
"y": 793
|
||||
}
|
||||
]
|
||||
},
|
||||
"state1": {
|
||||
"phase": "playing",
|
||||
"result": null,
|
||||
"score": 4,
|
||||
"remaining": 36,
|
||||
"progress": 0,
|
||||
"gameoverReason": "",
|
||||
"ball": {
|
||||
"x": 171,
|
||||
"y": 793,
|
||||
"vx": -103.2459330675854,
|
||||
"vy": 317.75830848240577
|
||||
},
|
||||
"paddle": {
|
||||
"x": 171.00000000000003,
|
||||
"y": 804,
|
||||
"w": 80
|
||||
},
|
||||
"balls": [
|
||||
{
|
||||
"x": 171,
|
||||
"y": 793
|
||||
},
|
||||
{
|
||||
"x": 187,
|
||||
"y": 793
|
||||
},
|
||||
{
|
||||
"x": 203,
|
||||
"y": 793
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": false,
|
||||
"notes": []
|
||||
}
|
||||
388
game-runtime/games/_wg1-gen/multiball/generated-factory.js
Normal file
@ -0,0 +1,388 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 多球打砖块变体工厂模块
|
||||
* 竖屏390×844,底部挡板,2-3球,砖块网格,道具掉落,胜负终态驻留。
|
||||
*/
|
||||
export default function createGame(opts) {
|
||||
const o = opts || {};
|
||||
let ctx = null;
|
||||
let phase = 'booting';
|
||||
let gameoverReason = '';
|
||||
let score = 0;
|
||||
let remaining = 0;
|
||||
let bricks = [];
|
||||
let balls = [];
|
||||
let items = [];
|
||||
let paddle = { x: 0, y: 0, w: 80, h: 16 };
|
||||
let pointerTargetX = 195;
|
||||
let inputSubs = [];
|
||||
let particles = null;
|
||||
let audioSynth = null;
|
||||
let math = null;
|
||||
let paddleWidenTimer = 0;
|
||||
const PADDLE_ORIG_W = 80;
|
||||
const WIDTH = 390, HEIGHT = 844;
|
||||
const BALL_RADIUS = 6;
|
||||
const BALL_SPEED = 350;
|
||||
const MAX_BALLS = 5;
|
||||
const BRICK_ROWS = 5;
|
||||
const BRICK_COLS = 8;
|
||||
const BRICK_W = 44, BRICK_H = 18, BRICK_GAP = 2;
|
||||
const BRICK_TOP_Y = 50;
|
||||
const BRICK_LEFT_X = (WIDTH - (BRICK_COLS * (BRICK_W + BRICK_GAP) - BRICK_GAP)) / 2;
|
||||
|
||||
function init(boot) {
|
||||
ctx = boot.ctx;
|
||||
phase = 'playing';
|
||||
gameoverReason = '';
|
||||
score = 0;
|
||||
remaining = 0;
|
||||
bricks = [];
|
||||
balls = [];
|
||||
items = [];
|
||||
paddleWidenTimer = 0;
|
||||
paddle.w = PADDLE_ORIG_W;
|
||||
paddle.x = (WIDTH - PADDLE_ORIG_W) / 2;
|
||||
paddle.y = HEIGHT - 40;
|
||||
|
||||
// 砖块网格
|
||||
for (let row = 0; row < BRICK_ROWS; row++) {
|
||||
for (let col = 0; col < BRICK_COLS; col++) {
|
||||
const x = BRICK_LEFT_X + col * (BRICK_W + BRICK_GAP);
|
||||
const y = BRICK_TOP_Y + row * (BRICK_H + BRICK_GAP);
|
||||
bricks.push({ x, y, w: BRICK_W, h: BRICK_H, alive: true });
|
||||
remaining++;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始球 (2~3)
|
||||
const ballCount = 2 + (boot.seed ? (boot.seed % 2) : 0); // 2或3
|
||||
for (let i = 0; i < ballCount; i++) {
|
||||
const angle = Math.PI * (0.6 + 0.3 * i / ballCount);
|
||||
const spd = BALL_SPEED + ctx.random.range(-20, 20);
|
||||
const vx = Math.cos(angle) * spd;
|
||||
const vy = Math.sin(angle) * spd;
|
||||
if (vy > 0) {
|
||||
balls.push({
|
||||
x: paddle.x + paddle.w * (0.2 + 0.6 * i / ballCount),
|
||||
y: paddle.y - BALL_RADIUS - 5,
|
||||
vx, vy,
|
||||
radius: BALL_RADIUS,
|
||||
alive: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 引擎接口
|
||||
const eng = ctx.getEngine();
|
||||
if (eng) {
|
||||
particles = eng.particles || null;
|
||||
if (eng.audio && eng.audio.synth) audioSynth = eng.audio.synth;
|
||||
math = eng.math || null;
|
||||
}
|
||||
|
||||
// 输入订阅
|
||||
const input = ctx.getInput();
|
||||
if (input) {
|
||||
const subDown = input.on('pointerdown', (e) => {
|
||||
pointerTargetX = e.x;
|
||||
});
|
||||
const subMove = input.on('pointermove', (e) => {
|
||||
pointerTargetX = e.x;
|
||||
});
|
||||
inputSubs.push(subDown, subMove);
|
||||
}
|
||||
pointerTargetX = WIDTH / 2;
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
if (phase !== 'playing') return;
|
||||
|
||||
// 挡板平滑跟随触摸点
|
||||
const diff = pointerTargetX - paddle.x;
|
||||
paddle.x += diff * 0.3;
|
||||
paddle.x = Math.max(0, Math.min(WIDTH - paddle.w, paddle.x));
|
||||
|
||||
// 扩板计时
|
||||
if (paddleWidenTimer > 0) {
|
||||
paddleWidenTimer -= dt;
|
||||
if (paddleWidenTimer <= 0) {
|
||||
paddle.w = PADDLE_ORIG_W;
|
||||
paddle.x = Math.max(0, Math.min(WIDTH - paddle.w, paddle.x));
|
||||
}
|
||||
}
|
||||
|
||||
// 球移动 + CCD
|
||||
const SUB_STEP = 2;
|
||||
for (let ball of balls) {
|
||||
if (!ball.alive) continue;
|
||||
const dx = ball.vx * dt;
|
||||
const dy = ball.vy * dt;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const steps = Math.max(1, Math.ceil(dist / SUB_STEP));
|
||||
const stepDx = dx / steps;
|
||||
const stepDy = dy / steps;
|
||||
|
||||
for (let s = 0; s < steps; s++) {
|
||||
ball.x += stepDx;
|
||||
ball.y += stepDy;
|
||||
|
||||
// 墙壁边界
|
||||
if (ball.x - ball.radius < 0) { ball.x = ball.radius; ball.vx = -ball.vx; }
|
||||
else if (ball.x + ball.radius > WIDTH) { ball.x = WIDTH - ball.radius; ball.vx = -ball.vx; }
|
||||
if (ball.y - ball.radius < 0) { ball.y = ball.radius; ball.vy = -ball.vy; }
|
||||
|
||||
// 球掉出底部
|
||||
if (ball.y + ball.radius > HEIGHT) {
|
||||
ball.alive = false;
|
||||
if (particles) {
|
||||
try {
|
||||
particles.spawnEmitter({
|
||||
pos: { x: ball.x, y: HEIGHT },
|
||||
count: 15, speed: 80, particleTime: 0.3,
|
||||
colorStart: { r: 1, g: 0.2, b: 0.2, a: 1 },
|
||||
colorEnd: { r: 0.8, g: 0, b: 0, a: 0 }
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 挡板碰撞
|
||||
if (ball.vy > 0 &&
|
||||
ball.y + ball.radius >= paddle.y - 1 &&
|
||||
ball.y - ball.radius <= paddle.y + paddle.h + 1 &&
|
||||
ball.x + ball.radius > paddle.x &&
|
||||
ball.x - ball.radius < paddle.x + paddle.w) {
|
||||
const hitPos = (ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
|
||||
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
|
||||
const newVx = Math.max(-speed, Math.min(speed, hitPos * 300));
|
||||
const newVy = -Math.sqrt(speed * speed - newVx * newVx);
|
||||
ball.vx = newVx;
|
||||
ball.vy = newVy;
|
||||
ball.y = paddle.y - ball.radius;
|
||||
if (audioSynth) {
|
||||
try { audioSynth.synthSfx([1, 0.02, 300, , , 0.1, , 0.8]); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// 砖块碰撞
|
||||
for (let brick of bricks) {
|
||||
if (!brick.alive) continue;
|
||||
const cx = ball.x, cy = ball.y, r = ball.radius;
|
||||
const rx = brick.x, ry = brick.y, rw = brick.w, rh = brick.h;
|
||||
const nearestX = Math.max(rx, Math.min(cx, rx + rw));
|
||||
const nearestY = Math.max(ry, Math.min(cy, ry + rh));
|
||||
const dxb = cx - nearestX, dyb = cy - nearestY;
|
||||
const distSq = dxb * dxb + dyb * dyb;
|
||||
if (distSq <= r * r) {
|
||||
brick.alive = false;
|
||||
remaining--;
|
||||
score++;
|
||||
// 粒子爆发
|
||||
if (particles) {
|
||||
try {
|
||||
particles.spawnEmitter({
|
||||
pos: { x: brick.x + brick.w / 2, y: brick.y + brick.h / 2 },
|
||||
count: 20, speed: 100, particleTime: 0.5,
|
||||
colorStart: { r: 1, g: 0.9, b: 0.4, a: 1 },
|
||||
colorEnd: { r: 1, g: 0.5, b: 0, a: 0 }
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
if (audioSynth) {
|
||||
try { audioSynth.synthSfx([1, 0.05, 440, , , 0.1, , 1.2]); } catch (_) {}
|
||||
}
|
||||
// 反弹
|
||||
if (distSq > 0.01) {
|
||||
const nl = Math.sqrt(distSq);
|
||||
const nx = dxb / nl, ny = dyb / nl;
|
||||
ball.x += nx * (r - nl);
|
||||
ball.y += ny * (r - nl);
|
||||
const dot = ball.vx * nx + ball.vy * ny;
|
||||
ball.vx -= 2 * dot * nx;
|
||||
ball.vy -= 2 * dot * ny;
|
||||
} else {
|
||||
ball.y = brick.y - r;
|
||||
ball.vy = -ball.vy;
|
||||
}
|
||||
// 道具掉落 (20%)
|
||||
if (ctx.random.next() < 0.2) {
|
||||
const type = ctx.random.next() < 0.5 ? 'extra_ball' : 'widen';
|
||||
items.push({
|
||||
x: brick.x + brick.w / 2,
|
||||
y: brick.y + brick.h / 2,
|
||||
type,
|
||||
radius: 6,
|
||||
speed: 150
|
||||
});
|
||||
}
|
||||
break; // 每子步只处理一块砖
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
balls = balls.filter(b => b.alive);
|
||||
|
||||
// 道具移动与收集
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
const item = items[i];
|
||||
item.y += item.speed * dt;
|
||||
// 挡板收集
|
||||
if (item.y + item.radius >= paddle.y &&
|
||||
item.y - item.radius <= paddle.y + paddle.h &&
|
||||
item.x + item.radius > paddle.x &&
|
||||
item.x - item.radius < paddle.x + paddle.w) {
|
||||
if (item.type === 'extra_ball') {
|
||||
if (balls.length < MAX_BALLS) {
|
||||
const angle = Math.PI * (0.6 + ctx.random.range(-0.15, 0.15));
|
||||
const spd = BALL_SPEED + ctx.random.range(-20, 20);
|
||||
balls.push({
|
||||
x: paddle.x + paddle.w / 2,
|
||||
y: paddle.y - BALL_RADIUS - 5,
|
||||
vx: Math.cos(angle) * spd,
|
||||
vy: Math.sin(angle) * spd,
|
||||
radius: BALL_RADIUS,
|
||||
alive: true
|
||||
});
|
||||
}
|
||||
} else if (item.type === 'widen') {
|
||||
paddle.w = PADDLE_ORIG_W * 1.5;
|
||||
paddleWidenTimer = 7;
|
||||
paddle.x = Math.max(0, Math.min(WIDTH - paddle.w, paddle.x));
|
||||
}
|
||||
// 收集粒子
|
||||
if (particles) {
|
||||
try {
|
||||
particles.spawnEmitter({
|
||||
pos: { x: item.x, y: item.y },
|
||||
count: 10, speed: 60, particleTime: 0.3,
|
||||
colorStart: { r: 0.2, g: 1, b: 0.2, a: 1 },
|
||||
colorEnd: { r: 0, g: 0.8, b: 0, a: 0 }
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
items.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
if (item.y > HEIGHT + 20) items.splice(i, 1);
|
||||
}
|
||||
|
||||
// 胜负判定
|
||||
if (remaining === 0) {
|
||||
phase = 'gameover';
|
||||
gameoverReason = 'cleared_all';
|
||||
} else if (balls.length === 0) {
|
||||
phase = 'gameover';
|
||||
gameoverReason = 'ball_lost';
|
||||
}
|
||||
}
|
||||
|
||||
function render(g) {
|
||||
// 背景
|
||||
g.fillStyle = '#0a0a1a';
|
||||
g.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
// 砖块
|
||||
for (let brick of bricks) {
|
||||
if (!brick.alive) continue;
|
||||
g.fillStyle = '#44aaff';
|
||||
g.fillRect(brick.x, brick.y, brick.w, brick.h);
|
||||
g.strokeStyle = '#3399ee';
|
||||
g.lineWidth = 1;
|
||||
g.strokeRect(brick.x, brick.y, brick.w, brick.h);
|
||||
}
|
||||
|
||||
// 道具
|
||||
for (let item of items) {
|
||||
g.fillStyle = item.type === 'extra_ball' ? '#00ff88' : '#ffaa00';
|
||||
g.beginPath();
|
||||
g.arc(item.x, item.y, item.radius, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
}
|
||||
|
||||
// 球
|
||||
for (let ball of balls) {
|
||||
if (!ball.alive) continue;
|
||||
g.fillStyle = '#ffffff';
|
||||
g.beginPath();
|
||||
g.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
g.fillStyle = '#cccccc';
|
||||
g.beginPath();
|
||||
g.arc(ball.x - 2, ball.y - 2, ball.radius * 0.5, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
}
|
||||
|
||||
// 挡板
|
||||
g.fillStyle = '#ffaa44';
|
||||
g.fillRect(paddle.x, paddle.y, paddle.w, paddle.h);
|
||||
g.fillStyle = '#ffcc88';
|
||||
g.fillRect(paddle.x, paddle.y, paddle.w, paddle.h / 2);
|
||||
|
||||
// HUD
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '16px monospace';
|
||||
g.fillText('Score: ' + score, 10, 28);
|
||||
g.fillText('Remaining: ' + remaining, 10, 52);
|
||||
g.fillText('Balls: ' + balls.length, 10, 76);
|
||||
|
||||
// 游戏结束画面
|
||||
if (phase === 'gameover') {
|
||||
g.fillStyle = 'rgba(0,0,0,0.6)';
|
||||
g.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
g.fillStyle = '#ffcc00';
|
||||
g.font = 'bold 32px sans-serif';
|
||||
g.textAlign = 'center';
|
||||
if (gameoverReason === 'cleared_all') {
|
||||
g.fillText('YOU WIN!', WIDTH / 2, HEIGHT / 2 - 20);
|
||||
} else {
|
||||
g.fillText('GAME OVER', WIDTH / 2, HEIGHT / 2 - 20);
|
||||
}
|
||||
g.font = '18px sans-serif';
|
||||
g.fillStyle = '#ffffff';
|
||||
g.fillText('Tap to restart (not auto)', WIDTH / 2, HEIGHT / 2 + 40);
|
||||
g.textAlign = 'left';
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
for (let sub of inputSubs) {
|
||||
try { sub.cancel(); } catch (_) {}
|
||||
}
|
||||
inputSubs = [];
|
||||
phase = 'booting';
|
||||
balls = [];
|
||||
bricks = [];
|
||||
items = [];
|
||||
paddleWidenTimer = 0;
|
||||
paddle.w = PADDLE_ORIG_W;
|
||||
pointerTargetX = WIDTH / 2;
|
||||
}
|
||||
|
||||
function _forensicsView() {
|
||||
const mainBall = balls.length > 0
|
||||
? { x: balls[0].x, y: balls[0].y, vx: balls[0].vx, vy: balls[0].vy }
|
||||
: { x: 0, y: 0, vx: 0, vy: 0 };
|
||||
const ballPositions = balls.map(b => ({ x: b.x, y: b.y }));
|
||||
return {
|
||||
seed: boot?.seed,
|
||||
state: () => ({
|
||||
phase,
|
||||
result: gameoverReason === 'cleared_all' ? 'win' : (gameoverReason === 'ball_lost' ? 'lose' : null),
|
||||
score,
|
||||
remaining,
|
||||
progress: 0,
|
||||
gameoverReason,
|
||||
ball: mainBall,
|
||||
paddle: { x: paddle.x, y: paddle.y, w: paddle.w },
|
||||
balls: ballPositions,
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
let boot;
|
||||
return { init: (b) => { boot = b; return init(b); }, update, render, destroy, _forensicsView };
|
||||
}
|
||||
89
game-runtime/games/_wg1-gen/multiball/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 uncaught[])。
|
||||
布局收编(对齐 ref 教训):canvas 顶在 (0,0) 满幅 390×844;状态行 absolute overlay + pointer-events:none,
|
||||
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 固定取证视口:390×844 移动竖屏基准;user-scalable=no 防缩放扰动像素哈希。 -->
|
||||
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
|
||||
<title>W-G1 generated game</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
|
||||
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
|
||||
#wrap { position: relative; width: 390px; margin: 0 auto; }
|
||||
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
|
||||
/* 状态行默认隐藏:boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
|
||||
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
|
||||
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
|
||||
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
|
||||
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
|
||||
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div id="status">booting…</div>
|
||||
<!-- 受控画布(opts.canvas 占位/输入兜底);real 引擎自建 #game-engine 叠加。 -->
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- esbuild 产物:iife 全局 __GameBundle(含 GameHostFactory + bootGameHost)。 -->
|
||||
<script src="./bundle.iife.js"></script>
|
||||
<script>
|
||||
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
|
||||
(function () {
|
||||
'use strict';
|
||||
var statusEl = document.getElementById('status');
|
||||
var canvas = document.getElementById('game');
|
||||
// 假绿守卫C·真接线探针:每次引擎能力被调,recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
|
||||
window.__engineCalls = [];
|
||||
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
|
||||
// 就绪/错误标记初值。
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = null;
|
||||
try {
|
||||
var G = window.__GameBundle;
|
||||
if (!G || typeof G.bootGameHost !== 'function') {
|
||||
throw new Error('__GameBundle.bootGameHost 未找到(bundle.iife.js 未正确加载或 globalName 不符)');
|
||||
}
|
||||
G.bootGameHost({
|
||||
canvas: canvas,
|
||||
statusEl: statusEl,
|
||||
recHook: recHook,
|
||||
// onReady:收集 internals(uncaught[] 等)供假绿守卫B 读。
|
||||
onReady: function (host, internals) {
|
||||
window.__genHost = host;
|
||||
window.__genInternals = internals || null;
|
||||
}
|
||||
})
|
||||
.then(function (host) {
|
||||
window.__genHost = window.__genHost || host;
|
||||
// 可测性别名:守卫H 经此读游戏可观测状态(host.state() 透传 game._forensicsView().state;游戏未实现则返回 {phase:'booting'})。
|
||||
// 宿主侧挂载,工厂仍零 window;与 ref 在 onReady 挂 __refForensics 同范式。
|
||||
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
|
||||
window.__genBooted = true; // CDP 探活标记(区分「脚本没加载」与「boot 抛错」)
|
||||
})
|
||||
.catch(function (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
|
||||
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
|
||||
});
|
||||
} catch (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
32
game-runtime/games/_wg1-gen/multiball/play-spec.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"controlCheck": {
|
||||
"paddlePath": "paddle.x",
|
||||
"paddleY": 800,
|
||||
"tapXs": [
|
||||
50,
|
||||
340
|
||||
],
|
||||
"tapsPerTarget": 6,
|
||||
"moveMin": 25,
|
||||
"tolerance": 70
|
||||
},
|
||||
"driver": {
|
||||
"type": "paddle-intercept",
|
||||
"ballPath": "ball.x",
|
||||
"paddleY": 800,
|
||||
"steps": 70,
|
||||
"stepMs": 130
|
||||
},
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
],
|
||||
"assertAfterPlay": [
|
||||
{
|
||||
"path": "remaining",
|
||||
"op": "decreased",
|
||||
"why": "多球+挡板接球击碎砖→剩余下降;碰撞失效/高速穿透(CCD缺)则不降"
|
||||
}
|
||||
],
|
||||
"expectLatch": true
|
||||
}
|
||||
34
game-runtime/games/_wg1-gen/runner/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/runner/generated-factory.js":{"bytes":7680,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/runner/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/runner/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/runner/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/runner/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164143},"games/_wg1-gen/runner/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5362},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4216},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/runner/generated-factory.js":{"bytesInOutput":3169}},"bytes":210896}}}
|
||||
77
game-runtime/games/_wg1-gen/runner/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/runner/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
game-runtime/games/_wg1-gen/runner/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
118
game-runtime/games/_wg1-gen/runner/evidence/verdict.json
Normal file
@ -0,0 +1,118 @@
|
||||
{
|
||||
"gameId": "runner",
|
||||
"url": "http://localhost:4320/games/_wg1-gen/runner/index.html",
|
||||
"ts": null,
|
||||
"guards": {
|
||||
"A_boot": {
|
||||
"pass": true
|
||||
},
|
||||
"C_frame": {
|
||||
"pass": true,
|
||||
"f0": 48,
|
||||
"f1": 78,
|
||||
"delta": 30
|
||||
},
|
||||
"I_control": {
|
||||
"pass": true,
|
||||
"skipped": "未声明 controlCheck(向后兼容)"
|
||||
},
|
||||
"D_render": {
|
||||
"pass": true,
|
||||
"bright": 304621,
|
||||
"maxCh": 255
|
||||
},
|
||||
"E_live": {
|
||||
"pass": true,
|
||||
"distinctStates": 19,
|
||||
"samples": 25
|
||||
},
|
||||
"B_uncaught": {
|
||||
"pass": true,
|
||||
"uncaught": []
|
||||
},
|
||||
"F_wiring": {
|
||||
"pass": true,
|
||||
"callCount": 47,
|
||||
"sample": [
|
||||
"particles.spawnEmitter",
|
||||
"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",
|
||||
"particles.spawnEmitter"
|
||||
],
|
||||
"expected": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
]
|
||||
},
|
||||
"G_input": {
|
||||
"pass": true,
|
||||
"inputHash": "4bf24e76",
|
||||
"controlHash": "3cd12b81",
|
||||
"atFrame": 813,
|
||||
"ctrlFrame": 816
|
||||
},
|
||||
"H_progress": {
|
||||
"pass": true,
|
||||
"checks": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"before": 80,
|
||||
"after": 90,
|
||||
"pass": true,
|
||||
"why": "自动跑→距离分上升(稳过);G 门验跳跃是否真改变存活、latch 验真撞死"
|
||||
}
|
||||
],
|
||||
"latch": {
|
||||
"pass": true,
|
||||
"dwelledMs": 600,
|
||||
"after": "gameover",
|
||||
"reason": ""
|
||||
},
|
||||
"state0": {
|
||||
"seed": null,
|
||||
"phase": "playing",
|
||||
"score": 80,
|
||||
"remaining": null,
|
||||
"progress": null,
|
||||
"gameoverReason": "",
|
||||
"result": null,
|
||||
"player": {
|
||||
"x": 80,
|
||||
"y": 688,
|
||||
"vy": 0,
|
||||
"onGround": true
|
||||
},
|
||||
"nextObstacle": null
|
||||
},
|
||||
"state1": {
|
||||
"seed": null,
|
||||
"phase": "gameover",
|
||||
"score": 90,
|
||||
"remaining": null,
|
||||
"progress": null,
|
||||
"gameoverReason": "collision",
|
||||
"result": "lose",
|
||||
"player": {
|
||||
"x": 80,
|
||||
"y": 688,
|
||||
"vy": 0,
|
||||
"onGround": true
|
||||
},
|
||||
"nextObstacle": {
|
||||
"x": 90.83333333333361
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": true,
|
||||
"notes": []
|
||||
}
|
||||
280
game-runtime/games/_wg1-gen/runner/generated-factory.js
Normal file
@ -0,0 +1,280 @@
|
||||
'use strict';
|
||||
/**
|
||||
* 无尽跑酷工厂模块
|
||||
* 逻辑视口 390×844,竖屏
|
||||
*/
|
||||
export default function createGame(opts) {
|
||||
const o = opts || {};
|
||||
let ctx = null;
|
||||
let bootData = null;
|
||||
let phase = 'playing'; // 'playing' | 'gameover'
|
||||
let score = 0;
|
||||
let gameoverReason = '';
|
||||
let player = { x: 80, y: 688, vy: 0, onGround: true }; // 固定x,y为地面时圆心
|
||||
let obstacles = [];
|
||||
let lastSpawnTime = 0;
|
||||
let spawnInterval = 1.2; // 秒
|
||||
const SPEED = 350; // 障碍物左移 px/s
|
||||
const SCORE_RATE = 100; // 分数增加速度 分/秒
|
||||
const GRAVITY = 2000; // 重力 px/s²
|
||||
const JUMP_VY = -620; // 跳跃初速度
|
||||
const GROUND_Y = 688; // 玩家落地时圆心y
|
||||
const PLAYER_R = 12;
|
||||
const OBS_W = 30;
|
||||
const OBS_H = 40;
|
||||
const RESTART_BTN = { x: 100, y: 400, w: 190, h: 80 };
|
||||
|
||||
let sub = null;
|
||||
let nowMs = 0;
|
||||
|
||||
// ---------- 引擎能力封装(防御) ----------
|
||||
function emitParticles(x, y, colorStart, count) {
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x, y },
|
||||
count: count || 20,
|
||||
speed: 120,
|
||||
particleTime: 0.5,
|
||||
colorStart: colorStart || { r: 1, g: 1, b: 1, a: 1 },
|
||||
colorEnd: { r: 0, g: 0, b: 0, a: 0 },
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
function playSfx(params) {
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.audio && eng.audio.synth) {
|
||||
try { eng.audio.synth.synthSfx(params); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 游戏状态重置 ----------
|
||||
function resetGame() {
|
||||
phase = 'playing';
|
||||
score = 0;
|
||||
gameoverReason = '';
|
||||
player = { x: 80, y: GROUND_Y, vy: 0, onGround: true };
|
||||
obstacles = [];
|
||||
lastSpawnTime = 0;
|
||||
spawnInterval = 1.2;
|
||||
}
|
||||
|
||||
// ---------- 初始 ----------
|
||||
function init(boot) {
|
||||
ctx = boot.ctx;
|
||||
bootData = boot;
|
||||
resetGame();
|
||||
|
||||
// 订阅输入
|
||||
sub = ctx.getInput().on('pointerdown', onTap);
|
||||
|
||||
// 演示引擎能力:常驻一个几乎不可见的粒子(可选)
|
||||
const eng = ctx.getEngine();
|
||||
if (eng && eng.particles) {
|
||||
try {
|
||||
eng.particles.spawnEmitter({
|
||||
pos: { x: -100, y: -100 }, // 屏幕外
|
||||
emitRate: 0.1,
|
||||
particleTime: 0.1,
|
||||
speed: 1,
|
||||
colorStart: { r: 0, g: 0, b: 0, a: 0 }
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 输入处理 ----------
|
||||
function onTap(e) {
|
||||
const x = e.x, y = e.y;
|
||||
if (phase === 'gameover') {
|
||||
// 点击重玩区域
|
||||
const rb = RESTART_BTN;
|
||||
if (x >= rb.x && x <= rb.x + rb.w && y >= rb.y && y <= rb.y + rb.h) {
|
||||
resetGame();
|
||||
emitParticles(player.x, player.y, { r: 0, g: 1, b: 0.5, a: 1 }, 25);
|
||||
playSfx([1, 0.05, 600, , , 0.1, , 1.5]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// playing 状态 → 跳跃
|
||||
if (player.onGround) {
|
||||
player.vy = JUMP_VY;
|
||||
player.onGround = false;
|
||||
emitParticles(player.x, player.y + PLAYER_R, { r: 1, g: 1, b: 0, a: 1 }, 16);
|
||||
playSfx([1, 0.05, 400, , , 0.12, , 1.3]);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 更新 ----------
|
||||
function update(dt) {
|
||||
nowMs = ctx.time.nowMs();
|
||||
|
||||
if (phase !== 'playing') return;
|
||||
|
||||
// 分数
|
||||
score += dt * SCORE_RATE;
|
||||
|
||||
// 玩家垂直运动
|
||||
if (!player.onGround) {
|
||||
player.vy += GRAVITY * dt;
|
||||
player.y += player.vy * dt;
|
||||
// 落地检测
|
||||
if (player.y >= GROUND_Y) {
|
||||
player.y = GROUND_Y;
|
||||
player.vy = 0;
|
||||
player.onGround = true;
|
||||
// 落地轻反馈(可选)
|
||||
emitParticles(player.x, player.y + PLAYER_R, { r: 0.5, g: 0.7, b: 1, a: 0.6 }, 8);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动障碍物
|
||||
for (let i = obstacles.length - 1; i >= 0; i--) {
|
||||
obstacles[i].x -= SPEED * dt;
|
||||
if (obstacles[i].x < -OBS_W) {
|
||||
obstacles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成新障碍物
|
||||
const tSec = nowMs / 1000;
|
||||
if (tSec - lastSpawnTime >= spawnInterval) {
|
||||
const interval = ctx.random.range(0.6, 1.0); // 密度:不跳必撞
|
||||
spawnInterval = interval;
|
||||
lastSpawnTime = tSec;
|
||||
obstacles.push({
|
||||
x: 390 + 10,
|
||||
y: 700 - OBS_H, // 地面顶部 y=700,矩形底部对齐
|
||||
w: OBS_W,
|
||||
h: OBS_H
|
||||
});
|
||||
}
|
||||
|
||||
// 碰撞检测(圆形 vs 矩形)
|
||||
const px = player.x, py = player.y;
|
||||
for (let obs of obstacles) {
|
||||
// 矩形中心
|
||||
const ox = obs.x + obs.w / 2;
|
||||
const oy = obs.y + obs.h / 2;
|
||||
const dx = Math.abs(px - ox);
|
||||
const dy = Math.abs(py - oy);
|
||||
if (dx < PLAYER_R + obs.w / 2 && dy < PLAYER_R + obs.h / 2) {
|
||||
// 碰撞
|
||||
phase = 'gameover';
|
||||
gameoverReason = 'collision';
|
||||
emitParticles(px, py, { r: 1, g: 0.2, b: 0.2, a: 1 }, 40);
|
||||
playSfx([1, 0.2, 80, , , 0.15, , 0.7]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 渲染 ----------
|
||||
function render(g) {
|
||||
// 背景
|
||||
g.fillStyle = '#1a1a2e';
|
||||
g.fillRect(0, 0, 390, 844);
|
||||
|
||||
// 地面
|
||||
g.fillStyle = '#2d4059';
|
||||
g.fillRect(0, 700, 390, 144);
|
||||
g.strokeStyle = '#4ecca3';
|
||||
g.lineWidth = 3;
|
||||
g.beginPath();
|
||||
g.moveTo(0, 700);
|
||||
g.lineTo(390, 700);
|
||||
g.stroke();
|
||||
|
||||
// 障碍物
|
||||
g.fillStyle = '#ff5722';
|
||||
for (let obs of obstacles) {
|
||||
g.fillRect(obs.x, obs.y, obs.w, obs.h);
|
||||
// 高光
|
||||
g.fillStyle = '#ff8a65';
|
||||
g.fillRect(obs.x + 4, obs.y + 4, obs.w - 8, 6);
|
||||
g.fillStyle = '#ff5722';
|
||||
}
|
||||
|
||||
// 角色(圆形)
|
||||
g.fillStyle = '#e23e57';
|
||||
g.beginPath();
|
||||
g.arc(player.x, player.y, PLAYER_R, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
// 眼睛
|
||||
g.fillStyle = '#ffffff';
|
||||
g.beginPath();
|
||||
g.arc(player.x - 4, player.y - 4, 3, 0, Math.PI * 2);
|
||||
g.arc(player.x + 4, player.y - 4, 3, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
g.fillStyle = '#000';
|
||||
g.beginPath();
|
||||
g.arc(player.x - 4, player.y - 4, 1.5, 0, Math.PI * 2);
|
||||
g.arc(player.x + 4, player.y - 4, 1.5, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
|
||||
// HUD 顶部信息
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '18px monospace';
|
||||
g.fillText('Score: ' + Math.floor(score), 10, 30);
|
||||
g.fillText('Status: ' + (phase === 'playing' ? 'Running' : 'Game Over'), 10, 55);
|
||||
|
||||
// Game Over 画面 + 重玩按钮
|
||||
if (phase === 'gameover') {
|
||||
g.fillStyle = 'rgba(0,0,0,0.5)';
|
||||
g.fillRect(0, 350, 390, 200);
|
||||
|
||||
g.fillStyle = '#e94560';
|
||||
const rb = RESTART_BTN;
|
||||
g.fillRect(rb.x, rb.y, rb.w, rb.h);
|
||||
g.fillStyle = '#ffffff';
|
||||
g.font = '22px monospace';
|
||||
g.fillText('Tap to Restart', rb.x + 18, rb.y + 50);
|
||||
|
||||
g.font = '16px monospace';
|
||||
g.fillText('You crashed! Score: ' + Math.floor(score), 100, rb.y - 20);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 销毁 ----------
|
||||
function destroy() {
|
||||
try { sub && sub.cancel(); } catch (_) {}
|
||||
sub = null;
|
||||
obstacles = [];
|
||||
}
|
||||
|
||||
// ---------- 可测性接口 ----------
|
||||
function _forensicsView() {
|
||||
function state() {
|
||||
// 前方最近障碍物
|
||||
let nextObs = null;
|
||||
for (let obs of obstacles) {
|
||||
if (obs.x > player.x) {
|
||||
if (nextObs === null || obs.x < nextObs.x) {
|
||||
nextObs = { x: obs.x };
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
seed: null,
|
||||
phase: phase,
|
||||
score: Math.floor(score),
|
||||
remaining: null,
|
||||
progress: null,
|
||||
gameoverReason: gameoverReason,
|
||||
result: phase === 'gameover' ? 'lose' : null,
|
||||
player: {
|
||||
x: player.x,
|
||||
y: player.y,
|
||||
vy: player.vy,
|
||||
onGround: player.onGround
|
||||
},
|
||||
nextObstacle: nextObs
|
||||
};
|
||||
}
|
||||
return { seed: null, state };
|
||||
}
|
||||
|
||||
return { init, update, render, destroy, _forensicsView };
|
||||
}
|
||||
89
game-runtime/games/_wg1-gen/runner/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 uncaught[])。
|
||||
布局收编(对齐 ref 教训):canvas 顶在 (0,0) 满幅 390×844;状态行 absolute overlay + pointer-events:none,
|
||||
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 固定取证视口:390×844 移动竖屏基准;user-scalable=no 防缩放扰动像素哈希。 -->
|
||||
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
|
||||
<title>W-G1 generated game</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
|
||||
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
|
||||
#wrap { position: relative; width: 390px; margin: 0 auto; }
|
||||
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
|
||||
/* 状态行默认隐藏:boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
|
||||
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
|
||||
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
|
||||
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
|
||||
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
|
||||
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div id="status">booting…</div>
|
||||
<!-- 受控画布(opts.canvas 占位/输入兜底);real 引擎自建 #game-engine 叠加。 -->
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- esbuild 产物:iife 全局 __GameBundle(含 GameHostFactory + bootGameHost)。 -->
|
||||
<script src="./bundle.iife.js"></script>
|
||||
<script>
|
||||
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
|
||||
(function () {
|
||||
'use strict';
|
||||
var statusEl = document.getElementById('status');
|
||||
var canvas = document.getElementById('game');
|
||||
// 假绿守卫C·真接线探针:每次引擎能力被调,recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
|
||||
window.__engineCalls = [];
|
||||
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
|
||||
// 就绪/错误标记初值。
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = null;
|
||||
try {
|
||||
var G = window.__GameBundle;
|
||||
if (!G || typeof G.bootGameHost !== 'function') {
|
||||
throw new Error('__GameBundle.bootGameHost 未找到(bundle.iife.js 未正确加载或 globalName 不符)');
|
||||
}
|
||||
G.bootGameHost({
|
||||
canvas: canvas,
|
||||
statusEl: statusEl,
|
||||
recHook: recHook,
|
||||
// onReady:收集 internals(uncaught[] 等)供假绿守卫B 读。
|
||||
onReady: function (host, internals) {
|
||||
window.__genHost = host;
|
||||
window.__genInternals = internals || null;
|
||||
}
|
||||
})
|
||||
.then(function (host) {
|
||||
window.__genHost = window.__genHost || host;
|
||||
// 可测性别名:守卫H 经此读游戏可观测状态(host.state() 透传 game._forensicsView().state;游戏未实现则返回 {phase:'booting'})。
|
||||
// 宿主侧挂载,工厂仍零 window;与 ref 在 onReady 挂 __refForensics 同范式。
|
||||
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
|
||||
window.__genBooted = true; // CDP 探活标记(区分「脚本没加载」与「boot 抛错」)
|
||||
})
|
||||
.catch(function (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
|
||||
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
|
||||
});
|
||||
} catch (e) {
|
||||
window.__genBooted = false;
|
||||
window.__genBootError = String((e && e.message) || e);
|
||||
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
119
game-runtime/games/_wg1-gen/runner/play-spec.json
Normal file
@ -0,0 +1,119 @@
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
},
|
||||
{
|
||||
"t": "tap",
|
||||
"x": 195,
|
||||
"y": 420
|
||||
},
|
||||
{
|
||||
"t": "wait",
|
||||
"ms": 500
|
||||
}
|
||||
],
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
"audio.synth"
|
||||
],
|
||||
"assertAfterPlay": [
|
||||
{
|
||||
"path": "score",
|
||||
"op": "increased",
|
||||
"why": "自动跑→距离分上升(稳过);G 门验跳跃是否真改变存活、latch 验真撞死"
|
||||
}
|
||||
],
|
||||
"expectLatch": true
|
||||
}
|
||||
34
game-runtime/games/_wg1-gen/saolei/bundle.iife.js
Normal file
@ -0,0 +1 @@
|
||||
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/saolei/generated-factory.js":{"bytes":10023,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/saolei/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/saolei/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/saolei/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/saolei/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164252},"games/_wg1-gen/saolei/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5349},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4216},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/saolei/generated-factory.js":{"bytesInOutput":4222}},"bytes":212045}}}
|
||||
77
game-runtime/games/_wg1-gen/saolei/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-G1 lane | 消费方:esbuild --global-name=__GameBundle → P1 宿主 bootGameHost 真玩
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
* 【职责】把「生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎」汇成一个 iife 全局 __GameBundle。
|
||||
* 产出靶契约(冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle):
|
||||
* iife 顶层全局名固定 **__GameBundle**;宿主从 window.__GameBundle 取 GameHostFactory 与 bootGameHost。
|
||||
*
|
||||
* 【最大化复用·最小新代码】
|
||||
* - 宿主:复用 src/host/boot-game-host.js 的通用 bootGameHost(不改)。
|
||||
* - 插件 wiring:用官方 L0 通用装载壳 src/host/generic-host-config.js 的 buildGenericHostConfig(标准 7 插件 + 偏序 + 视口 + 工厂 wiring,与官方 entry-generic.js 同一份),
|
||||
* override factory 为本目录生成的工厂 → 生成游戏跑在【官方 generic 标准装配】上。
|
||||
* - 注:本入口 = 官方 entry-generic.js 的「harness 可取证」超集——多透传 recHook/onReady 供 CDP 八门(F门真接线);
|
||||
* studio 渠道调 bootGameHost({canvas,seed}) 时 recHook 为空=no-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 iife)。
|
||||
*
|
||||
* 【Q4 铁律】本入口是「集成段 host」唯一引擎 import 落点(动态 import 后经 opts.engine 注入);
|
||||
* ./factory.js 游戏工厂零引擎 import,能力一律经 boot.ctx.getEngine()。
|
||||
* ════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// 通用宿主(引擎骨架参数化;本身零引擎 import,引擎经 opts.engine 注入)。
|
||||
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
|
||||
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 generated-factory.js)。
|
||||
import gameFactory from './generated-factory.js';
|
||||
|
||||
/** 【契约导出】GameHostFactory:宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
|
||||
export const GameHostFactory = gameFactory;
|
||||
// 默认导出同为 GameHostFactory(便于 import default 取用;与具名导出同一引用)。
|
||||
export default gameFactory;
|
||||
|
||||
/**
|
||||
* 【P2/W-G1 boot 入口】供 host 页在浏览器内启动本 bundle(引擎由本 bundle 注入后驱动)。
|
||||
* - 动态 import 引擎(Option C):调用时才求值,esbuild 已把引擎内联进本 bundle(无外部请求)。
|
||||
* - engineMode 强制 'real'(W-G1 真玩取证一律引擎掌帧)。
|
||||
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig(与 ref 同一标准集),factory override 为生成工厂。
|
||||
* - recHook / onReady 透传:供取证 harness 注入「引擎 call-ID 探针」与「internals(uncaught) 收集」(假绿守卫)。
|
||||
*
|
||||
* @param {Object} opts host 页传入入参。
|
||||
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布(real 通道引擎自建 mainCanvas#game-engine,本 canvas 作占位/输入兜底)。
|
||||
* @param {HTMLElement} [opts.statusEl] 状态行(可空)。
|
||||
* @param {number} [opts.seed] 主随机种子(缺省固定值,保确定性)。
|
||||
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void(取证路径注入 window.__engineCalls.push;渠道产物不注)。
|
||||
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调(取证收集 internals.uncaught 等)。
|
||||
* @param {any} [opts.engine] 覆盖引擎名字空间(缺省动态 import littlejsengine)。
|
||||
* @returns {Promise<object>} bootGameHost 的 host 句柄(frameCount/stepFrames/tap/do/state 等)。
|
||||
*/
|
||||
export async function bootGameHost(opts) {
|
||||
const o = opts || {};
|
||||
// 动态 import 引擎:求值时机延后到本函数被调用(浏览器运行期)。esbuild 内联,无外部请求。
|
||||
const engine = o.engine || (await import('littlejsengine'));
|
||||
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
|
||||
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
|
||||
// 官方 generic 标准装配(7 插件 + 偏序 + 视口 + 工厂 wiring;与 entry-generic.js 同一 buildGenericHostConfig)。
|
||||
const cfg = buildGenericHostConfig({ seed });
|
||||
// 调通用宿主:real 通道引擎掌帧;factory override 为本目录生成工厂;其余 wiring 取标准集。
|
||||
return bootGameHostCore({
|
||||
canvas: o.canvas,
|
||||
statusEl: o.statusEl || null,
|
||||
engine,
|
||||
seed,
|
||||
mode: o.mode || 'play', // W-G1 真玩=play
|
||||
engineMode: 'real', // 一律引擎掌帧(real 通道)
|
||||
viewport: cfg.viewport, // 390×844(与 ref 同,移动竖屏基准)
|
||||
factory: gameFactory, // ★ override:本目录【生成的游戏】(其余 wiring 全复用 ref)
|
||||
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
|
||||
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
|
||||
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
|
||||
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op)
|
||||
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
|
||||
});
|
||||
}
|
||||
BIN
game-runtime/games/_wg1-gen/saolei/evidence/after-play.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
game-runtime/games/_wg1-gen/saolei/evidence/first-paint.png
Normal file
|
After Width: | Height: | Size: 18 KiB |