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>
131 lines
4.7 KiB
TypeScript
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)
|
|
}
|
|
}
|