games-development-ai/.agents/skills/littlejs-game-dev.md
lili cbfd4d871b
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(acceptance): 闭合 playtest v3 与 A+ 可信消费链
固化 Match-3 生产者、视觉、音频与双 Judge 证据闭包。

将《山海行纪》r1.1 绑定新的不可变 release,并以生产预检现场核验 bundle、Registry/2 和 25 项 Writer 快照。

同步地图1平衡锁值、跨游戏回归修复、验收契约与 SoT 证据。
2026-07-28 20:16:13 -07:00

28 KiB
Raw Blame History

name, description
name description
littlejs-game-dev 当生成 agent 要写一款可上线 LittleJS 小游戏的 game-logic.js(接五法、调 11 注入插件、守受控面红线、mmx 产素材)时使用:代码分层结构、写什么 vs 调什么、插件 API 速查与 M3 易犯幻觉、资产放置。轻中档 code 层唯一作业手册。

skill: littlejs-game-dev —— AI 直接写 LittleJS 游戏代码(调插件 API,不重写实现)

定位:这是 生成 agent 造一款可上线的高质小游戏的唯一作业手册。绘境AI 的终态产物 = src/ 多文件 LittleJS 工程 (创始人 2026-06-20 拍板;取代已废的「gameDefinition JSON 投影」线)。本手册回答四件事: ① 代码长什么结构 ② 你写什么 / 调什么 ③ 怎么调 11 注入插件 API+ runtime-probe 仅取证不注入)④ 美术/音乐/音效放哪、怎么用 mmx 生成。

这一档定位(钉死):轻量 ≠ 简单。AI 参与深度低,但产物是高质小游戏——复杂能力由工程脚手架 + L2 插件库承担,你少写、多调插件做出卖相与深度,不是产没人会玩的简单玩具。

配套:契约匹配范例 = game-runtime/games/_template/(克隆起点,签名以现契约为准 —— 照它的结构改造,别抄旧 wanglanmei-ref 的 game-logic,那是旧 opts 签名漂移); 插件契约 SoT = 各 game-runtime/src/plugins/<name>/api.d.ts(签名以它为准);装载契约 = game-runtime/src/core/game-host.d.ts


0. 三句话心法

  1. 你只写「玩法身份(WHAT)」,通用机制(HOW)一律调插件——能调就别自己实现。 写碰撞数学/粒子/补间/计分/状态机 = 错;这些是插件。
  2. 你的唯一产出靶 = game-logic.js 的命名导出 createGame({plugins,bundle,viewport}),返回 GameInstance:init/update/render/destroy + 输入方法 handleTap(x,y)[/handleKey(key)],另可挂可选调试接口 _forensicsView()零引擎 import
  3. 受控面铁律:游戏里 绝不出现 Date.now / performance.now / Math.random / setTimeout / requestAnimationFrame / new AudioContext / import 'littlejsengine'。时间/随机/音频/引擎能力一律经 boot.ctx 或注入的插件拿;输入不调 ctx.getInput——写实例方法 handleTap(x,y)/handleKey(key),L1 据此在 pointerdown/keydown 时派发(确定性、可取证、引擎可换)。

0.5 卡壳就查(场景 recipe + 能力图谱 + 引擎摘要)

写某个具体场景卡住了、或不确定某能力从哪来,先读对应的一篇,别硬想:

  • 高频场景怎么落地read_file('.agents/skills/recipes/README.md') 是「要做 X → 读 Y」索引命中按钮点不动 / 计时慢 1000 倍 / 缺图崩 / 场景机卡菜单 / 结算不驻留——五类头号翻车各有一篇「抄这段形态 + 病根」。卡在哪读哪篇。
  • 有哪些插件、每件详细用法read_file('.agents/skills/plugin-capability-map.md')11 件注入插件各配 api.d.ts(精确签名)+ PLUGIN.md(用法散文/装配坑/示例,比 api.d.ts 更全)的指针;另有 6 件未注入储备——它们你用不到plugins.<键> 取不到、会 undefined别写。
  • 引擎能到底能干什么read_file('.agents/skills/engine-capabilities-brief.md')≤3KB引擎重活都被插件封装、你少碰 getEngine(),外加坐标系 2× 换算坑。

