Step 27.1: content and save
CI / check (push) Has been cancelled
Details
CI / check (push) Has been cancelled
Details
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
49e7cca67a
commit
bd2a8de100
|
|
@ -1995,3 +1995,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **Numbering:** received as "CR-025 — Service Bookings and Auto-Repair" for "Phase 25"; stored as **CR-027**, built as **Phase 27** (its 25.n = 27.n), since CR-025 (Short Away Summary) and CR-026 (Random Events) exist. Its decisions CR-025.D1–D7 are **0.5.153–0.5.159**. Its references: CR-006 → CR-009, CR-011 → CR-014, CR-014 → CR-015, CR-019 → CR-021, CR-021 → CR-023, CR-022 → CR-024, CR-023 → CR-025, CR-024 → CR-026.
|
||||
- **All seven decisions are recorded**: §8 says "Add CR-025.D1–D6", but §6 lists D7 (no double waiting) too.
|
||||
- **No double waiting, with short repairs:** CR-023 counts the shop as rest only when it lasts a full rest; a repair can take 5 minutes, shorter than a 10-minute rest. Then the rest simply runs on to its end, which is still "the later of the two", so the CR's rule holds without changing CR-023's.
|
||||
|
||||
## Step 27.1
|
||||
|
||||
- **The booking lives on the ship** (one per ship, as the CR has one booking per ship), with its timings: `startedAt` once the work is in the shop (the booking clears at the shop end), `waitingSince` while it waits for funds at the HQ.
|
||||
- **One upgrade level per booking** (`upgradeId`: the next level of that upgrade when the work starts), as the CR allows one level per visit.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ describe("about and debug info (Step 10K.10)", () => {
|
|||
expect(text).toContain("Build: test");
|
||||
// CR-014: the save, its format version and where it is stored; no player id.
|
||||
expect(text).toContain("Save: player-1");
|
||||
expect(text).toContain("Save version: 11");
|
||||
expect(text).toContain("Save version: 12");
|
||||
expect(text).toContain("Storage: unknown");
|
||||
expect(text).not.toContain("Player:");
|
||||
expect(text).toMatch(/Game time: \d{4}-\d{2}-\d{2}T/);
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ The `routes` catalog is the list of engine commands (CR-014). Each entry has `re
|
|||
|
||||
## Save document (`packages/schema/src/save.ts`, CR-014)
|
||||
|
||||
One document per company on the device, validated on every load and save: `saveVersion` (currently 11; 1 → 2 added fuel tanks, CR-015; 2 → 3 put every ship on Manual refueling, CR-018; 3 → 4 dropped stored boards, CR-019; 4 → 5 added pilot `mode` and `orders` and the company's `dispatchOffice` and `npcSpendingFloor`, CR-021; 5 → 6 added pilot `nextDecisionAt` and `emptyPeriods` and dispatch `independent`, CR-021; 6 → 7 added `player.welcomeSeen`, true for existing companies, CR-022; 7 → 8 added ship `shopPilotId`, null for existing ships, CR-023; 8 → 9 added company `lastManualDecisionAt` (null until the engine's first catch-up sets it) and `sleepAfterHours`, CR-024; 9 → 10 added the `logbook`, player `awaySince`, `lastAbsence` and `readyGoalsLogged`, and ship `shopOpen`, turning resolved dispatches into entries, CR-025; 10 → 11 added ship `hullConditionPct` (100), `quietTrips` and `shopRepair`, dispatch `event` and `eventApplied` (null), company `firstEventDone` and `tripsSinceDeal`, CR-026), `saveId`, `lastTimeSeen` (the clock never goes below it), `player` (world seed, tutorial state, deal accepted), `company` (name, credits, loan, the Dispatch Office and spending floor, and since CR-024 the last manual decision and the sleep threshold), `ships` (with `upgrades` as a level map, shop fields including `shopPilotId` (the pilot waiting out the shop as rest, CR-023), and `fuelFu`, `fuelAvgPrice`, `refuelPolicy` since CR-015), `pilots`, `bays`, `discoveredLocations`, `contractBoards` (stored boards), `dispatches` (sim record, status, resolution, acknowledged), `acceptedContracts`, `claimedGoals`, `logbook` (CR-025: entries of kind `flight` (deliveries and deadheads, with payout, pilot pay, fuel used, loan repayment and XP), `refuel` (top-up, bought, pilot or emergency), `rest`, `upgrade`, `discovery`, `level-up` and `goal-ready`, each with time, pilot and ship; ids made from what they record; kept `logbookRetentionRealDays` (7) and pruned on save). Written by `packages/engine/src/game/logbook.ts` helpers from arrivals, refuelling, rest, shop ends (a catch-up event for every `shopOpen` ship), and goals becoming ready (after every catch-up event and command). It mirrors the old server's database tables without player ids; the stored contract, dispatch record and resolution are type-checked against the sim types. `migrateSave(raw)` steps an older save through `SAVE_MIGRATIONS` (`applyMigrations`) and validates it, returning a readable error instead of throwing.
|
||||
One document per company on the device, validated on every load and save: `saveVersion` (currently 12; 1 → 2 added fuel tanks, CR-015; 2 → 3 put every ship on Manual refueling, CR-018; 3 → 4 dropped stored boards, CR-019; 4 → 5 added pilot `mode` and `orders` and the company's `dispatchOffice` and `npcSpendingFloor`, CR-021; 5 → 6 added pilot `nextDecisionAt` and `emptyPeriods` and dispatch `independent`, CR-021; 6 → 7 added `player.welcomeSeen`, true for existing companies, CR-022; 7 → 8 added ship `shopPilotId`, null for existing ships, CR-023; 8 → 9 added company `lastManualDecisionAt` (null until the engine's first catch-up sets it) and `sleepAfterHours`, CR-024; 9 → 10 added the `logbook`, player `awaySince`, `lastAbsence` and `readyGoalsLogged`, and ship `shopOpen`, turning resolved dispatches into entries, CR-025; 10 → 11 added ship `hullConditionPct` (100), `quietTrips` and `shopRepair`, dispatch `event` and `eventApplied` (null), company `firstEventDone` and `tripsSinceDeal`, CR-026; 11 → 12 added ship `booking` (null) and `autoRepair` (on, 40%), CR-027), `saveId`, `lastTimeSeen` (the clock never goes below it), `player` (world seed, tutorial state, deal accepted), `company` (name, credits, loan, the Dispatch Office and spending floor, and since CR-024 the last manual decision and the sleep threshold), `ships` (with `upgrades` as a level map, shop fields including `shopPilotId` (the pilot waiting out the shop as rest, CR-023), and `fuelFu`, `fuelAvgPrice`, `refuelPolicy` since CR-015), `pilots`, `bays`, `discoveredLocations`, `contractBoards` (stored boards), `dispatches` (sim record, status, resolution, acknowledged), `acceptedContracts`, `claimedGoals`, `logbook` (CR-025: entries of kind `flight` (deliveries and deadheads, with payout, pilot pay, fuel used, loan repayment and XP), `refuel` (top-up, bought, pilot or emergency), `rest`, `upgrade`, `discovery`, `level-up` and `goal-ready`, each with time, pilot and ship; ids made from what they record; kept `logbookRetentionRealDays` (7) and pruned on save). Written by `packages/engine/src/game/logbook.ts` helpers from arrivals, refuelling, rest, shop ends (a catch-up event for every `shopOpen` ship), and goals becoming ready (after every catch-up event and command). It mirrors the old server's database tables without player ids; the stored contract, dispatch record and resolution are type-checked against the sim types. `migrateSave(raw)` steps an older save through `SAVE_MIGRATIONS` (`applyMigrations`) and validates it, returning a readable error instead of throwing.
|
||||
|
||||
Commands (`packages/engine/src/game/`): `GameEngine.call(name, input)` runs the route catalog's endpoints on the save, with the same schemas and error codes (`CommandError`); `getPlayerState` with no save answers `unauthorized`. Every command resolves due arrivals first and prunes on save; saves are skipped when nothing changed. `stats` keeps lifetime delivery counts for goals, so seen trips older than `savedDispatchRetentionRealHours` can be pruned.
|
||||
|
||||
|
|
|
|||
|
|
@ -569,7 +569,7 @@ Spec: `memory-bank/change-requests/CR-026-random-events-hull.md` (received as "C
|
|||
|
||||
Spec: `memory-bank/change-requests/CR-027-service-bookings.md` (received as "CR-025 … Phase 25"). New decisions recorded as plan §0.5.153–0.5.159.
|
||||
|
||||
- [ ] Step 27.1 — Content and save
|
||||
- [x] **Step 27.1 — Content and save.** Config `autoRepair` (default 40%, 10–90% in steps of 5; the default must be a step). Save version 12: each ship has `booking` (repair, upgrade, HQ, by player or auto, created, started, waiting for funds since; null when none) and `autoRepair` (on, threshold); new ships start on at 40% with no booking; the 11 → 12 migration does the same for existing ships. Tests: `packages/content/test/config.test.ts`; `packages/schema/test/save.test.ts` (11 → 12); `packages/engine/test/trip-events.test.ts` (a bought ship).
|
||||
- [ ] Step 27.2 — Sim: routing and triggers
|
||||
- [ ] Step 27.3 — Engine: booking commands and service start
|
||||
- [ ] Step 27.4 — Engine: Independent pilots and catch-up
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ export const gameConfig: GameConfig = {
|
|||
responseWindowRealMinutes: 30,
|
||||
minTripFraction: 0.5,
|
||||
},
|
||||
autoRepair: { defaultThresholdPct: 40, minPct: 10, maxPct: 90, stepPct: 5 },
|
||||
hull: {
|
||||
floorPct: 5,
|
||||
ratingReductionPerPoint: 0.1,
|
||||
|
|
|
|||
|
|
@ -486,6 +486,13 @@ export const gameConfigSchema = z
|
|||
/** A speed-up never makes a trip shorter than this share of it. */
|
||||
minTripFraction: z.number().min(0).max(1),
|
||||
}),
|
||||
/** Auto-repair (decision 0.5.157): the default threshold and the choices, in percent. */
|
||||
autoRepair: z.object({
|
||||
defaultThresholdPct: z.number().min(0).max(100),
|
||||
minPct: z.number().min(0).max(100),
|
||||
maxPct: z.number().min(0).max(100),
|
||||
stepPct: z.number().positive(),
|
||||
}),
|
||||
/** Hull condition and repairs (decision 0.5.148). */
|
||||
hull: z.object({
|
||||
floorPct: z.number().min(0).max(100),
|
||||
|
|
@ -526,6 +533,17 @@ export const gameConfigSchema = z
|
|||
path: ["fleetSleep", "optionsHours"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
({ autoRepair: a }) =>
|
||||
a.minPct <= a.defaultThresholdPct &&
|
||||
a.defaultThresholdPct <= a.maxPct &&
|
||||
(a.defaultThresholdPct - a.minPct) % a.stepPct === 0 &&
|
||||
(a.maxPct - a.minPct) % a.stepPct === 0,
|
||||
{
|
||||
message: "The auto-repair default must be one of the steps from min to max",
|
||||
path: ["autoRepair", "defaultThresholdPct"],
|
||||
},
|
||||
)
|
||||
.refine(({ events }) => events.fireFrom < events.fireTo, {
|
||||
message: "Events fire inside the trip, early before late",
|
||||
path: ["events", "fireFrom"],
|
||||
|
|
|
|||
|
|
@ -147,3 +147,23 @@ describe("fleet sleep config (Step 24.1, decision 0.5.137)", () => {
|
|||
expect(withSleep({ defaultHours: 12, optionsHours: [12] })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("auto-repair config (Step 27.1, decision 0.5.157)", () => {
|
||||
const withAuto = (autoRepair: typeof gameConfig.autoRepair) =>
|
||||
gameConfigSchema.safeParse({ ...gameConfig, autoRepair }).success;
|
||||
|
||||
it("defaults to 40%, from 10% to 90% in steps of 5", () => {
|
||||
expect(gameConfig.autoRepair).toEqual({
|
||||
defaultThresholdPct: 40,
|
||||
minPct: 10,
|
||||
maxPct: 90,
|
||||
stepPct: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a default off the steps or outside the range", () => {
|
||||
expect(withAuto({ defaultThresholdPct: 42, minPct: 10, maxPct: 90, stepPct: 5 })).toBe(false);
|
||||
expect(withAuto({ defaultThresholdPct: 95, minPct: 10, maxPct: 90, stepPct: 5 })).toBe(false);
|
||||
expect(withAuto({ defaultThresholdPct: 40, minPct: 10, maxPct: 90, stepPct: 5 })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ const validGameConfig = {
|
|||
responseWindowRealMinutes: 30,
|
||||
minTripFraction: 0.5,
|
||||
},
|
||||
autoRepair: { defaultThresholdPct: 40, minPct: 10, maxPct: 90, stepPct: 5 },
|
||||
hull: {
|
||||
floorPct: 5,
|
||||
ratingReductionPerPoint: 0.1,
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ export function acceptDeal(save: SaveDocument, random: RandomSource, now: RealTi
|
|||
shopRepair: false,
|
||||
hullConditionPct: 100,
|
||||
quietTrips: content.config.events.shipCooldownTrips,
|
||||
booking: null,
|
||||
autoRepair: { on: true, thresholdPct: content.config.autoRepair.defaultThresholdPct },
|
||||
fromDeal: true,
|
||||
createdAt: now,
|
||||
lastArrivalAt: now,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ export function buyShip(
|
|||
shopRepair: false,
|
||||
hullConditionPct: 100,
|
||||
quietTrips: content.config.events.shipCooldownTrips,
|
||||
booking: null,
|
||||
autoRepair: { on: true, thresholdPct: content.config.autoRepair.defaultThresholdPct },
|
||||
fromDeal: false,
|
||||
createdAt: now,
|
||||
lastArrivalAt: now,
|
||||
|
|
|
|||
|
|
@ -316,6 +316,8 @@ export function makeOwner(
|
|||
shopRepair: false,
|
||||
hullConditionPct: 100,
|
||||
quietTrips: 2,
|
||||
booking: null,
|
||||
autoRepair: { on: true, thresholdPct: 40 },
|
||||
// Stands in for the deal's Courier (not a Market purchase, CR-013), with a full tank.
|
||||
fromDeal: true,
|
||||
createdAt: TEST_NOW,
|
||||
|
|
@ -384,6 +386,8 @@ export function addShip(
|
|||
shopRepair: false,
|
||||
hullConditionPct: 100,
|
||||
quietTrips: 2,
|
||||
booking: null,
|
||||
autoRepair: { on: true, thresholdPct: 40 },
|
||||
fromDeal: false,
|
||||
createdAt: TEST_NOW,
|
||||
lastArrivalAt: TEST_NOW,
|
||||
|
|
|
|||
|
|
@ -325,3 +325,18 @@ describe("hull wear (Step 26.9, decision 0.5.152)", () => {
|
|||
expect(ship.hullConditionPct).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("service settings on new ships (Step 27.1, decision 0.5.157)", () => {
|
||||
it("gives a bought ship auto-repair on at 40% and no booking", async () => {
|
||||
const test = newTestApp();
|
||||
const owner = await signUpOwner(test.app);
|
||||
await setCredits(test.app, 100_000);
|
||||
const { ship } = await test.app.call("buyShip", {
|
||||
modelId: "courier",
|
||||
bayId: owner.earthBayId,
|
||||
});
|
||||
const saved = (await readSave(test.app)).ships.find((s) => s.id === ship.id)!;
|
||||
expect(saved.autoRepair).toEqual({ on: true, thresholdPct: 40 });
|
||||
expect(saved.booking).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -105,6 +105,26 @@ export const savedShipSchema = z.object({
|
|||
hullConditionPct: z.number().min(0).max(100),
|
||||
/** Trips since its last event: none until the cooldown has passed (decision 0.5.145). */
|
||||
quietTrips: z.number().int().nonnegative(),
|
||||
/**
|
||||
* Service booked at an HQ (decision 0.5.153): a repair and/or one upgrade
|
||||
* level, booked by the player or by auto-repair; null when none.
|
||||
*/
|
||||
booking: z
|
||||
.object({
|
||||
repair: z.boolean(),
|
||||
upgradeId: idSchema.nullable(),
|
||||
/** The HQ (a location with one of the player's bays). */
|
||||
hqId: idSchema,
|
||||
by: z.enum(["player", "auto"]),
|
||||
createdAt: timeSchema,
|
||||
/** The work started in the shop; the booking clears when it ends. */
|
||||
startedAt: timeSchema.nullable(),
|
||||
/** At the HQ but unable to pay, since then (decision 0.5.156). */
|
||||
waitingSince: timeSchema.nullable(),
|
||||
})
|
||||
.nullable(),
|
||||
/** Auto-repair (decision 0.5.157): on or off, and the hull percent that triggers it. */
|
||||
autoRepair: z.object({ on: z.boolean(), thresholdPct: z.number().min(0).max(100) }),
|
||||
/** Handed over in the deal rather than bought (CR-013). */
|
||||
fromDeal: z.boolean(),
|
||||
createdAt: timeSchema,
|
||||
|
|
@ -187,7 +207,7 @@ export const savedDispatchSchema = z.object({
|
|||
});
|
||||
|
||||
/** The save version this build writes. Bump it with a new migration. */
|
||||
export const CURRENT_SAVE_VERSION = 11;
|
||||
export const CURRENT_SAVE_VERSION = 12;
|
||||
|
||||
export const saveDocumentSchema = z.object({
|
||||
saveVersion: z.literal(CURRENT_SAVE_VERSION),
|
||||
|
|
@ -520,6 +540,19 @@ function tripEvents(save: Doc): Doc {
|
|||
};
|
||||
}
|
||||
|
||||
/** Version 11 → 12 (CR-027 §5): every ship on auto-repair at the default threshold, no booking. */
|
||||
function serviceBookings(save: Doc): Doc {
|
||||
const thresholdPct = content.config.autoRepair.defaultThresholdPct;
|
||||
return {
|
||||
...save,
|
||||
ships: records(save.ships).map((ship) => ({
|
||||
...ship,
|
||||
booking: null,
|
||||
autoRepair: { on: true, thresholdPct },
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export const SAVE_MIGRATIONS: readonly SaveMigration[] = [
|
||||
{ from: 1, migrate: addFuelTanks },
|
||||
{ from: 2, migrate: manualRefueling },
|
||||
|
|
@ -531,6 +564,7 @@ export const SAVE_MIGRATIONS: readonly SaveMigration[] = [
|
|||
{ from: 8, migrate: fleetSleep },
|
||||
{ from: 9, migrate: logbook },
|
||||
{ from: 10, migrate: tripEvents },
|
||||
{ from: 11, migrate: serviceBookings },
|
||||
];
|
||||
|
||||
export type SaveParse = { ok: true; save: SaveDocument } | { ok: false; error: string };
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function fullSave(): SaveDocument {
|
|||
seed: 42,
|
||||
});
|
||||
return {
|
||||
saveVersion: 11,
|
||||
saveVersion: 12,
|
||||
saveId: "save-1",
|
||||
lastTimeSeen: T + 3_600_000,
|
||||
player: {
|
||||
|
|
@ -91,6 +91,8 @@ export function fullSave(): SaveDocument {
|
|||
shopRepair: false,
|
||||
hullConditionPct: 100,
|
||||
quietTrips: 2,
|
||||
booking: null,
|
||||
autoRepair: { on: true, thresholdPct: 40 },
|
||||
fromDeal: true,
|
||||
createdAt: T,
|
||||
lastArrivalAt: T + 200_000,
|
||||
|
|
@ -114,6 +116,8 @@ export function fullSave(): SaveDocument {
|
|||
shopRepair: false,
|
||||
hullConditionPct: 100,
|
||||
quietTrips: 2,
|
||||
booking: null,
|
||||
autoRepair: { on: true, thresholdPct: 40 },
|
||||
fromDeal: true,
|
||||
createdAt: T,
|
||||
lastArrivalAt: T,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { fullSave } from "./save-fixture";
|
|||
describe("save document (Step 14.2)", () => {
|
||||
it("validates a fully populated save", () => {
|
||||
expect(saveDocumentSchema.safeParse(fullSave()).success).toBe(true);
|
||||
expect(CURRENT_SAVE_VERSION).toBe(11);
|
||||
expect(CURRENT_SAVE_VERSION).toBe(12);
|
||||
});
|
||||
|
||||
it("rejects an invalid save with a readable error naming the field", () => {
|
||||
|
|
@ -84,7 +84,7 @@ describe("save migration 1 → 2: fuel tanks (CR-015 §5)", () => {
|
|||
if (!result.ok) throw new Error(result.error);
|
||||
const { save } = result;
|
||||
const before = fullSave();
|
||||
expect(save.saveVersion).toBe(11);
|
||||
expect(save.saveVersion).toBe(12);
|
||||
// The Courier has Extended Fuel Tanks 1 (40 fu × 4.8) and is in flight: the base
|
||||
// price, 5 cr/fu since CR-018 (decision 0.5.110).
|
||||
expect(save.ships[0]).toMatchObject({ fuelFu: 192, fuelAvgPrice: 5, refuelPolicy: "manual" });
|
||||
|
|
@ -116,7 +116,7 @@ describe("save migration 2 → 3: Manual refueling (CR-018 §3)", () => {
|
|||
const result = migrateSave(old);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
const { save } = result;
|
||||
expect(save.saveVersion).toBe(11);
|
||||
expect(save.saveVersion).toBe(12);
|
||||
expect(save.ships.map((s) => s.refuelPolicy)).toEqual(save.ships.map(() => "manual"));
|
||||
expect(save.ships.map((s) => [s.fuelFu, s.fuelAvgPrice])).toEqual(
|
||||
before.ships.map((s) => [s.fuelFu, s.fuelAvgPrice]),
|
||||
|
|
@ -142,7 +142,7 @@ describe("save migration 3 → 4: completable boards (CR-019 §3)", () => {
|
|||
for (const board of old.contractBoards) delete board.fleet;
|
||||
const result = migrateSave(old);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(result.save.saveVersion).toBe(11);
|
||||
expect(result.save.saveVersion).toBe(12);
|
||||
expect(result.save.contractBoards).toEqual([]);
|
||||
expect({ ...result.save, contractBoards: [] }).toEqual({ ...before, contractBoards: [] });
|
||||
});
|
||||
|
|
@ -164,7 +164,7 @@ describe("save migration 4 → 5: independent pilots (CR-021 §3)", () => {
|
|||
}
|
||||
const result = migrateSave(old);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(result.save.saveVersion).toBe(11);
|
||||
expect(result.save.saveVersion).toBe(12);
|
||||
expect(result.save.company).toMatchObject({ dispatchOffice: false, npcSpendingFloor: 500 });
|
||||
for (const pilot of result.save.pilots) {
|
||||
expect(pilot.mode).toBe("manual");
|
||||
|
|
@ -191,7 +191,7 @@ describe("save migration 6 → 7: welcome cards (CR-022 §3)", () => {
|
|||
delete old.player.welcomeSeen;
|
||||
const result = migrateSave(old);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(result.save.saveVersion).toBe(11);
|
||||
expect(result.save.saveVersion).toBe(12);
|
||||
expect(result.save.player.welcomeSeen).toBe(true);
|
||||
expect(result.save).toEqual(before);
|
||||
});
|
||||
|
|
@ -207,7 +207,7 @@ describe("save migration 7 → 8: shop time counts as rest (CR-023)", () => {
|
|||
for (const ship of old.ships) delete ship.shopPilotId;
|
||||
const result = migrateSave(old);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(result.save.saveVersion).toBe(11);
|
||||
expect(result.save.saveVersion).toBe(12);
|
||||
// Including the ship still in the shop: its upgrade finishes without the rest.
|
||||
expect(result.save.ships.map((s) => s.shopPilotId)).toEqual(before.ships.map(() => null));
|
||||
expect(result.save).toEqual(before);
|
||||
|
|
@ -225,7 +225,7 @@ describe("save migration 8 → 9: fleet sleep (CR-024 §3)", () => {
|
|||
delete old.company.sleepAfterHours;
|
||||
const result = migrateSave(old);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(result.save.saveVersion).toBe(11);
|
||||
expect(result.save.saveVersion).toBe(12);
|
||||
expect(result.save.company.lastManualDecisionAt).toBeNull();
|
||||
expect(result.save.company.sleepAfterHours).toBe(24);
|
||||
expect(result.save).toEqual(before);
|
||||
|
|
@ -245,7 +245,7 @@ describe("save migration 9 → 10: the logbook (CR-025 §4)", () => {
|
|||
for (const ship of old.ships) delete ship.shopOpen;
|
||||
const result = migrateSave(old);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(result.save.saveVersion).toBe(11);
|
||||
expect(result.save.saveVersion).toBe(12);
|
||||
// The resolved d0: its flight (payout = credits + pilot pay + loan) and its top-up.
|
||||
expect(result.save.logbook.map((e) => e.id)).toEqual(["d0:flight", "d0:top-up"]);
|
||||
expect(result.save.ships.map((s) => s.shopOpen)).toEqual([false, true]);
|
||||
|
|
@ -285,7 +285,7 @@ describe("save migration 10 → 11: events and hulls (CR-026 §6)", () => {
|
|||
}
|
||||
const result = migrateSave(old);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(result.save.saveVersion).toBe(11);
|
||||
expect(result.save.saveVersion).toBe(12);
|
||||
expect(result.save.ships.map((s) => s.hullConditionPct)).toEqual([100, 100]);
|
||||
expect(result.save.dispatches.map((d) => d.event)).toEqual([null, null]);
|
||||
// The deal is made, so the grace trips are behind it.
|
||||
|
|
@ -293,3 +293,25 @@ describe("save migration 10 → 11: events and hulls (CR-026 §6)", () => {
|
|||
expect(result.save).toEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe("save migration 11 → 12: service bookings (CR-027 §5)", () => {
|
||||
it("puts every ship on auto-repair at 40% with no booking, and changes nothing else", () => {
|
||||
const before = fullSave();
|
||||
const old = JSON.parse(JSON.stringify(before)) as Record<string, unknown> & {
|
||||
ships: Record<string, unknown>[];
|
||||
};
|
||||
old.saveVersion = 11;
|
||||
for (const ship of old.ships) {
|
||||
delete ship.booking;
|
||||
delete ship.autoRepair;
|
||||
}
|
||||
const result = migrateSave(old);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(result.save.saveVersion).toBe(12);
|
||||
expect(result.save.ships.map((s) => [s.booking, s.autoRepair])).toEqual([
|
||||
[null, { on: true, thresholdPct: 40 }],
|
||||
[null, { on: true, thresholdPct: 40 }],
|
||||
]);
|
||||
expect(result.save).toEqual(before);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue