Step 22.3: client: new company and How to play
CI / check (push) Has been cancelled
Details
CI / check (push) Has been cancelled
Details
A company started from Settings gets its own welcome; Settings -> Help reopens the cards, ending in Close (decisions 0.5.131, 0.5.132). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a75959f00a
commit
49998fb4ce
|
|
@ -1757,3 +1757,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
- **Escape skips**, like Skip.
|
- **Escape skips**, like Skip.
|
||||||
- **"Cross-fade instantly" means no animation**: with reduced motion the card changes at once. The global reduced-motion CSS would stop the slide anyway; the component also drops the class, so the rule doesn't rely on CSS alone.
|
- **"Cross-fade instantly" means no animation**: with reduced motion the card changes at once. The global reduced-motion CSS would stop the slide anyway; the component also drops the class, so the rule doesn't rely on CSS alone.
|
||||||
- **The gate hides itself as soon as the player closes it**, before the command returns, so the cards never flash back while the state refreshes.
|
- **The gate hides itself as soon as the player closes it**, before the command returns, so the cards never flash back while the state refreshes.
|
||||||
|
|
||||||
|
## Step 22.3
|
||||||
|
|
||||||
|
- **The welcome gate remembers a close per company id**: "Start a new company" keeps the save's id but makes a new company id, and the new company must get its own welcome.
|
||||||
|
- **How to play doesn't leave Settings**: "Close" just closes it; only a new company's welcome goes to Contracts.
|
||||||
|
|
|
||||||
|
|
@ -134,17 +134,18 @@ export function WelcomeGate() {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [closed, setClosed] = useState(false);
|
// Closed for this company: a new company (Settings) gets its own welcome.
|
||||||
|
const [closedFor, setClosedFor] = useState<string | null>(null);
|
||||||
const seen = useMutation({
|
const seen = useMutation({
|
||||||
mutationFn: () => api.call("markWelcomeSeen", {}),
|
mutationFn: () => api.call("markWelcomeSeen", {}),
|
||||||
onSuccess: (next) => queryClient.setQueryData(PLAYER_STATE_KEY, next),
|
onSuccess: (next) => queryClient.setQueryData(PLAYER_STATE_KEY, next),
|
||||||
});
|
});
|
||||||
if (state.welcomeSeen || closed) return null;
|
if (state.welcomeSeen || closedFor === state.company.id) return null;
|
||||||
return (
|
return (
|
||||||
<WelcomeCards
|
<WelcomeCards
|
||||||
final="start"
|
final="start"
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setClosed(true);
|
setClosedFor(state.company.id);
|
||||||
seen.mutate();
|
seen.mutate();
|
||||||
void navigate("/contracts");
|
void navigate("/contracts");
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { useState } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { WelcomeCards } from "../components/Welcome";
|
||||||
import { SpendingFloor } from "../components/IndependentPilots";
|
import { SpendingFloor } from "../components/IndependentPilots";
|
||||||
import { resetMapView } from "../map/camera";
|
import { resetMapView } from "../map/camera";
|
||||||
import { About } from "./AboutSettings";
|
import { About } from "./AboutSettings";
|
||||||
|
|
@ -18,6 +20,7 @@ export function isDevBuild(): boolean {
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
const state = useLoadedPlayer();
|
const state = useLoadedPlayer();
|
||||||
const prefs = usePreferences();
|
const prefs = usePreferences();
|
||||||
|
const [howToPlay, setHowToPlay] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Screen className="max-w-2xl">
|
<Screen className="max-w-2xl">
|
||||||
|
|
@ -87,7 +90,12 @@ export function Settings() {
|
||||||
<Button variant="secondary" size="sm" onClick={() => prefs.showTipsAgain()}>
|
<Button variant="secondary" size="sm" onClick={() => prefs.showTipsAgain()}>
|
||||||
Show tips again
|
Show tips again
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="secondary" size="sm" onClick={() => setHowToPlay(true)}>
|
||||||
|
How to play
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* The welcome cards again, ending in Close (decision 0.5.132). */}
|
||||||
|
{howToPlay && <WelcomeCards final="close" onClose={() => setHowToPlay(false)} />}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="About">
|
<Section title="About">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
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, sampleTutorialBoard } from "./render";
|
||||||
|
|
||||||
|
const NAME = "Deadhead Freight";
|
||||||
|
|
||||||
|
/** A company whose welcome is `seen`; "Start a new company" makes another with its welcome unseen. */
|
||||||
|
function company(seen: boolean) {
|
||||||
|
let state: PlayerState = samplePlayerState({ welcomeSeen: seen });
|
||||||
|
let companies = 1;
|
||||||
|
const api = fakeApi({
|
||||||
|
getPlayerState: () => state,
|
||||||
|
getTutorialBoard: () => sampleTutorialBoard(),
|
||||||
|
markWelcomeSeen: () => {
|
||||||
|
state = { ...state, welcomeSeen: true };
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
newCompany: () => {
|
||||||
|
companies++;
|
||||||
|
state = samplePlayerState({
|
||||||
|
welcomeSeen: false,
|
||||||
|
company: { id: `co${companies}`, name: NAME, credits: 0, loanBalance: 0 },
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startNewCompany() {
|
||||||
|
await userEvent.click(await screen.findByRole("button", { name: "Start a new company" }));
|
||||||
|
const confirm = screen.getByRole("dialog", { name: "Start a new company?" });
|
||||||
|
await userEvent.type(within(confirm).getByRole("textbox"), NAME);
|
||||||
|
await userEvent.click(within(confirm).getByRole("button", { name: "Start over" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("welcome for a new company and How to play (Step 22.3, decisions 0.5.131, 0.5.132)", () => {
|
||||||
|
it("shows the welcome for a company started from Settings, even after closing one earlier", async () => {
|
||||||
|
const api = company(false);
|
||||||
|
renderApp(api, "/settings");
|
||||||
|
// This company's welcome first.
|
||||||
|
const first = await screen.findByRole("dialog", { name: "Welcome to Deadhead" });
|
||||||
|
await userEvent.click(within(first).getByRole("button", { name: "Skip" }));
|
||||||
|
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
||||||
|
|
||||||
|
await userEvent.click(await screen.findByRole("link", { name: "Settings" }));
|
||||||
|
await startNewCompany();
|
||||||
|
const again = await screen.findByRole("dialog", { name: "Welcome to Deadhead" });
|
||||||
|
expect(again).toHaveTextContent("1 of 3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens How to play from Settings any time, ending in Close, without touching the save", async () => {
|
||||||
|
const api = company(true);
|
||||||
|
renderApp(api, "/settings");
|
||||||
|
await userEvent.click(await screen.findByRole("button", { name: "How to play" }));
|
||||||
|
const dialog = screen.getByRole("dialog", { name: "Welcome to Deadhead" });
|
||||||
|
await userEvent.click(within(dialog).getByRole("button", { name: "Next" }));
|
||||||
|
await userEvent.click(within(dialog).getByRole("button", { name: "Next" }));
|
||||||
|
expect(within(dialog).queryByRole("button", { name: "Let's fly" })).not.toBeInTheDocument();
|
||||||
|
await userEvent.click(within(dialog).getByRole("button", { name: "Close" }));
|
||||||
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
|
expect(api.call).not.toHaveBeenCalledWith("markWelcomeSeen", expect.anything());
|
||||||
|
// Still on Settings.
|
||||||
|
expect(screen.getByRole("heading", { level: 1, name: "Settings" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -496,5 +496,5 @@ Spec: `memory-bank/change-requests/CR-022-welcome-screens.md` (received as "CR-0
|
||||||
- **Other changes.** The three screens that wrote "Halvorsen Lunar Freight" now use `content.tutorialCompanyName`. The e2e `startCompany` (and the production-build test) expect the welcome and skip it, waiting for the save to record it before any reload. Screenshots: `docs/screenshots/22-2-welcome-1-phone.png`, `22-2-welcome-3-phone.png`, `22-2-after-welcome-phone.png`, and the same three for desktop.
|
- **Other changes.** The three screens that wrote "Halvorsen Lunar Freight" now use `content.tutorialCompanyName`. The e2e `startCompany` (and the production-build test) expect the welcome and skip it, waiting for the save to record it before any reload. Screenshots: `docs/screenshots/22-2-welcome-1-phone.png`, `22-2-welcome-3-phone.png`, `22-2-after-welcome-phone.png`, and the same three for desktop.
|
||||||
|
|
||||||
Tests (`apps/web/test/welcome.test.tsx`): through all three cards to "Let's fly", marked seen, on Contracts with the intro card; Skip on each card does the same; no welcome after a reload; the slide class only without reduced motion; focus stays inside. `packages/engine/test/start.test.ts`: `markWelcomeSeen` records it, and it survives a reload.
|
Tests (`apps/web/test/welcome.test.tsx`): through all three cards to "Let's fly", marked seen, on Contracts with the intro card; Skip on each card does the same; no welcome after a reload; the slide class only without reduced motion; focus stays inside. `packages/engine/test/start.test.ts`: `markWelcomeSeen` records it, and it survives a reload.
|
||||||
- [ ] Step 22.3 — Client: new company and "How to play"
|
- [x] **Step 22.3 — Client: new company and "How to play".** "Start a new company" makes a save whose welcome is unseen, so the gate shows the cards for it. The gate's "closed" flag is now kept per company id, so a welcome closed earlier in the session doesn't hide the new company's. Settings → Help has "How to play", which opens `WelcomeCards` ending in "Close" and leaves the save alone. Tests (`apps/web/test/welcome-settings.test.tsx`): after skipping one company's welcome, a company started from Settings shows card 1 again; How to play opens anywhere, ends in Close (not "Let's fly"), stays on Settings and never calls `markWelcomeSeen`.
|
||||||
- [ ] Step 22.4 — Regression
|
- [ ] Step 22.4 — Regression
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue