✨ 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:
131
src/systems/TreeSeedlingSystem.ts
Normal file
131
src/systems/TreeSeedlingSystem.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import Phaser from 'phaser'
|
||||
import { TILE_SIZE, TREE_SEEDLING_STAGE_MS } from '../config'
|
||||
import { TileType, PLANTABLE_TILES } from '../types'
|
||||
import type { TreeSeedlingState } from '../types'
|
||||
import { stateManager } from '../StateManager'
|
||||
import type { LocalAdapter } from '../NetworkAdapter'
|
||||
import type { WorldSystem } from './WorldSystem'
|
||||
|
||||
export class TreeSeedlingSystem {
|
||||
private scene: Phaser.Scene
|
||||
private adapter: LocalAdapter
|
||||
private worldSystem: WorldSystem
|
||||
private sprites = new Map<string, Phaser.GameObjects.Image>()
|
||||
|
||||
/**
|
||||
* @param scene - The Phaser scene this system belongs to
|
||||
* @param adapter - Network adapter for dispatching state actions
|
||||
* @param worldSystem - Used to refresh the terrain canvas when a seedling matures
|
||||
*/
|
||||
constructor(scene: Phaser.Scene, adapter: LocalAdapter, worldSystem: WorldSystem) {
|
||||
this.scene = scene
|
||||
this.adapter = adapter
|
||||
this.worldSystem = worldSystem
|
||||
}
|
||||
|
||||
/** Spawns sprites for all seedlings that exist in the saved state. */
|
||||
create(): void {
|
||||
const state = stateManager.getState()
|
||||
for (const s of Object.values(state.world.treeSeedlings)) {
|
||||
this.spawnSprite(s)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks all seedling growth timers and handles stage changes.
|
||||
* Stage 0→1: updates the sprite to the sapling texture.
|
||||
* Stage 1→2: removes the seedling, spawns a tree resource, and updates the terrain canvas.
|
||||
* @param delta - Frame delta in milliseconds
|
||||
*/
|
||||
update(delta: number): void {
|
||||
const advanced = stateManager.tickSeedlings(delta)
|
||||
for (const id of advanced) {
|
||||
const state = stateManager.getState()
|
||||
const seedling = state.world.treeSeedlings[id]
|
||||
if (!seedling) continue
|
||||
|
||||
if (seedling.stage === 2) {
|
||||
// Fully mature: become a FOREST tile and a real tree resource
|
||||
const { tileX, tileY } = seedling
|
||||
this.removeSprite(id)
|
||||
this.adapter.send({ type: 'REMOVE_TREE_SEEDLING', seedlingId: id })
|
||||
this.adapter.send({ type: 'CHANGE_TILE', tileX, tileY, tile: TileType.FOREST })
|
||||
this.worldSystem.refreshTerrainTile(tileX, tileY, TileType.FOREST)
|
||||
|
||||
const resourceId = `tree_grown_${tileX}_${tileY}_${Date.now()}`
|
||||
this.adapter.send({
|
||||
type: 'SPAWN_RESOURCE',
|
||||
resource: { id: resourceId, tileX, tileY, kind: 'tree', hp: 3 },
|
||||
})
|
||||
} else {
|
||||
// Stage 0→1: update sprite to sapling
|
||||
const sprite = this.sprites.get(id)
|
||||
if (sprite) sprite.setTexture(`seedling_${seedling.stage}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to plant a tree seedling on a grass tile.
|
||||
* Validates that the stockpile has at least one tree_seed, the tile type is
|
||||
* plantable (GRASS or DARK_GRASS), and no other object occupies the tile.
|
||||
* @param tileX - Target tile column
|
||||
* @param tileY - Target tile row
|
||||
* @param underlyingTile - The current tile type (stored on the seedling for later restoration)
|
||||
* @returns true if the seedling was planted, false if validation failed
|
||||
*/
|
||||
plantSeedling(tileX: number, tileY: number, underlyingTile: TileType): boolean {
|
||||
const state = stateManager.getState()
|
||||
|
||||
if ((state.world.stockpile.tree_seed ?? 0) <= 0) return false
|
||||
if (!PLANTABLE_TILES.has(underlyingTile)) return false
|
||||
|
||||
const occupied =
|
||||
Object.values(state.world.resources).some(r => r.tileX === tileX && r.tileY === tileY) ||
|
||||
Object.values(state.world.buildings).some(b => b.tileX === tileX && b.tileY === tileY) ||
|
||||
Object.values(state.world.crops).some(c => c.tileX === tileX && c.tileY === tileY) ||
|
||||
Object.values(state.world.treeSeedlings).some(s => s.tileX === tileX && s.tileY === tileY)
|
||||
|
||||
if (occupied) return false
|
||||
|
||||
const id = `seedling_${tileX}_${tileY}_${Date.now()}`
|
||||
const seedling: TreeSeedlingState = {
|
||||
id, tileX, tileY,
|
||||
stage: 0,
|
||||
stageTimerMs: TREE_SEEDLING_STAGE_MS,
|
||||
underlyingTile,
|
||||
}
|
||||
|
||||
this.adapter.send({ type: 'PLANT_TREE_SEED', seedling })
|
||||
this.spawnSprite(seedling)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and registers the sprite for a seedling.
|
||||
* @param s - Seedling state to render
|
||||
*/
|
||||
private spawnSprite(s: TreeSeedlingState): void {
|
||||
const x = (s.tileX + 0.5) * TILE_SIZE
|
||||
const y = (s.tileY + 0.5) * TILE_SIZE
|
||||
const key = `seedling_${Math.min(s.stage, 2)}`
|
||||
const sprite = this.scene.add.image(x, y, key)
|
||||
.setOrigin(0.5, 0.85)
|
||||
.setDepth(5)
|
||||
this.sprites.set(s.id, sprite)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the sprite for a seedling and removes it from the registry.
|
||||
* @param id - Seedling ID
|
||||
*/
|
||||
private removeSprite(id: string): void {
|
||||
const s = this.sprites.get(id)
|
||||
if (s) { s.destroy(); this.sprites.delete(id) }
|
||||
}
|
||||
|
||||
/** Destroys all seedling sprites and clears the registry. */
|
||||
destroy(): void {
|
||||
for (const id of [...this.sprites.keys()]) this.removeSprite(id)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user