boardgame-phaser/packages/sts-like-viewer/src/widgets/InventoryWidget.ts

248 lines
7.4 KiB
TypeScript
Raw Normal View History

import Phaser from "phaser";
import { MutableSignal } from "boardgame-core";
2026-04-17 18:08:51 +08:00
import {
type GridInventory,
type InventoryItem,
type GameItemMeta,
type RunState,
removeItemFromGrid,
placeItem,
} from "boardgame-core/samples/slay-the-spire-like";
import { ItemRenderer } from "./ItemRenderer";
import { DragController } from "./DragController";
import { LostItemManager } from "./LostItemManager";
2026-04-17 18:08:51 +08:00
export interface InventoryWidgetOptions {
scene: Phaser.Scene;
gameState: MutableSignal<RunState>;
x: number;
y: number;
cellSize: number;
gridGap?: number;
isLocked?: boolean;
}
/**
* Thin orchestrator for the inventory grid widget.
* Delegates rendering, drag logic, and lost-item management to focused modules.
*/
2026-04-17 18:08:51 +08:00
export class InventoryWidget {
private scene: Phaser.Scene;
private gameState: MutableSignal<RunState>;
private container: Phaser.GameObjects.Container;
private cellSize: number;
private gridGap: number;
private gridX = 0;
private gridY = 0;
private isLocked: boolean;
private renderer: ItemRenderer;
private dragController: DragController;
private lostItemManager: LostItemManager;
2026-04-17 18:08:51 +08:00
private pointerMoveHandler!: (pointer: Phaser.Input.Pointer) => void;
private pointerUpHandler!: (pointer: Phaser.Input.Pointer) => void;
private pointerDownHandler!: (pointer: Phaser.Input.Pointer) => void;
2026-04-17 18:08:51 +08:00
constructor(options: InventoryWidgetOptions) {
this.scene = options.scene;
this.gameState = options.gameState;
this.cellSize = options.cellSize;
this.gridGap = options.gridGap ?? 2;
this.isLocked = options.isLocked ?? false;
const inventory = this.gameState.value.inventory;
const gridW =
inventory.width * this.cellSize + (inventory.width - 1) * this.gridGap;
const gridH =
inventory.height * this.cellSize + (inventory.height - 1) * this.gridGap;
2026-04-17 18:08:51 +08:00
this.container = this.scene.add.container(options.x, options.y);
// Initialize sub-modules
this.renderer = new ItemRenderer({
scene: this.scene,
container: this.container,
cellSize: this.cellSize,
gridGap: this.gridGap,
gridX: this.gridX,
gridY: this.gridY,
});
this.dragController = new DragController({
scene: this.scene,
container: this.container,
cellSize: this.cellSize,
gridGap: this.gridGap,
gridX: this.gridX,
gridY: this.gridY,
getInventory: () => this.getInventory(),
getItemColor: (id) => this.renderer.getItemColor(id),
getItemCells: (item) => this.renderer.getItemCells(item),
onPlaceItem: (item) => this.handlePlaceItem(item),
onCreateLostItem: (id, shape, transform, meta) =>
this.handleCreateLostItem(id, shape, transform, meta),
});
this.lostItemManager = new LostItemManager({
scene: this.scene,
cellSize: this.cellSize,
gridGap: this.gridGap,
getItemColor: (id) => this.renderer.getItemColor(id),
onLostItemDragStart: (id, pointer) =>
this.dragController.startLostItemDrag(
id,
this.getLostItemShape(id),
this.getLostItemTransform(id),
this.getLostItemMeta(id),
pointer,
),
isDragging: () => this.dragController.isDragging(),
});
2026-04-17 19:45:34 +08:00
this.renderer.drawGridBackground(inventory.width, inventory.height);
2026-04-17 18:08:51 +08:00
this.drawItems();
this.setupInput();
this.scene.events.once("shutdown", () => this.destroy());
2026-04-17 18:08:51 +08:00
}
private getInventory(): GridInventory<GameItemMeta> {
return this.gameState.value
.inventory as unknown as GridInventory<GameItemMeta>;
2026-04-17 18:08:51 +08:00
}
private drawItems(): void {
const inventory = this.getInventory();
for (const [itemId, item] of inventory.items) {
if (this.renderer.hasItem(itemId)) continue;
const visuals = this.renderer.createItemVisuals(itemId, item);
this.setupItemInteraction(itemId, visuals);
2026-04-17 18:08:51 +08:00
}
}
private setupItemInteraction(
itemId: string,
visuals: ReturnType<typeof ItemRenderer.prototype.createItemVisuals>,
): void {
visuals.container.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
2026-04-17 18:08:51 +08:00
if (this.isLocked) return;
if (this.dragController.isDragging()) return;
2026-04-17 18:08:51 +08:00
if (pointer.button === 0) {
this.gameState.produce((state) => {
removeItemFromGrid(state.inventory, itemId);
});
this.renderer.removeItemVisuals(itemId);
this.dragController.startDrag(itemId, pointer);
2026-04-17 18:08:51 +08:00
}
});
}
private setupInput(): void {
this.pointerDownHandler = (pointer: Phaser.Input.Pointer) => {
if (!this.dragController.isDragging()) return;
if (pointer.button === 1) {
this.dragController.rotateDraggedItem();
2026-04-17 18:08:51 +08:00
}
2026-04-17 19:45:34 +08:00
};
this.pointerMoveHandler = (pointer: Phaser.Input.Pointer) => {
this.dragController.onPointerMove(pointer);
2026-04-17 18:08:51 +08:00
};
this.pointerUpHandler = (pointer: Phaser.Input.Pointer) => {
this.dragController.onPointerUp(pointer);
2026-04-17 18:08:51 +08:00
};
this.scene.input.on("pointermove", this.pointerMoveHandler);
this.scene.input.on("pointerup", this.pointerUpHandler);
this.scene.input.on("pointerdown", this.pointerDownHandler);
2026-04-17 18:08:51 +08:00
}
private handlePlaceItem(item: InventoryItem<GameItemMeta>): void {
this.gameState.produce((state) => {
placeItem(state.inventory, item);
});
2026-04-17 18:08:51 +08:00
const inventory = this.getInventory();
const placedItem = inventory.items.get(item.id);
if (placedItem) {
const visuals = this.renderer.createItemVisuals(item.id, placedItem);
this.setupItemInteraction(item.id, visuals);
2026-04-17 18:08:51 +08:00
}
}
private handleCreateLostItem(
itemId: string,
shape: InventoryItem<GameItemMeta>["shape"],
transform: InventoryItem<GameItemMeta>["transform"],
meta: InventoryItem<GameItemMeta>["meta"],
): void {
this.lostItemManager.createLostItem(
itemId,
shape,
transform,
meta,
this.dragController.getDraggedItemPosition().x,
this.dragController.getDraggedItemPosition().y,
);
2026-04-17 18:08:51 +08:00
}
private getLostItemShape(itemId: string) {
return this.lostItemManager.getLostItem(itemId)?.shape!;
2026-04-17 18:08:51 +08:00
}
private getLostItemTransform(itemId: string) {
return this.lostItemManager.getLostItem(itemId)?.transform!;
2026-04-17 18:08:51 +08:00
}
private getLostItemMeta(itemId: string) {
return this.lostItemManager.getLostItem(itemId)?.meta!;
2026-04-17 18:08:51 +08:00
}
public setLocked(locked: boolean): void {
this.isLocked = locked;
}
public getLostItems(): string[] {
return this.lostItemManager.getLostItemIds();
2026-04-17 18:08:51 +08:00
}
public clearLostItems(): void {
this.lostItemManager.clear();
2026-04-17 18:08:51 +08:00
}
public refresh(): void {
const inventory = this.getInventory();
// Remove visuals for items no longer in inventory
for (const [itemId] of inventory.items.entries()) {
// We need a way to track which items have visuals
// For now, clear and redraw
}
// Simple approach: destroy all and redraw
this.renderer.destroy();
this.renderer = new ItemRenderer({
scene: this.scene,
container: this.container,
cellSize: this.cellSize,
gridGap: this.gridGap,
gridX: this.gridX,
gridY: this.gridY,
});
this.renderer.drawGridBackground(inventory.width, inventory.height);
this.drawItems();
2026-04-17 18:08:51 +08:00
}
public destroy(): void {
this.scene.input.off("pointermove", this.pointerMoveHandler);
this.scene.input.off("pointerup", this.pointerUpHandler);
this.scene.input.off("pointerdown", this.pointerDownHandler);
2026-04-17 18:08:51 +08:00
this.dragController.destroy();
this.lostItemManager.destroy();
this.renderer.destroy();
2026-04-17 18:08:51 +08:00
this.container.destroy();
}
}