✨ Försterkreislauf: Setzlinge beim Fällen, Försterhaus, Förster-Job
- Gefällter Baum → 1–2 tree_seed im Stockpile (zufällig) - Neues Gebäude forester_hut (50 wood): Log-Hütten-Grafik, Klick öffnet Info-Panel - Zonenmarkierung: Edit-Zone-Tool, Radius 5 Tiles, halbtransparente Overlay-Anzeige - Neuer JobType 'forester': Nisse pflanzen Setzlinge auf markierten Zonen-Tiles - Chop-Priorisierung: Zonen-Bäume werden vor natürlichen Bäumen gefällt - Nisse-Panel & Info-Panel zeigen forester-Priorität-Button Closes #25 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import Phaser from 'phaser'
|
||||
import { TILE_SIZE, VILLAGER_SPEED, VILLAGER_SPAWN_INTERVAL, VILLAGER_WORK_TIMES, VILLAGER_NAMES } from '../config'
|
||||
import { TileType } from '../types'
|
||||
import { TILE_SIZE, VILLAGER_SPEED, VILLAGER_SPAWN_INTERVAL, VILLAGER_WORK_TIMES, VILLAGER_NAMES, WORLD_TILES } from '../config'
|
||||
import { TileType, PLANTABLE_TILES } from '../types'
|
||||
import type { VillagerState, VillagerJob, JobType, AIState, ItemId } from '../types'
|
||||
import { stateManager } from '../StateManager'
|
||||
import { findPath } from '../utils/pathfinding'
|
||||
@@ -40,6 +40,12 @@ export class VillagerSystem {
|
||||
|
||||
onMessage?: (msg: string) => void
|
||||
onNisseClick?: (villagerId: string) => void
|
||||
/**
|
||||
* Called when a Nisse completes a forester planting job.
|
||||
* GameScene wires this to TreeSeedlingSystem.plantSeedling so that the
|
||||
* seedling sprite is spawned alongside the state action.
|
||||
*/
|
||||
onPlantSeedling?: (tileX: number, tileY: number, tile: TileType) => void
|
||||
|
||||
/**
|
||||
* @param scene - The Phaser scene this system belongs to
|
||||
@@ -119,7 +125,7 @@ export class VillagerSystem {
|
||||
this.drawEnergyBar(rt.energyBar, v.x, v.y, v.energy)
|
||||
|
||||
// Job icon
|
||||
const icons: Record<string, string> = { chop: '🪓', mine: '⛏', farm: '🌾', '': '' }
|
||||
const icons: Record<string, string> = { chop: '🪓', mine: '⛏', farm: '🌾', forester: '🌲', '': '' }
|
||||
const jobType = v.aiState === 'sleeping' ? '💤' : (v.job ? (icons[v.job.type] ?? '') : '')
|
||||
rt.jobIcon.setText(jobType).setPosition(v.x + 10, v.y - 18)
|
||||
}
|
||||
@@ -278,7 +284,10 @@ export class VillagerSystem {
|
||||
this.adapter.send({ type: 'TILE_RECOVERY_START', tileX: res.tileX, tileY: res.tileY })
|
||||
this.worldSystem.removeResourceTile(res.tileX, res.tileY)
|
||||
this.resourceSystem.removeResource(job.targetId)
|
||||
this.addLog(v.id, '✓ Chopped tree (+2 wood)')
|
||||
// Chopping a tree yields 1–2 tree seeds in the stockpile
|
||||
const seeds = Math.random() < 0.5 ? 2 : 1
|
||||
this.adapter.send({ type: 'ADD_ITEMS', items: { tree_seed: seeds } })
|
||||
this.addLog(v.id, `✓ Chopped tree (+2 wood, +${seeds} tree seed)`)
|
||||
}
|
||||
} else if (job.type === 'mine') {
|
||||
const res = state.world.resources[job.targetId]
|
||||
@@ -297,6 +306,20 @@ export class VillagerSystem {
|
||||
this.adapter.send({ type: 'CHANGE_TILE', tileX: crop.tileX, tileY: crop.tileY, tile: 9 as any })
|
||||
this.addLog(v.id, `✓ Farmed ${crop.kind}`)
|
||||
}
|
||||
} else if (job.type === 'forester') {
|
||||
// Verify the tile is still empty and the stockpile still has seeds
|
||||
const tileType = state.world.tiles[job.tileY * WORLD_TILES + job.tileX] as TileType
|
||||
const hasSeeds = (state.world.stockpile.tree_seed ?? 0) > 0
|
||||
const tileOccupied =
|
||||
Object.values(state.world.resources).some(r => r.tileX === job.tileX && r.tileY === job.tileY) ||
|
||||
Object.values(state.world.buildings).some(b => b.tileX === job.tileX && b.tileY === job.tileY) ||
|
||||
Object.values(state.world.crops).some(c => c.tileX === job.tileX && c.tileY === job.tileY) ||
|
||||
Object.values(state.world.treeSeedlings).some(s => s.tileX === job.tileX && s.tileY === job.tileY)
|
||||
|
||||
if (hasSeeds && PLANTABLE_TILES.has(tileType) && !tileOccupied) {
|
||||
this.onPlantSeedling?.(job.tileX, job.tileY, tileType)
|
||||
this.addLog(v.id, `🌱 Planted seedling at (${job.tileX}, ${job.tileY})`)
|
||||
}
|
||||
}
|
||||
|
||||
// If the harvest produced nothing (resource already gone), clear the stale job
|
||||
@@ -337,6 +360,15 @@ export class VillagerSystem {
|
||||
* @param v - Villager state (used for position and priorities)
|
||||
* @returns The chosen job candidate, or null
|
||||
*/
|
||||
/**
|
||||
* Selects the best available job for a Nisse based on their priority settings.
|
||||
* Among jobs at the same priority level, the closest one wins.
|
||||
* For chop jobs, trees within a forester zone are preferred over natural trees —
|
||||
* natural trees are only offered when no forester-zone trees are available.
|
||||
* Returns null if no unclaimed job is available.
|
||||
* @param v - Villager state (used for position and priorities)
|
||||
* @returns The chosen job candidate, or null
|
||||
*/
|
||||
private pickJob(v: VillagerState): { type: JobType; targetId: string; tileX: number; tileY: number } | null {
|
||||
const state = stateManager.getState()
|
||||
const p = v.priorities
|
||||
@@ -348,14 +380,30 @@ export class VillagerSystem {
|
||||
const candidates: C[] = []
|
||||
|
||||
if (p.chop > 0) {
|
||||
// Build the set of all tiles belonging to forester zones for chop priority
|
||||
const zoneTiles = new Set<string>()
|
||||
for (const zone of Object.values(state.world.foresterZones)) {
|
||||
for (const key of zone.tiles) zoneTiles.add(key)
|
||||
}
|
||||
|
||||
const zoneChop: C[] = []
|
||||
const naturalChop: C[] = []
|
||||
|
||||
for (const res of Object.values(state.world.resources)) {
|
||||
if (res.kind !== 'tree' || this.claimed.has(res.id)) continue
|
||||
// Skip trees with no reachable neighbour — A* cannot enter an impassable goal
|
||||
// tile unless at least one passable neighbour exists to jump from.
|
||||
// Skip trees with no reachable neighbour — A* cannot reach them.
|
||||
if (!this.hasAdjacentPassable(res.tileX, res.tileY)) continue
|
||||
candidates.push({ type: 'chop', targetId: res.id, tileX: res.tileX, tileY: res.tileY, dist: dist(res.tileX, res.tileY), pri: p.chop })
|
||||
const c: C = { type: 'chop', targetId: res.id, tileX: res.tileX, tileY: res.tileY, dist: dist(res.tileX, res.tileY), pri: p.chop }
|
||||
if (zoneTiles.has(`${res.tileX},${res.tileY}`)) {
|
||||
zoneChop.push(c)
|
||||
} else {
|
||||
naturalChop.push(c)
|
||||
}
|
||||
}
|
||||
// Prefer zone trees; fall back to natural only when no zone trees are reachable.
|
||||
candidates.push(...(zoneChop.length > 0 ? zoneChop : naturalChop))
|
||||
}
|
||||
|
||||
if (p.mine > 0) {
|
||||
for (const res of Object.values(state.world.resources)) {
|
||||
if (res.kind !== 'rock' || this.claimed.has(res.id)) continue
|
||||
@@ -364,6 +412,7 @@ export class VillagerSystem {
|
||||
candidates.push({ type: 'mine', targetId: res.id, tileX: res.tileX, tileY: res.tileY, dist: dist(res.tileX, res.tileY), pri: p.mine })
|
||||
}
|
||||
}
|
||||
|
||||
if (p.farm > 0) {
|
||||
for (const crop of Object.values(state.world.crops)) {
|
||||
if (crop.stage < crop.maxStage || this.claimed.has(crop.id)) continue
|
||||
@@ -371,6 +420,28 @@ export class VillagerSystem {
|
||||
}
|
||||
}
|
||||
|
||||
if (p.forester > 0 && (state.world.stockpile.tree_seed ?? 0) > 0) {
|
||||
// Find empty plantable zone tiles to seed
|
||||
for (const zone of Object.values(state.world.foresterZones)) {
|
||||
for (const key of zone.tiles) {
|
||||
const [tx, ty] = key.split(',').map(Number)
|
||||
const targetId = `forester_tile_${tx}_${ty}`
|
||||
if (this.claimed.has(targetId)) continue
|
||||
// Skip if tile is not plantable
|
||||
const tileType = state.world.tiles[ty * WORLD_TILES + tx] as TileType
|
||||
if (!PLANTABLE_TILES.has(tileType)) continue
|
||||
// Skip if something occupies this tile
|
||||
const occupied =
|
||||
Object.values(state.world.resources).some(r => r.tileX === tx && r.tileY === ty) ||
|
||||
Object.values(state.world.buildings).some(b => b.tileX === tx && b.tileY === ty) ||
|
||||
Object.values(state.world.crops).some(c => c.tileX === tx && c.tileY === ty) ||
|
||||
Object.values(state.world.treeSeedlings).some(s => s.tileX === tx && s.tileY === ty)
|
||||
if (occupied) continue
|
||||
candidates.push({ type: 'forester', targetId, tileX: tx, tileY: ty, dist: dist(tx, ty), pri: p.forester })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) return null
|
||||
|
||||
// Lowest priority number wins; ties broken by distance
|
||||
@@ -481,7 +552,7 @@ export class VillagerSystem {
|
||||
y: (freeBed.tileY + 0.5) * TILE_SIZE,
|
||||
bedId: freeBed.id,
|
||||
job: null,
|
||||
priorities: { chop: 1, mine: 2, farm: 3 },
|
||||
priorities: { chop: 1, mine: 2, farm: 3, forester: 4 },
|
||||
energy: 100,
|
||||
aiState: 'idle',
|
||||
}
|
||||
@@ -558,7 +629,10 @@ export class VillagerSystem {
|
||||
const v = stateManager.getState().world.villagers[villagerId]
|
||||
if (!v) return '—'
|
||||
if (v.aiState === 'sleeping') return '💤 Sleeping'
|
||||
if (v.aiState === 'working' && v.job) return `⚒ ${v.job.type}ing`
|
||||
if (v.aiState === 'working' && v.job) {
|
||||
const label = v.job.type === 'forester' ? 'planting' : `${v.job.type}ing`
|
||||
return `⚒ ${label}`
|
||||
}
|
||||
if (v.aiState === 'walking' && v.job) return `🚶 → ${v.job.type}`
|
||||
if (v.aiState === 'walking') return '🚶 Walking'
|
||||
const carrying = v.job?.carrying
|
||||
|
||||
Reference in New Issue
Block a user