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