Files
nissefolk/src/systems/FarmingSystem.ts

205 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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