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.
|
- **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?".
|
- **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.
|
- **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 { content } from "@deadhead/content";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
import { formatCredits, locationName } from "../format";
|
import { formatCredits, locationName } from "../format";
|
||||||
import { useGameAction, useLoadedPlayer } from "../state/actions";
|
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.
|
// Hide as soon as the player dismisses; the refetch clears the list for real.
|
||||||
const [dismissed, setDismissed] = useState<ReadonlySet<string>>(new Set());
|
const [dismissed, setDismissed] = useState<ReadonlySet<string>>(new Set());
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
const summaries = state.summaries.filter((s) => !dismissed.has(s.dispatchId));
|
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;
|
if (summaries.length === 0) return null;
|
||||||
|
|
||||||
const shipName = (shipId: string, tutorial: boolean) =>
|
const shipName = (shipId: string, tutorial: boolean) =>
|
||||||
|
|
@ -68,6 +75,28 @@ export function AwaySummary() {
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={dismiss}
|
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 "@testing-library/jest-dom/vitest";
|
||||||
import { cleanup } from "@testing-library/react";
|
import { cleanup, configure } from "@testing-library/react";
|
||||||
import { afterEach, vi } from "vitest";
|
import { afterEach, vi } from "vitest";
|
||||||
|
|
||||||
afterEach(() => cleanup());
|
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;
|
// 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.
|
// the map's logic (projection, camera, scene, hit-testing) is unit-tested directly.
|
||||||
vi.mock("../src/map/MapCanvas", () => import("./map-canvas-double"));
|
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.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.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`).
|
- [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
|
- [ ] Step 10A.18 — Plan amendments and regression
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue