Files
nissefolk/src/systems/FarmingSystem.ts

205 lines
7.7 KiB
TypeScript
Raw Normal View History

2026-03-20 08:11:31 +00:00
import Phaser from 'phaser'
import { TILE_SIZE, CROP_CONFIGS } from '../config'
import { TileType } from '../types'
import type { CropKind, CropState, ItemId } from '../types'
import { stateManager } from '../StateManager'
import type { LocalAdapter } from '../NetworkAdapter'
export type FarmingTool = 'none' | 'hoe' | 'wheat_seed' | 'carrot_seed' | 'water'
const TOOL_CYCLE: FarmingTool[] = ['none', 'hoe', 'wheat_seed', 'carrot_seed', 'water']
const TOOL_LABELS: Record<FarmingTool, string> = {
none: '— None',
hoe: '⛏ Hoe (till grass)',
wheat_seed: '🌾 Wheat Seeds',
carrot_seed: '🥕 Carrot Seeds',
water: '💧 Watering Can',
}
export class FarmingSystem {
private scene: Phaser.Scene
private adapter: LocalAdapter
private currentTool: FarmingTool = 'none'
private cropSprites = new Map<string, Phaser.GameObjects.Image>()
private toolKey!: Phaser.Input.Keyboard.Key
private clickCooldown = 0
private readonly COOLDOWN = 300
/** Emitted when the tool changes — pass (tool, label) */
onToolChange?: (tool: FarmingTool, label: string) => void
/** Emitted for toast notifications */
onMessage?: (msg: string) => void
constructor(scene: Phaser.Scene, adapter: LocalAdapter) {
this.scene = scene
this.adapter = adapter
}
create(): void {
// Restore crop sprites for any saved crops
const state = stateManager.getState()
for (const crop of Object.values(state.world.crops)) {
this.spawnCropSprite(crop)
}
this.toolKey = this.scene.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.F)
// Left-click to use current tool
this.scene.input.on('pointerdown', (ptr: Phaser.Input.Pointer) => {
if (this.currentTool === 'none') return
if (ptr.rightButtonDown()) { this.setTool('none'); return }
if (this.clickCooldown > 0) return
this.useToolAt(ptr)
this.clickCooldown = this.COOLDOWN
})
}
/** Called every frame. */
update(delta: number): void {
if (this.clickCooldown > 0) this.clickCooldown -= delta
// F key cycles through tools
if (Phaser.Input.Keyboard.JustDown(this.toolKey)) {
const idx = TOOL_CYCLE.indexOf(this.currentTool)
this.setTool(TOOL_CYCLE[(idx + 1) % TOOL_CYCLE.length])
}
// Tick crop growth
const leveled = stateManager.tickCrops(delta)
for (const id of leveled) this.refreshCropSprite(id)
}
getCurrentTool(): FarmingTool { return this.currentTool }
private setTool(tool: FarmingTool): void {
this.currentTool = tool
this.onToolChange?.(tool, TOOL_LABELS[tool])
}
// ─── Tool actions ─────────────────────────────────────────────────────────
private useToolAt(ptr: Phaser.Input.Pointer): void {
const worldX = ptr.worldX
const worldY = ptr.worldY
2026-03-20 08:11:31 +00:00
const tileX = Math.floor(worldX / TILE_SIZE)
const tileY = Math.floor(worldY / TILE_SIZE)
const state = stateManager.getState()
const tile = state.world.tiles[tileY * 512 + tileX] as TileType
if (this.currentTool === 'hoe') this.tillSoil(tileX, tileY, tile)
else if (this.currentTool === 'water') this.waterTile(tileX, tileY, tile)
else this.plantCrop(tileX, tileY, tile, this.currentTool.replace('_seed', '') as CropKind)
}
private tillSoil(tileX: number, tileY: number, tile: TileType): void {
if (tile !== TileType.GRASS && tile !== TileType.DARK_GRASS) {
this.onMessage?.('Hoe only works on grass!')
return
}
const state = stateManager.getState()
const blocked =
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)
if (blocked) { this.onMessage?.('Something is in the way!'); return }
this.adapter.send({ type: 'CHANGE_TILE', tileX, tileY, tile: TileType.TILLED_SOIL })
this.onMessage?.('Soil tilled ✓')
}
private plantCrop(tileX: number, tileY: number, tile: TileType, kind: CropKind): void {
if (tile !== TileType.TILLED_SOIL && tile !== TileType.WATERED_SOIL) {
this.onMessage?.('Plant on tilled soil!')
return
}
const state = stateManager.getState()
const seedItem: ItemId = `${kind}_seed` as ItemId
const have = state.world.stockpile[seedItem] ?? 0
if (have <= 0) { this.onMessage?.(`No ${kind} seeds left!`); return }
if (Object.values(state.world.crops).some(c => c.tileX === tileX && c.tileY === tileY)) {
this.onMessage?.('Already planted here!')
return
}
const cfg = CROP_CONFIGS[kind]
const crop: CropState = {
id: `crop_${tileX}_${tileY}_${Date.now()}`,
tileX, tileY, kind,
stage: 0, maxStage: cfg.stages,
stageTimerMs: cfg.stageTimeMs,
watered: tile === TileType.WATERED_SOIL,
}
this.adapter.send({ type: 'PLANT_CROP', crop, seedItem })
this.spawnCropSprite(crop)
this.onMessage?.(`${kind} seed planted! 🌱`)
}
private waterTile(tileX: number, tileY: number, tile: TileType): void {
if (tile !== TileType.TILLED_SOIL && tile !== TileType.WATERED_SOIL) {
this.onMessage?.('Water tilled soil!')
return
}
this.adapter.send({ type: 'CHANGE_TILE', tileX, tileY, tile: TileType.WATERED_SOIL })
const state = stateManager.getState()
const crop = Object.values(state.world.crops).find(c => c.tileX === tileX && c.tileY === tileY)
if (crop) this.adapter.send({ type: 'WATER_CROP', cropId: crop.id })
this.onMessage?.('Watered! (2× growth speed)')
}
harvestCrop(id: string): void {
const state = stateManager.getState()
const crop = state.world.crops[id]
if (!crop) return
const cfg = CROP_CONFIGS[crop.kind]
this.adapter.send({ type: 'HARVEST_CROP', cropId: id, rewards: cfg.rewards })
this.adapter.send({ type: 'CHANGE_TILE', tileX: crop.tileX, tileY: crop.tileY, tile: TileType.TILLED_SOIL })
this.removeCropSprite(id)
const rewardStr = Object.entries(cfg.rewards).map(([k, v]) => `+${v} ${k}`).join(', ')
this.onMessage?.(`${crop.kind} harvested! ${rewardStr}`)
}
// ─── Sprite management ────────────────────────────────────────────────────
private spawnCropSprite(crop: CropState): void {
const x = (crop.tileX + 0.5) * TILE_SIZE
const y = (crop.tileY + 0.5) * TILE_SIZE
const key = this.spriteKey(crop.kind, crop.stage, crop.maxStage)
const sprite = this.scene.add.image(x, y, key)
sprite.setOrigin(0.5, 0.85).setDepth(7)
this.cropSprites.set(crop.id, sprite)
}
private refreshCropSprite(cropId: string): void {
const sprite = this.cropSprites.get(cropId)
if (!sprite) return
const crop = stateManager.getState().world.crops[cropId]
if (!crop) return
sprite.setTexture(this.spriteKey(crop.kind, crop.stage, crop.maxStage))
// Subtle pop animation on growth
this.scene.tweens.add({
targets: sprite, scaleX: 1.25, scaleY: 1.25, duration: 80,
yoyo: true, ease: 'Back.easeOut',
})
}
/** Called by VillagerSystem when a villager harvests a crop */
public removeCropSpritePublic(id: string): void {
this.removeCropSprite(id)
}
private removeCropSprite(id: string): void {
const s = this.cropSprites.get(id)
if (s) { s.destroy(); this.cropSprites.delete(id) }
}
private spriteKey(kind: CropKind, stage: number, maxStage: number): string {
return `crop_${kind}_${Math.min(stage, maxStage)}`
}
destroy(): void {
for (const id of [...this.cropSprites.keys()]) this.removeCropSprite(id)
}
}