diff --git a/packages/sts-like-viewer/src/scenes/IndexScene.ts b/packages/sts-like-viewer/src/scenes/IndexScene.ts index 26d7bc8..c097614 100644 --- a/packages/sts-like-viewer/src/scenes/IndexScene.ts +++ b/packages/sts-like-viewer/src/scenes/IndexScene.ts @@ -49,6 +49,11 @@ export class IndexScene extends ReactiveScene { scene: SceneKey.ShapeViewerScene, y: centerY + 140, }, + { + label: "Inventory Test", + scene: SceneKey.InventoryTestScene, + y: centerY + 210, + }, ]; for (const btn of buttons) { diff --git a/packages/sts-like-viewer/src/scenes/InventoryTestScene.ts b/packages/sts-like-viewer/src/scenes/InventoryTestScene.ts new file mode 100644 index 0000000..07ec183 --- /dev/null +++ b/packages/sts-like-viewer/src/scenes/InventoryTestScene.ts @@ -0,0 +1,227 @@ +import { ReactiveScene } from "boardgame-phaser"; +import { createButton } from "@/utils/createButton"; +import { GRID_CONFIG, ITEM_COLORS } 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 { SceneKey } from "./types"; + +export class InventoryTestScene extends ReactiveScene { + private inventorySignal!: InventorySignal; + private gridOffsetX = 0; + private gridOffsetY = 0; + + constructor() { + super("InventoryTestScene"); + } + + 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; + + this.drawGrid(); + this.drawItems(); + + this.add + .text(width / 2, 30, "Inventory Signal Test (4x6)", { + fontSize: "24px", + color: "#ffffff", + fontStyle: "bold", + }) + .setOrigin(0.5); + + this.createControls(); + + this.add + .text(width / 2, height - 40, "Items update reactively via signals", { + fontSize: "14px", + color: "#aaaaaa", + }) + .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 { + const graphics = this.add.graphics(); + + for (let y = 0; y < GRID_CONFIG.HEIGHT; y++) { + for (let x = 0; x < GRID_CONFIG.WIDTH; x++) { + const px = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE; + const py = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE; + + graphics.fillStyle(0x222233); + graphics.fillRect( + px + 1, + py + 1, + GRID_CONFIG.VIEWER_CELL_SIZE - 2, + GRID_CONFIG.VIEWER_CELL_SIZE - 2, + ); + graphics.lineStyle(1, 0x555577); + graphics.strokeRect( + px, + py, + GRID_CONFIG.VIEWER_CELL_SIZE, + GRID_CONFIG.VIEWER_CELL_SIZE, + ); + } + } + } + + 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 createControls(): void { + const { width } = this.scale; + + createButton({ + scene: this, + label: "返回菜单", + x: 100, + y: 40, + onClick: async () => { + await this.sceneController.launch(SceneKey.IndexScene); + }, + }); + + createButton({ + scene: this, + label: "添加道具", + x: width - 300, + y: 40, + onClick: () => { + this.addRandomItem(); + }, + }); + + createButton({ + scene: this, + label: "移除最后一个", + x: width - 150, + y: 40, + onClick: () => { + this.removeLastItem(); + }, + }); + } + + private addRandomItem(): void { + const items = data.desert.getItems(); + + this.inventorySignal.produce((inventory) => { + const usedIndices = new Set(); + + for (const item of inventory.items.values()) { + const match = item.id.match(/^item-(\d+)-/); + if (match) { + usedIndices.add(parseInt(match[1], 10)); + } + } + + let availableIndex = 0; + while (usedIndices.has(availableIndex) && availableIndex < items.length) { + availableIndex++; + } + + if (availableIndex >= items.length) { + return; + } + + const itemData = items[availableIndex]; + const id = `item-${availableIndex}-${Date.now().toString(16).slice(-4)}`; + createItemIn(inventory, id, itemData); + }); + } + + private removeLastItem(): void { + this.inventorySignal.produce((inventory) => { + const items = Array.from(inventory.items.entries()); + + if (items.length === 0) { + return; + } + + const [lastId] = items[items.length - 1]; + inventory.items.delete(lastId); + }); + } +} diff --git a/packages/sts-like-viewer/src/scenes/types.ts b/packages/sts-like-viewer/src/scenes/types.ts index 8ee15a8..9c00f55 100644 --- a/packages/sts-like-viewer/src/scenes/types.ts +++ b/packages/sts-like-viewer/src/scenes/types.ts @@ -1,6 +1,7 @@ export enum SceneKey { GridViewerScene = "GridViewerScene", IndexScene = "IndexScene", + InventoryTestScene = "InventoryTestScene", MapViewerScene = "MapViewerScene", ShapeViewerScene = "ShapeViewerScene", } diff --git a/packages/sts-like-viewer/src/state/inventory.ts b/packages/sts-like-viewer/src/state/inventory.ts new file mode 100644 index 0000000..2010d5c --- /dev/null +++ b/packages/sts-like-viewer/src/state/inventory.ts @@ -0,0 +1,24 @@ +import { mutableSignal } from "boardgame-core"; +import { + createGridInventory, + createItemIn, + data, + GameItemMeta, +} from "boardgame-core/samples/slay-the-spire-like"; + +function genId() { + return Math.random().toString(16).slice(-8); +} + +export type InventorySignal = ReturnType; + +export function createInventorySignal() { + const inventory = createGridInventory(4, 6); + + const startingItems = data.desert.getStartingItems(); + for (const data of startingItems) { + createItemIn(inventory, `${data.id}-${genId()}`, data); + } + + return mutableSignal(inventory); +}