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.
|
- **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`.
|
- **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.
|
- **"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.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.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).
|
- [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.16 — XP and leveling
|
||||||
- [ ] Step 3.17 — Dispatch resolution
|
- [ ] Step 3.17 — Dispatch resolution
|
||||||
- [ ] Step 3.18 — Sim package public API and coverage
|
- [ ] 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 "./tutorial";
|
||||||
export * from "./pilot";
|
export * from "./pilot";
|
||||||
export * from "./dispatch";
|
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