@@ -22,8 +22,6 @@ export class UIScene extends Phaser.Scene {
private villagerPanelVisible = false
private buildModeText ! : Phaser . GameObjects . Text
private farmToolText ! : Phaser . GameObjects . Text
private coordsText ! : Phaser . GameObjects . Text
private controlsHintText ! : Phaser . GameObjects . Text
private popText ! : Phaser . GameObjects . Text
private stockpileTitleText ! : Phaser . GameObjects . Text
private contextMenuGroup ! : Phaser . GameObjects . Group
@@ -61,6 +59,18 @@ export class UIScene extends Phaser.Scene {
/** True while the zone-edit tool is active (shown in ESC priority stack). */
private inForesterZoneEdit = false
// ── Action Bar ────────────────────────────────────────────────────────────
private static readonly BAR_H = 48
private static readonly TRAY_H = 68
private actionBarBg ! : Phaser . GameObjects . Rectangle
private actionBuildBtn ! : Phaser . GameObjects . Rectangle
private actionBuildLabel ! : Phaser . GameObjects . Text
private actionNisseBtn ! : Phaser . GameObjects . Rectangle
private actionNisseLabel ! : Phaser . GameObjects . Text
private actionTrayGroup ! : Phaser . GameObjects . Group
private actionTrayVisible = false
private activeCategory : 'build' | 'nisse' | null = null
constructor ( ) { super ( { key : 'UI' } ) }
/**
@@ -75,15 +85,14 @@ export class UIScene extends Phaser.Scene {
this . createBuildMenu ( )
this . createBuildModeIndicator ( )
this . createFarmToolIndicator ( )
this . createCoordsDisplay ( )
this . createDebugPanel ( )
this . createActionBar ( )
const gameScene = this . scene . get ( 'Game' )
gameScene . events . on ( 'buildModeChanged' , ( a : boolean , b : BuildingType ) = > this . onBuildModeChanged ( a , b ) )
gameScene . events . on ( 'farmToolChanged' , ( t : FarmingTool , l : string ) = > this . onFarmToolChanged ( t , l ) )
gameScene . events . on ( 'toast' , ( m : string ) = > this . showToast ( m ) )
gameScene . events . on ( 'openBuildMenu' , ( ) = > this . toggleBuildMenu ( ) )
gameScene . events . on ( 'cameraMoved' , ( pos : { tileX : number ; tileY : number } ) = > this . onCameraMoved ( pos ) )
this . input . keyboard ! . addKey ( Phaser . Input . Keyboard . KeyCodes . B )
. on ( 'down' , ( ) = > gameScene . events . emit ( 'uiRequestBuildMenu' ) )
@@ -103,6 +112,7 @@ export class UIScene extends Phaser.Scene {
this . nisseInfoGroup = this . add . group ( )
this . settingsGroup = this . add . group ( )
this . foresterPanelGroup = this . add . group ( )
this . actionTrayGroup = this . add . group ( )
gameScene . events . on ( 'foresterHutClicked' , ( id : string ) = > this . openForesterPanel ( id ) )
gameScene . events . on ( 'foresterZoneEditEnded' , ( ) = > this . onForesterEditEnded ( ) )
@@ -175,7 +185,7 @@ export class UIScene extends Phaser.Scene {
/** Creates the centered hint text element near the bottom of the screen. */
private createHintText ( ) : void {
this . hintText = this . add . text ( this . scale . width / 2 , this . scale . height - 40 , '' , {
this . hintText = this . add . text ( this . scale . width / 2 , this . scale . height - UIScene . BAR_H - 24 , '' , {
fontSize : '14px' , color : '#ffff88' , fontFamily : 'monospace' ,
backgroundColor : '#00000099' , padding : { x : 10 , y : 5 } ,
} ) . setOrigin ( 0.5 ) . setScrollFactor ( 0 ) . setDepth ( 100 ) . setVisible ( false )
@@ -270,6 +280,10 @@ export class UIScene extends Phaser.Scene {
this . villagerPanelVisible = false
this . villagerPanelGroup ? . destroy ( true )
this . scene . get ( 'Game' ) . events . emit ( 'uiMenuClose' )
if ( this . activeCategory === 'nisse' ) {
this . activeCategory = null
this . updateCategoryHighlights ( )
}
}
/**
@@ -386,24 +400,6 @@ export class UIScene extends Phaser.Scene {
this . farmToolText . setText ( tool === 'none' ? '' : ` [F] Farm: ${ label } [RMB cancel] ` ) . setVisible ( tool !== 'none' )
}
// ─── Coords + controls ────────────────────────────────────────────────────
/** Creates the tile-coordinate display and controls hint at the bottom-left. */
private createCoordsDisplay ( ) : void {
this . coordsText = this . add . text ( 10 , this . scale . height - 24 , '' , { fontSize : '11px' , color : '#666666' , fontFamily : 'monospace' } ) . setScrollFactor ( 0 ) . setDepth ( 100 )
this . controlsHintText = this . add . text ( 10 , this . scale . height - 42 , '[WASD] Pan [Scroll] Zoom [F] Farm [B] Build [V] Nisse [F3] Debug' , {
fontSize : '10px' , color : '#444444' , fontFamily : 'monospace' , backgroundColor : '#00000066' , padding : { x : 4 , y : 2 }
} ) . setScrollFactor ( 0 ) . setDepth ( 100 )
}
/**
* Updates the tile-coordinate display when the camera moves.
* @param pos - Tile position of the camera center
*/
private onCameraMoved ( pos : { tileX : number ; tileY : number } ) : void {
this . coordsText . setText ( ` Tile: ${ pos . tileX } , ${ pos . tileY } ` )
}
// ─── Debug Panel (F3) ─────────────────────────────────────────────────────
/** Creates the debug panel text object (initially hidden). */
@@ -547,6 +543,7 @@ export class UIScene extends Phaser.Scene {
if ( this . foresterPanelVisible ) { this . closeForesterPanel ( ) ; return }
if ( this . contextMenuVisible ) { this . hideContextMenu ( ) ; return }
if ( this . buildMenuVisible ) { this . closeBuildMenu ( ) ; return }
if ( this . actionTrayVisible ) { this . closeActionTray ( ) ; return }
if ( this . villagerPanelVisible ) { this . closeVillagerPanel ( ) ; return }
if ( this . nisseInfoVisible ) { this . closeNisseInfoPanel ( ) ; return }
if ( this . settingsVisible ) { this . closeSettings ( ) ; return }
@@ -589,8 +586,9 @@ export class UIScene extends Phaser.Scene {
{ label : '⚙️ Settings' , action : ( ) = > this . doSettings ( ) } ,
{ label : '🆕 New Game' , action : ( ) = > this . doNewGame ( ) } ,
]
// 32px header + entries × (btnH + 8px gap) + 8px bottom padding
const menuH = 32 + entries . length * ( btnH + 8 ) + 8
const keysBlock = '[WASD] Pan [Scroll] Zoom\n[F] Farm [B] Build [V] Nisse\n[F3] Debug [ESC] Menu'
// 32px header + entries × (btnH + 8px gap) + 8px sep + 46px keys block + 12px bottom padding
const menuH = 32 + entries . length * ( btnH + 8 ) + 8 + 46 + 12
const mx = this . scale . width / 2 - menuW / 2
const my = this . scale . height / 2 - menuH / 2
@@ -617,6 +615,14 @@ export class UIScene extends Phaser.Scene {
} ) . setOrigin ( 0 , 0.5 ) . setScrollFactor ( 0 ) . setDepth ( 402 )
)
} )
// Keyboard shortcuts reference at the bottom of the menu
const keysY = my + 32 + entries . length * ( btnH + 8 ) + 8
this . escMenuGroup . add (
this . add . text ( mx + menuW / 2 , keysY , keysBlock , {
fontSize : '10px' , color : '#555555' , fontFamily : 'monospace' , align : 'center' ,
} ) . setOrigin ( 0.5 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 401 )
)
}
/** Saves the game and shows a toast confirmation. */
@@ -1150,6 +1156,152 @@ export class UIScene extends Phaser.Scene {
}
}
// ─── Action Bar ───────────────────────────────────────────────────────────
/**
* Creates the persistent bottom action bar with Build and Nisse category buttons.
* The bar is always visible; individual button highlights change with the active category.
*/
private createActionBar ( ) : void {
const { width , height } = this . scale
const barY = height - UIScene . BAR_H
this . actionBarBg = this . add . rectangle ( 0 , barY , width , UIScene . BAR_H , 0x080808 , 0.92 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 300 )
this . actionBuildBtn = this . add . rectangle ( 8 , barY + 8 , 88 , 32 , 0x1a3a1a , 0.9 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 301 ) . setInteractive ( )
this . actionBuildBtn . on ( 'pointerover' , ( ) = > {
if ( this . activeCategory !== 'build' ) this . actionBuildBtn . setFillStyle ( 0x2a5a2a , 0.9 )
} )
this . actionBuildBtn . on ( 'pointerout' , ( ) = > {
if ( this . activeCategory !== 'build' ) this . actionBuildBtn . setFillStyle ( 0x1a3a1a , 0.9 )
} )
this . actionBuildBtn . on ( 'pointerdown' , ( ) = > this . toggleCategory ( 'build' ) )
this . actionBuildLabel = this . add . text ( 52 , barY + UIScene . BAR_H / 2 , '🔨 Build' , {
fontSize : '12px' , color : '#cccccc' , fontFamily : 'monospace' ,
} ) . setOrigin ( 0.5 , 0.5 ) . setScrollFactor ( 0 ) . setDepth ( 302 )
this . actionNisseBtn = this . add . rectangle ( 104 , barY + 8 , 88 , 32 , 0x1a1a3a , 0.9 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 301 ) . setInteractive ( )
this . actionNisseBtn . on ( 'pointerover' , ( ) = > {
if ( this . activeCategory !== 'nisse' ) this . actionNisseBtn . setFillStyle ( 0x2a2a5a , 0.9 )
} )
this . actionNisseBtn . on ( 'pointerout' , ( ) = > {
if ( this . activeCategory !== 'nisse' ) this . actionNisseBtn . setFillStyle ( 0x1a1a3a , 0.9 )
} )
this . actionNisseBtn . on ( 'pointerdown' , ( ) = > this . toggleCategory ( 'nisse' ) )
this . actionNisseLabel = this . add . text ( 148 , barY + UIScene . BAR_H / 2 , '👥 Nisse' , {
fontSize : '12px' , color : '#cccccc' , fontFamily : 'monospace' ,
} ) . setOrigin ( 0.5 , 0.5 ) . setScrollFactor ( 0 ) . setDepth ( 302 )
}
/**
* Toggles the given action bar category on or off.
* Selecting the active category deselects it; selecting a new one closes the previous.
* @param cat - The category to toggle ('build' or 'nisse')
*/
private toggleCategory ( cat : 'build' | 'nisse' ) : void {
if ( this . activeCategory === cat ) {
this . deactivateCategory ( )
return
}
// Close whatever was open before
if ( this . activeCategory === 'build' ) this . closeActionTray ( )
if ( this . activeCategory === 'nisse' && this . villagerPanelVisible ) this . closeVillagerPanel ( )
this . activeCategory = cat
this . updateCategoryHighlights ( )
if ( cat === 'build' ) {
this . openActionTray ( )
} else {
this . openVillagerPanel ( )
}
}
/**
* Deactivates the currently active category, closing its associated panel or tray.
*/
private deactivateCategory ( ) : void {
if ( this . activeCategory === 'build' ) this . closeActionTray ( )
if ( this . activeCategory === 'nisse' && this . villagerPanelVisible ) this . closeVillagerPanel ( )
this . activeCategory = null
this . updateCategoryHighlights ( )
}
/**
* Updates the visual highlight of the Build and Nisse buttons
* to reflect the current active category.
*/
private updateCategoryHighlights ( ) : void {
this . actionBuildBtn . setFillStyle ( this . activeCategory === 'build' ? 0x3d7a3d : 0x1a3a1a , 0.9 )
this . actionNisseBtn . setFillStyle ( this . activeCategory === 'nisse' ? 0x3d3d7a : 0x1a1a3a , 0.9 )
}
/**
* Builds and shows the building tool tray above the action bar.
* Each building is shown as a clickable tile with emoji and name.
*/
private openActionTray ( ) : void {
if ( this . actionTrayVisible ) return
this . actionTrayVisible = true
this . actionTrayGroup . destroy ( true )
this . actionTrayGroup = this . add . group ( )
const { width , height } = this . scale
const trayY = height - UIScene . BAR_H - UIScene . TRAY_H
const bg = this . add . rectangle ( 0 , trayY , width , UIScene . TRAY_H , 0x0d0d0d , 0.88 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 300 )
this . actionTrayGroup . add ( bg )
const buildings : { kind : BuildingType ; emoji : string ; label : string } [ ] = [
{ kind : 'floor' , emoji : '🪵' , label : 'Floor' } ,
{ kind : 'wall' , emoji : '🧱' , label : 'Wall' } ,
{ kind : 'chest' , emoji : '📦' , label : 'Chest' } ,
{ kind : 'bed' , emoji : '🛏' , label : 'Bed' } ,
{ kind : 'stockpile_zone' , emoji : '📦' , label : 'Stockpile' } ,
{ kind : 'forester_hut' , emoji : '🌲' , label : 'Forester' } ,
]
const itemW = 84
buildings . forEach ( ( b , i ) = > {
const bx = 8 + i * ( itemW + 4 )
const btn = this . add . rectangle ( bx , trayY + 4 , itemW , UIScene . TRAY_H - 8 , 0x1a2a1a , 0.9 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 301 ) . setInteractive ( )
btn . on ( 'pointerover' , ( ) = > btn . setFillStyle ( 0x2d4a2d , 0.9 ) )
btn . on ( 'pointerout' , ( ) = > btn . setFillStyle ( 0x1a2a1a , 0.9 ) )
btn . on ( 'pointerdown' , ( ) = > {
this . closeActionTray ( )
this . deactivateCategory ( )
this . scene . get ( 'Game' ) . events . emit ( 'selectBuilding' , b . kind )
} )
this . actionTrayGroup . add ( btn )
this . actionTrayGroup . add (
this . add . text ( bx + itemW / 2 , trayY + 18 , b . emoji , { fontSize : '18px' } )
. setOrigin ( 0.5 , 0.5 ) . setScrollFactor ( 0 ) . setDepth ( 302 )
)
this . actionTrayGroup . add (
this . add . text ( bx + itemW / 2 , trayY + 44 , b . label , {
fontSize : '10px' , color : '#cccccc' , fontFamily : 'monospace' ,
} ) . setOrigin ( 0.5 , 0.5 ) . setScrollFactor ( 0 ) . setDepth ( 302 )
)
} )
}
/**
* Hides and destroys the building tool tray.
*/
private closeActionTray ( ) : void {
if ( ! this . actionTrayVisible ) return
this . actionTrayVisible = false
this . actionTrayGroup . destroy ( true )
this . actionTrayGroup = this . add . group ( )
}
// ─── Resize ───────────────────────────────────────────────────────────────
/**
@@ -1170,11 +1322,16 @@ export class UIScene extends Phaser.Scene {
}
// Bottom elements
this . hintText . setPosition ( width / 2 , height - 40 )
this . hintText . setPosition ( width / 2 , height - UIScene . BAR_H - 24 )
this . toastText . setPosition ( width / 2 , 60 )
this . coordsText . setPosition ( 10 , height - 24 )
this . controlsHintText . setPosition ( 10 , height - 42 )
// Action bar — reposition persistent elements
this . actionBarBg . setPosition ( 0 , height - UIScene . BAR_H ) . setSize ( width , UIScene . BAR_H )
this . actionBuildBtn . setPosition ( 8 , height - UIScene . BAR_H + 8 )
this . actionBuildLabel . setPosition ( 48 , height - UIScene . BAR_H + UIScene . BAR_H / 2 )
this . actionNisseBtn . setPosition ( 104 , height - UIScene . BAR_H + 8 )
this . actionNisseLabel . setPosition ( 144 , height - UIScene . BAR_H + UIScene . BAR_H / 2 )
if ( this . actionTrayVisible ) this . closeActionTray ( )
// Close centered panels — their position is calculated on open, so they
// would be off-center if left open during a resize
if ( this . buildMenuVisible ) this . closeBuildMenu ( )