✨ overlay opacity: global setting + settings screen (Issue #16)
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, string> = {
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user