diff --git a/packages/sts-like-viewer/src/widgets/GridBackgroundRenderer.ts b/packages/sts-like-viewer/src/widgets/GridBackgroundRenderer.ts new file mode 100644 index 0000000..bbbf872 --- /dev/null +++ b/packages/sts-like-viewer/src/widgets/GridBackgroundRenderer.ts @@ -0,0 +1,71 @@ +import Phaser from "phaser"; + +export interface GridBackgroundRendererOptions { + scene: Phaser.Scene; + parentContainer: Phaser.GameObjects.Container; + cellSize: number; + gridGap: number; + gridX: number; + gridY: number; + /** Background fill color for each cell */ + cellBgColor?: number; + /** Border/stroke color for each cell */ + cellBorderColor?: number; +} + +/** + * Renders the static grid background (empty cells with borders). + * Separated from item rendering so it can be drawn once and left alone. + */ +export class GridBackgroundRenderer { + private scene: Phaser.Scene; + private parentContainer: Phaser.GameObjects.Container; + private cellSize: number; + private gridGap: number; + private gridX: number; + private gridY: number; + private cellBgColor: number; + private cellBorderColor: number; + + private graphics!: Phaser.GameObjects.Graphics; + + constructor(options: GridBackgroundRendererOptions) { + this.scene = options.scene; + this.parentContainer = options.parentContainer; + this.cellSize = options.cellSize; + this.gridGap = options.gridGap; + this.gridX = options.gridX; + this.gridY = options.gridY; + this.cellBgColor = options.cellBgColor ?? 0x1a1a2e; + this.cellBorderColor = options.cellBorderColor ?? 0x444477; + } + + /** + * Draw the grid background for the given dimensions. + * Should be called once during initialization. + */ + draw(width: number, height: number): void { + this.graphics = this.scene.add.graphics(); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const px = this.gridX + x * (this.cellSize + this.gridGap); + const py = this.gridY + y * (this.cellSize + this.gridGap); + + this.graphics.fillStyle(this.cellBgColor); + this.graphics.fillRect(px, py, this.cellSize, this.cellSize); + this.graphics.lineStyle(2, this.cellBorderColor); + this.graphics.strokeRect(px, py, this.cellSize, this.cellSize); + } + } + + this.parentContainer.add(this.graphics); + } + + /** + * Destroy the graphics object. + */ + destroy(): void { + this.graphics?.destroy(); + } +} diff --git a/packages/sts-like-viewer/src/widgets/InventoryItemSpawner.ts b/packages/sts-like-viewer/src/widgets/InventoryItemSpawner.ts new file mode 100644 index 0000000..4f07071 --- /dev/null +++ b/packages/sts-like-viewer/src/widgets/InventoryItemSpawner.ts @@ -0,0 +1,273 @@ +import Phaser from "phaser"; +import { MutableSignal } from "boardgame-core"; +import { + type InventoryItem, + type GameItemMeta, + type RunState, + type GridInventory, +} from "boardgame-core/samples/slay-the-spire-like"; +import type { Spawner } from "boardgame-phaser"; + +const ITEM_COLORS = [ + 0x3388ff, 0xff8833, 0x33ff88, 0xff3388, 0x8833ff, 0x33ffff, 0xffff33, + 0xff6633, +]; + +export interface InventoryItemSpawnerOptions { + scene: Phaser.Scene; + gameState: MutableSignal; + parentContainer: Phaser.GameObjects.Container; + cellSize: number; + gridGap: number; + gridX: number; + gridY: number; + isLocked: () => boolean; + isDragging: () => boolean; + onItemDragStart: ( + itemId: string, + item: InventoryItem, + itemContainer: Phaser.GameObjects.Container, + ) => void; +} + +/** + * Spawner for inventory items using the boardgame-phaser Spawner pattern. + * Reactively spawns/despawns/updates item visuals when gameState.inventory changes. + * + * Items currently being dragged are excluded from getData() to prevent + * the spawner from respawning them while they're in flight. + */ +export class InventoryItemSpawner + implements Spawner, Phaser.GameObjects.Container> +{ + private scene: Phaser.Scene; + private gameState: MutableSignal; + private parentContainer: Phaser.GameObjects.Container; + private cellSize: number; + private gridGap: number; + private gridX: number; + private gridY: number; + private isLocked: () => boolean; + private isDragging: () => boolean; + private onItemDragStart: ( + itemId: string, + item: InventoryItem, + itemContainer: Phaser.GameObjects.Container, + ) => void; + + private colorMap = new Map(); + private colorIdx = 0; + private draggingIds = new Set(); + + constructor(options: InventoryItemSpawnerOptions) { + this.scene = options.scene; + this.gameState = options.gameState; + this.parentContainer = options.parentContainer; + this.cellSize = options.cellSize; + this.gridGap = options.gridGap; + this.gridX = options.gridX; + this.gridY = options.gridY; + this.isLocked = options.isLocked; + this.isDragging = options.isDragging; + this.onItemDragStart = options.onItemDragStart; + } + + *getData(): Iterable> { + const inventory = this.getInventory(); + for (const [, item] of inventory.items) { + if (!this.draggingIds.has(item.id)) { + yield item; + } + } + } + + getKey(item: InventoryItem): string { + return item.id; + } + + onSpawn(item: InventoryItem): Phaser.GameObjects.Container { + const color = + this.colorMap.get(item.id) ?? + ITEM_COLORS[this.colorIdx++ % ITEM_COLORS.length]; + this.colorMap.set(item.id, color); + + const container = this.createItemVisuals(item, color); + this.setupInteraction(item, container); + this.parentContainer.add(container); + + return container; + } + + onDespawn( + obj: Phaser.GameObjects.Container, + _item: InventoryItem, + ): void { + obj.destroy(); + } + + onUpdate( + item: InventoryItem, + obj: Phaser.GameObjects.Container, + ): void { + const color = this.colorMap.get(item.id) ?? 0x888888; + this.rebuildItemVisuals(obj, item, color); + } + + /** + * Mark an item as being dragged so the spawner excludes it from getData(). + * Call this before removing the item from the inventory. + */ + markDragging(itemId: string): void { + this.draggingIds.add(itemId); + } + + /** + * Unmark an item after drag ends (placed or lost). + */ + unmarkDragging(itemId: string): void { + this.draggingIds.delete(itemId); + } + + /** + * Get the color assigned to an item (creates one if not yet assigned). + */ + getItemColor(itemId: string): number { + if (!this.colorMap.has(itemId)) { + this.colorMap.set( + itemId, + ITEM_COLORS[this.colorIdx++ % ITEM_COLORS.length], + ); + } + return this.colorMap.get(itemId)!; + } + + private getInventory(): GridInventory { + return this.gameState.value + .inventory as unknown as GridInventory; + } + + private createItemVisuals( + item: InventoryItem, + color: number, + ): Phaser.GameObjects.Container { + const container = this.scene.add.container(0, 0); + + const graphics = this.scene.add.graphics(); + const cells = this.getItemCells(item); + + for (const cell of cells) { + const px = this.gridX + cell.x * (this.cellSize + this.gridGap); + const py = this.gridY + cell.y * (this.cellSize + this.gridGap); + + graphics.fillStyle(color); + graphics.fillRect(px + 1, py + 1, this.cellSize - 2, this.cellSize - 2); + graphics.lineStyle(2, 0xffffff); + graphics.strokeRect(px, py, this.cellSize, this.cellSize); + } + + const firstCell = cells[0]; + const name = item.meta?.itemData.name ?? item.id; + const fontSize = Math.max(10, Math.floor(this.cellSize / 5)); + const text = this.scene.add + .text( + this.gridX + + firstCell.x * (this.cellSize + this.gridGap) + + this.cellSize / 2, + this.gridY + + firstCell.y * (this.cellSize + this.gridGap) + + this.cellSize / 2, + name, + { fontSize: `${fontSize}px`, color: "#fff", fontStyle: "bold" }, + ) + .setOrigin(0.5); + + container.add(graphics); + container.add(text); + + const hitRect = new Phaser.Geom.Rectangle( + this.gridX + firstCell.x * (this.cellSize + this.gridGap), + this.gridY + firstCell.y * (this.cellSize + this.gridGap), + this.cellSize, + this.cellSize, + ); + container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains); + + return container; + } + + private rebuildItemVisuals( + container: Phaser.GameObjects.Container, + item: InventoryItem, + color: number, + ): void { + container.removeAll(true); + + const graphics = this.scene.add.graphics(); + const cells = this.getItemCells(item); + + for (const cell of cells) { + const px = this.gridX + cell.x * (this.cellSize + this.gridGap); + const py = this.gridY + cell.y * (this.cellSize + this.gridGap); + + graphics.fillStyle(color); + graphics.fillRect(px + 1, py + 1, this.cellSize - 2, this.cellSize - 2); + graphics.lineStyle(2, 0xffffff); + graphics.strokeRect(px, py, this.cellSize, this.cellSize); + } + + const firstCell = cells[0]; + const name = item.meta?.itemData.name ?? item.id; + const fontSize = Math.max(10, Math.floor(this.cellSize / 5)); + const text = this.scene.add + .text( + this.gridX + + firstCell.x * (this.cellSize + this.gridGap) + + this.cellSize / 2, + this.gridY + + firstCell.y * (this.cellSize + this.gridGap) + + this.cellSize / 2, + name, + { fontSize: `${fontSize}px`, color: "#fff", fontStyle: "bold" }, + ) + .setOrigin(0.5); + + container.add(graphics); + container.add(text); + + const hitRect = new Phaser.Geom.Rectangle( + this.gridX + firstCell.x * (this.cellSize + this.gridGap), + this.gridY + firstCell.y * (this.cellSize + this.gridGap), + this.cellSize, + this.cellSize, + ); + container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains); + } + + private setupInteraction( + item: InventoryItem, + container: Phaser.GameObjects.Container, + ): void { + container.on("pointerdown", (pointer: Phaser.Input.Pointer) => { + if (this.isLocked()) return; + if (this.isDragging()) return; + if (pointer.button === 0) { + this.onItemDragStart(item.id, item, container); + } + }); + } + + private getItemCells( + item: InventoryItem, + ): { x: number; y: number }[] { + const cells: { x: number; y: number }[] = []; + const { offset } = item.transform; + for (let y = 0; y < item.shape.height; y++) { + for (let x = 0; x < item.shape.width; x++) { + if (item.shape.grid[y]?.[x]) { + cells.push({ x: x + offset.x, y: y + offset.y }); + } + } + } + return cells; + } +} diff --git a/packages/sts-like-viewer/src/widgets/InventoryWidget.ts b/packages/sts-like-viewer/src/widgets/InventoryWidget.ts index b0428a1..04f33d0 100644 --- a/packages/sts-like-viewer/src/widgets/InventoryWidget.ts +++ b/packages/sts-like-viewer/src/widgets/InventoryWidget.ts @@ -1,5 +1,6 @@ import Phaser from "phaser"; import { MutableSignal } from "boardgame-core"; +import { spawnEffect } from "boardgame-phaser"; import { type GridInventory, type InventoryItem, @@ -8,7 +9,8 @@ import { removeItemFromGrid, placeItem, } from "boardgame-core/samples/slay-the-spire-like"; -import { ItemRenderer } from "./ItemRenderer"; +import { InventoryItemSpawner } from "./InventoryItemSpawner"; +import { GridBackgroundRenderer } from "./GridBackgroundRenderer"; import { DragController } from "./DragController"; import { LostItemManager } from "./LostItemManager"; @@ -23,9 +25,13 @@ export interface InventoryWidgetOptions { } /** - * 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. + * Inventory widget using the Spawner pattern for reactive item rendering. + * + * Architecture: + * - InventoryItemSpawner + spawnEffect: reactive spawn/despawn/update of item visuals + * - GridBackgroundRenderer: static grid background drawn once + * - DragController: event-driven drag logic via dragDropEventEffect + * - LostItemManager: tracks items dropped outside valid placement */ export class InventoryWidget { private scene: Phaser.Scene; @@ -37,10 +43,12 @@ export class InventoryWidget { private gridY = 0; private isLocked: boolean; - private renderer: ItemRenderer; + private itemSpawner: InventoryItemSpawner; + private backgroundRenderer: GridBackgroundRenderer; private dragController: DragController; private lostItemManager: LostItemManager; + private spawnDispose: (() => void) | null = null; private rightClickHandler!: (pointer: Phaser.Input.Pointer) => void; constructor(options: InventoryWidgetOptions) { @@ -50,19 +58,38 @@ export class InventoryWidget { this.gridGap = options.gridGap ?? 2; this.isLocked = options.isLocked ?? false; - const inventory = this.gameState.value.inventory; + const inventory = this.getInventory(); this.container = this.scene.add.container(options.x, options.y); - this.renderer = new ItemRenderer({ + // 1. Static grid background (drawn once) + this.backgroundRenderer = new GridBackgroundRenderer({ scene: this.scene, - container: this.container, + parentContainer: this.container, cellSize: this.cellSize, gridGap: this.gridGap, gridX: this.gridX, gridY: this.gridY, }); + this.backgroundRenderer.draw(inventory.width, inventory.height); + // 2. Reactive item spawner + this.itemSpawner = new InventoryItemSpawner({ + scene: this.scene, + gameState: this.gameState, + parentContainer: this.container, + cellSize: this.cellSize, + gridGap: this.gridGap, + gridX: this.gridX, + gridY: this.gridY, + isLocked: () => this.isLocked, + isDragging: () => this.dragController.isDragging(), + onItemDragStart: (itemId, item, itemContainer) => { + this.handleItemDragStart(itemId, item, itemContainer); + }, + }); + + // 3. Drag controller this.dragController = new DragController({ scene: this.scene, container: this.container, @@ -71,17 +98,18 @@ export class InventoryWidget { gridX: this.gridX, gridY: this.gridY, getInventory: () => this.getInventory(), - getItemColor: (id) => this.renderer.getItemColor(id), + getItemColor: (id) => this.itemSpawner.getItemColor(id), onPlaceItem: (item) => this.handlePlaceItem(item), onCreateLostItem: (id, shape, transform, meta, x, y) => this.handleCreateLostItem(id, shape, transform, meta, x, y), }); + // 4. Lost item manager this.lostItemManager = new LostItemManager({ scene: this.scene, cellSize: this.cellSize, gridGap: this.gridGap, - getItemColor: (id) => this.renderer.getItemColor(id), + getItemColor: (id) => this.itemSpawner.getItemColor(id), onLostItemDragStart: (id, lostContainer) => this.dragController.startLostItemDrag( id, @@ -93,8 +121,10 @@ export class InventoryWidget { isDragging: () => this.dragController.isDragging(), }); - this.renderer.drawGridBackground(inventory.width, inventory.height); - this.drawItems(); + // Activate the spawner effect (auto-cleans up on dispose) + this.spawnDispose = spawnEffect(this.itemSpawner); + + // Right-click rotation handler this.setupInput(); this.scene.events.once("shutdown", () => this.destroy()); @@ -105,54 +135,30 @@ export class InventoryWidget { .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( + private handleItemDragStart( itemId: string, - visuals: ReturnType, item: InventoryItem, + itemContainer: Phaser.GameObjects.Container, ): 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); - } + // Remove from inventory state + this.gameState.produce((state) => { + removeItemFromGrid(state.inventory, itemId); }); - } - private setupInput(): void { - this.rightClickHandler = (pointer: Phaser.Input.Pointer) => { - if (!this.dragController.isDragging()) return; - if (pointer.button === 1) { - this.dragController.rotateDraggedItem(); - } - }; + // Mark as dragging so spawner excludes it from getData() + this.itemSpawner.markDragging(itemId); - this.scene.input.on("pointerdown", this.rightClickHandler); + // Start drag session + this.dragController.startDrag(itemId, item, itemContainer); } 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); - } + + // Unmark dragging so spawner picks it up on next effect run + this.itemSpawner.unmarkDragging(item.id); } private handleCreateLostItem( @@ -164,6 +170,9 @@ export class InventoryWidget { y: number, ): void { this.lostItemManager.createLostItem(itemId, shape, transform, meta, x, y); + + // Unmark dragging — item is now "lost" and no longer in inventory + this.itemSpawner.unmarkDragging(itemId); } private getLostItemShape(itemId: string) { @@ -178,6 +187,17 @@ export class InventoryWidget { return this.lostItemManager.getLostItem(itemId)?.meta!; } + 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); + } + public setLocked(locked: boolean): void { this.isLocked = locked; } @@ -190,28 +210,28 @@ export class InventoryWidget { this.lostItemManager.clear(); } + /** + * Force re-sync of item visuals with current inventory state. + * With spawnEffect this is usually automatic, but useful after + * external state changes that don't trigger the effect. + */ 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(); + // The spawner effect automatically re-syncs when gameState.value changes. + // If immediate refresh is needed, reading the signal triggers the effect. + void this.gameState.value; } public destroy(): void { this.scene.input.off("pointerdown", this.rightClickHandler); + if (this.spawnDispose) { + this.spawnDispose(); + this.spawnDispose = null; + } + this.dragController.destroy(); this.lostItemManager.destroy(); - this.renderer.destroy(); + this.backgroundRenderer.destroy(); this.container.destroy(); } } diff --git a/packages/sts-like-viewer/src/widgets/ItemRenderer.ts b/packages/sts-like-viewer/src/widgets/ItemRenderer.ts deleted file mode 100644 index 157118b..0000000 --- a/packages/sts-like-viewer/src/widgets/ItemRenderer.ts +++ /dev/null @@ -1,198 +0,0 @@ -import Phaser from "phaser"; -import { - type InventoryItem, - type GameItemMeta, -} from "boardgame-core/samples/slay-the-spire-like"; - -const ITEM_COLORS = [ - 0x3388ff, 0xff8833, 0x33ff88, 0xff3388, 0x8833ff, 0x33ffff, 0xffff33, - 0xff6633, -]; - -export interface ItemVisuals { - container: Phaser.GameObjects.Container; - graphics: Phaser.GameObjects.Graphics; - text: Phaser.GameObjects.Text; -} - -export interface ItemRendererOptions { - scene: Phaser.Scene; - container: Phaser.GameObjects.Container; - cellSize: number; - gridGap: number; - gridX: number; - gridY: number; -} - -/** - * Handles all Phaser visual rendering for inventory items and the grid background. - * Manages color assignment, graphics creation, and cleanup. - */ -export class ItemRenderer { - private scene: Phaser.Scene; - private container: Phaser.GameObjects.Container; - private cellSize: number; - private gridGap: number; - private gridX: number; - private gridY: number; - - private itemVisuals = new Map(); - private colorMap = new Map(); - private colorIdx = 0; - - private gridGraphics!: Phaser.GameObjects.Graphics; - - constructor(options: ItemRendererOptions) { - this.scene = options.scene; - this.container = options.container; - this.cellSize = options.cellSize; - this.gridGap = options.gridGap; - this.gridX = options.gridX; - this.gridY = options.gridY; - } - - /** - * Draw the grid background (empty cells with borders). - */ - drawGridBackground(width: number, height: number): void { - this.gridGraphics = this.scene.add.graphics(); - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const px = this.gridX + x * (this.cellSize + this.gridGap); - const py = this.gridY + y * (this.cellSize + this.gridGap); - - this.gridGraphics.fillStyle(0x1a1a2e); - this.gridGraphics.fillRect(px, py, this.cellSize, this.cellSize); - this.gridGraphics.lineStyle(2, 0x444477); - this.gridGraphics.strokeRect(px, py, this.cellSize, this.cellSize); - } - } - - this.container.add(this.gridGraphics); - } - - /** - * Create Phaser visuals for a single inventory item. - * Returns the visuals object for further interaction setup. - */ - createItemVisuals( - itemId: string, - item: InventoryItem, - ): ItemVisuals { - const color = - this.colorMap.get(itemId) ?? - ITEM_COLORS[this.colorIdx++ % ITEM_COLORS.length]; - this.colorMap.set(itemId, color); - - const graphics = this.scene.add.graphics(); - const cells = this.getItemCells(item); - - for (const cell of cells) { - const px = this.gridX + cell.x * (this.cellSize + this.gridGap); - const py = this.gridY + cell.y * (this.cellSize + this.gridGap); - - graphics.fillStyle(color); - graphics.fillRect(px + 1, py + 1, this.cellSize - 2, this.cellSize - 2); - graphics.lineStyle(2, 0xffffff); - graphics.strokeRect(px, py, this.cellSize, this.cellSize); - } - - const firstCell = cells[0]; - const name = item.meta?.itemData.name ?? item.id; - const fontSize = Math.max(10, Math.floor(this.cellSize / 5)); - const text = this.scene.add - .text( - this.gridX + - firstCell.x * (this.cellSize + this.gridGap) + - this.cellSize / 2, - this.gridY + - firstCell.y * (this.cellSize + this.gridGap) + - this.cellSize / 2, - name, - { fontSize: `${fontSize}px`, color: "#fff", fontStyle: "bold" }, - ) - .setOrigin(0.5); - - const hitRect = new Phaser.Geom.Rectangle( - this.gridX + firstCell.x * (this.cellSize + this.gridGap), - this.gridY + firstCell.y * (this.cellSize + this.gridGap), - this.cellSize, - this.cellSize, - ); - - const container = this.scene.add.container(0, 0); - container.add(graphics); - container.add(text); - container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains); - - const visuals: ItemVisuals = { container, graphics, text }; - this.itemVisuals.set(itemId, visuals); - this.container.add(container); - - return visuals; - } - - /** - * Remove and destroy all Phaser objects for a given item. - */ - removeItemVisuals(itemId: string): void { - const visuals = this.itemVisuals.get(itemId); - if (!visuals) return; - - visuals.container.destroy(); - visuals.graphics.destroy(); - visuals.text.destroy(); - this.itemVisuals.delete(itemId); - } - - /** - * Get the color assigned to an item (creates one if not yet assigned). - */ - getItemColor(itemId: string): number { - if (!this.colorMap.has(itemId)) { - this.colorMap.set( - itemId, - ITEM_COLORS[this.colorIdx++ % ITEM_COLORS.length], - ); - } - return this.colorMap.get(itemId)!; - } - - /** - * Compute the grid cells occupied by an item based on its shape and transform. - */ - getItemCells(item: InventoryItem): { x: number; y: number }[] { - const cells: { x: number; y: number }[] = []; - const { offset } = item.transform; - for (let y = 0; y < item.shape.height; y++) { - for (let x = 0; x < item.shape.width; x++) { - if (item.shape.grid[y]?.[x]) { - cells.push({ x: x + offset.x, y: y + offset.y }); - } - } - } - return cells; - } - - /** - * Check if visuals exist for a given item ID. - */ - hasItem(itemId: string): boolean { - return this.itemVisuals.has(itemId); - } - - /** - * Destroy all managed visuals and grid graphics. - */ - destroy(): void { - for (const visuals of this.itemVisuals.values()) { - visuals.container.destroy(); - visuals.graphics.destroy(); - visuals.text.destroy(); - } - this.itemVisuals.clear(); - this.colorMap.clear(); - this.gridGraphics.destroy(); - } -}