ui(biome-browser): 💄 Enhance BiomeBrowserPage UI with improved layout and interactive features for better biome navigation in Age of Dwarves guide
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
1c785d367b
commit
18ac012b96
1 changed files with 0 additions and 351 deletions
|
|
@ -1,351 +0,0 @@
|
|||
import { useState, useMemo, type ReactElement } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { FadeIn, PageTitle, Section, SectionHeading, Prose, FilterRow, FilterChip } from '@magic-civ/guide-engine'
|
||||
import { Heading, Text } from '@lilith/ui-typography'
|
||||
import { ALL_BIOMES, BIOME_CATEGORIES, QUALITY_COLORS, QUALITY_LABELS } from '@/data/ecology'
|
||||
import type { BiomeDisplay } from '@/data/ecology'
|
||||
|
||||
function BiomePieChart({ biomes }: { biomes: BiomeDisplay[] }): ReactElement {
|
||||
const total = biomes.length
|
||||
let currentAngle = 0
|
||||
|
||||
const slices = biomes.map((biome) => {
|
||||
const fraction = 1 / total
|
||||
const startAngle = currentAngle
|
||||
const endAngle = currentAngle + fraction * 360
|
||||
currentAngle = endAngle
|
||||
|
||||
const startRad = (startAngle - 90) * (Math.PI / 180)
|
||||
const endRad = (endAngle - 90) * (Math.PI / 180)
|
||||
const largeArc = fraction > 0.5 ? 1 : 0
|
||||
const x1 = 50 + 45 * Math.cos(startRad)
|
||||
const y1 = 50 + 45 * Math.sin(startRad)
|
||||
const x2 = 50 + 45 * Math.cos(endRad)
|
||||
const y2 = 50 + 45 * Math.sin(endRad)
|
||||
|
||||
return {
|
||||
biome,
|
||||
d: `M 50 50 L ${x1} ${y1} A 45 45 0 ${largeArc} 1 ${x2} ${y2} Z`,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<PieContainer>
|
||||
<svg viewBox="0 0 100 100" width="280" height="280">
|
||||
{slices.map(({ biome, d }) => (
|
||||
<path key={biome.id} d={d} fill={biome.color} stroke="#1a1510" strokeWidth="0.3">
|
||||
<title>{biome.name}</title>
|
||||
</path>
|
||||
))}
|
||||
</svg>
|
||||
<PieLegend>
|
||||
{BIOME_CATEGORIES.map(cat => {
|
||||
const count = biomes.filter(b => b.category === cat.id).length
|
||||
if (count === 0) return null
|
||||
return (
|
||||
<PieLegendItem key={cat.id}>
|
||||
<PieLegendDot style={{ background: cat.color }} />
|
||||
<span>{cat.label} ({count})</span>
|
||||
</PieLegendItem>
|
||||
)
|
||||
})}
|
||||
</PieLegend>
|
||||
</PieContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function BiomeCard({ biome }: { biome: BiomeDisplay }): ReactElement {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<BiomeSwatch style={{ background: biome.color }} />
|
||||
<CardTitle>{biome.name}</CardTitle>
|
||||
<CategoryTag>{biome.category}</CategoryTag>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<StatRow>
|
||||
<StatLabel>Quality Range</StatLabel>
|
||||
<QualityRange>
|
||||
{Array.from({ length: biome.quality_range[1] - biome.quality_range[0] + 1 }, (_, i) => {
|
||||
const q = biome.quality_range[0] + i
|
||||
return (
|
||||
<QualityDot key={q} style={{ background: QUALITY_COLORS[q] }} title={`Q${q} ${QUALITY_LABELS[q]}`}>
|
||||
{q}
|
||||
</QualityDot>
|
||||
)
|
||||
})}
|
||||
</QualityRange>
|
||||
</StatRow>
|
||||
<StatRow>
|
||||
<StatLabel>Temperature</StatLabel>
|
||||
<StatValue>{biome.temp_range[0].toFixed(2)} - {biome.temp_range[1].toFixed(2)}</StatValue>
|
||||
</StatRow>
|
||||
<StatRow>
|
||||
<StatLabel>Moisture</StatLabel>
|
||||
<StatValue>{biome.moisture_range[0].toFixed(2)} - {biome.moisture_range[1].toFixed(2)}</StatValue>
|
||||
</StatRow>
|
||||
<StatRow>
|
||||
<StatLabel>Fauna Capacity</StatLabel>
|
||||
<StatValue>{biome.fauna_capacity}</StatValue>
|
||||
</StatRow>
|
||||
<FloraBar>
|
||||
<FloraSegment
|
||||
style={{ width: `${biome.flora_climax.canopy * 100}%`, background: '#1a9928' }}
|
||||
title={`Canopy: ${biome.flora_climax.canopy}`}
|
||||
/>
|
||||
<FloraSegment
|
||||
style={{ width: `${biome.flora_climax.undergrowth * 100}%`, background: '#8cc634' }}
|
||||
title={`Undergrowth: ${biome.flora_climax.undergrowth}`}
|
||||
/>
|
||||
<FloraSegment
|
||||
style={{ width: `${biome.flora_climax.fungi * 100}%`, background: '#9040a0' }}
|
||||
title={`Fungi: ${biome.flora_climax.fungi}`}
|
||||
/>
|
||||
</FloraBar>
|
||||
<FloraLabels>
|
||||
<span>Canopy {biome.flora_climax.canopy}</span>
|
||||
<span>Undergrowth {biome.flora_climax.undergrowth}</span>
|
||||
<span>Fungi {biome.flora_climax.fungi}</span>
|
||||
</FloraLabels>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default function BiomeBrowserPage(): ReactElement {
|
||||
const [categoryFilter, setCategoryFilter] = useState<string>('all')
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (categoryFilter === 'all') return ALL_BIOMES
|
||||
return ALL_BIOMES.filter(b => b.category === categoryFilter)
|
||||
}, [categoryFilter])
|
||||
|
||||
return (
|
||||
<FadeIn duration="fast">
|
||||
<PageTitle>
|
||||
<Heading as="h1" size="2xl" marginBottom="xs">Biome Browser</Heading>
|
||||
<Text color="muted" size="sm">
|
||||
{ALL_BIOMES.length} biomes across aquatic, tropical, temperate, cold, elevation, and
|
||||
special categories. Each biome defines climate ranges, flora climax values, and fauna capacity.
|
||||
</Text>
|
||||
</PageTitle>
|
||||
|
||||
<Section>
|
||||
<SectionHeading>Distribution</SectionHeading>
|
||||
<Prose>
|
||||
Biomes emerge from substrate type, temperature, moisture, and canopy state.
|
||||
The BiomeClassifier reclassifies tiles each turn when conditions change significantly.
|
||||
</Prose>
|
||||
<BiomePieChart biomes={ALL_BIOMES} />
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHeading>All Biomes</SectionHeading>
|
||||
<FilterRow>
|
||||
<FilterChip $active={categoryFilter === 'all'} onClick={() => setCategoryFilter('all')}>
|
||||
All ({ALL_BIOMES.length})
|
||||
</FilterChip>
|
||||
{BIOME_CATEGORIES.map(cat => {
|
||||
const count = ALL_BIOMES.filter(b => b.category === cat.id).length
|
||||
return (
|
||||
<FilterChip
|
||||
key={cat.id}
|
||||
$active={categoryFilter === cat.id}
|
||||
onClick={() => setCategoryFilter(cat.id)}
|
||||
>
|
||||
{cat.label} ({count})
|
||||
</FilterChip>
|
||||
)
|
||||
})}
|
||||
</FilterRow>
|
||||
<BiomeGrid>
|
||||
{filtered.map(biome => (
|
||||
<BiomeCard key={biome.id} biome={biome} />
|
||||
))}
|
||||
</BiomeGrid>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHeading>Color Legend</SectionHeading>
|
||||
<ColorLegend>
|
||||
{ALL_BIOMES.map(biome => (
|
||||
<LegendEntry key={biome.id}>
|
||||
<LegendSwatch style={{ background: biome.color }} />
|
||||
<LegendName>{biome.name}</LegendName>
|
||||
</LegendEntry>
|
||||
))}
|
||||
</ColorLegend>
|
||||
</Section>
|
||||
</FadeIn>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styled components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PieContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
|
||||
const PieLegend = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
`
|
||||
|
||||
const PieLegendItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
`
|
||||
|
||||
const PieLegendDot = styled.span`
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const BiomeGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
||||
const Card = styled.div`
|
||||
background: ${({ theme }) => theme.colors.surface};
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const CardHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
`
|
||||
|
||||
const BiomeSwatch = styled.span`
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const CardTitle = styled.span`
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const CategoryTag = styled.span`
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
padding: 0.125rem 0.375rem;
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
border-radius: 3px;
|
||||
`
|
||||
|
||||
const CardBody = styled.div`
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
`
|
||||
|
||||
const StatRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.8125rem;
|
||||
`
|
||||
|
||||
const StatLabel = styled.span`
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
`
|
||||
|
||||
const StatValue = styled.span`
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
`
|
||||
|
||||
const QualityRange = styled.div`
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
`
|
||||
|
||||
const QualityDot = styled.span`
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
font-family: monospace;
|
||||
color: #fff;
|
||||
`
|
||||
|
||||
const FloraBar = styled.div`
|
||||
display: flex;
|
||||
height: 0.375rem;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
background: ${({ theme }) => theme.colors.border.default};
|
||||
margin-top: 0.25rem;
|
||||
`
|
||||
|
||||
const FloraSegment = styled.div`
|
||||
height: 100%;
|
||||
min-width: 1px;
|
||||
`
|
||||
|
||||
const FloraLabels = styled.div`
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.6875rem;
|
||||
font-family: monospace;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
`
|
||||
|
||||
const ColorLegend = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.5rem;
|
||||
`
|
||||
|
||||
const LegendEntry = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
`
|
||||
|
||||
const LegendSwatch = styled.span`
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const LegendName = styled.span`
|
||||
white-space: nowrap;
|
||||
`
|
||||
Loading…
Add table
Reference in a new issue