Step 3.15: add in-flight position and progress
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
039df0ac5b
commit
1472dc6786
|
|
@ -158,3 +158,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **Reposition check order:** ship idle → pilot idle → pilot assigned → not already there → bay at destination → landing → range → fuel credits. There are no skill checks (decision 0.5.24). Tutorial loaners cannot reposition.
|
||||
- **Contract and reposition records share one builder**; a reposition has `contract: null` and `expectedPayout: 0`.
|
||||
- **"Zero XP" is enforced at resolution** (Step 3.17), because the record has no XP field. That step's tests cover it.
|
||||
|
||||
## Step 3.15
|
||||
|
||||
- **Progress is linear in real time.** Takeoff and landing overhead are not modelled separately, so the ship moves at a constant rate along its line.
|
||||
- **A zero-length flight reports progress 1** instead of dividing by zero.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
- [x] **Step 3.12 — Dispatch validation.** `src/dispatch.ts`: `ShipState`, `PilotState`, `validateDispatch` (first failing reason, plan order), `tripFuelCost` (rounded). One test per failure reason plus tutorial cases.
|
||||
- [x] **Step 3.13 — Dispatch record creation.** `createDispatchRecord` in `src/dispatch.ts`: `DispatchRecord` with arrival from travel time, fuel cost, departure distance, a `tutorial` flag and a per-dispatch seed.
|
||||
- [x] **Step 3.14 — Reposition dispatch.** `validateReposition` and `createRepositionRecord` in `src/dispatch.ts`: empty flight to a player bay, range/landing/fuel only, zero payout. Zero XP is covered by resolution (Step 3.17).
|
||||
- [ ] Step 3.15 — In-flight position and progress
|
||||
- [x] **Step 3.15 — In-flight position and progress.** `src/flight.ts`: `dispatchProgress` (clamped 0–1) and `dispatchPosition` (straight line from origin at departure to destination at arrival).
|
||||
- [ ] Step 3.16 — XP and leveling
|
||||
- [ ] Step 3.17 — Dispatch resolution
|
||||
- [ ] Step 3.18 — Sim package public API and coverage
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import type { SimContent } from "./board";
|
||||
import { type RealTime, realToGame } from "./clock";
|
||||
import { type DispatchRecord, findLocation } from "./dispatch";
|
||||
import { locationPosition, type Position } from "./positions";
|
||||
|
||||
/** Fraction of the trip completed at a real time, clamped to [0, 1]. */
|
||||
export function dispatchProgress(record: DispatchRecord, now: RealTime): number {
|
||||
const duration = record.arrivalAt - record.departureAt;
|
||||
if (duration <= 0) return 1;
|
||||
return Math.min(1, Math.max(0, (now - record.departureAt) / duration));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ship position at a real time: a straight line from the origin's position at
|
||||
* departure to the destination's position at arrival.
|
||||
*/
|
||||
export function dispatchPosition(
|
||||
record: DispatchRecord,
|
||||
now: RealTime,
|
||||
content: SimContent,
|
||||
): Position {
|
||||
const { config } = content;
|
||||
const start = locationPosition(
|
||||
findLocation(record.originId, content),
|
||||
realToGame(record.departureAt, config),
|
||||
);
|
||||
const end = locationPosition(
|
||||
findLocation(record.destinationId, content),
|
||||
realToGame(record.arrivalAt, config),
|
||||
);
|
||||
const t = dispatchProgress(record, now);
|
||||
return { x: start.x + (end.x - start.x) * t, y: start.y + (end.y - start.y) * t };
|
||||
}
|
||||
|
|
@ -10,3 +10,4 @@ export * from "./board";
|
|||
export * from "./tutorial";
|
||||
export * from "./pilot";
|
||||
export * from "./dispatch";
|
||||
export * from "./flight";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import { content } from "@deadhead/content";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createRepositionRecord,
|
||||
dispatchPosition,
|
||||
dispatchProgress,
|
||||
locationPosition,
|
||||
realToGame,
|
||||
} from "../src/index";
|
||||
|
||||
const record = createRepositionRecord(
|
||||
{
|
||||
id: "r1",
|
||||
ship: { id: "s1", modelId: "courier", locationId: "earth", inFlight: false },
|
||||
pilot: { id: "p1", isAvatar: false, assignedShipId: "s1", inFlight: false, skills: {} },
|
||||
destinationId: "moon",
|
||||
now: Date.parse("2100-01-01T00:30:00Z"),
|
||||
worldSeed: 7,
|
||||
},
|
||||
content,
|
||||
);
|
||||
const midpoint = (record.departureAt + record.arrivalAt) / 2;
|
||||
|
||||
describe("in-flight progress", () => {
|
||||
it("is 0 at departure, 0.5 at the midpoint and 1 at arrival", () => {
|
||||
expect(dispatchProgress(record, record.departureAt)).toBe(0);
|
||||
expect(dispatchProgress(record, midpoint)).toBe(0.5);
|
||||
expect(dispatchProgress(record, record.arrivalAt)).toBe(1);
|
||||
});
|
||||
|
||||
it("stays clamped outside the flight", () => {
|
||||
expect(dispatchProgress(record, record.departureAt - 60_000)).toBe(0);
|
||||
expect(dispatchProgress(record, record.arrivalAt + 3_600_000)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("in-flight position", () => {
|
||||
const origin = locationPosition(
|
||||
content.location("earth")!,
|
||||
realToGame(record.departureAt, content.config),
|
||||
);
|
||||
const destination = locationPosition(
|
||||
content.location("moon")!,
|
||||
realToGame(record.arrivalAt, content.config),
|
||||
);
|
||||
|
||||
it("starts at the origin's position at departure", () => {
|
||||
expect(dispatchPosition(record, record.departureAt, content)).toEqual(origin);
|
||||
});
|
||||
|
||||
it("ends at the destination's position at arrival", () => {
|
||||
const end = dispatchPosition(record, record.arrivalAt, content);
|
||||
expect(end.x).toBeCloseTo(destination.x, 9);
|
||||
expect(end.y).toBeCloseTo(destination.y, 9);
|
||||
});
|
||||
|
||||
it("is halfway along the line at the midpoint", () => {
|
||||
const mid = dispatchPosition(record, midpoint, content);
|
||||
expect(mid.x).toBeCloseTo((origin.x + destination.x) / 2, 9);
|
||||
expect(mid.y).toBeCloseTo((origin.y + destination.y) / 2, 9);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue