Step 10G.2: contracts carry goods, shippers and Priority fields
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8a7b612bc7
commit
13f5b04ffb
|
|
@ -157,6 +157,10 @@ describe("contract dispatch", () => {
|
|||
const o = await owner();
|
||||
const contract = testContract({
|
||||
cargoTypeId: "heavy",
|
||||
goodId: "machine-parts",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 2,
|
||||
requiredSkill: { skillId: "heavy-cargo", level: 2 },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@ export function testContract(overrides: Partial<Contract> = {}): Contract {
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 10,
|
||||
payout: 2000,
|
||||
|
|
|
|||
|
|
@ -117,8 +117,8 @@ function LocationPanel({ locationId, onClose }: { locationId: string; onClose: (
|
|||
: `${formatMillionKm(distanceAt("earth", location.id, now))} from Earth`}
|
||||
</p>
|
||||
<p className="text-ink-400">
|
||||
Produces {location.cargoClassMix.produces.map(cargoName).join(", ") || "nothing"} · demands{" "}
|
||||
{location.cargoClassMix.demands.map(cargoName).join(", ") || "nothing"}
|
||||
Produces {location.cargoClassMix.produces.map(cargoName).join(", ") || "nothing"} ·
|
||||
demands {location.cargoClassMix.demands.map(cargoName).join(", ") || "nothing"}
|
||||
</p>
|
||||
<p className={hasBay ? "text-emerald-300" : "text-ink-400"}>
|
||||
{hasBay ? "Your HQ is here." : "No HQ here."}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ const heavyJob: Contract = {
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "heavy",
|
||||
goodId: "machine-parts",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 300,
|
||||
payout: 9000,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ const moonJob: Contract = {
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 10,
|
||||
payout: 900,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ const job = (
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: skill ? "high-value" : "standard",
|
||||
goodId: skill ? "medical-supplies" : "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: skill?.level ?? 0,
|
||||
massTonnes,
|
||||
payout: 1000,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ const marsJob: Contract = {
|
|||
originId: "earth",
|
||||
destinationId: "mars",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 15,
|
||||
payout: 500_000,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ const contract = (overrides: Partial<Contract>): Contract => ({
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 50,
|
||||
payout: 2000,
|
||||
|
|
@ -30,6 +34,10 @@ const boards: Record<string, Contract[]> = {
|
|||
id: "e2",
|
||||
destinationId: "mars",
|
||||
cargoTypeId: "heavy",
|
||||
goodId: "machine-parts",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 2,
|
||||
requiredSkill: { skillId: "heavy-cargo", level: 2 },
|
||||
requiredLongHaulLevel: 1,
|
||||
|
|
|
|||
|
|
@ -102,6 +102,10 @@ export function sampleTutorialBoard(
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes,
|
||||
payout,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ const contract = (id: string, massTonnes: number): Contract => ({
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes,
|
||||
payout: 1000 + massTonnes,
|
||||
|
|
|
|||
|
|
@ -203,3 +203,4 @@ Spec: `memory-bank/change-requests/CR-006-boards-that-fit.md`. New decisions rec
|
|||
|
||||
Spec: `memory-bank/change-requests/CR-007-cargo-goods.md` (supplied as "CR-005 / Phase 10E"; renumbered, see its implementation notes). New decisions recorded as plan §0.5.54–0.5.59. Baseline class mix before the change (1,000 Earth boards, all discovered): Standard 76.79 %, High-Value 23.21 %.
|
||||
- [x] **Step 10G.1 — Content: goods catalog.** `goodSchema` (id, name, `cargoClassId`, `massTonnes {min,max}`, `payMultiplier`, optional `tier` 1–5, `priorityEligible`, optional `priorityName`, `producedAt`, `demandedAt`; min ≤ max refinement) and `data/goods.ts` with all 29 goods from CR-007 §3, plus `goodsProducedAt`/`goodsDemandedAt`. Locations: `produces`/`demands` now derive from the catalog (good ids); the old class lists are kept as `cargoClassMix` (decision 0.5.57) and the sim's class selection and the UI's produces/demands now read them, so nothing visible changes yet; `shippers` added (≥ 1 each). Config: `priorityChance` 0.15, `priorityPayoutMultiplier` 1.5, `priorityReferenceShipModelId` "freighter", `priorityWindowFactor` 1. Bundle: `goods`, `good(id)`. Tests: `goods.test.ts` (schema, references, tiers by class, Standard good everywhere, ≥ 2 Standard goods each way Earth↔Moon, location lists in sync, Priority config), schema tests for Good and the new fields.
|
||||
- [x] **Step 10G.2 — Sim: contracts carry goods.** `Contract` gains `goodId`, `shipper`, `priority`, `deliveryWindowGameHours` (null unless Priority); the API `contractSchema` mirrors them now (the schema package's type-parity test requires it), so test fixtures across packages gained a good of their class. `contractPayout` = class base × good `payMultiplier` × tierMultiplier^tier × mass × distance × first-contact × Priority bonus. `buildContract` takes a good (the class comes from it) plus shipper and Priority fields; `tierFor` (0.5.55: Standard 0, High-Value/Dangerous the good's tier, Heavy the random roll or up to a fit pilot's level); `routeGoods` (0.5.54 fallback chain). Interim choice until 10G.3: the first route good in catalog order, the origin's first shipper, never Priority; fit jobs only use goods whose tier the pilot meets. `SimContent` gains `goods`. Tests: `contract.test.ts` (tiers from goods, Heavy 1–5, ×2 pay multiplier doubles payout, Priority = bonus × normal).
|
||||
|
|
|
|||
|
|
@ -85,6 +85,13 @@ export const contractSchema = z.object({
|
|||
originId: idSchema,
|
||||
destinationId: idSchema,
|
||||
cargoTypeId: idSchema,
|
||||
/** The named good (CR-007). */
|
||||
goodId: idSchema,
|
||||
/** Flavor: the shipping company (decision 0.5.58). */
|
||||
shipper: z.string().min(1),
|
||||
priority: z.boolean(),
|
||||
/** Priority only: the longest allowed trip in game hours (decision 0.5.56). */
|
||||
deliveryWindowGameHours: z.number().positive().nullable(),
|
||||
tier: z.number().int().min(0),
|
||||
massTonnes: z.number().positive(),
|
||||
payout: credits,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ const contract = {
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 12,
|
||||
payout: 480,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type {
|
||||
CargoType,
|
||||
GameConfig,
|
||||
Good,
|
||||
Location,
|
||||
ScriptedTutorialContract,
|
||||
ShipModel,
|
||||
|
|
@ -17,6 +18,7 @@ import { createRng, deriveSeed, type Rng, type Seed } from "./rng";
|
|||
export interface SimContent {
|
||||
locations: readonly Location[];
|
||||
cargoTypes: readonly CargoType[];
|
||||
goods: readonly Good[];
|
||||
skills: readonly Skill[];
|
||||
shipModels: readonly ShipModel[];
|
||||
scriptedTutorialContracts: readonly ScriptedTutorialContract[];
|
||||
|
|
@ -138,54 +140,85 @@ function fitJob(
|
|||
(c.requiredSkillId === null || skillLevel(skills, c.requiredSkillId) >= 1),
|
||||
);
|
||||
const cargoType = skilled.length > 0 ? rng.pick(skilled) : standardCargo(content);
|
||||
const tier =
|
||||
cargoType.requiredSkillId === null
|
||||
? 0
|
||||
: rng.int(1, skillLevel(skills, cargoType.requiredSkillId));
|
||||
// Only goods this pilot may carry: a good's own tier must not exceed their skill.
|
||||
const good = routeGoods(cargoType, origin, destination, content, (g) => {
|
||||
const skillId = cargoClassOf(g, content).requiredSkillId;
|
||||
return skillId === null || skillLevel(skills, skillId) >= (g.tier ?? 1);
|
||||
})[0]!;
|
||||
const skillId = cargoClassOf(good, content).requiredSkillId;
|
||||
const tier = tierFor(rng, good, content, skillId ? skillLevel(skills, skillId) : undefined);
|
||||
const massTonnes = rng.int(band.min, Math.min(band.max, model.cargoCapacityTonnes));
|
||||
return { destination, cargoType, tier, massTonnes };
|
||||
return { destination, good, tier, massTonnes };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Builds one contract from a chosen route, cargo, tier and mass. */
|
||||
export function cargoClassOf(good: Good, content: SimContent): CargoType {
|
||||
const cargoType = content.cargoTypes.find((c) => c.id === good.cargoClassId);
|
||||
if (!cargoType) throw new Error(`Unknown cargo class ${good.cargoClassId}`);
|
||||
return cargoType;
|
||||
}
|
||||
|
||||
/**
|
||||
* A contract's tier (decision 0.5.55): Standard is 0, High-Value and Dangerous
|
||||
* take the good's tier, and Heavy rolls 1 to `maxTier` (the skill's maximum,
|
||||
* or a pilot's level for a fit job).
|
||||
*/
|
||||
export function tierFor(rng: Rng, good: Good, content: SimContent, maxTier?: number): number {
|
||||
const cargoType = cargoClassOf(good, content);
|
||||
if (cargoType.requiredSkillId === null) return 0;
|
||||
if (good.tier !== undefined) return good.tier;
|
||||
return maxTier === undefined ? pickTier(rng, cargoType, content) : rng.int(1, maxTier);
|
||||
}
|
||||
|
||||
/** Builds one contract from a chosen route, good, tier and mass. */
|
||||
export function buildContract(
|
||||
args: {
|
||||
id: string;
|
||||
origin: Location;
|
||||
destination: Location;
|
||||
cargoType: CargoType;
|
||||
good: Good;
|
||||
tier: number;
|
||||
massTonnes: number;
|
||||
firstContact: boolean;
|
||||
periodIndex: number;
|
||||
shipper: string;
|
||||
priority: boolean;
|
||||
deliveryWindowGameHours: number | null;
|
||||
},
|
||||
content: SimContent,
|
||||
): Contract {
|
||||
const { config } = content;
|
||||
const cargoType = cargoClassOf(args.good, content);
|
||||
const offeredAt = args.periodIndex * boardPeriodMs(config);
|
||||
const distance = routeDistance(args.origin, args.destination, realToGame(offeredAt, config));
|
||||
return {
|
||||
id: args.id,
|
||||
originId: args.origin.id,
|
||||
destinationId: args.destination.id,
|
||||
cargoTypeId: args.cargoType.id,
|
||||
cargoTypeId: cargoType.id,
|
||||
goodId: args.good.id,
|
||||
shipper: args.shipper,
|
||||
priority: args.priority,
|
||||
deliveryWindowGameHours: args.priority ? args.deliveryWindowGameHours : null,
|
||||
tier: args.tier,
|
||||
massTonnes: args.massTonnes,
|
||||
payout: contractPayout(
|
||||
{
|
||||
cargoType: args.cargoType,
|
||||
cargoType,
|
||||
payMultiplier: args.good.payMultiplier,
|
||||
tier: args.tier,
|
||||
massTonnes: args.massTonnes,
|
||||
distanceMillionKm: distance,
|
||||
firstContact: args.firstContact,
|
||||
priority: args.priority,
|
||||
},
|
||||
config,
|
||||
),
|
||||
requiredSkill:
|
||||
args.cargoType.requiredSkillId === null
|
||||
cargoType.requiredSkillId === null
|
||||
? null
|
||||
: { skillId: args.cargoType.requiredSkillId, level: args.tier },
|
||||
: { skillId: cargoType.requiredSkillId, level: args.tier },
|
||||
requiredLongHaulLevel: requiredLongHaulLevel(distance, config),
|
||||
offeredAt,
|
||||
expiresAt: offeredAt + boardPeriodMs(config),
|
||||
|
|
@ -193,6 +226,33 @@ export function buildContract(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The goods a contract of this class can carry on this route (decision 0.5.54):
|
||||
* that class's goods made at the origin and wanted at the destination; else
|
||||
* Standard goods on the route; else any Standard good made at the origin.
|
||||
* `allow` filters out goods a crew can't take (fit jobs).
|
||||
*/
|
||||
export function routeGoods(
|
||||
cargoType: CargoType,
|
||||
origin: Location,
|
||||
destination: Location,
|
||||
content: SimContent,
|
||||
allow: (good: Good) => boolean = () => true,
|
||||
): Good[] {
|
||||
const made = content.goods.filter((g) => g.producedAt.includes(origin.id) && allow(g));
|
||||
const onRoute = made.filter((g) => g.demandedAt.includes(destination.id));
|
||||
const ofClass = onRoute.filter((g) => g.cargoClassId === cargoType.id);
|
||||
if (ofClass.length > 0) return ofClass;
|
||||
const standardId = standardCargo(content).id;
|
||||
const standardOnRoute = onRoute.filter((g) => g.cargoClassId === standardId);
|
||||
if (standardOnRoute.length > 0) return standardOnRoute;
|
||||
const anyStandard = content.goods.filter(
|
||||
(g) => g.producedAt.includes(origin.id) && g.cargoClassId === standardId,
|
||||
);
|
||||
if (anyStandard.length === 0) throw new Error(`${origin.id} makes no Standard good`);
|
||||
return anyStandard;
|
||||
}
|
||||
|
||||
function pickCargo(rng: Rng, origin: Location, destination: Location, content: SimContent) {
|
||||
const matching = content.cargoTypes.filter(
|
||||
(c) =>
|
||||
|
|
@ -239,7 +299,19 @@ export function generateContractBoard(input: BoardInput, content: SimContent): C
|
|||
const fit =
|
||||
kind === "fit" ? fitJob(rng, band, crews, origin, discovered, offeredAt, content) : null;
|
||||
if (fit) {
|
||||
return buildContract({ id, origin, ...fit, firstContact: false, periodIndex }, content);
|
||||
return buildContract(
|
||||
{
|
||||
id,
|
||||
origin,
|
||||
...fit,
|
||||
firstContact: false,
|
||||
periodIndex,
|
||||
shipper: origin.shippers[0]!,
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
},
|
||||
content,
|
||||
);
|
||||
}
|
||||
const guaranteed = kind !== "open";
|
||||
const plain = guaranteed && discovered.length > 0;
|
||||
|
|
@ -249,16 +321,20 @@ export function generateContractBoard(input: BoardInput, content: SimContent): C
|
|||
(discovered.length === 0 || rng.next() < config.firstContactChance);
|
||||
const destination = rng.pick(firstContact ? undiscovered : discovered);
|
||||
const cargoType = plain ? standardCargo(content) : pickCargo(rng, origin, destination, content);
|
||||
const good = routeGoods(cargoType, origin, destination, content)[0]!;
|
||||
return buildContract(
|
||||
{
|
||||
id,
|
||||
origin,
|
||||
destination,
|
||||
cargoType,
|
||||
tier: pickTier(rng, cargoType, content),
|
||||
good,
|
||||
tier: tierFor(rng, good, content),
|
||||
massTonnes: rng.int(band.min, band.max),
|
||||
firstContact,
|
||||
periodIndex,
|
||||
shipper: origin.shippers[0]!,
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
},
|
||||
content,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,8 +5,20 @@ export interface Contract {
|
|||
id: string;
|
||||
originId: string;
|
||||
destinationId: string;
|
||||
/** The cargo class (decision 0.5.6). */
|
||||
cargoTypeId: string;
|
||||
/** 0 for Standard; 1–5 otherwise, and equal to the required cargo-skill level (decision 0.5.6). */
|
||||
/** The named good (CR-007, decision 0.5.54); its class is `cargoTypeId`. */
|
||||
goodId: string;
|
||||
/** Flavor: the company shipping it, from the origin's list (decision 0.5.58). */
|
||||
shipper: string;
|
||||
/** A Priority contract must arrive within its delivery window (decision 0.5.56). */
|
||||
priority: boolean;
|
||||
/** Priority only: the longest trip allowed, in game hours; null otherwise. */
|
||||
deliveryWindowGameHours: number | null;
|
||||
/**
|
||||
* 0 for Standard; 1–5 otherwise, and equal to the required cargo-skill level
|
||||
* (decision 0.5.6). High-Value and Dangerous take it from the good (0.5.55).
|
||||
*/
|
||||
tier: number;
|
||||
massTonnes: number;
|
||||
/** Credits, fixed at offer time (decision 0.5.11). */
|
||||
|
|
@ -24,24 +36,30 @@ export interface Contract {
|
|||
|
||||
export interface PayoutInput {
|
||||
cargoType: CargoType;
|
||||
/** The good's pay multiplier (CR-007). */
|
||||
payMultiplier: number;
|
||||
tier: number;
|
||||
massTonnes: number;
|
||||
/** Route distance at offer time. */
|
||||
distanceMillionKm: number;
|
||||
firstContact: boolean;
|
||||
priority: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payout in whole credits: base pay per tonne per million km × tierMultiplier^tier
|
||||
* × mass × distance, × the first-contact multiplier when flagged.
|
||||
* Payout in whole credits: class base pay per tonne per million km × the good's
|
||||
* pay multiplier × tierMultiplier^tier × mass × distance, × the first-contact
|
||||
* multiplier and the Priority bonus when flagged (CR-007).
|
||||
*/
|
||||
export function contractPayout(input: PayoutInput, config: GameConfig): number {
|
||||
const { cargoType, tier, massTonnes, distanceMillionKm, firstContact } = input;
|
||||
const { cargoType, payMultiplier, tier, massTonnes, distanceMillionKm, firstContact } = input;
|
||||
const pay =
|
||||
cargoType.basePayPerTonnePerMillionKm *
|
||||
payMultiplier *
|
||||
cargoType.tierPayMultiplier ** tier *
|
||||
massTonnes *
|
||||
distanceMillionKm *
|
||||
(firstContact ? config.firstContactPayoutMultiplier : 1);
|
||||
(firstContact ? config.firstContactPayoutMultiplier : 1) *
|
||||
(input.priority ? config.priorityPayoutMultiplier : 1);
|
||||
return Math.round(pay);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Location, ShipModel } from "@deadhead/content";
|
||||
import { buildContract, standardCargo, type SimContent } from "./board";
|
||||
import { buildContract, routeGoods, standardCargo, type SimContent } from "./board";
|
||||
import type { Contract } from "./contract";
|
||||
|
||||
/** The tutorial loaner: the content's company ship (decision 0.5.29). */
|
||||
|
|
@ -48,16 +48,20 @@ export function generateTutorialBoard(input: TutorialBoardInput, content: SimCon
|
|||
? content.cargoTypes.find((c) => c.id === special.cargoTypeId)
|
||||
: standardCargo(content);
|
||||
if (!cargoType) throw new Error(`Unknown cargo ${special?.cargoTypeId}`);
|
||||
const good = routeGoods(cargoType, origin, destination, content)[0]!;
|
||||
const contract = buildContract(
|
||||
{
|
||||
id: `tutorial:${loanerLocationId}:${run}:${periodIndex}:${i}`,
|
||||
origin,
|
||||
destination,
|
||||
cargoType,
|
||||
good,
|
||||
tier: 0,
|
||||
massTonnes,
|
||||
firstContact: false,
|
||||
periodIndex,
|
||||
shipper: origin.shippers[0]!,
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
},
|
||||
content,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { content } from "@deadhead/content";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { contractPayout, type PayoutInput } from "../src/index";
|
||||
import { tierFor } from "../src/board";
|
||||
import { contractPayout, createRng, type PayoutInput } from "../src/index";
|
||||
|
||||
const config = content.config;
|
||||
const standard = content.cargoType("standard")!;
|
||||
|
|
@ -8,10 +9,12 @@ const dangerous = content.cargoType("dangerous")!;
|
|||
|
||||
const base: PayoutInput = {
|
||||
cargoType: standard,
|
||||
payMultiplier: 1,
|
||||
tier: 0,
|
||||
massTonnes: 10,
|
||||
distanceMillionKm: 100,
|
||||
firstContact: false,
|
||||
priority: false,
|
||||
};
|
||||
|
||||
describe("contract payout", () => {
|
||||
|
|
@ -50,3 +53,35 @@ describe("contract payout", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("contracts carry goods (CR-007)", () => {
|
||||
const rng = createRng(42);
|
||||
|
||||
it("takes the tier of a High-Value or Dangerous contract from its good", () => {
|
||||
for (const good of content.goods.filter(
|
||||
(g) => g.cargoClassId === "high-value" || g.cargoClassId === "dangerous",
|
||||
)) {
|
||||
expect(tierFor(rng, good, content)).toBe(good.tier);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the random 1–5 tier for Heavy goods and tier 0 for Standard", () => {
|
||||
const parts = content.good("machine-parts")!;
|
||||
const seen = new Set<number>();
|
||||
for (let i = 0; i < 200; i++) seen.add(tierFor(rng, parts, content));
|
||||
expect([...seen].sort()).toEqual([1, 2, 3, 4, 5]);
|
||||
expect(tierFor(rng, content.good("mail")!, content)).toBe(0);
|
||||
});
|
||||
|
||||
it("doubles the payout when the good's pay multiplier doubles", () => {
|
||||
expect(contractPayout({ ...base, payMultiplier: 2 }, config)).toBe(
|
||||
2 * contractPayout(base, config),
|
||||
);
|
||||
});
|
||||
|
||||
it("pays a Priority contract exactly the bonus times an identical normal one", () => {
|
||||
expect(contractPayout({ ...base, priority: true }, config)).toBe(
|
||||
contractPayout(base, config) * config.priorityPayoutMultiplier,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ const contract: Contract = {
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 10,
|
||||
payout: 2000,
|
||||
|
|
@ -99,6 +103,10 @@ describe("dispatch validation", () => {
|
|||
const heavy = {
|
||||
...contract,
|
||||
cargoTypeId: "heavy",
|
||||
goodId: "machine-parts",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 1,
|
||||
requiredSkill: { skillId: "heavy-cargo", level: 1 },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ const contract: Contract = {
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 10,
|
||||
payout: 2000,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ describe("resolution with a loan", () => {
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 10,
|
||||
payout: 2000,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ const shortStandard: Contract = {
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 10,
|
||||
payout: 1000,
|
||||
|
|
@ -18,6 +22,10 @@ const shortStandard: Contract = {
|
|||
const heavyTier2: Contract = {
|
||||
...shortStandard,
|
||||
cargoTypeId: "heavy",
|
||||
goodId: "machine-parts",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 2,
|
||||
requiredSkill: { skillId: "heavy-cargo", level: 2 },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ const contract: Contract = {
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 10,
|
||||
payout: 2000,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ const contract: Contract = {
|
|||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 10,
|
||||
payout: 2000,
|
||||
|
|
|
|||
|
|
@ -29,15 +29,17 @@ function distanceOf(contract: Contract): number {
|
|||
);
|
||||
}
|
||||
|
||||
/** The normal (non-tutorial) payout for the same Standard cargo and route. */
|
||||
/** The normal (non-tutorial) payout for the same Standard good and route. */
|
||||
function normalPayout(contract: Contract): number {
|
||||
return contractPayout(
|
||||
{
|
||||
cargoType: content.cargoType("standard")!,
|
||||
payMultiplier: content.good(contract.goodId)!.payMultiplier,
|
||||
tier: 0,
|
||||
massTonnes: contract.massTonnes,
|
||||
distanceMillionKm: distanceOf(contract),
|
||||
firstContact: false,
|
||||
priority: false,
|
||||
},
|
||||
config,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue