Files
Dune/components/spice-card-inspector.tsx
T
2026-09-08 07:37:52 +10:00

268 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useId } from 'react';
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
import { Eye, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import type { SpiceCard } from '@/game/cards';
import { territory } from '@/game/board';
import { PHASE_HELP } from '@/game/reference';
/** Only the supplied visible face is read. No deck, discard pile or game is accepted. */
export type SpiceCardInspectorProps = {
card: Readonly<SpiceCard>;
context?: string;
};
export function spiceCardTitle(card: Readonly<SpiceCard>): string {
if ('territory' in card)
return `${territory(card.territory).name} · ${card.amount} spice · sector ${card.sector}`;
if ('sandtrout' in card) return 'Sandtrout';
return `Shai-Hulud${card.thumper ? ' · Thumper encounter' : ''}${card.suppressed ? ' · suppressed' : ''}`;
}
/** Original vector illustration, with no claim to reproduce a printed card face. */
function SpiceIllustration({
kind,
}: {
kind: 'territory' | 'worm' | 'sandtrout';
}) {
const paint = `${useId()}-dune`;
return (
<svg
viewBox="0 0 260 190"
className="my-5 w-full"
aria-hidden="true"
focusable="false"
>
<defs>
<linearGradient id={paint} x1="0" y1="0" x2="0" y2="1">
<stop
offset="0"
stopColor={kind === 'sandtrout' ? '#344a42' : '#4f3d29'}
/>
<stop offset="1" stopColor="#171b17" />
</linearGradient>
</defs>
<rect
x="1"
y="1"
width="258"
height="188"
rx="90"
fill={`url(#${paint})`}
stroke="#ac8c54"
strokeOpacity="0.5"
/>
<circle cx="194" cy="47" r="16" fill="#dfbd7b" opacity="0.65" />
<path
d="M 12 121 Q 62 72 126 119 T 248 119 M 8 151 Q 76 95 144 147 T 252 143 M 30 174 Q 104 129 228 172"
fill="none"
stroke="#c5a76b"
strokeWidth="2"
strokeOpacity="0.65"
/>
{kind === 'territory' ? (
<g fill="#dfb976" stroke="#fff0c4" strokeWidth="1.5">
<path d="M 97 105 L 110 69 L 122 105 L 110 123 Z" />
<path d="M 126 112 L 144 58 L 161 112 L 144 135 Z" />
<path d="M 164 118 L 174 93 L 184 118 L 174 132 Z" />
</g>
) : kind === 'worm' ? (
<g fill="none" stroke="#dec18a">
<path
d="M 59 136 C 66 88 67 59 106 45 C 143 31 170 73 165 127"
strokeWidth="8"
/>
<ellipse
cx="116"
cy="77"
rx="31"
ry="37"
fill="#141814"
strokeWidth="3"
/>
<ellipse cx="116" cy="77" rx="20" ry="25" strokeWidth="1" />
<path
d="M 116 42 V 58 M 116 96 V 113 M 85 77 H 99 M 134 77 H 147 M 94 51 L 103 61 M 131 94 L 140 103 M 94 103 L 103 94 M 131 61 L 140 51"
strokeWidth="2"
/>
</g>
) : (
<g fill="#7c9579" fillOpacity="0.3" stroke="#c6d1aa" strokeWidth="2">
<path d="M 72 90 C 80 45 122 39 150 70 C 191 56 204 95 167 114 C 144 145 109 137 103 114 C 76 127 53 113 72 90 Z" />
<path
d="M 83 87 Q 120 63 159 87 M 96 107 Q 127 86 164 101"
fill="none"
/>
</g>
)}
</svg>
);
}
export function SpiceCardInspector({ card, context }: SpiceCardInspectorProps) {
const kind =
'territory' in card
? 'territory'
: 'sandtrout' in card
? 'sandtrout'
: 'worm';
const title = spiceCardTitle(card);
const faceName =
'territory' in card
? territory(card.territory).name
: kind === 'worm'
? 'Shai-Hulud'
: 'Sandtrout';
const description =
kind === 'territory'
? 'This spice blow names the territory, sector and base spice amount. The storm and card effects can change how much spice actually appears.'
: kind === 'worm'
? 'A Shai-Hulud encounter can consume forces and spice in the territory identified by the preceding card in its spice discard pile. Resolve protection, Nexus and riding decisions through the table.'
: 'Sandtrout breaks existing alliances and waits for the next active drawn Shai-Hulud. It suppresses that encounter, then the immediate replacement determines whether a spice blow is doubled.';
return (
<Dialog>
<DialogTrigger
render={
<Button
variant="outline"
className="min-h-11 min-w-11 motion-reduce:transition-none"
/>
}
aria-label={`Inspect spice card: ${title}${context ? ` · ${context}` : ''}`}
>
<Eye aria-hidden="true" />
Inspect spice card
</DialogTrigger>
<DialogPortal>
<DialogOverlay className="bg-black/75 motion-reduce:animate-none motion-reduce:transition-none" />
<DialogPrimitive.Popup className="fixed top-1/2 left-1/2 z-50 flex max-h-[calc(100dvh-1rem)] w-[calc(100%-1rem)] max-w-3xl -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-2xl border border-[#8e7952] bg-[#141814] text-[#eeeae0] shadow-2xl outline-none duration-150 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 motion-reduce:animate-none motion-reduce:transition-none sm:max-h-[calc(100dvh-3rem)] sm:w-[calc(100%-3rem)]">
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-[#414536] px-4 py-3">
<span className="text-xs font-semibold tracking-[0.16em] text-[#ddbc77] uppercase">
Spice card inspection
</span>
<DialogClose
render={
<Button
variant="outline"
className="min-h-11 min-w-11 motion-reduce:transition-none"
/>
}
>
<X aria-hidden="true" />
Close
</DialogClose>
</div>
<div className="min-h-0 overflow-y-auto overscroll-contain p-4 sm:p-6">
<div className="grid items-start gap-6 md:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
<article className="min-w-0 rounded-xl border border-[#a58b5c] bg-gradient-to-br from-[#493d29] via-[#2f3023] to-[#171b17] p-5 shadow-lg sm:p-6">
<p className="m-0 text-sm font-semibold tracking-wider text-[#f1d79f] uppercase">
{kind === 'territory'
? 'Spice blow'
: kind === 'worm'
? 'Worm encounter'
: 'Sandtrout'}
</p>
<SpiceIllustration kind={kind} />
<DialogTitle className="break-words font-serif text-3xl leading-tight text-[#fff3d6] sm:text-4xl">
{faceName}
</DialogTitle>
{'territory' in card && (
<dl className="mt-5 grid grid-cols-2 gap-4 border-y border-[#b59a64]/40 py-4">
<div>
<dt className="text-sm text-[#d6c9ad]">Card amount</dt>
<dd className="m-0 mt-1 font-serif text-4xl text-[#f6e4b9]">
{card.amount}
<span className="block font-sans text-sm">spice</span>
</dd>
</div>
<div>
<dt className="text-sm text-[#d6c9ad]">Sector</dt>
<dd className="m-0 mt-1 font-serif text-4xl text-[#f6e4b9]">
{card.sector}
</dd>
</div>
</dl>
)}
{'worm' in card && card.thumper && (
<p className="mt-4 text-base leading-7 text-[#eee5d2]">
Thumper-created encounter
</p>
)}
{'worm' in card && card.suppressed && (
<p className="mt-4 text-base leading-7 text-[#eee5d2]">
Suppressed by Sandtrout
</p>
)}
<p className="mt-5 mb-0 text-xs tracking-wider text-[#cebea0] uppercase">
Spice · gameplay guide
</p>
</article>
<section
className="flex min-w-0 flex-col gap-5"
aria-label="Spice card guidance"
>
{context && (
<p className="m-0 text-sm font-semibold text-[#ddbc77]">
{context}
</p>
)}
<DialogDescription className="m-0 text-base leading-7 text-[#eee5d2]">
{description}
</DialogDescription>
<p className="m-0 text-base leading-7 text-[#e0e4d8]">
Inspection does not draw a card or resolve an effect. Close
this view to use the table’s phase controls.
</p>
{kind === 'territory' ? (
<p className="m-0 text-base leading-7 text-[#e0e4d8]">
Spice cannot appear in the storm’s sector. A fresh blow
remains open for Harvester before acceptance. The amount on
this face is the card’s base value, not the total spice
currently on the board.
</p>
) : kind === 'worm' ? (
<p className="m-0 text-base leading-7 text-[#e0e4d8]">
First-turn drawn worms are ignored and later shuffled back.
A worm suppressed by Sandtrout does not devour the preceding
territory or create its own Nexus or Fremen ride. A normal
encounter follows the table’s worm and alliance decisions.
</p>
) : (
<>
<p className="m-0 text-base leading-7 text-[#e0e4d8]">
Territories drawn before the next active Shai-Hulud
receive their usual spice. Sandtrout remains pending until
that worm is suppressed, even across spice blows or turns.
</p>
<p className="m-0 text-base leading-7 text-[#e0e4d8]">
If the immediate replacement is a territory, double its
spice; Harvester may enhance that fresh blow again. If the
replacement is another Shai-Hulud, resolve it normally and
do not double a later territory. Spice still cannot appear
in the storm.
</p>
</>
)}
<p className="m-0 rounded-lg border border-[#454b3c] bg-[#1d221b] p-4 text-base leading-7 text-[#e0e4d8]">
{PHASE_HELP[1]}
</p>
</section>
</div>
</div>
</DialogPrimitive.Popup>
</DialogPortal>
</Dialog>
);
}