diff --git a/DECISIONS.md b/DECISIONS.md index f900acb..ad9c39f 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1831,3 +1831,11 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **"Back to work" stamps the time itself**, as well as through the catalog flag: the engine stamps after the handler, and the command's reply (the player state) should already read awake. - **Waking needs no other change**: pilots asleep were left waiting at the time of the last catch-up, which is the moment of the waking command, so the next catch-up has them decide from then. + +## Step 24.5 + +- **The banner and the Bridge status show only with an Independent pilot.** Sleep only stops Independent pilots; with none, "Fleet asleep" would ask the player to wake nobody. +- **The Bridge status sits right under the header**, before the ship panels, so a returning player sees it on a phone without scrolling. +- **The banner shows inside the away screen when it opens**, which needs unseen summaries. A fleet that went quiet with nothing new to report still shows the Bridge status. +- **Changing the threshold is itself a manual decision**: it's a game command the player makes, so it also wakes the fleet. +- **"3 days", not "72 h"**: thresholds over 48 hours that are whole days are written in days. diff --git a/apps/web/src/components/AwaySummary.tsx b/apps/web/src/components/AwaySummary.tsx index 699b504..08af687 100644 --- a/apps/web/src/components/AwaySummary.tsx +++ b/apps/web/src/components/AwaySummary.tsx @@ -7,6 +7,7 @@ import { formatCredits, locationName, toLocation } from "../format"; import { formatLocalTime } from "../screens/DispatchSheet"; import { useGameAction, useLoadedPlayer } from "../state/actions"; import { Button, Icon, Sheet } from "../ui"; +import { SleepBanner } from "./FleetSleep"; import { PilotLogbook, pilotLogbook } from "./PilotLogbook"; /** "While you were away": deliveries resolved since the player last looked (Step 9.2). */ @@ -46,6 +47,7 @@ export function AwaySummary() { return ( +
    {yours.map((s) => ( diff --git a/apps/web/src/components/FleetSleep.tsx b/apps/web/src/components/FleetSleep.tsx new file mode 100644 index 0000000..a1a9da4 --- /dev/null +++ b/apps/web/src/components/FleetSleep.tsx @@ -0,0 +1,77 @@ +import type { PlayerState } from "@deadhead/schema"; +import { formatLocalTime } from "../screens/DispatchSheet"; +import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; +import { Button, Icon, Panel } from "../ui"; + +/** A sleep threshold in words: "12 h", "24 h", "48 h", "3 days" (CR-024 §2). */ +export function sleepLabel(hours: number): string { + return hours > 48 && hours % 24 === 0 ? `${hours / 24} days` : `${hours} h`; +} + +/** The sleep time on the device, e.g. "Mar 4, 14:05". */ +export function formatSleepTime(time: number): string { + const date = new Date(time).toLocaleDateString(undefined, { month: "short", day: "numeric" }); + return `${date}, ${formatLocalTime(time)}`; +} + +/** + * The fleet has gone quiet (decision 0.5.137) and it matters: some pilot is + * Independent, so there is work waiting on the player's orders. + */ +export function fleetQuiet(state: PlayerState): boolean { + return state.fleetSleep.asleep && state.pilots.some((p) => p.mode === "independent"); +} + +/** "Back to work": a manual decision that wakes the fleet (decision 0.5.138). */ +function BackToWork() { + const wake = useGameAction("backToWork"); + return ( + <> + + {wake.isError && ( +

    + {errorMessage(wake.error)} +

    + )} + + ); +} + +/** The "while you were away" banner: when the fleet went quiet, and why. */ +export function SleepBanner() { + const state = useLoadedPlayer(); + if (!fleetQuiet(state)) return null; + const { sleepsAt, afterHours } = state.fleetSleep; + return ( + +

    + + Your fleet went quiet on {formatSleepTime(sleepsAt)} after {sleepLabel(afterHours)} without + orders. +

    + +
    + ); +} + +/** The Bridge's status while the fleet sleeps. */ +export function FleetAsleep() { + const state = useLoadedPlayer(); + if (!fleetQuiet(state)) return null; + return ( + +

    + + Fleet asleep — waiting for your orders +

    +

    + Independent pilots start no new jobs after {sleepLabel(state.fleetSleep.afterHours)} without + orders. Any decision wakes them. +

    + +
    + ); +} diff --git a/apps/web/src/components/IndependentPilots.tsx b/apps/web/src/components/IndependentPilots.tsx index b9dea6a..763224d 100644 --- a/apps/web/src/components/IndependentPilots.tsx +++ b/apps/web/src/components/IndependentPilots.tsx @@ -78,6 +78,8 @@ export function independentStatus(state: PlayerState, pilot: Pilot): string | nu return `Resting until ${formatLocalTime(pilot.restEndsAt)}`; } if (!pilot.assignedShipId) return "Independent: needs a ship"; + // The fleet has gone quiet: nothing new until the player decides (decision 0.5.137). + if (state.fleetSleep.asleep) return "Waiting for orders"; return `Waiting for new contracts at ${locationName(pilot.locationId ?? "")}`; } diff --git a/apps/web/src/screens/Dashboard.tsx b/apps/web/src/screens/Dashboard.tsx index a97339e..9299eca 100644 --- a/apps/web/src/screens/Dashboard.tsx +++ b/apps/web/src/screens/Dashboard.tsx @@ -1,4 +1,5 @@ import { content } from "@deadhead/content"; +import { FleetAsleep } from "../components/FleetSleep"; import { DispatchOfficePanel } from "../components/IndependentPilots"; import type { Dispatch, Pilot, PlayerState, Ship, TutorialState } from "@deadhead/schema"; import { dispatchProgress } from "@deadhead/sim"; @@ -93,6 +94,8 @@ export function Dashboard() { return ( + {/* While the fleet sleeps, first thing on the Bridge (CR-024). */} +
    diff --git a/apps/web/src/screens/Settings.tsx b/apps/web/src/screens/Settings.tsx index c33c95e..de69dcf 100644 --- a/apps/web/src/screens/Settings.tsx +++ b/apps/web/src/screens/Settings.tsx @@ -7,7 +7,9 @@ import { About } from "./AboutSettings"; import { CompanyName, NewCompany } from "./CompanySettings"; import { DevTools } from "./DevTools"; import { ExportSave, ImportSave } from "./SaveSettings"; -import { useLoadedPlayer } from "../state/actions"; +import { content } from "@deadhead/content"; +import { sleepLabel } from "../components/FleetSleep"; +import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; import { usePreferences, type ClockFormat, type CountdownFormat } from "../state/preferences"; import { Button, Panel, Screen, ScreenHeader } from "../ui"; @@ -29,6 +31,7 @@ export function Settings() {
    +
    @@ -123,6 +126,31 @@ function Section({ title, children }: { title: string; children: ReactNode }) { ); } +/** "Fleet goes quiet after": the sleep threshold (decision 0.5.137), a company setting. */ +function SleepThreshold() { + const state = useLoadedPlayer(); + const save = useGameAction("setSleepThreshold"); + if (state.tutorial) return null; + return ( +
    + + label="Fleet goes quiet after" + value={String(state.fleetSleep.afterHours)} + options={content.config.fleetSleep.optionsHours.map((h) => [String(h), sleepLabel(h)])} + onChange={(value) => save.mutate({ hours: Number(value) })} + /> +

    + Without any orders from you for this long, Independent pilots stop taking new jobs. +

    + {save.isError && ( +

    + {errorMessage(save.error)} +

    + )} +
    + ); +} + /** A labelled radio group of a few options, styled as a segmented control. */ function Choice(props: { label: string; diff --git a/apps/web/test/fleet-sleep.test.tsx b/apps/web/test/fleet-sleep.test.tsx new file mode 100644 index 0000000..dd375c5 --- /dev/null +++ b/apps/web/test/fleet-sleep.test.tsx @@ -0,0 +1,172 @@ +import type { Pilot, PlayerState, ResolutionSummary } from "@deadhead/schema"; +import { screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useClock } from "../src/clock/clock"; +import { sleepLabel } from "../src/components/FleetSleep"; +import { independentStatus } from "../src/components/IndependentPilots"; +import { pilotExtras } from "./fixtures"; +import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; + +const NOW = Date.parse("2100-01-03T12:00:00Z"); +const SLEPT = Date.parse("2100-01-02T09:30:00Z"); + +const rosa: Pilot = { + ...samplePlayerState().pilots[0]!, + id: "p1", + name: "Rosa", + isAvatar: false, + assignedShipId: "s1", + locationId: "earth", + ...pilotExtras, + mode: "independent", +}; + +const summary: ResolutionSummary = { + dispatchId: "d1", + shipId: "s1", + pilotId: "p1", + originId: "earth", + destinationId: "moon", + tutorial: false, + independent: true, + deadhead: false, + fuelCost: 100, + topUp: null, + goodId: "consumer-goods", + massTonnes: 6, + shipper: "Pacific Orbital Logistics", + credits: 600, + loanRepayment: 0, + pilotPay: 200, + pilotXp: 10, + discoveredLocationId: null, + scriptedId: null, + restEndsAt: null, + resolvedAt: SLEPT - 60_000, +}; + +/** An owner with Rosa flying Independent; asleep since SLEPT, or awake. */ +function owner(asleep: boolean, over: Partial = {}, afterHours = 24): PlayerState { + return samplePlayerState({ + tutorial: null, + bays: [{ id: "b", locationId: "earth" }], + pilots: [samplePlayerState().pilots[0]!, rosa], + ships: [ + { + id: "s1", + modelId: "courier", + name: "Wren", + status: "idle", + locationId: "earth", + dispatchId: null, + lastArrivalAt: 0, + ...shipExtras("courier"), + pilotId: "p1", + }, + ], + dispatchOffice: { owned: true, spendingFloor: 500 }, + fleetSleep: { afterHours, sleepsAt: asleep ? SLEPT : NOW + 3_600_000, asleep }, + ...over, + }); +} + +/** A fake API whose state wakes up on "Back to work". */ +function sleepyApi(over: Partial = {}) { + let state = owner(true, over); + const api = fakeApi({ + getPlayerState: () => state, + backToWork: () => { + state = owner(false, over); + return state; + }, + acknowledgeSummaries: () => ({ acknowledged: 0 }), + }); + return api; +} + +describe("fleet sleep on screen (Step 24.5, decisions 0.5.137, 0.5.138)", () => { + beforeEach(() => { + vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); + vi.setSystemTime(NOW); + useClock.setState({ offsetMs: 0, synced: true, now: NOW }); + }); + afterEach(() => vi.useRealTimers()); + + it("shows the Bridge status while asleep, and Back to work wakes the fleet", async () => { + const api = sleepyApi(); + renderApp(api, "/"); + const panel = await screen.findByLabelText("Fleet asleep"); + expect(panel).toHaveTextContent("Fleet asleep — waiting for your orders"); + await userEvent.click(within(panel).getByRole("button", { name: "Back to work" })); + expect(api.call).toHaveBeenCalledWith("backToWork", {}); + await waitFor(() => expect(screen.queryByLabelText("Fleet asleep")).not.toBeInTheDocument()); + }); + + it("shows nothing on the Bridge while awake", async () => { + renderApp(fakeApi({ getPlayerState: () => owner(false) }), "/"); + expect(await screen.findByRole("heading", { name: "Dashboard" })).toBeInTheDocument(); + expect(screen.queryByLabelText("Fleet asleep")).not.toBeInTheDocument(); + }); + + it("shows nothing on the Bridge when no pilot is Independent: nobody is waiting", async () => { + const manual = owner(true, { + pilots: [samplePlayerState().pilots[0]!, { ...rosa, mode: "manual" }], + }); + renderApp(fakeApi({ getPlayerState: () => manual }), "/"); + expect(await screen.findByRole("heading", { name: "Dashboard" })).toBeInTheDocument(); + expect(screen.queryByLabelText("Fleet asleep")).not.toBeInTheDocument(); + }); + + it("puts the sleep banner on the while-you-were-away screen, until Back to work", async () => { + const api = sleepyApi({ summaries: [summary] }); + renderApp(api, "/"); + const sheet = await screen.findByRole("dialog", { name: "While you were away" }); + const banner = within(sheet).getByLabelText("Fleet asleep"); + expect(banner).toHaveTextContent( + /^Your fleet went quiet on .+, .+ after 24 h without orders\./, + ); + await userEvent.click(within(banner).getByRole("button", { name: "Back to work" })); + await waitFor(() => + expect(within(sheet).queryByLabelText("Fleet asleep")).not.toBeInTheDocument(), + ); + }); + + it("leaves the banner off the away screen when the fleet didn't go quiet", async () => { + renderApp(fakeApi({ getPlayerState: () => owner(false, { summaries: [summary] }) }), "/"); + const sheet = await screen.findByRole("dialog", { name: "While you were away" }); + expect(within(sheet).queryByLabelText("Fleet asleep")).not.toBeInTheDocument(); + }); + + it("says Waiting for orders for a sleeping Independent pilot", () => { + const asleep = owner(true); + expect(independentStatus(asleep, rosa)).toBe("Waiting for orders"); + expect(independentStatus(owner(false), rosa)).toBe("Waiting for new contracts at Earth"); + }); + + it("saves the threshold in Settings → Company and shows the new one", async () => { + let state = owner(false); + const api = fakeApi({ + getPlayerState: () => state, + setSleepThreshold: ({ hours }) => { + state = owner(false, {}, hours); + return state; + }, + }); + renderApp(api, "/settings"); + const group = await screen.findByRole("group", { name: "Fleet goes quiet after" }); + expect( + within(group) + .getAllByRole("radio") + .map((r) => r.parentElement!.textContent), + ).toEqual(["12 h", "24 h", "48 h", "3 days"]); + expect(within(group).getByRole("radio", { name: "24 h" })).toBeChecked(); + await userEvent.click(within(group).getByRole("radio", { name: "3 days" })); + expect(api.call).toHaveBeenCalledWith("setSleepThreshold", { hours: 72 }); + await waitFor(() => expect(within(group).getByRole("radio", { name: "3 days" })).toBeChecked()); + }); + + it("words the thresholds in hours, then days", () => { + expect([12, 24, 48, 72].map(sleepLabel)).toEqual(["12 h", "24 h", "48 h", "3 days"]); + }); +}); diff --git a/docs/screenshots/24-5-away-banner-desktop.png b/docs/screenshots/24-5-away-banner-desktop.png new file mode 100644 index 0000000..4d4b9d5 Binary files /dev/null and b/docs/screenshots/24-5-away-banner-desktop.png differ diff --git a/docs/screenshots/24-5-away-banner-phone.png b/docs/screenshots/24-5-away-banner-phone.png new file mode 100644 index 0000000..8f21e84 Binary files /dev/null and b/docs/screenshots/24-5-away-banner-phone.png differ diff --git a/docs/screenshots/24-5-bridge-asleep-desktop.png b/docs/screenshots/24-5-bridge-asleep-desktop.png new file mode 100644 index 0000000..3245390 Binary files /dev/null and b/docs/screenshots/24-5-bridge-asleep-desktop.png differ diff --git a/docs/screenshots/24-5-bridge-asleep-phone.png b/docs/screenshots/24-5-bridge-asleep-phone.png new file mode 100644 index 0000000..22ed8e5 Binary files /dev/null and b/docs/screenshots/24-5-bridge-asleep-phone.png differ diff --git a/docs/screenshots/24-5-settings-desktop.png b/docs/screenshots/24-5-settings-desktop.png new file mode 100644 index 0000000..64ec3d2 Binary files /dev/null and b/docs/screenshots/24-5-settings-desktop.png differ diff --git a/docs/screenshots/24-5-settings-phone.png b/docs/screenshots/24-5-settings-phone.png new file mode 100644 index 0000000..d0082f5 Binary files /dev/null and b/docs/screenshots/24-5-settings-phone.png differ diff --git a/memory-bank/architecture.md b/memory-bank/architecture.md index e122bd5..c05d25c 100644 --- a/memory-bank/architecture.md +++ b/memory-bank/architecture.md @@ -64,6 +64,7 @@ The `routes` catalog is the list of engine commands (CR-014). Each entry has `re | `spendSkillPoint` | `POST /api/pilots/:pilotId/skills` | `pilotId`, `skillId` | `{ pilot }` | yes | | `acknowledgeSummaries` | `POST /api/summaries/acknowledge` | `dispatchIds`, optional `shipIds` (finished upgrades seen; at least one id overall) | `{ acknowledged }` | yes | | `acceptDeal` | `POST /api/deal/accept` | — | `PlayerState` | yes | +| `setSleepThreshold` | `POST /api/fleet/sleep-after` | `hours` (one of `fleetSleep.optionsHours`, else `validation-error`) | `PlayerState` | yes; a manual decision (CR-024) | | `backToWork` | `POST /api/fleet/wake` | — | `PlayerState` (awake) | yes; a manual decision that does nothing else: wakes the fleet (CR-024) | | `exportSave` | engine only (CR-014) | — | `{ code }`: `DEADHEAD1.` + base64url(gzip(save JSON)) | yes | | `importSave` | engine only (CR-014) | `code` (whitespace ignored) | `{ playerId, state }`; `invalid-save` if it isn't a good save | no | diff --git a/memory-bank/progress.md b/memory-bank/progress.md index c714b81..20f70ee 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -525,6 +525,6 @@ Spec: `memory-bank/change-requests/CR-024-fleet-sleep.md` (received as "CR-022 - [x] **Step 24.2 — Engine: manual decision flag.** `RouteDef.manualDecision` is required on every command in the catalog. Manual: dispatch and deadhead (`createDispatch`), buying ships, upgrades, refuelling, refuel policy, emergency fuel, leasing, hiring, assigning and unassigning, resting a pilot, skill points, the Dispatch Office, pilot mode and orders, the spending floor, the deal, renaming the company, the loan and goals. Not: reads, `acknowledgeSummaries`, `markWelcomeSeen`, export and import, new company, dev tools. The engine stamps `lastManualDecisionAt` after a flagged command succeeds; Independent pilots' trips go through the commands' functions, not the catalog, so they never do. Tests: `packages/schema/test/routes.test.ts` (every command declares it; the manual list); `packages/engine/test/fleet-sleep.test.ts` (a dispatch stamps it; viewing, a board, dismissing a summary, the welcome and an export don't; a refused command doesn't; Independent pilots' trips don't). - [x] **Step 24.3 — Engine: sleep in live play and catch-up.** `catchUp` processes Independent pilots' decisions only before the sleep time (`sleepsAt`, in `state.ts`), alongside the 7-day cap; arrivals, rest and shop ends resolve as before, and pilots due later wait at `until`. Live play is the same catch-up run by every command and by the loop. `PlayerState.fleetSleep` (`afterHours`, `sleepsAt`, `asleep`) tells the client, which shows "Waiting for orders" from it (Step 24.5). Tests (`packages/engine/test/fleet-sleep.test.ts`): two Independent pilots left 7 days start nothing at or after 24 hours, and the trips in flight then still resolve with pay; one go equals 5-hour pieces over 3 days; a trip the player sent lands after the sleep time as normal; a 48-hour threshold moves the cutoff. The 7-day cap test now uses a 30-day threshold so only the cap stops the pilots. - [x] **Step 24.4 — Engine: waking and "Back to work".** New command `backToWork` (`POST /api/fleet/wake`, a manual decision): it sets the last decision to now and returns the player state, awake. Any manual decision does the same, and pilots waiting at that time decide from it. Tests (`packages/engine/test/fleet-sleep.test.ts`): after two days asleep, "Back to work" brings Independent trips again, none before the moment of waking; so does buying fuel. `packages/schema/test/routes.test.ts` lists it as manual. -- [ ] Step 24.5 — Client: return banner, status, and setting +- [x] **Step 24.5 — Client: return banner, status, and setting.** `apps/web/src/components/FleetSleep.tsx`: `SleepBanner` at the top of "While you were away" ("Your fleet went quiet on [date, time] after [24 h] without orders." and Back to work) and `FleetAsleep` under the Bridge's header ("Fleet asleep — waiting for your orders", the same button), both only while `fleetSleep.asleep` and some pilot is Independent. `independentStatus` reads "Waiting for orders" while asleep (after flying, the shop and rest). Settings → Company: "Fleet goes quiet after" (12 h / 24 h / 48 h / 3 days) through the new `setSleepThreshold` command (one of the offered times; a manual decision). Tests: `apps/web/test/fleet-sleep.test.tsx` (the Bridge status asleep only, not awake or with no Independent pilot, and Back to work clears it; the banner on the away screen and its button; no banner awake; the pilot status; the setting saves 3 days and shows it checked; the labels); `packages/engine/test/fleet-sleep.test.ts` (the setting takes an offered time, wakes the fleet and refuses 36 h). Screenshots: `docs/screenshots/24-5-away-banner-{phone,desktop}.png`, `24-5-bridge-asleep-{phone,desktop}.png`, `24-5-settings-{phone,desktop}.png`. - [ ] Step 24.6 — Client: sleep notification (native) - [ ] Step 24.7 — Plan amendments and regression diff --git a/packages/engine/src/game/commands.ts b/packages/engine/src/game/commands.ts index e78bab4..2e501c9 100644 --- a/packages/engine/src/game/commands.ts +++ b/packages/engine/src/game/commands.ts @@ -74,6 +74,15 @@ export function commandTable(): CommandTable { save.company.lastManualDecisionAt = now; return playerState(save, now); }, + // One of the offered thresholds (decision 0.5.137); a decision, so it also wakes the fleet. + setSleepThreshold: ({ save, now }, input) => { + if (!content.config.fleetSleep.optionsHours.includes(input.hours)) { + throw fail("validation-error", "Choose one of the offered times."); + } + save.company.sleepAfterHours = input.hours; + save.company.lastManualDecisionAt = now; + return playerState(save, now); + }, markWelcomeSeen: ({ save, now }) => { save.player.welcomeSeen = true; return playerState(save, now); diff --git a/packages/engine/test/fleet-sleep.test.ts b/packages/engine/test/fleet-sleep.test.ts index 289ccfa..ce85e54 100644 --- a/packages/engine/test/fleet-sleep.test.ts +++ b/packages/engine/test/fleet-sleep.test.ts @@ -229,3 +229,21 @@ describe("waking the fleet (Step 24.4, decision 0.5.138)", () => { expect((await tripsFrom(test, wake)).length).toBeGreaterThan(0); }); }); + +describe("the sleep threshold setting (Step 24.5, decision 0.5.137)", () => { + it("takes one of the offered times, counts as a decision, and refuses others", async () => { + const test = newTestApp(); + await signUpOwner(test.app); + test.clock.now += HOUR; + const state = await test.app.call("setSleepThreshold", { hours: 72 }); + expect(state.fleetSleep).toEqual({ + afterHours: 72, + sleepsAt: test.clock.now + 72 * HOUR, + asleep: false, + }); + await expect(test.app.call("setSleepThreshold", { hours: 36 })).rejects.toMatchObject({ + code: "validation-error", + }); + expect((await readSave(test.app)).company.sleepAfterHours).toBe(72); + }); +}); diff --git a/packages/schema/src/requests.ts b/packages/schema/src/requests.ts index 38d9aa3..9f82d73 100644 --- a/packages/schema/src/requests.ts +++ b/packages/schema/src/requests.ts @@ -83,6 +83,8 @@ export const pilotResponseSchema = z.object({ pilot: pilotSchema }); export const pilotModeRequestSchema = z.object({ pilotId: idSchema, mode: pilotModeSchema }); export const pilotOrdersRequestSchema = z.object({ pilotId: idSchema, orders: pilotOrdersSchema }); /** CR-021: the credits Independent pilots never spend below (decision 0.5.127). */ +/** Settings → Company: real hours without orders before the fleet goes quiet (CR-024). */ +export const sleepThresholdRequestSchema = z.object({ hours: z.number().positive() }); export const spendingFloorRequestSchema = z.object({ credits: z.number().int().nonnegative() }); export const dispatchOfficeResponseSchema = z.object({ company: companySchema, diff --git a/packages/schema/src/routes.ts b/packages/schema/src/routes.ts index 5b679b4..ad7be7f 100644 --- a/packages/schema/src/routes.ts +++ b/packages/schema/src/routes.ts @@ -35,6 +35,7 @@ import { restPilotRequestSchema, shipResponseSchema, spendSkillPointRequestSchema, + sleepThresholdRequestSchema, spendingFloorRequestSchema, startUpgradeRequestSchema, unassignPilotRequestSchema, @@ -249,6 +250,14 @@ export const routes = { // "Back to work" wakes the fleet: it is a decision (decision 0.5.138). manualDecision: true, }, + setSleepThreshold: { + method: "POST", + path: "/api/fleet/sleep-after", + request: sleepThresholdRequestSchema, + response: playerStateSchema, + auth: true, + manualDecision: true, + }, markWelcomeSeen: { method: "POST", path: "/api/welcome/seen", diff --git a/packages/schema/test/routes.test.ts b/packages/schema/test/routes.test.ts index 3f1edc7..05fc428 100644 --- a/packages/schema/test/routes.test.ts +++ b/packages/schema/test/routes.test.ts @@ -23,6 +23,7 @@ describe("route catalog", () => { "startUpgrade", "refuelShip", "setRefuelPolicy", + "setSleepThreshold", "orderEmergencyFuel", "spendSkillPoint", "buyDispatchOffice", @@ -112,6 +113,7 @@ describe("manual decisions in the catalog (Step 24.2, decision 0.5.136)", () => "setPilotMode", "setPilotOrders", "setRefuelPolicy", + "setSleepThreshold", "setSpendingFloor", "spendSkillPoint", "startUpgrade",