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() /** * @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 }) 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(s.tileY + 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) } }