✨ add F3 debug view (Issue #6)
F3 toggles a debug overlay with: - FPS - Mouse world/tile coordinates - Tile type under cursor - Resources, buildings, crops on hovered tile - Nisse count broken down by AI state (idle/walking/working/sleeping) - Active jobs by type (chop/mine/farm) - Pathfinding visualization: cyan lines + destination highlight drawn in world space via DebugSystem Added DebugSystem to GameScene. VillagerSystem exposes getActivePaths() for the path visualization. JSDoc added to all previously undocumented methods in VillagerSystem, WorldSystem, GameScene, and UIScene.
This commit is contained in:
@@ -9,6 +9,7 @@ import { ResourceSystem } from '../systems/ResourceSystem'
|
||||
import { BuildingSystem } from '../systems/BuildingSystem'
|
||||
import { FarmingSystem } from '../systems/FarmingSystem'
|
||||
import { VillagerSystem } from '../systems/VillagerSystem'
|
||||
import { DebugSystem } from '../systems/DebugSystem'
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
private adapter!: LocalAdapter
|
||||
@@ -18,11 +19,16 @@ export class GameScene extends Phaser.Scene {
|
||||
private buildingSystem!: BuildingSystem
|
||||
private farmingSystem!: FarmingSystem
|
||||
villagerSystem!: VillagerSystem
|
||||
debugSystem!: DebugSystem
|
||||
private autosaveTimer = 0
|
||||
private menuOpen = false
|
||||
|
||||
constructor() { super({ key: 'Game' }) }
|
||||
|
||||
/**
|
||||
* Initialises all game systems, wires up inter-system events,
|
||||
* launches the UI scene overlay, and starts the autosave timer.
|
||||
*/
|
||||
create(): void {
|
||||
this.adapter = new LocalAdapter()
|
||||
|
||||
@@ -33,6 +39,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.farmingSystem = new FarmingSystem(this, this.adapter)
|
||||
this.villagerSystem = new VillagerSystem(this, this.adapter, this.worldSystem)
|
||||
this.villagerSystem.init(this.resourceSystem, this.farmingSystem)
|
||||
this.debugSystem = new DebugSystem(this, this.villagerSystem, this.worldSystem)
|
||||
|
||||
this.worldSystem.create()
|
||||
this.renderPersistentObjects()
|
||||
@@ -56,6 +63,8 @@ export class GameScene extends Phaser.Scene {
|
||||
this.villagerSystem.create()
|
||||
this.villagerSystem.onMessage = (msg) => this.events.emit('toast', msg)
|
||||
|
||||
this.debugSystem.create()
|
||||
|
||||
// Sync tile changes and building visuals through adapter
|
||||
this.adapter.onAction = (action) => {
|
||||
if (action.type === 'CHANGE_TILE') {
|
||||
@@ -74,10 +83,17 @@ export class GameScene extends Phaser.Scene {
|
||||
this.events.on('updatePriorities', (villagerId: string, priorities: { chop: number; mine: number; farm: number }) => {
|
||||
this.adapter.send({ type: 'UPDATE_PRIORITIES', villagerId, priorities })
|
||||
})
|
||||
this.events.on('debugToggle', () => this.debugSystem.toggle())
|
||||
|
||||
this.autosaveTimer = AUTOSAVE_INTERVAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Main game loop: updates all systems and emits the cameraMoved event for the UI.
|
||||
* Skips system updates while a menu is open.
|
||||
* @param _time - Total elapsed time (unused)
|
||||
* @param delta - Frame delta in milliseconds
|
||||
*/
|
||||
update(_time: number, delta: number): void {
|
||||
if (this.menuOpen) return
|
||||
|
||||
@@ -86,6 +102,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.resourceSystem.update(delta)
|
||||
this.farmingSystem.update(delta)
|
||||
this.villagerSystem.update(delta)
|
||||
this.debugSystem.update()
|
||||
|
||||
this.events.emit('cameraMoved', this.cameraSystem.getCenterTile())
|
||||
this.buildingSystem.update()
|
||||
@@ -119,6 +136,7 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/** Saves game state and destroys all systems cleanly on scene shutdown. */
|
||||
shutdown(): void {
|
||||
stateManager.save()
|
||||
this.worldSystem.destroy()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Phaser from 'phaser'
|
||||
import type { BuildingType, JobPriorities } from '../types'
|
||||
import type { FarmingTool } from '../systems/FarmingSystem'
|
||||
import type { DebugData } from '../systems/DebugSystem'
|
||||
import { stateManager } from '../StateManager'
|
||||
|
||||
const ITEM_ICONS: Record<string, string> = {
|
||||
@@ -28,9 +29,15 @@ export class UIScene extends Phaser.Scene {
|
||||
private contextMenuVisible = false
|
||||
private inBuildMode = false
|
||||
private inFarmMode = false
|
||||
private debugPanelText!: Phaser.GameObjects.Text
|
||||
private debugActive = false
|
||||
|
||||
constructor() { super({ key: 'UI' }) }
|
||||
|
||||
/**
|
||||
* Creates all HUD elements, wires up game scene events, and registers
|
||||
* keyboard shortcuts (B, V, F3, ESC).
|
||||
*/
|
||||
create(): void {
|
||||
this.createStockpilePanel()
|
||||
this.createHintText()
|
||||
@@ -39,6 +46,7 @@ export class UIScene extends Phaser.Scene {
|
||||
this.createBuildModeIndicator()
|
||||
this.createFarmToolIndicator()
|
||||
this.createCoordsDisplay()
|
||||
this.createDebugPanel()
|
||||
|
||||
const gameScene = this.scene.get('Game')
|
||||
gameScene.events.on('buildModeChanged', (a: boolean, b: BuildingType) => this.onBuildModeChanged(a, b))
|
||||
@@ -51,6 +59,8 @@ export class UIScene extends Phaser.Scene {
|
||||
.on('down', () => gameScene.events.emit('uiRequestBuildMenu'))
|
||||
this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.V)
|
||||
.on('down', () => this.toggleVillagerPanel())
|
||||
this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.F3)
|
||||
.on('down', () => this.toggleDebugPanel())
|
||||
|
||||
this.scale.on('resize', () => this.repositionUI())
|
||||
|
||||
@@ -71,14 +81,22 @@ export class UIScene extends Phaser.Scene {
|
||||
.on('down', () => this.hideContextMenu())
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
update(_t: number, delta: number): void {
|
||||
this.updateStockpile()
|
||||
this.updateToast(delta)
|
||||
this.updatePopText()
|
||||
if (this.debugActive) this.updateDebugPanel()
|
||||
}
|
||||
|
||||
// ─── Stockpile ────────────────────────────────────────────────────────────
|
||||
|
||||
/** 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, 165, 0x000000, 0.72).setOrigin(0, 0).setScrollFactor(0).setDepth(100)
|
||||
@@ -91,6 +109,7 @@ export class UIScene extends Phaser.Scene {
|
||||
this.popText = this.add.text(x + 10, y + 145, '👥 Nisse: 0 / 0', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101)
|
||||
}
|
||||
|
||||
/** Refreshes all item quantities and colors in the stockpile panel. */
|
||||
private updateStockpile(): void {
|
||||
const sp = stateManager.getState().world.stockpile
|
||||
for (const [item, t] of this.stockpileTexts) {
|
||||
@@ -100,6 +119,7 @@ export class UIScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the Nisse population / bed capacity counter. */
|
||||
private updatePopText(): void {
|
||||
const state = stateManager.getState()
|
||||
const beds = Object.values(state.world.buildings).filter(b => b.kind === 'bed').length
|
||||
@@ -109,6 +129,7 @@ export class UIScene extends Phaser.Scene {
|
||||
|
||||
// ─── Hint ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 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, '', {
|
||||
fontSize: '14px', color: '#ffff88', fontFamily: 'monospace',
|
||||
@@ -118,6 +139,7 @@ export class UIScene extends Phaser.Scene {
|
||||
|
||||
// ─── Toast ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Creates the toast notification text element (top center, initially hidden). */
|
||||
private createToast(): void {
|
||||
this.toastText = this.add.text(this.scale.width / 2, 60, '', {
|
||||
fontSize: '15px', color: '#88ff88', fontFamily: 'monospace',
|
||||
@@ -125,8 +147,16 @@ export class UIScene extends Phaser.Scene {
|
||||
}).setOrigin(0.5, 0).setScrollFactor(0).setDepth(102).setAlpha(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a toast message for 2.2 seconds then fades it out.
|
||||
* @param msg - Message to display
|
||||
*/
|
||||
showToast(msg: string): void { this.toastText.setText(msg).setAlpha(1); this.toastTimer = 2200 }
|
||||
|
||||
/**
|
||||
* Counts down the toast timer and triggers the fade-out tween when it expires.
|
||||
* @param delta - Frame delta in milliseconds
|
||||
*/
|
||||
private updateToast(delta: number): void {
|
||||
if (this.toastTimer <= 0) return
|
||||
this.toastTimer -= delta
|
||||
@@ -135,6 +165,7 @@ export class UIScene extends Phaser.Scene {
|
||||
|
||||
// ─── Build Menu ───────────────────────────────────────────────────────────
|
||||
|
||||
/** Creates and hides the build menu with buttons for each available building type. */
|
||||
private createBuildMenu(): void {
|
||||
this.buildMenuGroup = this.add.group()
|
||||
const buildings: { kind: BuildingType; label: string; cost: string }[] = [
|
||||
@@ -162,12 +193,18 @@ export class UIScene extends Phaser.Scene {
|
||||
this.buildMenuGroup.setVisible(false)
|
||||
}
|
||||
|
||||
/** Toggles the build menu open or closed. */
|
||||
private toggleBuildMenu(): void { this.buildMenuVisible ? this.closeBuildMenu() : this.openBuildMenu() }
|
||||
|
||||
/** Opens the build menu and notifies GameScene that a menu is active. */
|
||||
private openBuildMenu(): void { this.buildMenuVisible = true; this.buildMenuGroup.setVisible(true); this.scene.get('Game').events.emit('uiMenuOpen') }
|
||||
|
||||
/** Closes the build menu and notifies GameScene that no menu is active. */
|
||||
private closeBuildMenu(): void { this.buildMenuVisible = false; this.buildMenuGroup.setVisible(false); this.scene.get('Game').events.emit('uiMenuClose') }
|
||||
|
||||
// ─── Villager Panel (V key) ───────────────────────────────────────────────
|
||||
|
||||
/** Toggles the Nisse management panel open or closed. */
|
||||
private toggleVillagerPanel(): void {
|
||||
if (this.villagerPanelVisible) {
|
||||
this.closeVillagerPanel()
|
||||
@@ -176,18 +213,24 @@ export class UIScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the Nisse panel, builds its contents, and notifies GameScene. */
|
||||
private openVillagerPanel(): void {
|
||||
this.villagerPanelVisible = true
|
||||
this.buildVillagerPanel()
|
||||
this.scene.get('Game').events.emit('uiMenuOpen')
|
||||
}
|
||||
|
||||
/** Closes and destroys the Nisse panel and notifies GameScene. */
|
||||
private closeVillagerPanel(): void {
|
||||
this.villagerPanelVisible = false
|
||||
this.villagerPanelGroup?.destroy(true)
|
||||
this.scene.get('Game').events.emit('uiMenuClose')
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys and rebuilds the Nisse panel from current state.
|
||||
* Shows name, status, energy bar, and job priority buttons per Nisse.
|
||||
*/
|
||||
private buildVillagerPanel(): void {
|
||||
if (this.villagerPanelGroup) this.villagerPanelGroup.destroy(true)
|
||||
this.villagerPanelGroup = this.add.group()
|
||||
@@ -266,9 +309,16 @@ export class UIScene extends Phaser.Scene {
|
||||
|
||||
// ─── Build mode indicator ─────────────────────────────────────────────────
|
||||
|
||||
/** Creates the build-mode indicator text in the top-left corner (initially hidden). */
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private onBuildModeChanged(active: boolean, building: BuildingType): void {
|
||||
this.inBuildMode = active
|
||||
this.buildModeText.setText(active ? `🏗 BUILD: ${building.toUpperCase()} [RMB/ESC cancel]` : '').setVisible(active)
|
||||
@@ -276,9 +326,16 @@ export class UIScene extends Phaser.Scene {
|
||||
|
||||
// ─── Farm tool indicator ──────────────────────────────────────────────────
|
||||
|
||||
/** Creates the farm-tool indicator text below the build-mode indicator (initially hidden). */
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private onFarmToolChanged(tool: FarmingTool, label: string): void {
|
||||
this.inFarmMode = tool !== 'none'
|
||||
this.farmToolText.setText(tool === 'none' ? '' : `[F] Farm: ${label} [RMB cancel]`).setVisible(tool !== 'none')
|
||||
@@ -286,16 +343,88 @@ export class UIScene extends Phaser.Scene {
|
||||
|
||||
// ─── 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', {
|
||||
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). */
|
||||
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',
|
||||
])
|
||||
}
|
||||
|
||||
// ─── Context Menu ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user