import Phaser from 'phaser'; import { createGridInventory, placeItem, getAdjacentItems, parseShapeString, heroItemFighter1Data, type GridInventory, type InventoryItem, } from 'boardgame-core/samples/slay-the-spire-like'; import { ReactiveScene } from 'boardgame-phaser'; export class GridViewerScene extends ReactiveScene { private inventory: GridInventory; private readonly CELL_SIZE = 60; private readonly GRID_WIDTH = 6; private readonly GRID_HEIGHT = 4; private gridOffsetX = 0; private gridOffsetY = 0; constructor() { super('GridViewerScene'); this.inventory = createGridInventory(this.GRID_WIDTH, this.GRID_HEIGHT); } create(): void { super.create(); const { width, height } = this.scale; this.gridOffsetX = (width - this.GRID_WIDTH * this.CELL_SIZE) / 2; this.gridOffsetY = (height - this.GRID_HEIGHT * this.CELL_SIZE) / 2 + 20; // Place some sample items this.placeSampleItems(); // Draw grid this.drawGrid(); // Draw items this.drawItems(); // Title this.add.text(width / 2, 30, 'Grid Inventory Viewer (4x6)', { fontSize: '24px', color: '#ffffff', fontStyle: 'bold', }).setOrigin(0.5); // Controls this.createControls(); // Info text this.add.text(width / 2, height - 40, 'Hover over cells to see item details', { fontSize: '14px', color: '#aaaaaa', }).setOrigin(0.5); } private placeSampleItems(): void { // Place a few items from heroItemFighter1Data 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) { const data = heroItemFighter1Data[index]; const shape = parseShapeString(data.shape); const item: InventoryItem = { id: `item-${index}`, shape, transform: { offset: { x, y }, rotation: 0, flipX: false, flipY: false }, meta: { name: data.name, data }, }; placeItem(this.inventory, item); } } private drawGrid(): void { const graphics = this.add.graphics(); // Draw cells for (let y = 0; y < this.GRID_HEIGHT; y++) { for (let x = 0; x < this.GRID_WIDTH; x++) { const px = this.gridOffsetX + x * this.CELL_SIZE; const py = this.gridOffsetY + y * this.CELL_SIZE; const isOccupied = this.inventory.occupiedCells.has(`${x},${y}`); const color = isOccupied ? 0x334455 : 0x222233; graphics.fillStyle(color); graphics.fillRect(px + 1, py + 1, this.CELL_SIZE - 2, this.CELL_SIZE - 2); graphics.lineStyle(1, 0x555577); graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE); } } } private drawItems(): void { for (const [itemId, item] of this.inventory.items) { const shape = item.shape; const transform = item.transform; // Get occupied cells 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 }); } } } // Draw item cells with a unique color per item const itemColor = this.getItemColor(itemId); const graphics = this.add.graphics(); for (const cell of cells) { const px = this.gridOffsetX + cell.x * this.CELL_SIZE; const py = this.gridOffsetY + cell.y * this.CELL_SIZE; graphics.fillStyle(itemColor); graphics.fillRect(px + 2, py + 2, this.CELL_SIZE - 4, this.CELL_SIZE - 4); } // Item name label (at first cell) if (cells.length > 0) { const firstCell = cells[0]; const px = this.gridOffsetX + firstCell.x * this.CELL_SIZE; const py = this.gridOffsetY + firstCell.y * this.CELL_SIZE; const itemName = (item.meta?.name as string) ?? item.id; this.add.text(px + this.CELL_SIZE / 2, py + this.CELL_SIZE / 2, itemName, { fontSize: '11px', color: '#ffffff', fontStyle: 'bold', }).setOrigin(0.5); } // Adjacent items info const adjacent = getAdjacentItems(this.inventory, itemId); if (adjacent.size > 0) { const adjacentNames = Array.from(adjacent.values()).map(i => (i.meta?.name as string) ?? i.id).join(', '); const firstCell = cells[0]; const px = this.gridOffsetX + firstCell.x * this.CELL_SIZE; const py = this.gridOffsetY + firstCell.y * this.CELL_SIZE - 20; this.add.text(px + this.CELL_SIZE / 2, py, `邻接: ${adjacentNames}`, { fontSize: '10px', color: '#ffff88', }).setOrigin(0.5); } } } private getItemColor(itemId: string): number { // Generate a consistent color based on item ID const hash = itemId.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0); const colors = [0x4488cc, 0xcc8844, 0x44cc88, 0xcc4488, 0x8844cc, 0x44cccc]; return colors[hash % colors.length]; } private createControls(): void { const { width, height } = this.scale; // Back button this.createButton('返回菜单', 100, 40, async () => { await this.sceneController.launch('IndexScene'); }); // Clear button this.createButton('清空', width - 260, 40, async () => { this.inventory = createGridInventory(this.GRID_WIDTH, this.GRID_HEIGHT); await this.sceneController.restart(); }); // Random fill button this.createButton('随机填充', width - 130, 40, async () => { this.randomFill(); await this.sceneController.restart(); }); } private randomFill(): void { this.inventory = createGridInventory(this.GRID_WIDTH, this.GRID_HEIGHT); // Try to place random items let itemIndex = 0; for (let y = 0; y < this.GRID_HEIGHT && itemIndex < heroItemFighter1Data.length; y++) { for (let x = 0; x < this.GRID_WIDTH && itemIndex < heroItemFighter1Data.length; x++) { const data = heroItemFighter1Data[itemIndex]; const shape = parseShapeString(data.shape); // Check if placement is valid 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}`); } } } // Simple check if any cell is out of bounds or occupied let valid = true; for (const cell of occupiedCells) { const [cx, cy] = cell.split(',').map(Number); if (cx >= this.GRID_WIDTH || cy >= this.GRID_HEIGHT || this.inventory.occupiedCells.has(cell)) { valid = false; break; } } if (valid) { const item: InventoryItem = { id: `item-${itemIndex}`, shape, transform: { offset: { x, y }, rotation: 0, flipX: false, flipY: false }, meta: { name: data.name, data }, }; placeItem(this.inventory, item); itemIndex++; } } } } private createButton(label: string, x: number, y: number, onClick: () => void): void { const buttonWidth = 120; const buttonHeight = 36; const bg = this.add.rectangle(x, y, buttonWidth, buttonHeight, 0x444466) .setStrokeStyle(2, 0x7777aa) .setInteractive({ useHandCursor: true }); const text = this.add.text(x, y, label, { fontSize: '16px', color: '#ffffff', }).setOrigin(0.5); bg.on('pointerover', () => { bg.setFillStyle(0x555588); text.setScale(1.05); }); bg.on('pointerout', () => { bg.setFillStyle(0x444466); text.setScale(1); }); bg.on('pointerdown', onClick); } }