Refactor: centralize config and split inventory

Move magic numbers and style constants to src/config.ts.
Decompose InventoryWidget into DragController, ItemRenderer,
and LostItemManager for better separation of concerns. Add
createButton utility and remove unused CombatUnitWidget.
Update scenes to import from centralized config.
This commit is contained in:
2026-04-19 00:01:25 +08:00
parent d993d55576
commit 88d0c5bf55
13 changed files with 1834 additions and 1186 deletions
@@ -1,4 +1,7 @@
import Phaser from 'phaser';
import Phaser from "phaser";
import { ReactiveScene } from "boardgame-phaser";
import { createButton } from "@/utils/createButton";
import { GRID_CONFIG, UI_CONFIG, ITEM_COLORS } from "@/config";
import {
createGridInventory,
placeItem,
@@ -8,44 +11,49 @@ import {
type GridInventory,
type InventoryItem,
type GameItemMeta,
} from 'boardgame-core/samples/slay-the-spire-like';
import { ReactiveScene } from 'boardgame-phaser';
} from "boardgame-core/samples/slay-the-spire-like";
export class GridViewerScene extends ReactiveScene {
private inventory: GridInventory<GameItemMeta>;
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<GameItemMeta>(this.GRID_WIDTH, this.GRID_HEIGHT);
super("GridViewerScene");
this.inventory = createGridInventory<GameItemMeta>(
GRID_CONFIG.WIDTH,
GRID_CONFIG.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;
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.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);
this.add
.text(width / 2, height - 40, "Hover over cells to see item details", {
fontSize: "14px",
color: "#aaaaaa",
})
.setOrigin(0.5);
}
private placeSampleItems(): void {
@@ -64,7 +72,12 @@ export class GridViewerScene extends ReactiveScene {
const item: InventoryItem<GameItemMeta> = {
id: `item-${index}`,
shape,
transform: { offset: { x, y }, rotation: 0, flipX: false, flipY: false },
transform: {
offset: { x, y },
rotation: 0,
flipX: false,
flipY: false,
},
meta: { itemData, shape },
};
placeItem(this.inventory, item);
@@ -74,18 +87,28 @@ export class GridViewerScene extends ReactiveScene {
private drawGrid(): void {
const graphics = this.add.graphics();
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;
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, this.CELL_SIZE - 2, this.CELL_SIZE - 2);
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, this.CELL_SIZE, this.CELL_SIZE);
graphics.strokeRect(
px,
py,
GRID_CONFIG.VIEWER_CELL_SIZE,
GRID_CONFIG.VIEWER_CELL_SIZE,
);
}
}
}
@@ -110,72 +133,120 @@ export class GridViewerScene extends ReactiveScene {
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;
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, this.CELL_SIZE - 4, this.CELL_SIZE - 4);
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 * this.CELL_SIZE;
const py = this.gridOffsetY + firstCell.y * this.CELL_SIZE;
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 + this.CELL_SIZE / 2, py + this.CELL_SIZE / 2, itemName, {
fontSize: '11px',
color: '#ffffff',
fontStyle: 'bold',
}).setOrigin(0.5);
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 adjacentNames = Array.from(adjacent.values())
.map((i) => i.meta?.itemData.name ?? 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;
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 + this.CELL_SIZE / 2, py, `邻接: ${adjacentNames}`, {
fontSize: '10px',
color: '#ffff88',
}).setOrigin(0.5);
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);
const colors = [0x4488cc, 0xcc8844, 0x44cc88, 0xcc4488, 0x8844cc, 0x44cccc];
return colors[hash % colors.length];
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;
this.createButton('返回菜单', 100, 40, async () => {
await this.sceneController.launch('IndexScene');
createButton({
scene: this,
label: "返回菜单",
x: 100,
y: 40,
onClick: async () => {
await this.sceneController.launch("IndexScene");
},
});
this.createButton('清空', width - 260, 40, async () => {
this.inventory = createGridInventory<GameItemMeta>(this.GRID_WIDTH, this.GRID_HEIGHT);
await this.sceneController.restart();
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();
},
});
this.createButton('随机填充', width - 130, 40, async () => {
this.randomFill();
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>(this.GRID_WIDTH, this.GRID_HEIGHT);
this.inventory = createGridInventory<GameItemMeta>(
GRID_CONFIG.WIDTH,
GRID_CONFIG.HEIGHT,
);
const items = data.desert.items;
let itemIndex = 0;
for (let y = 0; y < this.GRID_HEIGHT && itemIndex < items.length; y++) {
for (let x = 0; x < this.GRID_WIDTH && itemIndex < items.length; x++) {
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);
@@ -190,8 +261,12 @@ export class GridViewerScene extends ReactiveScene {
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 as `${number},${number}`)) {
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;
}
@@ -201,7 +276,12 @@ export class GridViewerScene extends ReactiveScene {
const item: InventoryItem<GameItemMeta> = {
id: `item-${itemIndex}`,
shape,
transform: { offset: { x, y }, rotation: 0, flipX: false, flipY: false },
transform: {
offset: { x, y },
rotation: 0,
flipX: false,
flipY: false,
},
meta: { itemData, shape },
};
placeItem(this.inventory, item);
@@ -210,30 +290,4 @@ export class GridViewerScene extends ReactiveScene {
}
}
}
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);
}
}