replace polling timers with sorted event queues + action log

Crops, tree seedlings, and tile recovery no longer iterate all entries
every frame. Each event stores an absolute gameTime timestamp (growsAt).
A sorted priority queue is drained each tick — only due items are touched.

WorldState now tracks gameTime (ms); stateManager.advanceTime(delta)
increments it each frame. Save version bumped 5→6 with migration.

Action log ring buffer (15 entries) added to LocalAdapter; shown in
the F3 debug panel under "Last Actions".

Closes #36
Closes #37
This commit is contained in:
2026-03-24 08:08:05 +00:00
parent d02ed33435
commit 3b021127a4
8 changed files with 279 additions and 81 deletions

View File

@@ -2,6 +2,7 @@ import Phaser from 'phaser'
import { TILE_SIZE } from '../config'
import { TileType } from '../types'
import { stateManager } from '../StateManager'
import type { LocalAdapter } from '../NetworkAdapter'
import type { VillagerSystem } from './VillagerSystem'
import type { WorldSystem } from './WorldSystem'
@@ -18,6 +19,8 @@ export interface DebugData {
nisseByState: { idle: number; walking: number; working: number; sleeping: number }
jobsByType: { chop: number; mine: number; farm: number }
activePaths: number
/** Recent actions dispatched through the adapter (newest last). */
actionLog: readonly string[]
}
/** Human-readable names for TileType enum values. */
@@ -39,6 +42,7 @@ export class DebugSystem {
private scene: Phaser.Scene
private villagerSystem: VillagerSystem
private worldSystem: WorldSystem
private adapter: LocalAdapter
private pathGraphics!: Phaser.GameObjects.Graphics
private active = false
@@ -46,11 +50,13 @@ export class DebugSystem {
* @param scene - The Phaser scene this system belongs to
* @param villagerSystem - Used to read active paths for visualization
* @param worldSystem - Used to read tile types under the mouse
* @param adapter - Used to read the recent action log
*/
constructor(scene: Phaser.Scene, villagerSystem: VillagerSystem, worldSystem: WorldSystem) {
constructor(scene: Phaser.Scene, villagerSystem: VillagerSystem, worldSystem: WorldSystem, adapter: LocalAdapter) {
this.scene = scene
this.villagerSystem = villagerSystem
this.worldSystem = worldSystem
this.adapter = adapter
}
/**
@@ -159,6 +165,7 @@ export class DebugSystem {
nisseByState,
jobsByType,
activePaths: this.villagerSystem.getActivePaths().length,
actionLog: this.adapter.getActionLog(),
}
}
}

View File

@@ -74,8 +74,8 @@ export class FarmingSystem {
this.setTool(TOOL_CYCLE[(idx + 1) % TOOL_CYCLE.length])
}
// Tick crop growth
const leveled = stateManager.tickCrops(delta)
// Drain crop growth queue (no delta — gameTime is advanced by GameScene)
const leveled = stateManager.tickCrops()
for (const id of leveled) this.refreshCropSprite(id)
}
@@ -151,11 +151,13 @@ export class FarmingSystem {
}
const cfg = CROP_CONFIGS[kind]
const now = stateManager.getGameTime()
const crop: CropState = {
id: `crop_${tileX}_${tileY}_${Date.now()}`,
tileX, tileY, kind,
stage: 0, maxStage: cfg.stages,
stageTimerMs: cfg.stageTimeMs,
growsAt: now + cfg.stageTimeMs,
growsAtWatered: now + cfg.stageTimeMs / 2,
watered: tile === TileType.WATERED_SOIL,
}
this.adapter.send({ type: 'PLANT_CROP', crop, seedItem })

View File

@@ -38,7 +38,8 @@ export class TreeSeedlingSystem {
* @param delta - Frame delta in milliseconds
*/
update(delta: number): void {
const advanced = stateManager.tickSeedlings(delta)
// Drain seedling growth queue (no delta — gameTime is advanced by GameScene)
const advanced = stateManager.tickSeedlings()
for (const id of advanced) {
const state = stateManager.getState()
const seedling = state.world.treeSeedlings[id]
@@ -91,7 +92,7 @@ export class TreeSeedlingSystem {
const seedling: TreeSeedlingState = {
id, tileX, tileY,
stage: 0,
stageTimerMs: TREE_SEEDLING_STAGE_MS,
growsAt: stateManager.getGameTime() + TREE_SEEDLING_STAGE_MS,
underlyingTile,
}