diff --git a/packages/sts-like-viewer/src/scenes/PlaceholderEncounterScene.ts b/packages/sts-like-viewer/src/scenes/PlaceholderEncounterScene.ts index a418e66..fbf9553 100644 --- a/packages/sts-like-viewer/src/scenes/PlaceholderEncounterScene.ts +++ b/packages/sts-like-viewer/src/scenes/PlaceholderEncounterScene.ts @@ -6,12 +6,14 @@ import { type RunState, type EncounterResult, type MapNodeType, + type InventoryItem, + type GameItemMeta, } from 'boardgame-core/samples/slay-the-spire-like'; /** * 占位符遭遇场景 * - * 当前实现:从全局 gameState 读取当前遭遇信息并展示 + * 当前实现:从全局 gameState 读取当前遭遇信息并展示,左侧显示背包网格 * * 后续扩展:根据 encounter.type 路由到不同的专用遭遇场景 * - MapNodeType.Minion / Elite → CombatEncounterScene @@ -24,6 +26,12 @@ export class PlaceholderEncounterScene extends ReactiveScene { /** 全局游戏状态(由 App.tsx 注入) */ private gameState: MutableSignal; + // Grid constants + private readonly CELL_SIZE = 80; + private readonly GRID_PADDING = 40; + private gridOffsetX = 0; + private gridOffsetY = 0; + constructor(gameState: MutableSignal) { super('PlaceholderEncounterScene'); this.gameState = gameState; @@ -32,14 +40,20 @@ export class PlaceholderEncounterScene extends ReactiveScene { create(): void { super.create(); const { width, height } = this.scale; - const centerX = width / 2; - const centerY = height / 2; + const state = this.gameState.value; + + // Calculate grid position (left side, vertically centered) + this.gridOffsetX = this.GRID_PADDING; + const gridHeight = 4 * this.CELL_SIZE; + this.gridOffsetY = (height - gridHeight) / 2; + + // Draw inventory grid + this.drawInventoryGrid(); // Read encounter data from global state - const state = this.gameState.value; const node = state.map.nodes.get(state.currentNodeId); if (!node || !node.encounter) { - this.add.text(centerX, centerY, '没有遭遇数据', { + this.add.text(width / 2, height / 2, '没有遭遇数据', { fontSize: '24px', color: '#ff4444', }).setOrigin(0.5); @@ -53,6 +67,11 @@ export class PlaceholderEncounterScene extends ReactiveScene { }; const nodeId = node.id; + // Right side panel for encounter info + const rightPanelX = this.gridOffsetX + state.inventory.width * this.CELL_SIZE + 60; + const centerX = rightPanelX + 300; + const centerY = height / 2; + // Title this.add.text(centerX, centerY - 150, '遭遇', { fontSize: '32px', @@ -62,7 +81,7 @@ export class PlaceholderEncounterScene extends ReactiveScene { // Encounter type badge const typeLabel = this.getEncounterTypeLabel(encounter.type); - const typeBg = this.add.rectangle(centerX, centerY - 80, 120, 36, this.getEncounterTypeColor(encounter.type)); + this.add.rectangle(centerX, centerY - 80, 120, 36, this.getEncounterTypeColor(encounter.type)); this.add.text(centerX, centerY - 80, typeLabel, { fontSize: '16px', color: '#ffffff', @@ -79,7 +98,7 @@ export class PlaceholderEncounterScene extends ReactiveScene { this.add.text(centerX, centerY + 20, encounter.description || '(暂无描述)', { fontSize: '16px', color: '#aaaaaa', - wordWrap: { width: 600 }, + wordWrap: { width: 500 }, align: 'center', }).setOrigin(0.5); @@ -107,6 +126,98 @@ export class PlaceholderEncounterScene extends ReactiveScene { }); } + private drawInventoryGrid(): void { + const state = this.gameState.value; + const { width, height } = state.inventory; + + // Background panel for the grid area + const panelWidth = width * this.CELL_SIZE + 20; + const panelHeight = height * this.CELL_SIZE + 60; + const panelX = this.gridOffsetX - 10; + const panelY = this.gridOffsetY - 40; + + this.add.rectangle(panelX + panelWidth / 2, panelY + panelHeight / 2, panelWidth, panelHeight, 0x1a1a2e) + .setStrokeStyle(2, 0x333355); + + // Title + this.add.text(this.gridOffsetX + (width * this.CELL_SIZE) / 2, panelY + 10, '背包', { + fontSize: '18px', + color: '#ffffff', + fontStyle: 'bold', + }).setOrigin(0.5); + + const gridY = this.gridOffsetY + 15; + + // Draw empty cells + const graphics = this.add.graphics(); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const px = this.gridOffsetX + x * this.CELL_SIZE; + const py = gridY + y * this.CELL_SIZE; + + graphics.lineStyle(1, 0x444466); + graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE); + } + } + + // Draw items + const itemColors = [0x4488cc, 0xcc8844, 0x44cc88, 0xcc4488, 0x8844cc, 0x44cccc, 0xcccc44, 0x8888cc]; + let colorIndex = 0; + const itemColorMap = new Map(); + + for (const [itemId, item] of state.inventory.items) { + // Assign a color to this item + if (!itemColorMap.has(itemId)) { + itemColorMap.set(itemId, itemColors[colorIndex % itemColors.length]); + colorIndex++; + } + const itemColor = itemColorMap.get(itemId)!; + + // Get occupied cells for this item + const cells = this.getItemCells(item); + + // Draw filled cells + for (const cell of cells) { + const px = this.gridOffsetX + cell.x * this.CELL_SIZE; + const py = gridY + cell.y * this.CELL_SIZE; + + graphics.fillStyle(itemColor); + 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 label (at first cell) + if (cells.length > 0) { + const firstCell = cells[0]; + const px = this.gridOffsetX + firstCell.x * this.CELL_SIZE; + const py = gridY + firstCell.y * this.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: '12px', + color: '#ffffff', + fontStyle: 'bold', + }).setOrigin(0.5); + } + } + } + + private getItemCells(item: InventoryItem): { x: number; y: number }[] { + const cells: { x: number; y: number }[] = []; + const shape = item.shape; + const { offset } = item.transform; + + for (let y = 0; y < shape.height; y++) { + for (let x = 0; x < shape.width; x++) { + if (shape.grid[y]?.[x]) { + cells.push({ x: x + offset.x, y: y + offset.y }); + } + } + } + return cells; + } + private async completeEncounter(): Promise { const state = this.gameState.value;