From c7ebf49bf211d1838b8cdc18be7bda0cdea926bb Mon Sep 17 00:00:00 2001 From: tekki mariani Date: Mon, 23 Mar 2026 09:36:42 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20overlay=20opacity:=20global=20s?= =?UTF-8?q?etting=20+=20settings=20screen=20(Issue=20#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add UI_SETTINGS_KEY to config.ts for separate localStorage entry - Add uiOpacity field (default 0.8, range 0.4–1.0, 10 % steps) to UIScene - loadUISettings / saveUISettings persist opacity independently of game save - Replace all hardcoded panel BG alphas with this.uiOpacity: build menu, villager panel, context menu, ESC menu, confirm dialog, nisse info panel - Debug panel (F3) background synced via updateDebugPanelBackground() - Replace Settings toast with real Settings overlay: title, opacity − / value / + buttons, Close button - ESC key priority stack now includes settingsVisible - repositionUI closes settings panel on window resize Co-Authored-By: Claude Sonnet 4.6 --- src/config.ts | 3 + src/scenes/UIScene.ts | 172 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 166 insertions(+), 9 deletions(-) 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() } -- 2.49.1 From 174db14c7a3d1cd5af91fffeb07c32d50c16a3ea Mon Sep 17 00:00:00 2001 From: tekki mariani Date: Mon, 23 Mar 2026 09:38:09 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9D=20update=20CHANGELOG=20for=20I?= =?UTF-8?q?ssue=20#16=20overlay=20opacity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be15c4..47564de 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 +- **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 -- 2.49.1 From a5c37f20f6f7bda2affba9a3b041cb1902571080 Mon Sep 17 00:00:00 2001 From: tekki mariani Date: Mon, 23 Mar 2026 10:48:04 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=90=9B=20fix=20stockpile=20opacity,?= =?UTF-8?q?=20popText=20overlap,=20ESC=20menu=20padding=20(Issue=20#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Stockpile panel: use uiOpacity instead of hardcoded 0.72 - updateStaticPanelOpacity() replaces updateDebugPanelBackground() and also updates stockpilePanel.setAlpha() when opacity changes in Settings - Stockpile panel height 187→210; popText y 167→192 (8px gap after carrot row) - ESC menu menuH formula: 16+…+8 → 32+…+8 so last button has 16px bottom padding instead of 0px Co-Authored-By: Claude Sonnet 4.6 --- src/scenes/UIScene.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/scenes/UIScene.ts b/src/scenes/UIScene.ts index 074710c..19bbed7 100644 --- a/src/scenes/UIScene.ts +++ b/src/scenes/UIScene.ts @@ -127,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. */ @@ -570,7 +572,8 @@ 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 @@ -677,7 +680,7 @@ export class UIScene extends Phaser.Scene { minusBtn.on('pointerdown', () => { this.uiOpacity = Math.max(0.4, Math.round((this.uiOpacity - 0.1) * 10) / 10) this.saveUISettings() - this.updateDebugPanelBackground() + this.updateStaticPanelOpacity() this.buildSettings() }) this.settingsGroup.add(minusBtn) @@ -702,7 +705,7 @@ export class UIScene extends Phaser.Scene { plusBtn.on('pointerdown', () => { this.uiOpacity = Math.min(1.0, Math.round((this.uiOpacity + 0.1) * 10) / 10) this.saveUISettings() - this.updateDebugPanelBackground() + this.updateStaticPanelOpacity() this.buildSettings() }) this.settingsGroup.add(plusBtn) @@ -753,10 +756,12 @@ export class UIScene extends Phaser.Scene { } /** - * Applies the current uiOpacity to the debug panel text background. - * Called whenever uiOpacity changes so the debug panel stays in sync. + * 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 updateDebugPanelBackground(): void { + 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}` }) } -- 2.49.1 From 1d46446012963b09c341a293de712bdcff232b09 Mon Sep 17 00:00:00 2001 From: tekki mariani Date: Mon, 23 Mar 2026 10:48:52 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9D=20update=20CHANGELOG=20for=20I?= =?UTF-8?q?ssue=20#20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47564de..bb5be00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ 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 -- 2.49.1