@@ -3,6 +3,7 @@ import type { BuildingType, JobPriorities } from '../types'
import type { FarmingTool } from '../systems/FarmingSystem'
import type { FarmingTool } from '../systems/FarmingSystem'
import type { DebugData } from '../systems/DebugSystem'
import type { DebugData } from '../systems/DebugSystem'
import { stateManager } from '../StateManager'
import { stateManager } from '../StateManager'
import { UI_SETTINGS_KEY } from '../config'
const ITEM_ICONS : Record < string , string > = {
const ITEM_ICONS : Record < string , string > = {
wood : '🪵' , stone : '🪨' , wheat_seed : '🌱' , carrot_seed : '🥕' ,
wood : '🪵' , stone : '🪨' , wheat_seed : '🌱' , carrot_seed : '🥕' ,
@@ -46,6 +47,11 @@ export class UIScene extends Phaser.Scene {
logTexts : Phaser.GameObjects.Text [ ]
logTexts : Phaser.GameObjects.Text [ ]
} | null = null
} | 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' } ) }
constructor ( ) { super ( { key : 'UI' } ) }
/**
/**
@@ -53,6 +59,7 @@ export class UIScene extends Phaser.Scene {
* keyboard shortcuts (B, V, F3, ESC).
* keyboard shortcuts (B, V, F3, ESC).
*/
*/
create ( ) : void {
create ( ) : void {
this . loadUISettings ( )
this . createStockpilePanel ( )
this . createStockpilePanel ( )
this . createHintText ( )
this . createHintText ( )
this . createToast ( )
this . createToast ( )
@@ -85,6 +92,7 @@ export class UIScene extends Phaser.Scene {
this . escMenuGroup = this . add . group ( )
this . escMenuGroup = this . add . group ( )
this . confirmGroup = this . add . group ( )
this . confirmGroup = this . add . group ( )
this . nisseInfoGroup = this . add . group ( )
this . nisseInfoGroup = this . add . group ( )
this . settingsGroup = this . add . group ( )
this . input . on ( 'pointerdown' , ( ptr : Phaser.Input.Pointer ) = > {
this . input . on ( 'pointerdown' , ( ptr : Phaser.Input.Pointer ) = > {
if ( ptr . rightButtonDown ( ) ) {
if ( ptr . rightButtonDown ( ) ) {
@@ -196,7 +204,7 @@ export class UIScene extends Phaser.Scene {
{ kind : 'stockpile_zone' , label : '📦 Stockpile' , cost : 'free (workers deliver here)' } ,
{ kind : 'stockpile_zone' , label : '📦 Stockpile' , cost : 'free (workers deliver here)' } ,
]
]
const menuX = this . scale . width / 2 - 150 , menuY = this . scale . height / 2 - 140
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 ( 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 ) )
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 px = this . scale . width / 2 - panelW / 2
const py = this . scale . height / 2 - panelH / 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 ( bg )
this . villagerPanelGroup . add (
this . villagerPanelGroup . add (
@@ -383,10 +391,11 @@ export class UIScene extends Phaser.Scene {
/** Creates the debug panel text object (initially hidden). */
/** Creates the debug panel text object (initially hidden). */
private createDebugPanel ( ) : void {
private createDebugPanel ( ) : void {
const hexAlpha = Math . round ( this . uiOpacity * 255 ) . toString ( 16 ) . padStart ( 2 , '0' )
this . debugPanelText = this . add . text ( 10 , 80 , '' , {
this . debugPanelText = this . add . text ( 10 , 80 , '' , {
fontSize : '12px' ,
fontSize : '12px' ,
color : '#cccccc' ,
color : '#cccccc' ,
backgroundColor : ' #000000cc' ,
backgroundColor : ` #000000${ hexAlpha } ` ,
padding : { x : 8 , y : 6 } ,
padding : { x : 8 , y : 6 } ,
lineSpacing : 2 ,
lineSpacing : 2 ,
fontFamily : 'monospace' ,
fontFamily : 'monospace' ,
@@ -463,7 +472,7 @@ export class UIScene extends Phaser.Scene {
const mx = Math . min ( x , this . scale . width - menuW - 4 )
const mx = Math . min ( x , this . scale . width - menuW - 4 )
const my = Math . min ( y , this . scale . height - menuH - 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 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 300 )
this . contextMenuGroup . add ( bg )
this . contextMenuGroup . add ( bg )
@@ -521,6 +530,7 @@ export class UIScene extends Phaser.Scene {
if ( this . buildMenuVisible ) { this . closeBuildMenu ( ) ; return }
if ( this . buildMenuVisible ) { this . closeBuildMenu ( ) ; return }
if ( this . villagerPanelVisible ) { this . closeVillagerPanel ( ) ; return }
if ( this . villagerPanelVisible ) { this . closeVillagerPanel ( ) ; return }
if ( this . nisseInfoVisible ) { this . closeNisseInfoPanel ( ) ; return }
if ( this . nisseInfoVisible ) { this . closeNisseInfoPanel ( ) ; return }
if ( this . settingsVisible ) { this . closeSettings ( ) ; return }
if ( this . escMenuVisible ) { this . closeEscMenu ( ) ; return }
if ( this . escMenuVisible ) { this . closeEscMenu ( ) ; return }
// Build/farm mode: let BuildingSystem / FarmingSystem handle their own ESC key.
// Build/farm mode: let BuildingSystem / FarmingSystem handle their own ESC key.
// We only skip opening the ESC menu while those modes are active.
// 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 mx = this . scale . width / 2 - menuW / 2
const my = this . scale . height / 2 - menuH / 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 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 400 )
this . escMenuGroup . add ( bg )
this . escMenuGroup . add ( bg )
this . escMenuGroup . add (
this . escMenuGroup . add (
@@ -602,10 +612,153 @@ export class UIScene extends Phaser.Scene {
window . location . reload ( )
window . location . reload ( )
}
}
/** Opens an empty Settings panel (placeholder) . */
/** Opens the Settings overlay . */
private doSettings ( ) : void {
private doSettings ( ) : void {
this . closeEscMenu ( )
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. */
/** 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 dx = this . scale . width / 2 - dialogW / 2
const dy = this . scale . height / 2 - dialogH / 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 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 500 )
this . confirmGroup . add ( bg )
this . confirmGroup . add ( bg )
@@ -692,7 +845,6 @@ export class UIScene extends Phaser.Scene {
private openNisseInfoPanel ( villagerId : string ) : void {
private openNisseInfoPanel ( villagerId : string ) : void {
this . nisseInfoId = villagerId
this . nisseInfoId = villagerId
this . nisseInfoVisible = true
this . nisseInfoVisible = true
this . scene . get ( 'Game' ) . events . emit ( 'uiMenuOpen' )
this . buildNisseInfoPanel ( )
this . buildNisseInfoPanel ( )
}
}
@@ -703,7 +855,6 @@ export class UIScene extends Phaser.Scene {
this . nisseInfoId = null
this . nisseInfoId = null
this . nisseInfoGroup . destroy ( true )
this . nisseInfoGroup . destroy ( true )
this . nisseInfoGroup = this . add . group ( )
this . nisseInfoGroup = this . add . group ( )
this . scene . get ( 'Game' ) . events . emit ( 'uiMenuClose' )
}
}
/**
/**
@@ -730,7 +881,7 @@ export class UIScene extends Phaser.Scene {
// Background
// Background
this . nisseInfoGroup . add (
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 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 250 )
)
)
@@ -883,6 +1034,7 @@ export class UIScene extends Phaser.Scene {
if ( this . villagerPanelVisible ) this . closeVillagerPanel ( )
if ( this . villagerPanelVisible ) this . closeVillagerPanel ( )
if ( this . contextMenuVisible ) this . hideContextMenu ( )
if ( this . contextMenuVisible ) this . hideContextMenu ( )
if ( this . escMenuVisible ) this . closeEscMenu ( )
if ( this . escMenuVisible ) this . closeEscMenu ( )
if ( this . settingsVisible ) this . closeSettings ( )
if ( this . confirmVisible ) this . hideConfirm ( )
if ( this . confirmVisible ) this . hideConfirm ( )
if ( this . nisseInfoVisible ) this . closeNisseInfoPanel ( )
if ( this . nisseInfoVisible ) this . closeNisseInfoPanel ( )
}
}