ui(guide): 💄 Enhance EcosystemPage component with new ecosystem features, improved layout, and enhanced interactivity for better user experience

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-26 00:06:47 -07:00
parent 8515b079cc
commit 28773e2864

View file

@ -0,0 +1,255 @@
import type { ReactElement } from 'react'
import styled from 'styled-components'
import { FadeIn, PageTitle, Section, SectionHeading, Prose } from '@magic-civ/guide-engine'
import { Heading, Text } from '@lilith/ui-typography'
const QUALITY_TIERS = [
{ tier: 1, label: 'Prolific', color: '#666666', desc: 'Hardy survivors — common species only' },
{ tier: 2, label: 'Common', color: '#cccccc', desc: 'Healthy baseline — standard species diversity' },
{ tier: 3, label: 'Rare', color: '#4080e0', desc: 'Thriving ecosystem — rare species appear' },
{ tier: 4, label: 'Legendary', color: '#9040cc', desc: 'Natural wonder — landmark named, unique fauna' },
{ tier: 5, label: 'Epic', color: '#e07020', desc: 'Pristine wilderness — apex predators, ancient groves' },
] as const
const FLORA_COMPONENTS = [
{ name: 'Canopy Cover', field: 'canopy_cover', color: '#1a9928',
desc: 'Forest canopy density. Grows toward biome climax when climate matches. Shades undergrowth above 0.7.' },
{ name: 'Undergrowth', field: 'undergrowth', color: '#8cc634',
desc: 'Ground vegetation. Supports herbivores. Capped by canopy shade. Decays faster during drought.' },
{ name: 'Fungi Network', field: 'fungi_network', color: '#9040a0',
desc: 'Mycorrhizal network. Requires undergrowth > 0.3 and moisture > 0.15. Accelerates regrowth.' },
] as const
const ECOLOGY_WEIGHTS = [
{ name: 'Flora Health', weight: 0.30, desc: 'Canopy + undergrowth + fungi vs biome climax' },
{ name: 'Fauna Diversity', weight: 0.25, desc: 'Species count vs biome capacity' },
{ name: 'Biome Stability', weight: 0.25, desc: 'BiomeClassifier agrees with current biome_id' },
{ name: 'Population Balance', weight: 0.20, desc: 'Predator-prey ratio near 1:4 equilibrium' },
]
export default function EcosystemPage(): ReactElement {
return (
<FadeIn duration="fast">
<PageTitle>
<Heading as="h1" size="2xl" marginBottom="xs">Ecosystem</Heading>
<Text color="muted" size="sm">
The living world. Flora grows, fauna hunts, biomes shift, and tile quality
emerges from ecological health driving everything from resource yields to
natural wonder formation.
</Text>
</PageTitle>
<Section>
<SectionHeading>Quality Tiers (Q1-Q5)</SectionHeading>
<Prose>
Every land tile has an ecology quality from Q1 (Prolific) to Q5 (Epic).
Quality determines which species can spawn, food yield multipliers, and whether
a tile becomes a natural wonder. Q4-Q5 tiles are named by the FlavorGenerator
and function as emergent natural wonders.
</Prose>
<TierGrid>
{QUALITY_TIERS.map(({ tier, label, color, desc }) => (
<TierCard key={tier}>
<TierBadge style={{ background: color }}>Q{tier}</TierBadge>
<TierInfo>
<TierLabel>{label}</TierLabel>
<TierDesc>{desc}</TierDesc>
</TierInfo>
</TierCard>
))}
</TierGrid>
</Section>
<Section>
<SectionHeading>Flora Dynamics</SectionHeading>
<Prose>
Three vegetation layers are simulated per tile each turn: canopy, undergrowth,
and fungi network. Each grows toward biome-specific climax values, modulated
by climate match and tile quality. Cross-effects create feedback loops:
canopy shades undergrowth, undergrowth feeds fungi, fungi accelerates regrowth.
</Prose>
<ComponentGrid>
{FLORA_COMPONENTS.map(({ name, color, desc }) => (
<ComponentCard key={name}>
<ComponentDot style={{ background: color }} />
<div>
<ComponentName>{name}</ComponentName>
<ComponentDesc>{desc}</ComponentDesc>
</div>
</ComponentCard>
))}
</ComponentGrid>
</Section>
<Section>
<SectionHeading>Quality Scoring</SectionHeading>
<Prose>
Tile quality is a weighted composite of four ecology dimensions, recomputed
each turn. The score maps to Q1-Q5 tiers, capped by the biome's quality range.
</Prose>
<WeightTable>
<thead>
<tr><th>Dimension</th><th>Weight</th><th>Description</th></tr>
</thead>
<tbody>
{ECOLOGY_WEIGHTS.map(({ name, weight, desc }) => (
<tr key={name}>
<td>{name}</td>
<td>{(weight * 100).toFixed(0)}%</td>
<td>{desc}</td>
</tr>
))}
</tbody>
</WeightTable>
</Section>
<Section>
<SectionHeading>Succession and Desertification</SectionHeading>
<Prose>
When canopy remains above 80% for 50 consecutive turns, the BiomeClassifier
reclassifies the tile forest succession. Conversely, sustained drought
(moisture below 0.2 for 30 turns) triggers desertification with 2x flora decay.
After clearing (wildfire, logging), tiles progress through regrowth stages:
barren, scrub, young forest, forest with fungi accelerating the process up to 2x.
</Prose>
</Section>
<Section>
<SectionHeading>Food Web</SectionHeading>
<Prose>
Species traits determine predator-prey relationships. Carnivores eat strictly
smaller fauna (pack social grants +1 size tier). Herbivores reduce undergrowth
proportional to body size. Population dynamics follow Lotka-Volterra with
3 substeps per turn and 5% stochastic perturbation, preventing lock-step
oscillations. Populations that drop below 2 individuals migrate or go extinct.
</Prose>
</Section>
<Section>
<SectionHeading>Visualization Lenses</SectionHeading>
<Prose>
The climate simulation page now includes ecology overlay lenses. Toggle them
in the Layers panel to visualize canopy cover (green gradient), undergrowth
(yellow-green), fungi network (purple), tile quality (Q1-Q5 colors), fish
populations (blue on water), and wildlife habitat suitability.
</Prose>
</Section>
</FadeIn>
)
}
const TierGrid = styled.div`
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1rem;
`
const TierCard = styled.div`
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
background: ${({ theme }) => theme.colors.surface};
border: 1px solid ${({ theme }) => theme.colors.border.default};
border-radius: 6px;
`
const TierBadge = styled.span`
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 6px;
font-family: monospace;
font-weight: 700;
font-size: 0.875rem;
color: #fff;
flex-shrink: 0;
`
const TierInfo = styled.div`
display: flex;
flex-direction: column;
gap: 0.125rem;
`
const TierLabel = styled.span`
font-weight: 600;
font-size: 0.9375rem;
color: ${({ theme }) => theme.colors.text.primary};
`
const TierDesc = styled.span`
font-size: 0.8125rem;
color: ${({ theme }) => theme.colors.text.muted};
`
const ComponentGrid = styled.div`
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1rem;
`
const ComponentCard = styled.div`
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: ${({ theme }) => theme.colors.surface};
border: 1px solid ${({ theme }) => theme.colors.border.default};
border-radius: 6px;
`
const ComponentDot = styled.span`
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
margin-top: 0.25rem;
flex-shrink: 0;
`
const ComponentName = styled.div`
font-weight: 600;
font-size: 0.875rem;
color: ${({ theme }) => theme.colors.text.primary};
`
const ComponentDesc = styled.div`
font-size: 0.8125rem;
color: ${({ theme }) => theme.colors.text.muted};
margin-top: 0.125rem;
`
const WeightTable = styled.table`
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
font-size: 0.8125rem;
th {
text-align: left;
padding: 0.5rem 0.75rem;
border-bottom: 2px solid ${({ theme }) => theme.colors.border.default};
color: ${({ theme }) => theme.colors.text.muted};
font-weight: 600;
text-transform: uppercase;
font-size: 0.6875rem;
letter-spacing: 0.06em;
}
td {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid ${({ theme }) => theme.colors.border.default};
color: ${({ theme }) => theme.colors.text.primary};
}
td:nth-child(2) {
font-family: monospace;
font-weight: 600;
text-align: center;
width: 5rem;
}
`