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 Permalink 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 契约实现)
* ownertier2 引擎线M1-Phaser-scaffold+host
* 消费方:①生成 agent 产出的 Phaser 工程(经本宿主 boot 入沙箱 canvas 真跑②harnessU3 CDP 探针经它驱动九门 + 富游戏三门)
*
* ════════════════════════════════════════════════════════════════════════════
* 【本文件是什么 · 第二装载分支】
* 左支Tier0/1·LittleJS的宿主是 game-runtime/src/host/boot-game-host.jsbootGameHost
* 引擎掌帧、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.factoryPhaserGameFactory = (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 主循环 postupdateA4 铁律实现)。
* 每帧读 game.readState().phase落定终态 → 记首帧;已落定且 phase 恒定 → stableFrames++
* 已落定但 phase 又变(回弹)→ bounced=truelatch 门 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.frameheadless 无 → 退宿主帧计数)。 */
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;
},
};
/** 取当前活跃 scenereal 通道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.Gamecanvas=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 异步 bootscene preload/create故用 Promise 包裹到 create 完成才 resolve host。
* ════════════════════════════════════════════════════════════════════ */
return await new Promise((resolve, reject) => {
// 受控能力面realscene create 后注入真 scene先建占位引用create 内补 scene
let caps = null;
// tier2 宿主 scenePhaser 自掌帧、自渲;本 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) }); }
},
// createscene 就绪 → 建受控面(注入真 scene→ boot 游戏 → 挂逐帧 latch 轮询 → 就绪钩子。
create() {
const self = this;
(async () => {
try {
// 受控能力面:注入真 scene粒子 / 音频 / easing 经它调 Phaser
caps = createPhaserEngineCaps({ scene: self, recHook });
// 经装载契约建游戏实例。
game = factory(factoryOpts);
// game.boot注入 boot 上下文PhaserHostBootContextgame/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);
}
});
}