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 = { 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() 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 cam = this.scene.cameras.main const worldX = cam.scrollX + ptr.x const worldY = cam.scrollY + ptr.y 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) } }