1. 代码结构(可导航多文件)

一款游戏 = 一个目录,固定分层。每个文件单一职责,改一处不牵动全身:

这条「改一处不牵动全身」的工程规范性正是 A11 对话式调整回路的可改性根基——资产统一 assets.js、数值集中 core.js/balance.js,使一次自然语言调整只落到"那一处"(换资产/调数值 = 改一处、零 LLM改玩法 = 有界单文件重写。A11 回路机制(两段式判意图/执行 + 三断言)见 ../knowledge/tech-decisions.md §1.2。

games/<your-game>/
├── entry.js              # 打包入口:动态 import 引擎 → 委托 main.js(引擎 import 只在这)
├── index.html            # 加载 dist bundle,调 bootXxx 启动
├── src/
│   ├── main.js           # host 引导薄包装:调通用 bootGameHost,装配取证全局
│   ├── host-config.js    # 【L1 固定·不写】实例化全11插件 + 视口 + 工厂 wiring(harness 已配好、write 拒)
│   ├── game.js           # 【L1 固定·不写】薄 wrapper:摊平 opts.runtime→交 game-logic(write 拒)
│   ├── game-logic.js     # 【L3·你写·必写】export function createGame({plugins,bundle,viewport})→GameInstance 五法。零引擎 import
│   ├── core.js           # 【L3·推荐】纯逻辑:状态机 + 命令 API(无 I/O/引擎,node 可单测;小游戏可内联进 game-logic)
│   ├── balance.js        # 【内容】数值 / 关卡 / 价格 / 掉落表(纯数据 + 公式)
│   ├── render.js         # 【绘制】把状态画到 g;导出视口 W/H 常量
│   ├── assets.js         # 【资产声明】图键→文件名映射 + loadAssets(host 层,可用 Image)
│   └── audio-director.js # 【普通游戏音频编排】BGM/音效调度(可选;标准 Match-3 禁写)
├── assets/
│   ├── manifest.json     # 资产清单:file/role/bytes/sha256/source{tool,model,prompt}
│   ├── gen-ledger.md     # 生成历史(prompt/字节/hash/替换记录)——改素材的索引
│   └── *.jpg *.mp3       # 美术 / 音乐 / 音效(mmx 产物)
└── test/
    ├── *.test.mjs        # node 纯逻辑测试(core/balance)
    └── *.cdp.cjs         # e2e 真玩取证(九门)

分层依赖方向(只能往下依赖,不能反向): entry-bundle → host-config → game(L1 wrapper)→ game-logic(你写)→ {core, render, balance, assets};core/balance 最纯(零依赖、可单测)。

别把 4000 行的 wanglanmei-ref 整个抄过来——它是「进阶worked 参照」。克隆 _template/、follow 这个结构,逻辑写进 core.js


2. 写什么 vs 调什么(这是本手册的核心)

谁负责 具体
玩法身份 WHAT 你写 game-logic.js(必)把玩法接到五法 + 调注入的插件 · core.js 状态机与规则 · balance.js 数值/关卡/内容 · render.js 画面编排与命中区布局 · 事件→表现映射(普通游戏可触发 juice/音效;标准 Match-3 音频由 host 自动编排) · assets.js 资产声明
通用机制 HOW 调插件 碰撞 / 物理 / 手感 / 缓动 / 输入缓冲 / 粒子打击感 / 音频 / 存档 / 调色 / 计分胜负 / HUD-UI / 场景状态机 / 定时调度(共 11 注入件,见 §4;另 runtime-probe 仅取证不注入)
装载 + 取证 平台供给 bootGameHost 引擎五回调装载 · 插件注入 · boot.ctx.getEngine() · runtime-probe 六锚取证

判定口诀:"这段是『这款游戏特有的规则/内容/画面』吗?" 是 → 你写;"这是『换个游戏也一样』的通用能力吗?" 是 → 找插件调。找不到对应插件再自己写(并反馈:它可能该被沉淀成新插件)。


3. 装载契约(你的产出靶)

游戏工厂 = GameHostFactory(game-host.d.ts:66),host 调它建 GameInstance:

// src/game-logic.js —— 【你写这个】命名导出 createGame,零引擎 import
//   入参由 L1(game.js wrapper)替你摊平:扁平 { plugins, bundle, viewport },没有 opts.runtime 嵌套。
export function createGame({ plugins, bundle, viewport }) {
  // 插件已由 L1 host-config 扁平注入,用到才解构;标准 Match-3 不解构 audioMusic:
  const { sceneFsm, sessionScore, hudUi, timerScheduler /* …按需 gamefeel/juice/collision/physics/audioMusic */ } = plugins;
  const core = createCore();                 // 你的纯逻辑(可内联进本文件)

  return {
    async init(boot) {
      this.ctx = boot.ctx;                   // 受控面:time/random/getEngine/log(只在 init(boot) 拿;输入不在此订阅)
      this.assets = await loadAssets('./assets/');   // 容错:缺图降级不崩
    },
    update(dt) { bundle.tick(dt); core.update(dt); },              // ★ 必调 bundle.tick(驱动插件 onFrame)
    render(g) { drawFrame(g, core.state(), this.assets, hudUi, viewport); }, // g=引擎 mainContext;读 viewport.w/h
    handleTap(x, y) { core.tap(x, y); },     // ★输入入口:L1 在 pointerdown 时直达调用(别自己订阅输入)
    destroy() {},                            // 卸载(幂等)
  };
}

要点:

  • render(g) 的 g = boot.mainContext(引擎绘制面);绘制画在 g 上,不要用插件的 getContext2d()(那是另一块 overlay)。
  • 插件扁平注入:直接 const { sceneFsm, … } = plugins(plugins 是 createGame 入参;不是 opts.runtime.plugins —— 那是 L1 内部的、你见不到,写了就 undefined → boot 崩)。游戏只「用」,不 new、不 register
  • 时间/随机/引擎走 boot.ctx(init(boot) 内拿;入参里没有 ctx)。
  • 输入收归 L1(别自己订阅):不写 ctx.getInput()、不自建点击队列、不在 update 里挑时机消费点击——只写实例方法 handleTap(x,y)(点击)/handleKey(key)(键盘),L1 的 game.js wrapper 在 pointerdown/keydown 时直达调用。HJ-AGI 实测:M3 把点击塞 pendingClicks、在 update 的 play 分支(menu early-return 之后)才消费 → 菜单点击永不处理 → 启动不了;收归 L1 结构性根除。check 会拦 getInput 与"零输入方法"。〕
  • ⚠️ 驱动插件 onFrame:update(dt)必须 bundle.tick(dt)(bundle 是 createGame 入参)——否则 timer-scheduler/gamefeel 等的 onFrame 不推进、timer 永不到期、一局不结束。
  • viewport:画面口径读入参 viewport.w/h,别硬编码 390×844。
  • 调试视图(_forensicsView(),可选):挂 _forensicsView(){ state(), measures() },state() 导出可观测态(如 { phase, score })供人工调试观测——W-AXIS-V2 起验收不再读它(验收 = 四门机械投影 ∧ 测试 agent 视觉引导真玩),写不写都不影响过门;保留它是给调试与日志一个统一出口。另可选挂 _handleAction(action)(host.do() 走逻辑驱动,便于确定性 e2e)。范式见 _template/src/game-logic.js
  • 关键事件记日志(ctx.log):在 init/场景切换/得分/出错处 调 ctx.log('tag', 信息)(ctx=init(boot).boot.ctx)→ 写 window.__gameLog,便于读日志诊断运行时问题。插件调用已由 host 自动记(你只补游戏语义事件)。

4. 11 注入插件能力速查(调 API,别重写;另 runtime-probe 仅取证不注入)

完整签名以 game-runtime/src/plugins/<name>/api.d.ts 为准(本表只给「选型 + 头部调用」)。每件都是 createXxxPlugin(opts) 工厂,经 host-config 实例化、注入。命名全 engine 级、零玩法语义。 想看用法散文/装配坑/示例(比 api.d.ts 更全) → 同目录 PLUGIN.md(如 game-runtime/src/plugins/scene-fsm/PLUGIN.md);一张全表 + 未注入储备清单见 .agents/skills/plugin-capability-map.md

基元层(8 件 · 算法/物理/手感/音/存档)

插件 用于 头部 API
collision 碰撞查询(纯数学) collision.circleVsCircle/circleVsAabb/aabbVsAabb/satVsPolygon{hit,depth,nx,ny};raycast*;SpatialHash(#3 都挂实例)。Aabb 盒 #12:可传 {x,y,w,h}(左上角+尺寸,与 Rect/drawButton 一致)或 {x,y,hw,hh}(中心+半宽高)——两形态都对;Circle {x,y,r}
physics-lite 街机运动原语(非刚体) Projectile{step,reset}/integrateProjectile(state,dt,opts);springStep/criticalDamping;moveWithConstraint;clampSpeed/applyFriction⚠️ ProjectileState={px,py,vx,vy}(位置是 px/py 非 x/y);重力经 opts.gravity={x,y}`
gamefeel 手感 easing(13 曲线 in/out/inOut);内置 gamefeel.inputBuffer{consume,peek};工厂(免 new + 自动注入受控 time,#4):gamefeel.createCoyoteTimer(opts)→{setGrounded,isAvailable,consume}gamefeel.createComboWindow(opts)→{hit,getCount}
particles-juice 粒子 + 打击感 spawnEmitter(preset|cfg,x,y)(或 burst(x,y) 直发 burst 预设,#6/#10 母语别名);hitStop(dur)/getTimeScale;shakeScreen/getShakeOffset;flashScreen/pulseScale;PRESETS(burst/trail/drift)。⚠️ juice.render(g) 必须在你的 render(g) 里每帧手调(非自动,#11)——忘了 → 粒子/打击感不显示;step(dt) 默认 autoStep 自驱(经 onFrame),要手调 step 须工厂传 autoStep:false 防双步进
palette-post 调色 + 后处理 rgbToHsl/hslToRgb/applyPaletteMap/shiftHsl;setPost/renderPost(vignette/dither/scanline)或 applyPreset('retro'|'crt'|'soft'|'dither'|'none') 一键观感(#14)。换色色板(from/to)仍由你注入、插件不内置成品色
audio-music 程序化音频 普通游戏可用:loadSong(载曲)/play(intensity?)(播已载曲;入参是情绪强度 0..1、不是曲目名,#9)/stop/loop;setIntensity;playSfx(name)标准 Match-3 例外:该键不进入 L3Writer 不写任何音频逻辑。
save-progress KV 持久化 get(key,default)/set/remove/clear;createMemoryAdapter/createLocalStorageAdapter
runtime-probe 启动取证(六锚) mark(anchor)/getRecords/toJSONL/verify/verifyChain

编排层(4 件 · 让你"只写玩法、不手写编排")

插件 用于 头部 API
session-score 计分 + 胜负闸 addScore(d)(=add(d) 别名)/setScore/getScore/getBest/reset;win()/lose()/getOutcome()/isOver()胜负条件你写:if(lives<=0) s.lose()。best 接 save:createSessionScorePlugin({best:{load:()=>save.get('best',0),save:b=>save.set('best',b)}})
hud-ui HUD/UI 绘制 + 命中 纯:pointInRect/pointInCircle/layoutRow/layoutColumn/hitTest;绘制(传 g):measureText/drawText/drawPanel/drawButton/drawBar(g,...)
scene-fsm 场景/状态机 define(name,{onEnter,onExit,update,render})/start(name)/transition(to,payload?)/current()/update(dt)/render(g)/reset()状态名你定
timer-scheduler 受控时钟调度 after(ms,cb)/every(ms,cb)/sequence([{delayMs,run}])/cancel(h)/clear⚠️ 单位是毫秒 #7:3 秒写 after(3000,cb) 不是 after(3,cb)every(1000,cb)=每秒(update(dt) 的 dt=秒相反,别混)。经 onFrame+受控时钟(游戏须在 update 内 bundle.tick(dt) 驱动,见 §3),不要用 setTimeout

典型组合:菜单→玩→结算 用 scene-fsm;分数与最高分用 session-score(best 接 save-progress);倒计时/刷新波次用 timer-scheduler;分数/按钮/血条画面用 hud-ui;手感打击用 particles-juice + gamefeel

标准 Match-3两件 Writer 能力 + 一件 host-only 能力

绑定 match3.orthogonal-swap-v1puzzle L1 会在通用 11 件之外成对注入 plugins.match3ProducerProfileplugins.match3VisualTimeline。普通 puzzle 和其它品类没有这两个键。

  • 先读 game-runtime/src/plugins/match3-producer-profile/api.d.ts:棋盘 substrate、几何、格心、主题安全区和选中白环全部调用它不能自算另一套坐标或自画 selection。
  • 再读 game-runtime/src/plugins/match3-visual-timeline/api.d.tsPLUGIN.md:第二击同步业务结算时调用 begin()update(dt) 推进,render(g) 只读 view().tileshandleTap 入口先用 isLocked() 拒绝锁定期额外输入,重玩调用 reset()
  • 标准消除效果由固定 host 在 L3 的 render(g) 完成后自动合成L1 已把 match3ProducerProfile.geometry() 与冻结六色主题 palette 注入时间线。Writer 不得调用 match3VisualTimeline.renderEffects(),也不得另造匹配消失粒子、光环或碎片;只按 view().tiles 绘制主题棋子主体。
  • 第三件 match3AudioDirector 只属于可信 host绝不进入 plugins。它自动提供主题 BGM、交换、无效回滚、每轮 clear 的复合消除音、fall 落定音和带终局结果的 settle 音;声音严格跟随受保护视觉 transition 账,连锁不会在同步业务递归里提前叠播。
  • 标准 Match-3 Writer 不得解构或调用 audioMusic / playSfx,不得取 getAudioContext()、自行创建 AudioContext/oscillator/buffer source也不得另写标准 cue、BGM 或连锁升调逻辑。菜单、首击选中和重玩在 v1 本就没有 cue不要自行补齐静音按钮、首手势解锁、循环和主题资产均由 host 负责。
  • beforeKinds/swappedKinds/afterFallKinds/finalKinds 是 0-based 8×8 JSON 原语数组;pair/runs/matchedCells 是 1-basedview 的逻辑和视觉坐标也是 1-based视觉坐标可为浮点。
  • 业务事件仍必须在第二击同步调用栈内写完。时间线只播放 sealed 事务,不能补写事件、改业务盘面或重新算连锁;终局结果可同步 latch但终局场景必须等 settle 完成。
  • 禁止自己维护另一套 animPhase/visualQueue 代替受保护能力,也禁止给最终业务棋盘旧坐标闪白冒充消除。八轮以上会封存真实终盘并标记 visualComplete:false,不能因为视觉验收上限让已提交业务崩溃。

⚠️ M3 实测易犯的幻觉 API(别这么写 → 正确)

铁律:插件方法名一律以 src/plugins/<name>/api.d.ts 为准,别按"常见库套路"臆测。 以下是 M3 即便读了 api.d.ts 仍反复臆测出的错名(HJ-AGI-003 saae 实测),逐条记牢:

  • sceneFsm.defineScenes({...}) sceneFsm.define(name, {onEnter,onExit,update,render}) 逐个定义(没有 defineScenes)。
  • sceneFsm.define({ menu:{...}, play:{...}, over:{...} }) 单对象批量注册 → 每个场景各调一次 sceneFsm.define('menu', {...}); sceneFsm.define('play', {...}); sceneFsm.define('over', {...})(define(name, handlers) 第一参恒为场景名字符串;传对象 → 第一参变成 [object Object]、menu/play/over 全没注册 → transition('play') 找不到目标态被忽略 → 卡菜单进不去 play。实测合成/2048 类翻车点)。可链式 .define(...).define(...)回调表里 update(dt) 只收 dt、render(g) 只收 g、与五法同形——别臆造场景名/state 首参(#15),绝不写 update(scene, dt)
  • hudUi.drawButton(g, rect, opts) 回吐它绘制的命中矩形 {x,y,w,h}(阶段一B #5 改签名):直觉写法就对——const btn = hudUi.drawButton(g, {x,y,w,h}, {label}); if (hudUi.pointInRect(px,py,btn)) {...},drawButton 返回命中矩形、直接接住判命中即可(也可先单独定义 rect 再画再 pointInRect,两种都对)。命中判定 hudUi.pointInRect(px, py, rect)(签名 (px, py, rect);只在 hudUi,collision 插件没有 pointInRect 方法)。另:drawButton 第二参是 rect 对象 {x,y,w,h}、不是 label/坐标位置参——别写 drawButton(g, '开始', x, y, w, h)
  • 起局/交互按钮的命中矩形在场景 onEnter(状态进入点)确定性建立、render 只画不写——绝不让它只在 render 副作用里生成(menuStartBtn = renderMenu(...) 那种回写):并发渲染帧饥饿时 render 可能一次都没跑过 → 命中矩形恒 null → 菜单点不动、一局起不来(卡 menu 头号成因)。把按钮几何抽成纯函数,onEnter 调它设命中区、render 调它画,命中与绘制同源不漂移(_template* 六款范例的 menuStartRect/overRestartRect 即此写法)。
  • scene 的 update 回调臆造首参写成 update(running, dt) scene 的 update 回调只收一个参数 (dt)(sceneFsm.define(name, { update(dt){…} });sceneFsm.update(dt) 只透传 dt,没有 running 之类首参)。写错首参 → 真正的 dt 落到第二参=undefined → state.elapsedMs += undefined = NaN → 不再 spawn/计时、计时显示「NaNs」、游戏空转看着像没 bug 实则永不推进(实测 wanglanmei-v2:菜单能进 play 但无顾客、营业额恒 0)。任何回调签名都以 src/plugins/<name>/api.d.ts 为准,别臆造参数。
  • update(dt) 把 dt 当毫秒直接累加 elapsedMs += dt dt 单位是「秒」(≈1/60);游戏内毫秒计时(倒计时/spawn 间隔/耐心,常量如 60000/1300/4800)累加要 elapsedMs += dt * 1000。漏 ×1000 → 计时慢 1000 倍 → 永不到 spawn 阈值=零顾客、一局永不结束(实测 wanglanmei-v2 翻车点;_template 范例里 remainMs - dt*1000 就是这个换算)。
  • _forensicsView()(若写)在外层先算好 phase 再闭包返回(const phase=...; return { state(){ return { phase } } })→ 导出值在 state() 函数体内部实时计算(host 只在 boot 调一次 _forensicsView(),外层算的值定格成 boot 快照——调试时读到的恒是旧值,实测 wanglanmei-v2:游戏真在 play 但读到的 phase 恒 menu)。照 _template/src/game-logic.jsstate: () => ({ phase: sceneFsm.current(), ... }) 写。
  • 在 game-logic.js 取受控面用 opts.ctx / opts.boot 受控面在实例 init(boot)boot.ctx(boot.ctx.time / boot.ctx.random / boot.ctx.getEngine() / boot.ctx.log());输入不经 boot.ctx.getInput——写实例方法 handleTap(x,y)/handleKey(key),L1 派发;createGame 入参是扁平 {plugins,bundle,viewport},没有 ctx
  • 修 bug 时图省事写 Math.random() / Date.now() / setTimeout() boot.ctx.random()boot.ctx.random.next() / boot.ctx.random.range(a,b)boot.ctx.time()boot.ctx.time.nowMs()timer-scheduler.after(ms,cb)(#1:time/random 既可直接调用又保留方法,两形态都对;红线 check 必拦裸 Math.random/Date.now;越改越要守,thrash 时尤其别引入)。
  • sessionScore.add(n) = addScore(n) 别名(#6 阶段一B:直觉名直接可用,两者等价);其它方法名拿不准一律先 read_file 对应 api.d.ts
  • ⚠️ host-config.jsgame.js 都是 L1 固定 plumbing,你不写(amodel harness write_file 会拒)——通用能力、viewport(390×844)和工厂 wiring 已替你配好;标准 Match-3 还由 L1 装配 host-only 视觉 renderer 与音频 directoraudioMusic 不进入其 L3。你只写 game-logic.js(必)+ core/render/balance/assets。〔历史教训 HJ-AGI-003:曾让 agent 写 host-config + game.js 工厂,M3 反复破坏返回契约(漏 viewport 崩 reading 'w')/ registerOrder / 取插件漏 .plugins 崩 reading 'define',故把 wiring 全收进 L1 固定、L3 只写游戏逻辑收扁平 {plugins,bundle,viewport},结构性根除。〕
  • 在 game-logic.js 里写 opts.runtime.plugins / 把入参当 opts 处理 —— 新架构入参是扁平的 { plugins, bundle, viewport }(L1 的 game.js wrapper 已替你摊平 opts.runtime),写 opts.runtime.* 取到 undefined → boot 崩。 直接 export function createGame({ plugins, bundle, viewport }) { const { sceneFsm, sessionScore, hudUi, timerScheduler } = plugins; … }(plugins/bundle/viewport 都是入参、平级;无任何 opts/runtime 嵌套)。HJ-AGI-003:旧架构 M3 漏 .plugins 一层崩 reading 'define',故把嵌套收进 L1 wrapper、L3 只见扁平件,结构性根除。〕

5. 资产:美术/音乐/音效放哪 + 改图去哪找

  • 放置:普通游戏资产全部进 assets/(.jpg 美术、.mp3 音乐音效),并在 assets/manifest.json 登记每件 file/role/bytes/sha256/source{tool,model,prompt}。标准 Match-3 的主题音频由固定 L1 资产清单管理Writer 不新增或读取音频文件。
  • 声明:src/assets.js键名间接引用——IMAGE_FILES = { itemSoda: 'item-soda.jpg', ... } + 业务 id→键映射。代码里用 assets.images.itemSoda,不写死文件名。
  • 加载:game.jsinitawait loadAssets('./assets/');并行 new Image(),单图失败 → 该键置 null + 记 missing,游戏照常启动(render 走程序化/占位回退,绝不因缺图崩)。音频在首次手势后由 audio-director 懒加载解码。
  • 改一张图(LLM 怎么找):① 查 assets/manifest.json 找到该 role 的行(含原 prompt/hash);② mmx 重新生成,覆盖同名文件;③ 更新 manifest 该行 sha256/bytes/prompt;④ 文件名没变 → assets.js/render.js 都不用动;新增品类才往 IMAGE_FILES 加键。

6. 用 mmx 生成素材

mmx = MiniMax 官方多模态 CLI(本机已装;创始人 2026-06-12 拍板为美术/音乐现行方案,免 GPU)。直接 CLI 调,产物存 assets/ + 登记 manifest:

# 美术(模型 image-01)
mmx image generate --prompt "<主体> + 统一风格词 + 纯浅奶油背景,居中,简洁" --out item-soda.jpg
# 音乐(模型 music-2.6-free,对 API key 不限量)
mmx music generate --prompt "<情绪/配器/场景>" --instrumental --bpm 92 --out bgm-idle.mp3
# 音效经 speech 合成;视频 video generate(异步)

铁律:全套素材共用一组「风格词」(美术统一性之根,见 wanglanmei-ref manifest 的 styleWords);每次调用都把 prompt 写进 manifest/gen-ledger,便于复现与替换。参考成本:一款游戏 ~15 件素材 ≈ ¥0.5。

注:mmx 走公网,认证靠 ~/.mmx/credentials.json(非内网 key)。后端「生成触发 + 消费侧自动装配」产线尚在建(engine-E2/E3);当前 MVP 阶段 CLI 手动产素材 已足够。


7. 红线清单(违任一条 = 不合格)

  1. 游戏工厂 game.js 零引擎 import;引擎 import 只活 entry.js
  2. 游戏代码零 Date.now/performance.now/Math.random/setTimeout/setInterval/requestAnimationFrame/addEventListener/new AudioContext——全经 boot.ctx 或插件;标准 Match-3 进一步禁止所有 L3 音频入口。
  3. 通用机制能调插件就别自己写;反过来,玩法语义(规则/数值/胜负条件/状态名/美术)绝不塞进插件——插件是 engine 级、零品类词。
  4. render(g) 画在 host 注入的 g(mainContext);不自取 canvas。
  5. 资产缺失必须容错降级,绝不连坐崩溃。
  6. core.js/balance.js 保持纯净(无 I/O、无引擎),可 node 单测。
  7. index.html 必须自适应缩放 canvas(用 _template 那份:#game { width: min(100vw, calc(100vh*W/H)); height: min(100vh, calc(100vw*H/W)); ... !important } + #wrap flex 居中)。别回退到固定 390×844 + overflow:hidden——桌面/矮窗口会把画布底部(篮子/按钮/角色)裁在视区外且无法滚动(真人试玩才暴露,headless 测不到)。!important 是为覆盖 host 在 stub 通道写的内联 display 尺寸;buffer 不变(evidence 像素哈希不受影响),输入经 boot-game-host.toCanvasXYgetBoundingClientRect 归一,缩放下点击坐标仍准。

8. 自测(完成前必跑)

  • 纯逻辑:node --test games/<game>/test/*.test.mjs(core/balance 的规则、边界、胜负闸)。
  • 真玩取证(九门):e2e CDP harness 跑 A_boot/B_uncaught/C_frame/D_render/E_live(build-health 客观门)+ F_wiring/G_input/H_progress/I_control(driver 门);四件套证据(截图/probe/trajectory/checklist)。详见 skill game-e2e-cdp-harnesscheap-model-game-generation
  • 结构门:工厂独立默认导出、零引擎 import(import-graph 测);插件只调公开 API、不直透 littlejsengine。

相关 skill / 契约

  • 高频场景 recipe(要做 X→读 Y) → .agents/skills/recipes/README.md(命中矩形/计时器/资产回退/场景机/结算演出五篇)
  • 插件能力图谱(11 注入 + 6 储备 + PLUGIN.md 指针) → .agents/skills/plugin-capability-map.md
  • 引擎能力摘要(≤3KB) → .agents/skills/engine-capabilities-brief.md
  • 插件库总览与受控面 → game-runtime/src/core/api.d.ts(PluginContext 6 项 + getEngine)、各 plugins/<name>/api.d.tsPLUGIN.md
  • 装载契约 → game-runtime/src/core/game-host.d.ts(GameInstance 五法 + GameHostFactory)
  • 契约匹配范例 → game-runtime/games/_template/(克隆起点 + 照它改;签名以现契约为准)。wanglanmei-ref/ 是旧 opts 签名漂移的进阶参照,只看其规模/styleWords,别照它的 game-logic 写
  • 便宜模型造游戏 worker loop / 九门 → skill cheap-model-game-generationgame-e2e-cdp-harness
  • 渠道发行/打包 → skill runtime-and-multichannel