feat(runner-v2): P3 L0 通用装载壳——L1 生成 worker 的产出靶载体(node 5 门验过)

P3 第一块基石:把 wanglanmei-ref/entry-bundle.js 的「studio-consumable 引擎包入口」泛化为「任意生成游戏」通用壳——L1 worker 只写 generated-factory.js(一个 GameHostFactory),套本壳 esbuild 打 __GameBundle bundle 即得 studio 可消费引擎包,避免 20 个 prompt 各造一遍 7 插件 wiring。

新增(零改既有契约):① src/host/generic-host-config.js=buildGenericHostConfig(标准 7 能力插件集+六锚偏序+390×844 视口+wiring,零wanglanmei专属);② games/_generic/entry-generic.js=通用入口壳(动态 import 引擎 Option C node-safe 与 entry-bundle 同款;调用形 {canvas,seed} 逐字对齐 inject.ts:248;暴露 window.__GameBundle.{GameHostFactory,bootGameHost,default});③ games/_generic/generated-factory.js=占位 stub(L1 出码替换靶)+ P0 latch 终态范本(到 TARGET 置 gameover 驻留不可逆,_forensicsView().state().phase 暴露给 watchGameEnd);④ test/generic-shell.test.mjs=node 侧守门零依赖。

5 门验过(6c6g node,主笔亲跑复核):①esbuild iife __GameBundle.bootGameHost=function ②import-safe(node-load window 零触)③回归 src 161/161+games 57/57 ④B1 raw 203.8KB/gz 69KB ✔ ⑤占位工厂经受控面 init→tap→latch gameover 驻留。dist/ 已 gitignore 不入库。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-14 10:40:35 +00:00
parent dd4d883bbe
commit 3108009500
4 changed files with 500 additions and 0 deletions

View File

