import Phaser from "phaser"; import { MutableSignal } from "boardgame-core"; 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"; export interface InventoryWidgetOptions { scene: Phaser.Scene; gameState: MutableSignal; 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. * Uses event-driven drag via dragDropEventEffect from boardgame-phaser. */ export class InventoryWidget { private scene: Phaser.Scene; private gameState: MutableSignal; 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; private rightClickHandler!: (pointer: Phaser.Input.Pointer) => void; 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; this.container = this.scene.add.container(options.x, options.y); 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), onPlaceItem: (item) => this.handlePlaceItem(item), onCreateLostItem: (id, shape, transform, meta, x, y) => this.handleCreateLostItem(id, shape, transform, meta, x, y), }); this.lostItemManager = new LostItemManager({ scene: this.scene, cellSize: this.cellSize, gridGap: this.gridGap, getItemColor: (id) => this.renderer.getItemColor(id), onLostItemDragStart: (id, lostContainer) => this.dragController.startLostItemDrag( id, this.getLostItemShape(id), this.getLostItemTransform(id), this.getLostItemMeta(id), lostContainer, ), isDragging: () => this.dragController.isDragging(), }); this.renderer.drawGridBackground(inventory.width, inventory.height); this.drawItems(); this.setupInput(); this.scene.events.once("shutdown", () => this.destroy()); } private getInventory(): GridInventory { return this.gameState.value .inventory as unknown as GridInventory; } 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, item); } } private setupItemInteraction( itemId: string, visuals: ReturnType, item: InventoryItem, ): void { visuals.container.on("pointerdown", (pointer: Phaser.Input.Pointer) => { if (this.isLocked) return; if (this.dragController.isDragging()) return; if (pointer.button === 0) { this.gameState.produce((state) => { removeItemFromGrid(state.inventory, itemId); }); this.renderer.removeItemVisuals(itemId); this.dragController.startDrag(itemId, item, visuals.container); } }); } private setupInput(): void { this.rightClickHandler = (pointer: Phaser.Input.Pointer) => { if (!this.dragController.isDragging()) return; if (pointer.button === 1) { this.dragController.rotateDraggedItem(); } }; this.scene.input.on("pointerdown", this.rightClickHandler); } private handlePlaceItem(item: InventoryItem): void { this.gameState.produce((state) => { placeItem(state.inventory, item); }); 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, placedItem); } } private handleCreateLostItem( itemId: string, shape: InventoryItem["shape"], transform: InventoryItem["transform"], meta: InventoryItem["meta"], x: number, y: number, ): void { this.lostItemManager.createLostItem(itemId, shape, transform, meta, x, y); } private getLostItemShape(itemId: string) { return this.lostItemManager.getLostItem(itemId)?.shape!; } private getLostItemTransform(itemId: string) { return this.lostItemManager.getLostItem(itemId)?.transform!; } private getLostItemMeta(itemId: string) { return this.lostItemManager.getLostItem(itemId)?.meta!; } public setLocked(locked: boolean): void { this.isLocked = locked; } public getLostItems(): string[] { return this.lostItemManager.getLostItemIds(); } public clearLostItems(): void { this.lostItemManager.clear(); } public refresh(): void { const inventory = this.getInventory(); 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(); } public destroy(): void { this.scene.input.off("pointerdown", this.rightClickHandler); this.dragController.destroy(); this.lostItemManager.destroy(); this.renderer.destroy(); this.container.destroy(); } }