import { makeAutoObservable } from 'mobx'; import { DROP_COLLECT_ARC_HEIGHT, DROP_COLLECT_DURATION, DROP_EJECT_RADIUS, DROP_FALL_ARC_HEIGHT, DROP_FALL_DURATION, } from '../constants'; import { randomId } from '../id'; import type { DropPhase, Flight, ResourceDropData, ResourceType, Vec2 } from '../types'; /** A resource in flight — ejected from a chopped node or a killed rabbit, falling to a landing * spot, then (once the character gets close) flying into the inventory. Framework-free — no * React/DOM dependency. */ export class ResourceDrop { readonly id: string; resource: ResourceType; x: number; y: number; /** Visual height above ground during a flight, for the ballistic arc. 0 once landed. */ z: number; phase: DropPhase; flight?: Flight; constructor(resource: ResourceType, x: number, y: number, z: number, phase: DropPhase, flight: Flight | undefined, id = randomId('drop')) { this.id = id; this.resource = resource; this.x = x; this.y = y; this.z = z; this.phase = phase; this.flight = flight; makeAutoObservable(this, {}, { autoBind: true }); } /** Ejects a resource from `origin` on a ballistic arc to a random spot within a circle around * it; it only becomes pickable once it lands (see updateFlight). */ static eject(origin: Vec2, resource: ResourceType): ResourceDrop { const angle = Math.random() * Math.PI * 2; const landDistance = Math.random() * DROP_EJECT_RADIUS; const toX = origin.x + Math.cos(angle) * landDistance; const toY = origin.y + Math.sin(angle) * landDistance; return new ResourceDrop(resource, origin.x, origin.y, 0, 'falling', { fromX: origin.x, fromY: origin.y, toX, toY, elapsed: 0, duration: DROP_FALL_DURATION, arcHeight: DROP_FALL_ARC_HEIGHT, }); } /** Reconstructs a drop from saved data (a straight deep copy). */ static restore(data: ResourceDropData): ResourceDrop { return new ResourceDrop(data.resource, data.x, data.y, data.z, data.phase, data.flight ? { ...data.flight } : undefined, data.id); } /** Starts a landed drop flying into the inventory once the character gets close enough. */ startCollecting(target: Vec2): void { this.phase = 'collecting'; this.flight = { fromX: this.x, fromY: this.y, toX: target.x, toY: target.y, elapsed: 0, duration: DROP_COLLECT_DURATION, arcHeight: DROP_COLLECT_ARC_HEIGHT, }; } /** Advances an in-progress flight along its ballistic arc. Returns 'flying' while still en * route, 'landed' the instant a falling flight completes (or if there's no flight at all), or * 'collected' the instant a collecting flight reaches the character — the caller applies the * inventory gain and removes the drop only on 'collected'. */ updateFlight(deltaTime: number): 'flying' | 'landed' | 'collected' { if (this.phase === 'landed' || !this.flight) return 'landed'; const flight = this.flight; flight.elapsed += deltaTime; const t = Math.min(1, flight.elapsed / flight.duration); this.x = flight.fromX + (flight.toX - flight.fromX) * t; this.y = flight.fromY + (flight.toY - flight.fromY) * t; this.z = flight.arcHeight * 4 * t * (1 - t); if (t < 1) return 'flying'; if (this.phase === 'falling') { this.phase = 'landed'; this.flight = undefined; this.z = 0; return 'landed'; } return 'collected'; } }