@ -0,0 +1,90 @@
/**
* entry-generic.js P3 generic bundle 入口壳模板·esbuild 锁参打成 iifeglobalName=__GameBundle
* ownerP3/W-G1 生成主线 L0 消费方studio host 引擎包分支DB-manifest 内嵌路 wanglanmei-ref/entry-bundle.js 同消费方
*
*
* 本入口是什么 · wanglanmei-ref/entry-bundle.js 的关系
* entry-bundle.js wanglanmei 一款游戏的studio-consumablebundle 入口工厂写死 import game-shop
* entry-generic.js 把它泛化成任意生成游戏的通用入口壳
* · 游戏工厂来源参数化 `import createGame from './generated-factory.js'`L1 生成 worker 写入的产出靶
* · 装配走 buildGenericHostConfig零wanglanmei专属标准能力插件集不再走 buildRefHostConfig
* 这是 L1 生成 worker 套壳靶worker 只需产出 generated-factory.js一个 GameHostFactory
* 套进本壳 esbuild 打成 __GameBundle bundle即得一个 studio 可消费的引擎包避免 20 prompt 各造一遍
* 7 插件 wiring + boot 入口 execution §3.2 套壳分级框 / §7-S1
*
* 契约冻结·逐字对齐
* - iife 顶层全局名固定 **__GameBundle** game-package.schema.json/contract.ts/inject.ts 逐字一致
* studio iframe 注入本 bundle 文本 window.__GameBundle.bootGameHost 启动
* - boot 入口调用形 **{canvas, seed}** 逐字对齐 game-studio/src/host/inject.ts:248
* `bundleNS.bootGameHost({ canvas: canvas, seed: seed })`
* - bundle = 引擎(LittleJS) + 包装层(bootGameHost + generic 装配) + 生成游戏代码(generated-factory)
*
* Q4 · Option C引擎 import 只落本入口且经动态 import 延后求值
* littlejsengine ESM 在模块装载期触 window/AudioContext顶层静态 import 会让 node import 即抛
* 击穿 import-safe修法 entry-bundle 逐字同款引擎 import bootGameHost 内动态 import
* · node import 本入口时不触引擎bootGameHost 未被调 import-safe
* · esbuildbundle+iife splitting把动态 import 的引擎内联进单文件 引擎真入 bundle
* · 浏览器运行期 studio __GameBundle.bootGameHost 时才求值引擎window/AudioContext 已存在
*
*/
'use strict';
// 通用宿主(引擎骨架参数化;本身零引擎 import引擎经 opts.engine 注入)。
import { bootGameHost as bootGameHostCore } from '../../src/host/boot-game-host.js';
// generic 装配配置(标准能力插件集 / 偏序 / 视口 / wiring零wanglanmei专属
import { buildGenericHostConfig } from '../../src/host/generic-host-config.js';
// 【游戏工厂来源·参数化】L1 生成 worker 写入的生成工厂(默认导出 GameHostFactory零引擎 import
// 占位期 = games/_generic/generated-factory.js 的占位 stubL1 出码替换该文件即换游戏,本壳零改。
import createGame from './generated-factory.js';
/**
* 契约导出GameHostFactorystudio host window.__GameBundle.GameHostFactory 取它契约冻结
* = 生成工厂createGameL1 出码替换 generated-factory.js 的默认导出即换它
* @type {import('../../src/core/game-host.d.ts').GameHostFactory}
*/
export const GameHostFactory = createGame;
// 默认导出同为 GameHostFactory便于 import default 取用;与具名导出同一引用)。
export default createGame;
/**
* boot 入口 studio host iframe 内启动本 bundlestudio 仓无引擎故由本 bundle 注入引擎后驱动
* - 动态 import 引擎Option C调用时才求值esbuild 已把引擎内联进本 bundle无外部请求
* - engineMode 强制 'real'studio 引擎包分支一律引擎掌帧
* - 插件/视口/wiring 取自 buildGenericHostConfig标准能力插件集 + 390×844
* - 工厂取 createGame bundle generated-factory 默认导出studio 也可经 opts.factory 覆盖缺省即用它
*
* @param {Object} opts studio host 传入的最小入参调用形 {canvas, seed} 逐字对齐 inject.ts:248
* @param {HTMLCanvasElement} opts.canvas iframe 内目标画布real 通道引擎自建 mainCanvas canvas 作占位/输入兜底
* @param {HTMLElement} [opts.statusEl] 状态行可空
* @param {number} [opts.seed] 主随机种子缺省固定值保确定性studio versionId 派生后传
* @param {any} [opts.engine] 引擎名字空间覆盖缺省=动态 import littlejsengine测试可注入桩引擎
* @param {import('../../src/core/game-host.d.ts').GameHostFactory} [opts.factory] 工厂覆盖缺省= bundle createGame
* @returns {Promise<object>} bootGameHost host 句柄frameCount/stepFrames/tap/do/state
*/
export async function bootGameHost(opts) {
const o = opts || {};
// 动态 import 引擎求值时机延后到本函数被调用浏览器运行期。esbuild 内联,无外部请求。
// opts.engine 覆盖优先(测试可注桩引擎,免拉真引擎);缺省动态 import 真引擎。
const engine = o.engine || (await import('littlejsengine'));
// seed 缺省固定值(与 ref/inject 同口径:非数字回退 0x1234abcd规整无符号 32 位。
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
// generic 装配(标准能力插件集 / 偏序 / 视口 / wiring
const cfg = buildGenericHostConfig({ seed });
// 调通用宿主real 通道引擎掌帧canvas/statusEl 由 studio 传;其余取 generic 装配。
return bootGameHostCore({
canvas: o.canvas,
statusEl: o.statusEl || null,
engine,
seed,
mode: 'play', // studio 真玩=play
engineMode: 'real', // studio 引擎包分支一律引擎掌帧
viewport: cfg.viewport, // {w:390,h:844}P3 钉死)
factory: o.factory || cfg.factory || createGame, // 缺省用本 bundle 内置生成工厂
plugins: cfg.plugins,
registerOrder: cfg.registerOrder,
buildFactoryOpts: cfg.buildFactoryOpts,
// studio 渠道产物:不注 recHook引擎 call-ID 取证是 ref harness 专属;渠道产物无副作用)。
// 无 onReadystudio 不消费 ref 取证全局;就绪信号经 SDK lifecycle game_loaded/game_endstudio 轨接)。
});
}

View File

