Files
Dune/components/homeworld-cards.tsx
T
Quintine b448144c52
Verify and publish checkpoint container / container (push) Successful in 10m17s
Support Harkonnen special Karama truth commitments
2026-10-29 18:08:04 +11:00

229 lines
8.0 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 Image from 'next/image';
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
import { FACTIONS } from '@/game/catalog';
import { HOMEWORLD_CARDS, type HomeworldCard } from '@/game/homeworld-cards';
import { Button } from './ui/button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from './ui/tabs';
import {
Dialog,
DialogClose,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from './ui/dialog';
function HomeworldPopulation({
card,
side,
enlarged,
}: {
card: HomeworldCard;
side: 'high' | 'low';
enlarged: boolean;
}) {
const face = card[side];
return (
<section
aria-label={`${side === 'high' ? 'High' : 'Low'} population rules`}
className="space-y-4"
>
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#a88b60]/40 pb-3">
<h4 className="m-0 font-serif text-xl">
{side === 'high' ? 'High' : 'Low'} population
</h4>
<span className="rounded border border-[#a88b60]/60 px-3 py-1 text-base tabular-nums">
{face.reserves.min}–{face.reserves.max}{' '}
{card.reserveType === 'sardaukar' ? 'Sardaukar' : 'reserves'}
</span>
</div>
{face.gameplay.map((text) => (
<p
key={text}
className={
enlarged ? 'm-0 text-xl! leading-8!' : 'm-0 text-base! leading-7!'
}
>
{text}
</p>
))}
<div className="grid gap-2 border-t border-[#a88b60]/40 pt-3 text-sm leading-6 sm:grid-cols-2">
<p className="m-0">
<strong className="text-[#edcc8c]">
+{face.battleStrength} battle strength
</strong>
<br />
Added to the native faction’s dial here.
</p>
<p className="m-0">
<strong className="text-[#edcc8c]">
{face.battleStrength} explosion losses
</strong>
<br />
Native forces lost to a Lasgun/Shield explosion here.
</p>
</div>
</section>
);
}
export function HomeworldFace({
card,
enlarged = false,
}: {
card: HomeworldCard;
enlarged?: boolean;
}) {
const faction = FACTIONS.find((item) => item.id === card.faction)!;
return (
<article className="min-w-0 overflow-hidden rounded-xl border border-[#a88b60] bg-[#20271f] text-[#f2e8d3] shadow-lg">
<header
className="flex min-h-24 flex-wrap items-center gap-3 border-b border-[#a88b60]/50 px-4 py-4"
style={{ borderTop: `4px solid ${faction.color}` }}
>
<div className="min-w-0 flex-1">
<p className="m-0 text-sm leading-6 text-[#d1c7b0]">
{faction.name} · Homeworld
</p>
<h3 className="m-0 break-words font-serif text-2xl leading-tight text-[#fff1d4]">
{card.name}
</h3>
</div>
{card.id === 'salusa_secundus' && (
<span className="text-sm text-[#edcc8c]">Advanced only</span>
)}
</header>
<Image
src={`/art/homeworlds/${card.id}-v1.png`}
alt=""
width={1536}
height={768}
unoptimized
loading="lazy"
className="block aspect-[2/1] w-full object-cover"
/>
<Tabs defaultValue="high" className="gap-4 p-4">
<TabsList
aria-label={`${card.name} card face`}
className="grid h-auto! w-full grid-cols-2 bg-[#101912] p-1"
>
<TabsTrigger
value="high"
className="min-h-11 whitespace-normal px-2 text-[#d1c7b0] data-active:bg-[#3f513b] data-active:text-white"
>
High face
</TabsTrigger>
<TabsTrigger
value="reverse"
className="min-h-11 whitespace-normal px-2 text-[#d1c7b0] data-active:bg-[#3f513b] data-active:text-white"
>
Low / occupied face
</TabsTrigger>
</TabsList>
<TabsContent value="high">
<HomeworldPopulation card={card} side="high" enlarged={enlarged} />
</TabsContent>
<TabsContent value="reverse" className="space-y-6">
<HomeworldPopulation card={card} side="low" enlarged={enlarged} />
<section
aria-label="Occupied rules"
className="space-y-4 border-t-2 border-[#a88b60] pt-4"
>
<h4 className="m-0 font-serif text-xl">Occupied</h4>
{card.occupied.gameplay.map((text) => (
<p
key={text}
className={
enlarged
? 'm-0 text-xl! leading-8!'
: 'm-0 text-base! leading-7!'
}
>
{text}
</p>
))}
<p className="m-0 flex items-center gap-2 text-sm leading-6 text-[#edcc8c]">
<span aria-hidden="true" className="text-xl">
{'◆'.repeat(card.occupied.spiceIcons)}
</span>
{card.occupied.spiceIcons} bank spice during Spice Collection
</p>
<p className="m-0 text-sm leading-6 text-[#d1c7b0]">
The low-population penalty continues while occupied.
</p>
</section>
</TabsContent>
</Tabs>
{card.id === 'salusa_secundus' && (
<p className="m-0 px-4 pb-4 text-sm leading-6 text-[#d1c7b0]">
Both printed ranges include 2. The module’s minimum-high rule makes 2
Sardaukar high population.
</p>
)}
{!enlarged && (
<div className="px-4 pb-4">
<Dialog>
<DialogTrigger
render={
<Button
variant="outline"
className="min-h-11 w-full whitespace-normal"
/>
}
>
Inspect {card.name}
</DialogTrigger>
<DialogPortal>
<DialogOverlay className="bg-black/75 motion-reduce:animate-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-2xl -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border border-[#a88b60] bg-[#171d18] text-[#f2e8d3] shadow-2xl outline-none motion-reduce:animate-none">
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-[#a88b60]/40 px-4 py-3">
<DialogTitle className="text-base leading-6">
{card.name}
</DialogTitle>
<DialogClose
render={
<Button variant="outline" className="min-h-11 min-w-16" />
}
>
Close
</DialogClose>
</div>
<DialogDescription className="sr-only">
Both faces of the Homeworld card, including population ranges,
native battle values and occupied effects.
</DialogDescription>
<div className="min-h-0 overflow-y-auto overscroll-contain p-3 sm:p-5">
<HomeworldFace card={card} enlarged />
</div>
</DialogPrimitive.Popup>
</DialogPortal>
</Dialog>
</div>
)}
<footer className="border-t border-[#a88b60]/40 px-4 py-3 text-xs leading-5 tracking-wide text-[#c9baa0]">
Ecaz &amp; Moritani · 1 two-sided card + 1 world token
</footer>
</article>
);
}
export function HomeworldCardGallery() {
return (
<section aria-label="Homeworld card collection" className="space-y-4">
<p className="m-0 text-base leading-7 text-[#d1c7b0]">
13 worlds · Inspect either face to read its complete gameplay effects.
Supported Homeworld selections are available as unfinished previews;
some combinations remain unavailable.
</p>
<div className="grid grid-cols-1 items-start gap-5 xl:grid-cols-2">
{HOMEWORLD_CARDS.map((card) => (
<HomeworldFace key={card.id} card={card} />
))}
</div>
</section>
);
}