implement unified tile system (Issue #14)

- Tree seedlings: plant tree_seed on grass via farming tool; two-stage
  growth (sprout → sapling → young tree, ~1 min/stage); matures into
  a harvestable FOREST resource tile
- Tile recovery: Nisse chops start a 5-min DARK_GRASS→GRASS timer;
  terrain canvas updated live via WorldSystem.refreshTerrainTile()
- New TreeSeedlingSystem manages sprites, growth ticking, maturation
- BootScene generates seedling_0/1/2 textures procedurally
- FarmingSystem adds tree_seed to tool cycle (F key)
- Stockpile panel shows tree_seed (default: 5); panel height adjusted
- StateManager v5: treeSeedlings + tileRecovery in WorldState
- WorldSystem uses CanvasTexture for live single-pixel updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 16:15:21 +00:00
parent bbbb3e1f58
commit 18c8ccb644
12 changed files with 375 additions and 28 deletions

View File

@@ -20,6 +20,7 @@ export class BootScene extends Phaser.Scene {
this.buildResourceTextures()
this.buildPlayerTexture()
this.buildCropTextures()
this.buildSeedlingTextures()
this.buildUITextures()
this.buildVillagerAndBuildingTextures()
this.generateWorldIfNeeded()
@@ -287,6 +288,40 @@ export class BootScene extends Phaser.Scene {
g3.generateTexture('crop_carrot_3', W, H); g3.destroy()
}
// ─── Tree seedling textures (3 growth stages) ────────────────────────────
/**
* Generates textures for the three tree-seedling growth stages:
* seedling_0 small sprout
* seedling_1 sapling with leaves
* seedling_2 young tree (about to mature into a FOREST tile)
*/
private buildSeedlingTextures(): void {
// Stage 0: tiny sprout
const g0 = this.add.graphics()
g0.fillStyle(0x6D4C41); g0.fillRect(10, 20, 4, 10)
g0.fillStyle(0x66BB6A); g0.fillEllipse(12, 16, 12, 8)
g0.fillStyle(0x4CAF50); g0.fillEllipse(12, 13, 8, 6)
g0.generateTexture('seedling_0', 24, 32); g0.destroy()
// Stage 1: sapling
const g1 = this.add.graphics()
g1.fillStyle(0x6D4C41); g1.fillRect(9, 15, 5, 16)
g1.fillStyle(0x4CAF50); g1.fillCircle(12, 12, 8)
g1.fillStyle(0x66BB6A, 0.7); g1.fillCircle(7, 16, 5); g1.fillCircle(17, 16, 5)
g1.fillStyle(0x81C784); g1.fillCircle(12, 8, 5)
g1.generateTexture('seedling_1', 24, 32); g1.destroy()
// Stage 2: young tree (mature, ready to become a resource)
const g2 = this.add.graphics()
g2.fillStyle(0x000000, 0.15); g2.fillEllipse(12, 28, 16, 6)
g2.fillStyle(0x6D4C41); g2.fillRect(9, 14, 6, 14)
g2.fillStyle(0x2E7D32); g2.fillCircle(12, 9, 10)
g2.fillStyle(0x388E3C); g2.fillCircle(7, 13, 7); g2.fillCircle(17, 13, 7)
g2.fillStyle(0x43A047); g2.fillCircle(12, 6, 7)
g2.generateTexture('seedling_2', 24, 32); g2.destroy()
}
// ─── UI panel texture ─────────────────────────────────────────────────────
private buildUITextures(): void {

View File

@@ -1,5 +1,6 @@
import Phaser from 'phaser'
import { AUTOSAVE_INTERVAL, TILE_SIZE } from '../config'
import { TileType } from '../types'
import type { BuildingType } from '../types'
import { stateManager } from '../StateManager'
import { LocalAdapter } from '../NetworkAdapter'
@@ -10,6 +11,7 @@ import { BuildingSystem } from '../systems/BuildingSystem'
import { FarmingSystem } from '../systems/FarmingSystem'
import { VillagerSystem } from '../systems/VillagerSystem'
import { DebugSystem } from '../systems/DebugSystem'
import { TreeSeedlingSystem } from '../systems/TreeSeedlingSystem'
export class GameScene extends Phaser.Scene {
private adapter!: LocalAdapter
@@ -20,6 +22,7 @@ export class GameScene extends Phaser.Scene {
private farmingSystem!: FarmingSystem
villagerSystem!: VillagerSystem
debugSystem!: DebugSystem
private treeSeedlingSystem!: TreeSeedlingSystem
private autosaveTimer = 0
private menuOpen = false
@@ -37,9 +40,10 @@ export class GameScene extends Phaser.Scene {
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 = new VillagerSystem(this, this.adapter, this.worldSystem)
this.villagerSystem.init(this.resourceSystem, this.farmingSystem)
this.debugSystem = new DebugSystem(this, this.villagerSystem, this.worldSystem)
this.treeSeedlingSystem = new TreeSeedlingSystem(this, this.adapter, this.worldSystem)
this.debugSystem = new DebugSystem(this, this.villagerSystem, this.worldSystem)
this.worldSystem.create()
this.renderPersistentObjects()
@@ -57,8 +61,12 @@ export class GameScene extends Phaser.Scene {
}
this.farmingSystem.create()
this.farmingSystem.onMessage = (msg) => this.events.emit('toast', msg)
this.farmingSystem.onToolChange = (tool, label) => this.events.emit('farmToolChanged', tool, label)
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()
this.villagerSystem.create()
this.villagerSystem.onMessage = (msg) => this.events.emit('toast', msg)
@@ -70,6 +78,8 @@ export class GameScene extends Phaser.Scene {
this.adapter.onAction = (action) => {
if (action.type === 'CHANGE_TILE') {
this.worldSystem.setTile(action.tileX, action.tileY, action.tile)
} else if (action.type === 'SPAWN_RESOURCE') {
this.resourceSystem.spawnResourcePublic(action.resource)
}
}
@@ -102,9 +112,17 @@ export class GameScene extends Phaser.Scene {
this.resourceSystem.update(delta)
this.farmingSystem.update(delta)
this.treeSeedlingSystem.update(delta)
this.villagerSystem.update(delta)
this.debugSystem.update()
// 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)
}
this.events.emit('cameraMoved', this.cameraSystem.getCenterTile())
this.buildingSystem.update()
@@ -144,6 +162,7 @@ export class GameScene extends Phaser.Scene {
this.resourceSystem.destroy()
this.buildingSystem.destroy()
this.farmingSystem.destroy()
this.treeSeedlingSystem.destroy()
this.villagerSystem.destroy()
}
}

View File

@@ -6,7 +6,7 @@ import { stateManager } from '../StateManager'
const ITEM_ICONS: Record<string, string> = {
wood: '🪵', stone: '🪨', wheat_seed: '🌱', carrot_seed: '🥕',
wheat: '🌾', carrot: '🧡',
wheat: '🌾', carrot: '🧡', tree_seed: '🌲',
}
export class UIScene extends Phaser.Scene {
@@ -119,14 +119,14 @@ export class UIScene extends Phaser.Scene {
/** Creates the stockpile panel in the top-right corner with item rows and population count. */
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.stockpilePanel = this.add.rectangle(x, y, 168, 187, 0x000000, 0.72).setOrigin(0, 0).setScrollFactor(0).setDepth(100)
this.stockpileTitleText = 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
const items = ['wood','stone','wheat_seed','carrot_seed','tree_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, '👥 Nisse: 0 / 0', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101)
this.popText = this.add.text(x + 10, y + 167, '👥 Nisse: 0 / 0', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101)
}
/** Refreshes all item quantities and colors in the stockpile panel. */