修一处潜在语义错 + 补休闲玩法时间压力维度(限时/倒计时):
- 旧 state.t = nowMs/1000 是【绝对单调时】(受控面明禁跨设备比绝对值),用作 `t >= timeLimit` 规则会因时钟原点(performance.now/Date.now)立判或漂移——错
- 新增 state.elapsed = (now - startMs)/1000(init 记 startMs,相对秒),forensics 暴露 elapsed;规则 `elapsed >= config.timeLimit → win/lose` 正确(零新增 op,正交叠加任一族)
- 单测经注入时钟(createHostDevContext({clock}))验:init@1s 起点,clockMs=3000 时 elapsed=2(相对,非绝对 3)仍 playing,clockMs=4500 时 elapsed=3.5 触发 lose——证相对非绝对
验证:src 全量 197/197 绿(+1 时间压力);additive 改动经回归九门(tap-targets 重建探针 A–I 全绿,H score:0→3+latch)确认不破坏既有 6 族,探针已清场。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.9 KiB
E1 2D 适配器(声明式 gameDefinition → 可玩 GameInstance)
固定游戏架构的引擎线消费侧(plan
docs/plans/2026-06-17-002-feat-mac-engine-frontend-tier0-plan.mdU1/E1)。 把后端 SAA 图产的SourceProject.gameDefinition(声明式)适配成命中../core/game-host.d.ts契约的可玩游戏—— 换适配器即换引擎(gameDefinition 维度无关)。
文件
| 文件 | 职责 |
|---|---|
create-game-from-definition.js |
adaptGameDefinition(source) → GameHostFactory:声明式 gameDefinition → GameInstance |
scaffold-from-source.mjs |
buildFactorySource / scaffoldFromSource:SourceProject → 可被 ../../scripts/build.mjs 编译的 game 目录工件(E2) |
fixtures/*.source.json |
四族 archetype 的 SourceProject 样例 |
adapter.test.mjs / scaffold.test.mjs |
零依赖 node 单测(node --test src/adapter/*.test.mjs) |
契约要点
- 输出 = GameInstance:
{ init(boot), update(dt), render(g), destroy(), _forensicsView() }(../core/game-host.d.ts)。 - 🔴
_forensicsView()返{ seed?, state: () => snapshot }——state是 thunk;宿主boot-game-host.js的host.state()调的是_forensicsView().state()。直接返裸 snapshot 会让 host.state() 抛错、九门读不到态(必踩坑)。 - 受控面铁律:零引擎 import;能力经
boot.ctx.getEngine()/getInput()/time/random;render 只画到boot.mainContext(render(g) 入参);输入/绘制坐标 = 逻辑像素 390×844;禁Math.random/Date.now/addEventListener/rAF/new AudioContext/document/window。
声明式词汇(本切片支持)
- input op(behavior.trigger='input'):
hit-nearest-target(点最近目标计分)·control-x(实体 x 跟手=连续控制)·flap(竖直上冲)·move-lr(点左/右半 → 实体左/右移一步=方向式 steer)·tap-safe(点 safe 目标计分、点.hazards列出的 hazard 即记 miss=辨别/规避)。 - update op(trigger='update'):
drift-targets(目标慢漂移)·fall-and-catch(下落物+接住=控制类)·flappy-gap(重力+穿缝=flappy)·reach-goal(实体 x 抵达目标 x 容差内 → 计分+目标重生=导航/收集)。 - rule:
{ condition: "<state字段> <op> <数字|config.键>", outcome: win|lose }(序前优先);胜负 →phase=gameover+ latch 驻留。可引用 state 字段:score/misses/elapsed(相对秒,init 起算)。时间压力维度(限时/倒计时)=elapsed >= config.timeLimit → lose(零新增 op,复用 rule 机制);⚠ 勿用t(绝对单调时,禁作时限判)。 - 命中/接住/穿缝/抵达/胜负事件经
ctx.getEngine()发粒子+音效(满足九门 F 真接线 + game-feel)。
六类资产消费(E3,对齐 contracts/agent-loop/source-project.schema.json 冻结 assetSpec)
assetSpec = { id, category∈[sprite,character,effect,scene,ui,music], ref, url?, provider? }——引用/url,无内联数据。消费侧(引擎线)按类:
| 类 | 消费路径 | 兜底(spec-only 无 url) |
|---|---|---|
sprite / character |
实体 render 组件经 .asset 引用 → 有 url 浏览器 new Image+drawImage |
几何矩形(命中变暗) |
ui / effect |
同上(经 render 组件)/ effect 亦由命中粒子反馈体现 | 几何 / 粒子 |
scene |
顶层背景(非实体引用):取首个 category==='scene',有 url → drawImage 满幅 |
默认底色 #0d1117 |
music |
经 url 不可消费——冻结受控面 getEngine().audio 仅 zzfx synthSfx/synthSong(合成,非文件播放),无播 url 音频能力 |
静默(跨线契约缺口:需 engine audio.playUrl 或 music assetSpec 携 zzfx 歌数据;见 src/core/api.d.ts) |
边界(KTD1):适配器只解释声明式部分;复杂 behavior 真逻辑由核心 code agent 产、经 build.mjs 编进
__GameBundle(非适配器解释)。适配器覆盖简单声明式游戏的主要机制族(已 6 族,见下)。 更复杂族(aim-fire 抛射瞄准 / drag-aiming 拖拽弹弓 / match-3 消除):虽受控面有 keydown/keyup/pointermove/pointerup 足以接其输入,但其抛射物理/旋转瞄准/读盘消除是复杂 behavior 逻辑——按 KTD1 归 code-agent__GameBundle,不入声明式适配器(强行内联=越界 + 过拟合)。
已验证六族(均过真九门 A–I + 首局门)
覆盖休闲玩法的主要机制维度:点击 / 连续控制 / 离散方向 steer / 物理 / 导航 / 辨别(负反馈)。 时间压力维度(限时/倒计时)经
elapsed >= config.timeLimit规则正交叠加于任一族(复用现有 driver,无新族),单测已验。
| fixture | 族 | driver | 关键门 |
|---|---|---|---|
tap-target.source.json |
静态点击(clicker) | tap-targets | H 进展+latch |
moving-targets.source.json |
realtime 漂移 | tap-targets | E 活性 + 漂移目标命中 |
catch.source.json |
控制类(接住) | paddle-intercept | I 控制跟手 |
flappy.source.json |
flappy(重力穿缝) | flap-to-gap | 物理穿缝 |
seek.source.json |
导航/收集(seek) | seek-x | 方向式 steer(move-lr)+ 抵达计分(reach-goal) |
avoid.source.json |
辨别/规避(avoidance) | tap-targets(safeOnly) |
负反馈(tap-safe:点 safe 计分 / 点 hazard 即负)+ forensics safe 标记 |
加一个 op / archetype
- 在
SUPPORTED_INPUT_OPS/SUPPORTED_UPDATE_OPS注册 op 名。 - 在
onTap(input)或update行为循环里加 op 解释(操作entById(id)取的实体)。 - 关键事件经
ctx.getEngine()发反馈(过 F 门)。 - 若 driver 需读新实体态,在
_forensicsView().state()additive 暴露(如paddle/ball/bird/nextGap)。 - 加 fixture + 单测 + play-spec(driver/assertAfterPlay/expectLatch);
scaffoldFromSource→ build → 九门验。
验证
node --test src/adapter/adapter.test.mjs src/adapter/scaffold.test.mjs # 零依赖单测
# 九门(集成段/mini-desktop 权威;本机快走查):scaffoldFromSource → scripts/build.mjs → games/_wg1-gen/_shared/serve-and-play.sh