import Phaser from 'phaser'; import { ReactiveScene } from 'boardgame-phaser'; import { generatePointCrawlMap, type PointCrawlMap, MapNodeType } from 'boardgame-core/samples/slay-the-spire-like'; const NODE_COLORS: Record = { [MapNodeType.Start]: 0x44aa44, [MapNodeType.Combat]: 0xcc4444, [MapNodeType.Event]: 0xaaaa44, [MapNodeType.Elite]: 0xcc44cc, [MapNodeType.Shelter]: 0x44cccc, [MapNodeType.NPC]: 0x4488cc, [MapNodeType.Boss]: 0xcc8844, }; const NODE_LABELS: Record = { [MapNodeType.Start]: '起点', [MapNodeType.Combat]: '战斗', [MapNodeType.Event]: '事件', [MapNodeType.Elite]: '精英', [MapNodeType.Shelter]: '篝火', [MapNodeType.NPC]: 'NPC', [MapNodeType.Boss]: 'Boss', }; export class MapViewerScene extends ReactiveScene { private map: PointCrawlMap | null = null; private seed: number = Date.now(); // Layout constants private readonly LAYER_HEIGHT = 100; private readonly NODE_SPACING = 120; private readonly NODE_RADIUS = 25; constructor() { super('MapViewerScene'); } create(): void { super.create(); this.drawMap(); this.createControls(); } private drawMap(): void { this.children.removeAll(); this.map = generatePointCrawlMap(this.seed); const { width, height } = this.scale; const graphics = this.add.graphics(); // Draw edges first graphics.lineStyle(2, 0x666666); for (const [nodeId, node] of this.map.nodes) { const posX = this.getNodeX(node, width); const posY = this.getNodeY(node, height); for (const childId of node.childIds) { const child = this.map.nodes.get(childId); if (child) { const childX = this.getNodeX(child, width); const childY = this.getNodeY(child, height); graphics.lineBetween(posX, posY, childX, childY); } } } // Draw nodes for (const [nodeId, node] of this.map.nodes) { const posX = this.getNodeX(node, width); const posY = this.getNodeY(node, height); const color = NODE_COLORS[node.type as MapNodeType] ?? 0x888888; // Node circle graphics.fillStyle(color); graphics.fillCircle(posX, posY, this.NODE_RADIUS); graphics.lineStyle(2, 0xffffff); graphics.strokeCircle(posX, posY, this.NODE_RADIUS); // Node label const label = NODE_LABELS[node.type as MapNodeType] ?? node.type; this.add.text(posX, posY, label, { fontSize: '12px', color: '#ffffff', }).setOrigin(0.5); // Encounter name (if available) if ('encounter' in node && node.encounter) { this.add.text(posX, posY + this.NODE_RADIUS + 10, (node as any).encounter.name ?? '', { fontSize: '10px', color: '#cccccc', }).setOrigin(0.5); } } // Title this.add.text(width / 2, 30, `Map Viewer (Seed: ${this.seed})`, { fontSize: '24px', color: '#ffffff', fontStyle: 'bold', }).setOrigin(0.5); // Legend this.drawLegend(20, height - 200); } private getNodeX(node: any, sceneWidth: number): number { const layer = this.map!.layers[node.layerIndex]; const nodeIndex = layer.nodeIds.indexOf(node.id); const totalNodes = layer.nodeIds.length; const layerWidth = (totalNodes - 1) * this.NODE_SPACING; const startX = sceneWidth / 2 - layerWidth / 2; return startX + nodeIndex * this.NODE_SPACING; } private getNodeY(node: any, sceneHeight: number): number { return 80 + node.layerIndex * this.LAYER_HEIGHT; } private drawLegend(x: number, y: number): void { const graphics = this.add.graphics(); graphics.fillStyle(0x222222, 0.8); graphics.fillRect(x, y, 150, 180); let offsetY = y + 15; this.add.text(x + 10, offsetY, '图例:', { fontSize: '14px', color: '#ffffff', fontStyle: 'bold', }); offsetY += 25; for (const [type, color] of Object.entries(NODE_COLORS)) { graphics.fillStyle(color); graphics.fillCircle(x + 20, offsetY, 8); this.add.text(x + 40, offsetY - 5, NODE_LABELS[type as MapNodeType], { fontSize: '12px', color: '#ffffff', }); offsetY += 22; } } private createControls(): void { const { width, height } = this.scale; // Back button this.createButton('返回菜单', 100, 40, () => { this.scene.start('IndexScene'); }); // Regenerate button this.createButton('重新生成', width - 120, 40, () => { this.seed = Date.now(); this.drawMap(); this.createControls(); }); } private createButton(label: string, x: number, y: number, onClick: () => void): void { const buttonWidth = 140; 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); } }