🎉 initial commit
This commit is contained in:
182
src/systems/BuildingSystem.ts
Normal file
182
src/systems/BuildingSystem.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
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 = this.scene.cameras.main.scrollX + ptr.x
|
||||
const worldY = this.scene.cameras.main.scrollY + ptr.y
|
||||
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 = this.scene.cameras.main.scrollX + ptr.x
|
||||
const worldY = this.scene.cameras.main.scrollY + ptr.y
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user