games-development-ai/game-runtime/docs/ENGINE-CAPABILITIES.md
zizi 204eaede3b feat(game-runtime): T1b-β Phase 0 地基——装 littlejsengine@1.18.19 + 抽引擎真实签名
Phase 0(执行版 §2 P0/P1)地基,只装+抽,不碰任何插件 impl/受控面/host(Phase A+):

- 新建 game-runtime/package.json:ESM workspace,littlejsengine@1.18.19 入 deps
  (与 T1 spike/channel-spike 同钉版)、esbuild@0.28.1 入 devDeps(集成段打包用);
  npm i 跑通,esbuild 实测可 resolve+bundle littlejsengine(核心符号入产物);
  node_modules 经根 .gitignore 排除,仅 package.json+lock 入库。
- 新建 game-runtime/docs/ENGINE-CAPABILITIES.md:从引擎真实源码
  node_modules/littlejsengine/dist/littlejs.d.ts(v1.18.19,5710 行)逐能力抽
  导出名/完整签名/返回类型/坐标系语义/file:line 证据(R3 铁律:断言引擎能力必查源,
  不靠记忆/文档转述)。覆盖执行版 §7+P1 点名全部能力,13 处载重引用经逐行核对无漂移。
- 消两存疑项(均确证,带 file:line):
  · postProcess 可注入自定义 GLSL shader(PostProcessPlugin(shaderCode:string),
    shadertoy 风格;但 palette-post 现算法=CPU 像素缓冲/索引色/Bayer,迁 GLSL=整体重写);
  · medal 系统存在(Medal class + 本地存储,:3457)。
- 核到两处与执行版命名预期偏差(非脑补):无独立 Music 类(实为 ZzFXMusic extends Sound/
  Sound.playMusic);无 engineUpdate 导出(主循环=引擎内部 RAF,经 engineInit 五回调接管,A0 据此)。

§7 逐函数三分裁定表作为返回件给主会话审,不改 execution.md 正文(裁定不在 spec 冻结,
Phase 0 抽签名后填定)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 03:57:07 +00:00

207 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ENGINE-CAPABILITIES.md —— LittleJS 引擎真实能力索引(T1b-β Phase 0 P0/P1 产物)
> **唯一证据源**:`node_modules/littlejsengine/dist/littlejs.d.ts`(v1.18.19,277KB,5710 行,TS `declare module "littlejsengine"`)。
> **抽取纪律(R3 整改铁律)**:每条能力的签名/返回类型/坐标系**一律来自 `.d.ts` 源码 file:line**,不靠记忆、不靠文档转述、不脑补。核不到的项显式标「未核到/存疑」。
> **用途**:供执行版 §7「插件×引擎重叠清单」按 §1.5 三分判定测试(①返回类型兼容 ②坐标系一致 ③语义一致)逐函数比对。裁定表见本仓返回件(不改 execution.md 正文)。
>
> **坐标系总纲(贯穿全表,决定三分测试第②项)**:
> - 引擎**默认世界坐标(world space)**,`screenSpace=false`;Draw 系列均有可选 `screenSpace` 与可选 `context`(`CanvasRenderingContext2D`)尾参。
> - 引擎几何类型 = `Vector2{x,y}`(类,可链式);AABB 表示为 **`pos`(中心)+ `size`(全宽全高)** 的 `Vector2` 对(见 `isOverlapping` 签名)。
> - 插件受控面几何 = 朴素 `{x,y}` number 对;插件 AABB = **`{x,y,hw,hh}`(中心 + 半宽半高)**。
> - ∴ 引擎 `size`(全宽) vs 插件 `hw`(半宽) 差 **2× 换算**;引擎 `Vector2` 实例 vs 插件 POJO 形状不同——这是「坐标系一致」项普遍不过的根因(R3 已预判)。
---
## 0. 安装与构建事实(P0)
| 项 | 值 | 证据 |
|---|---|---|
| 包名@版本 | `littlejsengine@1.18.19`(与 T1 spike / channel-spike 同钉版) | `package.json` deps;`npm ls littlejsengine` |
| 入口(main / module) | `dist/littlejs.esm.js`(640KB ESM) | `npm view littlejsengine main` |
| 类型 | `dist/littlejs.d.ts` | `npm view littlejsengine types` |
| esbuild 可 resolve | ✅ 实测 `import * as L from 'littlejsengine'` 经 esbuild `bundle+iife+es2019` 产物 432KB,核心符号 `ParticleEmitter`/`engineInit`/`drawTile`/`RandomGenerator` 入产物 | 本机 esbuild 0.28.1 试打包 |
| 渲染模型 | 混合渲染:`glCanvas`(WebGL2 批渲) + `mainCanvas`/`mainContext`(Canvas2D 叠加层,文字/UI/自定义绘制) | `.d.ts:1754-1755,1763-1767,2126` |
---
## 1. 引擎生命周期 / 主循环(A0 地基所需)
| 导出名 | 完整签名 | 返回 | 语义 / 坐标系 | 证据 |
|---|---|---|---|---|
| `engineInit` | `engineInit(gameInit: GameInitCallback, gameUpdate: GameCallback, gameUpdatePost: GameCallback, gameRender: GameCallback, gameRenderPost: GameCallback, imageSources?: Array<string>, rootElement?: HTMLElement): Promise<void>` | `Promise<void>` | **引擎启动入口**。5 个回调:gameInit(启动一次,可 async)/gameUpdate(每帧 60fps,物理前,游戏逻辑)/gameUpdatePost(物理与对象更新后,即便暂停也调,UI)/gameRender(对象渲染前,画背景/世界)/gameRenderPost(对象渲染后,画 UI/叠层)。可选预载图与挂载 root DOM。 | `:148`(doc :129-147) |
| `engineUpdate` | **未导出**——主循环为引擎内部 RAF 驱动,外部只能经 engineInit 回调挂入;无独立 `engineUpdate` 导出 | — | A0 接管 = host 调 `engineInit` 让引擎掌帧,gameUpdate/gameRender 回调里接桩逻辑(不是手摇 engineUpdate) | grep `engineUpdate` 零命中(仅 `engineObjectsUpdate`) |
| `engineObjectsUpdate` | `engineObjectsUpdate(): void` | `void` | 手动更新所有对象/移除已毁/推进时间(一般由主循环自动调,可在循环外手动调) | `:152` |
| `engineAddPlugin` | `engineAddPlugin(update?: PluginCallback, render?: PluginCallback, glContextLost?: PluginCallback, glContextRestored?: PluginCallback): void` | `void` | 注册引擎级插件的 update/render 钩子(含 GL 上下文丢失/恢复回调)。`PluginCallback = () => any`。 | `:195`(type `:3`) |
| `frame` | `export let frame: number` | number | 当前更新帧序号(算时间用) | `:98` |
| `time` | `export let time: number` | number | **引擎起始至今秒数;受暂停/timeScale/帧率钳制影响**(游戏逻辑时钟) | `:99-102` |
| `timeReal` | `export let timeReal: number` | number | **真实墙钟秒数;不受暂停/timeScale/帧率钳制**(probe 启动耗时埋点应取此或独立墙钟) | `:103-106` |
| `timeScale` | `export let timeScale: number` | number | 全局时间缩放(hitStop/慢动作) | `:352` |
| `setTimeScale` | `setTimeScale(scale: number): void` | `void` | 设置 timeScale | `:638` |
| `paused` / `getPaused` / `setPaused` | `paused: boolean` / `getPaused(): boolean` / `setPaused(isPaused?: boolean): void` | — | 暂停态(暂停时 time 与对象不更新) | `:111,115,119` |
> **A0 关键事实**:`time` 会被 timeScale/pause/帧率钳制污染(`:103` 注明 timeReal 才是「not affected by pause, timescale, or frame rate clamping」)。∴ 执行版 §5/门0「probe 启动耗时取墙钟、与引擎 time 解耦」**有引擎源码硬支撑**——probe 须用 `timeReal` 或自管 `performance.now()`,**不可用 `time` 作 P75 埋点**。
---
## 2. 数学 / 插值 / 工具(gamefeel easing、确定性随机所需)
| 导出名 | 完整签名 | 返回 | 语义 | 证据 |
|---|---|---|---|---|
| `lerp` | `lerp(valueA: number, valueB: number, percent: number): number` | number | 线性插值 | `:987` |
| `lerpAngle` | `lerpAngle(angleA, angleB, percent: number): number` | number | 带环绕的角度插值 | `:980` |
| `lerpWrap` | `lerpWrap(valueA, valueB, percent, wrapSize?): number` | number | 带环绕的值插值 | `:967` |
| `smoothStep` | `smoothStep(percent: number): number` | number | smoothstep 缓动(对百分比施加) | `:1002` |
| `percent` | `percent(value, valueA, valueB): number` | number | value 在 [A,B] 间的百分比 | `:952` |
| `percentLerp` | `percentLerp(value, percentA, percentB, lerpA, lerpB): number` | number | percent+lerp 快捷组合 | `:997` |
| `oscillate` | `oscillate(frequency?, amplitude?, t?=time, offset?, type?): number` | number | 0~amplitude 振荡波;type 0=正弦/1=三角/2=方波/3=锯齿;**默认 t=`time`** | `:1052`(doc :1044-1051) |
| `clamp` | `clamp(value, min?, max?): number` | number | 钳制 | `:945` |
| `noise1D`/`noise2D` | `noise1D(x): number` / `noise2D(x,y): number` | number [0,1] | 梯度噪声(整数格确定性,非整数 smoothStep 插值) | `:1104,1110` |
> **easing 缺口**:引擎仅 `lerp`/`smoothStep`/`oscillate`。gamefeel 的 `quadIn/Out`、`cubicIn/Out`、**`elasticIn/Out/InOut`、`backIn/Out/InOut`** 引擎**确无**(grep 无 elastic/back/cubic easing 导出)。只有 linear(≈lerp)/smoothstep 两个有对应。
### 2.1 随机(受控面 random 透传所需)
| 导出名 | 完整签名 | 返回 | 语义 | 证据 |
|---|---|---|---|---|
| `RandomGenerator` (class) | `constructor(seed?: number)`;`float(a?,b?): number`;`int(a,b?): number`;`bool(chance?): boolean`;`sign(): number`;`floatSign(a?,b?): number`;`angle(): number`;`vec2(a?,b?): Vector2`;`randColor(...)`;`mutateColor(...)`;属性 `seed: number` | 见各方法 | **种子化确定性随机**(same seed → 同序)。引擎注释「Engine helper classes: …RandomGenerator」;引擎全局随机实现为 xorshift 类(spike 实证 same seed 同序) | `:1164-1212` |
| `rand` | `rand(valueA?, valueB?): number` | number | 全局(非种子)随机 | `:1118` |
---
## 3. 绘制 API(render.js 切引擎、palette 基元所需)
| 导出名 | 完整签名 | 返回 | 坐标系 | 证据 |
|---|---|---|---|---|
| `drawTile` | `drawTile(pos: Vector2, size?: Vector2, tileInfo?: TileInfo, color?: Color, angle?: number, mirror?: boolean, additiveColor?: Color, useWebGL?: boolean, screenSpace?: boolean, context?: CanvasRenderingContext2D\|OffscreenCanvasRenderingContext2D): void` | `void` | **世界坐标**(screenSpace 可切);可选 context 落 Canvas2D | `:1844` |
| `drawRect` | `drawRect(pos: Vector2, size?: Vector2, color?: Color, angle?: number, useWebGL?: boolean, screenSpace?: boolean, context?): void` | `void` | 世界坐标,中心定位 | `:1854` |
| `drawRectGradient` | `drawRectGradient(pos, size?, colorTop?, colorBottom?, angle?, useWebGL?, screenSpace?, context?): void` | `void` | 世界坐标,顶→底渐变 | `:1865` |
| `drawText` | `drawText(text: string\|number, pos: Vector2, size?: number, color?: Color, lineWidth?: number, lineColor?: Color, textAlign?: CanvasTextAlign, font?: string, fontStyle?: string, maxWidth?: number, angle?: number, context?): void` | `void` | 世界坐标,自动换行;CJK 经默认 font(spike 已验中文) | `:2000` |
| `drawTextScreen` | `drawTextScreen(text, pos, size, color?, ...): void` | `void` | **屏幕坐标**文字 | `:2016` |
| `drawCanvas2D` | `drawCanvas2D(pos: Vector2, size: Vector2, angle?, mirror?, drawFunction?: Canvas2DDrawFunction, screenSpace?, context?): void` | `void` | 自定义 Canvas2D 绘制回调(世界坐标变换内)——**Canvas2D 逃生口** | `:1984` |
| `mainCanvas` / `mainContext` | `export let mainCanvas: HTMLCanvasElement` / `mainContext: CanvasRenderingContext2D` | — | Canvas2D 叠加层与其 2d context | `:1763,1767` |
| `glCanvas` | `export let glCanvas: HTMLCanvasElement` | — | WebGL2 批渲 canvas | `:2126` |
### 3.1 颜色(palette-post RGB↔HSL 基元所需)
| 导出名 | 完整签名 | 返回 | 语义 | 证据 |
|---|---|---|---|---|
| `Color` (class) | `constructor(r?,g?,b?,a?)`;属性 `r/g/b/a: number`;`setHSLA(h?,s?,l?,a?): Color`;`HSLA(): Array<number>`;`setHex(hex): Color`;`toString(useAlpha?): string`;`lerp(c,percent): Color`;`add/subtract/multiply/divide/scale/clamp/mutate` | 见方法 | **单色 RGBA 对象,自带 HSL 互转**(setHSLA/HSLA)。**注意:是逐 Color 实例的转换,非像素缓冲批处理** | `:1365-1453` |
> **palette 基元裁定要点**:`Color.setHSLA`/`HSLA()` 提供 RGB↔HSL(单色,0~1 浮点 r/g/b)。但 palette-post 的 `applyPaletteMapToBuffer`/`ditherImageData` 是对 **`Uint8ClampedArray` RGBA 像素缓冲 / `ImageData`** 的 CPU 批处理 + 索引色精确匹配表 + Bayer 抖动——引擎**无对应缓冲级操作**,索引色表确无。
---
## 4. 粒子(particles-juice 所需)
| 导出名 | 完整签名 | 返回 | 坐标系 | 证据 |
|---|---|---|---|---|
| `ParticleEmitter` (class) | `constructor(pos: Vector2, angle?, emitSize?: number\|Vector2, emitTime?, emitRate?, emitConeAngle?, tileInfo?, colorStartA?, colorStartB?, colorEndA?, colorEndB?, particleTime?, sizeStart?, sizeEnd?, speed?, angleSpeed?, damping?, angleDamping?, gravityScale?, particleConeAngle?, fadeRate?, randomness?, collideTiles?, additive?, randomColorLinear?, renderOrder?, localSpace?)`;方法 `emitParticle(): Particle`、`isActive(): boolean`;**`extends EngineObject`** | 实例 | **世界坐标**:`pos` 世界位置,`emitSize`/`speed`(世界单位/帧@60fps) 全世界空间;发射器本身是引擎对象(自动入对象列表、随主循环更新渲染) | `:3232-3324`(constructor :3262) |
| `Particle` (class) | `constructor(emitter, pos, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, velocity?, angleVelocity?)` | 实例 | 世界/局部空间;通常由 emitter 自动创建 | `:3329-3344` |
| `particleEmitRateScale` | `export let particleEmitRateScale: number` | number | 全局发射率缩放(低画质模式,0 禁用所有 emitter) | `:479` |
> **坐标系冲突**:emitter 构造首参 `pos: Vector2` = **世界坐标**,且 `extends EngineObject`(受引擎主循环驱动)。particles-juice 受控面用朴素 `{x,y}` 像素坐标 + 自有 `EmitterConfig`/`evalCurve`/preset 体系——存在世界↔像素阻抗 + 「引擎对象生命周期 vs 插件自管发射」体系差。**A0 接管后才有引擎对象更新驱动**(否则 emitter 不 update = 空转)。
### 4.1 屏震 / 相机(particles-juice 屏震所需)
| 导出名 | 完整签名 | 返回 | 证据 |
|---|---|---|---|
| `cameraPos` | `export let cameraPos: Vector2` | Vector2 | `:334` |
| `setCameraPos` | `setCameraPos(pos: Vector2): void` | `void` | `:626` |
| `cameraScale` / `setCameraScale` | `cameraScale: number` / `setCameraScale(scale): void` | — | `:344,634` |
> **跨层警告**:屏震走引擎 `cameraPos` 需**渲染基座本身用引擎相机**(即 render.js 全切引擎 draw + 引擎相机)。若 render 仍是裸 Canvas2D,则 cameraPos 不被消费——屏震只能保留受控面像素偏移补层。
---
## 5. 音频(audio-music 所需)
| 导出名 | 完整签名 | 返回 | 语义 / 坐标系 | 证据 |
|---|---|---|---|---|
| `zzfxG` | `zzfxG(volume?, randomness?, frequency?, attack?, sustain?, release?, shape?, shapeCurve?, slide?, deltaSlide?, pitchJump?, pitchJumpTime?, repeatTime?, noise?, modulation?, bitCrush?, delay?, sustainVolume?, decay?, tremolo?, filter?): any[]` | **`Array`(采样样本数据)** | **纯合成核**:返回 PCM 样本,不播放。与插件 zzfxG 同口径 | `:2724` |
| `zzfxM` | `zzfxM(instruments: any[], patterns: any[], sequence: any[], BPM?: number): any[]` | **`Array`(左右声道样本)** | 纯音乐样本生成,不播放 | `:3730` |
| `zzfx` | `zzfx(...zzfxSound: any[]): AudioBufferSourceNode` | **`AudioBufferSourceNode`(已在播放)** | 合成 + 立即播放(合成核与播放耦合在一起) | `:2699` |
| `Sound` (class) | `constructor(asset?: string\|Array, randomness?, range?, taper?, onloadCallback?)`;`play(pos?: Vector2, volume?, pitch?, randomnessScale?, loop?, paused?): SoundInstance`;`playMusic(volume?, loop?, paused?): SoundInstance`;`playNote(semitoneOffset?, pos?, volume?): SoundInstance`;属性 `sampleChannels: any[][]`、`range`、`taper` | 见方法 | **`play` 是世界坐标定位音**:`pos: Vector2` 世界位置 + `range`/`taper` 距离衰减。`new Sound([.5,.5])` 可建 zzfx 音。 | `:2514-2578`(play :2551) |
| `SoundInstance` (class) | `constructor(sound, volume?, rate?, pan?, loop?, paused?)`;setVolume/pause/resume/stop(doc 示例) | 实例 | 单次播放实例控制(pause/resume/pan 立体声) | `:2595` |
| `ZzFXMusic` (class) | `constructor(zzfxMusic: [any[],any[],any[],number])`;`playMusic(volume?, loop?): SoundInstance`;**`extends Sound`** | 实例 | zzfx 音乐对象(**不存在独立 `Music` 类**,实为 `ZzFXMusic extends Sound`) | `:3710-3722` |
| `audioContext` | `export let audioContext: AudioContext` | AudioContext | 全局 Web Audio 上下文 | `:2482` |
> **音频裁定要点(§1.5 例已点名)**:①`zzfxG`/`zzfxM` = **纯合成核(返回样本)→ 可包装**。②`Sound.play(pos, ...)` = **世界坐标定位 + 相机距离衰减**;受控面无相机体系、用 PCM `playChannels` 直推声道——**空间语义不兼容 → 播放/混音层补层**。③`Music` 类**不存在**,须用 `ZzFXMusic`(extends Sound)或 `Sound.playMusic()`。
---
## 6. 碰撞 / 物理 / 瓦片(collision、physics-lite 所需)
### 6.1 引擎几何查询函数(与 collision 插件 MTV/RayHit 契约比对)
| 导出名 | 完整签名 | 返回 | 语义 / 坐标系 | 证据 |
|---|---|---|---|---|
| `isOverlapping` | `isOverlapping(posA: Vector2, sizeA: Vector2, posB: Vector2, sizeB?: Vector2): boolean` | **`boolean`** | AABB 重叠真假判;`pos`=中心,`size`=**全宽全高**;**无穿透深度/无 MTV 法线** | `:1021`(doc :1013-1020) |
| `isIntersecting` | `isIntersecting(start: Vector2, end: Vector2, pos: Vector2, size: Vector2): boolean` | **`boolean`** | **线段** start→end vs AABB 真假判;**无 t、无命中点、无法线** | `:1029`(doc :1022-1028) |
| `lineTest` | `lineTest(posStart: Vector2, posEnd: Vector2, testFunction: LineTestFunction, normal?: Vector2): Vector2\|undefined` | **`Vector2\|undefined`(命中位置)** | 沿线段逐点测试回调,返回首个碰撞**位置**;法线经可选 out 参数 `normal` 写回;**无标量 t、无结构化 RayHit** | `:1043`(doc :1035-1042) |
### 6.2 引擎物理体系(EngineObject,与 physics-lite 解耦运动学比对)
| 导出名 | 关键签名 / 属性 | 语义 | 证据 |
|---|---|---|---|
| `EngineObject` (class) | 属性 `pos/size/velocity: Vector2`、`mass/damping/angleDamping/restitution/friction/gravityScale/angleVelocity: number`、`clampSpeed: boolean`、`groundObject`、`collideTiles/collideSolidObjects/isSolid/collideRaycast: boolean`;方法 `updatePhysics(): void`、`updateTransforms(): void`、`applyForce(force: Vector2)`、`applyAcceleration(acceleration: Vector2)`、`getSpeed(): number`、`isOverlapping(pos, size?): boolean`、`isOverlappingObject(object): boolean`、`setCollision(...)`、`localToWorld/worldToLocal(pos: Vector2)` | **有状态 OOP 刚体**:对象入引擎对象列表,`updatePhysics()` 每帧由引擎自动调(60fps 帧步),velocity 为 `Vector2`,碰撞解析内建。坐标=世界 Vector2。 | `:2761-2920`(updatePhysics :2832,velocity :2804,clampSpeed :2812,applyForce :2886) |
| `objectMaxSpeed` | `export let objectMaxSpeed: number` | **全局**速度上限(防快速对象穿透) | `:469` |
| `gravity` | `export let gravity: Vector2` | **全局** Vector2 重力(spike 实证:`setGravity(标量)` 崩,须 Vector2) | `:470-474` |
| `objectDefaultFriction`/`objectDefaultRestitution` | `export let …: number` | 全局默认摩擦/弹性 | `:459,464` |
### 6.3 瓦片碰撞(tileCollision*)
| 导出名 | 完整签名 | 返回 | 证据 |
|---|---|---|---|
| `tileCollisionTest` | `tileCollisionTest(pos: Vector2, size?: Vector2, callbackObject?: EngineObject\|TileCollisionCallback, solidOnly?: boolean): TileCollisionLayer` | `TileCollisionLayer` | `:2950` |
| `tileCollisionRaycast` | `tileCollisionRaycast(posStart: Vector2, posEnd: Vector2, callbackObject?: EngineObject\|TileCollisionCallback, normal?: Vector2, solidOnly?: boolean): Vector2\|undefined` | `Vector2\|undefined`(位置,法线经 out 参) | `:2966` |
> **collision/physics 裁定要点**:①引擎 `isOverlapping`→bool、`isIntersecting`→bool、`lineTest`/`tileCollisionRaycast`→`Vector2\|undefined`(位置,无标量 t)——**均满足不了插件 `Manifold{hit,depth,nx,ny}`(MTV) 与 `RayHit{hit,t,px,py,nx,ny}` 契约**。②`SpatialHash`/圆×圆/圆×AABB/SAT 多边形/圆射线——引擎**确无**对应导出(grep 无 `SpatialHash`/`satVsPolygon`/`circleVsCircle`)。③physics-lite 是**无状态 `dtSeconds` 秒级纯函数**(`{px,py,vx,vy}` POJO),与引擎 `EngineObject` 有状态帧步刚体(`Vector2` velocity、全局 gravity/objectMaxSpeed)**体系级阻抗**。
---
## 7. 输入(受控面 input 所需)
| 导出名 | 完整签名 | 返回 | 语义 | 证据 |
|---|---|---|---|---|
| `keyIsDown` | `keyIsDown(key: string\|number, device?: number): boolean` | boolean | **轮询态**:当前键是否按下 | `:2265` |
| `keyWasPressed` | `keyWasPressed(key: string\|number, device?: number): boolean` | boolean | 本帧是否刚按下(轮询态) | `:2271` |
| `mouseIsDown` | `mouseIsDown(button: number): boolean` | boolean | 鼠标键当前按下 | `:2302` |
| `mouseWasPressed` | `mouseWasPressed(button: number): boolean` | boolean | 鼠标键本帧刚按下 | `:2308` |
| `mousePos` | `export let mousePos: Vector2` | Vector2 | 鼠标世界坐标 | `:2330` |
| `gamepadIsDown` | `gamepadIsDown(button: number, gamepad?: number): boolean` | boolean | 手柄键轮询态 | `:2403` |
> **input 裁定要点**:引擎输入是**每帧轮询态布尔**(isDown/wasPressed),无事件订阅。受控面 input 做「对引擎轮询态的归一化包装」(底层读引擎态),订阅形状留给消费者层——**归一化包装**成立。
---
## 8. 后处理(palette-post 所需)+ 两存疑项消解
### 8.1 存疑项 #1:postProcess 能否注入自定义 GLSL shader? —— ✅ **可注入(确证)**
| 导出名 | 完整签名 | 证据 |
|---|---|---|
| `PostProcessPlugin` (class) | `constructor(shaderCode: string, includeMainCanvas?: boolean, feedbackTexture?: boolean)`;属性 `shader: WebGLProgram`、`texture: WebGLTexture`、`vao: WebGLVertexArrayObject` | `:3578-3593` |
| `postProcess` | `export let postProcess: PostProcessPlugin`(全局实例) | `:3573` |
- `.d.ts:3563-3567` 命名空间注释明文:**"Supports shadertoy style post processing shaders"**、`new PostProcessPlugin()` setup post processing、可让其它 canvas 过一道最终 shader。
- 构造首参 = **原始 GLSL `shaderCode: string`**,编译成 `WebGLProgram`。**∴ 自定义 GLSL fragment shader 可注入**。
- **但对 palette-post 的影响**:postProcess 钩子接 GLSL(GPU 片元着色器),而 palette-post 算法是 **CPU 侧 `Uint8ClampedArray` 像素缓冲 + 索引色精确匹配表(`applyPaletteMapToBuffer`) + Bayer 有序抖动(`ditherImageData`)**(见 §3.1)。引擎能注入 GLSL ≠ palette-post 现算法能零改写复用——**把 CPU 索引色/抖动逻辑迁到 GLSL = 整体重写**(与 P1 核名是两回事,R3 预判成立)。结论:palette-post **postProcess 走「整体缺件补层」**(引擎提供 GLSL 注入位,但平台调色逻辑须自实现,可选未来重写为 GLSL)。
### 8.2 存疑项 #2:medal 系统是否存在? —— ✅ **存在(确证)**
| 导出名 | 完整签名 | 证据 |
|---|---|---|
| `Medal` (class) | `constructor(id: number, name: string, description?: string, icon?: string, src?: string)`;属性 `id/name/description/icon: …`、`unlocked: boolean`、`image`;方法 `unlock(): void`、`render(hidePercent?): void`、`renderIcon(pos: Vector2, size: number): void` | `:3457-3489` |
| `NewgroundsMedal` (class) | `extends Medal`(Newgrounds 远程成就) | `:3561` |
| `newgrounds` / `NewgroundsPlugin` | 全局 + 类(远程榜/成就 API) | `:3502,3507` |
- 引擎特性列(`.d.ts:60`)明文「Medal/achievement system with local storage」——**本地存储支撑的成就/勋章系统存在**。Phase A+ 若需成就可直接用(非本波范围,仅消存疑)。
---
## 附:覆盖能力清单核对(执行版 P1 点名项,逐项已核)
`ParticleEmitter`✓ `Sound`✓ `Music`→**实为 ZzFXMusic/Sound.playMusic(无独立 Music 类)** `zzfx`✓ `zzfxM`✓ `zzfxG`✓ `EngineObject(updatePhysics✓/objectMaxSpeed✓/collideSolidObjects✓)` `isOverlapping`✓ `isIntersecting`✓ `tileCollisionTest`✓ `RandomGenerator`✓ `time`✓ `timeScale`✓ `frame`✓ `engineInit`✓ `engineUpdate`→**未导出(内部 RAF,经 engineInit 回调接管)** `engineAddPlugin`✓ `cameraPos`✓ `lerp`✓ `smoothStep`✓ `percent`✓ `oscillate`✓ `drawTile`✓ `drawText`✓ `drawRect`✓ `Color`✓ + postProcess(GLSL 可注入)✓ medal(存在)✓。
> 两处与执行版命名预期的偏差(**已核到、非脑补**):①无独立 `Music` 类,引擎音乐 = `ZzFXMusic extends Sound` 或 `Sound.playMusic()`;②无 `engineUpdate` 导出,主循环为引擎内部 RAF,外部经 `engineInit` 五回调接管(A0 据此实现,不是手摇 engineUpdate)。