87 lines
3.5 KiB
TypeScript
87 lines
3.5 KiB
TypeScript
import type { Location, ShipModel } from "@deadhead/content";
|
||
import { buildContract, routeGoods, standardCargo, type SimContent } from "./board";
|
||
import { createRng, deriveSeed } from "./rng";
|
||
import type { Contract } from "./contract";
|
||
|
||
/** The tutorial loaner: the content's company ship (decision 0.5.29). */
|
||
export function loanerModel(content: SimContent): ShipModel {
|
||
const id = content.config.tutorialLoanerShipModelId;
|
||
const loaner = content.shipModels.find((s) => s.id === id);
|
||
if (!loaner) throw new Error(`Content has no loaner ship model "${id}"`);
|
||
return loaner;
|
||
}
|
||
|
||
export type TutorialLocationId = "earth" | "moon";
|
||
|
||
export interface TutorialBoardInput {
|
||
/** Where the loaner currently is; contracts start here (decision 0.5.3). */
|
||
loanerLocationId: TutorialLocationId;
|
||
/** Tutorial runs already flown; the board is for run `runsCompleted + 1`. */
|
||
runsCompleted: number;
|
||
periodIndex: number;
|
||
}
|
||
|
||
function location(id: string, content: SimContent): Location {
|
||
const found = content.locations.find((l) => l.id === id);
|
||
if (!found) throw new Error(`Unknown location ${id}`);
|
||
return found;
|
||
}
|
||
|
||
/**
|
||
* The tutorial "company board" (CR-001, Step 10A.3): three Standard contracts
|
||
* from the loaner's location to the other end of the Earth–Moon route, at the
|
||
* light, medium and full mass tiers, each paying the tutorial share. A scripted
|
||
* contract for the upcoming run (decision 0.5.33) replaces the medium slot.
|
||
* Each carries a Standard good made here and wanted there, preferring one whose
|
||
* range holds the tier's mass, else clamped to the good's range (0.5.59).
|
||
* Never Priority. The board is fully determined by its inputs.
|
||
*/
|
||
export function generateTutorialBoard(input: TutorialBoardInput, content: SimContent): Contract[] {
|
||
const { loanerLocationId, runsCompleted, periodIndex } = input;
|
||
const { config } = content;
|
||
const origin = location(loanerLocationId, content);
|
||
const destination = location(loanerLocationId === "earth" ? "moon" : "earth", content);
|
||
const tiers = config.tutorialMassTonnes;
|
||
const run = runsCompleted + 1;
|
||
const scripted = content.scriptedTutorialContracts.find((s) => s.run === run);
|
||
const rng = createRng(deriveSeed(0, `tutorial:${loanerLocationId}:${run}:${periodIndex}`));
|
||
|
||
const slot = (i: number, tierMass: number): Contract => {
|
||
const special = i === 1 ? scripted : undefined;
|
||
const cargoType = special
|
||
? content.cargoTypes.find((c) => c.id === special.cargoTypeId)
|
||
: standardCargo(content);
|
||
if (!cargoType) throw new Error(`Unknown cargo ${special?.cargoTypeId}`);
|
||
const goods = routeGoods(cargoType, origin, destination, content);
|
||
const holding = goods.filter(
|
||
(g) => g.massTonnes.min <= tierMass && tierMass <= g.massTonnes.max,
|
||
);
|
||
const good = rng.pick(holding.length > 0 ? holding : goods);
|
||
const massTonnes = Math.min(good.massTonnes.max, Math.max(good.massTonnes.min, tierMass));
|
||
const contract = buildContract(
|
||
{
|
||
id: `tutorial:${loanerLocationId}:${run}:${periodIndex}:${i}`,
|
||
origin,
|
||
destination,
|
||
good,
|
||
tier: 0,
|
||
massTonnes,
|
||
firstContact: false,
|
||
periodIndex,
|
||
shipper: rng.pick(origin.shippers),
|
||
priority: false,
|
||
deliveryWindowGameHours: null,
|
||
},
|
||
content,
|
||
);
|
||
const share = contract.payout * config.tutorialPayoutShare * (special?.payMultiplier ?? 1);
|
||
return {
|
||
...contract,
|
||
payout: Math.round(share),
|
||
...(special ? { scriptedId: special.id } : {}),
|
||
};
|
||
};
|
||
|
||
return [slot(0, tiers.light), slot(1, tiers.medium), slot(2, tiers.full)];
|
||
}
|