F 门/三联动门读 window.__engineCalls 找 resource.addCoins/consumeIngredient 前缀,
但旧版只有 engine-caps(粒子/音频)经 recHook 记 __engineCalls,资源系统只记内部
_callCounts、不进 __engineCalls → calls=0、F 门 + 三联动门真接线判据永假。
修(additive,不破契约必填项):host 经 boot 上下文透传 recHook → main.js 喂
createGameCore → resource-system 完单真调 addCoins/合成真调 consumeIngredient 时
recHook('resource.addCoins'/'resource.consumeIngredient') 记进 __engineCalls。
headless/静态校验下 recHook 缺省 no-op、不抛错。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
302 lines
19 KiB
JavaScript
302 lines
19 KiB
JavaScript
/**
|
||
* tier2/engine/boot-phaser-host.js —— tier2 第二装载分支宿主(boot 真 Phaser 工程,A4 host-load 契约实现)
|
||
* owner:tier2 引擎线(M1-Phaser-scaffold+host)
|
||
* 消费方:①生成 agent 产出的 Phaser 工程(经本宿主 boot 入沙箱 canvas 真跑);②harness(U3 CDP 探针经它驱动九门 + 富游戏三门)
|
||
*
|
||
* ════════════════════════════════════════════════════════════════════════════
|
||
* 【本文件是什么 · 第二装载分支】
|
||
* 左支(Tier0/1·LittleJS)的宿主是 game-runtime/src/host/boot-game-host.js(bootGameHost):
|
||
* 引擎掌帧、game.render(g) 绘制面 = 引擎 mainContext、生命周期对齐 engineInit 五回调。
|
||
* 本文件是 tier2 右支(Phaser)的宿主——fork 自 bootGameHost 的 opts.engine 注入边界范式,
|
||
* 但默认引擎换 Phaser,且核心语义反转:**Phaser 自掌帧、自挂 canvas、场景树自驱 update/render**,
|
||
* 宿主不调 game.render(g)(绘制由 Phaser scene 自己做,对齐 boot-phaser-host.d.ts 注释)。
|
||
*
|
||
* 【为什么 fork 而非复用 bootGameHost】(red line · 对齐 boot-phaser-host.d.ts §9-13、tier2 .agent 红线)
|
||
* 左支 mainContext 是 LittleJS setGLEnable(false) 后的纯 2D 绘制面、render(g) 的 g=引擎 mainContext。
|
||
* Phaser 自己掌帧、自挂 canvas、场景树驱动渲染——绘制面与生命周期语义完全不同。
|
||
* 两条装载路解耦并存、各自演进(F2「伸缩·并存」)。本宿主**不改左支 bootGameHost 的 opts.engine 注入边界、
|
||
* 不碰 ECS-lite 即取即跑装载契约**(tier2 .agent 红线:装载 / 契约 / 沙箱独立)。
|
||
*
|
||
* 【Q4 铁律不破 · opts.engine 注入边界】(沿用左支范式)
|
||
* 本模块**不顶层 import 'phaser'**——Phaser 名字空间经 opts.engine 注入(real 通道由调用方动态 import 后传入;
|
||
* headless/node 通道为 undefined,保 node import-safe,可做静态校验 node --check)。
|
||
*
|
||
* 【latch 终态轮询】(A4 铁律 · boot-phaser-host.d.ts §20-22)
|
||
* 游戏无 emit 通道 → 终态焊成可轮询 latch:游戏跑到结束态时把 readState().phase 焊成 'win'/'lose'/'gameover'
|
||
* 驻留在可读状态里,**宿主每帧轮询去读**,而非等游戏 emit。本宿主在 Phaser 主循环挂 postupdate 钩子逐帧轮询
|
||
* game.readState().phase,落定终态后 latch(记首次落定帧 + 后续多帧恒定校验,供 latch 门判「不回弹」)。
|
||
*
|
||
* 【装载契约(tier2/contracts/boot-phaser-host.d.ts,零改·实现)】
|
||
* - opts.factory:PhaserGameFactory = (opts?) => PhaserGameInstance(本宿主调它建游戏实例)。
|
||
* - boot 上下文 PhaserHostBootContext = { game(Phaser.Game), canvas, seed, assets? }(本宿主在 game.boot 时一次性注入)。
|
||
* - PhaserGameInstance 生命周期 boot/readState/injectInput?/destroy(无 render(g),Phaser 自渲)。
|
||
* ════════════════════════════════════════════════════════════════════════════
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
// 受控能力面(Phaser 版薄包装;本模块零引擎 import,能力面内部也经注入的 scene 调 Phaser,不顶层 import)。
|
||
import { createPhaserEngineCaps } from './engine-caps-phaser.js';
|
||
|
||
/** 取证固定环境(与左支 host FIXED_ENV 对齐口径;Phaser scale 用逻辑像素 + 像素比)。 */
|
||
const FIXED_DT = 1 / 60; // 逻辑步长(秒);Phaser 自掌帧,本值仅用于 host 侧轮询节流口径对齐
|
||
|
||
/**
|
||
* 启动 tier2 Phaser 宿主。返回 host 句柄(含驱动 / 取证钩子,供 harness 探针 / 上层组装取证)。
|
||
*
|
||
* @param {Object} opts 宿主选项(参数化入口,对齐左支 bootGameHost 的注入边界范式)
|
||
* @param {HTMLElement} opts.parent Phaser 挂载父容器(DOM 元素;Phaser 在其内自建 canvas)
|
||
* @param {any} [opts.engine] Phaser 名字空间(即 import 'phaser' 的默认导出);real 通道由调用方注入,headless/node 为 undefined
|
||
* @param {import('../contracts/boot-phaser-host.d.ts').PhaserGameFactory} opts.factory 游戏工厂(生成 agent 产出靶;本宿主调它建实例)
|
||
* @param {number} opts.seed 主随机种子(确定性;游戏须据它派生随机,禁裸 Math.random)
|
||
* @param {{w:number,h:number}} opts.viewport 逻辑视口(替写死宽高;mini-肥鹅默认 390×844)
|
||
* @param {Record<string, {image?: any}>} [opts.assets] 宿主预载资产图源(按 asset.ref 键;缺=游戏降级占位)
|
||
* @param {Record<string, unknown>} [opts.factoryOpts] 传给游戏工厂的选项(difficulty/variant 等;缺省 undefined)
|
||
* @param {(id:string)=>void} [opts.recHook] 引擎能力 call-ID 取证钩子(F 门真接线证据;缺省 no-op)
|
||
* @param {(host:object, internals:object)=>void} [opts.onReady] 就绪钩子(受控面 + 游戏全就绪后回调,供 harness 挂取证全局)
|
||
* @returns {Promise<object>} host 句柄(seed/frameCount/state/tap/injectInput/latchInfo/destroy 等驱动 + 取证面)
|
||
*/
|
||
export async function bootPhaserHost(opts) {
|
||
const parent = opts.parent;
|
||
// 【Q4 · opts.engine 注入边界】Phaser 名字空间经注入(real 通道由调用方动态 import 'phaser' 后传;headless/node 为 undefined)。
|
||
const PHASER = opts.engine;
|
||
const seed = (opts.seed >>> 0); // 调用方已解析种子(确定性;不读 URL)
|
||
const VIEW_W = opts.viewport.w;
|
||
const VIEW_H = opts.viewport.h;
|
||
const factory = opts.factory;
|
||
const factoryOpts = opts.factoryOpts; // 传给游戏工厂(缺省 undefined)
|
||
const assets = opts.assets || {}; // 宿主预载资产图源(按 ref 键)
|
||
const recHook = opts.recHook || (() => {}); // 引擎能力 call-ID 取证钩子
|
||
const onReady = opts.onReady || (() => {}); // 就绪钩子
|
||
|
||
/* ── 未捕获错误归集(评估门:console 零未捕获错误,对齐左支 host 范式)── */
|
||
/** @type {Array<{type:string,message:string}>} */
|
||
const uncaught = [];
|
||
if (typeof window !== 'undefined' && window.addEventListener) {
|
||
window.addEventListener('error', (e) => uncaught.push({ type: 'error', message: String((e && e.message) || e) }));
|
||
window.addEventListener('unhandledrejection', (e) =>
|
||
uncaught.push({ type: 'unhandledrejection', message: String((e && e.reason && e.reason.message) || (e && e.reason) || e) })
|
||
);
|
||
}
|
||
|
||
/* ── 游戏实例 + 受控能力面(scene create 后建;延后建避免拿不到 scene)── */
|
||
/** @type {import('../contracts/boot-phaser-host.d.ts').PhaserGameInstance|null} */
|
||
let game = null;
|
||
/** @type {any} Phaser.Game 实例(PHASER 注入后建)。 */
|
||
let phaserGame = null;
|
||
/** @type {HTMLCanvasElement|null} Phaser 自挂 canvas(探针像素回读 / 输入映射用)。 */
|
||
let canvas = null;
|
||
|
||
/* ── latch 终态轮询状态(A4 铁律核心)──
|
||
* 游戏无 emit → 宿主逐帧轮询 readState().phase,落定终态(win/lose/gameover)后 latch:
|
||
* 记首次落定帧号 + 后续连续恒定帧数(latch 门据它判「终态落定不回弹」)。 */
|
||
const TERMINAL_PHASES = new Set(['win', 'lose', 'gameover']);
|
||
const latchInfo = {
|
||
latched: false, // 是否已落定终态
|
||
phase: null, // 落定的终态值('win'/'lose'/'gameover')
|
||
settledAtFrame: -1, // 首次落定的帧号
|
||
stableFrames: 0, // 落定后连续恒定帧数(多帧轮询 phase 不变 → 证不回弹)
|
||
bounced: false, // 是否回弹(落定后 phase 又变了 → latch 门判 fail)
|
||
};
|
||
let frameCount = 0; // 宿主侧帧计数(postupdate 每帧 +1;探针帧源候选之一)
|
||
|
||
/**
|
||
* 逐帧 latch 轮询(挂 Phaser 主循环 postupdate;A4 铁律实现)。
|
||
* 每帧读 game.readState().phase:落定终态 → 记首帧;已落定且 phase 恒定 → stableFrames++;
|
||
* 已落定但 phase 又变(回弹)→ bounced=true(latch 门 fail 信号)。
|
||
*/
|
||
function pollLatch() {
|
||
frameCount++;
|
||
if (!game || typeof game.readState !== 'function') return;
|
||
let phase;
|
||
try {
|
||
// 探针 / 宿主只读 readState,禁篡改(可测性红线,对齐 engine-probe-hooks §可测性红线)。
|
||
phase = game.readState().phase;
|
||
} catch (e) {
|
||
uncaught.push({ type: 'readState', message: String((e && e.message) || e) });
|
||
return;
|
||
}
|
||
if (!latchInfo.latched) {
|
||
// 尚未落定:见到终态 phase → latch。
|
||
if (TERMINAL_PHASES.has(phase)) {
|
||
latchInfo.latched = true;
|
||
latchInfo.phase = phase;
|
||
latchInfo.settledAtFrame = frameCount;
|
||
latchInfo.stableFrames = 1;
|
||
}
|
||
} else {
|
||
// 已落定:校验「不回弹」——phase 恒定 → stableFrames++;变了 → 回弹(latch 门 fail)。
|
||
if (phase === latchInfo.phase) {
|
||
latchInfo.stableFrames++;
|
||
} else {
|
||
latchInfo.bounced = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ── host 驱动 + 取证句柄(harness 探针 / 上层组装用;引擎无关面)── */
|
||
const host = {
|
||
seed,
|
||
viewport: { w: VIEW_W, h: VIEW_H },
|
||
/** 宿主侧帧计数(探针帧源候选;real 下也可读 phaserGame.loop.frame)。 */
|
||
frameCount: () => frameCount,
|
||
/** Phaser 引擎帧号(探针帧源·A6 候选 game.loop.frame;headless 无 → 退宿主帧计数)。 */
|
||
engineFrame: () => (phaserGame && phaserGame.loop ? phaserGame.loop.frame : frameCount),
|
||
/** 游戏可观测状态快照(latch 门 / 富游戏三门读它;游戏未就绪 → booting)。 */
|
||
state: () => (game && typeof game.readState === 'function' ? game.readState() : { phase: 'booting' }),
|
||
/** latch 终态信息(latch 门据此判「落定不回弹 + 宿主可读」)。 */
|
||
latchInfo: () => ({ ...latchInfo }),
|
||
/**
|
||
* 注入一次归一化输入(harness 驱动 G/H/I 门用;与游戏内 Phaser input 订阅二者择一)。
|
||
* 优先走游戏 injectInput(若实现);否则经 Phaser input 模拟(real 通道)。
|
||
* @param {import('../contracts/boot-phaser-host.d.ts').PhaserInputEvent} ev 归一化输入事件
|
||
*/
|
||
injectInput(ev) {
|
||
if (game && typeof game.injectInput === 'function') {
|
||
// 首选:游戏实现的归一化输入入口(确定性,harness 友好)。
|
||
game.injectInput(ev);
|
||
return;
|
||
}
|
||
// 兜底:经 Phaser scene input 模拟(real 通道;headless 无 scene → no-op)。
|
||
const scene = _activeScene();
|
||
if (!scene || !scene.input) return;
|
||
try {
|
||
if (ev.type === 'tap' && scene.input.emit) {
|
||
// 模拟一次 pointerdown/up(坐标=受控面像素,Phaser 画布逻辑像素对齐)。
|
||
scene.input.emit('pointerdown', { x: ev.x, y: ev.y, worldX: ev.x, worldY: ev.y });
|
||
scene.input.emit('pointerup', { x: ev.x, y: ev.y, worldX: ev.x, worldY: ev.y });
|
||
}
|
||
} catch (e) { uncaught.push({ type: 'injectInput', message: String((e && e.message) || e) }); }
|
||
},
|
||
/** 卸载(幂等;harness session 切换 / 多游戏复位用;释放 Phaser.Game)。 */
|
||
destroy() {
|
||
try { if (game && typeof game.destroy === 'function') game.destroy(); } catch (e) { /* 幂等吞错 */ }
|
||
try { if (phaserGame && typeof phaserGame.destroy === 'function') phaserGame.destroy(true); } catch (e) { /* 幂等吞错 */ }
|
||
game = null;
|
||
phaserGame = null;
|
||
},
|
||
};
|
||
|
||
/** 取当前活跃 scene(real 通道;headless 无 → null)。 */
|
||
function _activeScene() {
|
||
if (!phaserGame || !phaserGame.scene || typeof phaserGame.scene.getScenes !== 'function') return null;
|
||
const arr = phaserGame.scene.getScenes(true);
|
||
return (arr && arr.length) ? arr[0] : null;
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════════════════
|
||
* headless / node 通道(保 import-safe + 静态校验):opts.engine 未注入时不起 Phaser,仅建 host 壳。
|
||
* 用途:① node --check 语法校验;② headless 逻辑跑(不需真渲染)。real 通道见下。
|
||
* ════════════════════════════════════════════════════════════════════ */
|
||
if (!PHASER) {
|
||
// 未注入 Phaser:建受控能力面(scene=null → 有状态能力 no-op、数学族纯函数),建游戏实例但不真渲染。
|
||
// 这条路保 node import-safe(静态校验跑得通),也是 headless 逻辑校验的入口。
|
||
const caps = createPhaserEngineCaps({ scene: null, recHook });
|
||
// 经装载契约建游戏实例(factory 是生成 agent 产出靶)。
|
||
game = factory(factoryOpts);
|
||
// game.boot:注入 boot 上下文(headless 下 game=null Phaser.Game,canvas=null)。
|
||
await Promise.resolve(game.boot({
|
||
game: null, // headless 无 Phaser.Game
|
||
canvas: null, // headless 无 canvas
|
||
seed,
|
||
assets,
|
||
// 受控能力面经 boot 上下文额外透传(游戏经 ctx.getEngine() 拿;headless 下 no-op 能力)。
|
||
// 注:契约 PhaserHostBootContext 不含 caps 字段,caps 经 game 内部约定取;此处 additive 透传不破契约必填项。
|
||
caps,
|
||
// recHook additive 透传:游戏逻辑(如资源系统完单/合成)经它把 call-ID 记进 window.__engineCalls,
|
||
// 供 F 门 / 富游戏三联动门证「真调资源(addCoins/consumeIngredient)、非自绘伪装」(同 caps 不破契约必填项)。
|
||
recHook,
|
||
}));
|
||
// 就绪钩子(harness 可挂取证全局;headless 下用于逻辑校验)。
|
||
onReady(host, { game, phaserGame: null, canvas: null, caps, uncaught, latchInfo, pollLatch });
|
||
return host;
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════════════════
|
||
* real 通道(Phaser 掌帧):建 Phaser.Game → scene create 注入受控面 + boot 游戏 → 挂逐帧 latch 轮询
|
||
* 控制流:Phaser 异步 boot(scene preload/create),故用 Promise 包裹到 create 完成才 resolve host。
|
||
* ════════════════════════════════════════════════════════════════════ */
|
||
return await new Promise((resolve, reject) => {
|
||
// 受控能力面(real:scene create 后注入真 scene;先建占位引用,create 内补 scene)。
|
||
let caps = null;
|
||
|
||
// tier2 宿主 scene:Phaser 自掌帧、自渲;本 scene 负责 boot 游戏 + 挂 latch 轮询。
|
||
// 生成 agent 写的游戏自己也是 Phaser scene 树,但经本宿主 scene 的 boot 注入受控面统一管理。
|
||
const HostScene = {
|
||
key: '__tier2HostScene',
|
||
// preload:宿主预置最小资产(粒子用 1px 白点纹理);游戏自己的资产由游戏 scene preload 装。
|
||
preload() {
|
||
// 生成 1px 白点纹理供受控粒子面缺省纹理用('__tier2_particle')。
|
||
try {
|
||
const g = this.make.graphics({ add: false });
|
||
g.fillStyle(0xffffff, 1);
|
||
g.fillRect(0, 0, 2, 2);
|
||
g.generateTexture('__tier2_particle', 2, 2);
|
||
g.destroy();
|
||
} catch (e) { uncaught.push({ type: 'preload', message: String((e && e.message) || e) }); }
|
||
},
|
||
// create:scene 就绪 → 建受控面(注入真 scene)→ boot 游戏 → 挂逐帧 latch 轮询 → 就绪钩子。
|
||
create() {
|
||
const self = this;
|
||
(async () => {
|
||
try {
|
||
// 受控能力面:注入真 scene(粒子 / 音频 / easing 经它调 Phaser)。
|
||
caps = createPhaserEngineCaps({ scene: self, recHook });
|
||
// 经装载契约建游戏实例。
|
||
game = factory(factoryOpts);
|
||
// game.boot:注入 boot 上下文(PhaserHostBootContext:game/canvas/seed/assets + 受控面 caps)。
|
||
await Promise.resolve(game.boot({
|
||
game: phaserGame, // Phaser.Game 实例(游戏经它拿 scene 管理器 / loop / scale)
|
||
canvas, // Phaser 自挂可见画布(探针像素回读 / 输入映射)
|
||
seed,
|
||
assets,
|
||
caps, // additive 透传受控面(游戏经 ctx.getEngine() 拿;不破契约必填项)
|
||
recHook, // additive 透传:游戏逻辑经它记 call-ID 进 __engineCalls(F 门 / 三联动门真接线证据)
|
||
}));
|
||
// 挂逐帧 latch 轮询:Phaser scene events 'postupdate' 每帧后调(A4 铁律核心)。
|
||
self.events.on('postupdate', pollLatch);
|
||
// 就绪钩子(harness 挂取证全局:window.__tier2Booted / window.__gameState / window.__tier2Host 等)。
|
||
onReady(host, { game, phaserGame, canvas, caps, uncaught, latchInfo, pollLatch });
|
||
// boot 就绪信号(A6 ③ boot 信号候选;探针 navigate 后轮询它判 boot 完成)。
|
||
if (typeof window !== 'undefined') window.__tier2Booted = true;
|
||
resolve(host);
|
||
} catch (e) {
|
||
// boot 失败:镜像错因,reject 供上层捕获。
|
||
const msg = String((e && e.message) || e);
|
||
uncaught.push({ type: 'game-boot', message: msg });
|
||
if (typeof window !== 'undefined') window.__tier2BootError = msg;
|
||
reject(e);
|
||
}
|
||
})();
|
||
},
|
||
};
|
||
|
||
// Phaser.Game 配置:固定视口 + 像素比 + 确定性(关随机种子由游戏控制,引擎不引入额外随机)。
|
||
const config = {
|
||
type: PHASER.AUTO, // 自动选 WebGL/Canvas(探针像素回读:harness 侧若需 2D readback,可在沙箱强制 CANVAS)
|
||
parent, // 挂载父容器
|
||
width: VIEW_W,
|
||
height: VIEW_H,
|
||
backgroundColor: '#000000', // 不透明黑底(与左支 clearBlack 对齐,保探针像素回读语义)
|
||
scale: { mode: PHASER.Scale.NONE }, // 固定逻辑像素(取证固定环境;不自适应缩放,保确定性坐标)
|
||
// 关 banner / audio 自动解锁延后(用户手势);fps 固定 60 保确定性步长。
|
||
banner: false,
|
||
fps: { target: 60, forceSetTimeOut: true },
|
||
scene: [HostScene],
|
||
};
|
||
|
||
try {
|
||
// 建 Phaser.Game(经注入的 PHASER 名字空间,本模块零顶层 import)。
|
||
phaserGame = new PHASER.Game(config);
|
||
// Phaser 自挂 canvas(探针选择器 / 像素回读 / 输入映射用;A6 ① canvas 选择器候选)。
|
||
canvas = phaserGame.canvas;
|
||
// 给 canvas 打 id(探针选择器候选:'#tier2-game',对齐 A6 重写映射「Phaser 自挂 canvas」)。
|
||
if (canvas) canvas.id = 'tier2-game';
|
||
} catch (e) {
|
||
const msg = String((e && e.message) || e);
|
||
if (typeof window !== 'undefined') window.__tier2BootError = msg;
|
||
reject(e);
|
||
}
|
||
});
|
||
}
|