From 41097b4765f1652aab8a37930103034d00dcb4c7 Mon Sep 17 00:00:00 2001 From: tekki mariani Date: Sat, 21 Mar 2026 14:13:53 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20ESC=20menu=20(Issue=20#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESC key follows priority stack: confirm dialog → context menu → build menu → villager panel → ESC menu → open ESC menu. Menu items: Save Game, Load Game, Settings (placeholder), New Game (with confirmation dialog). Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 4 + src/scenes/UIScene.ts | 189 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 189 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21bfe94..eee4719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- **ESC Menu**: pressing ESC when no overlay is open shows a pause menu with Save Game, Load Game, Settings (placeholder), and New Game; New Game requires confirmation before wiping the save +- ESC key now follows a priority stack: confirmation dialog → context menu → build menu → villager panel → ESC menu → (build/farm mode handled by their systems) → open ESC menu + ### Added - **F3 Debug View**: toggleable overlay showing FPS, tile type and contents under the cursor, Nisse count by AI state, active jobs by type, and pathfinding visualization (cyan lines in world space) diff --git a/src/scenes/UIScene.ts b/src/scenes/UIScene.ts index 9b186d9..9ae4961 100644 --- a/src/scenes/UIScene.ts +++ b/src/scenes/UIScene.ts @@ -31,6 +31,10 @@ export class UIScene extends Phaser.Scene { private inFarmMode = false private debugPanelText!: Phaser.GameObjects.Text private debugActive = false + private escMenuGroup!: Phaser.GameObjects.Group + private escMenuVisible = false + private confirmGroup!: Phaser.GameObjects.Group + private confirmVisible = false constructor() { super({ key: 'UI' }) } @@ -66,6 +70,8 @@ export class UIScene extends Phaser.Scene { this.input.mouse!.disableContextMenu() this.contextMenuGroup = this.add.group() + this.escMenuGroup = this.add.group() + this.confirmGroup = this.add.group() this.input.on('pointerdown', (ptr: Phaser.Input.Pointer) => { if (ptr.rightButtonDown()) { @@ -78,7 +84,7 @@ export class UIScene extends Phaser.Scene { }) this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.ESC) - .on('down', () => this.hideContextMenu()) + .on('down', () => this.handleEsc()) } /** @@ -488,6 +494,179 @@ export class UIScene extends Phaser.Scene { this.scene.get('Game').events.emit('uiMenuClose') } + // ─── ESC key handler ────────────────────────────────────────────────────── + + /** + * Handles ESC key presses with a priority stack: + * confirm dialog → context menu → build menu → villager panel → + * esc menu → build/farm mode (handled by their own systems) → open ESC menu. + */ + private handleEsc(): void { + if (this.confirmVisible) { this.hideConfirm(); return } + if (this.contextMenuVisible) { this.hideContextMenu(); return } + if (this.buildMenuVisible) { this.closeBuildMenu(); return } + if (this.villagerPanelVisible){ this.closeVillagerPanel(); return } + if (this.escMenuVisible) { this.closeEscMenu(); return } + // Build/farm mode: let BuildingSystem / FarmingSystem handle their own ESC key. + // We only skip opening the ESC menu while those modes are active. + if (this.inBuildMode || this.inFarmMode) return + this.openEscMenu() + } + + // ─── ESC Menu ───────────────────────────────────────────────────────────── + + /** Opens the ESC pause menu (New Game / Save / Load / Settings). */ + private openEscMenu(): void { + if (this.escMenuVisible) return + this.escMenuVisible = true + this.scene.get('Game').events.emit('uiMenuOpen') + this.buildEscMenu() + } + + /** Closes and destroys the ESC menu. */ + private closeEscMenu(): void { + if (!this.escMenuVisible) return + this.escMenuVisible = false + this.escMenuGroup.destroy(true) + this.escMenuGroup = this.add.group() + this.scene.get('Game').events.emit('uiMenuClose') + } + + /** Builds the ESC menu UI elements. */ + private buildEscMenu(): void { + if (this.escMenuGroup) this.escMenuGroup.destroy(true) + this.escMenuGroup = this.add.group() + + const menuW = 240 + const btnH = 40 + const entries: { label: string; action: () => void }[] = [ + { label: '💾 Save Game', action: () => this.doSaveGame() }, + { label: '📂 Load Game', action: () => this.doLoadGame() }, + { label: '⚙️ Settings', action: () => this.doSettings() }, + { label: '🆕 New Game', action: () => this.doNewGame() }, + ] + const menuH = 16 + entries.length * (btnH + 8) + 8 + const mx = this.scale.width / 2 - menuW / 2 + const my = this.scale.height / 2 - menuH / 2 + + const bg = this.add.rectangle(mx, my, menuW, menuH, 0x0a0a0a, 0.95) + .setOrigin(0, 0).setScrollFactor(0).setDepth(400) + this.escMenuGroup.add(bg) + this.escMenuGroup.add( + this.add.text(mx + menuW / 2, my + 12, 'MENU [ESC] close', { + fontSize: '11px', color: '#666666', fontFamily: 'monospace', + }).setOrigin(0.5, 0).setScrollFactor(0).setDepth(401) + ) + + entries.forEach((entry, i) => { + const by = my + 32 + i * (btnH + 8) + const btn = this.add.rectangle(mx + 12, by, menuW - 24, btnH, 0x1a1a2e, 0.9) + .setOrigin(0, 0).setScrollFactor(0).setDepth(401).setInteractive() + btn.on('pointerover', () => btn.setFillStyle(0x2a2a4e, 0.9)) + btn.on('pointerout', () => btn.setFillStyle(0x1a1a2e, 0.9)) + btn.on('pointerdown', entry.action) + this.escMenuGroup.add(btn) + this.escMenuGroup.add( + this.add.text(mx + 24, by + btnH / 2, entry.label, { + fontSize: '14px', color: '#dddddd', fontFamily: 'monospace', + }).setOrigin(0, 0.5).setScrollFactor(0).setDepth(402) + ) + }) + } + + /** Saves the game and shows a toast confirmation. */ + private doSaveGame(): void { + stateManager.save() + this.closeEscMenu() + this.showToast('Game saved!') + } + + /** Reloads the page to load the last save from localStorage. */ + private doLoadGame(): void { + this.closeEscMenu() + window.location.reload() + } + + /** Opens an empty Settings panel (placeholder). */ + private doSettings(): void { + this.closeEscMenu() + this.showToast('Settings — coming soon') + } + + /** Shows a confirmation dialog before starting a new game. */ + private doNewGame(): void { + this.closeEscMenu() + this.showConfirm( + 'Start a new game?\nAll progress will be lost.', + () => { stateManager.reset(); window.location.reload() }, + ) + } + + // ─── Confirm dialog ─────────────────────────────────────────────────────── + + /** + * Shows a modal confirmation dialog with OK and Cancel buttons. + * @param message - Message to display (newlines supported) + * @param onConfirm - Callback invoked when the user confirms + */ + private showConfirm(message: string, onConfirm: () => void): void { + this.hideConfirm() + this.confirmVisible = true + this.scene.get('Game').events.emit('uiMenuOpen') + + const dialogW = 280 + const dialogH = 130 + const dx = this.scale.width / 2 - dialogW / 2 + const dy = this.scale.height / 2 - dialogH / 2 + + const bg = this.add.rectangle(dx, dy, dialogW, dialogH, 0x0a0a0a, 0.97) + .setOrigin(0, 0).setScrollFactor(0).setDepth(500) + this.confirmGroup.add(bg) + + this.confirmGroup.add( + this.add.text(dx + dialogW / 2, dy + 20, message, { + fontSize: '13px', color: '#cccccc', fontFamily: 'monospace', + align: 'center', wordWrap: { width: dialogW - 32 }, + }).setOrigin(0.5, 0).setScrollFactor(0).setDepth(501) + ) + + const btnY = dy + dialogH - 44 + // Cancel button + const cancelBtn = this.add.rectangle(dx + 16, btnY, 110, 30, 0x333333, 0.9) + .setOrigin(0, 0).setScrollFactor(0).setDepth(501).setInteractive() + cancelBtn.on('pointerover', () => cancelBtn.setFillStyle(0x555555, 0.9)) + cancelBtn.on('pointerout', () => cancelBtn.setFillStyle(0x333333, 0.9)) + cancelBtn.on('pointerdown', () => this.hideConfirm()) + this.confirmGroup.add(cancelBtn) + this.confirmGroup.add( + this.add.text(dx + 71, btnY + 15, 'Cancel', { + fontSize: '13px', color: '#aaaaaa', fontFamily: 'monospace', + }).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(502) + ) + + // OK button + const okBtn = this.add.rectangle(dx + dialogW - 126, btnY, 110, 30, 0x4a1a1a, 0.9) + .setOrigin(0, 0).setScrollFactor(0).setDepth(501).setInteractive() + okBtn.on('pointerover', () => okBtn.setFillStyle(0x8a2a2a, 0.9)) + okBtn.on('pointerout', () => okBtn.setFillStyle(0x4a1a1a, 0.9)) + okBtn.on('pointerdown', () => { this.hideConfirm(); onConfirm() }) + this.confirmGroup.add(okBtn) + this.confirmGroup.add( + this.add.text(dx + dialogW - 71, btnY + 15, 'OK', { + fontSize: '13px', color: '#ff8888', fontFamily: 'monospace', + }).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(502) + ) + } + + /** Closes and destroys the confirmation dialog. */ + private hideConfirm(): void { + if (!this.confirmVisible) return + this.confirmVisible = false + this.confirmGroup.destroy(true) + this.confirmGroup = this.add.group() + this.scene.get('Game').events.emit('uiMenuClose') + } + // ─── Resize ─────────────────────────────────────────────────────────────── /** @@ -515,8 +694,10 @@ export class UIScene extends Phaser.Scene { // Close centered panels — their position is calculated on open, so they // would be off-center if left open during a resize - if (this.buildMenuVisible) this.closeBuildMenu() - if (this.villagerPanelVisible) this.closeVillagerPanel() - if (this.contextMenuVisible) this.hideContextMenu() + if (this.buildMenuVisible) this.closeBuildMenu() + if (this.villagerPanelVisible) this.closeVillagerPanel() + if (this.contextMenuVisible) this.hideContextMenu() + if (this.escMenuVisible) this.closeEscMenu() + if (this.confirmVisible) this.hideConfirm() } } -- 2.49.1