101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
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';
|
|
}
|
|
}
|