Files
nissefolk/src/systems/BuildingSystem.ts

183 lines
5.8 KiB
TypeScript

import Phaser from 'phaser'
import { TILE_SIZE, BUILDING_COSTS } from '../config'
import { TileType, IMPASSABLE } from '../types'
import type { BuildingType } from '../types'
import { stateManager } from '../StateManager'
import type { LocalAdapter } from '../NetworkAdapter'
const BUILDING_TILE: Partial<Record<BuildingType, TileType>> = {
floor: TileType.FLOOR,
wall: TileType.WALL,
chest: TileType.FLOOR, // chest placed on floor tile
// bed and stockpile_zone do NOT change the underlying tile
}
export class BuildingSystem {
private scene: Phaser.Scene
private adapter: LocalAdapter
private active = false
private selectedBuilding: BuildingType = 'floor'
private ghost!: Phaser.GameObjects.Rectangle
private ghostLabel!: Phaser.GameObjects.Text
private buildKey!: Phaser.Input.Keyboard.Key
private cancelKey!: Phaser.Input.Keyboard.Key
onModeChange?: (active: boolean, building: BuildingType) => void
onPlaced?: (msg: string) => void
constructor(scene: Phaser.Scene, adapter: LocalAdapter) {
this.scene = scene
this.adapter = adapter
}
create(): void {
this.ghost = this.scene.add.rectangle(0, 0, TILE_SIZE, TILE_SIZE, 0x00FF00, 0.35)
this.ghost.setDepth(20)
this.ghost.setVisible(false)
this.ghost.setStrokeStyle(2, 0x00FF00, 0.8)
this.ghostLabel = this.scene.add.text(0, 0, '', {
fontSize: '10px', color: '#ffffff', fontFamily: 'monospace',
backgroundColor: '#000000aa', padding: { x: 3, y: 2 }
})
this.ghostLabel.setDepth(21)
this.ghostLabel.setVisible(false)
this.ghostLabel.setOrigin(0.5, 1)
this.buildKey = this.scene.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.B)
this.cancelKey = this.scene.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.ESC)
// Click to place
this.scene.input.on('pointerdown', (ptr: Phaser.Input.Pointer) => {
if (!this.active) return
if (ptr.rightButtonDown()) {
this.deactivate()
return
}
this.tryPlace(ptr)
})
}
/** Select a building type and activate build mode */
selectBuilding(kind: BuildingType): void {
this.selectedBuilding = kind
this.activate()
}
private activate(): void {
this.active = true
this.ghost.setVisible(true)
this.ghostLabel.setVisible(true)
this.onModeChange?.(true, this.selectedBuilding)
}
deactivate(): void {
this.active = false
this.ghost.setVisible(false)
this.ghostLabel.setVisible(false)
this.onModeChange?.(false, this.selectedBuilding)
}
isActive(): boolean { return this.active }
update(): void {
if (Phaser.Input.Keyboard.JustDown(this.buildKey)) {
if (this.active) this.deactivate()
// If not active, UIScene opens build menu
}
if (Phaser.Input.Keyboard.JustDown(this.cancelKey)) {
this.deactivate()
}
if (!this.active) return
// Update ghost to follow mouse (snapped to tile grid)
const ptr = this.scene.input.activePointer
const worldX = ptr.worldX
const worldY = ptr.worldY
const tileX = Math.floor(worldX / TILE_SIZE)
const tileY = Math.floor(worldY / TILE_SIZE)
const snapX = tileX * TILE_SIZE + TILE_SIZE / 2
const snapY = tileY * TILE_SIZE + TILE_SIZE / 2
this.ghost.setPosition(snapX, snapY)
this.ghostLabel.setPosition(snapX, snapY - TILE_SIZE / 2 - 2)
// Color ghost based on can-build
const canBuild = this.canBuildAt(tileX, tileY)
const color = canBuild ? 0x00FF00 : 0xFF4444
this.ghost.setFillStyle(color, 0.35)
this.ghost.setStrokeStyle(2, color, 0.9)
const costs = BUILDING_COSTS[this.selectedBuilding] ?? {}
const costStr = Object.entries(costs).map(([k, v]) => `${v}${k[0].toUpperCase()}`).join(' ')
this.ghostLabel.setText(`${this.selectedBuilding} [${costStr}]`)
}
private canBuildAt(tileX: number, tileY: number): boolean {
const state = stateManager.getState()
const tile = state.world.tiles[tileY * 512 + tileX] as TileType // 512 = WORLD_TILES
// Can only build on passable ground tiles (not water, not existing buildings)
if (IMPASSABLE.has(tile)) return false
// Check no resource node on this tile
for (const res of Object.values(state.world.resources)) {
if (res.tileX === tileX && res.tileY === tileY) return false
}
// Check no existing building of any kind on this tile
for (const b of Object.values(state.world.buildings)) {
if (b.tileX === tileX && b.tileY === tileY) return false
}
// Check have enough resources
const costs = BUILDING_COSTS[this.selectedBuilding] ?? {}
for (const [item, qty] of Object.entries(costs)) {
const have = state.world.stockpile[item as keyof typeof state.world.stockpile] ?? 0
if (have < qty) return false
}
return true
}
private tryPlace(ptr: Phaser.Input.Pointer): void {
const worldX = ptr.worldX
const worldY = ptr.worldY
const tileX = Math.floor(worldX / TILE_SIZE)
const tileY = Math.floor(worldY / TILE_SIZE)
if (!this.canBuildAt(tileX, tileY)) {
this.onPlaced?.('Cannot build here!')
return
}
const costs = BUILDING_COSTS[this.selectedBuilding] ?? {}
const building = {
id: `building_${tileX}_${tileY}_${Date.now()}`,
tileX,
tileY,
kind: this.selectedBuilding,
ownerId: stateManager.getState().player.id,
}
this.adapter.send({ type: 'PLACE_BUILDING', building, costs })
// Only change the tile type for buildings that have a floor/wall tile mapping
const tileMapped = BUILDING_TILE[this.selectedBuilding]
if (tileMapped !== undefined) {
this.adapter.send({
type: 'CHANGE_TILE',
tileX,
tileY,
tile: tileMapped,
})
}
this.onPlaced?.(`Placed ${this.selectedBuilding}!`)
}
destroy(): void {
this.ghost.destroy()
this.ghostLabel.destroy()
}
}