diff --git a/packages/sts-like-viewer/src/scenes/MapViewerScene.ts b/packages/sts-like-viewer/src/scenes/MapViewerScene.ts index 6fdaa9e..f2a9a54 100644 --- a/packages/sts-like-viewer/src/scenes/MapViewerScene.ts +++ b/packages/sts-like-viewer/src/scenes/MapViewerScene.ts @@ -27,9 +27,25 @@ export class MapViewerScene extends ReactiveScene { private seed: number = Date.now(); // Layout constants - private readonly LAYER_HEIGHT = 100; - private readonly NODE_SPACING = 120; - private readonly NODE_RADIUS = 25; + private readonly LAYER_HEIGHT = 110; + private readonly NODE_SPACING = 140; + private readonly NODE_RADIUS = 28; + + // Scroll state + private mapContainer!: Phaser.GameObjects.Container; + private isDragging = false; + private dragStartX = 0; + private dragStartContainerX = 0; + private dragStartY = 0; + private dragStartContainerY = 0; + + // Fixed UI (always visible, not scrolled) + private titleText!: Phaser.GameObjects.Text; + private backButtonBg!: Phaser.GameObjects.Rectangle; + private backButtonText!: Phaser.GameObjects.Text; + private regenButtonBg!: Phaser.GameObjects.Rectangle; + private regenButtonText!: Phaser.GameObjects.Text; + private legendContainer!: Phaser.GameObjects.Container; constructor() { super('MapViewerScene'); @@ -37,28 +53,129 @@ export class MapViewerScene extends ReactiveScene { create(): void { super.create(); + this.drawFixedUI(); this.drawMap(); - this.createControls(); + } + + private drawFixedUI(): void { + const { width } = this.scale; + + // Title + this.titleText = this.add.text(width / 2, 30, '', { + fontSize: '24px', + color: '#ffffff', + fontStyle: 'bold', + }).setOrigin(0.5).setDepth(100); + + // Back button + this.backButtonBg = this.add.rectangle(100, 40, 140, 36, 0x444466) + .setStrokeStyle(2, 0x7777aa) + .setInteractive({ useHandCursor: true }) + .setDepth(100); + this.backButtonText = this.add.text(100, 40, '返回菜单', { + fontSize: '16px', + color: '#ffffff', + }).setOrigin(0.5).setDepth(100); + + this.backButtonBg.on('pointerover', () => { + this.backButtonBg.setFillStyle(0x555588); + this.backButtonText.setScale(1.05); + }); + this.backButtonBg.on('pointerout', () => { + this.backButtonBg.setFillStyle(0x444466); + this.backButtonText.setScale(1); + }); + this.backButtonBg.on('pointerdown', () => { + this.scene.start('IndexScene'); + }); + + // Regenerate button + this.regenButtonBg = this.add.rectangle(width - 120, 40, 140, 36, 0x444466) + .setStrokeStyle(2, 0x7777aa) + .setInteractive({ useHandCursor: true }) + .setDepth(100); + this.regenButtonText = this.add.text(width - 120, 40, '重新生成', { + fontSize: '16px', + color: '#ffffff', + }).setOrigin(0.5).setDepth(100); + + this.regenButtonBg.on('pointerover', () => { + this.regenButtonBg.setFillStyle(0x555588); + this.regenButtonText.setScale(1.05); + }); + this.regenButtonBg.on('pointerout', () => { + this.regenButtonBg.setFillStyle(0x444466); + this.regenButtonText.setScale(1); + }); + this.regenButtonBg.on('pointerdown', () => { + this.seed = Date.now(); + this.mapContainer.destroy(); + this.drawMap(); + }); + + // Legend (bottom-left, fixed) + this.legendContainer = this.add.container(20, this.scale.height - 200).setDepth(100); + const legendBg = this.add.rectangle(75, 90, 150, 180, 0x222222, 0.8); + this.legendContainer.add(legendBg); + + this.legendContainer.add( + this.add.text(10, 5, '图例:', { fontSize: '14px', color: '#ffffff', fontStyle: 'bold' }) + ); + + let offsetY = 30; + for (const [type, color] of Object.entries(NODE_COLORS)) { + this.legendContainer.add( + this.add.circle(20, offsetY, 8, color) + ); + this.legendContainer.add( + this.add.text(40, offsetY - 5, NODE_LABELS[type as MapNodeType], { fontSize: '12px', color: '#ffffff' }) + ); + offsetY += 22; + } + + // Hint text + this.add.text(width / 2, this.scale.height - 20, '拖拽滚动查看地图', { + fontSize: '14px', + color: '#888888', + }).setOrigin(0.5).setDepth(100); } private drawMap(): void { - this.children.removeAll(); this.map = generatePointCrawlMap(this.seed); const { width, height } = this.scale; - const graphics = this.add.graphics(); - // Draw edges first + // Update title + this.titleText.setText(`Map Viewer (Seed: ${this.seed})`); + + // Calculate map bounds + const maxLayer = 12; // TOTAL_LAYERS - 1 + const maxNodesInLayer = 6; // widest layer + const mapWidth = (maxNodesInLayer - 1) * this.NODE_SPACING + 200; + const mapHeight = maxLayer * this.LAYER_HEIGHT + 200; + + // Create scrollable container + this.mapContainer = this.add.container(width / 2, height / 2); + + // Background panel for the map area + const bg = this.add.rectangle(0, 0, mapWidth, mapHeight, 0x111122, 0.5) + .setOrigin(0.5); + this.mapContainer.add(bg); + + const graphics = this.add.graphics(); + this.mapContainer.add(graphics); + + // Draw edges graphics.lineStyle(2, 0x666666); for (const [nodeId, node] of this.map.nodes) { - const posX = this.getNodeX(node, width); - const posY = this.getNodeY(node, height); + const posX = this.getNodeX(node); + const posY = this.getNodeY(node); 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); + const childX = this.getNodeX(child); + const childY = this.getNodeY(child); graphics.lineBetween(posX, posY, childX, childY); } } @@ -66,8 +183,8 @@ export class MapViewerScene extends ReactiveScene { // Draw nodes for (const [nodeId, node] of this.map.nodes) { - const posX = this.getNodeX(node, width); - const posY = this.getNodeY(node, height); + const posX = this.getNodeX(node); + const posY = this.getNodeY(node); const color = NODE_COLORS[node.type as MapNodeType] ?? 0x888888; // Node circle @@ -78,107 +195,57 @@ export class MapViewerScene extends ReactiveScene { // 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); + this.mapContainer.add( + 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); + if ('encounter' in node && (node as any).encounter) { + this.mapContainer.add( + this.add.text(posX, posY + this.NODE_RADIUS + 12, (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); + // Setup drag-to-scroll + this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => { + this.isDragging = true; + this.dragStartX = pointer.x; + this.dragStartY = pointer.y; + this.dragStartContainerX = this.mapContainer.x; + this.dragStartContainerY = this.mapContainer.y; + }); - // Legend - this.drawLegend(20, height - 200); + this.input.on('pointermove', (pointer: Phaser.Input.Pointer) => { + if (!this.isDragging) return; + this.mapContainer.x = this.dragStartContainerX + (pointer.x - this.dragStartX); + this.mapContainer.y = this.dragStartContainerY + (pointer.y - this.dragStartY); + }); + + this.input.on('pointerup', () => { + this.isDragging = false; + }); + + this.input.on('pointerout', () => { + this.isDragging = false; + }); } - private getNodeX(node: any, sceneWidth: number): number { + private getNodeX(node: any): 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; + return -layerWidth / 2 + 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); + private getNodeY(node: any): number { + return -600 + node.layerIndex * this.LAYER_HEIGHT; } } diff --git a/packages/sts-like-viewer/src/ui/App.tsx b/packages/sts-like-viewer/src/ui/App.tsx index 994888a..457d347 100644 --- a/packages/sts-like-viewer/src/ui/App.tsx +++ b/packages/sts-like-viewer/src/ui/App.tsx @@ -15,7 +15,7 @@ export default function App() { return (