# Pixi DX-First Runtime & Assets Redesign Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 重构运行时与资源系统,在 Web H5 下建立可观测、可回滚、可维护的 DX 优先框架底座,并交付最小内置调试 UI。 **Architecture:** 采用 `kernel / scene / assets / devtools / ui-core` 五层结构。场景切换改为事务模型(prepare/commit/rollback),资源改为 `AssetGraph + AssetSession` 持有关系追踪。开发态通过 overlay 与命令面板直接消费运行时事件流,优先保证定位效率,再逐步做性能优化。 **Tech Stack:** TypeScript, PixiJS 8, Vite 5, Vitest(新增), @pixi/devtools(已有) --- ## Scope Check 本计划覆盖同一重构主题下的两个强耦合子系统(运行时内核与资源系统),并包含最小 devtools 交付。三部分共享同一事件流与状态模型,适合在单计划中分任务推进。 ## Planned File Structure ### Create - `src/kernel/AppRuntime.ts`:封装 renderer/ticker/stage 初始化、resize、render 入口 - `src/kernel/RuntimeEvents.ts`:定义运行时事件类型与发布接口 - `src/kernel/RuntimePlugin.ts`:插件接口与插件注册器 - `src/scene/SceneStateMachine.ts`:场景状态机与状态迁移校验 - `src/scene/TransitionTransaction.ts`:切换事务(prepare/commit/rollback) - `src/scene/SceneContext.ts`:场景可用上下文(assets/events/runtime) - `src/assets/AssetGraph.ts`:bundle 依赖图与解析 - `src/assets/AssetSession.ts`:会话申请/释放/owner 追踪 - `src/assets/AssetInspectorSnapshot.ts`:供 overlay 使用的只读快照 - `src/devtools/overlay/DebugOverlay.ts`:调试面板容器与刷新调度 - `src/devtools/overlay/widgets/SceneWidget.ts`:场景状态展示 - `src/devtools/overlay/widgets/AssetWidget.ts`:资源关系展示 - `src/devtools/overlay/widgets/EventWidget.ts`:事件时间线展示 - `src/ui-core/UiPanel.ts`:面板基础组件 - `src/ui-core/UiText.ts`:文本基础组件 - `src/ui-core/UiButton.ts`:按钮基础组件 - `src/ui-core/UiList.ts`:列表基础组件 - `src/devtools/CommandPalette.ts`:运行时命令入口 - `tests/kernel/scene-state-machine.test.ts`:状态机单测 - `tests/kernel/transition-transaction.test.ts`:事务单测 - `tests/kernel/runtime-events.test.ts`:运行时事件流单测 - `tests/assets/asset-session.test.ts`:资源会话单测 - `vitest.config.ts`:测试配置 ### Modify - `package.json`:新增 `test`、`test:watch` 脚本和 vitest 依赖 - `src/core/Game.ts`:退役为兼容壳,委托给 `AppRuntime`(过渡期) - `src/core/AssetManager.ts`:拆分职责,迁移至 `assets` 新实现 - `src/scene/SceneManager.ts`:接入状态机和事务 - `src/scene/BaseScene.ts`:场景生命周期改造(setup/enter/leave/dispose) - `src/init.ts`:运行时/场景/devtools 统一装配 - `src/main.ts`:开发态开关和命令面板入口 --- ### Task 1: 建立测试基线与重构骨架 **Files:** - Create: `vitest.config.ts` - Create: `tests/kernel/scene-state-machine.test.ts` - Modify: `package.json` - Test: `tests/kernel/scene-state-machine.test.ts` - [ ] **Step 1: 写一个失败的状态机迁移测试** ```ts import { describe, expect, it } from "vitest"; import { SceneStateMachine } from "@/scene/SceneStateMachine"; describe("SceneStateMachine", () => { it("rejects invalid transition from created to ready", () => { const sm = new SceneStateMachine("welcome"); expect(() => sm.transition("ready")).toThrowError( /invalid transition: created -> ready/ ); }); }); ``` - [ ] **Step 2: 运行测试并确认失败(因为目标文件尚不存在)** Run: `npm run test -- tests/kernel/scene-state-machine.test.ts` Expected: FAIL with module resolution error for `@/scene/SceneStateMachine` - [ ] **Step 3: 添加最小测试基础设施与最小实现** ```ts // vitest.config.ts import { defineConfig } from "vitest/config"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [tsconfigPaths()], test: { environment: "node", include: ["tests/**/*.test.ts"], }, }); ``` ```ts // src/scene/SceneStateMachine.ts export type SceneState = | "created" | "setup" | "assetsLoading" | "entering" | "ready" | "leaving" | "disposed"; const ALLOWED: Record = { created: ["setup"], setup: ["assetsLoading"], assetsLoading: ["entering"], entering: ["ready"], ready: ["leaving"], leaving: ["disposed", "ready"], disposed: [], }; export class SceneStateMachine { public state: SceneState = "created"; constructor(public readonly sceneId: string) {} transition(next: SceneState): void { if (!ALLOWED[this.state].includes(next)) { throw new Error(`invalid transition: ${this.state} -> ${next}`); } this.state = next; } } ``` - [ ] **Step 4: 再次运行测试并确认通过** Run: `npm run test -- tests/kernel/scene-state-machine.test.ts` Expected: PASS (1 passed) - [ ] **Step 5: 提交** ```bash git add package.json vitest.config.ts src/scene/SceneStateMachine.ts tests/kernel/scene-state-machine.test.ts git commit -m "test: add vitest baseline and scene state machine skeleton" ``` --- ### Task 2: 实现场景事务切换与回滚 **Files:** - Create: `src/scene/TransitionTransaction.ts` - Create: `tests/kernel/transition-transaction.test.ts` - Modify: `src/scene/SceneManager.ts` - Test: `tests/kernel/transition-transaction.test.ts` - [ ] **Step 1: 写失败测试,验证 prepare 失败会 rollback** ```ts import { describe, expect, it, vi } from "vitest"; import { TransitionTransaction } from "@/scene/TransitionTransaction"; it("rolls back when prepare fails", async () => { const tx = new TransitionTransaction({ prepare: vi.fn().mockRejectedValue(new Error("load failed")), commit: vi.fn(), rollback: vi.fn(), finalize: vi.fn(), }); await expect(tx.run()).rejects.toThrow(/load failed/); expect(tx.hooks.rollback).toHaveBeenCalledTimes(1); expect(tx.hooks.commit).not.toHaveBeenCalled(); }); ``` - [ ] **Step 2: 运行测试确认失败** Run: `npm run test -- tests/kernel/transition-transaction.test.ts` Expected: FAIL with missing module `@/scene/TransitionTransaction` - [ ] **Step 3: 写最小事务实现并接入 SceneManager** ```ts // src/scene/TransitionTransaction.ts export interface TransitionHooks { prepare: () => Promise; commit: () => Promise; rollback: () => Promise; finalize: () => Promise; } export class TransitionTransaction { constructor(public readonly hooks: TransitionHooks) {} async run(): Promise { try { await this.hooks.prepare(); await this.hooks.commit(); } catch (error) { await this.hooks.rollback(); throw error; } finally { await this.hooks.finalize(); } } } ``` ```ts // src/scene/SceneManager.ts (new usage snippet) const tx = new TransitionTransaction({ prepare: async () => { /* load target scene assets + setup */ }, commit: async () => { /* mount target and enter */ }, rollback: async () => { /* restore previous ready scene */ }, finalize: async () => { /* emit report */ }, }); await tx.run(); ``` - [ ] **Step 4: 运行测试确认通过** Run: `npm run test -- tests/kernel/transition-transaction.test.ts` Expected: PASS - [ ] **Step 5: 提交** ```bash git add src/scene/TransitionTransaction.ts src/scene/SceneManager.ts tests/kernel/transition-transaction.test.ts git commit -m "feat(scene): add transition transaction with rollback flow" ``` --- ### Task 3: 实现 AssetGraph 与 AssetSession **Files:** - Create: `src/assets/AssetGraph.ts` - Create: `src/assets/AssetSession.ts` - Create: `src/assets/AssetInspectorSnapshot.ts` - Create: `tests/assets/asset-session.test.ts` - Modify: `src/core/AssetManager.ts` - Test: `tests/assets/asset-session.test.ts` - [ ] **Step 1: 写失败测试,验证 owner/session 引用追踪** ```ts import { describe, expect, it } from "vitest"; import { AssetSession } from "@/assets/AssetSession"; it("tracks owner and releases resources by session", async () => { const session = new AssetSession("scene:welcome", "tx:1"); await session.require("bundle.home"); const snap = session.snapshot(); expect(snap.owners["bundle.home"]).toContain("scene:welcome"); await session.releaseAll(); expect(session.snapshot().activeBundles).toHaveLength(0); }); ``` - [ ] **Step 2: 运行测试确认失败** Run: `npm run test -- tests/assets/asset-session.test.ts` Expected: FAIL with missing module `@/assets/AssetSession` - [ ] **Step 3: 写最小实现并改造旧 AssetManager 为门面** ```ts // src/assets/AssetSession.ts type Snapshot = { activeBundles: string[]; owners: Record }; export class AssetSession { private bundles = new Set(); private ownerMap = new Map>(); constructor( private readonly ownerId: string, private readonly transitionId: string ) {} async require(bundleName: string): Promise { this.bundles.add(bundleName); if (!this.ownerMap.has(bundleName)) this.ownerMap.set(bundleName, new Set()); this.ownerMap.get(bundleName)!.add(this.ownerId); } async releaseAll(): Promise { this.bundles.clear(); this.ownerMap.clear(); } snapshot(): Snapshot { const owners: Record = {}; for (const [k, v] of this.ownerMap) owners[k] = [...v]; return { activeBundles: [...this.bundles], owners }; } } ``` ```ts // src/core/AssetManager.ts (facade idea) import { AssetSession } from "@/assets/AssetSession"; export class AssetManagerFacade { createSession(ownerId: string, transitionId: string): AssetSession { return new AssetSession(ownerId, transitionId); } } ``` - [ ] **Step 4: 运行测试确认通过** Run: `npm run test -- tests/assets/asset-session.test.ts` Expected: PASS - [ ] **Step 5: 提交** ```bash git add src/assets/AssetGraph.ts src/assets/AssetSession.ts src/assets/AssetInspectorSnapshot.ts src/core/AssetManager.ts tests/assets/asset-session.test.ts git commit -m "feat(assets): add asset graph/session with owner tracking" ``` --- ### Task 4: 落地 AppRuntime 与事件总线插件化 **Files:** - Create: `src/kernel/AppRuntime.ts` - Create: `src/kernel/RuntimeEvents.ts` - Create: `src/kernel/RuntimePlugin.ts` - Create: `tests/kernel/runtime-events.test.ts` - Modify: `src/core/Game.ts` - Modify: `src/init.ts` - Test: `tests/kernel/runtime-events.test.ts` - [ ] **Step 1: 先写失败测试,验证事件会在切换时发出** ```ts import { describe, expect, it } from "vitest"; import { createRuntimeEvents } from "@/kernel/RuntimeEvents"; it("emits transition events in order", () => { const events = createRuntimeEvents(); const names: string[] = []; events.onAny((e) => names.push(e.type)); events.emit({ type: "transition:started", transitionId: "tx:1" }); events.emit({ type: "transition:finished", transitionId: "tx:1" }); expect(names).toEqual(["transition:started", "transition:finished"]); }); ``` - [ ] **Step 2: 运行测试确认失败** Run: `npm run test -- tests/kernel/runtime-events.test.ts` Expected: FAIL with missing module `@/kernel/RuntimeEvents` - [ ] **Step 3: 添加运行时与事件实现,并让旧 Game 仅做委托** ```ts // src/kernel/RuntimeEvents.ts type RuntimeEvent = { type: string; [k: string]: unknown }; type Handler = (event: RuntimeEvent) => void; export function createRuntimeEvents() { const handlers = new Set(); return { emit(event: RuntimeEvent) { handlers.forEach((h) => h(event)); }, onAny(handler: Handler) { handlers.add(handler); return () => handlers.delete(handler); }, }; } ``` ```ts // src/core/Game.ts (delegation snippet) import { AppRuntime } from "@/kernel/AppRuntime"; const runtime = new AppRuntime(); export default runtime; ``` - [ ] **Step 4: 运行核心测试集合确认通过** Run: `npm run test -- tests/kernel/runtime-events.test.ts tests/kernel/scene-state-machine.test.ts tests/kernel/transition-transaction.test.ts` Expected: PASS - [ ] **Step 5: 提交** ```bash git add src/kernel/AppRuntime.ts src/kernel/RuntimeEvents.ts src/kernel/RuntimePlugin.ts src/core/Game.ts src/init.ts tests/kernel/runtime-events.test.ts git commit -m "refactor(kernel): introduce app runtime and event bus plugin system" ``` --- ### Task 5: 交付最小 Debug Overlay + Command Palette **Files:** - Create: `src/devtools/overlay/DebugOverlay.ts` - Create: `src/devtools/overlay/widgets/SceneWidget.ts` - Create: `src/devtools/overlay/widgets/AssetWidget.ts` - Create: `src/devtools/overlay/widgets/EventWidget.ts` - Create: `src/devtools/CommandPalette.ts` - Create: `src/ui-core/UiPanel.ts` - Create: `src/ui-core/UiText.ts` - Create: `src/ui-core/UiButton.ts` - Create: `src/ui-core/UiList.ts` - Modify: `src/main.ts` - Test: 手工验证(见步骤) - [ ] **Step 1: 定义手工验收断言(固定文本,后续逐条打勾)** ```md 1. 按 `~` 能切换 overlay 显隐,且不影响场景点击事件。 2. overlay 中可看到当前 sceneId、sceneState、最近 20 条 runtime 事件。 3. overlay 中可看到 active bundle 数和 owner/session 计数。 4. 命令面板执行 `scene.goto(welcome)` 后,sceneState 在 2 秒内回到 ready。 ``` - [ ] **Step 2: 运行开发环境并确认当前不满足断言** Run: `npm run dev` Expected: 无 overlay / 无 palette(作为失败基线) - [ ] **Step 3: 实现最小 UI 与 devtools 装配** ```ts // src/devtools/overlay/DebugOverlay.ts export class DebugOverlay { private visible = true; toggle(): void { this.visible = !this.visible; } isVisible(): boolean { return this.visible; } update(): void { /* render widgets from runtime snapshots */ } } ``` ```ts // src/devtools/CommandPalette.ts type Command = { id: string; run: () => Promise | void }; export class CommandPalette { private cmds = new Map(); register(c: Command): void { this.cmds.set(c.id, c); } execute(id: string): Promise | void { return this.cmds.get(id)?.run(); } } ``` ```ts // src/main.ts (hook snippet) window.addEventListener("keydown", (e) => { if (e.key === "`") overlay.toggle(); }); ``` - [ ] **Step 4: 手工验证通过** Run: `npm run dev` Expected: - 按快捷键可开关 overlay - overlay 显示场景状态/事件列表/资源摘要 - 命令 `scene.goto(welcome)` 可执行并反映到状态 - [ ] **Step 5: 提交** ```bash git add src/devtools src/ui-core src/main.ts git commit -m "feat(devtools): add debug overlay and runtime command palette" ``` --- ### Task 6: 收口迁移与回归验证 **Files:** - Modify: `src/scene/BaseScene.ts` - Modify: `src/stages/page_init.ts` - Modify: `src/stages/initSceneLayout.ts` - Modify: `src/stages/welcome/page_welcome.ts` - Modify: `src/stages/welcome2/page_welcome2.ts` - Test: 全量测试 + 构建验证 - [ ] **Step 1: 写迁移校验测试(至少一个场景完整生命周期)** ```ts import { describe, expect, it } from "vitest"; import { SceneStateMachine } from "@/scene/SceneStateMachine"; it("completes full lifecycle for migrated scene", () => { const sm = new SceneStateMachine("welcome"); sm.transition("setup"); sm.transition("assetsLoading"); sm.transition("entering"); sm.transition("ready"); expect(sm.state).toBe("ready"); }); ``` - [ ] **Step 2: 运行该测试确认失败(迁移前不满足)** Run: `npm run test -- tests/kernel/scene-state-machine.test.ts` Expected: FAIL(若场景未按新生命周期接线) - [ ] **Step 3: 迁移场景到新接口并清理旧调用路径** ```ts // BaseScene new interface sketch export interface RuntimeScene { setup(ctx: SceneContext): Promise | void; enter(ctx: SceneContext): Promise | void; leave(ctx: SceneContext): Promise | void; dispose(ctx: SceneContext): Promise | void; } ``` - [ ] **Step 4: 运行全量验证** Run: `npm run test` Expected: PASS (all tests) Run: `npm run build` Expected: build success with generated dist assets - [ ] **Step 5: 提交** ```bash git add src/scene/BaseScene.ts src/stages tests package.json git commit -m "refactor(scene): migrate scenes to runtime lifecycle and finalize rollout" ``` --- ## Final Verification Checklist - [ ] 所有测试通过:`npm run test` - [ ] 构建通过:`npm run build` - [ ] 开发态工具可用:`npm run dev` + overlay/palette 手工检查 - [ ] 无遗留占位项(无“待补充/稍后实现”字样)