docs(guide-specific): 📝 Update encyclopedia, full game guide, and map types documentation with expanded content
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
743efcf8d3
commit
1c785d367b
3 changed files with 0 additions and 853 deletions
|
|
@ -1,473 +0,0 @@
|
|||
import { useMemo, useEffect, type ReactElement } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import styled from 'styled-components'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { Heading, Text } from '@lilith/ui-typography'
|
||||
import { FadeIn } from '@magic-civ/guide-engine'
|
||||
import { allEncyclopediaEntries, encyclopediaCategories } from '@/data'
|
||||
import type { EncyclopediaCategory, EncyclopediaCategoryMeta, EncyclopediaEntry } from '@magic-civ/guide-engine'
|
||||
import { useState } from 'react'
|
||||
import { PageTitle, FilterRow, FilterChip, TagRow } from '@magic-civ/guide-engine'
|
||||
|
||||
// ─── Derived lookups from data ────────────────────────────────────────────────
|
||||
|
||||
const categoryMap = new Map<EncyclopediaCategory, EncyclopediaCategoryMeta>(
|
||||
encyclopediaCategories.map((c) => [c.id, c]),
|
||||
)
|
||||
|
||||
function catColor(id: EncyclopediaCategory): string {
|
||||
return categoryMap.get(id)?.color ?? '#888'
|
||||
}
|
||||
|
||||
function catLabel(id: EncyclopediaCategory): string {
|
||||
return categoryMap.get(id)?.label ?? id
|
||||
}
|
||||
|
||||
function catIcon(id: EncyclopediaCategory): string {
|
||||
return categoryMap.get(id)?.icon ?? ''
|
||||
}
|
||||
|
||||
type FilterCategory = EncyclopediaCategory | 'all'
|
||||
|
||||
// ─── Page styles ──────────────────────────────────────────────────────────────
|
||||
|
||||
const Controls = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
`
|
||||
|
||||
const SearchInput = styled.input`
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.875rem;
|
||||
background: ${({ theme }) => theme.colors.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
border-radius: 6px;
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
font-size: 0.875rem;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder { color: ${({ theme }) => theme.colors.text.muted}; }
|
||||
&:focus { border-color: ${({ theme }) => theme.colors.primary.main}; }
|
||||
`
|
||||
|
||||
|
||||
const CardGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.875rem;
|
||||
|
||||
@media (max-width: 900px) { grid-template-columns: repeat(2, 1fr); }
|
||||
@media (max-width: 540px) { grid-template-columns: 1fr; }
|
||||
`
|
||||
|
||||
const EmptyState = styled.div`
|
||||
padding: 3rem 1rem;
|
||||
text-align: center;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
font-size: 0.875rem;
|
||||
`
|
||||
|
||||
// ─── Card styles ──────────────────────────────────────────────────────────────
|
||||
|
||||
const Card = styled.div<{ $color: string }>`
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, transform 0.1s;
|
||||
background: ${({ theme }) => theme.colors.background.secondary};
|
||||
|
||||
&:hover {
|
||||
border-color: ${({ $color }) => $color};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
`
|
||||
|
||||
const CardBar = styled.div<{ $color: string }>`
|
||||
height: 3px;
|
||||
background: ${({ $color }) => $color};
|
||||
`
|
||||
|
||||
const CardBody = styled.div`
|
||||
padding: 0.875rem 1rem;
|
||||
`
|
||||
|
||||
const CardHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
`
|
||||
|
||||
const CardIcon = styled.span`
|
||||
font-size: 1.125rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.0625rem;
|
||||
`
|
||||
|
||||
const CardName = styled.h3`
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
`
|
||||
|
||||
const CardSummary = styled.p`
|
||||
font-size: 0.8125rem;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
line-height: 1.55;
|
||||
margin: 0 0 0.625rem;
|
||||
`
|
||||
|
||||
const Tag = styled.span`
|
||||
font-size: 0.6875rem;
|
||||
padding: 0.1rem 0.4375rem;
|
||||
border-radius: 4px;
|
||||
background: ${({ theme }) => theme.colors.background.primary};
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
`
|
||||
|
||||
const CategoryBadge = styled.span<{ $color: string }>`
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.75px;
|
||||
text-transform: uppercase;
|
||||
color: ${({ $color }) => $color};
|
||||
padding: 0.1rem 0.4375rem;
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, ${({ $color }) => $color} 14%, transparent);
|
||||
border: 1px solid color-mix(in srgb, ${({ $color }) => $color} 30%, transparent);
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
// ─── Detail modal styles ──────────────────────────────────────────────────────
|
||||
// Backdrop offsets the desktop sidebar (260px) so the modal centers over the
|
||||
// content area, not the full viewport.
|
||||
|
||||
const SIDEBAR_WIDTH = '260px'
|
||||
|
||||
const Backdrop = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: ${SIDEBAR_WIDTH};
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
left: 0;
|
||||
align-items: flex-end;
|
||||
padding: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const Modal = styled.div`
|
||||
background: ${({ theme }) => theme.colors.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
border-radius: 12px;
|
||||
max-width: 680px;
|
||||
width: 100%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
position: relative;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
max-width: 100%;
|
||||
max-height: 92dvh;
|
||||
border-radius: 16px 16px 0 0;
|
||||
border-bottom: none;
|
||||
padding: 1.25rem 1rem 2rem;
|
||||
}
|
||||
`
|
||||
|
||||
const ModalBar = styled.div<{ $color: string }>`
|
||||
height: 4px;
|
||||
background: ${({ $color }) => $color};
|
||||
border-radius: 2px;
|
||||
margin-bottom: 1.25rem;
|
||||
`
|
||||
|
||||
const ModalHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
`
|
||||
|
||||
const ModalTitleGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
`
|
||||
|
||||
const ModalName = styled.h2`
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
`
|
||||
|
||||
const CloseButton = styled.button`
|
||||
background: none;
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
border-radius: 6px;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
padding: 0.3125rem 0.5rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
border-color: ${({ theme }) => theme.colors.text.muted};
|
||||
}
|
||||
`
|
||||
|
||||
const ModalBody = styled.p`
|
||||
font-size: 0.875rem;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
line-height: 1.65;
|
||||
margin: 0 0 0.625rem;
|
||||
white-space: pre-line;
|
||||
`
|
||||
|
||||
const DesignNotesSection = styled.div<{ $color: string }>`
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 1.25rem;
|
||||
border-top: 1px solid color-mix(in srgb, ${({ $color }) => $color} 25%, transparent);
|
||||
`
|
||||
|
||||
const DesignNotesHeading = styled.div<{ $color: string }>`
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: color-mix(in srgb, ${({ $color }) => $color} 80%, #aaa);
|
||||
margin-bottom: 0.5rem;
|
||||
`
|
||||
|
||||
const DesignNotesText = styled.p`
|
||||
font-size: 0.8125rem;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
`
|
||||
|
||||
const ModalRelatedRow = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
`
|
||||
|
||||
const RelatedLabel = styled.span`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
`
|
||||
|
||||
const RelatedLink = styled(Link)`
|
||||
font-size: 0.75rem;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: ${({ theme }) => theme.colors.background.secondary};
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
text-decoration: none;
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
|
||||
&:hover { color: ${({ theme }) => theme.colors.text.primary}; }
|
||||
`
|
||||
|
||||
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
function EntryCard({ entry, onOpen }: { entry: EncyclopediaEntry; onOpen: (id: string) => void }): ReactElement {
|
||||
const color = catColor(entry.category)
|
||||
return (
|
||||
<Card
|
||||
$color={color}
|
||||
onClick={() => onOpen(entry.id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(ev) => ev.key === 'Enter' && onOpen(entry.id)}
|
||||
>
|
||||
<CardBar $color={color} />
|
||||
<CardBody>
|
||||
<CardHeader>
|
||||
<CardIcon>{catIcon(entry.category)}</CardIcon>
|
||||
<CardName>{entry.name}</CardName>
|
||||
</CardHeader>
|
||||
<CardSummary>{entry.summary}</CardSummary>
|
||||
<TagRow>
|
||||
<CategoryBadge $color={color}>{catLabel(entry.category)}</CategoryBadge>
|
||||
{entry.tags.slice(0, 4).map((tag) => <Tag key={tag}>{tag}</Tag>)}
|
||||
</TagRow>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function EntryModal({ entry, onClose }: { entry: EncyclopediaEntry; onClose: () => void }): ReactElement {
|
||||
const color = catColor(entry.category)
|
||||
const bodyParagraphs = entry.body.split('\n\n').filter(Boolean)
|
||||
const relatedEntries = entry.related
|
||||
.map((id) => allEncyclopediaEntries.find((e) => e.id === id))
|
||||
.filter((e): e is EncyclopediaEntry => e != null)
|
||||
|
||||
return createPortal(
|
||||
<Backdrop onClick={onClose}>
|
||||
<Modal onClick={(e) => e.stopPropagation()}>
|
||||
<ModalBar $color={color} />
|
||||
<ModalHeader>
|
||||
<ModalTitleGroup>
|
||||
<CategoryBadge $color={color}>
|
||||
{catIcon(entry.category)} {catLabel(entry.category)}
|
||||
</CategoryBadge>
|
||||
<ModalName>{entry.name}</ModalName>
|
||||
</ModalTitleGroup>
|
||||
<CloseButton onClick={onClose} aria-label="Close">✕</CloseButton>
|
||||
</ModalHeader>
|
||||
|
||||
{bodyParagraphs.map((para, i) => (
|
||||
<ModalBody key={i}>{para}</ModalBody>
|
||||
))}
|
||||
|
||||
<DesignNotesSection $color={color}>
|
||||
<DesignNotesHeading $color={color}>◆ Design Notes</DesignNotesHeading>
|
||||
<DesignNotesText>{entry.design_notes}</DesignNotesText>
|
||||
</DesignNotesSection>
|
||||
|
||||
{relatedEntries.length > 0 && (
|
||||
<ModalRelatedRow>
|
||||
<RelatedLabel>Related:</RelatedLabel>
|
||||
{relatedEntries.map((rel) => (
|
||||
<RelatedLink key={rel.id} to={`/encyclopedia/${rel.id}`}>
|
||||
{rel.name}
|
||||
</RelatedLink>
|
||||
))}
|
||||
</ModalRelatedRow>
|
||||
)}
|
||||
</Modal>
|
||||
</Backdrop>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Page ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function EncyclopediaPage(): ReactElement {
|
||||
const { topicId } = useParams<{ topicId?: string }>()
|
||||
const navigate = useNavigate()
|
||||
const [query, setQuery] = useState('')
|
||||
const [activeCategory, setActiveCategory] = useState<FilterCategory>('all')
|
||||
|
||||
const selected = topicId
|
||||
? (allEncyclopediaEntries.find((e) => e.id === topicId) ?? null)
|
||||
: null
|
||||
|
||||
// Escape closes the modal
|
||||
useEffect(() => {
|
||||
if (!selected) return
|
||||
const handler = (ev: KeyboardEvent) => { if (ev.key === 'Escape') navigate('/encyclopedia') }
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
}, [selected, navigate])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.toLowerCase().trim()
|
||||
return allEncyclopediaEntries.filter((entry) => {
|
||||
if (activeCategory !== 'all' && entry.category !== activeCategory) return false
|
||||
if (!q) return true
|
||||
return (
|
||||
entry.name.toLowerCase().includes(q) ||
|
||||
entry.summary.toLowerCase().includes(q) ||
|
||||
entry.tags.some((t) => t.toLowerCase().includes(q))
|
||||
)
|
||||
})
|
||||
}, [query, activeCategory])
|
||||
|
||||
const countFor = (cat: FilterCategory) =>
|
||||
cat === 'all'
|
||||
? allEncyclopediaEntries.length
|
||||
: allEncyclopediaEntries.filter((e) => e.category === cat).length
|
||||
|
||||
return (
|
||||
<FadeIn>
|
||||
<PageTitle>
|
||||
<Heading as="h1" size="2xl" marginBottom="xs">Encyclopedia</Heading>
|
||||
<Text color="muted" size="sm">
|
||||
{allEncyclopediaEntries.length} entries covering game systems, magic, combat, and world lore.
|
||||
</Text>
|
||||
</PageTitle>
|
||||
|
||||
<Controls>
|
||||
<SearchInput
|
||||
type="search"
|
||||
placeholder="Search entries, tags…"
|
||||
value={query}
|
||||
onChange={(ev) => setQuery(ev.target.value)}
|
||||
aria-label="Search encyclopedia"
|
||||
/>
|
||||
<FilterRow role="tablist">
|
||||
<FilterChip
|
||||
$active={activeCategory === 'all'}
|
||||
role="tab"
|
||||
aria-selected={activeCategory === 'all'}
|
||||
onClick={() => setActiveCategory('all')}
|
||||
>
|
||||
All ({countFor('all')})
|
||||
</FilterChip>
|
||||
{encyclopediaCategories.map((cat) => (
|
||||
<FilterChip
|
||||
key={cat.id}
|
||||
$active={activeCategory === cat.id}
|
||||
$color={cat.color}
|
||||
role="tab"
|
||||
aria-selected={activeCategory === cat.id}
|
||||
onClick={() => setActiveCategory(cat.id)}
|
||||
>
|
||||
{cat.icon} {cat.label} ({countFor(cat.id)})
|
||||
</FilterChip>
|
||||
))}
|
||||
</FilterRow>
|
||||
</Controls>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<EmptyState>No entries match "{query}"</EmptyState>
|
||||
) : (
|
||||
<CardGrid>
|
||||
{filtered.map((entry) => (
|
||||
<EntryCard
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
onOpen={(id) => navigate(`/encyclopedia/${id}`)}
|
||||
/>
|
||||
))}
|
||||
</CardGrid>
|
||||
)}
|
||||
|
||||
{selected && (
|
||||
<EntryModal entry={selected} onClose={() => navigate('/encyclopedia')} />
|
||||
)}
|
||||
</FadeIn>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
import type { ReactElement } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import { FadeIn } from '@magic-civ/guide-engine'
|
||||
import { PageTitle, Section, SectionHeading, Prose } from '@magic-civ/guide-engine'
|
||||
|
||||
const Title = styled.h1`
|
||||
font-size: 1.625rem;
|
||||
font-weight: 700;
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
margin: 0 0 0.5rem;
|
||||
`
|
||||
|
||||
const Subtitle = styled.p`
|
||||
font-size: 0.875rem;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
`
|
||||
|
||||
const Grid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
`
|
||||
|
||||
const Chip = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: ${({ theme }) => theme.colors.surface};
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
border-radius: 6px;
|
||||
`
|
||||
|
||||
const Highlight = styled.span`
|
||||
color: ${({ theme }) => theme.colors.primary.light};
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const Table = styled.table`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8125rem;
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
td {
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
`
|
||||
|
||||
const AboutRow = styled.div`
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 2.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
`
|
||||
|
||||
const AboutLink = styled(Link)`
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
border-radius: 6px;
|
||||
background: ${({ theme }) => theme.colors.surface};
|
||||
transition: border-color 150ms, color 150ms;
|
||||
|
||||
&:hover {
|
||||
border-color: ${({ theme }) => theme.colors.primary.dark};
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
}
|
||||
`
|
||||
|
||||
const RACES = [
|
||||
'High Elf', 'Human', 'Dwarf', 'Orc',
|
||||
'Dark Elf', 'Halfling', 'Gnome', 'Lizardfolk',
|
||||
'Troll', 'Beastkin', 'Nomad', 'Undead',
|
||||
'Draconic', 'Fae', 'Golem', 'Demon',
|
||||
]
|
||||
|
||||
const FUSIONS = [
|
||||
{ name: 'Theurgy', schools: 'Life + Aether' },
|
||||
{ name: 'Artifice', schools: 'Nature + Aether' },
|
||||
{ name: 'Demonology', schools: 'Death + Chaos' },
|
||||
{ name: 'Druidism', schools: 'Life + Nature' },
|
||||
{ name: 'Requiem', schools: 'Life + Death' },
|
||||
{ name: 'Crusade', schools: 'Life + Chaos' },
|
||||
{ name: 'Shadow Weaving', schools: 'Death + Aether' },
|
||||
{ name: 'Blight', schools: 'Death + Nature' },
|
||||
{ name: 'Primal Fury', schools: 'Nature + Chaos' },
|
||||
{ name: 'Arcane Storm', schools: 'Chaos + Aether' },
|
||||
]
|
||||
|
||||
const SYSTEMS = [
|
||||
'Diplomacy & treaties',
|
||||
'Social policies (5 trees)',
|
||||
'Naval units & combat',
|
||||
'Freepeople havens',
|
||||
'Trade routes & deals',
|
||||
'7 government types',
|
||||
'Strategic resources',
|
||||
'Game speed options',
|
||||
'Starting era selection',
|
||||
'Espionage',
|
||||
'Religion',
|
||||
'Advanced AI personalities',
|
||||
]
|
||||
|
||||
export default function FullGamePage(): ReactElement {
|
||||
return (
|
||||
<FadeIn>
|
||||
<PageTitle>
|
||||
<Title>Full Game</Title>
|
||||
<Subtitle>
|
||||
Everything in the complete release of Magic Civilization — all 16 races,
|
||||
10 fusions, and every system unlocked.
|
||||
</Subtitle>
|
||||
</PageTitle>
|
||||
|
||||
<Section>
|
||||
<SectionHeading>16 Playable Races</SectionHeading>
|
||||
<Prose>
|
||||
Each race has unique units, buildings, racial heritage techs, school
|
||||
pairings, and a distinct playstyle ranging from arcane masters to
|
||||
industrial powerhouses to conquest hordes.
|
||||
</Prose>
|
||||
<Grid>
|
||||
{RACES.map((r) => (
|
||||
<Chip key={r}>{r}</Chip>
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHeading>All 10 Fusions</SectionHeading>
|
||||
<Prose>
|
||||
Every pair of magic schools produces a unique fusion discipline with
|
||||
exclusive techs, units, and buildings. The 2-school limit means each
|
||||
player picks one fusion path per game.
|
||||
</Prose>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fusion</th>
|
||||
<th>Schools</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{FUSIONS.map((f) => (
|
||||
<tr key={f.name}>
|
||||
<td><Highlight>{f.name}</Highlight></td>
|
||||
<td>{f.schools}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHeading>Additional Systems</SectionHeading>
|
||||
<Prose>
|
||||
The full release adds depth beyond the early access core loop
|
||||
with these systems:
|
||||
</Prose>
|
||||
<Grid>
|
||||
{SYSTEMS.map((s) => (
|
||||
<Chip key={s}>{s}</Chip>
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
<AboutRow>
|
||||
<AboutLink to="/crowdfund">🎯 Crowdfund</AboutLink>
|
||||
<AboutLink to="/team">👥 Team</AboutLink>
|
||||
</AboutRow>
|
||||
</FadeIn>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
import type { ReactElement } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { FadeIn } from '@magic-civ/guide-engine'
|
||||
import { Heading, Text } from '@lilith/ui-typography'
|
||||
import { EncyclopediaCallout } from '@magic-civ/guide-engine'
|
||||
import { allMapTypes } from '@/data'
|
||||
import { PageTitle, CardGrid, Card, SectionHeading, Description } from '@magic-civ/guide-engine'
|
||||
|
||||
const Name = styled.h2`
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: ${({ theme }) => theme.colors.primary.light};
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const StatsGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.375rem;
|
||||
`
|
||||
|
||||
const Stat = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.8125rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: ${({ theme }) => theme.colors.background.secondary};
|
||||
border-radius: 4px;
|
||||
|
||||
.label { color: ${({ theme }) => theme.colors.text.muted}; }
|
||||
.value { font-weight: 700; color: ${({ theme }) => theme.colors.primary.main}; }
|
||||
`
|
||||
|
||||
const SectionLabel = styled.div`
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
`
|
||||
|
||||
const TopologySection = styled.section`
|
||||
margin-top: 3rem;
|
||||
`
|
||||
|
||||
const TopologySectionSubtext = styled.p`
|
||||
font-size: 0.8125rem;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
margin: 0 0 1.5rem;
|
||||
`
|
||||
|
||||
const TopologyCard = styled.div`
|
||||
background: ${({ theme }) => theme.colors.surface};
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
`
|
||||
|
||||
const TopologyName = styled.h3`
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: ${({ theme }) => theme.colors.primary.light};
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const TopologyBadge = styled.span<{ $default?: boolean }>`
|
||||
display: inline-block;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
background: ${({ theme, $default }) =>
|
||||
$default
|
||||
? `color-mix(in srgb, ${theme.colors.primary.main} 20%, transparent)`
|
||||
: theme.colors.background.secondary};
|
||||
color: ${({ theme, $default }) =>
|
||||
$default ? theme.colors.primary.main : theme.colors.text.muted};
|
||||
align-self: flex-start;
|
||||
`
|
||||
|
||||
const TopologyDesc = styled.p`
|
||||
font-size: 0.8125rem;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const TopologyMath = styled.p`
|
||||
font-size: 0.75rem;
|
||||
font-family: monospace;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
background: ${({ theme }) => theme.colors.background.secondary};
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin: 0;
|
||||
line-height: 1.7;
|
||||
`
|
||||
|
||||
const TOPOLOGY_MODES = [
|
||||
{
|
||||
name: 'Sphere',
|
||||
isDefault: true,
|
||||
desc: 'East and west edges wrap normally. Cross the north or south pole and you emerge on the opposite side of the globe, shifted 180° in longitude — just like a real sphere. Poles are fully navigable.',
|
||||
math: 'Pole crossing: new_x = (x + W/2) % W\nnew_y reflects back from the edge',
|
||||
},
|
||||
{
|
||||
name: 'Cylinder',
|
||||
isDefault: false,
|
||||
desc: 'East and west edges wrap (you can sail off the right edge and appear on the left). North and south poles are hard walls — same as standard Civ maps.',
|
||||
math: 'East-west: new_x = ((x % W) + W) % W\nNorth/south: hard boundary',
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
isDefault: false,
|
||||
desc: 'Hard walls on all four edges. Units cannot cross any map boundary. Traditional flat-map behavior.',
|
||||
math: 'No wrapping — all boundaries are walls',
|
||||
},
|
||||
]
|
||||
|
||||
export default function MapTypesPage(): ReactElement {
|
||||
return (
|
||||
<FadeIn duration="fast">
|
||||
<PageTitle>
|
||||
<Heading as="h1" size="2xl" marginBottom="xs">Map Types</Heading>
|
||||
<Text color="muted" size="sm">
|
||||
{allMapTypes.length} world generation presets with distinct landmass shapes and gameplay implications.
|
||||
</Text>
|
||||
</PageTitle>
|
||||
<EncyclopediaCallout entryId="climate_simulation" />
|
||||
<CardGrid>
|
||||
{allMapTypes.map((m) => (
|
||||
<Card key={m.id}>
|
||||
<Name>{m.name}</Name>
|
||||
<Description>{m.description}</Description>
|
||||
<SectionLabel>Parameters</SectionLabel>
|
||||
<StatsGrid>
|
||||
<Stat>
|
||||
<span className="label">Continents</span>
|
||||
<span className="value">{m.continent_count.min}–{m.continent_count.max}</span>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<span className="label">Ocean</span>
|
||||
<span className="value">{Math.round(m.ocean_percentage.target * 100)}%</span>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<span className="label">Coast pref</span>
|
||||
<span className="value">{(m.generation_params.start_position_prefer_coast as boolean) ? 'Yes' : 'No'}</span>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<span className="label">Min spacing</span>
|
||||
<span className="value">{m.generation_params.start_position_min_distance as number}</span>
|
||||
</Stat>
|
||||
</StatsGrid>
|
||||
</Card>
|
||||
))}
|
||||
</CardGrid>
|
||||
|
||||
<TopologySection>
|
||||
<SectionHeading>World Topology</SectionHeading>
|
||||
<TopologySectionSubtext>
|
||||
How map edges connect. Unlike most strategy games, this game supports a fully playable spherical world where the poles are real locations units can traverse.
|
||||
</TopologySectionSubtext>
|
||||
<CardGrid style={{ '--col-min': '280px' } as React.CSSProperties}>
|
||||
{TOPOLOGY_MODES.map((t) => (
|
||||
<TopologyCard key={t.name}>
|
||||
<TopologyName>{t.name}</TopologyName>
|
||||
{t.isDefault && <TopologyBadge $default>Default</TopologyBadge>}
|
||||
<TopologyDesc>{t.desc}</TopologyDesc>
|
||||
<TopologyMath>{t.math}</TopologyMath>
|
||||
</TopologyCard>
|
||||
))}
|
||||
</CardGrid>
|
||||
</TopologySection>
|
||||
</FadeIn>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue