Step 10L.2: effective range from origin
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
faa23d2945
commit
0a0189b3e7
|
|
@ -1135,3 +1135,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **Handling time becomes per model where set** (`ShipModel.handlingGameHoursPerTonne`, optional, defaulting to the game-wide value); upgrades scale the model's own base.
|
||||
- **Roles are content** (`role`), one line per model including the loaner, for the Market (10L.7).
|
||||
- **The new sprites are listed in the manifest as pending** (`file: null`) so the "every ship has a sprite in the manifest" test holds; they use the placeholder until art arrives (10L.10).
|
||||
|
||||
## Step 10L.2
|
||||
|
||||
- **The factor applies to the whole leg from the origin** (decision 0.5.78): `checkRoute` is the single range check, so dispatch, reposition, the board's eligibility and the map pick it up together. The destination's factor plays no part in that leg.
|
||||
- **The dispatch sheet quotes range from the origin**, rounded, e.g. "reaches 130M km from Moon", so the number matches what the check used.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
checkRoute,
|
||||
contractTripGameHours,
|
||||
isInShop,
|
||||
rangeFrom,
|
||||
pilotShare,
|
||||
dutyAfterTrip,
|
||||
LONG_HAUL_SKILL_ID,
|
||||
|
|
@ -92,7 +93,7 @@ export function explainProblems(
|
|||
if (!route.ok) {
|
||||
problems.push(
|
||||
route.reason === "out-of-range"
|
||||
? `Out of range right now (${model.name} reaches ${model.fuelRangeMillionKm}M km)`
|
||||
? `Out of range right now (${model.name} reaches ${Math.round(rangeFrom(model, location(contract.originId)))}M km from ${locationName(contract.originId)})`
|
||||
: INELIGIBLE_REASONS["cannot-land"],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ Spec: `memory-bank/change-requests/CR-011-settings-backup.md` (received as "CR-0
|
|||
Spec: `memory-bank/change-requests/CR-012-lighter-clipper-range.md` (received as "CR-009 … Phase 10I"). New decisions recorded as plan §0.5.77–0.5.81; §0.5.67 and §0.5.68 amended.
|
||||
|
||||
- [x] **Step 10L.1 — Content: range factors and new ships.** `Location.launchRangeFactor` (Earth 1, Moon 1.3, Mars 1.15, Ceres 1.3, Jupiter 1). `ShipModel.role` (all six) and optional `ShipModel.handlingGameHoursPerTonne` (the sim's effective stats now start from it). New models: **Lighter** (42,400 cr, 1.6M km/h, 80 t, range 10, overhead 2 h, handling 0.012 h/t, fuel 350/M km) and **Clipper** (61,600 cr, 6.5M km/h, 30 t, range 198, overhead 2 h, fuel 600/M km); archetypes `lighter`, `clipper`. Config `noStrandMarginGameHours` 48. Manifest lists `ship-lighter` / `ship-clipper` as pending art. Tests: the CR's orderings replace Step 2.3's (`ships.test.ts`), range factors (`locations.test.ts`), Lighter handling (`effective-stats.test.ts`).
|
||||
- [ ] Step 10L.2 — Sim: effective range from origin
|
||||
- [x] **Step 10L.2 — Sim: effective range from origin.** `rangeFrom(ship, from)` = range × origin's `launchRangeFactor`; `checkRoute` compares the leg against it (dispatch, reposition, board eligibility and the map all go through it). The dispatch sheet quotes range from the origin. Tests (`range-from.test.ts`): 100 → 130 from the Moon and 115 from Mars; a leg between 1× and 1.3× fails from Earth and passes from the Moon; with every factor 1 the result matches the old check over 16 years of samples.
|
||||
- [ ] Step 10L.3 — Sim: no-strand rule
|
||||
- [ ] Step 10L.4 — Content tuning: reach table
|
||||
- [ ] Step 10L.5 — Upgrade caps for the new lineup
|
||||
|
|
|
|||
|
|
@ -11,7 +11,18 @@ export function canLandAt(ship: ShipModel, location: Location): boolean {
|
|||
return LANDING_RANK[ship.landingClass] >= LANDING_RANK[location.landingRequirement];
|
||||
}
|
||||
|
||||
/** Whether a ship can fly from one location to another at a game time, or why not. */
|
||||
/**
|
||||
* A ship's range for a leg launched from `from` (decisions 0.5.77, 0.5.78):
|
||||
* its (effective) range times the origin's launch range factor.
|
||||
*/
|
||||
export function rangeFrom(ship: ShipModel, from: Location): number {
|
||||
return ship.fuelRangeMillionKm * from.launchRangeFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a ship can fly from one location to another at a game time, or why
|
||||
* not. Range is per leg, from the origin, at departure (decision 0.5.77).
|
||||
*/
|
||||
export function checkRoute(
|
||||
ship: ShipModel,
|
||||
from: Location,
|
||||
|
|
@ -19,7 +30,7 @@ export function checkRoute(
|
|||
gameTime: GameTime,
|
||||
): RouteCheck {
|
||||
if (!canLandAt(ship, to)) return { ok: false, reason: "cannot-land" };
|
||||
if (routeDistance(from, to, gameTime) > ship.fuelRangeMillionKm) {
|
||||
if (routeDistance(from, to, gameTime) > rangeFrom(ship, from)) {
|
||||
return { ok: false, reason: "out-of-range" };
|
||||
}
|
||||
return { ok: true };
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export { routeDistance } from "./distance";
|
|||
export { travelTimeGameHours } from "./travel";
|
||||
export { fuelCost } from "./fuel";
|
||||
|
||||
export { canLandAt, checkRoute } from "./eligibility";
|
||||
export { canLandAt, checkRoute, rangeFrom } from "./eligibility";
|
||||
export type { RouteCheck } from "./eligibility";
|
||||
|
||||
export { contractPayout } from "./contract";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import { content, type Location, type ShipModel } from "@deadhead/content";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { checkRoute, rangeFrom, realToGame, routeDistance } from "../src/index";
|
||||
|
||||
const loc = (id: string) => content.location(id)!;
|
||||
const courier = content.shipModel("courier")!;
|
||||
const withRange = (range: number): ShipModel => ({ ...courier, fuelRangeMillionKm: range });
|
||||
const start = realToGame(Date.parse(content.config.epoch.realStart), content.config);
|
||||
const MONTH = 30.44 * 24 * 3_600_000;
|
||||
const samples = Array.from({ length: 16 * 12 + 1 }, (_, i) => start + i * MONTH);
|
||||
|
||||
describe("range from the origin (Step 10L.2)", () => {
|
||||
it("stretches a 100M km range to 130 from the Moon and 115 from Mars", () => {
|
||||
expect(rangeFrom(withRange(100), loc("earth"))).toBe(100);
|
||||
expect(rangeFrom(withRange(100), loc("moon"))).toBeCloseTo(130);
|
||||
expect(rangeFrom(withRange(100), loc("mars"))).toBeCloseTo(115);
|
||||
});
|
||||
|
||||
it("passes a leg from the Moon that fails from Earth when it is between 1× and 1.3× range", () => {
|
||||
const t = samples[0]!;
|
||||
const d = routeDistance(loc("moon"), loc("mars"), t);
|
||||
const ship = withRange(d / 1.2);
|
||||
expect(routeDistance(loc("earth"), loc("mars"), t)).toBeGreaterThan(ship.fuelRangeMillionKm);
|
||||
expect(checkRoute(ship, loc("earth"), loc("mars"), t)).toEqual({
|
||||
ok: false,
|
||||
reason: "out-of-range",
|
||||
});
|
||||
expect(checkRoute(ship, loc("moon"), loc("mars"), t)).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("matches the old check exactly when every factor is 1", () => {
|
||||
const flat = (l: Location): Location => ({ ...l, launchRangeFactor: 1 });
|
||||
const ids = ["earth", "moon", "mars", "ceres-station", "jupiter-cloud-platform"];
|
||||
for (const range of [5, 198, 450, 1250]) {
|
||||
for (const from of ids) {
|
||||
for (const to of ids) {
|
||||
if (from === to) continue;
|
||||
for (const t of samples) {
|
||||
const old = routeDistance(loc(from), loc(to), t) <= range;
|
||||
expect(checkRoute(withRange(range), flat(loc(from)), flat(loc(to)), t).ok).toBe(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue