/** * tier2/engine/engine-caps-phaser.js —— tier2 富游戏引擎受控能力面(Phaser 版 · 薄包装 Phaser 内建) * owner:tier2 引擎线(M1-Phaser-scaffold+host)| 消费方:①boot-phaser-host(注入受控面给游戏 boot);②生成 agent 写的 Phaser 工程(经 ctx.getEngine() 拿能力) * * ════════════════════════════════════════════════════════════════════════════ * 【这份是什么 · 引擎能力面 Phaser 化】 * 左支(Tier0/1·LittleJS)的受控能力面在 game-runtime/src/core/api.d.ts(EngineCapabilities:particles/audio.synth/math), * 由 host-dev/engine-caps.js 背书 LittleJS 内建(ParticleEmitter / zzfx / lerp)。 * 本文件是 tier2 右支(Phaser)的等价能力面——同一组受控能力(粒子 / 音频 / easing / math), * 但底座换成 Phaser 内建薄包装。接口口径对齐 api.d.ts 的 EngineCapabilities,但形状 Phaser 化。 * * 【为什么是受控面而非裸 Phaser】(边界模型「引擎可换」铁律,对齐 api.d.ts §受控面铁律) * 生成 agent 写的游戏不直接调 Phaser 裸 API 做粒子 / 发声 / 缓动,而是经本受控面调。 * 理由:① 引擎可换(Phaser→Pixi 只换本文件,不改游戏);② 粒子 / 发声是「引擎覆盖的有状态能力」, * 走受控面句柄统一管理;③ 几何归一化 {x,y},禁裸 Phaser.Math.Vector2 泄漏进游戏。 * * 【一职一路 · 降级语义】(对齐 api.d.ts §62-68 创始人 2026-06-13 裁) * - 引擎覆盖的有状态能力(particles / audio):无 Phaser scene 时(headless / node import-safe)整面优雅 no-op, * 不留 sim / vendored 降级(有意替代会掩盖真接线缺陷,F 门要查真调引擎)。 * - 纯数学族(math.lerp / smoothStep / easing):保内置纯函数 fallback(公式恒等,非有意替代), * 便于 node 静态校验与 headless 逻辑跑通;real 通道下与 Phaser.Math 口径一致。 * * 【接法】createPhaserEngineCaps({ scene, recHook }) → EngineCapabilities 形状对象。 * scene:Phaser.Scene 实例(host 在 scene create 后注入;粒子 / 音频 / 缓动经它的 add / sound / tweens 调)。 * recHook:引擎能力调用 call-ID 取证钩子(F 门真接线证据;缺省 no-op)。每次真调 Phaser 内建打一个 call-ID。 * ════════════════════════════════════════════════════════════════════════════ */ 'use strict'; /** * 创建 Phaser 受控能力面。host 在 scene create 完成后调本工厂建一份,经 boot.ctx.getEngine() 透传给游戏。 * @param {Object} deps 依赖注入 * @param {any} [deps.scene] Phaser.Scene 实例(headless / 静态校验时为 null → 有状态能力 no-op、数学族走纯函数) * @param {(id: string) => void} [deps.recHook] 引擎能力调用 call-ID 取证钩子(F 门真接线证据;缺省 no-op) * @returns {Object} EngineCapabilities 形状(particles / audio / math),口径对齐 api.d.ts */ export function createPhaserEngineCaps(deps) { // scene 缺省 null(headless / node import-safe):有状态能力优雅 no-op;数学族走内置纯函数 fallback。 const scene = (deps && deps.scene) || null; // call-ID 取证钩子(缺省 no-op):每次真调 Phaser 内建打一个 ID,F 门据 __engineCalls 证「真经受控面调引擎、非自绘伪装」。 const recHook = (deps && deps.recHook) || (() => {}); /* ────────────────────────────────────────────────────────────────────── * ① 粒子受控面(薄包装 Phaser GameObjects.Particles.ParticleEmitter) * 归一化发射器参数(受控面像素 + 中性形态),host 映射到 Phaser 发射器;返回受控句柄(isActive/stop)。 * 无 scene → 优雅 no-op 句柄(不留 sim 降级,对齐左支「null-engine→no-op」)。 * ────────────────────────────────────────────────────────────────────── */ const particles = { /** * 喷一束粒子。 * @param {Object} spec 归一化发射参数({x,y}=受控面像素坐标,count=一次性颗数,lifeMs/speed/colorStart...) * @returns {{ isActive: () => boolean, stop: () => void }} 受控句柄(封装 Phaser 发射器;游戏只见本面) */ emit(spec) { // 无 scene(headless / 静态校验):返回 no-op 句柄,不抛错(保 node import-safe),不留 sim 降级。 if (!scene || !scene.add || typeof scene.add.particles !== 'function') { return { isActive: () => false, stop: () => {} }; } // F 门取证:真调引擎前打 call-ID(证真经受控面调 Phaser,非游戏自绘伪装)。 recHook('engine.particles.emit'); const s = spec || {}; // 受控面像素坐标 {x,y} → Phaser 世界坐标(Phaser 画布像素坐标系,host 不再二次换算,视口已逻辑像素对齐)。 const x = typeof s.x === 'number' ? s.x : 0; const y = typeof s.y === 'number' ? s.y : 0; const count = typeof s.count === 'number' ? s.count : 12; const lifeMs = typeof s.lifeMs === 'number' ? s.lifeMs : 600; const speed = typeof s.speed === 'number' ? s.speed : 120; // Phaser 3.60+ 粒子 API:scene.add.particles(x, y, textureKey, config)。 // 纹理键缺省用宿主预置的 1px 白点 '__tier2_particle'(host 在 boot 时确保已 generateTexture 注册)。 const emitter = scene.add.particles(x, y, (s.texture || '__tier2_particle'), { lifespan: lifeMs, speed: { min: speed * 0.5, max: speed }, scale: { start: typeof s.sizeStart === 'number' ? s.sizeStart : 0.6, end: typeof s.sizeEnd === 'number' ? s.sizeEnd : 0 }, // 一次性 burst:发 count 颗后停(emitting:false + explode)。 emitting: false, }); // burst:一次性喷 count 颗(Phaser explode)。 try { emitter.explode(count, x, y); } catch (e) { /* 部分 Phaser 版本签名差异,吞错保不中断 */ } // 受控句柄:封装 Phaser 发射器,游戏只见 isActive/stop(不见 Phaser 对象)。 return { isActive() { // Phaser 发射器活跃判定(无 alive 粒子且不发射即非活跃)。 try { return !!emitter && typeof emitter.getAliveParticleCount === 'function' && emitter.getAliveParticleCount() > 0; } catch (e) { return false; } }, stop() { // 停止并销毁(幂等)。 recHook('engine.particles.stop'); try { if (emitter && !emitter.destroyed) emitter.destroy(); } catch (e) { /* 幂等吞错 */ } }, }; }, }; /* ────────────────────────────────────────────────────────────────────── * ② 音频受控面(薄包装 Phaser Sound) * 左支是 zzfx 程序化合成(EngineAudioSynth.synthSfx/synthSong);Phaser 走 sound manager 播放预载音频键。 * 富游戏 spike 期音频是次要,先给最小受控播放面(playSfx/playBgm),无 scene → no-op。 * ────────────────────────────────────────────────────────────────────── */ const audio = { /** * 播一个短音效(按预载音频键)。 * @param {string} key 音频键(宿主预载;缺省 / 未载 → 静默 no-op) * @param {Object} [opts] { volume?: number } */ playSfx(key, opts) { if (!scene || !scene.sound || typeof scene.sound.play !== 'function') return; recHook('engine.audio.playSfx'); try { scene.sound.play(key, { volume: (opts && opts.volume) != null ? opts.volume : 0.8 }); } catch (e) { /* 未载音频键 / 解码失败:静默 no-op,不中断游戏 */ } }, /** * 播 / 切背景音乐(循环)。 * @param {string} key 音频键 * @param {Object} [opts] { volume?: number } */ playBgm(key, opts) { if (!scene || !scene.sound || typeof scene.sound.add !== 'function') return; recHook('engine.audio.playBgm'); try { const m = scene.sound.add(key, { loop: true, volume: (opts && opts.volume) != null ? opts.volume : 0.5 }); m.play(); } catch (e) { /* 未载 / 解码失败:静默 no-op */ } }, }; /* ────────────────────────────────────────────────────────────────────── * ③ 数学受控面(纯函数族 · 口径对齐 Phaser.Math,恒可用) * lerp / smoothStep / easing:这是「纯数学族」,无 scene 也保内置纯函数 fallback(公式恒等,非有意替代)。 * real 通道下与 Phaser.Math 口径一致(lerp/SmoothStep/Easing),便于游戏直接复用引擎曲线又可 headless 跑通。 * ────────────────────────────────────────────────────────────────────── */ const math = { /** * 线性插值(口径对齐 Phaser.Math.Linear / LittleJS lerp)。 * @param {number} a 起点 * @param {number} b 终点 * @param {number} t 比例 [0,1] * @returns {number} */ lerp(a, b, t) { // 纯函数恒等:a + (b-a)*t(Phaser.Math.Linear 同口径)。 return a + (b - a) * t; }, /** * smoothstep 缓动(口径对齐 Phaser.Math.SmoothStep / LittleJS smoothStep)。 * @param {number} t 比例 [0,1] * @returns {number} */ smoothStep(t) { // 钳 [0,1] 后经典 3t²-2t³(与 Phaser.Math.SmoothStep(t,0,1) 恒等)。 const x = t < 0 ? 0 : t > 1 ? 1 : t; return x * x * (3 - 2 * x); }, /** * 缓动曲线族(可选 · 对齐 api.d.ts EngineEasing 11 条曲线的子集;纯函数恒可用)。 * 富游戏 UI 动效常用 quadIn/quadOut/cubicOut;与 Phaser.Math.Easing 同口径,便于游戏切引擎曲线时无缝。 */ easing: { quadIn(t) { return t * t; }, quadOut(t) { return t * (2 - t); }, cubicOut(t) { const f = t - 1; return f * f * f + 1; }, }, }; // 返回 EngineCapabilities 形状(particles / audio / math);口径对齐 api.d.ts,但 Phaser 化。 return { particles, audio, math }; }