2026-03-20 08:11:31 +00:00
|
|
|
import Phaser from 'phaser'
|
|
|
|
|
import { AUTOSAVE_INTERVAL, TILE_SIZE } from '../config'
|
2026-03-21 16:15:21 +00:00
|
|
|
import { TileType } from '../types'
|
2026-03-20 08:11:31 +00:00
|
|
|
import type { BuildingType } from '../types'
|
|
|
|
|
import { stateManager } from '../StateManager'
|
|
|
|
|
import { LocalAdapter } from '../NetworkAdapter'
|
|
|
|
|
import { WorldSystem } from '../systems/WorldSystem'
|
|
|
|
|
import { CameraSystem } from '../systems/CameraSystem'
|
|
|
|
|
import { ResourceSystem } from '../systems/ResourceSystem'
|
|
|
|
|
import { BuildingSystem } from '../systems/BuildingSystem'
|
|
|
|
|
import { FarmingSystem } from '../systems/FarmingSystem'
|
|
|
|
|
import { VillagerSystem } from '../systems/VillagerSystem'
|
2026-03-21 12:11:54 +00:00
|
|
|
import { DebugSystem } from '../systems/DebugSystem'
|
2026-03-21 16:15:21 +00:00
|
|
|
import { TreeSeedlingSystem } from '../systems/TreeSeedlingSystem'
|
2026-03-23 13:07:36 +00:00
|
|
|
import { ForesterZoneSystem } from '../systems/ForesterZoneSystem'
|
2026-03-20 08:11:31 +00:00
|
|
|
|
|
|
|
|
export class GameScene extends Phaser.Scene {
|
|
|
|
|
private adapter!: LocalAdapter
|
|
|
|
|
private worldSystem!: WorldSystem
|
|
|
|
|
private cameraSystem!: CameraSystem
|
|
|
|
|
private resourceSystem!: ResourceSystem
|
|
|
|
|
private buildingSystem!: BuildingSystem
|
|
|
|
|
private farmingSystem!: FarmingSystem
|
|
|
|
|
villagerSystem!: VillagerSystem
|
2026-03-21 12:11:54 +00:00
|
|
|
debugSystem!: DebugSystem
|
2026-03-21 16:15:21 +00:00
|
|
|
private treeSeedlingSystem!: TreeSeedlingSystem
|
2026-03-23 13:07:36 +00:00
|
|
|
foresterZoneSystem!: ForesterZoneSystem
|
2026-03-20 08:11:31 +00:00
|
|
|
private autosaveTimer = 0
|
|
|
|
|
private menuOpen = false
|
|
|
|
|
|
|
|
|
|
constructor() { super({ key: 'Game' }) }
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/**
|
|
|
|
|
* Initialises all game systems, wires up inter-system events,
|
|
|
|
|
* launches the UI scene overlay, and starts the autosave timer.
|
|
|
|
|
*/
|
2026-03-20 08:11:31 +00:00
|
|
|
create(): void {
|
|
|
|
|
this.adapter = new LocalAdapter()
|
|
|
|
|
|
|
|
|
|
this.worldSystem = new WorldSystem(this)
|
|
|
|
|
this.cameraSystem = new CameraSystem(this, this.adapter)
|
|
|
|
|
this.resourceSystem = new ResourceSystem(this, this.adapter)
|
|
|
|
|
this.buildingSystem = new BuildingSystem(this, this.adapter)
|
|
|
|
|
this.farmingSystem = new FarmingSystem(this, this.adapter)
|
2026-03-21 16:15:21 +00:00
|
|
|
this.villagerSystem = new VillagerSystem(this, this.adapter, this.worldSystem)
|
2026-03-20 08:11:31 +00:00
|
|
|
this.villagerSystem.init(this.resourceSystem, this.farmingSystem)
|
2026-03-21 16:15:21 +00:00
|
|
|
this.treeSeedlingSystem = new TreeSeedlingSystem(this, this.adapter, this.worldSystem)
|
2026-03-23 13:07:36 +00:00
|
|
|
this.foresterZoneSystem = new ForesterZoneSystem(this, this.adapter)
|
2026-03-21 16:15:21 +00:00
|
|
|
this.debugSystem = new DebugSystem(this, this.villagerSystem, this.worldSystem)
|
2026-03-20 08:11:31 +00:00
|
|
|
|
|
|
|
|
this.worldSystem.create()
|
|
|
|
|
this.renderPersistentObjects()
|
|
|
|
|
|
|
|
|
|
this.cameraSystem.create()
|
|
|
|
|
|
|
|
|
|
this.resourceSystem.create()
|
|
|
|
|
this.resourceSystem.onHarvest = (msg) => this.events.emit('toast', msg)
|
|
|
|
|
|
|
|
|
|
this.buildingSystem.create()
|
|
|
|
|
this.buildingSystem.onModeChange = (active, building) => this.events.emit('buildModeChanged', active, building)
|
|
|
|
|
this.buildingSystem.onPlaced = (msg) => {
|
|
|
|
|
this.events.emit('toast', msg)
|
|
|
|
|
this.renderPersistentObjects()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.farmingSystem.create()
|
2026-03-21 16:15:21 +00:00
|
|
|
this.farmingSystem.onMessage = (msg) => this.events.emit('toast', msg)
|
|
|
|
|
this.farmingSystem.onToolChange = (tool, label) => this.events.emit('farmToolChanged', tool, label)
|
|
|
|
|
this.farmingSystem.onPlantTreeSeed = (tileX, tileY, tile) =>
|
|
|
|
|
this.treeSeedlingSystem.plantSeedling(tileX, tileY, tile)
|
|
|
|
|
|
|
|
|
|
this.treeSeedlingSystem.create()
|
2026-03-20 08:11:31 +00:00
|
|
|
|
2026-03-23 13:07:36 +00:00
|
|
|
this.foresterZoneSystem.create()
|
|
|
|
|
this.foresterZoneSystem.refreshOverlay()
|
|
|
|
|
this.foresterZoneSystem.onEditEnded = () => this.events.emit('foresterZoneEditEnded')
|
|
|
|
|
this.foresterZoneSystem.onZoneChanged = (id, tiles) => this.events.emit('foresterZoneChanged', id, tiles)
|
|
|
|
|
|
2026-03-20 08:11:31 +00:00
|
|
|
this.villagerSystem.create()
|
2026-03-23 13:07:36 +00:00
|
|
|
this.villagerSystem.onMessage = (msg) => this.events.emit('toast', msg)
|
|
|
|
|
this.villagerSystem.onNisseClick = (id) => this.events.emit('nisseClicked', id)
|
|
|
|
|
this.villagerSystem.onPlantSeedling = (tileX, tileY, tile) =>
|
|
|
|
|
this.treeSeedlingSystem.plantSeedling(tileX, tileY, tile)
|
2026-03-20 08:11:31 +00:00
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
this.debugSystem.create()
|
|
|
|
|
|
2026-03-20 08:11:31 +00:00
|
|
|
// Sync tile changes and building visuals through adapter
|
|
|
|
|
this.adapter.onAction = (action) => {
|
|
|
|
|
if (action.type === 'CHANGE_TILE') {
|
|
|
|
|
this.worldSystem.setTile(action.tileX, action.tileY, action.tile)
|
2026-03-23 12:21:23 +00:00
|
|
|
this.worldSystem.refreshTerrainTile(action.tileX, action.tileY, action.tile)
|
2026-03-21 16:15:21 +00:00
|
|
|
} else if (action.type === 'SPAWN_RESOURCE') {
|
|
|
|
|
this.resourceSystem.spawnResourcePublic(action.resource)
|
2026-03-23 11:55:24 +00:00
|
|
|
this.worldSystem.addResourceTile(action.resource.tileX, action.resource.tileY)
|
2026-03-23 13:07:36 +00:00
|
|
|
} else if (action.type === 'FORESTER_ZONE_UPDATE') {
|
|
|
|
|
this.foresterZoneSystem.refreshOverlay()
|
2026-03-20 08:11:31 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 13:07:36 +00:00
|
|
|
// Detect left-clicks on forester huts to open the zone panel
|
|
|
|
|
this.input.on('pointerdown', (ptr: Phaser.Input.Pointer) => {
|
|
|
|
|
if (ptr.rightButtonDown() || this.menuOpen) return
|
|
|
|
|
if (this.buildingSystem.isActive()) return
|
|
|
|
|
const tileX = Math.floor(ptr.worldX / TILE_SIZE)
|
|
|
|
|
const tileY = Math.floor(ptr.worldY / TILE_SIZE)
|
|
|
|
|
const state = stateManager.getState()
|
|
|
|
|
const hut = Object.values(state.world.buildings).find(
|
|
|
|
|
b => b.kind === 'forester_hut' && b.tileX === tileX && b.tileY === tileY
|
|
|
|
|
)
|
|
|
|
|
if (hut) {
|
|
|
|
|
this.events.emit('foresterHutClicked', hut.id)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-20 08:11:31 +00:00
|
|
|
this.scene.launch('UI')
|
|
|
|
|
|
|
|
|
|
this.events.on('selectBuilding', (kind: BuildingType) => this.buildingSystem.selectBuilding(kind))
|
|
|
|
|
this.events.on('uiMenuOpen', () => { this.menuOpen = true })
|
|
|
|
|
this.events.on('uiMenuClose', () => { this.menuOpen = false })
|
|
|
|
|
this.events.on('uiRequestBuildMenu', () => {
|
|
|
|
|
if (!this.buildingSystem.isActive()) this.events.emit('openBuildMenu')
|
|
|
|
|
})
|
2026-03-23 13:07:36 +00:00
|
|
|
this.events.on('updatePriorities', (villagerId: string, priorities: { chop: number; mine: number; farm: number; forester: number }) => {
|
2026-03-20 08:11:31 +00:00
|
|
|
this.adapter.send({ type: 'UPDATE_PRIORITIES', villagerId, priorities })
|
|
|
|
|
})
|
2026-03-23 13:07:36 +00:00
|
|
|
|
|
|
|
|
this.events.on('foresterZoneEditStart', (buildingId: string) => {
|
|
|
|
|
this.foresterZoneSystem.startEditMode(buildingId)
|
|
|
|
|
this.menuOpen = false // keep game ticking while zone editor is open
|
|
|
|
|
})
|
|
|
|
|
this.events.on('foresterZoneEditStop', () => {
|
|
|
|
|
this.foresterZoneSystem.exitEditMode()
|
|
|
|
|
})
|
2026-03-21 12:11:54 +00:00
|
|
|
this.events.on('debugToggle', () => this.debugSystem.toggle())
|
2026-03-20 08:11:31 +00:00
|
|
|
|
|
|
|
|
this.autosaveTimer = AUTOSAVE_INTERVAL
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
2026-03-20 08:11:31 +00:00
|
|
|
update(_time: number, delta: number): void {
|
|
|
|
|
if (this.menuOpen) return
|
|
|
|
|
|
|
|
|
|
this.cameraSystem.update(delta)
|
|
|
|
|
|
|
|
|
|
this.resourceSystem.update(delta)
|
|
|
|
|
this.farmingSystem.update(delta)
|
2026-03-21 16:15:21 +00:00
|
|
|
this.treeSeedlingSystem.update(delta)
|
2026-03-20 08:11:31 +00:00
|
|
|
this.villagerSystem.update(delta)
|
2026-03-21 12:11:54 +00:00
|
|
|
this.debugSystem.update()
|
2026-03-20 08:11:31 +00:00
|
|
|
|
2026-03-21 16:15:21 +00:00
|
|
|
// Tick tile-recovery timers; refresh canvas for any tiles that reverted to GRASS
|
|
|
|
|
const recovered = stateManager.tickTileRecovery(delta)
|
|
|
|
|
for (const key of recovered) {
|
|
|
|
|
const [tx, ty] = key.split(',').map(Number)
|
|
|
|
|
this.worldSystem.refreshTerrainTile(tx, ty, TileType.GRASS)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 08:11:31 +00:00
|
|
|
this.events.emit('cameraMoved', this.cameraSystem.getCenterTile())
|
|
|
|
|
this.buildingSystem.update()
|
|
|
|
|
|
|
|
|
|
this.autosaveTimer -= delta
|
|
|
|
|
if (this.autosaveTimer <= 0) {
|
|
|
|
|
this.autosaveTimer = AUTOSAVE_INTERVAL
|
|
|
|
|
stateManager.save()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Render game objects that persist across sessions (buildings + crop sprites etc.) */
|
|
|
|
|
private renderPersistentObjects(): void {
|
|
|
|
|
const state = stateManager.getState()
|
|
|
|
|
for (const building of Object.values(state.world.buildings)) {
|
|
|
|
|
const wx = building.tileX * TILE_SIZE + TILE_SIZE / 2
|
|
|
|
|
const wy = building.tileY * TILE_SIZE + TILE_SIZE / 2
|
|
|
|
|
const name = `bobj_${building.id}`
|
|
|
|
|
if (this.children.getByName(name)) continue
|
|
|
|
|
|
|
|
|
|
if (building.kind === 'chest') {
|
|
|
|
|
const g = this.add.graphics().setName(name).setDepth(8)
|
|
|
|
|
g.fillStyle(0x8B4513); g.fillRect(wx - 10, wy - 7, 20, 14)
|
|
|
|
|
g.fillStyle(0xCD853F); g.fillRect(wx - 9, wy - 6, 18, 6)
|
|
|
|
|
g.lineStyle(1, 0x5C3317); g.strokeRect(wx - 10, wy - 7, 20, 14)
|
|
|
|
|
} else if (building.kind === 'bed') {
|
|
|
|
|
this.add.image(wx, wy, 'bed_obj').setName(name).setDepth(8)
|
|
|
|
|
} else if (building.kind === 'stockpile_zone') {
|
|
|
|
|
this.add.image(wx, wy, 'stockpile_obj').setName(name).setDepth(4).setAlpha(0.8)
|
2026-03-23 13:07:36 +00:00
|
|
|
} else if (building.kind === 'forester_hut') {
|
|
|
|
|
// Draw a simple log-cabin silhouette for the forester hut
|
|
|
|
|
const g = this.add.graphics().setName(name).setDepth(8)
|
|
|
|
|
// Body
|
|
|
|
|
g.fillStyle(0x6B3F16); g.fillRect(wx - 12, wy - 9, 24, 18)
|
|
|
|
|
// Roof
|
|
|
|
|
g.fillStyle(0x4a2800); g.fillTriangle(wx - 14, wy - 9, wx + 14, wy - 9, wx, wy - 22)
|
|
|
|
|
// Door
|
|
|
|
|
g.fillStyle(0x2a1500); g.fillRect(wx - 4, wy + 1, 8, 8)
|
|
|
|
|
// Tree symbol on the roof
|
|
|
|
|
g.fillStyle(0x228B22); g.fillTriangle(wx - 6, wy - 11, wx + 6, wy - 11, wx, wy - 20)
|
2026-03-20 08:11:31 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 12:11:54 +00:00
|
|
|
/** Saves game state and destroys all systems cleanly on scene shutdown. */
|
2026-03-20 08:11:31 +00:00
|
|
|
shutdown(): void {
|
|
|
|
|
stateManager.save()
|
|
|
|
|
this.worldSystem.destroy()
|
|
|
|
|
this.resourceSystem.destroy()
|
|
|
|
|
this.buildingSystem.destroy()
|
|
|
|
|
this.farmingSystem.destroy()
|
2026-03-21 16:15:21 +00:00
|
|
|
this.treeSeedlingSystem.destroy()
|
2026-03-23 13:07:36 +00:00
|
|
|
this.foresterZoneSystem.destroy()
|
2026-03-20 08:11:31 +00:00
|
|
|
this.villagerSystem.destroy()
|
|
|
|
|
}
|
|
|
|
|
}
|