games-development-ai/tier2/engine/boot-phaser-host.js
zizi 8ea6133e63 fix(tier2): recHook 透传到资源系统——F 门/三联动门记 resource.* call-ID
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>
2026-06-23 20:48:40 +00:00

302 lines
19 KiB
JavaScript
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.

/**
* 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);
}
});
}