97 lines
3.6 KiB
TypeScript
97 lines
3.6 KiB
TypeScript
import { content } from "@deadhead/content";
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { useNavigate } from "react-router";
|
|
import { useApi } from "../api/context";
|
|
import { formatCredits } from "../format";
|
|
import { errorMessage, useLoadedPlayer } from "../state/actions";
|
|
import { PLAYER_STATE_KEY } from "../state/player";
|
|
import { Art, Button, Sheet } from "../ui";
|
|
import { useAwayOpen } from "./AwaySummary";
|
|
|
|
/**
|
|
* The one-time deal (decision 0.5.30): shown when the server says it is
|
|
* available, after any delivery summary has been dismissed. There is no
|
|
* decline option.
|
|
*/
|
|
export function DealModal() {
|
|
const awayOpen = useAwayOpen((s) => s.open);
|
|
const state = useLoadedPlayer();
|
|
const api = useApi();
|
|
const queryClient = useQueryClient();
|
|
const navigate = useNavigate();
|
|
const accept = useMutation({
|
|
mutationFn: () => api.call("acceptDeal", {}),
|
|
onSuccess: (newState) => {
|
|
queryClient.setQueryData(PLAYER_STATE_KEY, newState);
|
|
void navigate("/");
|
|
},
|
|
});
|
|
|
|
if (!state.tutorial?.dealAvailable || state.summaries.length > 0 || awayOpen) return null;
|
|
|
|
const { dealLoanAmount } = content;
|
|
const { loanRepaymentShare } = content.config;
|
|
// The deal's ships (decisions 0.5.30, 0.5.82).
|
|
const courier = content.shipModel(content.config.deal.avatarShipModelId)!;
|
|
const clipper = content.shipModel(content.config.deal.veteranShipModelId)!;
|
|
const bay = content.location(content.config.deal.bayLocationId)!;
|
|
const veteran = content.veteranPilot;
|
|
|
|
return (
|
|
<Sheet
|
|
titleId="deal-title"
|
|
eyebrow="Incoming transmission"
|
|
title="An offer from Halvorsen"
|
|
tone="amber"
|
|
layer="z-30"
|
|
>
|
|
<div className="mt-3 flex items-center gap-3">
|
|
<div className="flex shrink-0 -space-x-6">
|
|
<Art sprite={courier.sprite} className="size-16 -rotate-12" />
|
|
<Art sprite={clipper.sprite} className="size-16 rotate-12" />
|
|
</div>
|
|
<p className="text-sm text-ink-200">
|
|
Old Halvorsen is retiring. He'll sell you his {courier.name}, his {clipper.name}, and
|
|
his {bay.name} bay — and {veteran.name}, who flies that {clipper.name}. Pay him back out
|
|
of every job.
|
|
</p>
|
|
</div>
|
|
<dl className="mt-4 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 rounded-xl bg-space-950/60 p-3 text-sm ring-1 ring-amber-400/25">
|
|
<dt className="font-display text-xs text-ink-400">You get</dt>
|
|
<dd aria-label="Ships and bay">
|
|
A {courier.name} for you, a {clipper.name} for {veteran.name}, and a bay at {bay.name}
|
|
</dd>
|
|
<dt className="font-display text-xs text-ink-400">Pilot</dt>
|
|
<dd aria-label="Veteran pilot">
|
|
…and {veteran.name}. {veteran.bio}
|
|
</dd>
|
|
<dt className="font-display text-xs text-ink-400">Loan</dt>
|
|
<dd
|
|
aria-label="Loan amount"
|
|
className="font-display font-semibold text-amber-200 tabular-nums"
|
|
>
|
|
{formatCredits(dealLoanAmount)}
|
|
</dd>
|
|
<dt className="font-display text-xs text-ink-400">Repayment</dt>
|
|
<dd aria-label="Repayment share">
|
|
{Math.round(loanRepaymentShare * 100)}% of every payout until it's paid off
|
|
</dd>
|
|
</dl>
|
|
{accept.isError && (
|
|
<p role="alert" className="mt-3 text-sm text-rose-300">
|
|
{errorMessage(accept.error)}
|
|
</p>
|
|
)}
|
|
<Button
|
|
size="lg"
|
|
block
|
|
onClick={() => accept.mutate()}
|
|
disabled={accept.isPending}
|
|
className="mt-5"
|
|
>
|
|
Shake on it
|
|
</Button>
|
|
</Sheet>
|
|
);
|
|
}
|