feat: adding widgets to the game

This commit is contained in:
2026-04-17 18:08:51 +08:00
parent f7a6154c68
commit 28a2623bd1
3 changed files with 690 additions and 115 deletions
@@ -3,26 +3,21 @@ import { ReactiveScene } from 'boardgame-phaser';
import { MutableSignal } from 'boardgame-core';
import {
resolveEncounter,
removeItem,
type RunState,
type EncounterResult,
type MapNodeType,
type InventoryItem,
type GameItemMeta,
} from 'boardgame-core/samples/slay-the-spire-like';
import { InventoryWidget } from '@/widgets/InventoryWidget';
/**
* 占位符遭遇场景
*
* 左侧显示背包网格(80x80 每格),右侧显示遭遇信息。
* 左侧显示背包网格(使用 InventoryWidget),右侧显示遭遇信息。
*/
export class PlaceholderEncounterScene extends ReactiveScene {
private gameState: MutableSignal<RunState>;
// Grid constants
private readonly CELL_SIZE = 80;
private readonly GRID_GAP = 2;
private gridX = 0;
private gridY = 0;
private inventoryWidget: InventoryWidget | null = null;
constructor(gameState: MutableSignal<RunState>) {
super('PlaceholderEncounterScene');
@@ -34,24 +29,37 @@ export class PlaceholderEncounterScene extends ReactiveScene {
const { width, height } = this.scale;
const state = this.gameState.value;
// ── Layout: split screen into left (grid) and right (encounter) ──
const gridCols = state.inventory.width; // 6
const gridRows = state.inventory.height; // 4
const gridW = gridCols * this.CELL_SIZE;
const gridH = gridRows * this.CELL_SIZE;
const leftPanelW = gridW + 40; // panel padding
const gridCols = state.inventory.width;
const gridRows = state.inventory.height;
const cellSize = 80;
const gridW = gridCols * cellSize + (gridCols - 1) * 2;
const gridH = gridRows * cellSize + (gridRows - 1) * 2;
const leftPanelW = gridW + 40;
this.gridX = 60;
this.gridY = (height - gridH) / 2 + 20;
this.inventoryWidget = new InventoryWidget({
scene: this,
gameState: this.gameState,
x: 60,
y: (height - gridH) / 2 + 20,
cellSize,
gridGap: 2,
});
// Ensure camera shows the full grid
this.cameras.main.setBounds(0, 0, width, height);
this.cameras.main.setScroll(0, 0);
// ── LEFT PANEL: inventory grid ──
this.drawLeftPanel(leftPanelW, gridW, gridH);
// Panel background
this.add.rectangle(
60 + leftPanelW / 2, this.inventoryWidgetY(gridH),
leftPanelW + 10, gridH + 50,
0x111122, 0.9
).setStrokeStyle(2, 0x5555aa);
// "背包" title
this.add.text(60 + gridW / 2, (height - gridH) / 2, '背包', {
fontSize: '22px', color: '#ffffff', fontStyle: 'bold',
}).setOrigin(0.5);
// ── RIGHT PANEL: encounter info ──
const node = state.map.nodes.get(state.currentNodeId);
if (!node || !node.encounter) {
const rightX = leftPanelW + 80;
@@ -64,80 +72,11 @@ export class PlaceholderEncounterScene extends ReactiveScene {
this.drawRightPanel(node, leftPanelW, width, height);
}
// ───────────────────── LEFT PANEL ─────────────────────
private drawLeftPanel(panelW: number, gridW: number, gridH: number): void {
// Panel background
this.add.rectangle(
this.gridX + panelW / 2, this.gridY + gridH / 2,
panelW + 10, gridH + 50,
0x111122, 0.9
).setStrokeStyle(2, 0x5555aa);
// "背包" title
this.add.text(this.gridX + gridW / 2, this.gridY - 20, '背包', {
fontSize: '22px', color: '#ffffff', fontStyle: 'bold',
}).setOrigin(0.5);
const graphics = this.add.graphics();
// Draw empty cell backgrounds
for (let y = 0; y < 4; y++) {
for (let x = 0; x < 6; x++) {
const px = this.gridX + x * this.CELL_SIZE;
const py = this.gridY + y * this.CELL_SIZE;
// Dark cell fill
graphics.fillStyle(0x1a1a2e);
graphics.fillRect(px + 1, py + 1, this.CELL_SIZE - 2, this.CELL_SIZE - 2);
// Cell border
graphics.lineStyle(2, 0x444477);
graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE);
}
}
// Draw items on top
this.drawItems(graphics);
private inventoryWidgetY(gridH: number): number {
const { height } = this.scale;
return (height - gridH) / 2 + 20 + gridH / 2;
}
private drawItems(graphics: Phaser.GameObjects.Graphics): void {
const state = this.gameState.value;
const palette = [0x3388ff, 0xff8833, 0x33ff88, 0xff3388, 0x8833ff, 0x33ffff, 0xffff33, 0xff6633];
const colorMap = new Map<string, number>();
let idx = 0;
for (const [id, item] of state.inventory.items) {
const color = colorMap.get(id) ?? palette[idx++ % palette.length];
colorMap.set(id, color);
const cells = this.getItemCells(item);
if (cells.length === 0) continue;
// Filled cells
for (const c of cells) {
const px = this.gridX + c.x * this.CELL_SIZE;
const py = this.gridY + c.y * this.CELL_SIZE;
graphics.fillStyle(color);
graphics.fillRect(px + 2, py + 2, this.CELL_SIZE - 4, this.CELL_SIZE - 4);
graphics.lineStyle(2, 0xffffff);
graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE);
}
// Item name
const first = cells[0];
const name = item.meta?.itemData.name ?? item.id;
this.add.text(
this.gridX + first.x * this.CELL_SIZE + this.CELL_SIZE / 2,
this.gridY + first.y * this.CELL_SIZE + this.CELL_SIZE / 2,
name, { fontSize: '12px', color: '#fff', fontStyle: 'bold' }
).setOrigin(0.5);
}
}
// ───────────────────── RIGHT PANEL ─────────────────────
private drawRightPanel(node: any, leftPanelW: number, width: number, height: number): void {
const encounter = {
type: node.type as MapNodeType,
@@ -151,12 +90,10 @@ export class PlaceholderEncounterScene extends ReactiveScene {
const cx = rightX + rightW / 2;
const cy = height / 2;
// Title
this.add.text(cx, cy - 180, '遭遇', {
fontSize: '36px', color: '#fff', fontStyle: 'bold',
}).setOrigin(0.5);
// Type badge
const typeLabel = this.getTypeLabel(encounter.type);
const badgeColor = this.getTypeColor(encounter.type);
this.add.rectangle(cx, cy - 110, 140, 40, badgeColor);
@@ -164,28 +101,23 @@ export class PlaceholderEncounterScene extends ReactiveScene {
fontSize: '18px', color: '#fff', fontStyle: 'bold',
}).setOrigin(0.5);
// Name
this.add.text(cx, cy - 50, encounter.name, {
fontSize: '28px', color: '#fff',
}).setOrigin(0.5);
// Description
this.add.text(cx, cy + 10, encounter.description || '(暂无描述)', {
fontSize: '18px', color: '#bbb',
wordWrap: { width: rightW - 40 }, align: 'center',
}).setOrigin(0.5);
// Node id
this.add.text(cx, cy + 80, `节点: ${nodeId}`, {
fontSize: '14px', color: '#666',
}).setOrigin(0.5);
// Placeholder notice
this.add.text(cx, cy + 130, '(此为占位符遭遇,后续将替换为真实遭遇场景)', {
fontSize: '14px', color: '#ff8844', fontStyle: 'italic',
}).setOrigin(0.5);
// Buttons
this.createButton('完成遭遇', cx, cy + 200, 220, 50, async () => {
await this.completeEncounter();
});
@@ -194,21 +126,6 @@ export class PlaceholderEncounterScene extends ReactiveScene {
});
}
// ───────────────────── Helpers ─────────────────────
private getItemCells(item: InventoryItem<GameItemMeta>): { x: number; y: number }[] {
const cells: { x: number; y: number }[] = [];
const { offset } = item.transform;
for (let y = 0; y < item.shape.height; y++) {
for (let x = 0; x < item.shape.width; x++) {
if (item.shape.grid[y]?.[x]) {
cells.push({ x: x + offset.x, y: y + offset.y });
}
}
}
return cells;
}
private getTypeLabel(type: MapNodeType): string {
const m: Record<MapNodeType, string> = {
start: '起点', end: '终点', minion: '战斗', elite: '精英战斗',
@@ -240,6 +157,15 @@ export class PlaceholderEncounterScene extends ReactiveScene {
const node = state.map.nodes.get(state.currentNodeId);
if (!node || !node.encounter) return;
// Clear lost items from inventory
if (this.inventoryWidget) {
const lostIds = this.inventoryWidget.getLostItems();
for (const lostId of lostIds) {
removeItem(state, lostId);
}
this.inventoryWidget.clearLostItems();
}
const result: EncounterResult = this.generatePlaceholderResult(node.type);
resolveEncounter(state, result);
await this.sceneController.launch('GameFlowScene');