✨ 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:
@@ -11,6 +11,8 @@ import type { FarmingSystem } from './FarmingSystem'
|
||||
|
||||
const ARRIVAL_PX = 3
|
||||
|
||||
const WORK_LOG_MAX = 20
|
||||
|
||||
interface VillagerRuntime {
|
||||
sprite: Phaser.GameObjects.Image
|
||||
nameLabel: Phaser.GameObjects.Text
|
||||
@@ -20,6 +22,8 @@ interface VillagerRuntime {
|
||||
destination: 'job' | 'stockpile' | 'bed' | null
|
||||
workTimer: number
|
||||
idleScanTimer: number
|
||||
/** Runtime-only activity log; not persisted. Max WORK_LOG_MAX entries. */
|
||||
workLog: string[]
|
||||
}
|
||||
|
||||
export class VillagerSystem {
|
||||
@@ -35,6 +39,7 @@ export class VillagerSystem {
|
||||
private nameIndex = 0
|
||||
|
||||
onMessage?: (msg: string) => void
|
||||
onNisseClick?: (villagerId: string) => void
|
||||
|
||||
/**
|
||||
* @param scene - The Phaser scene this system belongs to
|
||||
@@ -139,13 +144,21 @@ export class VillagerSystem {
|
||||
// Carrying items? → find stockpile
|
||||
if (v.job?.carrying && Object.values(v.job.carrying).some(n => (n ?? 0) > 0)) {
|
||||
const sp = this.nearestBuilding(v, 'stockpile_zone')
|
||||
if (sp) { this.beginWalk(v, rt, sp.tileX, sp.tileY, 'stockpile'); return }
|
||||
if (sp) {
|
||||
this.addLog(v.id, '→ Hauling to stockpile')
|
||||
this.beginWalk(v, rt, sp.tileX, sp.tileY, 'stockpile')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Low energy → find bed
|
||||
if (v.energy < 25) {
|
||||
const bed = this.findBed(v)
|
||||
if (bed) { this.beginWalk(v, rt, bed.tileX, bed.tileY, 'bed'); return }
|
||||
if (bed) {
|
||||
this.addLog(v.id, '→ Going to sleep (low energy)')
|
||||
this.beginWalk(v, rt, bed.tileX, bed.tileY, 'bed')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Find a job
|
||||
@@ -156,6 +169,7 @@ export class VillagerSystem {
|
||||
type: 'VILLAGER_SET_JOB', villagerId: v.id,
|
||||
job: { type: job.type, targetId: job.targetId, tileX: job.tileX, tileY: job.tileY, carrying: {} },
|
||||
})
|
||||
this.addLog(v.id, `→ Walking to ${job.type} at (${job.tileX}, ${job.tileY})`)
|
||||
this.beginWalk(v, rt, job.tileX, job.tileY, 'job')
|
||||
} else {
|
||||
// No job available — wait before scanning again
|
||||
@@ -218,10 +232,12 @@ export class VillagerSystem {
|
||||
this.adapter.send({ type: 'VILLAGER_DEPOSIT', villagerId: v.id })
|
||||
this.adapter.send({ type: 'VILLAGER_SET_AI', villagerId: v.id, aiState: 'idle' })
|
||||
rt.idleScanTimer = 0 // scan for a new job immediately after deposit
|
||||
this.addLog(v.id, '✓ Deposited at stockpile')
|
||||
break
|
||||
|
||||
case 'bed':
|
||||
this.adapter.send({ type: 'VILLAGER_SET_AI', villagerId: v.id, aiState: 'sleeping' })
|
||||
this.addLog(v.id, '💤 Sleeping...')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -261,6 +277,7 @@ export class VillagerSystem {
|
||||
// Clear the FOREST tile so the area becomes passable for future pathfinding
|
||||
this.adapter.send({ type: 'CHANGE_TILE', tileX: res.tileX, tileY: res.tileY, tile: TileType.DARK_GRASS })
|
||||
this.resourceSystem.removeResource(job.targetId)
|
||||
this.addLog(v.id, '✓ Chopped tree (+2 wood)')
|
||||
}
|
||||
} else if (job.type === 'mine') {
|
||||
const res = state.world.resources[job.targetId]
|
||||
@@ -269,6 +286,7 @@ export class VillagerSystem {
|
||||
// Clear the ROCK tile so the area becomes passable for future pathfinding
|
||||
this.adapter.send({ type: 'CHANGE_TILE', tileX: res.tileX, tileY: res.tileY, tile: TileType.GRASS })
|
||||
this.resourceSystem.removeResource(job.targetId)
|
||||
this.addLog(v.id, '✓ Mined rock (+2 stone)')
|
||||
}
|
||||
} else if (job.type === 'farm') {
|
||||
const crop = state.world.crops[job.targetId]
|
||||
@@ -276,6 +294,7 @@ export class VillagerSystem {
|
||||
this.adapter.send({ type: 'VILLAGER_HARVEST_CROP', villagerId: v.id, cropId: job.targetId })
|
||||
this.farmingSystem.removeCropSpritePublic(job.targetId)
|
||||
this.adapter.send({ type: 'CHANGE_TILE', tileX: crop.tileX, tileY: crop.tileY, tile: 9 as any })
|
||||
this.addLog(v.id, `✓ Farmed ${crop.kind}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +323,7 @@ export class VillagerSystem {
|
||||
if (v.energy >= 100) {
|
||||
rt.sprite.setAngle(0)
|
||||
this.adapter.send({ type: 'VILLAGER_SET_AI', villagerId: v.id, aiState: 'idle' })
|
||||
this.addLog(v.id, '✓ Woke up (energy full)')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +487,10 @@ export class VillagerSystem {
|
||||
const energyBar = this.scene.add.graphics().setDepth(12)
|
||||
const jobIcon = this.scene.add.text(v.x, v.y - 18, '', { fontSize: '10px' }).setDepth(13)
|
||||
|
||||
this.runtime.set(v.id, { sprite, nameLabel, energyBar, jobIcon, path: [], destination: null, workTimer: 0, idleScanTimer: 0 })
|
||||
sprite.setInteractive()
|
||||
sprite.on('pointerdown', () => this.onNisseClick?.(v.id))
|
||||
|
||||
this.runtime.set(v.id, { sprite, nameLabel, energyBar, jobIcon, path: [], destination: null, workTimer: 0, idleScanTimer: 0, workLog: [] })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -486,6 +509,21 @@ export class VillagerSystem {
|
||||
g.fillStyle(col); g.fillRect(x - W/2, y - 28, W * (energy / 100), H)
|
||||
}
|
||||
|
||||
// ─── Work log ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Prepends a message to the runtime work log for the given Nisse.
|
||||
* Trims the log to WORK_LOG_MAX entries. No-ops if the Nisse is not found.
|
||||
* @param villagerId - Target Nisse ID
|
||||
* @param msg - Log message to prepend
|
||||
*/
|
||||
private addLog(villagerId: string, msg: string): void {
|
||||
const rt = this.runtime.get(villagerId)
|
||||
if (!rt) return
|
||||
rt.workLog.unshift(msg)
|
||||
if (rt.workLog.length > WORK_LOG_MAX) rt.workLog.length = WORK_LOG_MAX
|
||||
}
|
||||
|
||||
// ─── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -506,6 +544,15 @@ export class VillagerSystem {
|
||||
return '💭 Idle'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the runtime work log for the given Nisse (newest first).
|
||||
* @param villagerId - The Nisse's ID
|
||||
* @returns Array of log strings, or empty array if not found
|
||||
*/
|
||||
getWorkLog(villagerId: string): string[] {
|
||||
return [...(this.runtime.get(villagerId)?.workLog ?? [])]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current world position and remaining path for every Nisse
|
||||
* that is currently in the 'walking' state. Used by DebugSystem for
|
||||
|
||||
Reference in New Issue
Block a user