refactor: use InventoryItemState in InventoryItemContainer

Replace local signal-based state management with the dedicated
`InventoryItemState` class to encapsulate item properties and
logic.
This commit is contained in:
hypercross 2026-04-20 19:50:25 +08:00
parent 19dfafeb30
commit fd347e7a62
2 changed files with 65 additions and 37 deletions

View File

@ -1,6 +1,6 @@
import Phaser from "phaser"; import Phaser from "phaser";
import { dragDropEventEffect, DragDropEventType } from "boardgame-phaser"; import { dragDropEventEffect, DragDropEventType } from "boardgame-phaser";
import { GRID_CONFIG, ITEM_COLORS } from "@/config"; import { GRID_CONFIG } from "@/config";
import { import {
IDENTITY_TRANSFORM, IDENTITY_TRANSFORM,
ParsedShape, ParsedShape,
@ -8,15 +8,15 @@ import {
transformShape, transformShape,
type GameItem, type GameItem,
} from "boardgame-core/samples/slay-the-spire-like"; } from "boardgame-core/samples/slay-the-spire-like";
import { computed, signal, Signal } from "@preact/signals-core";
import { DisposableBag } from "../../../framework/dist"; import { DisposableBag } from "../../../framework/dist";
import { InventoryItemState } from "@/state/InventoryItemState";
export interface InventoryItemContainerCallbacks { export interface InventoryItemContainerCallbacks {
onMoveItem: (itemId: string, newX: number, newY: number) => boolean; onMoveItem: (itemId: string, newX: number, newY: number) => boolean;
} }
export class InventoryItemContainer extends Phaser.GameObjects.Container { export class InventoryItemContainer extends Phaser.GameObjects.Container {
private item: Signal<GameItem | undefined>; private itemState: InventoryItemState;
constructor( constructor(
scene: Phaser.Scene, scene: Phaser.Scene,
@ -36,43 +36,34 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
); );
this.add([graphics, label]); this.add([graphics, label]);
this.item = signal(); this.itemState = new InventoryItemState();
const disposables = new DisposableBag(this); const disposables = new DisposableBag(this);
const itemName = computed(() => {
const item = this.item.value;
return item?.meta?.itemData.name ?? item?.id ?? "";
});
disposables.addEffect(() => { disposables.addEffect(() => {
label.setText(itemName.value); label.setText(this.itemState.name.value);
}); });
const shape = computed(() => {
return this.item.value?.shape;
});
const color = computed(() => {
return this.getItemColor(this.item.value?.id ?? "");
});
disposables.addEffect(() => { disposables.addEffect(() => {
graphics.clear(); graphics.clear();
if (!shape.value) return; if (!this.itemState.shape.value) return;
const cellRects = this.renderGraphics(graphics, shape.value, color.value); const cellRects = this.renderGraphics(
graphics,
this.itemState.shape.value,
this.itemState.color.value,
);
this.setupInteractive(cellRects); this.setupInteractive(cellRects);
}); });
const transform = computed(() => {
return this.item.value?.transform;
});
disposables.addEffect(() => { disposables.addEffect(() => {
if (!transform.value) return; if (!this.itemState.transform.value) return;
this.snapBack(transform.value); this.snapBack(this.itemState.transform.value);
}); });
} }
setItem(item: GameItem) { setItem(item: GameItem): void {
this.item.value = item; this.itemState.setItem(item);
} }
renderGraphics( renderGraphics(
@ -101,11 +92,6 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
return cellRects; return cellRects;
} }
private getItemColor(itemId: string): number {
const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
return ITEM_COLORS[hash % ITEM_COLORS.length];
}
private setupInteractive(cellRects: { x: number; y: number }[]): void { private setupInteractive(cellRects: { x: number; y: number }[]): void {
this.setScrollFactor(0); this.setScrollFactor(0);
const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE; const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
@ -115,15 +101,14 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
hitArea: { x: number; y: number }[], hitArea: { x: number; y: number }[],
x: number, x: number,
y: number, y: number,
) => { ) =>
return hitArea.some( hitArea.some(
(cell) => (cell) =>
x >= cell.x && x >= cell.x &&
x < cell.x + cellSize && x < cell.x + cellSize &&
y >= cell.y && y >= cell.y &&
y < cell.y + cellSize, y < cell.y + cellSize,
); ),
},
useHandCursor: true, useHandCursor: true,
} as Phaser.Types.Input.InputConfiguration); } as Phaser.Types.Input.InputConfiguration);
@ -140,7 +125,7 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
} else if (event.type === DragDropEventType.UP) { } else if (event.type === DragDropEventType.UP) {
this.setAlpha(1); this.setAlpha(1);
if (!this.handleDragEnd()) { if (!this.handleDragEnd()) {
const t = this.item.peek()?.transform; const t = this.itemState.transform.peek();
t && this.snapBack(t); t && this.snapBack(t);
} }
startX = startY = 0; startX = startY = 0;
@ -149,9 +134,8 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
} }
private handleDragEnd(): boolean { private handleDragEnd(): boolean {
const item = this.item.value; const item = this.itemState.item;
if (!item) return false; if (!item) return false;
const itemId = item.id;
const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE; const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
const shapeWidth = item.shape?.width ?? 1; const shapeWidth = item.shape?.width ?? 1;
@ -169,7 +153,7 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
clampedX !== item.transform.offset.x || clampedX !== item.transform.offset.x ||
clampedY !== item.transform.offset.y clampedY !== item.transform.offset.y
) { ) {
return this.callbacks.onMoveItem(itemId, clampedX, clampedY); return this.callbacks.onMoveItem(item.id, clampedX, clampedY);
} }
return false; return false;
} }

View File

@ -0,0 +1,44 @@
import { computed, signal, Signal } from "@preact/signals-core"
import { ITEM_COLORS } from "@/config"
import {
type GameItem,
type ParsedShape,
type Transform2D,
} from "boardgame-core/samples/slay-the-spire-like"
export class InventoryItemState {
private readonly _item: Signal<GameItem | undefined>
readonly name: Signal<string>
readonly shape: Signal<ParsedShape | undefined>
readonly color: Signal<number>
readonly transform: Signal<Transform2D | undefined>
constructor(initialItem?: GameItem) {
this._item = signal(initialItem)
this.name = computed(() => {
const item = this._item.value
return item?.meta?.itemData.name ?? item?.id ?? ""
})
this.shape = computed(() => this._item.value?.shape)
this.color = computed(() => this.computeColor(this._item.value?.id ?? ""))
this.transform = computed(() => this._item.value?.transform)
}
get item(): GameItem | undefined {
return this._item.value
}
setItem(item: GameItem): void {
this._item.value = item
}
private computeColor(itemId: string): number {
const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0)
return ITEM_COLORS[hash % ITEM_COLORS.length]
}
}