105 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
}
|