2026-03-20 08:11:31 +00:00
import Phaser from 'phaser'
import type { BuildingType , JobPriorities } from '../types'
import type { FarmingTool } from '../systems/FarmingSystem'
2026-03-21 12:11:54 +00:00
import type { DebugData } from '../systems/DebugSystem'
2026-03-20 08:11:31 +00:00
import { stateManager } from '../StateManager'
const ITEM_ICONS : Record < string , string > = {
wood : '🪵' , stone : '🪨' , wheat_seed : '🌱' , carrot_seed : '🥕' ,
wheat : '🌾' , carrot : '🧡' ,
}
export class UIScene extends Phaser . Scene {
private stockpileTexts : Map < string , Phaser.GameObjects.Text > = new Map ( )
private stockpilePanel ! : Phaser . GameObjects . Rectangle
private hintText ! : Phaser . GameObjects . Text
private toastText ! : Phaser . GameObjects . Text
private toastTimer = 0
private buildMenuGroup ! : Phaser . GameObjects . Group
private buildMenuVisible = false
private villagerPanelGroup ! : Phaser . GameObjects . Group
private villagerPanelVisible = false
private buildModeText ! : Phaser . GameObjects . Text
private farmToolText ! : Phaser . GameObjects . Text
private coordsText ! : Phaser . GameObjects . Text
2026-03-20 12:19:57 +00:00
private controlsHintText ! : Phaser . GameObjects . Text
2026-03-20 08:11:31 +00:00
private popText ! : Phaser . GameObjects . Text
2026-03-20 12:19:57 +00:00
private stockpileTitleText ! : Phaser . GameObjects . Text
2026-03-20 11:57:09 +00:00
private contextMenuGroup ! : Phaser . GameObjects . Group
private contextMenuVisible = false
private inBuildMode = false
private inFarmMode = false
2026-03-21 12:11:54 +00:00
private debugPanelText ! : Phaser . GameObjects . Text
private debugActive = false
2026-03-20 08:11:31 +00:00
constructor ( ) { super ( { key : 'UI' } ) }
2026-03-21 12:11:54 +00:00
/ * *
* Creates all HUD elements , wires up game scene events , and registers
* keyboard shortcuts ( B , V , F3 , ESC ) .
* /
2026-03-20 08:11:31 +00:00
create ( ) : void {
this . createStockpilePanel ( )
this . createHintText ( )
this . createToast ( )
this . createBuildMenu ( )
this . createBuildModeIndicator ( )
this . createFarmToolIndicator ( )
this . createCoordsDisplay ( )
2026-03-21 12:11:54 +00:00
this . createDebugPanel ( )
2026-03-20 08:11:31 +00:00
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' ) )
this . input . keyboard ! . addKey ( Phaser . Input . Keyboard . KeyCodes . V )
. on ( 'down' , ( ) = > this . toggleVillagerPanel ( ) )
2026-03-21 12:11:54 +00:00
this . input . keyboard ! . addKey ( Phaser . Input . Keyboard . KeyCodes . F3 )
. on ( 'down' , ( ) = > this . toggleDebugPanel ( ) )
2026-03-20 08:11:31 +00:00
this . scale . on ( 'resize' , ( ) = > this . repositionUI ( ) )
2026-03-20 11:57:09 +00:00
this . input . mouse ! . disableContextMenu ( )
this . contextMenuGroup = this . add . group ( )
this . input . on ( 'pointerdown' , ( ptr : Phaser.Input.Pointer ) = > {
if ( ptr . rightButtonDown ( ) ) {
if ( ! this . inBuildMode && ! this . inFarmMode && ! this . buildMenuVisible && ! this . villagerPanelVisible ) {
this . showContextMenu ( ptr . x , ptr . y )
}
} else if ( this . contextMenuVisible ) {
this . hideContextMenu ( )
}
} )
this . input . keyboard ! . addKey ( Phaser . Input . Keyboard . KeyCodes . ESC )
. on ( 'down' , ( ) = > this . hideContextMenu ( ) )
2026-03-20 08:11:31 +00:00
}
2026-03-21 12:11:54 +00:00
/ * *
* Updates the stockpile display , toast fade timer , population count ,
* and the debug panel each frame .
* @param _t - Total elapsed time ( unused )
* @param delta - Frame delta in milliseconds
* /
2026-03-20 08:11:31 +00:00
update ( _t : number , delta : number ) : void {
this . updateStockpile ( )
this . updateToast ( delta )
this . updatePopText ( )
2026-03-21 12:11:54 +00:00
if ( this . debugActive ) this . updateDebugPanel ( )
2026-03-20 08:11:31 +00:00
}
// ─── Stockpile ────────────────────────────────────────────────────────────
2026-03-21 12:11:54 +00:00
/** Creates the stockpile panel in the top-right corner with item rows and population count. */
2026-03-20 08:11:31 +00:00
private createStockpilePanel ( ) : void {
const x = this . scale . width - 178 , y = 10
this . stockpilePanel = this . add . rectangle ( x , y , 168 , 165 , 0x000000 , 0.72 ) . setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 100 )
2026-03-20 12:19:57 +00:00
this . stockpileTitleText = this . add . text ( x + 10 , y + 7 , '⚡ STOCKPILE' , { fontSize : '11px' , color : '#aaaaaa' , fontFamily : 'monospace' } ) . setScrollFactor ( 0 ) . setDepth ( 101 )
2026-03-20 08:11:31 +00:00
const items = [ 'wood' , 'stone' , 'wheat_seed' , 'carrot_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 )
} )
2026-03-20 17:07:34 +00:00
this . popText = this . add . text ( x + 10 , y + 145 , '👥 Nisse: 0 / 0' , { fontSize : '11px' , color : '#aaaaaa' , fontFamily : 'monospace' } ) . setScrollFactor ( 0 ) . setDepth ( 101 )
2026-03-20 08:11:31 +00:00
}
2026-03-21 12:11:54 +00:00
/** Refreshes all item quantities and colors in the stockpile panel. */
2026-03-20 08:11:31 +00:00
private updateStockpile ( ) : void {
const sp = stateManager . getState ( ) . world . stockpile
for ( const [ item , t ] of this . stockpileTexts ) {
const qty = sp [ item as keyof typeof sp ] ? ? 0
t . setStyle ( { color : qty > 0 ? '#88dd88' : '#444444' } )
t . setText ( ` ${ ITEM_ICONS [ item ] } ${ item } : ${ qty } ` )
}
}
2026-03-21 12:11:54 +00:00
/** Updates the Nisse population / bed capacity counter. */
2026-03-20 08:11:31 +00:00
private updatePopText ( ) : void {
const state = stateManager . getState ( )
const beds = Object . values ( state . world . buildings ) . filter ( b = > b . kind === 'bed' ) . length
const current = Object . keys ( state . world . villagers ) . length
2026-03-20 17:07:34 +00:00
this . popText ? . setText ( ` 👥 Nisse: ${ current } / ${ beds } [V] ` )
2026-03-20 08:11:31 +00:00
}
// ─── Hint ─────────────────────────────────────────────────────────────────
2026-03-21 12:11:54 +00:00
/** Creates the centered hint text element near the bottom of the screen. */
2026-03-20 08:11:31 +00:00
private createHintText ( ) : void {
this . hintText = this . add . text ( this . scale . width / 2 , this . scale . height - 40 , '' , {
fontSize : '14px' , color : '#ffff88' , fontFamily : 'monospace' ,
backgroundColor : '#00000099' , padding : { x : 10 , y : 5 } ,
} ) . setOrigin ( 0.5 ) . setScrollFactor ( 0 ) . setDepth ( 100 ) . setVisible ( false )
}
// ─── Toast ────────────────────────────────────────────────────────────────
2026-03-21 12:11:54 +00:00
/** Creates the toast notification text element (top center, initially hidden). */
2026-03-20 08:11:31 +00:00
private createToast ( ) : void {
this . toastText = this . add . text ( this . scale . width / 2 , 60 , '' , {
fontSize : '15px' , color : '#88ff88' , fontFamily : 'monospace' ,
backgroundColor : '#00000099' , padding : { x : 12 , y : 6 } ,
} ) . setOrigin ( 0.5 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 102 ) . setAlpha ( 0 )
}
2026-03-21 12:11:54 +00:00
/ * *
* Displays a toast message for 2.2 seconds then fades it out .
* @param msg - Message to display
* /
2026-03-20 08:11:31 +00:00
showToast ( msg : string ) : void { this . toastText . setText ( msg ) . setAlpha ( 1 ) ; this . toastTimer = 2200 }
2026-03-21 12:11:54 +00:00
/ * *
* Counts down the toast timer and triggers the fade - out tween when it expires .
* @param delta - Frame delta in milliseconds
* /
2026-03-20 08:11:31 +00:00
private updateToast ( delta : number ) : void {
if ( this . toastTimer <= 0 ) return
this . toastTimer -= delta
if ( this . toastTimer <= 0 ) this . tweens . add ( { targets : this.toastText , alpha : 0 , duration : 400 } )
}
// ─── Build Menu ───────────────────────────────────────────────────────────
2026-03-21 12:11:54 +00:00
/** Creates and hides the build menu with buttons for each available building type. */
2026-03-20 08:11:31 +00:00
private createBuildMenu ( ) : void {
this . buildMenuGroup = this . add . group ( )
const buildings : { kind : BuildingType ; label : string ; cost : string } [ ] = [
{ kind : 'floor' , label : 'Floor' , cost : '2 wood' } ,
{ kind : 'wall' , label : 'Wall' , cost : '3 wood + 1 stone' } ,
{ kind : 'chest' , label : 'Chest' , cost : '5 wood + 2 stone' } ,
{ kind : 'bed' , label : '🛏 Bed' , cost : '6 wood (+1 villager)' } ,
{ 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 )
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 ) )
buildings . forEach ( ( b , i ) = > {
const btnY = menuY + 38 + i * 46
const btn = this . add . rectangle ( menuX + 14 , btnY , 272 , 38 , 0x1a3a1a , 0.9 ) . setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 201 ) . setInteractive ( )
btn . on ( 'pointerover' , ( ) = > btn . setFillStyle ( 0x2d6a4f , 0.9 ) )
btn . on ( 'pointerout' , ( ) = > btn . setFillStyle ( 0x1a3a1a , 0.9 ) )
btn . on ( 'pointerdown' , ( ) = > { this . closeBuildMenu ( ) ; this . scene . get ( 'Game' ) . events . emit ( 'selectBuilding' , b . kind ) } )
this . buildMenuGroup . add ( btn )
this . buildMenuGroup . add ( this . add . text ( menuX + 24 , btnY + 5 , b . label , { fontSize : '13px' , color : '#ffffff' , fontFamily : 'monospace' } ) . setScrollFactor ( 0 ) . setDepth ( 202 ) )
this . buildMenuGroup . add ( this . add . text ( menuX + 24 , btnY + 22 , ` Cost: ${ b . cost } ` , { fontSize : '10px' , color : '#888888' , fontFamily : 'monospace' } ) . setScrollFactor ( 0 ) . setDepth ( 202 ) )
} )
this . buildMenuGroup . setVisible ( false )
}
2026-03-21 12:11:54 +00:00
/** Toggles the build menu open or closed. */
2026-03-20 08:11:31 +00:00
private toggleBuildMenu ( ) : void { this . buildMenuVisible ? this . closeBuildMenu ( ) : this . openBuildMenu ( ) }
2026-03-21 12:11:54 +00:00
/** Opens the build menu and notifies GameScene that a menu is active. */
2026-03-20 08:11:31 +00:00
private openBuildMenu ( ) : void { this . buildMenuVisible = true ; this . buildMenuGroup . setVisible ( true ) ; this . scene . get ( 'Game' ) . events . emit ( 'uiMenuOpen' ) }
2026-03-21 12:11:54 +00:00
/** Closes the build menu and notifies GameScene that no menu is active. */
2026-03-20 08:11:31 +00:00
private closeBuildMenu ( ) : void { this . buildMenuVisible = false ; this . buildMenuGroup . setVisible ( false ) ; this . scene . get ( 'Game' ) . events . emit ( 'uiMenuClose' ) }
// ─── Villager Panel (V key) ───────────────────────────────────────────────
2026-03-21 12:11:54 +00:00
/** Toggles the Nisse management panel open or closed. */
2026-03-20 08:11:31 +00:00
private toggleVillagerPanel ( ) : void {
if ( this . villagerPanelVisible ) {
this . closeVillagerPanel ( )
} else {
this . openVillagerPanel ( )
}
}
2026-03-21 12:11:54 +00:00
/** Opens the Nisse panel, builds its contents, and notifies GameScene. */
2026-03-20 08:11:31 +00:00
private openVillagerPanel ( ) : void {
this . villagerPanelVisible = true
this . buildVillagerPanel ( )
this . scene . get ( 'Game' ) . events . emit ( 'uiMenuOpen' )
}
2026-03-21 12:11:54 +00:00
/** Closes and destroys the Nisse panel and notifies GameScene. */
2026-03-20 08:11:31 +00:00
private closeVillagerPanel ( ) : void {
this . villagerPanelVisible = false
this . villagerPanelGroup ? . destroy ( true )
this . scene . get ( 'Game' ) . events . emit ( 'uiMenuClose' )
}
2026-03-21 12:11:54 +00:00
/ * *
* Destroys and rebuilds the Nisse panel from current state .
* Shows name , status , energy bar , and job priority buttons per Nisse .
* /
2026-03-20 08:11:31 +00:00
private buildVillagerPanel ( ) : void {
if ( this . villagerPanelGroup ) this . villagerPanelGroup . destroy ( true )
this . villagerPanelGroup = this . add . group ( )
const state = stateManager . getState ( )
const villagers = Object . values ( state . world . villagers )
const panelW = 420
const rowH = 60
const panelH = Math . max ( 100 , villagers . length * rowH + 50 )
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 )
this . villagerPanelGroup . add ( bg )
this . villagerPanelGroup . add (
2026-03-20 17:07:34 +00:00
this . add . text ( px + panelW / 2 , py + 12 , '👥 NISSE [V] close' , { fontSize : '12px' , color : '#aaaaaa' , fontFamily : 'monospace' } )
2026-03-20 08:11:31 +00:00
. setOrigin ( 0.5 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 211 )
)
if ( villagers . length === 0 ) {
this . villagerPanelGroup . add (
2026-03-20 17:07:34 +00:00
this . add . text ( px + panelW / 2 , py + panelH / 2 , 'No Nisse yet.\nBuild a 🛏 Bed first!' , {
2026-03-20 08:11:31 +00:00
fontSize : '13px' , color : '#666666' , fontFamily : 'monospace' , align : 'center'
} ) . setOrigin ( 0.5 ) . setScrollFactor ( 0 ) . setDepth ( 211 )
)
}
villagers . forEach ( ( v , i ) = > {
const ry = py + 38 + i * rowH
const gameScene = this . scene . get ( 'Game' ) as any
// Name + status
const statusText = gameScene . villagerSystem ? . getStatusText ( v . id ) ? ? '—'
this . villagerPanelGroup . add (
this . add . text ( px + 12 , ry , ` ${ v . name } ` , { fontSize : '13px' , color : '#ffffff' , fontFamily : 'monospace' } ) . setScrollFactor ( 0 ) . setDepth ( 211 )
)
this . villagerPanelGroup . add (
this . add . text ( px + 12 , ry + 16 , statusText , { fontSize : '10px' , color : '#888888' , fontFamily : 'monospace' } ) . setScrollFactor ( 0 ) . setDepth ( 211 )
)
// Energy bar
const eg = this . add . graphics ( ) . setScrollFactor ( 0 ) . setDepth ( 211 )
eg . fillStyle ( 0x333333 ) ; eg . fillRect ( px + 12 , ry + 30 , 80 , 6 )
const col = v . energy > 60 ? 0x4CAF50 : v.energy > 30 ? 0xFF9800 : 0xF44336
eg . fillStyle ( col ) ; eg . fillRect ( px + 12 , ry + 30 , 80 * v . energy / 100 , 6 )
this . villagerPanelGroup . add ( eg )
// Job priority buttons: chop / mine / farm
const jobs : Array < { key : keyof JobPriorities ; label : string } > = [
{ key : 'chop' , label : '🪓' } , { key : 'mine' , label : '⛏' } , { key : 'farm' , label : '🌾' }
]
jobs . forEach ( ( job , ji ) = > {
const bx = px + 110 + ji * 100
const pri = v . priorities [ job . key ]
const label = pri === 0 ? ` ${ job . label } OFF ` : ` ${ job . label } P ${ pri } `
const btn = this . add . text ( bx , ry + 6 , label , {
fontSize : '11px' , color : pri === 0 ? '#555555' : '#ffffff' ,
fontFamily : 'monospace' , backgroundColor : pri === 0 ? '#1a1a1a' : '#1a4a1a' ,
padding : { x : 6 , y : 4 }
} ) . setScrollFactor ( 0 ) . setDepth ( 212 ) . setInteractive ( )
btn . on ( 'pointerover' , ( ) = > btn . setStyle ( { backgroundColor : '#2d6a4f' } ) )
btn . on ( 'pointerout' , ( ) = > btn . setStyle ( { backgroundColor : pri === 0 ? '#1a1a1a' : '#1a4a1a' } ) )
btn . on ( 'pointerdown' , ( ) = > {
const newPri = ( ( v . priorities [ job . key ] + 1 ) % 5 ) // 0→1→2→3→4→0
const newPriorities : JobPriorities = { . . . v . priorities , [ job . key ] : newPri }
this . scene . get ( 'Game' ) . events . emit ( 'updatePriorities' , v . id , newPriorities )
this . closeVillagerPanel ( )
this . openVillagerPanel ( ) // Rebuild to reflect change
} )
this . villagerPanelGroup . add ( btn )
} )
} )
}
// ─── Build mode indicator ─────────────────────────────────────────────────
2026-03-21 12:11:54 +00:00
/** Creates the build-mode indicator text in the top-left corner (initially hidden). */
2026-03-20 08:11:31 +00:00
private createBuildModeIndicator ( ) : void {
this . buildModeText = this . add . text ( 10 , 10 , '' , { fontSize : '13px' , color : '#ffff00' , fontFamily : 'monospace' , backgroundColor : '#00000099' , padding : { x : 8 , y : 4 } } ) . setScrollFactor ( 0 ) . setDepth ( 100 ) . setVisible ( false )
}
2026-03-21 12:11:54 +00:00
/ * *
* Shows or hides the build - mode indicator based on whether build mode is active .
* @param active - Whether build mode is currently active
* @param building - The selected building type
* /
2026-03-20 08:11:31 +00:00
private onBuildModeChanged ( active : boolean , building : BuildingType ) : void {
2026-03-20 11:57:09 +00:00
this . inBuildMode = active
2026-03-20 08:11:31 +00:00
this . buildModeText . setText ( active ? ` 🏗 BUILD: ${ building . toUpperCase ( ) } [RMB/ESC cancel] ` : '' ) . setVisible ( active )
}
// ─── Farm tool indicator ──────────────────────────────────────────────────
2026-03-21 12:11:54 +00:00
/** Creates the farm-tool indicator text below the build-mode indicator (initially hidden). */
2026-03-20 08:11:31 +00:00
private createFarmToolIndicator ( ) : void {
this . farmToolText = this . add . text ( 10 , 44 , '' , { fontSize : '13px' , color : '#aaffaa' , fontFamily : 'monospace' , backgroundColor : '#00000099' , padding : { x : 8 , y : 4 } } ) . setScrollFactor ( 0 ) . setDepth ( 100 ) . setVisible ( false )
}
2026-03-21 12:11:54 +00:00
/ * *
* Shows or hides the farm - tool indicator and updates the active tool label .
* @param tool - Currently selected farm tool
* @param label - Human - readable label for the tool
* /
2026-03-20 08:11:31 +00:00
private onFarmToolChanged ( tool : FarmingTool , label : string ) : void {
2026-03-20 11:57:09 +00:00
this . inFarmMode = tool !== 'none'
2026-03-20 08:11:31 +00:00
this . farmToolText . setText ( tool === 'none' ? '' : ` [F] Farm: ${ label } [RMB cancel] ` ) . setVisible ( tool !== 'none' )
}
// ─── Coords + controls ────────────────────────────────────────────────────
2026-03-21 12:11:54 +00:00
/** Creates the tile-coordinate display and controls hint at the bottom-left. */
2026-03-20 08:11:31 +00:00
private createCoordsDisplay ( ) : void {
this . coordsText = this . add . text ( 10 , this . scale . height - 24 , '' , { fontSize : '11px' , color : '#666666' , fontFamily : 'monospace' } ) . setScrollFactor ( 0 ) . setDepth ( 100 )
2026-03-21 12:11:54 +00:00
this . controlsHintText = this . add . text ( 10 , this . scale . height - 42 , '[WASD] Pan [Scroll] Zoom [F] Farm [B] Build [V] Nisse [F3] Debug' , {
2026-03-20 08:11:31 +00:00
fontSize : '10px' , color : '#444444' , fontFamily : 'monospace' , backgroundColor : '#00000066' , padding : { x : 4 , y : 2 }
} ) . setScrollFactor ( 0 ) . setDepth ( 100 )
}
2026-03-21 12:11:54 +00:00
/ * *
* Updates the tile - coordinate display when the camera moves .
* @param pos - Tile position of the camera center
* /
2026-03-20 08:11:31 +00:00
private onCameraMoved ( pos : { tileX : number ; tileY : number } ) : void {
this . coordsText . setText ( ` Tile: ${ pos . tileX } , ${ pos . tileY } ` )
}
2026-03-21 12:11:54 +00:00
// ─── Debug Panel (F3) ─────────────────────────────────────────────────────
/** Creates the debug panel text object (initially hidden). */
private createDebugPanel ( ) : void {
this . debugPanelText = this . add . text ( 10 , 80 , '' , {
fontSize : '12px' ,
color : '#cccccc' ,
backgroundColor : '#000000cc' ,
padding : { x : 8 , y : 6 } ,
lineSpacing : 2 ,
fontFamily : 'monospace' ,
} ) . setScrollFactor ( 0 ) . setDepth ( 150 ) . setVisible ( false )
}
/** Toggles the debug panel and notifies GameScene to toggle the pathfinding overlay. */
private toggleDebugPanel ( ) : void {
this . debugActive = ! this . debugActive
this . debugPanelText . setVisible ( this . debugActive )
this . scene . get ( 'Game' ) . events . emit ( 'debugToggle' )
}
/ * *
* Reads current debug data from DebugSystem and updates the panel text .
* Called every frame while debug mode is active .
* /
private updateDebugPanel ( ) : void {
const gameScene = this . scene . get ( 'Game' ) as any
const debugSystem = gameScene . debugSystem
if ( ! debugSystem ? . isActive ( ) ) return
const ptr = this . input . activePointer
const data = debugSystem . getDebugData ( ptr ) as DebugData
const resLine = data . resourcesOnTile . length > 0
? data . resourcesOnTile . map ( r = > ` ${ r . kind } (hp: ${ r . hp } ) ` ) . join ( ', ' )
: '—'
const bldLine = data . buildingsOnTile . length > 0 ? data . buildingsOnTile . join ( ', ' ) : '—'
const cropLine = data . cropsOnTile . length > 0
? data . cropsOnTile . map ( c = > ` ${ c . kind } ( ${ c . stage } / ${ c . maxStage } ) ` ) . join ( ', ' )
: '—'
const { idle , walking , working , sleeping } = data . nisseByState
const { chop , mine , farm } = data . jobsByType
this . debugPanelText . setText ( [
'── F3 DEBUG ──────────────────' ,
` FPS: ${ data . fps } ` ,
'' ,
` Mouse world: ${ data . mouseWorld . x . toFixed ( 1 ) } , ${ data . mouseWorld . y . toFixed ( 1 ) } ` ,
` Mouse tile: ${ data . mouseTile . tileX } , ${ data . mouseTile . tileY } ` ,
` Tile type: ${ data . tileType } ` ,
` Resources: ${ resLine } ` ,
` Buildings: ${ bldLine } ` ,
` Crops: ${ cropLine } ` ,
'' ,
` Nisse: ${ data . nisseTotal } total ` ,
` idle: ${ idle } walking: ${ walking } working: ${ working } sleeping: ${ sleeping } ` ,
'' ,
` Jobs active: ` ,
` chop: ${ chop } mine: ${ mine } farm: ${ farm } ` ,
'' ,
` Paths: ${ data . activePaths } (cyan lines in world) ` ,
'' ,
'[F3] close' ,
] )
}
2026-03-20 11:57:09 +00:00
// ─── Context Menu ─────────────────────────────────────────────────────────
/ * *
* Shows the right - click context menu at the given screen coordinates .
* Any previously open context menu is closed first .
* @param x - Screen x position of the pointer
* @param y - Screen y position of the pointer
* /
private showContextMenu ( x : number , y : number ) : void {
this . hideContextMenu ( )
const menuW = 150
const btnH = 32
const menuH = 8 + 2 * ( btnH + 6 ) - 6 + 8
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 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 300 )
this . contextMenuGroup . add ( bg )
const entries : { label : string ; action : ( ) = > void } [ ] = [
{
label : '🏗 Build' ,
action : ( ) = > { this . hideContextMenu ( ) ; this . scene . get ( 'Game' ) . events . emit ( 'uiRequestBuildMenu' ) } ,
} ,
{
2026-03-20 17:07:34 +00:00
label : '👥 Nisse' ,
2026-03-20 11:57:09 +00:00
action : ( ) = > { this . hideContextMenu ( ) ; this . toggleVillagerPanel ( ) } ,
} ,
]
entries . forEach ( ( entry , i ) = > {
const by = my + 8 + i * ( btnH + 6 )
const btn = this . add . rectangle ( mx + 8 , by , menuW - 16 , btnH , 0x1a3a1a , 0.9 )
. setOrigin ( 0 , 0 ) . setScrollFactor ( 0 ) . setDepth ( 301 ) . setInteractive ( )
btn . on ( 'pointerover' , ( ) = > btn . setFillStyle ( 0x2d6a4f , 0.9 ) )
btn . on ( 'pointerout' , ( ) = > btn . setFillStyle ( 0x1a3a1a , 0.9 ) )
btn . on ( 'pointerdown' , entry . action )
this . contextMenuGroup . add ( btn )
this . contextMenuGroup . add (
this . add . text ( mx + 16 , by + btnH / 2 , entry . label , {
fontSize : '13px' , color : '#ffffff' , fontFamily : 'monospace' ,
} ) . setOrigin ( 0 , 0.5 ) . setScrollFactor ( 0 ) . setDepth ( 302 )
)
} )
this . contextMenuVisible = true
this . scene . get ( 'Game' ) . events . emit ( 'uiMenuOpen' )
}
/ * *
* Closes and destroys the context menu if it is currently visible .
* /
private hideContextMenu ( ) : void {
if ( ! this . contextMenuVisible ) return
this . contextMenuGroup . destroy ( true )
this . contextMenuGroup = this . add . group ( )
this . contextMenuVisible = false
this . scene . get ( 'Game' ) . events . emit ( 'uiMenuClose' )
}
2026-03-20 08:11:31 +00:00
// ─── Resize ───────────────────────────────────────────────────────────────
2026-03-20 12:19:57 +00:00
/ * *
* Repositions all fixed UI elements after a canvas resize .
* Open overlay panels are closed so they reopen correctly centered .
* /
2026-03-20 08:11:31 +00:00
private repositionUI ( ) : void {
const { width , height } = this . scale
2026-03-20 12:19:57 +00:00
// Stockpile panel — anchored to top-right; move all elements by the delta
const newPanelX = width - 178
const deltaX = newPanelX - this . stockpilePanel . x
if ( deltaX !== 0 ) {
this . stockpilePanel . setX ( newPanelX )
this . stockpileTitleText . setX ( this . stockpileTitleText . x + deltaX )
this . stockpileTexts . forEach ( t = > t . setX ( t . x + deltaX ) )
this . popText . setX ( this . popText . x + deltaX )
}
// Bottom elements
this . hintText . setPosition ( width / 2 , height - 40 )
this . toastText . setPosition ( width / 2 , 60 )
2026-03-20 08:11:31 +00:00
this . coordsText . setPosition ( 10 , height - 24 )
2026-03-20 12:19:57 +00:00
this . controlsHintText . setPosition ( 10 , height - 42 )
// 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 ( )
if ( this . villagerPanelVisible ) this . closeVillagerPanel ( )
if ( this . contextMenuVisible ) this . hideContextMenu ( )
2026-03-20 08:11:31 +00:00
}
}