diff --git a/packages/sts-like-viewer/src/scenes/PlaceholderEncounterScene.ts b/packages/sts-like-viewer/src/scenes/PlaceholderEncounterScene.ts index fbf9553..e2ed79e 100644 --- a/packages/sts-like-viewer/src/scenes/PlaceholderEncounterScene.ts +++ b/packages/sts-like-viewer/src/scenes/PlaceholderEncounterScene.ts @@ -13,24 +13,16 @@ import { /** * 占位符遭遇场景 * - * 当前实现:从全局 gameState 读取当前遭遇信息并展示,左侧显示背包网格 - * - * 后续扩展:根据 encounter.type 路由到不同的专用遭遇场景 - * - MapNodeType.Minion / Elite → CombatEncounterScene - * - MapNodeType.Shop → ShopEncounterScene - * - MapNodeType.Camp → CampEncounterScene - * - MapNodeType.Event → EventEncounterScene - * - MapNodeType.Curio → CurioEncounterScene + * 左侧显示背包网格(80x80 每格),右侧显示遭遇信息。 */ 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; + private readonly GRID_GAP = 2; + private gridX = 0; + private gridY = 0; constructor(gameState: MutableSignal) { super('PlaceholderEncounterScene'); @@ -42,175 +34,174 @@ export class PlaceholderEncounterScene extends ReactiveScene { const { width, height } = this.scale; 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; + // ── 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 - // Draw inventory grid - this.drawInventoryGrid(); + this.gridX = 60; + this.gridY = (height - gridH) / 2 + 20; - // Read encounter data from global state + // 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); + + // ── RIGHT PANEL: encounter info ── const node = state.map.nodes.get(state.currentNodeId); if (!node || !node.encounter) { - this.add.text(width / 2, height / 2, '没有遭遇数据', { - fontSize: '24px', - color: '#ff4444', + const rightX = leftPanelW + 80; + this.add.text(rightX + 300, height / 2, '没有遭遇数据', { + fontSize: '24px', color: '#ff4444', }).setOrigin(0.5); return; } - const encounter = { - type: node.type, - name: node.encounter.name, - description: node.encounter.description, - }; - 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', - color: '#ffffff', - fontStyle: 'bold', - }).setOrigin(0.5); - - // Encounter type badge - const typeLabel = this.getEncounterTypeLabel(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', - fontStyle: 'bold', - }).setOrigin(0.5); - - // Encounter name - this.add.text(centerX, centerY - 30, encounter.name, { - fontSize: '24px', - color: '#ffffff', - }).setOrigin(0.5); - - // Encounter description - this.add.text(centerX, centerY + 20, encounter.description || '(暂无描述)', { - fontSize: '16px', - color: '#aaaaaa', - wordWrap: { width: 500 }, - align: 'center', - }).setOrigin(0.5); - - // Node ID info - this.add.text(centerX, centerY + 80, `节点: ${nodeId}`, { - fontSize: '12px', - color: '#666666', - }).setOrigin(0.5); - - // Placeholder notice - this.add.text(centerX, centerY + 130, '(此为占位符遭遇,后续将替换为真实遭遇场景)', { - fontSize: '14px', - color: '#ff8844', - fontStyle: 'italic', - }).setOrigin(0.5); - - // Complete button - this.createButton('完成遭遇', centerX, centerY + 200, 200, 50, async () => { - await this.completeEncounter(); - }); - - // Back button (without resolving) - this.createButton('暂不处理', centerX, centerY + 270, 200, 40, async () => { - await this.sceneController.launch('GameFlowScene'); - }); + this.drawRightPanel(node, leftPanelW, width, height); } - private drawInventoryGrid(): void { - const state = this.gameState.value; - const { width, height } = state.inventory; + // ───────────────────── LEFT PANEL ───────────────────── - // 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; + 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); - 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', + // "背包" title + this.add.text(this.gridX + gridW / 2, this.gridY - 20, '背包', { + fontSize: '22px', 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); + // 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 - const itemColors = [0x4488cc, 0xcc8844, 0x44cc88, 0xcc4488, 0x8844cc, 0x44cccc, 0xcccc44, 0x8888cc]; - let colorIndex = 0; - const itemColorMap = new Map(); + // Draw items on top + this.drawItems(graphics); + } - 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)!; + 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(); + let idx = 0; + + for (const [id, item] of state.inventory.items) { + const color = colorMap.get(id) ?? palette[idx++ % palette.length]; + colorMap.set(id, color); - // Get occupied cells for this item const cells = this.getItemCells(item); + if (cells.length === 0) continue; - // 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; + // 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(itemColor); + 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 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); - } + // 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, + name: node.encounter.name as string, + description: node.encounter.description as string, + }; + const nodeId = node.id as string; + + const rightX = leftPanelW + 60; + const rightW = width - rightX - 40; + 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); + this.add.text(cx, cy - 110, typeLabel, { + 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(); + }); + this.createButton('暂不处理', cx, cy + 270, 220, 40, async () => { + await this.sceneController.launch('GameFlowScene'); + }); + } + + // ───────────────────── Helpers ───────────────────── + 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]) { + 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 }); } } @@ -218,96 +209,51 @@ export class PlaceholderEncounterScene extends ReactiveScene { return cells; } + private getTypeLabel(type: MapNodeType): string { + const m: Record = { + start: '起点', end: '终点', minion: '战斗', elite: '精英战斗', + event: '事件', camp: '营地', shop: '商店', curio: '奇遇', + }; + return m[type] ?? type; + } + + private getTypeColor(type: MapNodeType): number { + const m: Record = { + start: 0x44aa44, end: 0xcc8844, minion: 0xcc4444, elite: 0xcc44cc, + event: 0xaaaa44, camp: 0x44cccc, shop: 0x4488cc, curio: 0x8844cc, + }; + return m[type] ?? 0x888888; + } + + private createButton(label: string, x: number, y: number, w: number, h: number, onClick: () => void): void { + const bg = this.add.rectangle(x, y, w, h, 0x444466) + .setStrokeStyle(2, 0x7777aa).setInteractive({ useHandCursor: true }); + const txt = this.add.text(x, y, label, { fontSize: '16px', color: '#fff' }).setOrigin(0.5); + + bg.on('pointerover', () => { bg.setFillStyle(0x555588); txt.setScale(1.05); }); + bg.on('pointerout', () => { bg.setFillStyle(0x444466); txt.setScale(1); }); + bg.on('pointerdown', onClick); + } + private async completeEncounter(): Promise { const state = this.gameState.value; - - // Get current encounter info const node = state.map.nodes.get(state.currentNodeId); if (!node || !node.encounter) return; - // 生成模拟遭遇结果 const result: EncounterResult = this.generatePlaceholderResult(node.type); - - // 调用进度管理器结算遭遇 resolveEncounter(state, result); - - // 返回游戏流程场景 await this.sceneController.launch('GameFlowScene'); } private generatePlaceholderResult(type: MapNodeType): EncounterResult { - // 根据遭遇类型生成不同的模拟结果 switch (type) { - case 'minion': - case 'elite': - return { hpLost: type === 'elite' ? 15 : 8, goldEarned: type === 'elite' ? 30 : 15 }; - case 'camp': - return { hpGained: 15 }; - case 'shop': - return { goldEarned: 0 }; + case 'minion': return { hpLost: 8, goldEarned: 15 }; + case 'elite': return { hpLost: 15, goldEarned: 30 }; + case 'camp': return { hpGained: 15 }; + case 'shop': return { goldEarned: 0 }; case 'curio': - case 'event': - return { goldEarned: 20 }; - default: - return {}; + case 'event': return { goldEarned: 20 }; + default: return {}; } } - - private getEncounterTypeLabel(type: MapNodeType): string { - const labels: Record = { - start: '起点', - end: '终点', - minion: '战斗', - elite: '精英战斗', - event: '事件', - camp: '营地', - shop: '商店', - curio: '奇遇', - }; - return labels[type] ?? type; - } - - private getEncounterTypeColor(type: MapNodeType): number { - const colors: Record = { - start: 0x44aa44, - end: 0xcc8844, - minion: 0xcc4444, - elite: 0xcc44cc, - event: 0xaaaa44, - camp: 0x44cccc, - shop: 0x4488cc, - curio: 0x8844cc, - }; - return colors[type] ?? 0x888888; - } - - private createButton( - label: string, - x: number, - y: number, - width: number, - height: number, - onClick: () => void - ): void { - const bg = this.add.rectangle(x, y, width, height, 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); - } }