import { ReactiveScene } from "boardgame-phaser"; import { createButton } from "@/utils/createButton"; import { GRID_CONFIG, ITEM_COLORS } from "@/config"; import { createGridInventory, placeItem, getAdjacentItems, parseShapeString, data, type GridInventory, type InventoryItem, type GameItemMeta, } from "boardgame-core/samples/slay-the-spire-like"; import { SceneKey } from "./types"; export class GridViewerScene extends ReactiveScene { private inventory: GridInventory; private gridOffsetX = 0; private gridOffsetY = 0; constructor() { super("GridViewerScene"); this.inventory = createGridInventory( GRID_CONFIG.WIDTH, GRID_CONFIG.HEIGHT, ); } create(): void { super.create(); 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 + 20; this.placeSampleItems(); this.drawGrid(); this.drawItems(); this.add .text(width / 2, 30, "Grid Inventory Viewer (4x6)", { fontSize: "24px", color: "#ffffff", fontStyle: "bold", }) .setOrigin(0.5); this.createControls(); this.add .text(width / 2, height - 40, "Hover over cells to see item details", { fontSize: "14px", color: "#aaaaaa", }) .setOrigin(0.5); } private placeSampleItems(): void { const items = data.desert.getItems(); const sampleItems = [ { index: 0, x: 0, y: 0 }, { index: 3, x: 3, y: 0 }, { index: 6, x: 0, y: 2 }, { index: 12, x: 3, y: 2 }, ]; for (const { index, x, y } of sampleItems) { if (index >= items.length) continue; const itemData = items[index]; const shape = parseShapeString(itemData.shape); const item: InventoryItem = { id: `item-${index}`, shape, transform: { offset: { x, y }, rotation: 0, flipX: false, flipY: false, }, meta: { itemData, shape }, }; placeItem(this.inventory, item); } } 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; const isOccupied = this.inventory.occupiedCells.has(`${x},${y}`); const color = isOccupied ? 0x334455 : 0x222233; graphics.fillStyle(color); 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 { for (const [itemId, item] of this.inventory.items) { 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); } const adjacent = getAdjacentItems(this.inventory, itemId); if (adjacent.size > 0) { const adjacentNames = Array.from(adjacent.values()) .map((i) => i.meta?.itemData.name ?? i.id) .join(", "); 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 - 20; this.add .text( px + GRID_CONFIG.VIEWER_CELL_SIZE / 2, py, `邻接: ${adjacentNames}`, { fontSize: "10px", color: "#ffff88", }, ) .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, height } = this.scale; createButton({ scene: this, label: "返回菜单", x: 100, y: 40, onClick: async () => { await this.sceneController.launch(SceneKey.IndexScene); }, }); createButton({ scene: this, label: "清空", x: width - 260, y: 40, onClick: async () => { this.inventory = createGridInventory( GRID_CONFIG.WIDTH, GRID_CONFIG.HEIGHT, ); await this.sceneController.restart(); }, }); createButton({ scene: this, label: "随机填充", x: width - 130, y: 40, onClick: async () => { this.randomFill(); await this.sceneController.restart(); }, }); } private randomFill(): void { this.inventory = createGridInventory( GRID_CONFIG.WIDTH, GRID_CONFIG.HEIGHT, ); const items = data.desert.getItems(); let itemIndex = 0; for (let y = 0; y < GRID_CONFIG.HEIGHT && itemIndex < items.length; y++) { for (let x = 0; x < GRID_CONFIG.WIDTH && itemIndex < items.length; x++) { const itemData = items[itemIndex]; const shape = parseShapeString(itemData.shape); const occupiedCells = new Set(); for (let sy = 0; sy < shape.height; sy++) { for (let sx = 0; sx < shape.width; sx++) { if (shape.grid[sy]?.[sx]) { occupiedCells.add(`${sx + x},${sy + y}`); } } } let valid = true; for (const cell of occupiedCells) { const [cx, cy] = cell.split(",").map(Number); if ( cx >= GRID_CONFIG.WIDTH || cy >= GRID_CONFIG.HEIGHT || this.inventory.occupiedCells.has(cell as `${number},${number}`) ) { valid = false; break; } } if (valid) { const item: InventoryItem = { id: `item-${itemIndex}`, shape, transform: { offset: { x, y }, rotation: 0, flipX: false, flipY: false, }, meta: { itemData, shape }, }; placeItem(this.inventory, item); itemIndex++; } } } } }