185 lines
5.2 KiB
TypeScript
185 lines
5.2 KiB
TypeScript
|
|
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, number> = {
|
||
|
|
[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, string> = {
|
||
|
|
[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);
|
||
|
|
}
|
||
|
|
}
|