✨ 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:
@@ -1,5 +1,6 @@
|
||||
import { SAVE_KEY, WORLD_TILES, CROP_CONFIGS } from './config'
|
||||
import { SAVE_KEY, WORLD_TILES, CROP_CONFIGS, TREE_SEEDLING_STAGE_MS, TILE_RECOVERY_MS } from './config'
|
||||
import type { GameStateData, GameAction, PlayerState, WorldState, ItemId } from './types'
|
||||
import { TileType } from './types'
|
||||
|
||||
const DEFAULT_PLAYER: PlayerState = {
|
||||
id: 'player1',
|
||||
@@ -15,13 +16,15 @@ function makeEmptyWorld(seed: number): WorldState {
|
||||
buildings: {},
|
||||
crops: {},
|
||||
villagers: {},
|
||||
stockpile: { wood: 10, stone: 5, wheat_seed: 10, carrot_seed: 5, wheat: 0, carrot: 0 },
|
||||
stockpile: { wood: 10, stone: 5, wheat_seed: 10, carrot_seed: 5, wheat: 0, carrot: 0, tree_seed: 5 },
|
||||
treeSeedlings: {},
|
||||
tileRecovery: {},
|
||||
}
|
||||
}
|
||||
|
||||
function makeDefaultState(): GameStateData {
|
||||
return {
|
||||
version: 4,
|
||||
version: 5,
|
||||
world: makeEmptyWorld(Math.floor(Math.random() * 999999)),
|
||||
player: { ...DEFAULT_PLAYER, inventory: { ...DEFAULT_PLAYER.inventory } },
|
||||
}
|
||||
@@ -146,6 +149,26 @@ class StateManager {
|
||||
if (v) v.priorities = { ...action.priorities }
|
||||
break
|
||||
}
|
||||
|
||||
case 'PLANT_TREE_SEED': {
|
||||
w.treeSeedlings[action.seedling.id] = { ...action.seedling }
|
||||
w.stockpile.tree_seed = Math.max(0, (w.stockpile.tree_seed ?? 0) - 1)
|
||||
// Cancel any tile recovery on this tile
|
||||
delete w.tileRecovery[`${action.seedling.tileX},${action.seedling.tileY}`]
|
||||
break
|
||||
}
|
||||
|
||||
case 'REMOVE_TREE_SEEDLING':
|
||||
delete w.treeSeedlings[action.seedlingId]
|
||||
break
|
||||
|
||||
case 'SPAWN_RESOURCE':
|
||||
w.resources[action.resource.id] = { ...action.resource }
|
||||
break
|
||||
|
||||
case 'TILE_RECOVERY_START':
|
||||
w.tileRecovery[`${action.tileX},${action.tileY}`] = TILE_RECOVERY_MS
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +186,47 @@ class StateManager {
|
||||
return advanced
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances all tree-seedling growth timers.
|
||||
* Returns IDs of seedlings that have reached stage 2 (ready to mature into a tree).
|
||||
* @param delta - Frame delta in milliseconds
|
||||
* @returns Array of seedling IDs that are now mature
|
||||
*/
|
||||
tickSeedlings(delta: number): string[] {
|
||||
const advanced: string[] = []
|
||||
for (const s of Object.values(this.state.world.treeSeedlings)) {
|
||||
s.stageTimerMs -= delta
|
||||
if (s.stageTimerMs <= 0) {
|
||||
s.stage = Math.min(s.stage + 1, 2)
|
||||
s.stageTimerMs = TREE_SEEDLING_STAGE_MS
|
||||
advanced.push(s.id)
|
||||
}
|
||||
}
|
||||
return advanced
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks tile-recovery timers.
|
||||
* Returns keys ("tileX,tileY") of tiles that have now recovered back to GRASS.
|
||||
* @param delta - Frame delta in milliseconds
|
||||
* @returns Array of recovered tile keys
|
||||
*/
|
||||
tickTileRecovery(delta: number): string[] {
|
||||
const recovered: string[] = []
|
||||
const rec = this.state.world.tileRecovery
|
||||
for (const key of Object.keys(rec)) {
|
||||
rec[key] -= delta
|
||||
if (rec[key] <= 0) {
|
||||
delete rec[key]
|
||||
recovered.push(key)
|
||||
// Update tiles array directly (DARK_GRASS → GRASS)
|
||||
const [tx, ty] = key.split(',').map(Number)
|
||||
this.state.world.tiles[ty * WORLD_TILES + tx] = TileType.GRASS
|
||||
}
|
||||
}
|
||||
return recovered
|
||||
}
|
||||
|
||||
save(): void {
|
||||
try { localStorage.setItem(SAVE_KEY, JSON.stringify(this.state)) } catch (_) {}
|
||||
}
|
||||
@@ -172,10 +236,12 @@ class StateManager {
|
||||
const raw = localStorage.getItem(SAVE_KEY)
|
||||
if (!raw) return null
|
||||
const p = JSON.parse(raw) as GameStateData
|
||||
if (p.version !== 4) return null
|
||||
if (!p.world.crops) p.world.crops = {}
|
||||
if (!p.world.villagers) p.world.villagers = {}
|
||||
if (!p.world.stockpile) p.world.stockpile = {}
|
||||
if (p.version !== 5) return null
|
||||
if (!p.world.crops) p.world.crops = {}
|
||||
if (!p.world.villagers) p.world.villagers = {}
|
||||
if (!p.world.stockpile) p.world.stockpile = {}
|
||||
if (!p.world.treeSeedlings) p.world.treeSeedlings = {}
|
||||
if (!p.world.tileRecovery) p.world.tileRecovery = {}
|
||||
// Reset in-flight AI states to idle on load so runtime timers start fresh
|
||||
for (const v of Object.values(p.world.villagers)) {
|
||||
if (v.aiState === 'walking' || v.aiState === 'working') v.aiState = 'idle'
|
||||
|
||||
Reference in New Issue
Block a user