diff --git a/.agents/skills/littlejs-game-dev.md b/.agents/skills/littlejs-game-dev.md index 0c9655e1..2b745e44 100644 --- a/.agents/skills/littlejs-game-dev.md +++ b/.agents/skills/littlejs-game-dev.md @@ -113,7 +113,7 @@ export function createGame({ plugins, bundle, viewport }) { | **collision** | 碰撞查询(纯数学) | `circleVsCircle/circleVsAabb/aabbVsAabb/satVsPolygon`→`{hit,depth,nx,ny}`;`raycast*`;`SpatialHash`。**⚠️ Aabb=中心+半宽半高 `{x,y,hw,hh}`(非 {x,y,w,h});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);`InputBuffer{consume,peek}`;`CoyoteTimer{setGrounded,isAvailable,consume}`;`ComboWindow{hit,getCount}` | -| **particles-juice** | 粒子 + 打击感 | `spawnEmitter(preset\|cfg,x,y)`;`hitStop(dur)`/`getTimeScale`;`shakeScreen`/`getShakeOffset`;`flashScreen`/`pulseScale`;`PRESETS`(burst/trail/drift)。**⚠️ `render(g)` 必须每帧手调(非自动);`step(dt)` 默认 autoStep 自驱(经 onFrame),要手调 step 须工厂传 `autoStep:false` 防双步进** | +| **particles-juice** | 粒子 + 打击感 | `spawnEmitter(preset\|cfg,x,y)`(或 `burst(x,y)` 直发 burst 预设,#6/#10 母语别名);`hitStop(dur)`/`getTimeScale`;`shakeScreen`/`getShakeOffset`;`flashScreen`/`pulseScale`;`PRESETS`(burst/trail/drift)。**⚠️ `render(g)` 必须每帧手调(非自动);`step(dt)` 默认 autoStep 自驱(经 onFrame),要手调 step 须工厂传 `autoStep:false` 防双步进** | | **palette-post** | 调色 + 后处理 | `rgbToHsl/hslToRgb/applyPaletteMap/shiftHsl`;`setPost/renderPost`(vignette/dither/scanline)。**色板由你注入,插件不内置成品色** | | **audio-music** | 程序化音频 | `loadSong/play/stop/loop`;`setIntensity`/`setIntensityLayering`;`playSfx('blip'\|'thud'\|'chime')` | | **save-progress** | KV 持久化 | `get(key,default)/set/remove/clear`;`createMemoryAdapter`/`createLocalStorageAdapter` | diff --git a/cheap-worker/cheap_roles.py b/cheap-worker/cheap_roles.py index f66765c2..f11d3d26 100644 --- a/cheap-worker/cheap_roles.py +++ b/cheap-worker/cheap_roles.py @@ -46,7 +46,7 @@ A 读手册(1)+ 读你的起点 game-logic.js(4)→ B 按 brief 定玩 - **入参是扁平的 { plugins, bundle, viewport }——没有 opts、没有 opts.runtime、没有 ctx**(L1 已替你摊平)。受控面 ctx 只在五法 **init(boot) 的 boot.ctx**;**调用形态钉死**:时间 **ctx.time.nowMs()**、随机 **ctx.random.next()/ctx.random.range(a,b)**、画布 ctx.getContext2d()、引擎 ctx.getEngine()、日志 ctx.log(tag,msg)。**time/random 是对象、不是函数——绝不写 ctx.nowMs()/ctx.random()**(实测翻车:当函数调 → 抛错 → 游戏空转不 spawn/不计分)。五法 = init(boot)/update(dt)/render(g)/destroy()(+必须 _forensicsView)+ **输入方法 handleTap(x,y)**(见下「输入契约」)。 - **插件扁平注入:直接 const { sceneFsm, sessionScore, hudUi, timerScheduler } = plugins(用到才解构)**;**绝不写 opts.runtime.plugins**(那是 L1 内部的,你见不到 → 写了就 undefined → boot 崩 reading 'define')。游戏只用插件、不 new、不 register。 - **可用插件键(全 11 已注入,用到才取)**:sceneFsm / sessionScore / hudUi / timerScheduler / save / gamefeel / juice / palettePost / audioMusic / collision / physics。 -- **插件方法名一律以 api.d.ts 为准、别臆测**(实测幻觉:scene-fsm 写 defineScenes〔应 define〕;particles-juice 没有 burst〔事件 API 名以其 api.d.ts 为准〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ 实测易犯的幻觉 API」逐条列 ❌→✅。 +- **插件方法名一律以 api.d.ts 为准、别臆测**(实测幻觉:scene-fsm 写 defineScenes〔应 define〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ 实测易犯的幻觉 API」逐条列 ❌→✅。 - **hudUi.drawButton(g, rect, opts) 回吐它绘制的命中矩形 { x, y, w, h }**:✅ 直觉写法就对——const btn = hudUi.drawButton(g, { x, y, w, h }, { label }); if (hudUi.pointInRect(px, py, btn)) 命中处理(drawButton 返回命中矩形,直接接住判命中即可)。命中判定走 hudUi.pointInRect(px, py, rect)(**只在 hudUi,collision 插件没有 pointInRect 方法**)。**第二参是 rect 对象 { x, y, w, h }、不是 label/坐标位置参**——别写 drawButton(g, '开始', x, y, w, h) 这种位置参形态。 - **sceneFsm.define(name, handlers) 逐场景调一次——绝不单对象批量注册**:❌ sceneFsm.define({ menu: {...}, play: {...}, over: {...} }) → 第一参当成了场景名「[object Object]」、menu/play/over 全没真注册 → 后续 transition('play') 找不到目标态被忽略 → 卡菜单(实测合成/2048 类翻车点)。✅ **每个场景各调一次**:sceneFsm.define('menu', { onEnter(){...} }); sceneFsm.define('play', { onEnter(){...}, update(dt){...} }); sceneFsm.define('over', { onEnter(){...} });(define 第一参恒为场景名字符串、第二参才是回调表 { onEnter, onExit, update, render };可链式 .define(...).define(...))。 - **画面读 viewport.w/h**(viewport 是入参;别硬编码 390×844)。 diff --git a/game-runtime/src/plugins/particles-juice/api.d.ts b/game-runtime/src/plugins/particles-juice/api.d.ts index 11a79733..33222e8c 100644 --- a/game-runtime/src/plugins/particles-juice/api.d.ts +++ b/game-runtime/src/plugins/particles-juice/api.d.ts @@ -207,6 +207,8 @@ export interface ParticlesJuicePlugin extends Plugin { y: number, overrides?: Partial ): number; + /** burst 预设的母语别名(#10:AI 直觉 juice.burst(x,y) 直接可用)= spawnEmitter('burst', x, y, overrides)。 */ + burst(x: number, y: number, overrides?: Partial): number; /** 停止一个 continuous 发射器(不再补充新粒子)。幂等。 */ stopEmitter(id: number): void; /** 手动推进一帧(autoStep:false 时用)。 */ diff --git a/game-runtime/src/plugins/particles-juice/impl.js b/game-runtime/src/plugins/particles-juice/impl.js index c10e01b1..b7bfd9a3 100644 --- a/game-runtime/src/plugins/particles-juice/impl.js +++ b/game-runtime/src/plugins/particles-juice/impl.js @@ -459,6 +459,18 @@ export function createParticlesJuicePlugin(opts) { return id; }, + /** + * burst 预设的母语别名(#10 阶段一B:AI 直觉 juice.burst(x,y) 直接可用)= spawnEmitter('burst', x, y, overrides)。 + * 引 plugin 自身委托、不依赖 this。 + * @param {number} x 喷发原点 x(像素)。 + * @param {number} y 喷发原点 y(像素)。 + * @param {Partial} [overrides] 在 burst 预设上的局部覆盖(count/角度等)。 + * @returns {number} 发射器 id(可用它 stopEmitter)。 + */ + burst(x, y, overrides) { + return plugin.spawnEmitter('burst', x, y, overrides); + }, + /** * 停止一个发射器(转发引擎句柄 stop→销毁引擎发射器)。幂等(未知 id / 已停均静默)。 * @param {number} id 发射器 id。 diff --git a/game-runtime/src/plugins/particles-juice/test/particles-juice.test.mjs b/game-runtime/src/plugins/particles-juice/test/particles-juice.test.mjs index 137b9087..5267fa0e 100644 --- a/game-runtime/src/plugins/particles-juice/test/particles-juice.test.mjs +++ b/game-runtime/src/plugins/particles-juice/test/particles-juice.test.mjs @@ -129,6 +129,18 @@ test('曲线求值:四类曲线端点恒等、衰减/线性单调、t 越界 * 再用「从预设配置 + evalCurve 独立算出的期望值」逐字段断言(不反查内部实现,钉契约不钉实现)。 * ──────────────────────────────────────────────────────────────────────────── */ +test('#10 burst(x,y) 是 spawnEmitter("burst",x,y) 的母语别名(同 spec 入引擎)', () => { + let captured = null; + const { context } = makeMockEngineContext((spec) => { captured = spec; return { isActive: () => true, stop() {} }; }); + const p = createParticlesJuicePlugin({ autoStep: false }); + p.init(context); + const id = p.burst(100, 200); + assert.equal(typeof id, 'number', 'burst 返发射器 id'); + assert.ok(captured, 'burst 应经 spawnEmitter 把 spec 交给引擎'); + assert.deepEqual(captured.pos, { x: 100, y: 200 }, 'burst 透传发射原点'); + assert.equal(captured.emitTime, 1 / 60, 'burst → emitTime=1/60(与 spawnEmitter("burst") 同)'); +}); + test('toEngineSpec 纯映射(经 spy 捕获):burst 预设逐字段换算正确', () => { let captured = null; const { context } = makeMockEngineContext((spec) => { captured = spec; return { isActive: () => true, stop() {} }; }); diff --git a/game-runtime/tools/amodel-gen/prompt.mjs b/game-runtime/tools/amodel-gen/prompt.mjs index 38d82e96..23e7dec3 100644 --- a/game-runtime/tools/amodel-gen/prompt.mjs +++ b/game-runtime/tools/amodel-gen/prompt.mjs @@ -89,7 +89,7 @@ A 读手册(1)+ 读你的起点 game-logic.js(4)→ B 按 brief 定玩法(核心 - **入参是扁平的 { plugins, bundle, viewport }——没有 opts、没有 opts.runtime、没有 ctx**(L1 已替你摊平)。受控面 ctx 只在五法 **init(boot) 的 boot.ctx**;**调用形态钉死**:时间 **ctx.time.nowMs()**、随机 **ctx.random.next()/ctx.random.range(a,b)**、画布 ctx.getContext2d()、引擎 ctx.getEngine()、日志 ctx.log(tag,msg)。**time/random 是对象、不是函数——绝不写 ctx.nowMs()/ctx.random()**(实测 M3 翻车:当函数调 → 抛错 → 游戏空转不 spawn/不计分)。五法 = init(boot)/update(dt)/render(g)/destroy()(+必须 _forensicsView)+ **输入方法 handleTap(x,y)**(见下「输入契约」)。 - **插件扁平注入:直接 const { sceneFsm, sessionScore, hudUi, timerScheduler } = plugins(用到才解构)**;**绝不写 opts.runtime.plugins**(那是 L1 内部的,你见不到 → 写了就 undefined → boot 崩 reading 'define')。游戏只用插件、不 new、不 register。 - **可用插件键(全 11 已注入,用到才取)**:sceneFsm / sessionScore / hudUi / timerScheduler / save / gamefeel / juice / palettePost / audioMusic / collision / physics。 -- **插件方法名一律以 api.d.ts 为准、别臆测**(M3 实测幻觉:scene-fsm 写 defineScenes〔应 define〕;particles-juice 没有 burst〔事件 API 名以其 api.d.ts 为准〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ M3 实测易犯的幻觉 API」逐条列 ❌→✅。 +- **插件方法名一律以 api.d.ts 为准、别臆测**(M3 实测幻觉:scene-fsm 写 defineScenes〔应 define〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ M3 实测易犯的幻觉 API」逐条列 ❌→✅。 - **hudUi.drawButton(g, rect, opts) 回吐它绘制的命中矩形 { x, y, w, h }**:✅ 直觉写法就对——const btn = hudUi.drawButton(g, { x, y, w, h }, { label }); if (hudUi.pointInRect(px, py, btn)) 命中处理(drawButton 返回命中矩形,直接接住判命中即可)。命中判定走 hudUi.pointInRect(px, py, rect)(**只在 hudUi,collision 插件没有 pointInRect 方法**)。**第二参是 rect 对象 { x, y, w, h }、不是 label/坐标位置参**——别写 drawButton(g, '开始', x, y, w, h) 这种位置参形态。 - **sceneFsm.define(name, handlers) 逐场景调一次——绝不单对象批量注册**:❌ sceneFsm.define({ menu: {...}, play: {...}, over: {...} }) → 第一参当成了场景名「[object Object]」、menu/play/over 全没真注册 → 后续 transition('play') 找不到目标态被忽略 → 卡菜单(实测合成/2048 类翻车点)。✅ **每个场景各调一次**:sceneFsm.define('menu', { onEnter(){...} }); sceneFsm.define('play', { onEnter(){...}, update(dt){...} }); sceneFsm.define('over', { onEnter(){...} });(define 第一参恒为场景名字符串、第二参才是回调表 { onEnter, onExit, update, render };可链式 .define(...).define(...))。 - **画面读 viewport.w/h**(viewport 是入参;别硬编码 390×844)。