@ -0,0 +1,178 @@
/**
* generated-factory.js 占位 stub生成工厂占位L1 出码替换本文件
* ownerP3/W-G1 生成主线 L0 消费方entry-generic.jsimport 本默认导出作 GameHostFactory+ L1 生成 worker出码替换靶
*
*
* 占位说明 · 务必先读
* 本文件是 L0 通用装载壳的占位工厂用途仅为让 entry-generic.js 立即可 esbuild + 可启动验证
* **L1 生成 worker 出码时整文件替换本文件**写入真实生成游戏的 GameHostFactory默认导出
* 本占位同时是 L1 最小范本演示三条硬约束
* 纯工厂零引擎 import引擎能力一律经 boot.ctx.getEngine()本占位极简未用引擎能力但保持零 import
* 实现并默认导出一个 GameHostFactory(opts?) => GameInstance对齐 game-host.d.ts:59
* **P0 latch 终态** N 分进入 'gameover' **驻留**不自动重开 _forensicsView().state().phase 暴露
*
* 为何必须演示 latch 终态P3 硬约束
* studio host watchGameEnd 轮询 host.state().phaseinject.ts:217 'gameover' 代发 game_end{completed:true}
* 门② 闭环锚轮询是间隔采样若终态是瞬时 phase某帧置 gameover 下帧又变轮询会漏报致闭环静默断裂
* memory runner-v2-p1-loading-contract P3 硬约束故终态必须**驻留**一旦 gameoverstate().phase 恒为 'gameover'
*
* 极简玩法点击画布计分 TARGET_SCORE 分进入 gameover 终态驻留无引擎能力依赖可在 stub/real/node 任一通道跑
*
*/
'use strict';
/** 目标分(点击累计到此分进入终态)。占位用极小值,便于启动验证快速到达终态。 */
const TARGET_SCORE = 3;
/**
* 装载契约 GameHostFactory建占位游戏实例纯工厂零引擎 import
* @param {Object} [opts] 工厂选项host wiring + additive 扩展位本占位不用 runtime演示纯净工厂
* @returns {import('../../src/core/game-host.d.ts').GameInstance}
*/
export default function createGenericPlaceholderGame(opts) {
// 本占位不复用任何能力插件(演示「纯净工厂仅吃 boot 脊柱」路径opts.runtime 留给真实游戏用。
void opts;
/* ── 闭包态init 内由 boot 填充render 读绘制面)── */
/** @type {import('../../src/core/game-host.d.ts').GameHostBootContext|null} */
let boot = null;
/** @type {CanvasRenderingContext2D|null} 绘制面(=boot.mainContextrender 的 g 同此面)。 */
let mainCtx = null;
let seed = 0;
// 受控输入订阅句柄destroy 时取消,幂等)。
let subTap = null;
// 游戏态:分数 + 阶段('playing' → 'gameover' latch 驻留)。
let score = 0;
/** @type {'booting'|'playing'|'gameover'} */
let phase = 'booting';
let elapsedMs = 0; // 逻辑时长update 累计;仅作旁证,不驱动终态)。
/**
* 计一次点击分 playing 阶段加分 TARGET_SCORE 进入 gameover 并驻留latch不可逆
* 这是 P0 latch 终态的核心一旦置 gameover本函数后续点击不再改 phase终态驻留
*/
function scoreOne() {
if (phase !== 'playing') return; // 终态/未就绪不计分gameover 后点击无效=驻留)
score += 1;
if (score >= TARGET_SCORE) {
phase = 'gameover'; // latch置入终态后恒为 gameoverwatchGameEnd 轮询任一时刻读到的都是 gameover
}
}
/*
* 装载契约 GameInstance 五件init/update/render/onInput?/destroy
* */
/**
* init(boot)建游戏对象订阅受控输入保存绘制面boot=装载契约脊柱 {ctx, mainContext, canvas, seed}
* @param {import('../../src/core/game-host.d.ts').GameHostBootContext} b
*/
function init(b) {
boot = b;
mainCtx = b.mainContext; // render 的绘制面real=引擎 mainContext / stub=可见画布 ctx
seed = b.seed >>> 0;
score = 0;
elapsedMs = 0;
phase = 'playing'; // init 后即可玩
// 订阅受控输入:每个 pointerdown 计一次分(经 boot.ctx.getInput(),不触裸 DOM
// ctx.getInput().on 返回订阅句柄(含 canceldestroy 时取消(与 ref 同口径)。
try {
const input = boot.ctx.getInput();
if (input && typeof input.on === 'function') {
subTap = input.on('pointerdown', () => scoreOne());
}
} catch (_) {
// 输入面缺失极简通道不致命onInput 备选入口仍可计分(见下)。
subTap = null;
}
}
/**
* update(dt)逻辑步dt 引擎恒定步长占位仅累计逻辑时长作旁证终态由点击驱动update 不改 phase
* @param {number} dt
*/
function update(dt) {
if (phase === 'booting') return;
elapsedMs += (typeof dt === 'number' ? dt : 1 / 60) * 1000;
}
/**
* render(g)绘制段g=boot 注入的绘制面占位画极简文本分数/阶段证明绘制面可用
* 占位用 fillText 简画真实生成游戏在此画美术引擎粒子已自渲落同一 g
* @param {CanvasRenderingContext2D} g
*/
function render(g) {
const ctx2d = g || mainCtx;
if (!ctx2d) return;
try {
// 极简铺底占位纯色背景证明绘制面可写。坐标按逻辑像素host 已在 real 外层补 DPR 变换)。
ctx2d.fillStyle = phase === 'gameover' ? '#2b1a1a' : '#10243a';
ctx2d.fillRect(0, 0, 390, 844);
ctx2d.fillStyle = '#e8e8e8';
ctx2d.font = '24px sans-serif';
ctx2d.fillText(phase === 'gameover' ? 'GAME OVER' : ('SCORE ' + score), 24, 64);
ctx2d.font = '14px sans-serif';
ctx2d.fillText('tap to score · target ' + TARGET_SCORE, 24, 96);
} catch (_) {
// 绘制面只读/异常不致命(占位绘制非验收项;终态判据走 state().phase
}
}
/**
* onInput(ev)可选输入入口不便用 ctx.getInput 的极简通道备选pointerdown 计分
* init ctx.getInput().on 订阅二者择一生效host 通常走 ctx.getInput()此入口兜底极简通道
* @param {{type:string, x?:number, y?:number, key?:string}} ev
*/
function onInput(ev) {
if (ev && ev.type === 'pointerdown') scoreOne();
}
/**
* destroy()卸载卫生取消输入订阅幂等P3 装载多游戏复位用
*/
function destroy() {
try { if (subTap && subTap.cancel) subTap.cancel(); } catch (_) { /* 幂等 */ }
subTap = null;
boot = null;
mainCtx = null;
phase = 'booting';
}
/*
* GameInstance + host wiring 扩展面
* _forensicsView / _handleAction host 取证与 host.do/state() host wiring
* host.state() _forensicsView().state().phase watchGameEnd 据此判终态必须暴露 phase
* */
return {
// ── 装载契约 GameInstance 五件 ──
init,
update,
render,
onInput,
destroy,
// ── host wiring 扩展(非契约成员)──
/**
* @internal host 取证返回 forensics 视图host.state() 透传 state() phasewatchGameEnd 判终态依赖
* state() 返回 JSON 安全快照phase latch 终态gameover 驻留
*/
_forensicsView: () => ({
seed,
state: () => ({
phase, // 'booting' | 'playing' | 'gameover'gameover 后驻留watchGameEnd 轮询读到恒为 gameover
score,
target: TARGET_SCORE,
elapsedMs,
gameoverReason: phase === 'gameover' ? 'reached_target_score' : '',
}),
}),
/**
* @internal host wiringhost.do(action) 透传到游戏逻辑公开 API
* 占位支持 {type:'tap'} 计分e2e 辅助 UI 点击同走 scoreOne 逻辑公开 API
*/
_handleAction: (action) => {
if (action && action.type === 'tap') scoreOne();
},
};
}

