diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be15c4..bb5be00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Fixed +- **Stockpile panel** (Issue #20): panel background now uses `uiOpacity` and updates live when Settings opacity changes; panel height increased so the Nisse count row no longer overlaps the carrot row +- **ESC menu** (Issue #20): internal bottom padding corrected — last button now has 16px gap to panel edge instead of 0px + +### Added +- **Overlay Opacity Setting** (Issue #16): all UI overlay backgrounds (build menu, villager panel, context menu, ESC menu, confirm dialog, Nisse info panel, debug panel) now use a central `uiOpacity` value instead of hardcoded alphas +- **Settings Screen**: ESC menu → Settings now opens a real overlay with an overlay-opacity row (− / value% / + step buttons, range 40 %–100 %, default 80 %); setting persisted in `localStorage` under `tg_ui_settings`, separate from game save so New Game does not wipe it + ### Added - **Unified Tile System** (Issue #14): - Tree seedlings: player plants `tree_seed` on grass/dark-grass via the F-key farming tool; seedling grows through two stages (sprout → sapling → young tree, ~1 min each); on maturity it becomes a FOREST tile with a harvestable tree resource 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..19bbed7 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()) { @@ -119,14 +127,16 @@ export class UIScene extends Phaser.Scene { /** Creates the stockpile panel in the top-right corner with item rows and population count. */ private createStockpilePanel(): void { const x = this.scale.width - 178, y = 10 - this.stockpilePanel = this.add.rectangle(x, y, 168, 187, 0x000000, 0.72).setOrigin(0, 0).setScrollFactor(0).setDepth(100) + // 7 items × 22px + 26px header + 12px gap + 18px popText row + 10px bottom = 210px + this.stockpilePanel = this.add.rectangle(x, y, 168, 210, 0x000000, this.uiOpacity).setOrigin(0, 0).setScrollFactor(0).setDepth(100) this.stockpileTitleText = this.add.text(x + 10, y + 7, '⚡ STOCKPILE', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101) const items = ['wood','stone','wheat_seed','carrot_seed','tree_seed','wheat','carrot'] as const items.forEach((item, i) => { const t = this.add.text(x + 10, y + 26 + i * 22, `${ITEM_ICONS[item]} ${item}: 0`, { fontSize: '13px', color: '#88dd88', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101) this.stockpileTexts.set(item, t) }) - this.popText = this.add.text(x + 10, y + 167, '👥 Nisse: 0 / 0', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101) + // last item (i=6) bottom edge ≈ y+190 → popText starts at y+192 with 8px gap + this.popText = this.add.text(x + 10, y + 192, '👥 Nisse: 0 / 0', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101) } /** Refreshes all item quantities and colors in the stockpile panel. */ @@ -196,7 +206,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 +273,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 +393,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 +474,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 +532,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. @@ -560,11 +572,12 @@ export class UIScene extends Phaser.Scene { { label: '⚙️ Settings', action: () => this.doSettings() }, { label: '🆕 New Game', action: () => this.doNewGame() }, ] - const menuH = 16 + entries.length * (btnH + 8) + 8 + // 32px header + entries × (btnH + 8px gap) + 8px bottom padding + const menuH = 32 + 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) + 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 +615,155 @@ 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.updateStaticPanelOpacity() + 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.updateStaticPanelOpacity() + 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 all static UI elements that are not + * rebuilt on open (stockpile panel, debug panel background). + * Called whenever uiOpacity changes. + */ + private updateStaticPanelOpacity(): void { + this.stockpilePanel.setAlpha(this.uiOpacity) + 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 +792,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 +886,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 +1039,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() }