Files
boardgame-phaser/packages/sts-like-viewer/src/scenes/GridViewerScene.ts
T
2026-04-14 11:10:14 +08:00

257 lines
7.8 KiB
TypeScript

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<string>();
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);
}
}