View File

@ -0,0 +1,126 @@
/**
* generic-shell.test.mjs P3 generic 通用装载壳 node 侧守门import-safe + 占位工厂 latch 终态
* ownerP3/W-G1 生成主线 L0 运行node --test零依赖本机 6c6g 可跑浏览器真渲门留 mini-desktop
*
* 本测覆盖三件本机 node 可验的最强证据像素级真渲证据留集成段 mini-desktop
* import-safeentry-generic.js / generic-host-config.js / generated-factory.js node import 即安全
* 顶层零 window/AudioContext 调用引擎经动态 import 延后求值Option C导出齐备
* generic-host-configbuildGenericHostConfig 返回标准 7 件插件集 + 偏序 + 390×844 视口 + wiring node 侧可装配
* 占位工厂 latch 终态真实受控面 bundlecreateHostDevContextnode 侧可建不需 window/引擎驱动占位工厂
* initgetInput().onpointerdown 计分 TARGET 进入 gameover **驻留**再点不复活
* _forensicsView().state().phase 终态 latch= host.state() 透传给 watchGameEnd 的判据inject.ts:217
*
* 为何不直接跑 bootGameHostbootGameHost 真启动需 window/document/RAF/引擎浏览器面node 侧不可跑
* node 侧用 setupCommon 同款手法 bundlefactory(opts)game.initinputBridge._emit 驱动占位工厂经受控面通道
* latch 终态bootGameHost 整壳真启动含引擎掌帧的真渲门留 mini-desktop staging-ops.md
*/
'use strict';
import { test } from 'node:test';
import assert from 'node:assert/strict';
/* ── ① import-safe + 导出齐备 ───────────────────────────────────────────── */
const MODULES = [
['../../../src/host/generic-host-config.js', ['buildGenericHostConfig', 'GENERIC_VIEWPORT']],
['../generated-factory.js', ['default']],
['../entry-generic.js', ['GameHostFactory', 'bootGameHost', 'default']],
];
for (const [path, expectedExports] of MODULES) {
test(`import-safe${path} 可在 node 导入且导出齐备(顶层不触 window/引擎)`, async () => {
const mod = await import(path);
for (const name of expectedExports) {
assert.ok(name in mod, `${path} 缺导出 ${name}`);
}
});
}
/* ── ② generic-host-config标准插件集 + 偏序 + 视口 + wiring ──────────────── */
test('generic-host-config返回标准 7 件插件集 + 六锚偏序 + 390×844 视口 + wiringnode 侧可装配)', async () => {
const { buildGenericHostConfig, GENERIC_VIEWPORT } = await import('../../../src/host/generic-host-config.js');
const cfg = buildGenericHostConfig({ seed: 42 });
// 标准 7 件插件实例齐备。
for (const k of ['collision', 'physics', 'gamefeel', 'juice', 'palettePost', 'audioMusic', 'save']) {
assert.ok(cfg.plugins[k], `缺标准插件 ${k}`);
}
// 注册偏序 = 7 件(保六锚 canonical 顺序)。
assert.equal(cfg.registerOrder.length, 7, '注册偏序应为 7 件');
// 视口 = P3 钉死 390×844。
assert.deepEqual(cfg.viewport, { w: 390, h: 844 });
assert.deepEqual(GENERIC_VIEWPORT, { w: 390, h: 844 });
// wiringbuildFactoryOpts 经 runtime 暴露 plugins/bundle/getRealAudioCtx。
const fo = cfg.buildFactoryOpts(null, { bundle: { tag: 'b' }, plugins: cfg.plugins, getRealAudioCtx: () => null });
assert.ok(fo.runtime && fo.runtime.plugins && fo.runtime.bundle, 'buildFactoryOpts 应返回 runtime.{plugins,bundle}');
});
/* ── ③ 占位工厂 latch 终态(经真实受控面 bundle 驱动node 侧可跑)──────────── */
test('占位工厂:经受控面通道 init→tap 计分→到 TARGET 进入 gameover 并驻留P0 latch 终态)', async () => {
const createGame = (await import('../generated-factory.js')).default;
const { createHostDevContext } = await import('../../../src/core/plugin.js');
// 建真实受控面 bundlenode 侧可建:不需 window/引擎context2d 给最小桩,绘制非本测验收项)。
// 这是 setupCommon 同款手法boot-game-host.js:165bundle.context 即 boot.ctxinputBridge._emit 即 host.tap 的底层。
const fillRectCalls = [];
const ctx2dStub = {
fillStyle: '',
font: '',
fillRect: (...a) => fillRectCalls.push(a),
fillText: () => {},
save: () => {},
restore: () => {},
setTransform: () => {},
};
let nowMs = 0;
const bundle = createHostDevContext({
context2d: ctx2dStub,
seed: 42,
audioFactory: () => null, // 受控音频node 侧无 AudioContext返 null占位工厂不依赖
clock: () => nowMs,
// 无 engineFactorygetEngine() 返 null占位工厂零引擎能力依赖符合纯净工厂路径
});
// factory(opts) → game.init(boot)boot=装载契约脊柱ctx=受控面,触发占位工厂的 getInput().on 订阅)。
const game = createGame(undefined); // 占位工厂演示纯净工厂:不传 runtime
await game.init({
ctx: bundle.context,
mainContext: ctx2dStub,
canvas: null,
seed: 42,
});
// 初始init 后 phase=playingscore=0。
let st = game._forensicsView().state();
assert.equal(st.phase, 'playing', 'init 后应为 playing');
assert.equal(st.score, 0);
const TARGET = st.target;
assert.ok(TARGET >= 1, 'target 应 ≥1');
// 经受控输入桥 _emit('pointerdown')= host.tap 的底层路径)逐次计分,到 TARGET 进终态。
for (let i = 0; i < TARGET; i++) {
nowMs += 16;
bundle.inputBridge._emit('pointerdown', { x: 10, y: 10 });
}
st = game._forensicsView().state();
assert.equal(st.phase, 'gameover', '到 TARGET 分应进入 gameover 终态');
assert.equal(st.score, TARGET, 'gameover 时分数应=TARGET');
assert.equal(st.gameoverReason, 'reached_target_score');
// 【latch 驻留】终态后再点多次phase 恒为 gameover分数不再涨watchGameEnd 任一时刻轮询读到的都是 gameover
for (let i = 0; i < 5; i++) {
nowMs += 16;
bundle.inputBridge._emit('pointerdown', { x: 10, y: 10 });
}
st = game._forensicsView().state();
assert.equal(st.phase, 'gameover', 'latch终态后再点应恒为 gameover不复活');
assert.equal(st.score, TARGET, 'latch终态后分数不再变');
// update/render 可调不抛绘制面桩可写render 走到 fillRect绘制面真被写
game.update(1 / 60);
game.render(ctx2dStub);
assert.ok(fillRectCalls.length > 0, 'render 应写绘制面fillRect 被调)');
// destroy 幂等可调。
game.destroy();
game.destroy();
});

