diff --git a/docs/superpowers/plans/2026-04-26-pixi-dx-runtime-assets-redesign-plan.md b/docs/superpowers/plans/2026-04-26-pixi-dx-runtime-assets-redesign-plan.md new file mode 100644 index 0000000..22f0829 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-pixi-dx-runtime-assets-redesign-plan.md @@ -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 = { + 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 手工检查 +- [ ] 无遗留占位项(无“待补充/稍后实现”字样) + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1c35d76 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1363 @@ +{ + "name": "pixi-demo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pixi-demo", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@pixi/sound": "^6.0.1", + "@pixi/ui": "^2.1.2", + "@tweenjs/tween.js": "^25.0.0", + "pixi.js": "^8.2.5", + "vite": "^5.3.5", + "vite-tsconfig-paths": "^4.3.2" + }, + "devDependencies": { + "@pixi/devtools": "^2.0.1", + "typescript": "^5.5.4", + "vitest": "^2.1.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "license": "MIT" + }, + "node_modules/@pixi/devtools": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "peerDependencies": { + "pixi.js": "^7 || ^8" + } + }, + "node_modules/@pixi/sound": { + "version": "6.0.1", + "license": "MIT", + "peerDependencies": { + "pixi.js": "^8.0.0" + } + }, + "node_modules/@pixi/ui": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "tweedle.js": "^2.1.0", + "typed-signals": "^2.5.0" + }, + "peerDependencies": { + "pixi.js": "^8.0.2" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz", + "integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz", + "integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz", + "integrity": "sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz", + "integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz", + "integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz", + "integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz", + "integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz", + "integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz", + "integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz", + "integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz", + "integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.19.1", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.19.1", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz", + "integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz", + "integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz", + "integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "license": "MIT" + }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.12", + "license": "MIT" + }, + "node_modules/@types/earcut": { + "version": "2.1.4", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.44", + "license": "BSD-3-Clause" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/earcut": { + "version": "2.2.4", + "license": "ISC" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "license": "MIT" + }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "license": "ISC" + }, + "node_modules/pixi.js": { + "version": "8.2.5", + "dependencies": { + "@pixi/colord": "^2.9.6", + "@types/css-font-loading-module": "^0.0.12", + "@types/earcut": "^2.1.4", + "@webgpu/types": "^0.1.40", + "@xmldom/xmldom": "^0.8.10", + "earcut": "^2.2.4", + "eventemitter3": "^5.0.1", + "ismobilejs": "^1.1.1", + "parse-svg-path": "^0.1.2" + } + }, + "node_modules/postcss": { + "version": "8.4.39", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.19.1", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.19.1", + "@rollup/rollup-android-arm64": "4.19.1", + "@rollup/rollup-darwin-arm64": "4.19.1", + "@rollup/rollup-darwin-x64": "4.19.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.19.1", + "@rollup/rollup-linux-arm-musleabihf": "4.19.1", + "@rollup/rollup-linux-arm64-gnu": "4.19.1", + "@rollup/rollup-linux-arm64-musl": "4.19.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.19.1", + "@rollup/rollup-linux-riscv64-gnu": "4.19.1", + "@rollup/rollup-linux-s390x-gnu": "4.19.1", + "@rollup/rollup-linux-x64-gnu": "4.19.1", + "@rollup/rollup-linux-x64-musl": "4.19.1", + "@rollup/rollup-win32-arm64-msvc": "4.19.1", + "@rollup/rollup-win32-ia32-msvc": "4.19.1", + "@rollup/rollup-win32-x64-msvc": "4.19.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsconfck": { + "version": "3.1.1", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tweedle.js": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/typed-signals": { + "version": "2.5.0", + "license": "CC0-1.0" + }, + "node_modules/typescript": { + "version": "5.5.4", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.3.5", + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "4.3.2", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/public/bg.png b/public/bg.png deleted file mode 100644 index 7b0da65..0000000 Binary files a/public/bg.png and /dev/null differ diff --git a/public/controller_prompt_bg.png b/public/controller_prompt_bg.png deleted file mode 100644 index a55828a..0000000 Binary files a/public/controller_prompt_bg.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json index b48a360..6ef0840 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -4,14 +4,6 @@ "name":"load-screen", "assets":[ { - "alias":"cbg", - "src":"/controller_prompt_bg.png" - }, - { - "alias":"bg", - "src":"/bg.png" - }, - { "alias":"dnf", "src":"https://www.kkkk1000.com/images/learnPixiJS-AnimatedSprite/dnf.png" }, diff --git a/src/devtools/overlay/DebugOverlay.ts b/src/devtools/overlay/DebugOverlay.ts index 6d25cad..1d69951 100644 --- a/src/devtools/overlay/DebugOverlay.ts +++ b/src/devtools/overlay/DebugOverlay.ts @@ -19,8 +19,10 @@ export class DebugOverlay { private readonly assetWidget: AssetWidget; private readonly eventWidget: EventWidget; private readonly commandPalette = new CommandPalette(); + private readonly statusText = new UiButton("Ready", () => {}); private visible = false; private readonly offCallbacks: Array<() => void> = []; + private statusTimer: number | null = null; private readonly onWindowKeydown = (event: KeyboardEvent) => { if (event.defaultPrevented) { return; @@ -59,6 +61,9 @@ export class DebugOverlay { const refreshBtn = new UiButton("Refresh", () => this.refresh()); this.root.appendChild(refreshBtn.element); + this.statusText.element.style.cursor = "default"; + this.statusText.element.style.opacity = "0.9"; + this.root.appendChild(this.statusText.element); this.registerCommands(); this.bindKeys(); @@ -105,9 +110,20 @@ export class DebugOverlay { title: "Reload Current Scene", run: () => { const current = this.deps.sceneManager.currentScene; - if (current) { - void this.deps.sceneManager.changeScene(current.name); + if (!current) { + this.showStatus("No current scene", "warn"); + return; } + this.showStatus(`Reloading ${current.name}...`, "info"); + void this.deps.sceneManager + .changeScene(current.name) + .then(() => { + this.showStatus(`Reloaded ${current.name}`, "success"); + }) + .catch((error: unknown) => { + const msg = error instanceof Error ? error.message : String(error); + this.showStatus(`Reload failed: ${msg}`, "error"); + }); }, }); } @@ -118,6 +134,10 @@ export class DebugOverlay { off(); } this.offCallbacks.length = 0; + if (this.statusTimer !== null) { + window.clearTimeout(this.statusTimer); + this.statusTimer = null; + } this.eventWidget.dispose(); this.commandPalette.dispose(); this.root.remove(); @@ -128,6 +148,34 @@ export class DebugOverlay { this.assetWidget.render(); this.eventWidget.render(); } + + private showStatus( + text: string, + level: "info" | "success" | "warn" | "error" = "info" + ): void { + this.statusText.element.textContent = text; + const color = + level === "success" + ? "#22c55e" + : level === "warn" + ? "#f59e0b" + : level === "error" + ? "#ef4444" + : "#60a5fa"; + this.statusText.element.style.background = "rgba(15,23,42,0.75)"; + this.statusText.element.style.borderColor = color; + this.statusText.element.style.color = color; + + if (this.statusTimer !== null) { + window.clearTimeout(this.statusTimer); + } + this.statusTimer = window.setTimeout(() => { + this.statusText.element.textContent = "Ready"; + this.statusText.element.style.borderColor = "rgba(148,163,184,0.45)"; + this.statusText.element.style.color = "#f8fafc"; + this.statusTimer = null; + }, 2200); + } } function isEditableTarget(target: EventTarget | null): boolean { diff --git a/src/init.ts b/src/init.ts index 391ad04..8c595fd 100644 --- a/src/init.ts +++ b/src/init.ts @@ -36,6 +36,7 @@ export async function initApp(): Promise { await game.init(); const sceneModules = import.meta.glob("./stages/**/page_*.ts", { eager: true }); + const enabledSceneNames = new Set(["init", "00_global"]); for (const path in sceneModules) { try { @@ -43,6 +44,9 @@ export async function initApp(): Promise { const match = path.match(/page_(.*?)\.ts$/); if (!match) continue; const fileSceneName = match[1]; + if (!enabledSceneNames.has(fileSceneName)) { + continue; + } const raw = (mod as { default: unknown }).default; if (typeof raw !== "function") { diff --git a/src/scene/BaseScene.ts b/src/scene/BaseScene.ts index 8f4ed18..88a544f 100644 --- a/src/scene/BaseScene.ts +++ b/src/scene/BaseScene.ts @@ -1,5 +1,6 @@ import { Container, Ticker } from "pixi.js"; import { SceneType } from "@/enums/SceneType"; +import type { SceneLoadingOverlayConfig, SceneLoadingOverlayState } from "./SceneLoadingOverlay"; import type { IBaseScene } from "./types"; export abstract class BaseScene implements IBaseScene { @@ -9,6 +10,7 @@ export abstract class BaseScene implements IBaseScene { _assetsLoaded: boolean = false; _layoutDone: boolean = false; + _loadingReporter?: (state?: SceneLoadingOverlayState) => void; constructor(name: string, type: SceneType = SceneType.Normal) { this.name = name; @@ -31,6 +33,18 @@ export abstract class BaseScene implements IBaseScene { protected onSceneEnter(): Promise | void {} protected onSceneExit(): Promise | void {} + protected getSceneLoadingOverlayConfig(): SceneLoadingOverlayConfig | undefined { + return undefined; + } + + protected reportSceneLoading(state?: SceneLoadingOverlayState): void { + this._loadingReporter?.(state); + } + + getLoadingOverlayConfig(): SceneLoadingOverlayConfig | undefined { + return this.getSceneLoadingOverlayConfig(); + } + // 旧生命周期接口:与当前 runtime/transaction 对齐,内部桥接到新钩子 async loadBundle(): Promise { await this.onSceneLoadBundle(); diff --git a/src/scene/SceneLoadingOverlay.ts b/src/scene/SceneLoadingOverlay.ts new file mode 100644 index 0000000..5bddf8e --- /dev/null +++ b/src/scene/SceneLoadingOverlay.ts @@ -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 = { + 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 = 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 { + 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; diff --git a/src/scene/SceneManager.ts b/src/scene/SceneManager.ts index 598e4d4..7ba0140 100644 --- a/src/scene/SceneManager.ts +++ b/src/scene/SceneManager.ts @@ -3,6 +3,7 @@ import Game from "@/core/Game"; import { logger } from "@/core/Logger"; import { SceneType } from "@/enums/SceneType"; import { BaseScene } from "./BaseScene"; +import SceneLoadingOverlay from "./SceneLoadingOverlay"; import { TransitionTransaction } from "./TransitionTransaction"; import type { IBaseScene } from "./types"; @@ -30,6 +31,7 @@ class SceneManager { private scenes: Map = new Map(); private _currentScene: IBaseScene | null = null; private changeCallbacks: StageChangeCallback[] = []; + private loadingOverlay = new SceneLoadingOverlay(); private constructor() { this.game = Game.getInstance(); @@ -107,15 +109,7 @@ class SceneManager { scene.stage.visible = true; } - if (!scene._assetsLoaded) { - await scene.loadBundle?.(); - scene._assetsLoaded = true; - } - if (!scene._layoutDone) { - await scene.layout?.(); - scene._layoutDone = true; - } - await scene.onLoad?.(); + await this.runSceneEnterLifecycle(scene); await this.emitStageChange(scene, undefined); logger.debug(`SceneManager: initialized scene "${name}"`); @@ -134,6 +128,10 @@ class SceneManager { } const target = this.getSceneOrThrow(name); + if (target === previous) { + await this.reloadCurrentScene(previous); + return; + } let previousVisibleBefore = true; let previousMountedBefore = false; let targetVisibleBefore = true; @@ -179,17 +177,7 @@ class SceneManager { } // 生命周期 - if (!target._assetsLoaded) { - await target.loadBundle?.(); - target._assetsLoaded = true; - } - - if (!target._layoutDone) { - await target.layout?.(); - target._layoutDone = true; - } - - await target.onLoad?.(); + await this.runSceneEnterLifecycle(target); // 触发回调 await this.emitStageChange(target, previous); @@ -236,6 +224,41 @@ class SceneManager { await transaction.execute(); } + /** + * 同名切换视为「就地重载」:不能走普通切换销毁分支, + * 否则会把当前场景自己的 stage 销毁后再尝试进入,导致界面丢失。 + */ + private async reloadCurrentScene(scene: IBaseScene): Promise { + if (scene.type === SceneType.Normal) { + const oldStage = scene.stage; + if (this.game.stage.children.includes(oldStage)) { + this.game.stage.removeChild(oldStage); + } + oldStage.destroy({ children: true }); + + if (scene._assetsLoaded) { + await scene.unLoadBundle?.(); + scene._assetsLoaded = false; + } + await scene.onUnLoad?.(); + + const nextStage = new Container(); + nextStage.label = scene.name; + (scene as { stage: Container }).stage = nextStage; + scene._layoutDone = false; + + this.game.stage.addChild(nextStage); + } else { + scene.stage.visible = false; + await scene.onUnLoad?.(); + scene.stage.visible = true; + } + + await this.runSceneEnterLifecycle(scene); + await this.emitStageChange(scene, scene); + logger.debug(`SceneManager: reloaded scene "${scene.name}"`); + } + /** 获取已注册场景 */ getScene(name: string): IBaseScene | undefined { if (!name) { @@ -299,6 +322,43 @@ class SceneManager { return this.scenes.has(name); } + + private async runSceneEnterLifecycle(scene: IBaseScene): Promise { + const base = scene instanceof BaseScene ? scene : undefined; + const config = base?.getLoadingOverlayConfig?.(); + const enableOverlay = config?.enabled ?? true; + + if (enableOverlay) { + this.loadingOverlay.show(config); + base!._loadingReporter = (state) => { + this.loadingOverlay.update(state); + }; + } + + try { + if (!scene._assetsLoaded) { + await scene.loadBundle?.(); + scene._assetsLoaded = true; + } + if (!scene._layoutDone) { + await scene.layout?.(); + scene._layoutDone = true; + } + await scene.onLoad?.(); + if (enableOverlay) { + await this.loadingOverlay.hide(); + } + } catch (error) { + if (enableOverlay) { + await this.loadingOverlay.hide(); + } + throw error; + } finally { + if (base) { + base._loadingReporter = undefined; + } + } + } } export default SceneManager; diff --git a/src/stages/_global/page_00_global.ts b/src/stages/_global/page_00_global.ts index c4ea017..f728262 100644 --- a/src/stages/_global/page_00_global.ts +++ b/src/stages/_global/page_00_global.ts @@ -10,6 +10,10 @@ const GLOBAL_BGM_URL = "/bg.mp3"; export default class Global extends BaseScene { stage: Container = new Container(); + private bgmStarted = false; + private readonly resumeBgmOnInteraction = (): void => { + this.tryStartGlobalBgm(); + }; constructor() { super("00_global", SceneType.Resident); @@ -49,12 +53,37 @@ export default class Global extends BaseScene { // this.stage.addChild(this.btn.getView()); } - onLoad(): void { + async onLoad(): Promise { logger.info("Resident scene 00_global onLoad"); - soundManager.play(GLOBAL_BGM_ALIAS, { loop: true, volume: 0.4 }); + this.bindAutoPlayGuards(); + this.tryStartGlobalBgm(); } - onUnLoad(): void { + async onUnLoad(): Promise { + this.unbindAutoPlayGuards(); soundManager.stop(GLOBAL_BGM_ALIAS); + this.bgmStarted = false; + } + + private tryStartGlobalBgm(): void { + if (this.bgmStarted) { + return; + } + soundManager.play(GLOBAL_BGM_ALIAS, { loop: true, volume: 0.4 }); + this.bgmStarted = true; + this.unbindAutoPlayGuards(); + } + + private bindAutoPlayGuards(): void { + const opts: AddEventListenerOptions = { passive: true }; + window.addEventListener("pointerdown", this.resumeBgmOnInteraction, opts); + window.addEventListener("keydown", this.resumeBgmOnInteraction); + window.addEventListener("touchstart", this.resumeBgmOnInteraction, opts); + } + + private unbindAutoPlayGuards(): void { + window.removeEventListener("pointerdown", this.resumeBgmOnInteraction); + window.removeEventListener("keydown", this.resumeBgmOnInteraction); + window.removeEventListener("touchstart", this.resumeBgmOnInteraction); } } diff --git a/src/stages/initSceneLayout.ts b/src/stages/initSceneLayout.ts deleted file mode 100644 index d5bb3f8..0000000 --- a/src/stages/initSceneLayout.ts +++ /dev/null @@ -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; diff --git a/src/stages/page_init.ts b/src/stages/page_init.ts index 7a85cb7..695e5a6 100644 --- a/src/stages/page_init.ts +++ b/src/stages/page_init.ts @@ -3,34 +3,35 @@ import { assetManager } from "@/core/AssetManager"; import Game from "@/core/Game"; import { BaseScene } from "@/scene/BaseScene"; import { SceneType } from "@/enums/SceneType"; -import { initSceneLayout } from "@/stages/initSceneLayout"; import position from "@/utils/Position"; import { AnimatedSprite, Container, Rectangle, - Sprite, Text, TextStyle, Texture, Ticker, } from "pixi.js"; -const Z_BG = 0; const Z_HERO = 2; const Z_TEXT = 3; +const INIT_LAYOUT = { + titleYRatio: 0.12, + subtitleYRatio: 0.2, + heroYRatio: 0.46, + startCtaOffsetY: -110, +} as const; export default class InitScene extends BaseScene { stage = new Container(); private game = Game.getInstance(); private assets: Record = {} as Record; - private bg?: Sprite; private titleText?: Text; private subtitleText?: Text; private pixie?: AnimatedSprite; private startBtn?: Button; - private loadingBtn?: Button; private readonly onResize = (): void => { this.placeHeroElements(); @@ -41,29 +42,13 @@ export default class InitScene extends BaseScene { } protected async onSceneLoadBundle(): Promise { - this.loadingBtn = new Button({ - text: "加载中…", - position: () => position.get("center", "center"), - onClick: () => {}, - fontSize: 22, - padding: { x: 48, y: 28 }, - }); - this.loadingBtn._comp.zIndex = 100; - this.stage.addChild(this.loadingBtn.getView()); - const bundle = await assetManager.loadBundle("load-screen", (progress: number) => { - this.loadingBtn!.textObj.text = - progress >= 1 ? "即将就绪" : `加载中 ${Math.round(progress * 100)}%`; + this.reportSceneLoading({ progress }); }); - // 延迟3秒 - await new Promise(resolve => setTimeout(resolve, 3000)); if (bundle) { this.assets = bundle as unknown as Record; } - - this.stage.removeChild(this.loadingBtn._comp); - this.loadingBtn = undefined; } protected async onSceneUnloadBundle(): Promise { @@ -74,13 +59,6 @@ export default class InitScene extends BaseScene { this.stage.sortableChildren = true; this.stage.eventMode = "passive"; - const texBg = this.assets["bg"] ?? this.assets["cbg"]; - if (texBg) { - this.bg = Sprite.from(texBg); - this.bg.zIndex = Z_BG; - this.stage.addChild(this.bg); - } - const titleStyle = new TextStyle({ fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", fontSize: 44, @@ -145,11 +123,11 @@ export default class InitScene extends BaseScene { padding: { x: 72, y: 36 }, position: () => position.get("center", "bottom", { - y: initSceneLayout.startCtaOffsetY, + y: INIT_LAYOUT.startCtaOffsetY, x: 0, }), onClick: () => { - this.changeScene("welcome", initSceneLayout.nextSceneTransition); + // 当前版本仅保留开始界面,先保留按钮交互占位。 }, }); this.startBtn._comp.zIndex = Z_TEXT; @@ -171,19 +149,19 @@ export default class InitScene extends BaseScene { this.pixie?.update(ticker); } + protected getSceneLoadingOverlayConfig() { + return { + title: "初始化资源中", + loadingText: "正在准备", + minDisplayMs: 420, + safeTop: 28, + safeBottom: 42, + }; + } + private placeHeroElements(): void { - const { width: W, height: H } = this.game.getInfo(); - const L = initSceneLayout; - - /* 全屏层:背景 cover */ - if (this.bg) { - const tw = this.bg.texture.width; - const th = this.bg.texture.height; - const scale = Math.max(W / tw, H / th); - this.bg.setSize(tw * scale, th * scale); - this.bg.anchor.set(0.5); - this.bg.position.set(W / 2, H / 2); - } + const { height: H } = this.game.getInfo(); + const L = INIT_LAYOUT; /* 上区:标题 / 副标题(锚点中心,与 position 语义一致) */ if (this.titleText) { diff --git a/src/stages/welcome/circle.ts b/src/stages/welcome/circle.ts deleted file mode 100644 index a9b7d63..0000000 --- a/src/stages/welcome/circle.ts +++ /dev/null @@ -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 - } -}) \ No newline at end of file diff --git a/src/stages/welcome/page_welcome.ts b/src/stages/welcome/page_welcome.ts deleted file mode 100644 index 000ab91..0000000 --- a/src/stages/welcome/page_welcome.ts +++ /dev/null @@ -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"); - } -} \ No newline at end of file diff --git a/src/stages/welcome2/page_welcome2.ts b/src/stages/welcome2/page_welcome2.ts deleted file mode 100644 index c043846..0000000 --- a/src/stages/welcome2/page_welcome2.ts +++ /dev/null @@ -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"); - } -} diff --git a/tests/kernel/transition-transaction.test.ts b/tests/kernel/transition-transaction.test.ts index 1b3e700..8d6652d 100644 --- a/tests/kernel/transition-transaction.test.ts +++ b/tests/kernel/transition-transaction.test.ts @@ -152,4 +152,36 @@ describe("SceneManager rollback safety", () => { expect(stage.children.includes(previous.stage as never)).toBe(true); expect(stage.children.includes(target.stage as never)).toBe(false); }); + + it("reloads current scene without leaving stage empty", async () => { + const module = await import("@/scene/SceneManager"); + const SceneManager = module.default; + const manager = SceneManager.getInstance(); + + const current = createScene("same", SceneType.Normal, { + _assetsLoaded: true, + _layoutDone: true, + onUnLoad: vi.fn(async () => {}), + unLoadBundle: vi.fn(async () => {}), + loadBundle: vi.fn(async () => {}), + layout: vi.fn(async () => {}), + onLoad: vi.fn(async function (this: IBaseScene) { + (this.stage as unknown as MockContainer).addChild(new MockContainer()); + }), + }); + + manager.registerScene(current); + await manager.initScene("same"); + const beforeStage = current.stage; + + await manager.changeScene("same"); + + const afterStage = current.stage as unknown as MockContainer; + const rootStage = manager["game"].stage as unknown as MockContainer; + + expect(manager.currentScene).toBe(current); + expect(afterStage).not.toBe(beforeStage); + expect(afterStage.children.length).toBeGreaterThan(0); + expect(rootStage.children.includes(afterStage as never)).toBe(true); + }); });