diff --git a/src/config.ts b/src/config.ts index 0cb0ee0..34f56ed 100644 --- a/src/config.ts +++ b/src/config.ts @@ -49,6 +49,9 @@ export const VILLAGER_NAMES = [ export const SAVE_KEY = 'tg_save_v5' export const AUTOSAVE_INTERVAL = 30_000 +/** localStorage key for UI settings (opacity etc.) — separate from the game save. */ +export const UI_SETTINGS_KEY = 'tg_ui_settings' + /** Milliseconds for one tree-seedling stage to advance (two stages = full tree). */ export const TREE_SEEDLING_STAGE_MS = 60_000 // 1 min per stage → 2 min total diff --git a/src/scenes/UIScene.ts b/src/scenes/UIScene.ts index 050ae16..074710c 100644 --- a/src/scenes/UIScene.ts +++ b/src/scenes/UIScene.ts @@ -3,6 +3,7 @@ import type { BuildingType, JobPriorities } from '../types' import type { FarmingTool } from '../systems/FarmingSystem' import type { DebugData } from '../systems/DebugSystem' import { stateManager } from '../StateManager' +import { UI_SETTINGS_KEY } from '../config' const ITEM_ICONS: Record = { wood: '🪵', stone: '🪨', wheat_seed: '🌱', carrot_seed: '🥕', @@ -46,6 +47,11 @@ export class UIScene extends Phaser.Scene { logTexts: Phaser.GameObjects.Text[] } | null = null + /** Current overlay background opacity (0.4–1.0, default 0.8). Persisted in localStorage. */ + private uiOpacity = 0.8 + private settingsGroup!: Phaser.GameObjects.Group + private settingsVisible = false + constructor() { super({ key: 'UI' }) } /** @@ -53,6 +59,7 @@ export class UIScene extends Phaser.Scene { * keyboard shortcuts (B, V, F3, ESC). */ create(): void { + this.loadUISettings() this.createStockpilePanel() this.createHintText() this.createToast() @@ -85,6 +92,7 @@ export class UIScene extends Phaser.Scene { this.escMenuGroup = this.add.group() this.confirmGroup = this.add.group() this.nisseInfoGroup = this.add.group() + this.settingsGroup = this.add.group() this.input.on('pointerdown', (ptr: Phaser.Input.Pointer) => { if (ptr.rightButtonDown()) { @@ -196,7 +204,7 @@ export class UIScene extends Phaser.Scene { { kind: 'stockpile_zone', label: '📦 Stockpile', cost: 'free (workers deliver here)' }, ] const menuX = this.scale.width / 2 - 150, menuY = this.scale.height / 2 - 140 - const bg = this.add.rectangle(menuX, menuY, 300, 280, 0x000000, 0.88).setOrigin(0,0).setScrollFactor(0).setDepth(200) + const bg = this.add.rectangle(menuX, menuY, 300, 280, 0x000000, this.uiOpacity).setOrigin(0,0).setScrollFactor(0).setDepth(200) this.buildMenuGroup.add(bg) this.buildMenuGroup.add(this.add.text(menuX + 150, menuY + 14, 'BUILD MENU [B/ESC]', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setOrigin(0.5,0).setScrollFactor(0).setDepth(201)) @@ -263,7 +271,7 @@ export class UIScene extends Phaser.Scene { const px = this.scale.width / 2 - panelW / 2 const py = this.scale.height / 2 - panelH / 2 - const bg = this.add.rectangle(px, py, panelW, panelH, 0x0a0a0a, 0.92).setOrigin(0,0).setScrollFactor(0).setDepth(210) + const bg = this.add.rectangle(px, py, panelW, panelH, 0x0a0a0a, this.uiOpacity).setOrigin(0,0).setScrollFactor(0).setDepth(210) this.villagerPanelGroup.add(bg) this.villagerPanelGroup.add( @@ -383,10 +391,11 @@ export class UIScene extends Phaser.Scene { /** Creates the debug panel text object (initially hidden). */ private createDebugPanel(): void { + const hexAlpha = Math.round(this.uiOpacity * 255).toString(16).padStart(2, '0') this.debugPanelText = this.add.text(10, 80, '', { fontSize: '12px', color: '#cccccc', - backgroundColor: '#000000cc', + backgroundColor: `#000000${hexAlpha}`, padding: { x: 8, y: 6 }, lineSpacing: 2, fontFamily: 'monospace', @@ -463,7 +472,7 @@ export class UIScene extends Phaser.Scene { const mx = Math.min(x, this.scale.width - menuW - 4) const my = Math.min(y, this.scale.height - menuH - 4) - const bg = this.add.rectangle(mx, my, menuW, menuH, 0x000000, 0.88) + const bg = this.add.rectangle(mx, my, menuW, menuH, 0x000000, this.uiOpacity) .setOrigin(0, 0).setScrollFactor(0).setDepth(300) this.contextMenuGroup.add(bg) @@ -521,6 +530,7 @@ export class UIScene extends Phaser.Scene { if (this.buildMenuVisible) { this.closeBuildMenu(); return } if (this.villagerPanelVisible){ this.closeVillagerPanel(); return } if (this.nisseInfoVisible) { this.closeNisseInfoPanel(); return } + if (this.settingsVisible) { this.closeSettings(); 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. @@ -564,7 +574,7 @@ export class UIScene extends Phaser.Scene { 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) + const bg = this.add.rectangle(mx, my, menuW, menuH, 0x0a0a0a, this.uiOpacity) .setOrigin(0, 0).setScrollFactor(0).setDepth(400) this.escMenuGroup.add(bg) this.escMenuGroup.add( @@ -602,10 +612,153 @@ export class UIScene extends Phaser.Scene { window.location.reload() } - /** Opens an empty Settings panel (placeholder). */ + /** Opens the Settings overlay. */ private doSettings(): void { this.closeEscMenu() - this.showToast('Settings — coming soon') + this.openSettings() + } + + // ─── Settings overlay ───────────────────────────────────────────────────── + + /** Opens the settings overlay if it is not already open. */ + private openSettings(): void { + if (this.settingsVisible) return + this.settingsVisible = true + this.scene.get('Game').events.emit('uiMenuOpen') + this.buildSettings() + } + + /** Closes and destroys the settings overlay. */ + private closeSettings(): void { + if (!this.settingsVisible) return + this.settingsVisible = false + this.settingsGroup.destroy(true) + this.settingsGroup = this.add.group() + this.scene.get('Game').events.emit('uiMenuClose') + } + + /** + * Builds the settings overlay with an overlay-opacity row (step buttons). + * Destroying and recreating this method is used to refresh the displayed value. + */ + private buildSettings(): void { + if (this.settingsGroup) this.settingsGroup.destroy(true) + this.settingsGroup = this.add.group() + + const panelW = 280 + const panelH = 130 + const px = this.scale.width / 2 - panelW / 2 + const py = this.scale.height / 2 - panelH / 2 + + // Background + const bg = this.add.rectangle(px, py, panelW, panelH, 0x0a0a0a, this.uiOpacity) + .setOrigin(0, 0).setScrollFactor(0).setDepth(450) + this.settingsGroup.add(bg) + + // Title + this.settingsGroup.add( + this.add.text(px + panelW / 2, py + 14, '⚙️ SETTINGS [ESC close]', { + fontSize: '11px', color: '#666666', fontFamily: 'monospace', + }).setOrigin(0.5, 0).setScrollFactor(0).setDepth(451) + ) + + // Opacity label + this.settingsGroup.add( + this.add.text(px + 16, py + 58, 'Overlay opacity:', { + fontSize: '13px', color: '#cccccc', fontFamily: 'monospace', + }).setOrigin(0, 0.5).setScrollFactor(0).setDepth(451) + ) + + // Minus button + const minusBtn = this.add.rectangle(px + 170, py + 47, 26, 22, 0x1a1a2e, 0.9) + .setOrigin(0, 0).setScrollFactor(0).setDepth(451).setInteractive() + minusBtn.on('pointerover', () => minusBtn.setFillStyle(0x2a2a4e, 0.9)) + minusBtn.on('pointerout', () => minusBtn.setFillStyle(0x1a1a2e, 0.9)) + minusBtn.on('pointerdown', () => { + this.uiOpacity = Math.max(0.4, Math.round((this.uiOpacity - 0.1) * 10) / 10) + this.saveUISettings() + this.updateDebugPanelBackground() + this.buildSettings() + }) + this.settingsGroup.add(minusBtn) + this.settingsGroup.add( + this.add.text(px + 183, py + 58, '−', { + fontSize: '15px', color: '#ffffff', fontFamily: 'monospace', + }).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(452) + ) + + // Value display + this.settingsGroup.add( + this.add.text(px + 215, py + 58, `${Math.round(this.uiOpacity * 100)}%`, { + fontSize: '13px', color: '#aaaaaa', fontFamily: 'monospace', + }).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(451) + ) + + // Plus button + const plusBtn = this.add.rectangle(px + 242, py + 47, 26, 22, 0x1a1a2e, 0.9) + .setOrigin(0, 0).setScrollFactor(0).setDepth(451).setInteractive() + plusBtn.on('pointerover', () => plusBtn.setFillStyle(0x2a2a4e, 0.9)) + plusBtn.on('pointerout', () => plusBtn.setFillStyle(0x1a1a2e, 0.9)) + plusBtn.on('pointerdown', () => { + this.uiOpacity = Math.min(1.0, Math.round((this.uiOpacity + 0.1) * 10) / 10) + this.saveUISettings() + this.updateDebugPanelBackground() + this.buildSettings() + }) + this.settingsGroup.add(plusBtn) + this.settingsGroup.add( + this.add.text(px + 255, py + 58, '+', { + fontSize: '15px', color: '#ffffff', fontFamily: 'monospace', + }).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(452) + ) + + // Close button + const closeBtnRect = this.add.rectangle(px + panelW / 2 - 50, py + 92, 100, 28, 0x1a1a2e, 0.9) + .setOrigin(0, 0).setScrollFactor(0).setDepth(451).setInteractive() + closeBtnRect.on('pointerover', () => closeBtnRect.setFillStyle(0x2a2a4e, 0.9)) + closeBtnRect.on('pointerout', () => closeBtnRect.setFillStyle(0x1a1a2e, 0.9)) + closeBtnRect.on('pointerdown', () => this.closeSettings()) + this.settingsGroup.add(closeBtnRect) + this.settingsGroup.add( + this.add.text(px + panelW / 2, py + 106, 'Close', { + fontSize: '13px', color: '#dddddd', fontFamily: 'monospace', + }).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(452) + ) + } + + /** + * Loads UI settings from localStorage and applies the stored opacity value. + * Falls back to the default (0.8) if no setting is found. + */ + private loadUISettings(): void { + try { + const raw = localStorage.getItem(UI_SETTINGS_KEY) + if (raw) { + const parsed = JSON.parse(raw) as { opacity?: number } + if (typeof parsed.opacity === 'number') { + this.uiOpacity = Math.max(0.4, Math.min(1.0, parsed.opacity)) + } + } + } catch (_) {} + } + + /** + * Persists the current UI settings (opacity) to localStorage. + * Stored separately from the game save so New Game does not wipe it. + */ + private saveUISettings(): void { + try { + localStorage.setItem(UI_SETTINGS_KEY, JSON.stringify({ opacity: this.uiOpacity })) + } catch (_) {} + } + + /** + * Applies the current uiOpacity to the debug panel text background. + * Called whenever uiOpacity changes so the debug panel stays in sync. + */ + private updateDebugPanelBackground(): void { + const hexAlpha = Math.round(this.uiOpacity * 255).toString(16).padStart(2, '0') + this.debugPanelText.setStyle({ backgroundColor: `#000000${hexAlpha}` }) } /** Shows a confirmation dialog before starting a new game. */ @@ -634,7 +787,7 @@ export class UIScene extends Phaser.Scene { 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) + const bg = this.add.rectangle(dx, dy, dialogW, dialogH, 0x0a0a0a, this.uiOpacity) .setOrigin(0, 0).setScrollFactor(0).setDepth(500) this.confirmGroup.add(bg) @@ -728,7 +881,7 @@ export class UIScene extends Phaser.Scene { // Background this.nisseInfoGroup.add( - this.add.rectangle(px, py, panelW, panelH, 0x050510, 0.93) + this.add.rectangle(px, py, panelW, panelH, 0x050510, this.uiOpacity) .setOrigin(0, 0).setScrollFactor(0).setDepth(250) ) @@ -881,6 +1034,7 @@ export class UIScene extends Phaser.Scene { if (this.villagerPanelVisible) this.closeVillagerPanel() if (this.contextMenuVisible) this.hideContextMenu() if (this.escMenuVisible) this.closeEscMenu() + if (this.settingsVisible) this.closeSettings() if (this.confirmVisible) this.hideConfirm() if (this.nisseInfoVisible) this.closeNisseInfoPanel() }