refactor: clean up
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
export { ReactiveScene } from './scenes/ReactiveScene';
|
||||
export type { ReactiveSceneOptions } from './scenes/ReactiveScene';
|
||||
// Resource management
|
||||
export { DisposableBag } from './utils';
|
||||
export type { IDisposable, DisposableItem } from './utils';
|
||||
|
||||
export { bindSignal, bindGameObjectProperty, bindRegion, bindCollection } from './bindings';
|
||||
export type { BindRegionOptions, BindCollectionOptions } from './bindings';
|
||||
// Data-driven object spawning
|
||||
export { spawnEffect } from './spawner';
|
||||
export type { Spawner } from './spawner';
|
||||
|
||||
export { InputMapper, PromptHandler, createInputMapper, createPromptHandler } from './input';
|
||||
export type { InputMapperOptions, PromptHandlerOptions } from './input';
|
||||
// Scene base classes
|
||||
export { GameHostScene } from './scenes';
|
||||
export type { GameHostSceneOptions } from './scenes';
|
||||
|
||||
export { GameUI } from './ui/GameUI';
|
||||
export type { GameUIOptions } from './ui/GameUI';
|
||||
|
||||
export { PromptDialog } from './ui/PromptDialog';
|
||||
export { CommandLog } from './ui/CommandLog';
|
||||
// React ↔ Phaser bridge
|
||||
export { PhaserGame, PhaserScene, phaserContext, defaultPhaserConfig, GameUI } from './ui';
|
||||
export type { PhaserGameProps, PhaserSceneProps, GameUIOptions } from './ui';
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import Phaser from 'phaser';
|
||||
import { effect, type ReadonlySignal } from '@preact/signals-core';
|
||||
import type { GameHost } from 'boardgame-core';
|
||||
import { DisposableBag, type IDisposable } from '../utils';
|
||||
|
||||
type CleanupFn = void | (() => void);
|
||||
|
||||
export interface GameHostSceneOptions<TState extends Record<string, unknown>> {
|
||||
gameHost: GameHost<TState>;
|
||||
}
|
||||
|
||||
export abstract class GameHostScene<TState extends Record<string, unknown>>
|
||||
extends Phaser.Scene
|
||||
implements IDisposable
|
||||
{
|
||||
protected disposables = new DisposableBag();
|
||||
protected gameHost!: GameHost<TState>;
|
||||
|
||||
init(data: GameHostSceneOptions<TState>): void {
|
||||
this.gameHost = data.gameHost;
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.events.on('shutdown', this.dispose, this);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
|
||||
/** 获取当前状态的只读快照 */
|
||||
protected get state(): TState {
|
||||
return this.gameHost.state.value;
|
||||
}
|
||||
|
||||
/** 注册响应式监听(场景关闭时自动清理) */
|
||||
protected watch(fn: () => CleanupFn): void {
|
||||
this.disposables.add(effect(fn));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { GameHostScene } from './GameHostScene';
|
||||
export type { GameHostSceneOptions } from './GameHostScene';
|
||||
@@ -0,0 +1,49 @@
|
||||
import Phaser from 'phaser';
|
||||
import { effect, type ReadonlySignal } from '@preact/signals-core';
|
||||
|
||||
type GO = Phaser.GameObjects.GameObject;
|
||||
|
||||
export interface Spawner<TData, TObject extends GO = GO> {
|
||||
/** 数据源迭代器 */
|
||||
getData(): Iterable<TData>;
|
||||
/** 获取数据的唯一键 */
|
||||
getKey(t: TData): string;
|
||||
/** 创建新对象 */
|
||||
onSpawn(t: TData): TObject | null;
|
||||
/** 销毁旧对象 */
|
||||
onDespawn(obj: TObject): void;
|
||||
/** 更新已有对象 */
|
||||
onUpdate(t: TData, obj: TObject): void;
|
||||
}
|
||||
|
||||
export function spawnEffect<TData, TObject extends GO = GO>(
|
||||
spawner: Spawner<TData, TObject>,
|
||||
): () => void {
|
||||
const objects = new Map<string, TObject>();
|
||||
|
||||
return effect(() => {
|
||||
const current = new Set<string>();
|
||||
|
||||
for (const t of spawner.getData()) {
|
||||
const key = spawner.getKey(t);
|
||||
current.add(key);
|
||||
|
||||
const existing = objects.get(key);
|
||||
if (!existing) {
|
||||
const obj = spawner.onSpawn(t);
|
||||
if (obj) {
|
||||
objects.set(key, obj);
|
||||
}
|
||||
} else {
|
||||
spawner.onUpdate(t, existing);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, obj] of objects) {
|
||||
if (!current.has(key)) {
|
||||
spawner.onDespawn(obj);
|
||||
objects.delete(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import Phaser from 'phaser';
|
||||
import { signal, useSignal, useSignalEffect, type Signal } from '@preact/signals';
|
||||
import { createContext, h } from 'preact';
|
||||
import { useContext } from 'preact/hooks';
|
||||
|
||||
export const phaserContext = createContext<Signal<Phaser.Game | undefined>>(signal(undefined));
|
||||
|
||||
export const defaultPhaserConfig: Phaser.Types.Core.GameConfig = {
|
||||
type: Phaser.AUTO,
|
||||
width: 560,
|
||||
height: 560,
|
||||
parent: 'phaser-container',
|
||||
backgroundColor: '#f9fafb',
|
||||
scene: [],
|
||||
};
|
||||
|
||||
export interface PhaserGameProps {
|
||||
config?: Partial<Phaser.Types.Core.GameConfig>;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export function PhaserGame(props: PhaserGameProps) {
|
||||
const gameSignal = useSignal<Phaser.Game>();
|
||||
|
||||
useSignalEffect(() => {
|
||||
const phaserGame = new Phaser.Game(props.config || defaultPhaserConfig);
|
||||
gameSignal.value = phaserGame;
|
||||
|
||||
return () => {
|
||||
gameSignal.value = undefined;
|
||||
phaserGame.destroy(true);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="phaser-container" className="w-full h-full">
|
||||
<phaserContext.Provider value={gameSignal}>
|
||||
{props.children}
|
||||
</phaserContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface PhaserSceneProps {
|
||||
sceneKey: string;
|
||||
scene: Phaser.Scene;
|
||||
autoStart: boolean;
|
||||
data?: object;
|
||||
}
|
||||
|
||||
export function PhaserScene(props: PhaserSceneProps) {
|
||||
const context = useContext(phaserContext);
|
||||
|
||||
useSignalEffect(() => {
|
||||
const game = context.value;
|
||||
if (!game) return;
|
||||
|
||||
game.scene.add(props.sceneKey, props.scene, props.autoStart, props.data);
|
||||
return () => {
|
||||
game.scene.remove(props.sceneKey);
|
||||
};
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { GameUI } from './GameUI';
|
||||
export type { GameUIOptions } from './GameUI';
|
||||
|
||||
export { PhaserGame, PhaserScene, phaserContext, defaultPhaserConfig } from './PhaserBridge';
|
||||
export type { PhaserGameProps, PhaserSceneProps } from './PhaserBridge';
|
||||
@@ -0,0 +1,46 @@
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export type DisposableItem = IDisposable | (() => void);
|
||||
|
||||
export class DisposableBag implements IDisposable {
|
||||
private _disposables = new Set<DisposableItem>();
|
||||
private _isDisposed = false;
|
||||
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { DisposableBag } from './disposable';
|
||||
export type { IDisposable, DisposableItem } from './disposable';
|
||||
Reference in New Issue
Block a user