deadhead-space-freight-tycoon/apps/web/src/components/SkillPointPrompt.tsx

105 lines
3.6 KiB
TypeScript

import type { Pilot } from "@deadhead/schema";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router";
import { useApi } from "../api/context";
import { useLoadedPlayer } from "../state/actions";
import { usePreferences } from "../state/preferences";
import { Button, Sheet } from "../ui";
import { useAwayOpen } from "./AwaySummary";
/** The id of the skill point pop-up in dismissed tips (CR-011 one-time tips). */
export const SKILL_POINTS_TIP = "skill-points";
/** "You" for the player's own pilot, else the pilot's first name. */
function callName(pilot: Pilot): string {
return pilot.isAvatar ? "You" : (pilot.name.split(" ")[0] ?? pilot.name);
}
/** "You", "You and Rosa", "You, Rosa and Jin". */
function listNames(names: string[]): string {
return names.length < 2 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
}
/**
* The skill point nudge (decision 0.5.109): once the business is the player's
* and the notification permission prompt has been answered, a one-time tip
* listing every pilot with unspent skill points. "Show tips again" brings it
* back.
*/
export function SkillPointPrompt() {
const state = useLoadedPlayer();
const awayOpen = useAwayOpen((s) => s.open);
const api = useApi();
const navigate = useNavigate();
const dismissed = usePreferences((p) => p.dismissedTips.includes(SKILL_POINTS_TIP));
const dismissTip = usePreferences((p) => p.dismissTip);
const owner = state.tutorial === null;
// After the deal, wait for the player to answer the permission prompt (decision 0.5.91).
const [promptAnswered, setPromptAnswered] = useState(false);
useEffect(() => {
if (!owner) return;
let live = true;
void (api.notificationPrompt?.() ?? Promise.resolve()).then(() => {
if (live) setPromptAnswered(true);
});
return () => {
live = false;
};
}, [api, owner]);
const ready = state.pilots.filter((p) => p.skillPointsAvailable > 0);
if (!owner || !promptAnswered || dismissed || ready.length === 0) return null;
// Let delivery summaries go first.
if (state.summaries.length > 0 || awayOpen) return null;
const names = ready.map(callName);
const subject =
ready.length === 1
? `${names[0]} ${ready[0]!.isAvatar ? "have" : "has"}`
: `${listNames(names)} ${ready.length === 2 ? "both" : "all"} have`;
return (
<Sheet
titleId="skill-points-title"
eyebrow="Pilots"
title="Your pilots are ready to train"
tone="cyan"
layer="z-30"
>
<p className="mt-3 text-sm text-ink-200">
{subject} skill points to spend. Skills unlock better-paying contracts: heavier loads,
valuable cargo, longer routes.
</p>
<p aria-label="Unspent skill points" className="mt-3 text-sm text-ink-100">
{ready.map((pilot, i) => (
<span key={pilot.id}>
{i > 0 && " · "}
<strong className="font-semibold text-ink-100">{callName(pilot)}:</strong>{" "}
{pilot.skillPointsAvailable} {pilot.skillPointsAvailable === 1 ? "point" : "points"}
</span>
))}
</p>
<div className="mt-5 flex gap-3">
<Button
size="lg"
className="flex-1"
onClick={() => {
dismissTip(SKILL_POINTS_TIP);
void navigate("/pilots");
}}
>
Go to Pilots
</Button>
<Button
size="lg"
variant="secondary"
className="flex-1"
onClick={() => dismissTip(SKILL_POINTS_TIP)}
>
Later
</Button>
</div>
</Sheet>
);
}