diff --git a/packages/sts-like-viewer/src/config/index.ts b/packages/sts-like-viewer/src/config/index.ts index a8d016f..5cb099f 100644 --- a/packages/sts-like-viewer/src/config/index.ts +++ b/packages/sts-like-viewer/src/config/index.ts @@ -33,6 +33,29 @@ export const GRID_CONFIG = { WIDGET_CELL_SIZE: 80, /** Gap between grid cells (pixels) */ GRID_GAP: 2, + /** Empty cell background color */ + CELL_EMPTY_COLOR: 0x222233, + /** Occupied cell background color */ + CELL_OCCUPIED_COLOR: 0x334455, + /** Grid line color */ + GRID_LINE_COLOR: 0x555577, + /** Title text style */ + TITLE_STYLE: { + fontSize: "24px", + color: "#ffffff", + fontStyle: "bold", + } as const, + /** Subtitle/hint text style */ + SUBTITLE_STYLE: { + fontSize: "14px", + color: "#aaaaaa", + } as const, + /** Item name text style */ + ITEM_NAME_STYLE: { + fontSize: "11px", + color: "#ffffff", + fontStyle: "bold", + } as const, } as const; // ── Shape Viewer ──────────────────────────────────────────────────────────── diff --git a/packages/sts-like-viewer/src/scenes/InventoryItemSpawner.ts b/packages/sts-like-viewer/src/scenes/InventoryItemSpawner.ts new file mode 100644 index 0000000..f0e0d1d --- /dev/null +++ b/packages/sts-like-viewer/src/scenes/InventoryItemSpawner.ts @@ -0,0 +1,142 @@ +import Phaser from "phaser"; +import { spawnEffect, type Spawner } from "boardgame-phaser"; +import { GRID_CONFIG, ITEM_COLORS } from "@/config"; +import type { + GameItemMeta, + InventoryItem, +} from "boardgame-core/samples/slay-the-spire-like"; +import type { InventorySignal } from "@/state/inventory"; + +export class InventoryItemSpawner implements Spawner< + [string, InventoryItem], + Phaser.GameObjects.Container +> { + constructor( + private scene: Phaser.Scene, + private inventorySignal: InventorySignal, + private gridOffsetX: number, + private gridOffsetY: number, + ) {} + + *getData(): Iterable<[string, InventoryItem]> { + const inventory = this.inventorySignal.value; + yield* inventory.items.entries(); + } + + getKey(entry: [string, InventoryItem]): string { + return entry[0]; + } + + private getCells( + item: InventoryItem, + ): { x: number; y: number }[] { + const cells: { x: number; y: number }[] = []; + const shape = item.shape; + const transform = item.transform; + + for (let y = 0; y < shape.height; y++) { + for (let x = 0; x < shape.width; x++) { + if (shape.grid[y]?.[x]) { + const finalX = x + transform.offset.x; + const finalY = y + transform.offset.y; + cells.push({ x: finalX, y: finalY }); + } + } + } + return cells; + } + + onSpawn( + entry: [string, InventoryItem], + ): Phaser.GameObjects.Container | null { + const [itemId, item] = entry; + const container = this.scene.add.container(0, 0); + const cells = this.getCells(item); + + const itemColor = this.getItemColor(itemId); + const graphics = this.scene.add.graphics(); + + for (const cell of cells) { + const px = this.gridOffsetX + cell.x * GRID_CONFIG.VIEWER_CELL_SIZE; + const py = this.gridOffsetY + cell.y * GRID_CONFIG.VIEWER_CELL_SIZE; + + graphics.fillStyle(itemColor); + graphics.fillRect( + px + 2, + py + 2, + GRID_CONFIG.VIEWER_CELL_SIZE - 4, + GRID_CONFIG.VIEWER_CELL_SIZE - 4, + ); + } + + if (cells.length > 0) { + const firstCell = cells[0]; + const px = this.gridOffsetX + firstCell.x * GRID_CONFIG.VIEWER_CELL_SIZE; + const py = this.gridOffsetY + firstCell.y * GRID_CONFIG.VIEWER_CELL_SIZE; + const itemName = item.meta?.itemData.name ?? item.id; + + const text = this.scene.add.text( + px + GRID_CONFIG.VIEWER_CELL_SIZE / 2, + py + GRID_CONFIG.VIEWER_CELL_SIZE / 2, + itemName, + GRID_CONFIG.ITEM_NAME_STYLE, + ); + text.setOrigin(0.5); + + container.add([graphics, text]); + } else { + container.add(graphics); + } + + return container; + } + + onUpdate( + entry: [string, InventoryItem], + obj: Phaser.GameObjects.Container, + ): void { + const [, item] = entry; + const cells = this.getCells(item); + + if (cells.length > 0) { + const firstCell = cells[0]; + const px = this.gridOffsetX + firstCell.x * GRID_CONFIG.VIEWER_CELL_SIZE; + const py = this.gridOffsetY + firstCell.y * GRID_CONFIG.VIEWER_CELL_SIZE; + + this.scene.tweens.add({ + targets: obj, + x: px, + y: py, + duration: 200, + ease: "Power2", + }); + } + } + + onDespawn(obj: Phaser.GameObjects.Container): void { + this.scene.tweens.add({ + targets: obj, + alpha: 0, + scale: 0.5, + duration: 200, + ease: "Back.easeIn", + onComplete: () => obj.destroy(), + }); + } + + private getItemColor(itemId: string): number { + const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0); + return ITEM_COLORS[hash % ITEM_COLORS.length]; + } +} + +export function createInventoryItemSpawner( + scene: Phaser.Scene, + inventorySignal: InventorySignal, + gridOffsetX: number, + gridOffsetY: number, +) { + return spawnEffect( + new InventoryItemSpawner(scene, inventorySignal, gridOffsetX, gridOffsetY), + ); +} diff --git a/packages/sts-like-viewer/src/scenes/InventoryTestScene.ts b/packages/sts-like-viewer/src/scenes/InventoryTestScene.ts index 07ec183..1eb8435 100644 --- a/packages/sts-like-viewer/src/scenes/InventoryTestScene.ts +++ b/packages/sts-like-viewer/src/scenes/InventoryTestScene.ts @@ -1,18 +1,13 @@ import { ReactiveScene } from "boardgame-phaser"; import { createButton } from "@/utils/createButton"; -import { GRID_CONFIG, ITEM_COLORS } from "@/config"; +import { GRID_CONFIG } from "@/config"; import { createInventorySignal } from "@/state/inventory"; -import { - createItemIn, - data, - type GameItemMeta, -} from "boardgame-core/samples/slay-the-spire-like"; -import type { InventorySignal } from "@/state/inventory"; -import type { InventoryItem } from "boardgame-core/samples/slay-the-spire-like"; +import { createItemIn, data } from "boardgame-core/samples/slay-the-spire-like"; +import { createInventoryItemSpawner } from "./InventoryItemSpawner"; import { SceneKey } from "./types"; export class InventoryTestScene extends ReactiveScene { - private inventorySignal!: InventorySignal; + private inventorySignal = createInventorySignal(); private gridOffsetX = 0; private gridOffsetY = 0; @@ -23,59 +18,62 @@ export class InventoryTestScene extends ReactiveScene { create(): void { super.create(); - this.inventorySignal = createInventorySignal(); - const { width, height } = this.scale; - this.gridOffsetX = - (width - GRID_CONFIG.WIDTH * GRID_CONFIG.VIEWER_CELL_SIZE) / 2; - this.gridOffsetY = - (height - GRID_CONFIG.HEIGHT * GRID_CONFIG.VIEWER_CELL_SIZE) / 2 + 40; + const inventory = this.inventorySignal.value; + const invWidth = inventory.width; + const invHeight = inventory.height; - this.drawGrid(); - this.drawItems(); + this.gridOffsetX = (width - invWidth * GRID_CONFIG.VIEWER_CELL_SIZE) / 2; + this.gridOffsetY = + (height - invHeight * GRID_CONFIG.VIEWER_CELL_SIZE) / 2 + 40; + + this.drawGrid(invWidth, invHeight); + this.setupItemSpawner(); this.add - .text(width / 2, 30, "Inventory Signal Test (4x6)", { - fontSize: "24px", - color: "#ffffff", - fontStyle: "bold", - }) + .text( + width / 2, + 30, + "Inventory Signal Test (4x6)", + GRID_CONFIG.TITLE_STYLE, + ) .setOrigin(0.5); this.createControls(); this.add - .text(width / 2, height - 40, "Items update reactively via signals", { - fontSize: "14px", - color: "#aaaaaa", - }) + .text( + width / 2, + height - 40, + "Items update reactively via signals", + GRID_CONFIG.SUBTITLE_STYLE, + ) .setOrigin(0.5); - - // React to inventory changes by re-rendering - this.addEffect(() => { - const inventory = this.inventorySignal.value; - // Track item count for reactivity - const itemCount = inventory.items.size; - return () => {}; - }); } - private drawGrid(): void { + private drawGrid(invWidth: number, invHeight: number): void { const graphics = this.add.graphics(); - for (let y = 0; y < GRID_CONFIG.HEIGHT; y++) { - for (let x = 0; x < GRID_CONFIG.WIDTH; x++) { + for (let y = 0; y < invHeight; y++) { + for (let x = 0; x < invWidth; x++) { const px = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE; const py = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE; - graphics.fillStyle(0x222233); + const isOccupied = this.inventorySignal.value.occupiedCells.has( + `${x},${y}`, + ); + graphics.fillStyle( + isOccupied + ? GRID_CONFIG.CELL_OCCUPIED_COLOR + : GRID_CONFIG.CELL_EMPTY_COLOR, + ); graphics.fillRect( px + 1, py + 1, GRID_CONFIG.VIEWER_CELL_SIZE - 2, GRID_CONFIG.VIEWER_CELL_SIZE - 2, ); - graphics.lineStyle(1, 0x555577); + graphics.lineStyle(1, GRID_CONFIG.GRID_LINE_COLOR); graphics.strokeRect( px, py, @@ -86,68 +84,14 @@ export class InventoryTestScene extends ReactiveScene { } } - private drawItems(): void { - const inventory = this.inventorySignal.value; - for (const [itemId, item] of inventory.items) { - this.drawItem(itemId, item); - } - } - - private drawItem(itemId: string, item: InventoryItem): void { - const shape = item.shape; - const transform = item.transform; - - const cells: { x: number; y: number }[] = []; - for (let y = 0; y < shape.height; y++) { - for (let x = 0; x < shape.width; x++) { - if (shape.grid[y]?.[x]) { - const finalX = x + transform.offset.x; - const finalY = y + transform.offset.y; - cells.push({ x: finalX, y: finalY }); - } - } - } - - const itemColor = this.getItemColor(itemId); - const graphics = this.add.graphics(); - - for (const cell of cells) { - const px = this.gridOffsetX + cell.x * GRID_CONFIG.VIEWER_CELL_SIZE; - const py = this.gridOffsetY + cell.y * GRID_CONFIG.VIEWER_CELL_SIZE; - - graphics.fillStyle(itemColor); - graphics.fillRect( - px + 2, - py + 2, - GRID_CONFIG.VIEWER_CELL_SIZE - 4, - GRID_CONFIG.VIEWER_CELL_SIZE - 4, - ); - } - - if (cells.length > 0) { - const firstCell = cells[0]; - const px = this.gridOffsetX + firstCell.x * GRID_CONFIG.VIEWER_CELL_SIZE; - const py = this.gridOffsetY + firstCell.y * GRID_CONFIG.VIEWER_CELL_SIZE; - const itemName = item.meta?.itemData.name ?? item.id; - - this.add - .text( - px + GRID_CONFIG.VIEWER_CELL_SIZE / 2, - py + GRID_CONFIG.VIEWER_CELL_SIZE / 2, - itemName, - { - fontSize: "11px", - color: "#ffffff", - fontStyle: "bold", - }, - ) - .setOrigin(0.5); - } - } - - 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 setupItemSpawner(): void { + const spawner = createInventoryItemSpawner( + this, + this.inventorySignal, + this.gridOffsetX, + this.gridOffsetY, + ); + this.disposables.add(spawner); } private createControls(): void { diff --git a/packages/sts-like-viewer/src/ui/App.tsx b/packages/sts-like-viewer/src/ui/App.tsx index f212e20..cf14ba3 100644 --- a/packages/sts-like-viewer/src/ui/App.tsx +++ b/packages/sts-like-viewer/src/ui/App.tsx @@ -4,6 +4,7 @@ import { MapViewerScene } from "@/scenes/MapViewerScene"; import { GridViewerScene } from "@/scenes/GridViewerScene"; import { ShapeViewerScene } from "@/scenes/ShapeViewerScene"; import { GAME_CONFIG } from "@/config"; +import { InventoryTestScene } from "@/scenes/InventoryTestScene"; export default function App() { return ( @@ -11,6 +12,7 @@ export default function App() {
+