✨ add Nisse info panel with work log (Issue #9)
Clicking a Nisse opens a top-left panel showing name, AI status, energy bar, active job, job priority buttons, and a live work log (last 10 of 20 runtime-only entries). Closes via ESC, ✕, or clicking another Nisse. Dynamic parts (status/energy/job/log) refresh each frame without rebuilding the full group. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,7 +61,8 @@ export class GameScene extends Phaser.Scene {
|
||||
this.farmingSystem.onToolChange = (tool, label) => this.events.emit('farmToolChanged', tool, label)
|
||||
|
||||
this.villagerSystem.create()
|
||||
this.villagerSystem.onMessage = (msg) => this.events.emit('toast', msg)
|
||||
this.villagerSystem.onMessage = (msg) => this.events.emit('toast', msg)
|
||||
this.villagerSystem.onNisseClick = (id) => this.events.emit('nisseClicked', id)
|
||||
|
||||
this.debugSystem.create()
|
||||
|
||||
|
||||
@@ -35,6 +35,16 @@ export class UIScene extends Phaser.Scene {
|
||||
private escMenuVisible = false
|
||||
private confirmGroup!: Phaser.GameObjects.Group
|
||||
private confirmVisible = false
|
||||
private nisseInfoGroup!: Phaser.GameObjects.Group
|
||||
private nisseInfoVisible = false
|
||||
private nisseInfoId: string | null = null
|
||||
private nisseInfoDynamic: {
|
||||
statusText: Phaser.GameObjects.Text
|
||||
energyBar: Phaser.GameObjects.Graphics
|
||||
energyPct: Phaser.GameObjects.Text
|
||||
jobText: Phaser.GameObjects.Text
|
||||
logTexts: Phaser.GameObjects.Text[]
|
||||
} | null = null
|
||||
|
||||
constructor() { super({ key: 'UI' }) }
|
||||
|
||||
@@ -68,10 +78,13 @@ export class UIScene extends Phaser.Scene {
|
||||
|
||||
this.scale.on('resize', () => this.repositionUI())
|
||||
|
||||
gameScene.events.on('nisseClicked', (id: string) => this.openNisseInfoPanel(id))
|
||||
|
||||
this.input.mouse!.disableContextMenu()
|
||||
this.contextMenuGroup = this.add.group()
|
||||
this.escMenuGroup = this.add.group()
|
||||
this.confirmGroup = this.add.group()
|
||||
this.nisseInfoGroup = this.add.group()
|
||||
|
||||
this.input.on('pointerdown', (ptr: Phaser.Input.Pointer) => {
|
||||
if (ptr.rightButtonDown()) {
|
||||
@@ -98,6 +111,7 @@ export class UIScene extends Phaser.Scene {
|
||||
this.updateToast(delta)
|
||||
this.updatePopText()
|
||||
if (this.debugActive) this.updateDebugPanel()
|
||||
if (this.nisseInfoVisible) this.refreshNisseInfoPanel()
|
||||
}
|
||||
|
||||
// ─── Stockpile ────────────────────────────────────────────────────────────
|
||||
@@ -502,11 +516,12 @@ export class UIScene extends Phaser.Scene {
|
||||
* esc menu → build/farm mode (handled by their own systems) → open ESC menu.
|
||||
*/
|
||||
private handleEsc(): void {
|
||||
if (this.confirmVisible) { this.hideConfirm(); return }
|
||||
if (this.contextMenuVisible) { this.hideContextMenu(); return }
|
||||
if (this.buildMenuVisible) { this.closeBuildMenu(); return }
|
||||
if (this.villagerPanelVisible){ this.closeVillagerPanel(); return }
|
||||
if (this.escMenuVisible) { this.closeEscMenu(); return }
|
||||
if (this.confirmVisible) { this.hideConfirm(); return }
|
||||
if (this.contextMenuVisible) { this.hideContextMenu(); return }
|
||||
if (this.buildMenuVisible) { this.closeBuildMenu(); return }
|
||||
if (this.villagerPanelVisible){ this.closeVillagerPanel(); return }
|
||||
if (this.nisseInfoVisible) { this.closeNisseInfoPanel(); 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.
|
||||
if (this.inBuildMode || this.inFarmMode) return
|
||||
@@ -667,6 +682,176 @@ export class UIScene extends Phaser.Scene {
|
||||
this.scene.get('Game').events.emit('uiMenuClose')
|
||||
}
|
||||
|
||||
// ─── Nisse Info Panel ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Opens (or switches to) the Nisse info panel for the given Nisse ID.
|
||||
* If another Nisse's panel is already open, it is replaced.
|
||||
* @param villagerId - ID of the Nisse to display
|
||||
*/
|
||||
private openNisseInfoPanel(villagerId: string): void {
|
||||
this.nisseInfoId = villagerId
|
||||
this.nisseInfoVisible = true
|
||||
this.scene.get('Game').events.emit('uiMenuOpen')
|
||||
this.buildNisseInfoPanel()
|
||||
}
|
||||
|
||||
/** Closes and destroys the Nisse info panel. */
|
||||
private closeNisseInfoPanel(): void {
|
||||
if (!this.nisseInfoVisible) return
|
||||
this.nisseInfoVisible = false
|
||||
this.nisseInfoId = null
|
||||
this.nisseInfoGroup.destroy(true)
|
||||
this.nisseInfoGroup = this.add.group()
|
||||
this.scene.get('Game').events.emit('uiMenuClose')
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the static skeleton of the Nisse info panel (background, name, close
|
||||
* button, labels, priority buttons) and stores references to the dynamic parts
|
||||
* (status text, energy bar, job text, work log texts).
|
||||
*/
|
||||
private buildNisseInfoPanel(): void {
|
||||
this.nisseInfoGroup.destroy(true)
|
||||
this.nisseInfoGroup = this.add.group()
|
||||
this.nisseInfoDynamic = null
|
||||
|
||||
const id = this.nisseInfoId
|
||||
if (!id) return
|
||||
|
||||
const state = stateManager.getState()
|
||||
const v = state.world.villagers[id]
|
||||
if (!v) { this.closeNisseInfoPanel(); return }
|
||||
|
||||
const LOG_ROWS = 10
|
||||
const panelW = 280
|
||||
const panelH = 120 + LOG_ROWS * 14 + 16
|
||||
const px = 10, py = 10
|
||||
|
||||
// Background
|
||||
this.nisseInfoGroup.add(
|
||||
this.add.rectangle(px, py, panelW, panelH, 0x050510, 0.93)
|
||||
.setOrigin(0, 0).setScrollFactor(0).setDepth(250)
|
||||
)
|
||||
|
||||
// Name
|
||||
this.nisseInfoGroup.add(
|
||||
this.add.text(px + 10, py + 10, v.name, {
|
||||
fontSize: '14px', color: '#ffffff', fontFamily: 'monospace',
|
||||
}).setScrollFactor(0).setDepth(251)
|
||||
)
|
||||
|
||||
// Close button
|
||||
const closeBtn = this.add.text(px + panelW - 12, py + 10, '✕', {
|
||||
fontSize: '13px', color: '#888888', fontFamily: 'monospace',
|
||||
}).setOrigin(1, 0).setScrollFactor(0).setDepth(251).setInteractive()
|
||||
closeBtn.on('pointerover', () => closeBtn.setStyle({ color: '#ffffff' }))
|
||||
closeBtn.on('pointerout', () => closeBtn.setStyle({ color: '#888888' }))
|
||||
closeBtn.on('pointerdown', () => this.closeNisseInfoPanel())
|
||||
this.nisseInfoGroup.add(closeBtn)
|
||||
|
||||
// Dynamic: status text
|
||||
const statusTxt = this.add.text(px + 10, py + 28, '', {
|
||||
fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace',
|
||||
}).setScrollFactor(0).setDepth(251)
|
||||
this.nisseInfoGroup.add(statusTxt)
|
||||
|
||||
// Dynamic: energy bar + pct
|
||||
const energyBar = this.add.graphics().setScrollFactor(0).setDepth(251)
|
||||
this.nisseInfoGroup.add(energyBar)
|
||||
const energyPct = this.add.text(px + 136, py + 46, '', {
|
||||
fontSize: '10px', color: '#888888', fontFamily: 'monospace',
|
||||
}).setScrollFactor(0).setDepth(251)
|
||||
this.nisseInfoGroup.add(energyPct)
|
||||
|
||||
// Dynamic: job text
|
||||
const jobTxt = this.add.text(px + 10, py + 60, '', {
|
||||
fontSize: '11px', color: '#cccccc', fontFamily: 'monospace',
|
||||
}).setScrollFactor(0).setDepth(251)
|
||||
this.nisseInfoGroup.add(jobTxt)
|
||||
|
||||
// Static: priority label + buttons
|
||||
const jobKeys: Array<{ key: string; icon: string }> = [
|
||||
{ key: 'chop', icon: '🪓' }, { key: 'mine', icon: '⛏' }, { key: 'farm', icon: '🌾' },
|
||||
]
|
||||
jobKeys.forEach((j, i) => {
|
||||
const pri = v.priorities[j.key as keyof typeof v.priorities]
|
||||
const label = pri === 0 ? `${j.icon} OFF` : `${j.icon} P${pri}`
|
||||
const bx = px + 10 + i * 88
|
||||
const btn = this.add.text(bx, py + 78, label, {
|
||||
fontSize: '11px', color: pri === 0 ? '#555555' : '#ffffff',
|
||||
fontFamily: 'monospace', backgroundColor: pri === 0 ? '#1a1a1a' : '#1a3a1a',
|
||||
padding: { x: 5, y: 3 },
|
||||
}).setScrollFactor(0).setDepth(252).setInteractive()
|
||||
btn.on('pointerover', () => btn.setStyle({ backgroundColor: '#2d6a4f' }))
|
||||
btn.on('pointerout', () => btn.setStyle({ backgroundColor: pri === 0 ? '#1a1a1a' : '#1a3a1a' }))
|
||||
btn.on('pointerdown', () => {
|
||||
const newPri = (v.priorities[j.key as keyof typeof v.priorities] + 1) % 5
|
||||
const newPriorities = { ...v.priorities, [j.key]: newPri }
|
||||
this.scene.get('Game').events.emit('updatePriorities', id, newPriorities)
|
||||
// Rebuild panel so priority buttons reflect the new values immediately
|
||||
this.buildNisseInfoPanel()
|
||||
})
|
||||
this.nisseInfoGroup.add(btn)
|
||||
})
|
||||
|
||||
// Static: work log header
|
||||
this.nisseInfoGroup.add(
|
||||
this.add.text(px + 10, py + 98, '── Work Log ──────────────────', {
|
||||
fontSize: '10px', color: '#555555', fontFamily: 'monospace',
|
||||
}).setScrollFactor(0).setDepth(251)
|
||||
)
|
||||
|
||||
// Dynamic: log text rows (pre-allocated)
|
||||
const logTexts: Phaser.GameObjects.Text[] = []
|
||||
for (let i = 0; i < LOG_ROWS; i++) {
|
||||
const t = this.add.text(px + 10, py + 112 + i * 14, '', {
|
||||
fontSize: '10px', color: '#888888', fontFamily: 'monospace',
|
||||
}).setScrollFactor(0).setDepth(251)
|
||||
this.nisseInfoGroup.add(t)
|
||||
logTexts.push(t)
|
||||
}
|
||||
|
||||
this.nisseInfoDynamic = { statusText: statusTxt, energyBar, energyPct, jobText: jobTxt, logTexts }
|
||||
this.refreshNisseInfoPanel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates only the dynamic parts of the Nisse info panel (status, energy,
|
||||
* job, work log) without destroying and recreating the full group.
|
||||
* Called every frame while the panel is visible.
|
||||
*/
|
||||
private refreshNisseInfoPanel(): void {
|
||||
const dyn = this.nisseInfoDynamic
|
||||
if (!dyn || !this.nisseInfoId) return
|
||||
|
||||
const state = stateManager.getState()
|
||||
const v = state.world.villagers[this.nisseInfoId]
|
||||
if (!v) { this.closeNisseInfoPanel(); return }
|
||||
|
||||
const gameScene = this.scene.get('Game') as any
|
||||
const workLog = (gameScene.villagerSystem?.getWorkLog(this.nisseInfoId) ?? []) as string[]
|
||||
const statusStr = (gameScene.villagerSystem?.getStatusText(this.nisseInfoId) ?? '—') as string
|
||||
|
||||
dyn.statusText.setText(statusStr)
|
||||
|
||||
// Energy bar
|
||||
const px = 10, py = 10
|
||||
dyn.energyBar.clear()
|
||||
dyn.energyBar.fillStyle(0x333333); dyn.energyBar.fillRect(px + 10, py + 46, 120, 7)
|
||||
const col = v.energy > 60 ? 0x4CAF50 : v.energy > 30 ? 0xFF9800 : 0xF44336
|
||||
dyn.energyBar.fillStyle(col); dyn.energyBar.fillRect(px + 10, py + 46, 120 * (v.energy / 100), 7)
|
||||
dyn.energyPct.setText(`${Math.round(v.energy)}%`)
|
||||
|
||||
// Job
|
||||
dyn.jobText.setText(`Job: ${v.job ? `${v.job.type} → (${v.job.tileX}, ${v.job.tileY})` : '—'}`)
|
||||
|
||||
// Work log rows
|
||||
dyn.logTexts.forEach((t, i) => {
|
||||
t.setText(workLog[i] ?? '')
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Resize ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -699,5 +884,6 @@ export class UIScene extends Phaser.Scene {
|
||||
if (this.contextMenuVisible) this.hideContextMenu()
|
||||
if (this.escMenuVisible) this.closeEscMenu()
|
||||
if (this.confirmVisible) this.hideConfirm()
|
||||
if (this.nisseInfoVisible) this.closeNisseInfoPanel()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user