Step 10A.17: list ready goals in the away summary
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
28cb919c05
commit
f2f719415d
|
|
@ -587,3 +587,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **The map was already reachable during the tutorial** (its tab exists from the first screen); this step adds the lock and reachability.
|
||||
- **Locked locations are labelled "🔒 Name"** on the canvas (they were dimmed already), replacing the earlier "Name?".
|
||||
- **Reachability uses sim `checkRoute` (range and landing) from Earth at the current server time,** over `content.purchasableShipModels`, so it changes as planets move. When nothing can reach it the panel says so.
|
||||
|
||||
## Step 10A.17
|
||||
|
||||
- **The summary lists every goal currently ready to claim,** not only ones that became ready since the last visit. The server does not track when a goal became ready, and an unclaimed ready goal is worth pointing out on any return.
|
||||
- **"Claim on HQ" acknowledges the deliveries and navigates to `/#goals`.**
|
||||
- **Web tests wait up to 3 s for elements** (`asyncUtilTimeout`); the 1 s default occasionally ran out under a fully parallel run.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { content } from "@deadhead/content";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { formatCredits, locationName } from "../format";
|
||||
import { useGameAction, useLoadedPlayer } from "../state/actions";
|
||||
|
||||
|
|
@ -10,7 +11,13 @@ export function AwaySummary() {
|
|||
// Hide as soon as the player dismisses; the refetch clears the list for real.
|
||||
const [dismissed, setDismissed] = useState<ReadonlySet<string>>(new Set());
|
||||
|
||||
const navigate = useNavigate();
|
||||
const summaries = state.summaries.filter((s) => !dismissed.has(s.dispatchId));
|
||||
// Goals waiting to be claimed (Step 10A.17).
|
||||
const readyGoals = state.goals
|
||||
.filter((g) => g.status === "ready")
|
||||
.map((g) => content.onboardingGoal(g.id))
|
||||
.filter((g) => g !== undefined);
|
||||
if (summaries.length === 0) return null;
|
||||
|
||||
const shipName = (shipId: string, tutorial: boolean) =>
|
||||
|
|
@ -68,6 +75,28 @@ export function AwaySummary() {
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{readyGoals.length > 0 && (
|
||||
<div className="mt-3 rounded-lg border border-emerald-500/40 bg-emerald-500/10 p-3 text-sm">
|
||||
<h3 className="font-semibold text-emerald-300">Goals ready to claim</h3>
|
||||
<ul aria-label="Ready goals" className="mt-1 space-y-0.5">
|
||||
{readyGoals.map((goal) => (
|
||||
<li key={goal.id}>
|
||||
{goal.title} <span className="text-slate-400">+{formatCredits(goal.reward)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
dismiss();
|
||||
void navigate({ pathname: "/", hash: "#goals" });
|
||||
}}
|
||||
className="mt-2 font-medium text-emerald-300 underline"
|
||||
>
|
||||
Claim on HQ
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismiss}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import { content } from "@deadhead/content";
|
||||
import type { PlayerState } from "@deadhead/schema";
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fakeApi, renderApp, samplePlayerState } from "./render";
|
||||
|
||||
function returningPlayer(acknowledged: boolean): PlayerState {
|
||||
return samplePlayerState({
|
||||
tutorial: null,
|
||||
company: { id: "c", name: "D", credits: 900, loanBalance: 4000 },
|
||||
ships: [
|
||||
{
|
||||
id: "s1",
|
||||
modelId: "courier",
|
||||
name: "Dusty Mule",
|
||||
status: "idle",
|
||||
locationId: "moon",
|
||||
dispatchId: null,
|
||||
pilotId: "avatar",
|
||||
},
|
||||
],
|
||||
summaries: acknowledged
|
||||
? []
|
||||
: [
|
||||
{
|
||||
dispatchId: "d1",
|
||||
shipId: "s1",
|
||||
pilotId: "avatar",
|
||||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
tutorial: false,
|
||||
credits: 538,
|
||||
loanRepayment: 230,
|
||||
pilotXp: 10,
|
||||
discoveredLocationId: null,
|
||||
scriptedId: null,
|
||||
resolvedAt: 1,
|
||||
},
|
||||
],
|
||||
goals: content.onboardingGoals.map((g) => ({
|
||||
id: g.id,
|
||||
status: g.id === "first-own-delivery" ? "ready" : "incomplete",
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
describe("something waiting on return", () => {
|
||||
it("lists ready goals alongside the delivery, and the link opens HQ's goals", async () => {
|
||||
let acknowledged = false;
|
||||
const api = fakeApi({
|
||||
getPlayerState: () => returningPlayer(acknowledged),
|
||||
acknowledgeSummaries: () => {
|
||||
acknowledged = true;
|
||||
return { acknowledged: 1 };
|
||||
},
|
||||
});
|
||||
renderApp(api, "/fleet");
|
||||
|
||||
const dialog = await screen.findByRole("dialog", { name: "While you were away" });
|
||||
expect(dialog).toHaveTextContent("Dusty Mule");
|
||||
const ready = within(dialog).getByRole("list", { name: "Ready goals" });
|
||||
expect(ready).toHaveTextContent(content.onboardingGoal("first-own-delivery")!.title);
|
||||
|
||||
await userEvent.click(within(dialog).getByRole("button", { name: "Claim on HQ" }));
|
||||
expect(screen.queryByRole("dialog", { name: "While you were away" })).not.toBeInTheDocument();
|
||||
expect(await screen.findByRole("heading", { level: 1, name: "HQ" })).toBeInTheDocument();
|
||||
const goals = screen.getByRole("region", { name: "Goals" });
|
||||
expect(within(goals).getByRole("button", { name: "Claim" })).toBeInTheDocument();
|
||||
await waitFor(() =>
|
||||
expect(api.call).toHaveBeenCalledWith("acknowledgeSummaries", { dispatchIds: ["d1"] }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not list goals when none are ready", async () => {
|
||||
const state = returningPlayer(false);
|
||||
const quiet = {
|
||||
...state,
|
||||
goals: state.goals.map((g) => ({ ...g, status: "incomplete" as const })),
|
||||
};
|
||||
renderApp(fakeApi({ getPlayerState: () => quiet }));
|
||||
const dialog = await screen.findByRole("dialog", { name: "While you were away" });
|
||||
expect(within(dialog).queryByRole("list", { name: "Ready goals" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { cleanup, configure } from "@testing-library/react";
|
||||
import { afterEach, vi } from "vitest";
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
// findBy*/waitFor default to 1 s, which a busy parallel run can exceed; wait up to 3 s.
|
||||
configure({ asyncUtilTimeout: 3000 });
|
||||
|
||||
// PixiJS needs WebGL, which jsdom lacks. Component tests use a stand-in canvas;
|
||||
// the map's logic (projection, camera, scene, hit-testing) is unit-tested directly.
|
||||
vi.mock("../src/map/MapCanvas", () => import("./map-canvas-double"));
|
||||
|
|
|
|||
|
|
@ -122,5 +122,5 @@ Spec: `memory-bank/change-requests/CR-001-faster-start.md`. Decisions recorded a
|
|||
- [x] **Step 10A.14 — Client: loan on HQ.** HQ's savings goal is replaced: during the tutorial a "Company runs: n of 4" card; afterwards `LoanCard` (balance, paid-off bar, "Pay down" with amounts above credits or balance blocked in the UI). Delivery summaries show "X cr went to the lender". Step 9.1 tutorial-goal tests replaced.
|
||||
- [x] **Step 10A.15 — Client: goals on HQ.** `src/components/GoalsCard.tsx` (`#goals`): the next three unclaimed goals with rewards; ready goals have "Claim", which calls `claimGoal`; the refreshed state brings the next goal into view. Hidden during the tutorial.
|
||||
- [x] **Step 10A.16 — Client: whole map from the start.** The Map tab works from the first screen (tutorial included); undiscovered locations are labelled with a lock; tapping one shows "Undiscovered — deliver here to unlock" and "Reachable with: …" from the sim route check over purchasable models from Earth (`src/map/reach.ts`).
|
||||
- [ ] Step 10A.17 — Client: something waiting on return
|
||||
- [x] **Step 10A.17 — Client: something waiting on return.** The "while you were away" dialog lists goals ready to claim, with a "Claim on HQ" link that dismisses it and opens HQ's goals.
|
||||
- [ ] Step 10A.18 — Plan amendments and regression
|
||||
|
|
|
|||
Loading…
Reference in New Issue