/** * host-dev/engine-caps.js — 引擎能力背书工厂(EngineCapabilities 三面:math/particles/audio.synth) * owner:Runner v2 P1(提升自 host.js:197-318 的 makeEngineCaps 闭包)| 消费方:host-dev/host.js + games/wanglanmei-ref/src/main.js(real 通道) * * ════════════════════════════════════════════════════════════════════════════ * 【为什么单列本模块(P1 提升·搬运非新造)】 * host-dev 与 wanglanmei-ref real 通道都要把「插件能力→引擎」薄包装层接进受控面 EngineCapabilities。 * 原实现是 host.js bootHostDev 内的闭包 makeEngineCaps(host.js:197-318,已包装 particles/audio.synth/math 三面)。 * P1 把它提升为「吃 {LJS, engineMath, engineMode, recHook} 的纯工厂函数」,host-dev / ref 共用同一份—— * 这是「模块化已有件」(搬运,语义零变),非从零造。引擎 import 仍只活在调用方(host.js / entry.js / ref entry.js), * 本模块不 import 引擎(引擎名字空间经入参 LJS 注入),故 node 可 import 它(规避 F7:顶层 import 引擎在 node 抛 window)。 * * 【逐行保语义(回归基线=门0 8/8 + real 门 6/6,绝不可破)】 * - 首行 engineMode!=='real' → return null:保 null-engine 回归短路(stub/node 不背书引擎,host.js:198 原语义)。 * - recHook 默认 no-op:原闭包内 `window.__engineCalls.push` 由调用方经 recHook 注入(取证路径才注,渠道产物不注 → 探针无副作用)。 * - mathRaw = engineMath(调用方传入 buildEngineMath(LJS) 的结果,host.js:204 同口径)。 * - particles/audio.synth 三面包装体逐字搬自 host.js:214-316,引擎引用经入参 LJS.*。 * * 【引擎↔插件 边界(创始人 2026-06-13 拍)】引擎有 ParticleEmitter/zzfxG/zzfxM → 此处薄包装;插件侧删自研 sim/vendored 合成核,一职一路。 * ════════════════════════════════════════════════════════════════════════════ */ 'use strict'; /** * 【P1 提升】构造引擎能力背书工厂(EngineCapabilities 三面:math 含 easing 门面 / particles / audio.synth)。 * - 仅 real 通道返真能力面;stub/node 通道短路返 null(保 null-engine 回归战场 + 规避 F7 引擎 import 在 node 抛错)。 * real/stub 都经此工厂(buildBundle/createHostDevContext 注入),若无条件背书引擎,stub 通道也会拿到引擎能力 → 破 null 回归; * 故首行按 engineMode 短路:stub→返 null(等价「未注入」,触发 plugin.js getEngineShared 的 null 分支:告警一次 + getEngine() 返 null)。 * - 引擎 import 唯一活点=调用方(host.js / entry.js LJS@顶部 import);插件零直接 import,一律经 ctx.getEngine().*(Q4 铁律)。 * - math/easing=纯数学族特例(政策 §1.3):门面逐字节/ULP 等价引擎 LJS.lerp/smoothStep/Ease;gamefeel 侧保内置实现作 null-engine fallback(非有意替代)。 * easing 门面**只暴 11 条等价曲线**——backInOut/elasticInOut 不入面(引擎 IN_OUT 拼接是另一条曲线,差 6.6%/17%;gamefeel 对其保内置=补层)。 * * @param {Object} opts * @param {any} opts.LJS 引擎名字空间(含 ParticleEmitter/zzfxG/zzfxM/screenToWorld/vec2/rgb/cameraScale 等)。 * @param {import('../src/core/api.d.ts').EngineMath} opts.engineMath 已构造的引擎数学门面(buildEngineMath(LJS) 的结果,调用方传入)。 * @param {string} opts.engineMode 通道模式('real' 才背书引擎;其余短路返 null)。 * @param {((id:string)=>void)} [opts.recHook] 真接线门②取证钩子:每个引擎能力叶方法被调时记一条 call-ID。 * **默认 no-op**(不传或传 undefined → 探针无副作用、不挂 window.__engineCalls,渠道产物物理保证)。 * @returns {import('../src/core/api.d.ts').EngineCapabilities|null} 引擎能力三面;非 real 返 null。 */ export function createEngineCaps(opts) { const LJS = opts.LJS; const engineMath = opts.engineMath; const engineMode = opts.engineMode; // recHook 默认 no-op:取证路径由调用方注入(id => window.__engineCalls.push(id));渠道产物不注 → 探针零副作用。 const rec = opts.recHook || (() => { /* 默认 no-op:探针绝不入渠道产物 */ }); if (engineMode !== 'real') return null; // stub/node:不背书引擎(保 null-engine 回归;F7:node 本就 import 不了引擎) // math 门面(buildEngineMath 真活包装引擎,调用方传入)外再裹一层 rec:lerp/smoothStep 直裹,easing 子面逐曲线裹(保全部键与口径不变)。 const mathRaw = engineMath; const math = { lerp: (a, b, p) => { rec('math.lerp'); return mathRaw.lerp(a, b, p); }, smoothStep: (p) => { rec('math.smoothStep'); return mathRaw.smoothStep(p); }, easing: Object.fromEntries(Object.keys(mathRaw.easing).map((k) => [k, (t) => { rec('math.easing.' + k); return mathRaw.easing[k](t); }])), }; return { // ── math(含 easing 门面;外层 rec 记调用,内层 mathRaw 真包装引擎 lerp/smoothStep/Ease)── math, // ── particles ──薄包装引擎 ParticleEmitter:EngineEmitterSpec(像素口径)→ 引擎世界口径,返封装句柄。 particles: { /** * 受控面 spawnEmitter:EngineEmitterSpec(像素口径)→ 引擎 ParticleEmitter(世界口径),返封装句柄(禁泄漏引擎对象)。 * 边界模型(创始人 2026-06-13 拍):引擎有 ParticleEmitter → 此处薄包装;插件侧删自研 sim 积分循环,一职一路。 * 引擎自渲:new ParticleEmitter 经 EngineObject 构造自入 engineObjects(esm.js:3580),引擎主循环每帧自调 * update(发射+积分,esm.js:8348)+ render(drawTile 落 mainContext,esm.js:8654/8658)→ host 无需手动 add/render。 * @param {import('../src/core/api.d.ts').EngineEmitterSpec} spec 归一化发射参数(像素/中性形态) * @returns {import('../src/core/api.d.ts').EngineEmitterHandle} 封装句柄(仅 isActive/stop,不见引擎对象) */ spawnEmitter(spec) { rec('particles.spawnEmitter'); // 真接线门②:记一次「插件经 ctx.getEngine().particles 真调引擎粒子构造」 // 1) 像素↔世界阻抗换算(cameraScale 默认 32,esm.js:2785;host 未 setCameraScale 故用引擎 live binding 默认值,I3/OQ-2)。 const worldPos = LJS.screenToWorld(LJS.vec2(spec.pos.x, spec.pos.y)); // 像素→世界(esm.js:5067/screenToWorld) const SCALE = LJS.cameraScale; // 世界单位/像素(用 live binding,随引擎相机变) // 2) burst/continuous → emitTime/emitRate(I1:引擎无「spawn 恰好 N」原语,burst 用「1 逻辑帧窗 × count×60 速率」近似 ≈N 颗)。 const emitTime = spec.emitTime != null ? spec.emitTime : (spec.count != null ? 1 / 60 : 0); // burst=1帧窗(满即自停) / continuous=0(forever) const emitRate = spec.emitRate != null ? spec.emitRate : (spec.count != null ? spec.count * 60 : 100); // burst: count×60 颗/秒(该窗内恰发 ≈count 颗) // 3) speed:像素/秒 → 世界/帧@60(引擎 speed 单位来源 esm.js:8253,无再乘 dt)。 const engSpeed = (spec.speed != null ? spec.speed : 0) / SCALE / 60; // 4) damping:每帧速度乘子(spec 已由插件把 drag/秒折算为每帧乘子,这里直透;缺省 1=无阻)。 const engDamping = spec.damping != null ? spec.damping : 1; // 5) 颜色:归一化 {r,g,b,a} → 引擎 Color(esm.js:1547 rgb)。缺省不透明白→透明,保 real 像素门「有色像素命中>0」(I2)。 const cs = spec.colorStart || { r: 1, g: 1, b: 1, a: 1 }; const ce = spec.colorEnd || { r: 1, g: 1, b: 1, a: 0 }; const colorStart = LJS.rgb(cs.r, cs.g, cs.b, cs.a); const colorEnd = LJS.rgb(ce.r, ce.g, ce.b, ce.a); // 6) 构造引擎发射器(27 参,按 d.ts:3232 构造签名逐位映射)。 const em = new LJS.ParticleEmitter( worldPos, // pos(世界) spec.angle != null ? spec.angle : 0, // angle (spec.emitSize != null ? spec.emitSize : 0) / SCALE, // emitSize 像素→世界 emitTime, // emitTime(0=forever) emitRate, // emitRate spec.coneAngle != null ? spec.coneAngle : Math.PI, // emitConeAngle(半锥角) undefined, // tileInfo: untextured(2D fillRect 路) colorStart, colorStart, colorEnd, colorEnd, // colorStartA/B + colorEndA/B(A=B → 固定色不随机) spec.particleTime != null ? spec.particleTime : 0.5, // particleTime (spec.sizeStart != null ? spec.sizeStart : 6) / SCALE, // sizeStart 像素→世界 (spec.sizeEnd != null ? spec.sizeEnd : 0) / SCALE, // sizeEnd 像素→世界 engSpeed, // speed(世界/帧@60) 0, // angleSpeed: 0(无玩法语义,不旋转) engDamping, // damping 1, // angleDamping: 1(无阻) spec.gravityScale != null ? spec.gravityScale : 0, // gravityScale(host 未 setGravity,全局 gravity=0 故暂不生效,OQ-4 follow-up) Math.PI, // particleConeAngle(粒子自转锥,留默认) 0.1, // fadeRate(默认) 0, // randomness: 0(spec 已显式给参,关引擎默认 .2 额外抖动求确定) false, // collideTiles: false spec.additive != null ? spec.additive : false, // additive true // randomColorLinear(A=B 时无影响) ); // 7) 封装句柄(禁泄漏引擎对象;isActive/stop 转发,stop 幂等销毁)。 return { isActive() { return em.isActive(); }, // esm.js:8482 stop() { if (!em.destroyed) em.destroy(true); }, // 幂等(已 destroyed 直返,不二次销毁) }; }, }, // ── audio.synth ──薄包装引擎 zzfxG/zzfxM:参数包/曲谱 → PCM 样本(不播放)。 // 边界模型(创始人 2026-06-13 拍):引擎有 zzfxG/zzfxM → 此处薄包装;插件侧删 vendored 合成核,一职一路。 // 引擎 zzfxG **不烤主音量 ×0.3**(样本=s*volume,esm.js:7391;×0.3 在引擎 gain 层 soundVolume=.3,esm.js:3121/6777) // → 返回的样本是「未乘 0.3 的 PCM」,与 api.d.ts EngineAudioSynth 口径一致;×0.3 由插件播放层补(audio-music/impl.js playChannels)。 // 合成异常/失败返 null(插件侧 → 发声 no-op,不回退 vendored)。 audio: { synth: { /** * ZzFX 单音效合成核:参数包 → PCM 单声道样本数组(未乘 0.3,不播放)。薄包装引擎 LJS.zzfxG。 * @param {number[]} params ZzFX 参数包 * @returns {number[]|null} PCM 单声道样本;合成异常返 null */ synthSfx: (params) => { rec('audio.synth.synthSfx'); // 真接线门②:记一次「插件经 ctx.getEngine().audio.synth 真调引擎 zzfxG」 try { // 引擎 zzfxG 收 positional 参数(与 vendored 同签名,esm.js:7326)→ 展开参数包。 return LJS.zzfxG(...params); } catch (e) { // 合成异常降级为 null(不连坐游戏,记 stderr);插件侧据 null 走发声 no-op。 console.error('[host][engine-audio] zzfxG 合成异常,返 null:' + (e && e.message ? e.message : e)); return null; } }, /** * ZzFXM 曲谱合成核:乐器/模式/序列/BPM → [L,R] 双声道样本(未乘 0.3,不播放)。薄包装引擎 LJS.zzfxM。 * @param {number[][]} instruments 乐器表 * @param {number[][][]} patterns 模式表 * @param {number[]} sequence 序列 * @param {number} [bpm] 节拍(缺省引擎默 125) * @returns {number[][]|null} [L,R] 双声道样本;合成异常返 null */ synthSong: (instruments, patterns, sequence, bpm) => { rec('audio.synth.synthSong'); // 真接线门②:记一次「插件经 ctx.getEngine().audio.synth 真调引擎 zzfxM」 try { // 引擎 zzfxM 签名与 vendored 逐字一致(esm.js:10837 `zzfxM(instruments, patterns, sequence, BPM=125)`)。 return LJS.zzfxM(instruments, patterns, sequence, bpm); } catch (e) { console.error('[host][engine-audio] zzfxM 合成异常,返 null:' + (e && e.message ? e.message : e)); return null; } }, }, }, }; }