🎉 initial commit
This commit is contained in:
205
src/systems/FarmingSystem.ts
Normal file
205
src/systems/FarmingSystem.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user