feat: adding widgets to the game
This commit is contained in:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user