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"), ), ), }; }