🎉 initial commit
This commit is contained in:
365
src/scenes/BootScene.ts
Normal file
365
src/scenes/BootScene.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
import Phaser from 'phaser'
|
||||
import { TILE_SIZE, WORLD_TILES } from '../config'
|
||||
import { TileType } from '../types'
|
||||
import { generateTerrain, findSpawn } from '../utils/noise'
|
||||
import { stateManager } from '../StateManager'
|
||||
|
||||
const TOTAL_TILES = 11 // 0-10 in the tileset strip
|
||||
|
||||
export class BootScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super({ key: 'Boot' })
|
||||
}
|
||||
|
||||
preload(): void {
|
||||
this.createLoadingBar()
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.buildTileset()
|
||||
this.buildResourceTextures()
|
||||
this.buildPlayerTexture()
|
||||
this.buildCropTextures()
|
||||
this.buildUITextures()
|
||||
this.buildVillagerAndBuildingTextures()
|
||||
this.generateWorldIfNeeded()
|
||||
this.scene.start('Game')
|
||||
}
|
||||
|
||||
// ─── Loading bar ──────────────────────────────────────────────────────────
|
||||
|
||||
private createLoadingBar(): void {
|
||||
const { width, height } = this.scale
|
||||
const barW = 400, barH = 20
|
||||
const x = width / 2 - barW / 2
|
||||
const y = height / 2
|
||||
|
||||
const border = this.add.graphics()
|
||||
border.lineStyle(2, 0xffffff)
|
||||
border.strokeRect(x - 2, y - 2, barW + 4, barH + 4)
|
||||
|
||||
const bar = this.add.graphics()
|
||||
this.load.on('progress', (v: number) => {
|
||||
bar.clear()
|
||||
bar.fillStyle(0x4CAF50)
|
||||
bar.fillRect(x, y, barW * v, barH)
|
||||
})
|
||||
|
||||
this.add.text(width / 2, y - 40, 'Loading...', {
|
||||
fontSize: '20px', color: '#ffffff', fontFamily: 'monospace'
|
||||
}).setOrigin(0.5)
|
||||
}
|
||||
|
||||
// ─── Tileset (TOTAL_TILES × TILE_SIZE wide, TILE_SIZE tall) ──────────────
|
||||
|
||||
private buildTileset(): void {
|
||||
const T = TILE_SIZE
|
||||
const g = this.add.graphics()
|
||||
|
||||
const drawTile = (idx: number, cb: (g: Phaser.GameObjects.Graphics) => void) => {
|
||||
g.save(); g.translateCanvas(idx * T, 0); cb(g); g.restore()
|
||||
}
|
||||
|
||||
// 0 – Deep water
|
||||
drawTile(TileType.DEEP_WATER, g => {
|
||||
g.fillStyle(0x1565C0); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0x1976D2, 0.6)
|
||||
for (let i = 0; i < 3; i++) g.fillRect(4 + i * 10, 8 + i * 8, 14, 3)
|
||||
})
|
||||
|
||||
// 1 – Shallow water
|
||||
drawTile(TileType.SHALLOW_WATER, g => {
|
||||
g.fillStyle(0x42A5F5); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0x64B5F6, 0.7)
|
||||
g.fillRect(5, 12, 22, 3); g.fillRect(8, 20, 16, 3)
|
||||
})
|
||||
|
||||
// 2 – Sand
|
||||
drawTile(TileType.SAND, g => {
|
||||
g.fillStyle(0xF5DEB3); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0xDEB887, 0.5)
|
||||
g.fillRect(4, 4, 6, 6); g.fillRect(18, 14, 8, 8); g.fillRect(10, 22, 5, 5)
|
||||
})
|
||||
|
||||
// 3 – Grass
|
||||
drawTile(TileType.GRASS, g => {
|
||||
g.fillStyle(0x66BB6A); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0x4CAF50, 0.6)
|
||||
g.fillRect(3, 8, 4, 6); g.fillRect(14, 4, 4, 8); g.fillRect(24, 16, 3, 7)
|
||||
})
|
||||
|
||||
// 4 – Dark grass
|
||||
drawTile(TileType.DARK_GRASS, g => {
|
||||
g.fillStyle(0x43A047); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0x388E3C, 0.6)
|
||||
g.fillRect(2, 6, 5, 8); g.fillRect(16, 3, 5, 10); g.fillRect(22, 18, 4, 8)
|
||||
})
|
||||
|
||||
// 5 – Forest floor (under trees)
|
||||
drawTile(TileType.FOREST, g => {
|
||||
g.fillStyle(0x33691E); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0x2E7D32, 0.5); g.fillRect(0, 0, T, T)
|
||||
})
|
||||
|
||||
// 6 – Rock ground
|
||||
drawTile(TileType.ROCK, g => {
|
||||
g.fillStyle(0x616161); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0x757575, 0.6)
|
||||
g.fillRect(4, 4, 10, 10); g.fillRect(18, 16, 8, 8)
|
||||
})
|
||||
|
||||
// 7 – Built floor (wood plank)
|
||||
drawTile(TileType.FLOOR, g => {
|
||||
g.fillStyle(0xD2A679); g.fillRect(0, 0, T, T)
|
||||
g.lineStyle(1, 0xB8895A)
|
||||
g.strokeRect(1, 1, T - 2, T - 2)
|
||||
g.strokeRect(1, T / 2, T - 2, 1)
|
||||
})
|
||||
|
||||
// 8 – Built wall
|
||||
drawTile(TileType.WALL, g => {
|
||||
g.fillStyle(0x9E9E9E); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0x757575)
|
||||
for (let row = 0; row < 4; row++) {
|
||||
for (let col = 0; col < 2; col++) {
|
||||
const ox = col * 16 + (row % 2 === 0 ? 0 : 8)
|
||||
g.fillRect(ox + 1, row * 8 + 1, 14, 6)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 9 – Tilled soil
|
||||
drawTile(TileType.TILLED_SOIL, g => {
|
||||
g.fillStyle(0x5D3A1A); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0x4A2E0E)
|
||||
for (let row = 0; row < 4; row++) {
|
||||
g.fillRect(2, 4 + row * 8, T - 4, 2)
|
||||
}
|
||||
g.fillStyle(0x7B4F2A, 0.5)
|
||||
g.fillRect(3, 5, T - 6, 1)
|
||||
g.fillRect(3, 13, T - 6, 1)
|
||||
})
|
||||
|
||||
// 10 – Watered soil
|
||||
drawTile(TileType.WATERED_SOIL, g => {
|
||||
g.fillStyle(0x3E2208); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0x2C5F8A, 0.25); g.fillRect(0, 0, T, T)
|
||||
g.fillStyle(0x2A1505)
|
||||
for (let row = 0; row < 4; row++) {
|
||||
g.fillRect(2, 4 + row * 8, T - 4, 2)
|
||||
}
|
||||
g.fillStyle(0x4A90D9, 0.3)
|
||||
g.fillRect(3, 5, T - 6, 1)
|
||||
g.fillRect(3, 13, T - 6, 1)
|
||||
})
|
||||
|
||||
g.generateTexture('tiles', TOTAL_TILES * T, T)
|
||||
g.destroy()
|
||||
}
|
||||
|
||||
// ─── Tree and rock textures ───────────────────────────────────────────────
|
||||
|
||||
private buildResourceTextures(): void {
|
||||
// Tree (32 × 52)
|
||||
const tg = this.add.graphics()
|
||||
tg.fillStyle(0x000000, 0.18); tg.fillEllipse(16, 44, 22, 8)
|
||||
tg.fillStyle(0x6D4C41); tg.fillRect(11, 28, 10, 18)
|
||||
tg.fillStyle(0x2E7D32); tg.fillCircle(16, 20, 15)
|
||||
tg.fillStyle(0x388E3C); tg.fillCircle(10, 25, 11); tg.fillCircle(22, 25, 11)
|
||||
tg.fillStyle(0x43A047); tg.fillCircle(16, 14, 10)
|
||||
tg.generateTexture('tree', 32, 52)
|
||||
tg.destroy()
|
||||
|
||||
// Rock (40 × 34)
|
||||
const rg = this.add.graphics()
|
||||
rg.fillStyle(0x000000, 0.18); rg.fillEllipse(20, 30, 36, 8)
|
||||
rg.fillStyle(0x78909C); rg.fillEllipse(20, 20, 36, 26)
|
||||
rg.fillStyle(0x90A4AE); rg.fillEllipse(14, 14, 20, 16)
|
||||
rg.fillStyle(0xB0BEC5, 0.6); rg.fillEllipse(12, 10, 10, 8)
|
||||
rg.generateTexture('rock', 40, 34)
|
||||
rg.destroy()
|
||||
}
|
||||
|
||||
// ─── Player texture (placeholder – no player body in game) ───────────────
|
||||
|
||||
private buildPlayerTexture(): void {
|
||||
const T = TILE_SIZE
|
||||
const dirs = ['down', 'up', 'left', 'right'] as const
|
||||
dirs.forEach(dir => {
|
||||
const g = this.add.graphics()
|
||||
g.fillStyle(0x000000, 0.2); g.fillEllipse(T / 2, T - 4, 20, 8)
|
||||
g.fillStyle(0xE53935); g.fillCircle(T / 2, T / 2 - 2, 11)
|
||||
g.fillStyle(0x3949AB)
|
||||
g.fillRect(T / 2 - 8, T / 2 + 5, 7, 10)
|
||||
g.fillRect(T / 2 + 1, T / 2 + 5, 7, 10)
|
||||
g.fillStyle(0xFFFFFF)
|
||||
if (dir === 'down') g.fillTriangle(T/2, T/2+2, T/2-5, T/2-4, T/2+5, T/2-4)
|
||||
if (dir === 'up') g.fillTriangle(T/2, T/2-5, T/2-5, T/2+3, T/2+5, T/2+3)
|
||||
if (dir === 'left') g.fillTriangle(T/2-5, T/2, T/2+3, T/2-5, T/2+3, T/2+5)
|
||||
if (dir === 'right') g.fillTriangle(T/2+5, T/2, T/2-3, T/2-5, T/2-3, T/2+5)
|
||||
g.generateTexture(`player_${dir}`, T, T)
|
||||
g.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Crop textures ────────────────────────────────────────────────────────
|
||||
|
||||
private buildCropTextures(): void {
|
||||
this.buildWheatTextures()
|
||||
this.buildCarrotTextures()
|
||||
}
|
||||
|
||||
private buildWheatTextures(): void {
|
||||
const W = 32, H = 40
|
||||
|
||||
const g0 = this.add.graphics()
|
||||
g0.fillStyle(0x8BC34A); g0.fillRect(14, 26, 4, 12)
|
||||
g0.fillStyle(0x9CCC65); g0.fillEllipse(16, 24, 10, 8)
|
||||
g0.generateTexture('crop_wheat_0', W, H); g0.destroy()
|
||||
|
||||
const g1 = this.add.graphics()
|
||||
g1.fillStyle(0x7CB342)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
g1.fillRect(8 + i * 8, 16 + i * 2, 3, 22 - i * 2)
|
||||
}
|
||||
g1.fillStyle(0x9CCC65)
|
||||
g1.fillEllipse(10, 14, 8, 6); g1.fillEllipse(18, 12, 8, 6); g1.fillEllipse(26, 16, 8, 6)
|
||||
g1.generateTexture('crop_wheat_1', W, H); g1.destroy()
|
||||
|
||||
const g2 = this.add.graphics()
|
||||
g2.fillStyle(0x558B2F)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
g2.fillRect(5 + i * 7, 8 + (i % 2) * 4, 3, 30 - (i % 2) * 4)
|
||||
}
|
||||
g2.fillStyle(0x689F38)
|
||||
g2.fillEllipse(7, 6, 7, 5); g2.fillEllipse(14, 4, 7, 5)
|
||||
g2.fillEllipse(21, 7, 7, 5); g2.fillEllipse(28, 5, 7, 5)
|
||||
g2.generateTexture('crop_wheat_2', W, H); g2.destroy()
|
||||
|
||||
const g3 = this.add.graphics()
|
||||
g3.fillStyle(0x795548)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
g3.fillRect(3 + i * 6, 14 + (i % 2) * 2, 2, 24)
|
||||
}
|
||||
g3.fillStyle(0xFDD835)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
g3.fillEllipse(4 + i * 6, 10 + (i % 2) * 2, 6, 12)
|
||||
}
|
||||
g3.fillStyle(0xF9A825, 0.7)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
g3.fillRect(3 + i * 6, 4 + (i % 2) * 2, 2, 8)
|
||||
}
|
||||
g3.generateTexture('crop_wheat_3', W, H); g3.destroy()
|
||||
}
|
||||
|
||||
private buildCarrotTextures(): void {
|
||||
const W = 32, H = 40
|
||||
|
||||
const g0 = this.add.graphics()
|
||||
g0.fillStyle(0x4CAF50); g0.fillRect(14, 26, 4, 12)
|
||||
g0.fillStyle(0x66BB6A); g0.fillEllipse(16, 24, 10, 8)
|
||||
g0.generateTexture('crop_carrot_0', W, H); g0.destroy()
|
||||
|
||||
const g1 = this.add.graphics()
|
||||
g1.fillStyle(0x388E3C)
|
||||
g1.fillRect(14, 22, 4, 16)
|
||||
g1.fillEllipse(11, 18, 10, 8); g1.fillEllipse(21, 18, 10, 8)
|
||||
g1.fillEllipse(16, 14, 8, 10)
|
||||
g1.generateTexture('crop_carrot_1', W, H); g1.destroy()
|
||||
|
||||
const g2 = this.add.graphics()
|
||||
g2.fillStyle(0x2E7D32)
|
||||
g2.fillRect(14, 18, 4, 20)
|
||||
g2.fillEllipse(9, 14, 12, 10); g2.fillEllipse(23, 14, 12, 10)
|
||||
g2.fillEllipse(16, 8, 10, 12); g2.fillEllipse(13, 10, 8, 8); g2.fillEllipse(19, 10, 8, 8)
|
||||
g2.fillStyle(0xE65100, 0.6); g2.fillEllipse(16, 36, 8, 6)
|
||||
g2.generateTexture('crop_carrot_2', W, H); g2.destroy()
|
||||
|
||||
const g3 = this.add.graphics()
|
||||
g3.fillStyle(0x1B5E20)
|
||||
g3.fillRect(14, 14, 4, 16)
|
||||
g3.fillEllipse(8, 10, 12, 10); g3.fillEllipse(24, 10, 12, 10)
|
||||
g3.fillEllipse(16, 4, 10, 12); g3.fillEllipse(13, 6, 8, 8); g3.fillEllipse(19, 6, 8, 8)
|
||||
g3.fillStyle(0xFF6F00); g3.fillEllipse(16, 30, 12, 10)
|
||||
g3.fillStyle(0xFF8F00)
|
||||
g3.fillTriangle(10, 28, 22, 28, 16, 40)
|
||||
g3.fillStyle(0xFFCC02, 0.4); g3.fillEllipse(13, 27, 5, 4)
|
||||
g3.generateTexture('crop_carrot_3', W, H); g3.destroy()
|
||||
}
|
||||
|
||||
// ─── UI panel texture ─────────────────────────────────────────────────────
|
||||
|
||||
private buildUITextures(): void {
|
||||
const pg = this.add.graphics()
|
||||
pg.fillStyle(0x000000, 0.65)
|
||||
pg.fillRoundedRect(0, 0, 200, 100, 8)
|
||||
pg.generateTexture('panel', 200, 100)
|
||||
pg.destroy()
|
||||
}
|
||||
|
||||
// ─── Villager + building object textures ──────────────────────────────────
|
||||
|
||||
private buildVillagerAndBuildingTextures(): void {
|
||||
// ── Villager gnome (24 × 28) ──────────────────────────────────────────
|
||||
const vg = this.add.graphics()
|
||||
vg.fillStyle(0x000000, 0.15); vg.fillEllipse(12, 26, 18, 6)
|
||||
vg.fillStyle(0x1A237E); vg.fillRect(7, 16, 5, 9)
|
||||
vg.fillRect(12, 16, 5, 9)
|
||||
vg.fillStyle(0x3949AB); vg.fillRect(5, 9, 14, 10)
|
||||
vg.fillStyle(0xFFCC80); vg.fillCircle(12, 7, 6)
|
||||
vg.fillStyle(0x4E342E); vg.fillCircle(10, 6, 1); vg.fillCircle(14, 6, 1)
|
||||
vg.generateTexture('villager', 24, 28)
|
||||
vg.destroy()
|
||||
|
||||
// ── Bed (32 × 32) ─────────────────────────────────────────────────────
|
||||
const bg = this.add.graphics()
|
||||
bg.fillStyle(0x6D4C41); bg.fillRect(1, 1, 30, 30)
|
||||
bg.fillStyle(0xEFEBE9); bg.fillRect(3, 3, 26, 10)
|
||||
bg.fillStyle(0x5C6BC0); bg.fillRect(3, 15, 26, 14)
|
||||
bg.lineStyle(1, 0x4E342E, 0.8); bg.strokeRect(1, 1, 30, 30)
|
||||
bg.fillStyle(0xBCAAA4); bg.fillRect(3, 13, 26, 3)
|
||||
bg.generateTexture('bed_obj', 32, 32)
|
||||
bg.destroy()
|
||||
|
||||
// ── Stockpile zone (32 × 32) ──────────────────────────────────────────
|
||||
const sg = this.add.graphics()
|
||||
sg.fillStyle(0xFFF9C4, 0.5); sg.fillRect(0, 0, 32, 32)
|
||||
sg.lineStyle(2, 0xF9A825, 0.9)
|
||||
for (let i = 0; i < 32; i += 6) { sg.strokeRect(i, 0, 6, 32) }
|
||||
sg.fillStyle(0x8D6E63); sg.fillRect(6, 10, 9, 9); sg.fillRect(17, 10, 9, 9)
|
||||
sg.fillRect(6, 20, 9, 9); sg.fillRect(17, 20, 9, 9)
|
||||
sg.lineStyle(1, 0x5D4037)
|
||||
sg.strokeRect(6, 10, 9, 9); sg.strokeRect(17, 10, 9, 9)
|
||||
sg.strokeRect(6, 20, 9, 9); sg.strokeRect(17, 20, 9, 9)
|
||||
sg.generateTexture('stockpile_obj', 32, 32)
|
||||
sg.destroy()
|
||||
}
|
||||
|
||||
// ─── Terrain generation ───────────────────────────────────────────────────
|
||||
|
||||
private generateWorldIfNeeded(): void {
|
||||
const state = stateManager.getState()
|
||||
if (Object.keys(state.world.resources).length === 0) {
|
||||
const tiles = generateTerrain(state.world.seed)
|
||||
const mutableTiles = state.world.tiles as number[]
|
||||
for (let i = 0; i < tiles.length; i++) mutableTiles[i] = tiles[i]
|
||||
|
||||
for (let y = 0; y < WORLD_TILES; y++) {
|
||||
for (let x = 0; x < WORLD_TILES; x++) {
|
||||
const tile = tiles[y * WORLD_TILES + x]
|
||||
if (tile === TileType.FOREST && Math.random() < 0.7) {
|
||||
const id = `tree_${x}_${y}`
|
||||
state.world.resources[id] = { id, tileX: x, tileY: y, kind: 'tree', hp: 3 }
|
||||
} else if (tile === TileType.ROCK && Math.random() < 0.5) {
|
||||
const id = `rock_${x}_${y}`
|
||||
state.world.resources[id] = { id, tileX: x, tileY: y, kind: 'rock', hp: 5 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const spawn = findSpawn(tiles)
|
||||
;(state.player as { x: number; y: number }).x = (spawn.tileX + 0.5) * 32
|
||||
;(state.player as { x: number; y: number }).y = (spawn.tileY + 0.5) * 32
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/scenes/GameScene.ts
Normal file
130
src/scenes/GameScene.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import Phaser from 'phaser'
|
||||
import { AUTOSAVE_INTERVAL, TILE_SIZE } from '../config'
|
||||
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'
|
||||
|
||||
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
|
||||
private autosaveTimer = 0
|
||||
private menuOpen = false
|
||||
|
||||
constructor() { super({ key: 'Game' }) }
|
||||
|
||||
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)
|
||||
this.villagerSystem = new VillagerSystem(this, this.adapter, this.worldSystem)
|
||||
this.villagerSystem.init(this.resourceSystem, this.farmingSystem)
|
||||
|
||||
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()
|
||||
this.farmingSystem.onMessage = (msg) => this.events.emit('toast', msg)
|
||||
this.farmingSystem.onToolChange = (tool, label) => this.events.emit('farmToolChanged', tool, label)
|
||||
|
||||
this.villagerSystem.create()
|
||||
this.villagerSystem.onMessage = (msg) => this.events.emit('toast', msg)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
})
|
||||
this.events.on('updatePriorities', (villagerId: string, priorities: { chop: number; mine: number; farm: number }) => {
|
||||
this.adapter.send({ type: 'UPDATE_PRIORITIES', villagerId, priorities })
|
||||
})
|
||||
|
||||
this.autosaveTimer = AUTOSAVE_INTERVAL
|
||||
}
|
||||
|
||||
update(_time: number, delta: number): void {
|
||||
if (this.menuOpen) return
|
||||
|
||||
this.cameraSystem.update(delta)
|
||||
|
||||
this.resourceSystem.update(delta)
|
||||
this.farmingSystem.update(delta)
|
||||
this.villagerSystem.update(delta)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shutdown(): void {
|
||||
stateManager.save()
|
||||
this.worldSystem.destroy()
|
||||
this.resourceSystem.destroy()
|
||||
this.buildingSystem.destroy()
|
||||
this.farmingSystem.destroy()
|
||||
this.villagerSystem.destroy()
|
||||
}
|
||||
}
|
||||
283
src/scenes/UIScene.ts
Normal file
283
src/scenes/UIScene.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import Phaser from 'phaser'
|
||||
import type { BuildingType, JobPriorities } from '../types'
|
||||
import type { FarmingTool } from '../systems/FarmingSystem'
|
||||
import { stateManager } from '../StateManager'
|
||||
|
||||
const ITEM_ICONS: Record<string, string> = {
|
||||
wood: '🪵', stone: '🪨', wheat_seed: '🌱', carrot_seed: '🥕',
|
||||
wheat: '🌾', carrot: '🧡',
|
||||
}
|
||||
|
||||
export class UIScene extends Phaser.Scene {
|
||||
private stockpileTexts: Map<string, Phaser.GameObjects.Text> = new Map()
|
||||
private stockpilePanel!: Phaser.GameObjects.Rectangle
|
||||
private hintText!: Phaser.GameObjects.Text
|
||||
private toastText!: Phaser.GameObjects.Text
|
||||
private toastTimer = 0
|
||||
private buildMenuGroup!: Phaser.GameObjects.Group
|
||||
private buildMenuVisible = false
|
||||
private villagerPanelGroup!: Phaser.GameObjects.Group
|
||||
private villagerPanelVisible = false
|
||||
private buildModeText!: Phaser.GameObjects.Text
|
||||
private farmToolText!: Phaser.GameObjects.Text
|
||||
private coordsText!: Phaser.GameObjects.Text
|
||||
private popText!: Phaser.GameObjects.Text
|
||||
|
||||
constructor() { super({ key: 'UI' }) }
|
||||
|
||||
create(): void {
|
||||
this.createStockpilePanel()
|
||||
this.createHintText()
|
||||
this.createToast()
|
||||
this.createBuildMenu()
|
||||
this.createBuildModeIndicator()
|
||||
this.createFarmToolIndicator()
|
||||
this.createCoordsDisplay()
|
||||
|
||||
const gameScene = this.scene.get('Game')
|
||||
gameScene.events.on('buildModeChanged', (a: boolean, b: BuildingType) => this.onBuildModeChanged(a, b))
|
||||
gameScene.events.on('farmToolChanged', (t: FarmingTool, l: string) => this.onFarmToolChanged(t, l))
|
||||
gameScene.events.on('toast', (m: string) => this.showToast(m))
|
||||
gameScene.events.on('openBuildMenu', () => this.toggleBuildMenu())
|
||||
gameScene.events.on('cameraMoved', (pos: { tileX: number; tileY: number }) => this.onCameraMoved(pos))
|
||||
|
||||
this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.B)
|
||||
.on('down', () => gameScene.events.emit('uiRequestBuildMenu'))
|
||||
this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.V)
|
||||
.on('down', () => this.toggleVillagerPanel())
|
||||
|
||||
this.scale.on('resize', () => this.repositionUI())
|
||||
}
|
||||
|
||||
update(_t: number, delta: number): void {
|
||||
this.updateStockpile()
|
||||
this.updateToast(delta)
|
||||
this.updatePopText()
|
||||
}
|
||||
|
||||
// ─── Stockpile ────────────────────────────────────────────────────────────
|
||||
|
||||
private createStockpilePanel(): void {
|
||||
const x = this.scale.width - 178, y = 10
|
||||
this.stockpilePanel = this.add.rectangle(x, y, 168, 165, 0x000000, 0.72).setOrigin(0, 0).setScrollFactor(0).setDepth(100)
|
||||
this.add.text(x + 10, y + 7, '⚡ STOCKPILE', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101)
|
||||
const items = ['wood','stone','wheat_seed','carrot_seed','wheat','carrot'] as const
|
||||
items.forEach((item, i) => {
|
||||
const t = this.add.text(x + 10, y + 26 + i * 22, `${ITEM_ICONS[item]} ${item}: 0`, { fontSize: '13px', color: '#88dd88', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101)
|
||||
this.stockpileTexts.set(item, t)
|
||||
})
|
||||
this.popText = this.add.text(x + 10, y + 145, '👥 Pop: 0 / 0', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101)
|
||||
}
|
||||
|
||||
private updateStockpile(): void {
|
||||
const sp = stateManager.getState().world.stockpile
|
||||
for (const [item, t] of this.stockpileTexts) {
|
||||
const qty = sp[item as keyof typeof sp] ?? 0
|
||||
t.setStyle({ color: qty > 0 ? '#88dd88' : '#444444' })
|
||||
t.setText(`${ITEM_ICONS[item]} ${item}: ${qty}`)
|
||||
}
|
||||
}
|
||||
|
||||
private updatePopText(): void {
|
||||
const state = stateManager.getState()
|
||||
const beds = Object.values(state.world.buildings).filter(b => b.kind === 'bed').length
|
||||
const current = Object.keys(state.world.villagers).length
|
||||
this.popText?.setText(`👥 Pop: ${current} / ${beds} [V] manage`)
|
||||
}
|
||||
|
||||
// ─── Hint ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private createHintText(): void {
|
||||
this.hintText = this.add.text(this.scale.width / 2, this.scale.height - 40, '', {
|
||||
fontSize: '14px', color: '#ffff88', fontFamily: 'monospace',
|
||||
backgroundColor: '#00000099', padding: { x: 10, y: 5 },
|
||||
}).setOrigin(0.5).setScrollFactor(0).setDepth(100).setVisible(false)
|
||||
}
|
||||
|
||||
// ─── Toast ────────────────────────────────────────────────────────────────
|
||||
|
||||
private createToast(): void {
|
||||
this.toastText = this.add.text(this.scale.width / 2, 60, '', {
|
||||
fontSize: '15px', color: '#88ff88', fontFamily: 'monospace',
|
||||
backgroundColor: '#00000099', padding: { x: 12, y: 6 },
|
||||
}).setOrigin(0.5, 0).setScrollFactor(0).setDepth(102).setAlpha(0)
|
||||
}
|
||||
|
||||
showToast(msg: string): void { this.toastText.setText(msg).setAlpha(1); this.toastTimer = 2200 }
|
||||
|
||||
private updateToast(delta: number): void {
|
||||
if (this.toastTimer <= 0) return
|
||||
this.toastTimer -= delta
|
||||
if (this.toastTimer <= 0) this.tweens.add({ targets: this.toastText, alpha: 0, duration: 400 })
|
||||
}
|
||||
|
||||
// ─── Build Menu ───────────────────────────────────────────────────────────
|
||||
|
||||
private createBuildMenu(): void {
|
||||
this.buildMenuGroup = this.add.group()
|
||||
const buildings: { kind: BuildingType; label: string; cost: string }[] = [
|
||||
{ kind: 'floor', label: 'Floor', cost: '2 wood' },
|
||||
{ kind: 'wall', label: 'Wall', cost: '3 wood + 1 stone' },
|
||||
{ kind: 'chest', label: 'Chest', cost: '5 wood + 2 stone' },
|
||||
{ kind: 'bed', label: '🛏 Bed', cost: '6 wood (+1 villager)' },
|
||||
{ kind: 'stockpile_zone', label: '📦 Stockpile', cost: 'free (workers deliver here)' },
|
||||
]
|
||||
const menuX = this.scale.width / 2 - 150, menuY = this.scale.height / 2 - 140
|
||||
const bg = this.add.rectangle(menuX, menuY, 300, 280, 0x000000, 0.88).setOrigin(0,0).setScrollFactor(0).setDepth(200)
|
||||
this.buildMenuGroup.add(bg)
|
||||
this.buildMenuGroup.add(this.add.text(menuX + 150, menuY + 14, 'BUILD MENU [B/ESC]', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setOrigin(0.5,0).setScrollFactor(0).setDepth(201))
|
||||
|
||||
buildings.forEach((b, i) => {
|
||||
const btnY = menuY + 38 + i * 46
|
||||
const btn = this.add.rectangle(menuX + 14, btnY, 272, 38, 0x1a3a1a, 0.9).setOrigin(0,0).setScrollFactor(0).setDepth(201).setInteractive()
|
||||
btn.on('pointerover', () => btn.setFillStyle(0x2d6a4f, 0.9))
|
||||
btn.on('pointerout', () => btn.setFillStyle(0x1a3a1a, 0.9))
|
||||
btn.on('pointerdown', () => { this.closeBuildMenu(); this.scene.get('Game').events.emit('selectBuilding', b.kind) })
|
||||
this.buildMenuGroup.add(btn)
|
||||
this.buildMenuGroup.add(this.add.text(menuX + 24, btnY + 5, b.label, { fontSize: '13px', color: '#ffffff', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(202))
|
||||
this.buildMenuGroup.add(this.add.text(menuX + 24, btnY + 22, `Cost: ${b.cost}`, { fontSize: '10px', color: '#888888', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(202))
|
||||
})
|
||||
this.buildMenuGroup.setVisible(false)
|
||||
}
|
||||
|
||||
private toggleBuildMenu(): void { this.buildMenuVisible ? this.closeBuildMenu() : this.openBuildMenu() }
|
||||
private openBuildMenu(): void { this.buildMenuVisible = true; this.buildMenuGroup.setVisible(true); this.scene.get('Game').events.emit('uiMenuOpen') }
|
||||
private closeBuildMenu(): void { this.buildMenuVisible = false; this.buildMenuGroup.setVisible(false); this.scene.get('Game').events.emit('uiMenuClose') }
|
||||
|
||||
// ─── Villager Panel (V key) ───────────────────────────────────────────────
|
||||
|
||||
private toggleVillagerPanel(): void {
|
||||
if (this.villagerPanelVisible) {
|
||||
this.closeVillagerPanel()
|
||||
} else {
|
||||
this.openVillagerPanel()
|
||||
}
|
||||
}
|
||||
|
||||
private openVillagerPanel(): void {
|
||||
this.villagerPanelVisible = true
|
||||
this.buildVillagerPanel()
|
||||
this.scene.get('Game').events.emit('uiMenuOpen')
|
||||
}
|
||||
|
||||
private closeVillagerPanel(): void {
|
||||
this.villagerPanelVisible = false
|
||||
this.villagerPanelGroup?.destroy(true)
|
||||
this.scene.get('Game').events.emit('uiMenuClose')
|
||||
}
|
||||
|
||||
private buildVillagerPanel(): void {
|
||||
if (this.villagerPanelGroup) this.villagerPanelGroup.destroy(true)
|
||||
this.villagerPanelGroup = this.add.group()
|
||||
|
||||
const state = stateManager.getState()
|
||||
const villagers = Object.values(state.world.villagers)
|
||||
const panelW = 420
|
||||
const rowH = 60
|
||||
const panelH = Math.max(100, villagers.length * rowH + 50)
|
||||
const px = this.scale.width / 2 - panelW / 2
|
||||
const py = this.scale.height / 2 - panelH / 2
|
||||
|
||||
const bg = this.add.rectangle(px, py, panelW, panelH, 0x0a0a0a, 0.92).setOrigin(0,0).setScrollFactor(0).setDepth(210)
|
||||
this.villagerPanelGroup.add(bg)
|
||||
|
||||
this.villagerPanelGroup.add(
|
||||
this.add.text(px + panelW/2, py + 12, '👥 VILLAGERS [V] close', { fontSize: '12px', color: '#aaaaaa', fontFamily: 'monospace' })
|
||||
.setOrigin(0.5, 0).setScrollFactor(0).setDepth(211)
|
||||
)
|
||||
|
||||
if (villagers.length === 0) {
|
||||
this.villagerPanelGroup.add(
|
||||
this.add.text(px + panelW/2, py + panelH/2, 'No villagers yet.\nBuild a 🛏 Bed first!', {
|
||||
fontSize: '13px', color: '#666666', fontFamily: 'monospace', align: 'center'
|
||||
}).setOrigin(0.5).setScrollFactor(0).setDepth(211)
|
||||
)
|
||||
}
|
||||
|
||||
villagers.forEach((v, i) => {
|
||||
const ry = py + 38 + i * rowH
|
||||
const gameScene = this.scene.get('Game') as any
|
||||
|
||||
// Name + status
|
||||
const statusText = gameScene.villagerSystem?.getStatusText(v.id) ?? '—'
|
||||
this.villagerPanelGroup.add(
|
||||
this.add.text(px + 12, ry, `${v.name}`, { fontSize: '13px', color: '#ffffff', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(211)
|
||||
)
|
||||
this.villagerPanelGroup.add(
|
||||
this.add.text(px + 12, ry + 16, statusText, { fontSize: '10px', color: '#888888', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(211)
|
||||
)
|
||||
|
||||
// Energy bar
|
||||
const eg = this.add.graphics().setScrollFactor(0).setDepth(211)
|
||||
eg.fillStyle(0x333333); eg.fillRect(px + 12, ry + 30, 80, 6)
|
||||
const col = v.energy > 60 ? 0x4CAF50 : v.energy > 30 ? 0xFF9800 : 0xF44336
|
||||
eg.fillStyle(col); eg.fillRect(px + 12, ry + 30, 80 * v.energy / 100, 6)
|
||||
this.villagerPanelGroup.add(eg)
|
||||
|
||||
// Job priority buttons: chop / mine / farm
|
||||
const jobs: Array<{ key: keyof JobPriorities; label: string }> = [
|
||||
{ key: 'chop', label: '🪓' }, { key: 'mine', label: '⛏' }, { key: 'farm', label: '🌾' }
|
||||
]
|
||||
jobs.forEach((job, ji) => {
|
||||
const bx = px + 110 + ji * 100
|
||||
const pri = v.priorities[job.key]
|
||||
const label = pri === 0 ? `${job.label} OFF` : `${job.label} P${pri}`
|
||||
const btn = this.add.text(bx, ry + 6, label, {
|
||||
fontSize: '11px', color: pri === 0 ? '#555555' : '#ffffff',
|
||||
fontFamily: 'monospace', backgroundColor: pri === 0 ? '#1a1a1a' : '#1a4a1a',
|
||||
padding: { x: 6, y: 4 }
|
||||
}).setScrollFactor(0).setDepth(212).setInteractive()
|
||||
|
||||
btn.on('pointerover', () => btn.setStyle({ backgroundColor: '#2d6a4f' }))
|
||||
btn.on('pointerout', () => btn.setStyle({ backgroundColor: pri === 0 ? '#1a1a1a' : '#1a4a1a' }))
|
||||
btn.on('pointerdown', () => {
|
||||
const newPri = ((v.priorities[job.key] + 1) % 5) // 0→1→2→3→4→0
|
||||
const newPriorities: JobPriorities = { ...v.priorities, [job.key]: newPri }
|
||||
this.scene.get('Game').events.emit('updatePriorities', v.id, newPriorities)
|
||||
this.closeVillagerPanel()
|
||||
this.openVillagerPanel() // Rebuild to reflect change
|
||||
})
|
||||
this.villagerPanelGroup.add(btn)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Build mode indicator ─────────────────────────────────────────────────
|
||||
|
||||
private createBuildModeIndicator(): void {
|
||||
this.buildModeText = this.add.text(10, 10, '', { fontSize: '13px', color: '#ffff00', fontFamily: 'monospace', backgroundColor: '#00000099', padding: { x: 8, y: 4 } }).setScrollFactor(0).setDepth(100).setVisible(false)
|
||||
}
|
||||
private onBuildModeChanged(active: boolean, building: BuildingType): void {
|
||||
this.buildModeText.setText(active ? `🏗 BUILD: ${building.toUpperCase()} [RMB/ESC cancel]` : '').setVisible(active)
|
||||
}
|
||||
|
||||
// ─── Farm tool indicator ──────────────────────────────────────────────────
|
||||
|
||||
private createFarmToolIndicator(): void {
|
||||
this.farmToolText = this.add.text(10, 44, '', { fontSize: '13px', color: '#aaffaa', fontFamily: 'monospace', backgroundColor: '#00000099', padding: { x: 8, y: 4 } }).setScrollFactor(0).setDepth(100).setVisible(false)
|
||||
}
|
||||
private onFarmToolChanged(tool: FarmingTool, label: string): void {
|
||||
this.farmToolText.setText(tool === 'none' ? '' : `[F] Farm: ${label} [RMB cancel]`).setVisible(tool !== 'none')
|
||||
}
|
||||
|
||||
// ─── Coords + controls ────────────────────────────────────────────────────
|
||||
|
||||
private createCoordsDisplay(): void {
|
||||
this.coordsText = this.add.text(10, this.scale.height - 24, '', { fontSize: '11px', color: '#666666', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(100)
|
||||
this.add.text(10, this.scale.height - 42, '[WASD] Pan [Scroll] Zoom [F] Farm [B] Build [V] Villagers', {
|
||||
fontSize: '10px', color: '#444444', fontFamily: 'monospace', backgroundColor: '#00000066', padding: { x: 4, y: 2 }
|
||||
}).setScrollFactor(0).setDepth(100)
|
||||
}
|
||||
private onCameraMoved(pos: { tileX: number; tileY: number }): void {
|
||||
this.coordsText.setText(`Tile: ${pos.tileX}, ${pos.tileY}`)
|
||||
}
|
||||
|
||||
// ─── Resize ───────────────────────────────────────────────────────────────
|
||||
|
||||
private repositionUI(): void {
|
||||
const { width, height } = this.scale
|
||||
this.hintText.setPosition(width/2, height - 40)
|
||||
this.toastText.setPosition(width/2, 60)
|
||||
this.coordsText.setPosition(10, height - 24)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user