View File

@ -0,0 +1,106 @@
/**
* src/host/generic-host-config.js 通用宿主装配配置P3 generic ·零wanglanmei专属·标准能力插件集
* ownerP3/W-G1 生成主线 L0 消费方games/_generic/entry-generic.jsgeneric bundle 入口壳
*
*
* 本文件是什么 · 为何抽出
* 通用宿主 bootGameHost(opts) 需要四组装配入参标准能力插件集 / 注册偏序 / 视口 / 工厂 wiring
* ref-host-config.js wanglanmei 怎么配 bootGameHost固化了但其中夹了 wanglanmei 专属游戏代码
* import game-shop 工厂render.js 视口常量save namespace='wanglanmei-ref'
* P3 生成主线下20 prompt 各自产出一份 GameHostFactory若各自重写7 件插件实例化 + 偏序 + wiring
* 会造 20 份重复装配故本文件把装配标准能力插件集泛化为零专属的单一来源所有 generic bundle 共用
* · plugins = 能力库标准集collision/physics/gamefeel/juice/palettePost/save/audioMusic 七件实例
* 这是插件库能力面生成游戏经 boot.ctx.getEngine() / 已注册插件用不必自己实例化
* · registerOrder = ref 偏序保六锚 canonical 顺序启动链取证依赖此偏序
* · viewport = P3 钉死 390×844GamePackage 不加 viewport 字段全平台统一逻辑视口
* · buildFactoryOpts = factoryOpts.runtime 标准插件集 + bundle + 真实 AudioContext 取法暴露给游戏
* ref 同形生成游戏想复用 collision/juice 等能力时经 opts.runtime.plugins 一职一路
*
* Q4 铁律 · 零引擎 import本文件仅装配插件实例 + 视口 + 工厂 wiring引擎名字空间由 bootGameHost
* opts.engine 注入real 通道由 entry 包装层动态 import 后传入本文件 node import-safe不触 window/AudioContext
*
* ref-host-config.js 的区别
* ref-host-config = wanglanmei 专属装配 game-shop 工厂 + render 视口 + wanglanmei save namespace
* 本文件 = 零专属通用装配 import 任何游戏工厂工厂由 entry-generic.js import 生成工厂传入
* save namespace 用通用 'zaomeng-generic'视口硬编码 390×844两者正交互不引用
*
*/
'use strict';
// 能力库 7 件标准业务插件工厂host 拥有,经工厂 opts.runtime 注入给游戏,一职一路)。
// 与 ref-host-config 照搬同一组 import同一份插件库实现保 generic 与 ref 能力面一致。
import { createCollisionPlugin } from '../plugins/collision/impl.js';
import { createPhysicsLitePlugin } from '../plugins/physics-lite/impl.js';
import { createGamefeelPlugin } from '../plugins/gamefeel/impl.js';
import { createParticlesJuicePlugin } from '../plugins/particles-juice/impl.js';
import { createPalettePostPlugin } from '../plugins/palette-post/impl.js';
import { createAudioMusicPlugin } from '../plugins/audio-music/impl.js';
import { createSaveProgressPlugin, createMemoryAdapter, createLocalStorageAdapter } from '../plugins/save-progress/impl.js';
/**
* P3 generic 壳逻辑视口390×844P3 钉死
* 全平台统一逻辑视口GamePackage 不加 viewport 字段故此处硬编码而非读包
*/
export const GENERIC_VIEWPORT = { w: 390, h: 844 };
/**
* 装配通用宿主调 bootGameHost 所需的标准入参标准插件集 / 注册偏序 / 视口 / 工厂 wiring
* entry-generic.js canvas/statusEl/engine/seed/mode/engineMode/factory 后传 bootGameHost
* 本函数零wanglanmei专属不含任何游戏工厂工厂由调用方entry-generic另行 import 生成工厂后传入
*
* @param {{seed:number}} args 装配入参仅需 seed用于粒子插件确定性
* @returns {{plugins:object, registerOrder:Array<object>, viewport:{w:number,h:number}, buildFactoryOpts:Function}}
* - plugins标准能力插件实例集 { collision, physics, gamefeel, juice, palettePost, audioMusic, save }
* - registerOrder注册偏序保六锚 canonical 顺序
* - viewport{w:390,h:844}P3 钉死
* - buildFactoryOpts构造工厂 opts 回调 runtime 暴露标准插件集 + bundle + 真实 AudioContext 取法
*/
export function buildGenericHostConfig(args) {
// seed 缺省 0确定性兜底规整为无符号 32 位(与 host/ref 同口径)。
const seed = ((args && args.seed) != null ? args.seed : 0) >>> 0;
/* 7 host opts.runtime.plugins
* 参数与 ref-host-config 同口径 generic ref 能力一致这些参数是能力库默认面非游戏玩法 */
const collision = createCollisionPlugin({ cellSize: 96 });
const physics = createPhysicsLitePlugin();
const gamefeel = createGamefeelPlugin({ inputBufferMs: 260, inputTypes: ['pointerdown'] });
const juice = createParticlesJuicePlugin({ seed, maxParticles: 600 });
const palettePost = createPalettePostPlugin({ vignette: { enabled: true, strength: 0.22 } });
const audioMusic = createAudioMusicPlugin({ masterVolume: 0.8 });
// 存档后端:优先浏览器 localStorage不可用环境node/隐私模式)回退内存 adapter错误路径留痕。
let saveAdapter;
try {
saveAdapter = createLocalStorageAdapter();
} catch (e) {
// node 侧 import-safe 不会走到此buildGenericHostConfig 仅在 bootGameHost 运行期调);浏览器隐私模式可能触发。
console.warn('[generic] localStorage 不可用,存档退内存 adapter' + (e && e.message));
saveAdapter = createMemoryAdapter();
}
// 存档命名空间用通用 'zaomeng-generic'非wanglanmei专属P3 多游戏各自 namespace 由生成域决定,此处给通用默认)。
const save = createSaveProgressPlugin({ namespace: 'zaomeng-generic', adapter: saveAdapter });
const plugins = { collision, physics, gamefeel, juice, palettePost, audioMusic, save };
// 注册偏序保六锚偏序collision→physics→gamefeel→juice→palettePost→save→audioMusic与 ref 同序)。
const registerOrder = [collision, physics, gamefeel, juice, palettePost, save, audioMusic];
return {
plugins,
registerOrder,
viewport: GENERIC_VIEWPORT,
/**
* 构造工厂 optshost wiring runtime 把标准插件集 + bundle + 真实 AudioContext 取法暴露给游戏
* ref-host-config.buildFactoryOpts 同形生成游戏若复用能力插件collision/juice/save
* opts.runtime.plugins 一职一路纯净游戏不取 runtime 也可仅吃 boot 脊柱
* @param {import('../core/api.d.ts').PluginContext} _bootCtx 本帧受控面本回调不直接用由游戏经 boot
* @param {{bundle:object, plugins:object, getRealAudioCtx:Function}} hostInternals host 内部句柄
*/
buildFactoryOpts: (_bootCtx, hostInternals) => ({
runtime: {
plugins: hostInternals.plugins,
bundle: hostInternals.bundle,
getRealAudioCtx: hostInternals.getRealAudioCtx,
},
}),
};
}