deadhead-space-freight-tycoon/apps/web/src/map/scene.ts

126 lines
4.1 KiB
TypeScript

import { content } from "@deadhead/content";
import type { Dispatch, PlayerState } from "@deadhead/schema";
import { bodyPosition, dispatchProgress, loanerModel, realToGame } from "@deadhead/sim";
import { locationMapPoint, radialScale, type MapPoint } from "./projection";
import { mapStyle } from "./style";
export interface SceneLocation {
id: string;
name: string;
/**
* Map position; for a child body (the Moon) its parent's position, since
* children are drawn at a fixed screen offset from it (decision 0.5.39).
*/
point: MapPoint;
/** The parent location of a child body, else null. */
parentId: string | null;
/** Map radius of its orbit around the Sun (the faint ring); null for moons. */
orbitRadius: number | null;
discovered: boolean;
hasBay: boolean;
/** Idle ships here (the tutorial loaner included), shown as a badge. */
idleShips: number;
/** Map art asset ids (CR-003). */
sprite: string;
badge: string | null;
}
export interface SceneFlight {
dispatchId: string;
/** The ship's id, or "loaner" for a tutorial run. */
shipId: string;
originId: string;
destinationId: string;
/** Origin at departure and destination at arrival (anchors; see `point`): the route line. */
from: MapPoint;
to: MapPoint;
/** Where the ship is now, on the route line. */
marker: MapPoint;
progress: number;
/** The flying ship's model and its map art asset id (CR-003). */
modelId: string;
sprite: string;
}
export interface Scene {
locations: SceneLocation[];
flights: SceneFlight[];
}
function location(id: string) {
const found = content.location(id);
if (!found) throw new Error(`Unknown location ${id}`);
return found;
}
/** The location whose map position a location is drawn from: its parent, if it has one. */
export function anchorLocation(id: string) {
return location(mapStyle.childOffsets[id]?.parentId ?? id);
}
/**
* A flight's route and current marker (Step 10.2). The marker moves along the
* straight route line by the sim's progress, in map space, so it always stays
* on the line however the map is scaled.
*/
export function flightMarker(dispatch: Dispatch, now: number, modelId: string): SceneFlight {
const from = locationMapPoint(
anchorLocation(dispatch.originId),
realToGame(dispatch.departureAt, content.config),
);
const to = locationMapPoint(
anchorLocation(dispatch.destinationId),
realToGame(dispatch.arrivalAt, content.config),
);
const progress = dispatchProgress(dispatch, now);
return {
dispatchId: dispatch.id,
shipId: dispatch.shipId,
originId: dispatch.originId,
destinationId: dispatch.destinationId,
from,
to,
marker: { x: from.x + (to.x - from.x) * progress, y: from.y + (to.y - from.y) * progress },
progress,
modelId,
sprite: content.shipModel(modelId)?.sprite ?? `ship-${modelId}`,
};
}
/** Everything the map draws at real time `now`, in map coordinates. */
export function buildScene(state: PlayerState, now: number): Scene {
const gameTime = realToGame(now, content.config);
const idleAt = (locationId: string) =>
state.ships.filter((s) => s.status === "idle" && s.locationId === locationId).length +
(state.tutorial && !state.tutorial.runInFlight && state.tutorial.loanerLocationId === locationId
? 1
: 0);
return {
locations: content.locations.map((loc) => {
const sun = bodyPosition(loc.body, gameTime);
return {
id: loc.id,
name: loc.name,
point: locationMapPoint(anchorLocation(loc.id), gameTime),
parentId: mapStyle.childOffsets[loc.id]?.parentId ?? null,
orbitRadius: loc.body === "moon" ? null : radialScale(Math.hypot(sun.x, sun.y)),
discovered: state.locations.some((l) => l.id === loc.id && l.discovered),
hasBay: state.bays.some((b) => b.locationId === loc.id),
idleShips: idleAt(loc.id),
sprite: loc.sprite,
badge: loc.badge ?? null,
};
}),
flights: state.activeDispatches.map((d) =>
flightMarker(
d,
now,
d.tutorial
? loanerModel(content).id
: (state.ships.find((s) => s.id === d.shipId)?.modelId ?? "courier"),
),
),
};
}