Step 10D.9: rest in the away summary and on HQ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ea561b4d74
commit
9939cd3eed
|
|
@ -813,3 +813,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **"Eligible" means the dispatch sheet would allow it:** a ship is stuck when `pairOptions` finds no contract on this board with no problems for that ship and its pilot. Uncrewed ships and resting pilots get no prompt (resting pilots already show their countdown elsewhere).
|
||||
- **One prompt per stuck ship**, naming the ship and pilot, so a location with several idle ships stays clear.
|
||||
- **`Countdown` takes an optional `format`**; the board refresh uses `mm:ss` (`formatClock`) as the CR asks.
|
||||
|
||||
## Step 10D.9
|
||||
|
||||
- **The summary gives the wake time as the device's local clock time** (like the dispatch sheet's real ETA), since the player reads it on returning; HQ gives a live countdown instead.
|
||||
- **The HQ rest card is always shown after the tutorial**, with "Everyone is ready to fly." when nobody rests, so the player learns the mechanic exists before it first bites.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { content } from "@deadhead/content";
|
|||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { formatCredits, locationName } from "../format";
|
||||
import { formatLocalTime } from "../screens/DispatchSheet";
|
||||
import { useGameAction, useLoadedPlayer } from "../state/actions";
|
||||
|
||||
/** "While you were away": deliveries resolved since the player last looked (Step 9.2). */
|
||||
|
|
@ -72,6 +73,12 @@ export function AwaySummary() {
|
|||
Discovered {locationName(s.discoveredLocationId)}!
|
||||
</div>
|
||||
)}
|
||||
{s.restEndsAt !== null && (
|
||||
<div className="mt-1 text-sky-300">
|
||||
{state.pilots.find((p) => p.id === s.pilotId)?.name ?? "The pilot"} is resting
|
||||
until {formatLocalTime(s.restEndsAt)}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ export function HQ() {
|
|||
const flying = state.ships.filter((s) => s.status === "in-flight").length;
|
||||
const runsBeforeDeal = content.config.tutorialRunsBeforeDeal;
|
||||
const next = [...state.activeDispatches].sort((a, b) => a.arrivalAt - b.arrivalAt)[0];
|
||||
// Resting pilots and who wakes first (CR-004).
|
||||
const resting = state.pilots
|
||||
.filter((p) => p.resting && p.restEndsAt !== null)
|
||||
.sort((a, b) => a.restEndsAt! - b.restEndsAt!);
|
||||
|
||||
return (
|
||||
<section className="space-y-4 p-4">
|
||||
|
|
@ -52,6 +56,19 @@ export function HQ() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{!state.tutorial && (
|
||||
<div aria-label="Pilot rest" className="rounded-lg border border-slate-800 p-3">
|
||||
<h2 className="text-sm font-semibold text-slate-300">Pilots resting: {resting.length}</h2>
|
||||
{resting[0] ? (
|
||||
<p className="mt-1">
|
||||
{resting[0].name} wakes in <Countdown to={resting[0].restEndsAt!} done="now" />
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-1 text-sm text-slate-400">Everyone is ready to fly.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!state.tutorial && state.ships.length === 0 && (
|
||||
<p className="text-sm text-slate-400">No ships yet — buy one in the Market.</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import type { Pilot, ResolutionSummary } from "@deadhead/schema";
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useClock } from "../src/clock/clock";
|
||||
import { formatLocalTime } from "../src/screens/DispatchSheet";
|
||||
import { fakeApi, renderApp, samplePlayerState } from "./render";
|
||||
|
||||
const NOW = Date.parse("2100-01-01T00:00:00Z");
|
||||
|
||||
const pilot = (id: string, name: string, extra: Partial<Pilot> = {}): Pilot => ({
|
||||
id,
|
||||
name,
|
||||
isAvatar: false,
|
||||
xp: 0,
|
||||
level: 0,
|
||||
skills: {},
|
||||
skillPointsAvailable: 0,
|
||||
assignedShipId: null,
|
||||
status: "idle",
|
||||
locationId: "earth",
|
||||
dutyHours: 0,
|
||||
dutyLimit: 12,
|
||||
resting: false,
|
||||
restEndsAt: null,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const summary = (extra: Partial<ResolutionSummary>): ResolutionSummary => ({
|
||||
dispatchId: "d1",
|
||||
shipId: "s1",
|
||||
pilotId: "p-rae",
|
||||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
tutorial: false,
|
||||
credits: 500,
|
||||
loanRepayment: 0,
|
||||
pilotXp: 10,
|
||||
discoveredLocationId: null,
|
||||
scriptedId: null,
|
||||
restEndsAt: null,
|
||||
resolvedAt: NOW,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const pilots = [
|
||||
pilot("p-rae", "Rae Okafor", { resting: true, restEndsAt: NOW + 40 * 60_000 }),
|
||||
pilot("p-jun", "Jun Sato", { resting: true, restEndsAt: NOW + 9 * 60_000 }),
|
||||
pilot("p-cy", "Cy Fresh"),
|
||||
];
|
||||
|
||||
describe("rest in summaries and on HQ (Step 10D.9)", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
|
||||
vi.setSystemTime(NOW);
|
||||
useClock.setState({ offsetMs: 0, synced: true, now: NOW });
|
||||
});
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it("says in the away summary that a pilot is resting, only for trips that ended in rest", async () => {
|
||||
const restEndsAt = NOW + 40 * 60_000;
|
||||
renderApp(
|
||||
fakeApi({
|
||||
getPlayerState: () =>
|
||||
samplePlayerState({
|
||||
tutorial: null,
|
||||
pilots,
|
||||
summaries: [
|
||||
summary({ dispatchId: "d1", restEndsAt }),
|
||||
summary({ dispatchId: "d2", pilotId: "p-cy" }),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
"/",
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog", { name: "While you were away" });
|
||||
const [rested, plain] = within(dialog).getAllByRole("listitem");
|
||||
expect(rested).toHaveTextContent(`Rae Okafor is resting until ${formatLocalTime(restEndsAt)}`);
|
||||
expect(plain).not.toHaveTextContent("resting");
|
||||
});
|
||||
|
||||
it("shows how many pilots are resting and who wakes first", async () => {
|
||||
renderApp(
|
||||
fakeApi({ getPlayerState: () => samplePlayerState({ tutorial: null, pilots }) }),
|
||||
"/",
|
||||
);
|
||||
const card = await screen.findByLabelText("Pilot rest");
|
||||
expect(card).toHaveTextContent("Pilots resting: 2");
|
||||
expect(card).toHaveTextContent("Jun Sato wakes in 9m 00s");
|
||||
});
|
||||
});
|
||||
|
|
@ -172,5 +172,5 @@ Spec: `memory-bank/change-requests/CR-004-pilot-duty-rest.md`. New decisions rec
|
|||
- [x] **Step 10D.6 — Client: pilot duty on the Pilots screen.** Each pilot card shows a duty bar ("9.0 h of 14 h", `progressbar` with the hours and limit); resting pilots show "Resting · wakes in …" instead. Idle, non-resting pilots (after the tutorial) have "Rest now" with an inline confirmation stating the rest length and that it covers at least one board refresh, then call `restPilot`. Test: `pilots-duty.test.tsx`.
|
||||
- [x] **Step 10D.7 — Client: duty in the dispatch flow.** The dispatch sheet disables a resting pilot's pair with "Resting — mm:ss" (new `formatClock`). Eligible pairs show "Duty after" (sim `tripGameHours` + `dutyAfterTrip`) and "Will need rest on arrival" when `needsRest`. Client validation now passes the pilot's real `restEndsAt` (dispatch sheet and deadhead picker, which shows "Pilot is resting"). Test: `dispatch-duty.test.tsx`.
|
||||
- [x] **Step 10D.8 — Client: board refresh and the "nothing here" case.** Every location board shows "New contracts in mm:ss" to `periodEndsAt` and refetches itself just after it. For each idle, crewed, non-resting ship at the location with no contract it can take (by the sim, via `pairOptions`), a prompt offers "Rest [pilot] — wakes to a new board" (`restPilot`) and "Deadhead (fly empty)" (opens the Fleet's deadhead picker, now exported). Test: `board-refresh.test.tsx`.
|
||||
- [ ] Step 10D.9 — Client: rest in summaries and on HQ
|
||||
- [x] **Step 10D.9 — Client: rest in summaries and on HQ.** The "While you were away" entry for a trip that ended in mandatory rest says "[Pilot] is resting until [local time]". HQ (after the tutorial) has a "Pilot rest" card: "Pilots resting: N" and the soonest to wake with a countdown, or "Everyone is ready to fly." Test: `rest-summary-hq.test.tsx`.
|
||||
- [ ] Step 10D.10 — Plan amendments and regression
|
||||
|
|
|
|||
Loading…
Reference in New Issue