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>
179 lines
9.2 KiB
JavaScript
179 lines
9.2 KiB
JavaScript
/**
|
||
* generated-factory.js — 【占位 stub】生成工厂占位(L1 出码替换本文件)
|
||
* owner:P3/W-G1 生成主线 L0 | 消费方:entry-generic.js(import 本默认导出作 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().phase(inject.ts:217),到 'gameover' 代发 game_end{completed:true}
|
||
* (门② 闭环锚)。轮询是「间隔采样」——若终态是「瞬时 phase」(某帧置 gameover 下帧又变),轮询会漏报致闭环静默断裂
|
||
* (见 memory runner-v2-p1-loading-contract P3 硬约束)。故终态必须**驻留**:一旦 gameover,state().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.mainContext,render 的 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:置入终态后恒为 gameover(watchGameEnd 轮询任一时刻读到的都是 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 返回订阅句柄(含 cancel),destroy 时取消(与 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()(含 phase,watchGameEnd 判终态依赖)。
|
||
* 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 wiring:host.do(action) 透传到游戏逻辑公开 API。
|
||
* 占位支持 {type:'tap'} 计分(e2e 辅助;与 UI 点击同走 scoreOne 逻辑公开 API)。
|
||
*/
|
||
_handleAction: (action) => {
|
||
if (action && action.type === 'tap') scoreOne();
|
||
},
|
||
};
|
||
}
|