Files
nissefolk/src/systems/TreeSeedlingSystem.ts
tekki mariani cd171c859c fix depth sorting for world objects by tileY
Fixes #31. All trees, rocks, seedlings and buildings now use
tileY+5 as depth instead of a fixed value, so objects further
down the screen always render in front of objects above them
regardless of spawn order. Build ghost moved to depth 1000/1001.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:40:27 +00:00

131 lines
4.7 KiB
TypeScript

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 })
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)
}
}