Browse Source
- Introduced SceneLoadingOverlay for improved user feedback during asset loading. - Updated SceneManager to manage loading overlay visibility and state reporting. - Refactored scene lifecycle methods to integrate loading overlay functionality. - Removed unused assets and scenes to streamline project structure. Made-with: Cursormaster
17 changed files with 2292 additions and 186 deletions
@ -0,0 +1,531 @@ |
|||||
|
# 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<SceneState, SceneState[]> = { |
||||
|
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<void>; |
||||
|
commit: () => Promise<void>; |
||||
|
rollback: () => Promise<void>; |
||||
|
finalize: () => Promise<void>; |
||||
|
} |
||||
|
|
||||
|
export class TransitionTransaction { |
||||
|
constructor(public readonly hooks: TransitionHooks) {} |
||||
|
async run(): Promise<void> { |
||||
|
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<string, string[]> }; |
||||
|
|
||||
|
export class AssetSession { |
||||
|
private bundles = new Set<string>(); |
||||
|
private ownerMap = new Map<string, Set<string>>(); |
||||
|
constructor( |
||||
|
private readonly ownerId: string, |
||||
|
private readonly transitionId: string |
||||
|
) {} |
||||
|
async require(bundleName: string): Promise<void> { |
||||
|
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<void> { |
||||
|
this.bundles.clear(); |
||||
|
this.ownerMap.clear(); |
||||
|
} |
||||
|
snapshot(): Snapshot { |
||||
|
const owners: Record<string, string[]> = {}; |
||||
|
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<Handler>(); |
||||
|
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> | void }; |
||||
|
export class CommandPalette { |
||||
|
private cmds = new Map<string, Command>(); |
||||
|
register(c: Command): void { this.cmds.set(c.id, c); } |
||||
|
execute(id: string): Promise<void> | 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> | void; |
||||
|
enter(ctx: SceneContext): Promise<void> | void; |
||||
|
leave(ctx: SceneContext): Promise<void> | void; |
||||
|
dispose(ctx: SceneContext): Promise<void> | 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 手工检查 |
||||
|
- [ ] 无遗留占位项(无“待补充/稍后实现”字样) |
||||
|
|
||||
File diff suppressed because it is too large
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 317 KiB |
@ -0,0 +1,165 @@ |
|||||
|
import Game from "@/core/Game"; |
||||
|
import { Container, Graphics, Text, TextStyle } from "pixi.js"; |
||||
|
|
||||
|
export interface SceneLoadingOverlayConfig { |
||||
|
enabled?: boolean; |
||||
|
minDisplayMs?: number; |
||||
|
maskAlpha?: number; |
||||
|
safeTop?: number; |
||||
|
safeBottom?: number; |
||||
|
safeHorizontal?: number; |
||||
|
panelWidth?: number; |
||||
|
panelHeight?: number; |
||||
|
title?: string; |
||||
|
loadingText?: string; |
||||
|
readyText?: string; |
||||
|
} |
||||
|
|
||||
|
export interface SceneLoadingOverlayState { |
||||
|
progress?: number; |
||||
|
text?: string; |
||||
|
} |
||||
|
|
||||
|
const DEFAULT_CONFIG: Required<SceneLoadingOverlayConfig> = { |
||||
|
enabled: true, |
||||
|
minDisplayMs: 350, |
||||
|
maskAlpha: 0.55, |
||||
|
safeTop: 24, |
||||
|
safeBottom: 34, |
||||
|
safeHorizontal: 16, |
||||
|
panelWidth: 460, |
||||
|
panelHeight: 138, |
||||
|
title: "资源加载中", |
||||
|
loadingText: "加载中", |
||||
|
readyText: "即将就绪", |
||||
|
}; |
||||
|
|
||||
|
export class SceneLoadingOverlay { |
||||
|
private readonly game = Game.getInstance(); |
||||
|
private readonly root = new Container(); |
||||
|
private readonly mask = new Graphics(); |
||||
|
private readonly panel = new Graphics(); |
||||
|
private readonly progressBg = new Graphics(); |
||||
|
private readonly progressFill = new Graphics(); |
||||
|
private readonly titleText = new Text(); |
||||
|
private readonly statusText = new Text(); |
||||
|
|
||||
|
private config: Required<SceneLoadingOverlayConfig> = DEFAULT_CONFIG; |
||||
|
private mounted = false; |
||||
|
private startedAt = 0; |
||||
|
private progress = 0; |
||||
|
|
||||
|
constructor() { |
||||
|
this.root.zIndex = 999999; |
||||
|
this.root.label = "scene-loading-overlay"; |
||||
|
|
||||
|
this.titleText.style = new TextStyle({ |
||||
|
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
||||
|
fontSize: 20, |
||||
|
fontWeight: "700", |
||||
|
fill: 0xffffff, |
||||
|
align: "center", |
||||
|
letterSpacing: 1, |
||||
|
}); |
||||
|
this.titleText.anchor.set(0.5); |
||||
|
|
||||
|
this.statusText.style = new TextStyle({ |
||||
|
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
||||
|
fontSize: 18, |
||||
|
fill: 0xd6ecff, |
||||
|
align: "center", |
||||
|
letterSpacing: 1, |
||||
|
}); |
||||
|
this.statusText.anchor.set(0.5); |
||||
|
|
||||
|
this.root.addChild( |
||||
|
this.mask, |
||||
|
this.panel, |
||||
|
this.progressBg, |
||||
|
this.progressFill, |
||||
|
this.titleText, |
||||
|
this.statusText |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
show(config?: SceneLoadingOverlayConfig): void { |
||||
|
this.config = { ...DEFAULT_CONFIG, ...(config ?? {}) }; |
||||
|
if (!this.config.enabled) { |
||||
|
return; |
||||
|
} |
||||
|
this.startedAt = performance.now(); |
||||
|
this.progress = 0; |
||||
|
this.titleText.text = this.config.title; |
||||
|
this.update({ progress: 0 }); |
||||
|
|
||||
|
if (!this.mounted) { |
||||
|
this.game.stage.addChild(this.root); |
||||
|
this.mounted = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
update(state?: SceneLoadingOverlayState): void { |
||||
|
if (!this.config.enabled || !this.mounted) { |
||||
|
return; |
||||
|
} |
||||
|
if (typeof state?.progress === "number") { |
||||
|
this.progress = Math.max(0, Math.min(1, state.progress)); |
||||
|
} |
||||
|
|
||||
|
const { width: W, height: H } = this.game.getInfo(); |
||||
|
const safeTop = Math.max(0, this.config.safeTop); |
||||
|
const safeBottom = Math.max(0, this.config.safeBottom); |
||||
|
const safeHorizontal = Math.max(0, this.config.safeHorizontal); |
||||
|
const usableW = Math.max(240, W - safeHorizontal * 2); |
||||
|
const usableH = Math.max(120, H - safeTop - safeBottom); |
||||
|
const panelW = Math.min(this.config.panelWidth, Math.max(280, usableW * 0.76)); |
||||
|
const panelH = this.config.panelHeight; |
||||
|
const panelX = (W - panelW) / 2; |
||||
|
const idealPanelY = safeTop + usableH * 0.5 - panelH / 2; |
||||
|
const panelY = Math.max(safeTop, Math.min(idealPanelY, H - safeBottom - panelH)); |
||||
|
const barW = panelW - 56; |
||||
|
const barH = 18; |
||||
|
const barX = panelX + 28; |
||||
|
const barY = panelY + 62; |
||||
|
const fillW = Math.max(10, barW * this.progress); |
||||
|
|
||||
|
this.mask.clear(); |
||||
|
this.mask.rect(0, 0, W, H); |
||||
|
this.mask.fill({ color: 0x04070d, alpha: this.config.maskAlpha }); |
||||
|
|
||||
|
this.panel.clear(); |
||||
|
this.panel.roundRect(panelX, panelY, panelW, panelH, 18); |
||||
|
this.panel.fill({ color: 0x0f1f35, alpha: 0.9 }); |
||||
|
this.panel.stroke({ color: 0x7fc0ff, alpha: 0.28, width: 2 }); |
||||
|
|
||||
|
this.progressBg.clear(); |
||||
|
this.progressBg.roundRect(barX, barY, barW, barH, 9); |
||||
|
this.progressBg.fill({ color: 0x28425f, alpha: 0.96 }); |
||||
|
|
||||
|
this.progressFill.clear(); |
||||
|
this.progressFill.roundRect(barX, barY, fillW, barH, 9); |
||||
|
this.progressFill.fill({ color: 0x66d9ff, alpha: 0.98 }); |
||||
|
|
||||
|
const percent = Math.round(this.progress * 100); |
||||
|
this.titleText.position.set(W / 2, panelY + 35); |
||||
|
this.statusText.position.set(W / 2, panelY + 102); |
||||
|
this.statusText.text = |
||||
|
state?.text ?? (percent >= 100 ? this.config.readyText : `${this.config.loadingText} ${percent}%`); |
||||
|
} |
||||
|
|
||||
|
async hide(): Promise<void> { |
||||
|
if (!this.config.enabled || !this.mounted) { |
||||
|
return; |
||||
|
} |
||||
|
const elapsed = performance.now() - this.startedAt; |
||||
|
if (elapsed < this.config.minDisplayMs) { |
||||
|
await new Promise((resolve) => setTimeout(resolve, this.config.minDisplayMs - elapsed)); |
||||
|
} |
||||
|
this.progress = 1; |
||||
|
this.update({ progress: 1, text: this.config.readyText }); |
||||
|
this.game.stage.removeChild(this.root); |
||||
|
this.mounted = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default SceneLoadingOverlay; |
||||
@ -1,15 +0,0 @@ |
|||||
/** |
|
||||
* Init 首屏在逻辑画布(Game.getInfo,与 designWidth 缩放一致)下的布局参数。 |
|
||||
* 分区:上区标题 → 中区角色 → 底区主按钮;改间距只改此处。 |
|
||||
*/ |
|
||||
export const initSceneLayout = { |
|
||||
titleYRatio: 0.12, |
|
||||
subtitleYRatio: 0.2, |
|
||||
heroYRatio: 0.46, |
|
||||
/** 主按钮相对底边的上移(逻辑像素,与 position.get 的 options.y 一致) */ |
|
||||
startCtaOffsetY: -110, |
|
||||
/** 首屏进入欢迎页时的切场景策略(对齐 scene transaction 参数) */ |
|
||||
nextSceneTransition: { |
|
||||
isHolderLast: false, |
|
||||
}, |
|
||||
} as const; |
|
||||
@ -1,15 +0,0 @@ |
|||||
import { Graphics} from "pixi.js" |
|
||||
|
|
||||
export default new (class Circle{ |
|
||||
render(){ |
|
||||
const circle = new Graphics(); |
|
||||
circle.label = "circle"; |
|
||||
circle.circle(0, 0, 32); |
|
||||
circle.fill(0xfb6a8f); |
|
||||
circle.x = 130; |
|
||||
circle.y = 130; |
|
||||
circle.interactive = true; |
|
||||
// circle.buttonMode = true;
|
|
||||
return circle |
|
||||
} |
|
||||
}) |
|
||||
@ -1,34 +0,0 @@ |
|||||
import Button from "@/components/Button"; |
|
||||
import { Container } from "pixi.js"; |
|
||||
import { BaseScene } from "@/scene/BaseScene"; |
|
||||
import { SceneType } from "@/enums/SceneType"; |
|
||||
import position from "@/utils/Position"; |
|
||||
|
|
||||
export default class WelcomeScene extends BaseScene { |
|
||||
stage: Container = new Container(); |
|
||||
private startBtn?: Button; |
|
||||
|
|
||||
constructor() { |
|
||||
super("welcome", SceneType.Normal); |
|
||||
} |
|
||||
|
|
||||
protected onSceneLayout(): void { |
|
||||
this.startBtn = new Button({ |
|
||||
text: "进入游戏", |
|
||||
onClick: () => { |
|
||||
this.changeScene("welcome2"); |
|
||||
}, |
|
||||
position: () => position.center(), |
|
||||
}); |
|
||||
|
|
||||
this.stage.addChild(this.startBtn.getView()); |
|
||||
} |
|
||||
|
|
||||
protected onSceneEnter(): void { |
|
||||
console.log("welcome onLoad"); |
|
||||
} |
|
||||
|
|
||||
protected onSceneExit(): void { |
|
||||
console.log("welcome onUnLoad"); |
|
||||
} |
|
||||
} |
|
||||
@ -1,46 +0,0 @@ |
|||||
import { BaseScene } from "@/scene/BaseScene"; |
|
||||
import { SceneType } from "@/enums/SceneType"; |
|
||||
import { Container, FederatedPointerEvent, Graphics } from "pixi.js"; |
|
||||
|
|
||||
export default class Welcome2Scene extends BaseScene { |
|
||||
stage = new Container(); |
|
||||
private circle?: Graphics; |
|
||||
private goWelcome = (_e: FederatedPointerEvent): void => { |
|
||||
if (!this.circle) { |
|
||||
return; |
|
||||
} |
|
||||
this.circle.x += 10; |
|
||||
this.changeScene("welcome"); |
|
||||
}; |
|
||||
|
|
||||
constructor() { |
|
||||
super("welcome2", SceneType.Normal); |
|
||||
} |
|
||||
|
|
||||
protected onSceneLayout(): void { |
|
||||
this.circle = new Graphics(); |
|
||||
this.circle.label = "circle"; |
|
||||
this.circle.circle(0, 0, 32); |
|
||||
this.circle.fill(0xfb6a8f); |
|
||||
this.circle.x = 130; |
|
||||
this.circle.y = 300; |
|
||||
this.circle.eventMode = "static"; |
|
||||
this.circle.cursor = "pointer"; |
|
||||
|
|
||||
this.circle.on("touchend", this.goWelcome); |
|
||||
this.circle.on("mousedown", this.goWelcome); |
|
||||
this.stage.addChild(this.circle); |
|
||||
} |
|
||||
|
|
||||
protected onSceneEnter(): void { |
|
||||
console.log("onLoad 2"); |
|
||||
} |
|
||||
|
|
||||
protected onSceneExit(): void { |
|
||||
if (this.circle) { |
|
||||
this.circle.off("touchend", this.goWelcome); |
|
||||
this.circle.off("mousedown", this.goWelcome); |
|
||||
} |
|
||||
console.log("onUnLoad 2"); |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue