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:
parent
8515b079cc
commit
28773e2864
1 changed files with 255 additions and 0 deletions
255
guide/age-of-four/src/pages/EcosystemPage.tsx
Normal file
255
guide/age-of-four/src/pages/EcosystemPage.tsx
Normal 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;
|
||||
}
|
||||
`
|
||||
Loading…
Add table
Reference in a new issue