diff --git a/eslint.config.js b/eslint.config.js index 5d57925..88b51a6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,7 +28,7 @@ export default tseslint.config( files: ["**/*.ts", "**/*.tsx"], rules: { // --- Project Conventions --- - quotes: ["error", "single", { avoidEscape: true }], + quotes: ["error", "double", { avoidEscape: true }], "comma-dangle": ["error", "always-multiline"], indent: ["error", 2, { SwitchCase: 1 }], @@ -68,7 +68,6 @@ export default tseslint.config( "import/no-duplicates": "error", // --- General --- - "no-console": "warn", "prefer-const": "error", "no-var": "error", eqeqeq: ["error", "always"], diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index 7b780f9..1e36f26 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -1,19 +1,38 @@ // Resource management -export { DisposableBag } from './utils'; -export type { IDisposable, DisposableItem } from './utils'; +export { DisposableBag } from "./utils"; +export type { IDisposable, DisposableItem } from "./utils"; // Drag & drop utilities -export { dragDropEventEffect, DragDropEventType } from './utils'; -export type { DragDropEvent } from './utils'; +export { dragDropEventEffect, DragDropEventType } from "./utils"; +export type { DragDropEvent } from "./utils"; // Data-driven object spawning -export { spawnEffect } from './spawner'; -export type { Spawner } from './spawner'; +export { spawnEffect } from "./spawner"; +export type { Spawner } from "./spawner"; // Scene base classes -export { ReactiveScene, GameHostScene, FadeScene, FADE_SCENE_KEY } from './scenes'; -export type { ReactiveSceneOptions, ReactiveScenePhaserData, SceneController, GameHostSceneOptions, FadeSceneData } from './scenes'; +export { + ReactiveScene, + GameHostScene, + FadeScene, + FADE_SCENE_KEY, +} from "./scenes"; +export type { + ReactiveSceneOptions, + ReactiveScenePhaserData, + SceneController, + GameHostSceneOptions, + FadeSceneData, +} from "./scenes"; // React ↔ Phaser bridge -export { PhaserGame, PhaserScene, phaserContext, defaultPhaserConfig, GameUI, type PhaserGameContext, type SceneController as PhaserSceneController } from './ui'; -export type { PhaserGameProps, PhaserSceneProps, GameUIOptions } from './ui'; +export { + PhaserGame, + PhaserScene, + phaserContext, + defaultPhaserConfig, + GameUI, + type PhaserGameContext, + type SceneController as PhaserSceneController, +} from "./ui"; +export type { PhaserGameProps, PhaserSceneProps, GameUIOptions } from "./ui"; diff --git a/packages/framework/src/scenes/FadeScene.ts b/packages/framework/src/scenes/FadeScene.ts index e886699..b0dafb9 100644 --- a/packages/framework/src/scenes/FadeScene.ts +++ b/packages/framework/src/scenes/FadeScene.ts @@ -1,89 +1,83 @@ -import Phaser from 'phaser'; -import { ReactiveScene, type ReactiveScenePhaserData } from './ReactiveScene'; +import { ReactiveScene } from "./ReactiveScene"; export interface FadeSceneData { - [key: string]: unknown; + [key: string]: unknown; } /** * 处理淡入淡出到黑色的过渡场景 */ export class FadeScene extends ReactiveScene { - private overlay!: Phaser.GameObjects.Rectangle; - private isFading = false; + private overlay!: Phaser.GameObjects.Rectangle; + private isFading = false; - constructor() { - super(FADE_SCENE_KEY); + constructor() { + super(FADE_SCENE_KEY); + } + + create(): void { + super.create(); + + // 创建黑色遮罩层,覆盖整个游戏区域 + const game = this.game; + this.overlay = this.add + .rectangle(0, 0, game.scale.width, game.scale.height, 0x000000, 1) + .setOrigin(0) + .setAlpha(1) + .setDepth(999999) + .setInteractive({ useHandCursor: false }); + + // 防止遮罩阻挡输入 + this.overlay.disableInteractive(); + } + + /** + * 淡入(从黑色到透明) + * @param duration 动画时长(毫秒) + */ + fadeIn(duration = 300): Promise { + return this.fadeTo(0, duration); + } + + /** + * 淡出(从透明到黑色) + * @param duration 动画时长(毫秒) + */ + fadeOut(duration = 300): Promise { + return this.fadeTo(1, duration); + } + + /** + * 淡入淡出到指定透明度 + */ + private fadeTo(targetAlpha: number, duration: number): Promise { + // 如果 overlay 还未初始化,直接返回 resolved promise + if (!this.overlay) { + console.warn("FadeScene: overlay 未初始化,跳过过渡动画"); + return Promise.resolve(); } - create(): void { - super.create(); - - // 创建黑色遮罩层,覆盖整个游戏区域 - const game = this.game; - this.overlay = this.add.rectangle( - 0, - 0, - game.scale.width, - game.scale.height, - 0x000000, - 1 - ).setOrigin(0) - .setAlpha(1) - .setDepth(999999) - .setInteractive({ useHandCursor: false }); - - // 防止遮罩阻挡输入 - this.overlay.disableInteractive(); + if (this.isFading) { + console.warn("FadeScene: 正在进行过渡动画"); } - /** - * 淡入(从黑色到透明) - * @param duration 动画时长(毫秒) - */ - fadeIn(duration = 300): Promise { - return this.fadeTo(0, duration); - } + this.isFading = true; + this.overlay.setAlpha(targetAlpha === 1 ? 0 : 1); - /** - * 淡出(从透明到黑色) - * @param duration 动画时长(毫秒) - */ - fadeOut(duration = 300): Promise { - return this.fadeTo(1, duration); - } - - /** - * 淡入淡出到指定透明度 - */ - private fadeTo(targetAlpha: number, duration: number): Promise { - // 如果 overlay 还未初始化,直接返回 resolved promise - if (!this.overlay) { - console.warn('FadeScene: overlay 未初始化,跳过过渡动画'); - return Promise.resolve(); - } - - if (this.isFading) { - console.warn('FadeScene: 正在进行过渡动画'); - } - - this.isFading = true; - this.overlay.setAlpha(targetAlpha === 1 ? 0 : 1); - - return new Promise((resolve) => { - this.tweens.add({ - targets: this.overlay, - alpha: targetAlpha, - duration, - ease: 'Linear', - onComplete: () => { - this.isFading = false; - resolve(); - }, - }); - }); - } + return new Promise((resolve) => { + this.tweens.add({ + targets: this.overlay, + alpha: targetAlpha, + duration, + ease: "Linear", + onComplete: () => { + this.isFading = false; + resolve(); + }, + }); + }); + } } // 导出常量供 PhaserGame 使用 -export const FADE_SCENE_KEY = '__fade__'; +export const FADE_SCENE_KEY = "__fade__"; diff --git a/packages/framework/src/scenes/GameHostScene.ts b/packages/framework/src/scenes/GameHostScene.ts index 1c51959..263d819 100644 --- a/packages/framework/src/scenes/GameHostScene.ts +++ b/packages/framework/src/scenes/GameHostScene.ts @@ -1,36 +1,37 @@ -import type { GameHost } from 'boardgame-core'; -import { ReactiveScene } from './ReactiveScene'; +import { ReactiveScene } from "./ReactiveScene"; + +import type { GameHost } from "boardgame-core"; export interface GameHostSceneOptions> { - gameHost: GameHost; - [key: string]: unknown; + gameHost: GameHost; + [key: string]: unknown; } -export abstract class GameHostScene> - extends ReactiveScene> -{ - public get gameHost(): GameHost { - const gameHost = this.initData.gameHost as GameHost; - if (!gameHost) { - throw new Error( - `GameHostScene (${this.scene.key}): gameHost 未提供。` + - `确保在 PhaserScene 组件的 data 属性中传入 gameHost。` - ); - } - return gameHost; +export abstract class GameHostScene< + TState extends Record, +> extends ReactiveScene> { + public get gameHost(): GameHost { + const gameHost = this.initData.gameHost as GameHost; + if (!gameHost) { + throw new Error( + `GameHostScene (${this.scene.key}): gameHost 未提供。` + + "确保在 PhaserScene 组件的 data 属性中传入 gameHost。", + ); } + return gameHost; + } - public get state(): TState { - return this.gameHost?.state.value; - } + public get state(): TState { + return this.gameHost?.state.value; + } - addInterruption(promise: Promise){ - this.gameHost?.addInterruption(promise); - } + addInterruption(promise: Promise) { + this.gameHost?.addInterruption(promise); + } - addTweenInterruption(tween: Phaser.Tweens.Tween){ - this.addInterruption(new Promise( - resolve => tween.once('complete', resolve) - )); - } + addTweenInterruption(tween: Phaser.Tweens.Tween) { + this.addInterruption( + new Promise((resolve) => tween.once("complete", resolve)), + ); + } } diff --git a/packages/framework/src/scenes/ReactiveScene.ts b/packages/framework/src/scenes/ReactiveScene.ts index 1989e10..25abe0a 100644 --- a/packages/framework/src/scenes/ReactiveScene.ts +++ b/packages/framework/src/scenes/ReactiveScene.ts @@ -1,87 +1,90 @@ -import Phaser from 'phaser'; -import { effect, type ReadonlySignal } from '@preact/signals-core'; -import { DisposableBag, type IDisposable } from '../utils'; +import { effect, type ReadonlySignal } from "@preact/signals-core"; +import { Scene } from "phaser"; + +import { DisposableBag, type IDisposable } from "../utils"; + +import type Phaser from "phaser"; type CleanupFn = void | (() => void); // 前向声明,避免循环导入 export interface SceneController { - launch(sceneKey: string): Promise; - restart(): Promise; - currentScene: ReadonlySignal; - isTransitioning: ReadonlySignal; + launch(sceneKey: string): Promise; + restart(): Promise; + currentScene: ReadonlySignal; + isTransitioning: ReadonlySignal; } export interface ReactiveScenePhaserData { - phaserGame: ReadonlySignal<{ game: Phaser.Game }>; - sceneController: SceneController; + phaserGame: ReadonlySignal<{ game: Phaser.Game }>; + sceneController: SceneController; } -export interface ReactiveSceneOptions = {}> { - key?: string; +export interface ReactiveSceneOptions { + key?: string; } /** * 通用的响应式 Scene 基类 * @typeparam TData - 通过 init(data) 接收的数据类型(必须包含 phaserGame 和 sceneController) */ -export abstract class ReactiveScene = {}> - extends Phaser.Scene - implements IDisposable +export abstract class ReactiveScene + extends Scene + implements IDisposable { - protected disposables = new DisposableBag(); - private _initData?: TData & ReactiveScenePhaserData; + protected disposables = new DisposableBag(); + private _initData?: TData & ReactiveScenePhaserData; - /** - * 获取通过 init() 注入的数据 - * 在 create() 阶段保证可用 - */ - public get initData(): TData & ReactiveScenePhaserData { - if (!this._initData) { - throw new Error( - `ReactiveScene (${this.scene.key}): initData 尚未初始化。` + - `确保场景通过 PhaserScene 组件注册,并在 create() 阶段访问。` - ); - } - return this._initData; + /** + * 获取通过 init() 注入的数据 + * 在 create() 阶段保证可用 + */ + public get initData(): TData & ReactiveScenePhaserData { + if (!this._initData) { + throw new Error( + `ReactiveScene (${this.scene.key}): initData 尚未初始化。` + + "确保场景通过 PhaserScene 组件注册,并在 create() 阶段访问。", + ); } + return this._initData; + } - /** - * 获取 Phaser game 实例的响应式信号 - */ - public get phaserGame(): ReadonlySignal<{ game: Phaser.Game }> { - return this.initData.phaserGame; - } + /** + * 获取 Phaser game 实例的响应式信号 + */ + public get phaserGame(): ReadonlySignal<{ game: Phaser.Game }> { + return this.initData.phaserGame; + } - /** - * 获取场景控制器 - */ - public get sceneController(): SceneController { - return this.initData.sceneController; - } + /** + * 获取场景控制器 + */ + public get sceneController(): SceneController { + return this.initData.sceneController; + } - constructor(key?: string) { - super(key); - } + constructor(key?: string) { + super(key); + } - init(data: TData & ReactiveScenePhaserData): void { - this._initData = data; - } + init(data: TData & ReactiveScenePhaserData): void { + this._initData = data; + } - create(): void { - this.events.on('shutdown', this.dispose, this); - } + create(): void { + this.events.on("shutdown", this.dispose, this); + } - dispose(): void { - this.disposables.dispose(); - } + dispose(): void { + this.disposables.dispose(); + } - public addDisposable(disposable: IDisposable): void { - this.disposables.add(disposable); - } + public addDisposable(disposable: IDisposable): void { + this.disposables.add(disposable); + } - /** 注册响应式监听(场景关闭时自动清理) */ - public addEffect(fn: () => CleanupFn): void { - this.disposables.add(effect(fn)); - } + /** 注册响应式监听(场景关闭时自动清理) */ + public addEffect(fn: () => CleanupFn): void { + this.disposables.add(effect(fn)); + } } diff --git a/packages/framework/src/scenes/index.ts b/packages/framework/src/scenes/index.ts index c7e004f..5eb51a0 100644 --- a/packages/framework/src/scenes/index.ts +++ b/packages/framework/src/scenes/index.ts @@ -1,8 +1,12 @@ -export { ReactiveScene } from './ReactiveScene'; -export type { ReactiveSceneOptions, ReactiveScenePhaserData, SceneController } from './ReactiveScene'; +export { ReactiveScene } from "./ReactiveScene"; +export type { + ReactiveSceneOptions, + ReactiveScenePhaserData, + SceneController, +} from "./ReactiveScene"; -export { FadeScene, FADE_SCENE_KEY } from './FadeScene'; -export type { FadeSceneData } from './FadeScene'; +export { FadeScene, FADE_SCENE_KEY } from "./FadeScene"; +export type { FadeSceneData } from "./FadeScene"; -export { GameHostScene } from './GameHostScene'; -export type { GameHostSceneOptions } from './GameHostScene'; +export { GameHostScene } from "./GameHostScene"; +export type { GameHostSceneOptions } from "./GameHostScene"; diff --git a/packages/framework/src/ui/GameUI.tsx b/packages/framework/src/ui/GameUI.tsx index 8daa512..d9e0161 100644 --- a/packages/framework/src/ui/GameUI.tsx +++ b/packages/framework/src/ui/GameUI.tsx @@ -1,11 +1,11 @@ export interface GameUIOptions { container: HTMLElement; - root: any; + root: HTMLElement; } export class GameUI { private container: HTMLElement; - private root: any; + private root: HTMLElement; constructor(options: GameUIOptions) { this.container = options.container; @@ -13,13 +13,13 @@ export class GameUI { } mount(): void { - import('preact').then(({ render }) => { + import("preact").then(({ render }) => { render(this.root, this.container); }); } unmount(): void { - import('preact').then(({ render }) => { + import("preact").then(({ render }) => { render(null, this.container); }); } diff --git a/packages/framework/src/ui/PhaserBridge.tsx b/packages/framework/src/ui/PhaserBridge.tsx index af06895..9a643ad 100644 --- a/packages/framework/src/ui/PhaserBridge.tsx +++ b/packages/framework/src/ui/PhaserBridge.tsx @@ -1,219 +1,243 @@ -import Phaser from 'phaser'; import { signal, useSignal, useSignalEffect } from '@preact/signals'; -import { createContext, h } from 'preact'; +import Phaser, { AUTO } from 'phaser'; +import { createContext } from 'preact'; import { useContext, useEffect, useRef } from 'preact/hooks'; -import { ReadonlySignal } from "@preact/signals-core"; + +import { + FadeScene as FadeSceneClass, + FADE_SCENE_KEY, +} from '../scenes/FadeScene'; + import type { ReactiveScene } from '../scenes'; -import { FadeScene as FadeSceneClass, FADE_SCENE_KEY } from '../scenes/FadeScene'; +import type { ReadonlySignal } from '@preact/signals-core'; export interface SceneController { - /** 启动场景(带淡入淡出过渡) */ - launch(sceneKey: string): Promise; - /** 重新启动当前场景(带淡入淡出过渡) */ - restart(): Promise; - /** 当前活跃场景 key */ - currentScene: ReadonlySignal; - /** 是否正在过渡 */ - isTransitioning: ReadonlySignal; + /** 启动场景(带淡入淡出过渡) */ + launch(sceneKey: string): Promise; + /** 重新启动当前场景(带淡入淡出过渡) */ + restart(): Promise; + /** 当前活跃场景 key */ + currentScene: ReadonlySignal; + /** 是否正在过渡 */ + isTransitioning: ReadonlySignal; } export interface PhaserGameContext { - game: Phaser.Game; - sceneController: SceneController; + game: Phaser.Game; + sceneController: SceneController; } -export const phaserContext = createContext | null>(null); +export const phaserContext = + createContext | null>(null); export const defaultPhaserConfig: Phaser.Types.Core.GameConfig = { - type: Phaser.AUTO, - width: 560, - height: 560, - parent: 'phaser-container', - backgroundColor: '#f9fafb', - scene: [], + type: AUTO, + width: 560, + height: 560, + parent: 'phaser-container', + backgroundColor: '#f9fafb', + scene: [], }; export interface PhaserGameProps { - config?: Partial; - /** 初始启动的场景 key */ - initialScene?: string; - children?: any; + config?: Partial; + /** 初始启动的场景 key */ + initialScene?: string; + children?: preact.ComponentChildren; } export function PhaserGame(props: PhaserGameProps) { - const gameSignal = useSignal({ game: undefined!, sceneController: undefined! }); - const initialSceneLaunched = useRef(false); + const gameSignal = useSignal({ + game: undefined!, + sceneController: undefined!, + }); + const initialSceneLaunched = useRef(false); - useSignalEffect(() => { - const config: Phaser.Types.Core.GameConfig = { - ...defaultPhaserConfig, - ...props.config, - }; - const phaserGame = new Phaser.Game(config); + useSignalEffect(() => { + const config: Phaser.Types.Core.GameConfig = { + ...defaultPhaserConfig, + ...props.config, + }; + const phaserGame = new Phaser.Game(config); - // 添加 FadeScene 并启动它来初始化 overlay - const fadeScene = new FadeSceneClass(); - phaserGame.scene.add(FADE_SCENE_KEY, fadeScene, true); // 改为 true 以触发 create + // 添加 FadeScene 并启动它来初始化 overlay + const fadeScene = new FadeSceneClass(); + phaserGame.scene.add(FADE_SCENE_KEY, fadeScene, true); // 改为 true 以触发 create - // 创建 SceneController - const currentScene = signal(null); - const isTransitioning = signal(false); + // 创建 SceneController + const currentScene = signal(null); + const isTransitioning = signal(false); - const sceneController: SceneController = { - async launch(sceneKey: string) { - if (isTransitioning.value) { - console.warn('SceneController: 正在进行场景切换'); - return; - } - - // 等待场景注册完成(最多等待 100ms) - let retries = 0; - while (!phaserGame.scene.getScene(sceneKey) && retries < 10) { - await new Promise(resolve => setTimeout(resolve, 10)); - retries++; - } - - // 验证场景是否已注册 - if (!phaserGame.scene.getScene(sceneKey)) { - console.error(`SceneController: 场景 "${sceneKey}" 未注册`); - return; - } - - isTransitioning.value = true; - const fade = phaserGame.scene.getScene(FADE_SCENE_KEY) as FadeSceneClass; - - // 淡出到黑色 - phaserGame.scene.bringToTop(FADE_SCENE_KEY); - await fade.fadeOut(300); - - // 停止当前场景 - if (currentScene.value) { - phaserGame.scene.stop(currentScene.value); - } - - // 确保场景已注册后再启动 - // (场景应该已经在 PhaserScene 组件中注册) - if (!phaserGame.scene.getScene(sceneKey)) { - console.error(`SceneController: 场景 "${sceneKey}" 在切换时仍未注册`); - isTransitioning.value = false; - return; - } - - // 启动新场景 - phaserGame.scene.start(sceneKey); - currentScene.value = sceneKey; - - // 淡入 - await fade.fadeIn(300); - isTransitioning.value = false; - }, - async restart() { - if (isTransitioning.value) { - console.warn('SceneController: 正在进行场景切换'); - return; - } - - if (!currentScene.value) { - console.warn('SceneController: 没有当前场景,无法 restart'); - return; - } - - const sceneKey = currentScene.value; - const scene = phaserGame.scene.getScene(sceneKey); - - if (!scene) { - console.error(`SceneController: 场景 "${sceneKey}" 不存在`); - return; - } - - isTransitioning.value = true; - const fade = phaserGame.scene.getScene(FADE_SCENE_KEY) as FadeSceneClass; - - // 淡出到黑色 - phaserGame.scene.bringToTop(FADE_SCENE_KEY); - await fade.fadeOut(300); - - // 重启当前场景 - scene.scene.restart(); - - // 淡入 - await fade.fadeIn(300); - isTransitioning.value = false; - }, - currentScene, - isTransitioning, - }; - - gameSignal.value = { game: phaserGame, sceneController }; - - return () => { - gameSignal.value = { game: undefined!, sceneController: undefined! }; - initialSceneLaunched.current = false; - phaserGame.destroy(true); - }; - }); - - // 启动初始场景(仅一次) - useEffect(() => { - const ctx = gameSignal.value; - if (!initialSceneLaunched.current && props.initialScene && ctx?.sceneController) { - initialSceneLaunched.current = true; - // 使用 microtask 确保所有子组件的场景注册已完成 - Promise.resolve().then(() => { - ctx.sceneController.launch(props.initialScene!); - }); - } - }, [gameSignal.value, props.initialScene]); - - return ( -
- - {props.children} - -
- ); -} - -export interface PhaserSceneProps = {}> { - sceneKey: string; - scene: ReactiveScene; - data?: TData; - children?: any; -} - -export const phaserSceneContext = createContext | null>(null); - -export function PhaserScene = {}>(props: PhaserSceneProps) { - const phaserGameSignal = useContext(phaserContext); - const sceneSignal = useSignal>(); - const registered = useRef(false); - - useSignalEffect(() => { - if (!phaserGameSignal) return; - const ctx = phaserGameSignal.value; - if (!ctx?.game) return; - - const game = ctx.game; - - // 注册场景到 Phaser(但不启动) - if (!game.scene.getScene(props.sceneKey)) { - const initData = { - ...props.data, - phaserGame: phaserGameSignal, - sceneController: ctx.sceneController, - }; - game.scene.add(props.sceneKey, props.scene, false, initData); + const sceneController: SceneController = { + async launch(sceneKey: string) { + if (isTransitioning.value) { + console.warn('SceneController: 正在进行场景切换'); + return; } - sceneSignal.value = props.scene; - registered.current = true; + // 等待场景注册完成(最多等待 100ms) + let retries = 0; + while (!phaserGame.scene.getScene(sceneKey) && retries < 10) { + await new Promise((resolve) => setTimeout(resolve, 10)); + retries++; + } - return () => { - sceneSignal.value = undefined; - registered.current = false; - // 不在这里移除场景,让 SceneController 管理生命周期 - }; - }); + // 验证场景是否已注册 + if (!phaserGame.scene.getScene(sceneKey)) { + console.error(`SceneController: 场景 "${sceneKey}" 未注册`); + return; + } - return }>{props.children}; + isTransitioning.value = true; + const fade = phaserGame.scene.getScene( + FADE_SCENE_KEY, + ) as FadeSceneClass; + + // 淡出到黑色 + phaserGame.scene.bringToTop(FADE_SCENE_KEY); + await fade.fadeOut(300); + + // 停止当前场景 + if (currentScene.value) { + phaserGame.scene.stop(currentScene.value); + } + + // 确保场景已注册后再启动 + // (场景应该已经在 PhaserScene 组件中注册) + if (!phaserGame.scene.getScene(sceneKey)) { + console.error(`SceneController: 场景 "${sceneKey}" 在切换时仍未注册`); + isTransitioning.value = false; + return; + } + + // 启动新场景 + phaserGame.scene.start(sceneKey); + currentScene.value = sceneKey; + + // 淡入 + await fade.fadeIn(300); + isTransitioning.value = false; + }, + async restart() { + if (isTransitioning.value) { + console.warn('SceneController: 正在进行场景切换'); + return; + } + + if (!currentScene.value) { + console.warn('SceneController: 没有当前场景,无法 restart'); + return; + } + + const sceneKey = currentScene.value; + const scene = phaserGame.scene.getScene(sceneKey); + + if (!scene) { + console.error(`SceneController: 场景 "${sceneKey}" 不存在`); + return; + } + + isTransitioning.value = true; + const fade = phaserGame.scene.getScene( + FADE_SCENE_KEY, + ) as FadeSceneClass; + + // 淡出到黑色 + phaserGame.scene.bringToTop(FADE_SCENE_KEY); + await fade.fadeOut(300); + + // 重启当前场景 + scene.scene.restart(); + + // 淡入 + await fade.fadeIn(300); + isTransitioning.value = false; + }, + currentScene, + isTransitioning, + }; + + gameSignal.value = { game: phaserGame, sceneController }; + + return () => { + gameSignal.value = { game: undefined!, sceneController: undefined! }; + initialSceneLaunched.current = false; + phaserGame.destroy(true); + }; + }); + + // 启动初始场景(仅一次) + useEffect(() => { + const ctx = gameSignal.value; + if ( + !initialSceneLaunched.current && + props.initialScene && + ctx?.sceneController + ) { + initialSceneLaunched.current = true; + // 使用 microtask 确保所有子组件的场景注册已完成 + Promise.resolve().then(() => { + ctx.sceneController.launch(props.initialScene!); + }); + } + }, [gameSignal.value, props.initialScene]); + + return ( +
+ + {props.children} + +
+ ); +} + +export interface PhaserSceneProps { + sceneKey: string; + scene: ReactiveScene; + data?: TData; + children?: any; +} + +export const phaserSceneContext = + createContext | null>(null); + +export function PhaserScene(props: PhaserSceneProps) { + const phaserGameSignal = useContext(phaserContext); + const sceneSignal = useSignal>(); + const registered = useRef(false); + + useSignalEffect(() => { + if (!phaserGameSignal) return; + const ctx = phaserGameSignal.value; + if (!ctx?.game) return; + + const game = ctx.game; + + // 注册场景到 Phaser(但不启动) + if (!game.scene.getScene(props.sceneKey)) { + const initData = { + ...props.data, + phaserGame: phaserGameSignal, + sceneController: ctx.sceneController, + }; + game.scene.add(props.sceneKey, props.scene, false, initData); + } + + sceneSignal.value = props.scene; + registered.current = true; + + return () => { + sceneSignal.value = undefined; + registered.current = false; + // 不在这里移除场景,让 SceneController 管理生命周期 + }; + }); + + return ( + } + > + {props.children} + + ); } diff --git a/packages/framework/src/ui/index.ts b/packages/framework/src/ui/index.ts index 258b9ed..00c5de6 100644 --- a/packages/framework/src/ui/index.ts +++ b/packages/framework/src/ui/index.ts @@ -1,5 +1,12 @@ -export { GameUI } from './GameUI'; -export type { GameUIOptions } from './GameUI'; +export { GameUI } from "./GameUI"; +export type { GameUIOptions } from "./GameUI"; -export { PhaserGame, PhaserScene, phaserContext, defaultPhaserConfig, type PhaserGameContext, type SceneController } from './PhaserBridge'; -export type { PhaserGameProps, PhaserSceneProps } from './PhaserBridge'; +export { + PhaserGame, + PhaserScene, + phaserContext, + defaultPhaserConfig, + type PhaserGameContext, + type SceneController, +} from "./PhaserBridge"; +export type { PhaserGameProps, PhaserSceneProps } from "./PhaserBridge"; diff --git a/packages/framework/src/utils/disposable.ts b/packages/framework/src/utils/disposable.ts index e56ae57..3dbeb84 100644 --- a/packages/framework/src/utils/disposable.ts +++ b/packages/framework/src/utils/disposable.ts @@ -1,46 +1,42 @@ export interface IDisposable { - dispose(): void; + dispose(): void; } export type DisposableItem = IDisposable | (() => void); export class DisposableBag implements IDisposable { - private _disposables = new Set(); - private _isDisposed = false; + private _disposables = new Set(); + private _isDisposed = false; - get isDisposed(): boolean { - return this._isDisposed; + get isDisposed(): boolean { + return this._isDisposed; + } + + add(item: DisposableItem): void { + if (this._isDisposed) { + this._execute(item); + return; + } + this._disposables.add(item); + } + + dispose(): void { + if (this._isDisposed) return; + + this._isDisposed = true; + + for (const item of this._disposables) { + this._execute(item); } - add(item: DisposableItem): void { - if (this._isDisposed) { - this._execute(item); - return; - } - this._disposables.add(item); - } - - dispose(): void { - if (this._isDisposed) return; - - this._isDisposed = true; - - for (const item of this._disposables) { - try { - this._execute(item); - } catch (error) { - console.error('Error during resource disposal:', error); - } - } - - this._disposables.clear(); - } - - private _execute(item: DisposableItem): void { - if (typeof item === 'function') { - item(); - } else { - item.dispose(); - } + this._disposables.clear(); + } + + private _execute(item: DisposableItem): void { + if (typeof item === "function") { + item(); + } else { + item.dispose(); } + } } diff --git a/packages/framework/src/utils/dnd.ts b/packages/framework/src/utils/dnd.ts index bcd7fb9..2709ea2 100644 --- a/packages/framework/src/utils/dnd.ts +++ b/packages/framework/src/utils/dnd.ts @@ -1,4 +1,4 @@ -import { DisposableBag } from "./disposable"; +import type { DisposableBag } from "./disposable"; type PointerRecord = { id: number; diff --git a/packages/framework/src/utils/index.ts b/packages/framework/src/utils/index.ts index 53ba7f5..6cbd009 100644 --- a/packages/framework/src/utils/index.ts +++ b/packages/framework/src/utils/index.ts @@ -1,3 +1,7 @@ -export { DisposableBag } from './disposable'; -export type { IDisposable, DisposableItem } from './disposable'; -export { dragDropEventEffect, DragDropEventType, type DragDropEvent } from './dnd'; +export { DisposableBag } from "./disposable"; +export type { IDisposable, DisposableItem } from "./disposable"; +export { + dragDropEventEffect, + DragDropEventType, + type DragDropEvent, +} from "./dnd";