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> = { 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(1000) 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(1001) 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() } }