Files
boardgame-phaser/packages/sts-like-viewer/src/scenes/GridViewerScene.ts
T
hypercross 033a8e4a40 refactor: clean up scenes and introduce SceneKey enum
- Remove redundant `GameFlowScene` and `PlaceholderEncounterScene`
- Introduce `SceneKey` enum for type-safe scene management
- Clean up unused imports and configuration references in existing
  scenes
- Standardize scene navigation using `SceneKey`
2026-04-20 15:36:12 +08:00

294 lines
7.9 KiB
TypeScript

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<GameItemMeta>;
private gridOffsetX = 0;
private gridOffsetY = 0;
constructor() {
super("GridViewerScene");
this.inventory = createGridInventory<GameItemMeta>(
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<GameItemMeta> = {
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<GameItemMeta>(
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<GameItemMeta>(
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<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}`);
}
}
}
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<GameItemMeta> = {
id: `item-${itemIndex}`,
shape,
transform: {
offset: { x, y },
rotation: 0,
flipX: false,
flipY: false,
},
meta: { itemData, shape },
};
placeItem(this.inventory, item);
itemIndex++;
}
}
}
}
}