Compare commits
3 Commits
e17abb3a3e
...
d5811bfa0e
| Author | SHA1 | Date |
|---|---|---|
|
|
d5811bfa0e | |
|
|
be0322df67 | |
|
|
103f8f1e72 |
18
DECISIONS.md
18
DECISIONS.md
|
|
@ -1778,3 +1778,21 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **Decision 0.5.134: the Contracts chips count ships ready for a job.** "Idle" counted ships whose pilot was resting, so a location looked like it had a pilot available when none could fly. Uncrewed ships, ships in the shop and Independent pilots' ships don't count either: none of them can take a job the player picks. The chip's label is now "N ship(s) ready".
|
||||
- **Rest is checked against the clock on the board**, not the `resting` flag from the last state fetch, so the count changes when rest ends. The Jobs tab badge keeps using the flag, to avoid re-rendering the whole app every second.
|
||||
- **The Dispatch sheet is unchanged**: it already lists a resting pilot's ship as blocked, with the time left.
|
||||
|
||||
## CR-023 recorded (Shop Time Counts as Rest)
|
||||
|
||||
- **Numbering:** received as "CR-021 — Shop Time Counts as Rest" for "Phase 21"; stored as **CR-023**, built as **Phase 23** (its 21.n = 23.n), since CR-021 (Independent Pilots) and CR-022 (Welcome Screens) exist. Its decision CR-021.D1 is **0.5.135**. Its references: CR-006 → CR-009, CR-011 → CR-014, CR-019 → CR-021; CR-004 is unchanged.
|
||||
|
||||
## Step 23.1
|
||||
|
||||
- **The content test compares in game hours**, as the CR asks: shop time is set in real minutes and rest in game hours, so the shop time is multiplied by the time compression first.
|
||||
|
||||
## Step 23.2
|
||||
|
||||
- **The pilot is recorded on the ship** (`shopPilotId`), the CR's second option. An "assigned since" time on the pilot would also need the shop's start time; the ship already knows when the shop ends.
|
||||
- **Leaving the ship is the only way to lose the rest.** A pilot can fly only their assigned ship, so flying another one means being unassigned first. Leaving and coming back during the upgrade also loses it: they weren't with the ship the whole time.
|
||||
- **The shop's end is a catch-up event**, not a check on read, so it happens at its own time in the queue (decision 0.5.129): an Independent pilot waiting on it decides after it, and the save is the same however the time is split. At the same moment it comes before arrivals and decisions.
|
||||
- **A rest that ends with the shop brings an Independent pilot's decision forward** to the shop's end plus the usual turnaround. Without it, the pilot would sit idle until the old rest end.
|
||||
- **Existing saves:** the migration gives every ship `shopPilotId: null`. Recording the current pilot instead would reset duty for long-finished upgrades too, since the migration can't tell the time.
|
||||
- **"Shorter than a rest doesn't count"** is checked at the start of the upgrade (shop time against the rest length), so no pilot is recorded.
|
||||
- **No sim change:** the reset is bookkeeping on the save (who was aboard, when the shop ends), so it lives in the engine beside the other catch-up events; the sim's duty and rest rules are unchanged.
|
||||
|
|
|
|||
|
|
@ -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: 7");
|
||||
expect(text).toContain("Save version: 8");
|
||||
expect(text).toContain("Storage: unknown");
|
||||
expect(text).not.toContain("Player:");
|
||||
expect(text).toMatch(/Game time: \d{4}-\d{2}-\d{2}T/);
|
||||
|
|
|
|||
|
|
@ -37,9 +37,10 @@ export function shipExtras(
|
|||
};
|
||||
}
|
||||
|
||||
/** CR-021 fields of a Manual pilot with default orders, home at Earth. Spread into Pilot fixtures. */
|
||||
export const pilotExtras: Pick<Pilot, "mode" | "orders"> = {
|
||||
/** CR-021 and CR-023 fields of a Manual pilot with default orders, home at Earth, not in the shop. Spread into Pilot fixtures. */
|
||||
export const pilotExtras: Pick<Pilot, "mode" | "orders" | "restingInShopUntil"> = {
|
||||
mode: "manual",
|
||||
restingInShopUntil: null,
|
||||
orders: {
|
||||
destinationIds: null,
|
||||
cargoClassIds: content.cargoTypes.map((c) => c.id),
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Living reference for how the code is organised. Update it after any save format
|
|||
| `ServerTime` | `realTime`, `gameTime` |
|
||||
| `Company` | `id`, `name`, `credits`, `loanBalance` |
|
||||
| `Ship` | `id`, `modelId`, `name`, `status` (`idle` / `in-flight`), `locationId` (null in flight), `dispatchId` (null when idle), `pilotId`, `lastArrivalAt` (latest resolved arrival, else delivery time; picks the default Contracts location), `upgrades` (`{ upgradeId: level }`), `stats` (effective: speed, capacity, tank capacity and burn rate (CR-015), overhead, handling per tonne), `inShopUntil` (null unless in the shop now), `shop` (`{ upgradeId, level, until }` of the latest job until acknowledged; CR-009) |
|
||||
| `Pilot` | `id`, `name`, `isAvatar`, `isVeteran` (the deal's veteran, CR-010), `xp`, `level`, `skills` (`{ skillId: level }`), `skillPointsAvailable`, `assignedShipId`, `status`, `locationId` (null in flight), `dutyHours`, `dutyLimit`, `resting`, `restEndsAt` (null unless resting; CR-004) |
|
||||
| `Pilot` | `id`, `name`, `isAvatar`, `isVeteran` (the deal's veteran, CR-010), `xp`, `level`, `skills` (`{ skillId: level }`), `skillPointsAvailable`, `assignedShipId`, `status`, `locationId` (null in flight), `dutyHours`, `dutyLimit`, `resting`, `restEndsAt` (null unless resting; CR-004), `restingInShopUntil` (waiting with their ship in the shop, which counts as rest: when the shop time ends, else null; CR-023) |
|
||||
| `Bay` | `id`, `locationId` |
|
||||
| `LocationState` | `id`, `discovered` |
|
||||
| `Contract` | mirrors sim `Contract`: route, `cargoTypeId` (class), `goodId`, `shipper`, `priority`, `deliveryWindowGameHours` (Priority only, else null), `tier`, `massTonnes` (to 0.1 t under 10 t), `payout`, requirements, `offeredAt`/`expiresAt`, `firstContact`, optional `scriptedId` on a scripted tutorial contract (CR-007) |
|
||||
|
|
@ -81,7 +81,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 5; 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), `saveId`, `lastTimeSeen` (the clock never goes below it), `player` (world seed, tutorial state, deal accepted), `company` (name, credits, loan), `ships` (with `upgrades` as a level map, shop fields, and `fuelFu`, `fuelAvgPrice`, `refuelPolicy` since CR-015), `pilots`, `bays`, `discoveredLocations`, `contractBoards` (stored boards), `dispatches` (sim record, status, resolution, acknowledged), `acceptedContracts`, `claimedGoals`. 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 8; 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), `saveId`, `lastTimeSeen` (the clock never goes below it), `player` (world seed, tutorial state, deal accepted), `company` (name, credits, loan), `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`. 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
# Change Request CR-023 — Shop Time Counts as Rest
|
||||
|
||||
> **Numbering note (added when recorded):** this request arrived titled "CR-021 — Shop Time Counts as Rest" for "Phase 21". CR-021 (Independent Pilots) and CR-022 (Welcome Screens), and Phases 21 and 22, already existed, so it is stored as **CR-023** and done as **Phase 23**: every "21.n" below means **23.n**. Its references map to this repo as: "Phase 13 and CR-001 through CR-020" = Phase 13 and **CR-001 to CR-022**; "CR-004 pilot rest" is the same number; "CR-006 ship upgrades" = **CR-009**; "CR-011 local-first" = **CR-014** (the resolution loop and catch-up are CR-021's event queue, decision 0.5.129); "CR-019 independent pilots" = **CR-021**. Its new decision CR-021.D1 is recorded as **0.5.135 Shop time counts as rest**.
|
||||
>
|
||||
> **On recording:** rest is 10 game hours (10 real minutes at 60× compression) and the shop takes 10, 20 or 30 real minutes (10, 20 or 30 game hours), so every shop time already covers a full rest.
|
||||
|
||||
**Project:** Deadhead: Space Freight Tycoon
|
||||
**Applies to:** a build with Phase 13 and CR-001 through CR-020 complete
|
||||
**Do this:** as **Phase 21** (small)
|
||||
**Related docs:** `change-request-pilot-rest.md` (CR-004), `change-request-ship-upgrades.md` (CR-006), `change-request-local-first.md` (CR-011), `change-request-independent-pilots.md` (CR-019), `DECISIONS.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Why
|
||||
|
||||
An upgrade takes a ship out of service for 10–30 real minutes, and a full rest is 10 real minutes. Today a tired pilot can wait out an upgrade and then still have to rest afterward: double downtime while they were standing around. From now on, a pilot who stays with their ship while it's in the shop comes out rested.
|
||||
|
||||
## 2. Rules
|
||||
|
||||
| Rule | Detail |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Duty reset | When an upgrade finishes, the pilot assigned to that ship has their duty hours set to zero, **if** they were assigned to it for the whole shop time and didn't fly in the meantime. |
|
||||
| Already resting | If the pilot was resting when the upgrade started, the rest continues as normal alongside it. If the rest would end after the upgrade, it also ends when the upgrade finishes. |
|
||||
| Not locked in | The pilot isn't blocked during the upgrade. They can still be unassigned at the HQ and fly another ship; if they do, they don't get the reset. |
|
||||
| Shop time vs rest | All shop times are at least the rest duration. A content test enforces this; if a future shop time is shorter, it doesn't count as rest. |
|
||||
| Independent pilots | Same rule (CR-019). An Independent pilot whose ship is in the shop waits with it and comes out rested. |
|
||||
|
||||
## 3. Decision
|
||||
|
||||
Record in `DECISIONS.md` and `implementation-plan.md` §0.5 with the next free number (labeled CR-021.D1 here).
|
||||
|
||||
- **CR-021.D1 Shop time counts as rest.** Per §2.
|
||||
|
||||
## 4. Steps — Phase 21
|
||||
|
||||
### Step 21.1 — Content test
|
||||
|
||||
- **Test:** Every upgrade level's shop time, converted to game hours, is at least the rest duration.
|
||||
|
||||
### Step 21.2 — Sim and engine: duty reset on upgrade completion
|
||||
|
||||
- When an upgrade's shop time ends (in the resolution loop or catch-up), if the ship's current pilot has been assigned to it since the upgrade started and hasn't flown since, set their duty hours to zero and clear any rest that would end later. Track this with an "assigned since" time on the pilot, or by recording the pilot id on the upgrade when it starts.
|
||||
- **Test:** A pilot near the duty limit whose ship finishes an upgrade has zero duty hours and can be dispatched immediately. A pilot who was unassigned during the upgrade and flew another ship keeps their duty hours. A pilot already resting when the upgrade started is free when the upgrade ends, if their rest would have ended later. Catch-up produces the same result as live play.
|
||||
|
||||
### Step 21.3 — Client: status line
|
||||
|
||||
- While the ship is in the shop, the assigned pilot's status reads "Resting while in the shop — mm:ss" (the upgrade countdown). The ship's Fleet card keeps its "In the shop" countdown.
|
||||
- **Test:** Component test: a pilot whose ship is in the shop shows the new status with the right countdown; after unassigning them, the status no longer says they're resting.
|
||||
|
||||
### Step 21.4 — Regression
|
||||
|
||||
- Run the full suite.
|
||||
- **Test:** CI is green. Manual check: fly a pilot close to the duty limit, start an upgrade at the HQ, and confirm they're ready to fly the moment the upgrade finishes.
|
||||
|
||||
## 5. Out of scope
|
||||
|
||||
Rest facilities at HQs, shortening shop times, and anything else in `implementation-plan.md` §0.3.
|
||||
|
|
@ -190,6 +190,7 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple
|
|||
132. **How to play** (CR-022). Settings → Help reopens the three cards any time, ending in "Close"; it doesn't change the saved flag.
|
||||
133. **Boards are for the player's pilots** (fix after CR-021). The qualifying fleet (0.5.115) counts only Manual pilots and the ships not assigned to an Independent pilot, so the player never sees jobs only an Independent pilot could fly; Independent pilots check each job against their own pair. With no Manual pair at all, the whole fleet counts.
|
||||
134. **Ready ships** (fix after CR-022). The Contracts screen's location chips and the Jobs tab badge count only ships a job could go out on now: idle, with an assigned pilot who isn't resting or Independent, and not in the shop. Where idle ships are docked but none is ready, the board says so and names the pilot who wakes first.
|
||||
135. **Shop time counts as rest** (CR-023). When an upgrade finishes, the pilot who was assigned to the ship for the whole shop time (never unassigned, so never flying another ship) has their duty reset to zero, and a rest that would end later ends then. A shop time shorter than a rest doesn't count. Independent pilots alike.
|
||||
|
||||
### 0.6 Translating change requests written for the server (CR-014 §7)
|
||||
|
||||
|
|
@ -883,6 +884,12 @@ Steps 22.1–22.4 are specified in `memory-bank/change-requests/CR-022-welcome-s
|
|||
|
||||
---
|
||||
|
||||
## Phase 23 — Shop Time Counts as Rest (CR-023)
|
||||
|
||||
Steps 23.1–23.4 are specified in `memory-bank/change-requests/CR-023-shop-time-rest.md` §4 (written there as 21.1–21.4). Stop for review at the end.
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (Base Game)
|
||||
|
||||
- All step tests pass in CI.
|
||||
|
|
|
|||
|
|
@ -505,3 +505,12 @@ Spec: `memory-bank/change-requests/CR-022-welcome-screens.md` (received as "CR-0
|
|||
|
||||
- [x] **Boards ignore Independent pilots (decision 0.5.133).** Reported: with Rosa Independent, the board still showed jobs only that pilot could fly. The board's qualifying fleet now leaves out Independent pilots and their ships, both when a board is generated and when it's read, so a board already stored this period hides those jobs at once. Independent pilots still check each job against their own pair; with no Manual pair at all, the whole fleet counts. Tests (`packages/engine/test/completable-boards.test.ts`): heavy jobs vanish from the board when the Freighter's pilot goes Independent, this period and the next five; the fallback to the whole fleet.
|
||||
- [x] **Ready ships, not idle ships (decision 0.5.134).** Reported: a resting pilot still showed as available on the board. The location chips on Contracts counted every idle ship, so a ship whose pilot was resting still showed as ready. They now count ships ready for a job (`readyShips` in `apps/web/src/game.ts`): crewed, the pilot not resting (checked against the clock, so the count updates the moment rest ends) or Independent, and not in the shop. The Jobs tab badge uses the same rule. With idle ships here but none ready, the board says "No pilot here is ready to fly" and who wakes first. Tests (`apps/web/test/ready-ships.test.tsx`, `location-boards.test.tsx`). Screenshots: `docs/screenshots/fix-ready-ships-resting-{phone,desktop}.png`.
|
||||
|
||||
## Phase 23 — Shop Time Counts as Rest (CR-023)
|
||||
|
||||
Spec: `memory-bank/change-requests/CR-023-shop-time-rest.md` (received as "CR-021 … Phase 21"). New decision recorded as plan §0.5.135.
|
||||
|
||||
- [x] **Step 23.1 — Content test.** Every upgrade level's shop time, in game hours (real minutes × time compression), is at least `restGameHours`: today 10, 20 and 30 game hours against a 10-hour rest. Test: `packages/content/test/upgrades.test.ts`.
|
||||
- [x] **Step 23.2 — Engine: duty reset on upgrade completion.** `startUpgrade` records the ship's pilot as `shopPilotId` (save version 8; the 7 → 8 migration sets it null, so an upgrade already under way finishes without the rest), but only when the shop time covers a full rest. `unassignPilot` clears it: a pilot can only fly another ship after leaving this one, so "assigned the whole time" and "didn't fly" are the same check. The shop's end is a new event in the catch-up queue (before arrivals and decisions at the same moment): `finishShop` sets the pilot's duty to zero, ends a rest that would run later, and brings an Independent pilot's next decision forward to the turnaround after it. The dev "finish shop" does the same at once. `Pilot.restingInShopUntil` tells the client. Tests (`packages/engine/test/shop-rest.test.ts`): a pilot near the limit is at zero duty and flies the moment the upgrade ends; one who left and flew another ship keeps their duty; one who left and came back doesn't get the reset; a longer rest ends with the shop and a shorter one runs its course; an Independent pilot comes out rested and decides after the turnaround; catch-up at once equals 30-second steps. `packages/schema/test/save.test.ts`: the 7 → 8 migration.
|
||||
- [ ] Step 23.3 — Client: status line
|
||||
- [ ] Step 23.4 — Regression
|
||||
|
|
|
|||
|
|
@ -81,3 +81,14 @@ describe("ship upgrades content (CR-009)", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shop time counts as rest (Step 23.1, decision 0.5.135)", () => {
|
||||
it("keeps every upgrade level in the shop for at least a full rest, in game hours", () => {
|
||||
const { upgradeLevels, timeCompression, restGameHours } = gameConfig;
|
||||
for (const level of upgradeLevels) {
|
||||
// Real minutes × compression = game minutes.
|
||||
const shopGameHours = (level.shopRealMinutes * timeCompression) / 60;
|
||||
expect(shopGameHours).toBeGreaterThanOrEqual(restGameHours);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
import type { RandomSource } from "../core";
|
||||
import { locationBoard } from "./boards";
|
||||
import { dispatchContract, dispatchReposition } from "./dispatch";
|
||||
import { refuelShip } from "./fleet";
|
||||
import { finishShop, refuelShip } from "./fleet";
|
||||
import { scheduleDecision } from "./independent";
|
||||
import { resolveArrivals } from "./resolve";
|
||||
import { discoveredIds, pilotState, shipState } from "./state";
|
||||
|
|
@ -77,6 +77,18 @@ function nextDecision(save: SaveDocument, limit: RealTime): SavedPilot | null {
|
|||
return best;
|
||||
}
|
||||
|
||||
/** The earliest shop end by `limit` with a pilot waiting it out (decision 0.5.135). */
|
||||
function nextShopEnd(save: SaveDocument, limit: RealTime): SaveDocument["ships"][number] | null {
|
||||
let best: SaveDocument["ships"][number] | null = null;
|
||||
for (const ship of save.ships) {
|
||||
const t = ship.inShopUntil;
|
||||
if (ship.shopPilotId === null || t === null || t > limit) continue;
|
||||
if (!best || t < best.inShopUntil! || (t === best.inShopUntil && ship.id < best.id))
|
||||
best = ship;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function nextArrival(save: SaveDocument, limit: RealTime): RealTime | null {
|
||||
let earliest: RealTime | null = null;
|
||||
for (const d of save.dispatches) {
|
||||
|
|
@ -163,9 +175,9 @@ function decide(save: SaveDocument, pilot: SavedPilot, t: RealTime, viewed: stri
|
|||
}
|
||||
|
||||
/**
|
||||
* Replays time up to `until` as an event queue (decision 0.5.129): arrivals
|
||||
* and Independent pilots' decision points in time order; at the same moment
|
||||
* arrivals first, then pilots by id. Each arrival resolves at its own time, so
|
||||
* Replays time up to `until` as an event queue (decision 0.5.129): shop ends
|
||||
* (decision 0.5.135), arrivals and Independent pilots' decision points in time
|
||||
* order; at the same moment shop ends first, then arrivals, then pilots by id. Each arrival resolves at its own time, so
|
||||
* the result doesn't depend on how the time is split.
|
||||
*/
|
||||
export function catchUp(
|
||||
|
|
@ -179,9 +191,20 @@ export function catchUp(
|
|||
let decisions = 0;
|
||||
let reached = Number.NEGATIVE_INFINITY;
|
||||
for (let events = 0; events < maxEvents; events++) {
|
||||
const shop = nextShopEnd(save, until);
|
||||
const shopAt = shop?.inShopUntil ?? null;
|
||||
const arrival = nextArrival(save, until);
|
||||
const pilot = nextDecision(save, decideLimit);
|
||||
const decisionAt = pilot?.nextDecisionAt ?? null;
|
||||
if (
|
||||
shopAt !== null &&
|
||||
(arrival === null || shopAt <= arrival) &&
|
||||
(decisionAt === null || shopAt <= decisionAt)
|
||||
) {
|
||||
finishShop(save, shop!);
|
||||
reached = shopAt;
|
||||
continue;
|
||||
}
|
||||
if (arrival === null && decisionAt === null) {
|
||||
// Pilots who would have decided after the cap pick up from `until`.
|
||||
if (decideLimit < until) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
acknowledge,
|
||||
assignPilot,
|
||||
buyShip,
|
||||
finishShop,
|
||||
hirePilot,
|
||||
leaseBay,
|
||||
restPilot,
|
||||
|
|
@ -115,6 +116,8 @@ export function commandTable(): CommandTable {
|
|||
case "finish-shop":
|
||||
// Done now: the "out of the shop" note still shows, as for a finished job.
|
||||
if (ship.inShopUntil !== null && ship.inShopUntil > now) ship.inShopUntil = now;
|
||||
// Its pilot comes out rested at once (decision 0.5.135).
|
||||
if (ship.shopPilotId !== null) finishShop(save, ship);
|
||||
break;
|
||||
case "move":
|
||||
if (ship.status !== "idle")
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ export function acceptDeal(save: SaveDocument, random: RandomSource, now: RealTi
|
|||
inShopUntil: null,
|
||||
shopUpgradeId: null,
|
||||
shopLevel: null,
|
||||
shopPilotId: null,
|
||||
fromDeal: true,
|
||||
createdAt: now,
|
||||
lastArrivalAt: now,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { content } from "@deadhead/content";
|
||||
import { newPilotOrders } from "./independent";
|
||||
import { newPilotOrders, scheduleDecision } from "./independent";
|
||||
import type { Bay, Pilot, SaveDocument, Ship } from "@deadhead/schema";
|
||||
import {
|
||||
effectiveStats,
|
||||
|
|
@ -75,6 +75,7 @@ export function buyShip(
|
|||
inShopUntil: null,
|
||||
shopUpgradeId: null,
|
||||
shopLevel: null,
|
||||
shopPilotId: null,
|
||||
fromDeal: false,
|
||||
createdAt: now,
|
||||
lastArrivalAt: now,
|
||||
|
|
@ -164,9 +165,29 @@ export function unassignPilot(save: SaveDocument, pilotId: string, now: RealTime
|
|||
throw fail("no-bay-at-location", "Pilots can only leave their ship at one of your bays.");
|
||||
}
|
||||
Object.assign(pilot, { assignedShipId: null, locationId: ship.locationId });
|
||||
// Leaving the ship gives up the shop rest (decision 0.5.135).
|
||||
if (ship.shopPilotId === pilot.id) ship.shopPilotId = null;
|
||||
return toApiPilot(save, pilot, now);
|
||||
}
|
||||
|
||||
/**
|
||||
* The shop time ends (decision 0.5.135): the pilot who stayed with the ship
|
||||
* the whole time comes out rested. Duty goes to zero, and a rest that would
|
||||
* end later ends now. An Independent pilot who was waiting for that rest
|
||||
* decides after the usual turnaround.
|
||||
*/
|
||||
export function finishShop(save: SaveDocument, ship: SavedShip): void {
|
||||
const at = ship.inShopUntil;
|
||||
const pilot = save.pilots.find((p) => p.id === ship.shopPilotId);
|
||||
ship.shopPilotId = null;
|
||||
if (!pilot || at === null || pilot.assignedShipId !== ship.id) return;
|
||||
pilot.dutyHours = 0;
|
||||
if (pilot.restEndsAt !== null && pilot.restEndsAt > at) {
|
||||
pilot.restEndsAt = at;
|
||||
if (pilot.nextDecisionAt !== null) scheduleDecision(pilot, at);
|
||||
}
|
||||
}
|
||||
|
||||
/** Rests an idle pilot now, at any duty level (CR-004): duty resets and they can't fly until it ends. */
|
||||
export function restPilot(save: SaveDocument, pilotId: string, now: RealTime): Pilot {
|
||||
requireTutorialOver(save, "resting a pilot");
|
||||
|
|
@ -226,10 +247,14 @@ export function startUpgrade(
|
|||
if (!check.ok) throw fail(check.reason, UPGRADE_MESSAGES[check.reason]);
|
||||
save.company.credits -= check.cost;
|
||||
ship.upgrades = { ...ship.upgrades, [upgrade.id]: check.level };
|
||||
// The pilot aboard waits it out as rest, if it lasts a full rest (decision 0.5.135).
|
||||
const pilot = save.pilots.find((p) => p.assignedShipId === ship.id);
|
||||
const coversRest = check.shopUntil - now >= restEndsAt(now, content.config) - now;
|
||||
Object.assign(ship, {
|
||||
inShopUntil: check.shopUntil,
|
||||
shopUpgradeId: upgrade.id,
|
||||
shopLevel: check.level,
|
||||
shopPilotId: pilot && coversRest ? pilot.id : null,
|
||||
});
|
||||
return toApiShip(save, ship, now);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,9 +126,10 @@ export function toApiDispatch(dispatch: SavedDispatch): Dispatch {
|
|||
export function toApiPilot(save: SaveDocument, pilot: SavedPilot, now: RealTime): Pilot {
|
||||
const resting = isResting(pilot.restEndsAt, now);
|
||||
const level = pilotLevel(pilot.xp, content.config);
|
||||
const shipLocation = pilot.assignedShipId
|
||||
? (save.ships.find((s) => s.id === pilot.assignedShipId)?.locationId ?? null)
|
||||
: null;
|
||||
const ship = pilot.assignedShipId
|
||||
? save.ships.find((s) => s.id === pilot.assignedShipId)
|
||||
: undefined;
|
||||
const shipLocation = ship?.locationId ?? null;
|
||||
return {
|
||||
id: pilot.id,
|
||||
name: pilot.name,
|
||||
|
|
@ -146,6 +147,8 @@ export function toApiPilot(save: SaveDocument, pilot: SavedPilot, now: RealTime)
|
|||
dutyLimit: dutyLimit(pilot.skills, content.config),
|
||||
resting,
|
||||
restEndsAt: resting ? pilot.restEndsAt : null,
|
||||
restingInShopUntil:
|
||||
ship && ship.shopPilotId === pilot.id && isInShop(ship, now) ? ship.inShopUntil : null,
|
||||
mode: pilot.mode,
|
||||
orders: pilot.orders,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
import { content } from "@deadhead/content";
|
||||
import { MS_PER_MINUTE, upgradeShopMs } from "@deadhead/sim";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { turnaroundMs } from "../src/game/independent";
|
||||
import {
|
||||
addPilot,
|
||||
addShip,
|
||||
edit,
|
||||
newTestApp,
|
||||
readSave,
|
||||
setCredits,
|
||||
signUpOwner,
|
||||
type TestApp,
|
||||
} from "./support";
|
||||
|
||||
const SHOP_MS = upgradeShopMs(1, content.config);
|
||||
const LIMIT = content.config.dutyLimitGameHours;
|
||||
const pilotIn = async (test: TestApp, id: string) =>
|
||||
(await test.app.call("getPlayerState", {})).pilots.find((p) => p.id === id)!;
|
||||
|
||||
/** An owner with a Courier at Earth crewed by `p1`, one trip short of the duty limit. */
|
||||
async function tiredPilot(test: TestApp) {
|
||||
await signUpOwner(test.app);
|
||||
await setCredits(test.app, 100_000);
|
||||
await addShip(test.app, { id: "f1", modelId: "courier", name: "Wren" });
|
||||
await addPilot(test.app, { id: "p1", name: "Ada", assignedShipId: "f1", dutyHours: LIMIT - 0.5 });
|
||||
}
|
||||
|
||||
const upgrade = (test: TestApp) =>
|
||||
test.app.call("startUpgrade", { shipId: "f1", upgradeId: "engines" });
|
||||
|
||||
describe("shop time counts as rest (Step 23.2, decision 0.5.135)", () => {
|
||||
it("brings a pilot near the duty limit out of the shop with no duty, ready to fly", async () => {
|
||||
const test = newTestApp();
|
||||
await tiredPilot(test);
|
||||
const startedAt = test.clock.now;
|
||||
await upgrade(test);
|
||||
expect((await pilotIn(test, "p1")).restingInShopUntil).toBe(startedAt + SHOP_MS);
|
||||
|
||||
test.clock.now = startedAt + SHOP_MS;
|
||||
const pilot = await pilotIn(test, "p1");
|
||||
expect(pilot.dutyHours).toBe(0);
|
||||
expect(pilot.resting).toBe(false);
|
||||
expect(pilot.restingInShopUntil).toBeNull();
|
||||
const { dispatch } = await test.app.call("createDispatch", {
|
||||
kind: "reposition",
|
||||
shipId: "f1",
|
||||
pilotId: "p1",
|
||||
destinationId: "moon",
|
||||
});
|
||||
expect(dispatch.status).toBe("in-flight");
|
||||
});
|
||||
|
||||
it("keeps the duty of a pilot who left the ship and flew another one", async () => {
|
||||
const test = newTestApp();
|
||||
await tiredPilot(test);
|
||||
await addShip(test.app, { id: "f2", modelId: "courier", name: "Kite" });
|
||||
// Fresh enough that the Moon trip adds duty without forcing a rest.
|
||||
await edit(test.app, (save) => {
|
||||
save.pilots.find((p) => p.id === "p1")!.dutyHours = 2;
|
||||
});
|
||||
const startedAt = test.clock.now;
|
||||
await upgrade(test);
|
||||
await test.app.call("unassignPilot", { pilotId: "p1" });
|
||||
expect((await pilotIn(test, "p1")).restingInShopUntil).toBeNull();
|
||||
await test.app.call("assignPilot", { pilotId: "p1", shipId: "f2" });
|
||||
await test.app.call("createDispatch", {
|
||||
kind: "reposition",
|
||||
shipId: "f2",
|
||||
pilotId: "p1",
|
||||
destinationId: "moon",
|
||||
});
|
||||
|
||||
test.clock.now = startedAt + SHOP_MS + 60 * MS_PER_MINUTE;
|
||||
const pilot = await pilotIn(test, "p1");
|
||||
// At the Moon with the trip added to their duty, not reset by the shop.
|
||||
expect(pilot.status).toBe("idle");
|
||||
expect(pilot.dutyHours).toBeGreaterThan(2);
|
||||
expect((await readSave(test.app)).ships.find((s) => s.id === "f1")!.shopPilotId).toBeNull();
|
||||
});
|
||||
|
||||
it("doesn't reset a pilot who left the ship and came back during the upgrade", async () => {
|
||||
const test = newTestApp();
|
||||
await tiredPilot(test);
|
||||
const startedAt = test.clock.now;
|
||||
await upgrade(test);
|
||||
await test.app.call("unassignPilot", { pilotId: "p1" });
|
||||
await test.app.call("assignPilot", { pilotId: "p1", shipId: "f1" });
|
||||
test.clock.now = startedAt + SHOP_MS;
|
||||
expect((await pilotIn(test, "p1")).dutyHours).toBe(LIMIT - 0.5);
|
||||
});
|
||||
|
||||
it("frees a pilot whose rest would have ended after the upgrade, and leaves a shorter rest alone", async () => {
|
||||
const test = newTestApp();
|
||||
await tiredPilot(test);
|
||||
const startedAt = test.clock.now;
|
||||
await addShip(test.app, { id: "f2", modelId: "courier", name: "Kite" });
|
||||
await addPilot(test.app, { id: "p2", name: "Bo", assignedShipId: "f2" });
|
||||
await edit(test.app, (save) => {
|
||||
save.pilots.find((p) => p.id === "p1")!.restEndsAt = startedAt + 3 * SHOP_MS;
|
||||
save.pilots.find((p) => p.id === "p2")!.restEndsAt = startedAt + SHOP_MS / 2;
|
||||
});
|
||||
await upgrade(test);
|
||||
await test.app.call("startUpgrade", { shipId: "f2", upgradeId: "engines" });
|
||||
|
||||
// The short rest runs its course alongside the upgrade.
|
||||
test.clock.now = startedAt + SHOP_MS / 2;
|
||||
expect((await pilotIn(test, "p2")).resting).toBe(false);
|
||||
expect((await pilotIn(test, "p1")).resting).toBe(true);
|
||||
|
||||
test.clock.now = startedAt + SHOP_MS;
|
||||
const p1 = await pilotIn(test, "p1");
|
||||
expect(p1.resting).toBe(false);
|
||||
expect(p1.dutyHours).toBe(0);
|
||||
});
|
||||
|
||||
it("brings an Independent pilot out rested, flying after the turnaround", async () => {
|
||||
const test = newTestApp();
|
||||
await tiredPilot(test);
|
||||
await test.app.call("buyDispatchOffice", {});
|
||||
await edit(test.app, (save) => {
|
||||
save.pilots.find((p) => p.id === "p1")!.restEndsAt = test.clock.now + 3 * SHOP_MS;
|
||||
});
|
||||
const startedAt = test.clock.now;
|
||||
await upgrade(test);
|
||||
await test.app.call("setPilotMode", { pilotId: "p1", mode: "independent" });
|
||||
|
||||
test.clock.now = startedAt + SHOP_MS;
|
||||
await test.app.call("getPlayerState", {});
|
||||
const save = await readSave(test.app);
|
||||
const p1 = save.pilots.find((p) => p.id === "p1")!;
|
||||
expect(p1.dutyHours).toBe(0);
|
||||
expect(p1.restEndsAt).toBe(startedAt + SHOP_MS);
|
||||
expect(p1.nextDecisionAt).toBe(startedAt + SHOP_MS + turnaroundMs());
|
||||
});
|
||||
|
||||
it("gives the same save caught up at once as in live play", async () => {
|
||||
const once = newTestApp();
|
||||
await tiredPilot(once);
|
||||
await once.app.call("buyDispatchOffice", {});
|
||||
await upgrade(once);
|
||||
await once.app.call("setPilotMode", { pilotId: "p1", mode: "independent" });
|
||||
const { code } = await once.app.call("exportSave", {});
|
||||
const live = newTestApp();
|
||||
live.clock.now = once.clock.now;
|
||||
await live.app.call("importSave", { code });
|
||||
|
||||
const end = once.clock.now + 3 * 60 * MS_PER_MINUTE;
|
||||
once.clock.now = end;
|
||||
await once.app.call("getPlayerState", {});
|
||||
while (live.clock.now < end) {
|
||||
live.clock.now = Math.min(end, live.clock.now + 30_000);
|
||||
await live.app.call("getPlayerState", {});
|
||||
}
|
||||
|
||||
const a = await readSave(once.app);
|
||||
const b = await readSave(live.app);
|
||||
expect(a.dispatches.some((d) => d.independent)).toBe(true);
|
||||
expect({ ...b, lastTimeSeen: 0 }).toEqual({ ...a, lastTimeSeen: 0 });
|
||||
});
|
||||
});
|
||||
|
|
@ -51,6 +51,7 @@ describe("company creation on the device (Step 14.8)", () => {
|
|||
dutyLimit: content.config.dutyLimitGameHours,
|
||||
resting: false,
|
||||
restEndsAt: null,
|
||||
restingInShopUntil: null,
|
||||
mode: "manual",
|
||||
orders: {
|
||||
destinationIds: null,
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ export function makeOwner(
|
|||
inShopUntil: null,
|
||||
shopUpgradeId: null,
|
||||
shopLevel: null,
|
||||
shopPilotId: null,
|
||||
// Stands in for the deal's Courier (not a Market purchase, CR-013), with a full tank.
|
||||
fromDeal: true,
|
||||
createdAt: TEST_NOW,
|
||||
|
|
@ -372,6 +373,7 @@ export function addShip(
|
|||
inShopUntil: null,
|
||||
shopUpgradeId: null,
|
||||
shopLevel: null,
|
||||
shopPilotId: null,
|
||||
fromDeal: false,
|
||||
createdAt: TEST_NOW,
|
||||
lastArrivalAt: TEST_NOW,
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ export const pilotSchema = z.object({
|
|||
/** Resting pilots cannot fly until `restEndsAt` (null when not resting). */
|
||||
resting: z.boolean(),
|
||||
restEndsAt: timeSchema.nullable(),
|
||||
/**
|
||||
* Waiting with their ship in the shop, which counts as rest (decision
|
||||
* 0.5.135): when the shop time ends; null otherwise.
|
||||
*/
|
||||
restingInShopUntil: timeSchema.nullable(),
|
||||
/** Manual, or Independent: flies jobs on their own (CR-021). The avatar is always Manual. */
|
||||
mode: pilotModeSchema,
|
||||
orders: pilotOrdersSchema,
|
||||
|
|
|
|||
|
|
@ -86,6 +86,12 @@ export const savedShipSchema = z.object({
|
|||
/** The latest shop job, kept until the player has seen it. */
|
||||
shopUpgradeId: idSchema.nullable(),
|
||||
shopLevel: z.number().int().positive().nullable(),
|
||||
/**
|
||||
* The pilot aboard when the upgrade started, while they stay with the ship:
|
||||
* they come out rested (decision 0.5.135). Cleared when they leave it, and
|
||||
* when the shop time ends.
|
||||
*/
|
||||
shopPilotId: idSchema.nullable(),
|
||||
/** Handed over in the deal rather than bought (CR-013). */
|
||||
fromDeal: z.boolean(),
|
||||
createdAt: timeSchema,
|
||||
|
|
@ -164,7 +170,7 @@ export const savedDispatchSchema = z.object({
|
|||
});
|
||||
|
||||
/** The save version this build writes. Bump it with a new migration. */
|
||||
export const CURRENT_SAVE_VERSION = 7;
|
||||
export const CURRENT_SAVE_VERSION = 8;
|
||||
|
||||
export const saveDocumentSchema = z.object({
|
||||
saveVersion: z.literal(CURRENT_SAVE_VERSION),
|
||||
|
|
@ -337,6 +343,17 @@ function welcomeSeen(save: Doc): Doc {
|
|||
return { ...save, player: { ...(save.player as Doc), welcomeSeen: true } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Version 7 → 8 (CR-023): no ship has a pilot waiting out the shop. An upgrade
|
||||
* already under way finishes as before, without the rest.
|
||||
*/
|
||||
function shopPilot(save: Doc): Doc {
|
||||
return {
|
||||
...save,
|
||||
ships: records(save.ships).map((ship) => ({ ...ship, shopPilotId: null })),
|
||||
};
|
||||
}
|
||||
|
||||
export const SAVE_MIGRATIONS: readonly SaveMigration[] = [
|
||||
{ from: 1, migrate: addFuelTanks },
|
||||
{ from: 2, migrate: manualRefueling },
|
||||
|
|
@ -344,6 +361,7 @@ export const SAVE_MIGRATIONS: readonly SaveMigration[] = [
|
|||
{ from: 4, migrate: independentPilots },
|
||||
{ from: 5, migrate: catchUpState },
|
||||
{ from: 6, migrate: welcomeSeen },
|
||||
{ from: 7, migrate: shopPilot },
|
||||
];
|
||||
|
||||
export type SaveParse = { ok: true; save: SaveDocument } | { ok: false; error: string };
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function fullSave(): SaveDocument {
|
|||
seed: 42,
|
||||
});
|
||||
return {
|
||||
saveVersion: 7,
|
||||
saveVersion: 8,
|
||||
saveId: "save-1",
|
||||
lastTimeSeen: T + 3_600_000,
|
||||
player: {
|
||||
|
|
@ -76,6 +76,7 @@ export function fullSave(): SaveDocument {
|
|||
inShopUntil: null,
|
||||
shopUpgradeId: null,
|
||||
shopLevel: null,
|
||||
shopPilotId: null,
|
||||
fromDeal: true,
|
||||
createdAt: T,
|
||||
lastArrivalAt: T + 200_000,
|
||||
|
|
@ -93,6 +94,7 @@ export function fullSave(): SaveDocument {
|
|||
inShopUntil: T + 7_200_000,
|
||||
shopUpgradeId: "cargo-bay",
|
||||
shopLevel: 1,
|
||||
shopPilotId: null,
|
||||
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(7);
|
||||
expect(CURRENT_SAVE_VERSION).toBe(8);
|
||||
});
|
||||
|
||||
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(7);
|
||||
expect(save.saveVersion).toBe(8);
|
||||
// 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(7);
|
||||
expect(save.saveVersion).toBe(8);
|
||||
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(7);
|
||||
expect(result.save.saveVersion).toBe(8);
|
||||
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(7);
|
||||
expect(result.save.saveVersion).toBe(8);
|
||||
expect(result.save.company).toMatchObject({ dispatchOffice: false, npcSpendingFloor: 500 });
|
||||
for (const pilot of result.save.pilots) {
|
||||
expect(pilot.mode).toBe("manual");
|
||||
|
|
@ -191,8 +191,25 @@ 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(7);
|
||||
expect(result.save.saveVersion).toBe(8);
|
||||
expect(result.save.player.welcomeSeen).toBe(true);
|
||||
expect(result.save).toEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe("save migration 7 → 8: shop time counts as rest (CR-023)", () => {
|
||||
it("gives no ship a pilot waiting out the shop, and changes nothing else", () => {
|
||||
const before = fullSave();
|
||||
const old = JSON.parse(JSON.stringify(before)) as Record<string, unknown> & {
|
||||
ships: Record<string, unknown>[];
|
||||
};
|
||||
old.saveVersion = 7;
|
||||
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(8);
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ const pilot = {
|
|||
dutyLimit: 14,
|
||||
resting: false,
|
||||
restEndsAt: null,
|
||||
restingInShopUntil: null,
|
||||
mode: "manual",
|
||||
orders: {
|
||||
destinationIds: null,
|
||||
|
|
|
|||
Loading…
Reference in New Issue