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. */ 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 pointerMoveHandler!: (pointer: Phaser.Input.Pointer) => void; private pointerUpHandler!: (pointer: Phaser.Input.Pointer) => void; private pointerDownHandler!: (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; const gridW = inventory.width * this.cellSize + (inventory.width - 1) * this.gridGap; const gridH = inventory.height * this.cellSize + (inventory.height - 1) * this.gridGap; 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(), }); 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); } } private setupItemInteraction( itemId: string, visuals: ReturnType, ): 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, pointer); } }); } private setupInput(): void { this.pointerDownHandler = (pointer: Phaser.Input.Pointer) => { if (!this.dragController.isDragging()) return; if (pointer.button === 1) { this.dragController.rotateDraggedItem(); } }; this.pointerMoveHandler = (pointer: Phaser.Input.Pointer) => { this.dragController.onPointerMove(pointer); }; this.pointerUpHandler = (pointer: Phaser.Input.Pointer) => { this.dragController.onPointerUp(pointer); }; this.scene.input.on("pointermove", this.pointerMoveHandler); this.scene.input.on("pointerup", this.pointerUpHandler); this.scene.input.on("pointerdown", this.pointerDownHandler); } 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); } } private handleCreateLostItem( itemId: string, shape: InventoryItem["shape"], transform: InventoryItem["transform"], meta: InventoryItem["meta"], ): void { this.lostItemManager.createLostItem( itemId, shape, transform, meta, this.dragController.getDraggedItemPosition().x, this.dragController.getDraggedItemPosition().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(); // 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(); } public destroy(): void { this.scene.input.off("pointermove", this.pointerMoveHandler); this.scene.input.off("pointerup", this.pointerUpHandler); this.scene.input.off("pointerdown", this.pointerDownHandler); this.dragController.destroy(); this.lostItemManager.destroy(); this.renderer.destroy(); this.container.destroy(); } }