ui(guide): 💄 Update SurvivalGuidePage component layout, styling, and functionality for better UX and new content in Age of Dwarves
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
0414b0d53c
commit
a8a7fb6f63
1 changed files with 11 additions and 322 deletions
|
|
@ -1,5 +1,4 @@
|
|||
import type { ReactElement } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { FadeIn } from '@magic-civ/guide-engine'
|
||||
import { PageTitle, PageHeading, PageSubtitle, Section, SectionHeading, Prose } from '@magic-civ/guide-engine'
|
||||
import { allBuildings, climateSpec } from '@/data'
|
||||
|
|
@ -9,345 +8,36 @@ import {
|
|||
getEffectValue,
|
||||
formatTech,
|
||||
} from '@/data/derived'
|
||||
import {
|
||||
CascadesGrid, CascadeRow, CascadeStep, CascadeArrow, CascadeLabel,
|
||||
InfoGrid, InfoTile, TileLabel, TileValue, TileSub,
|
||||
Callout, CalloutTitle,
|
||||
PathsRow, PathBlock, PathTitle, ProtTable, ProtTh, ProtTd, ProtTdStrong,
|
||||
ScenarioGrid, ScenarioCard, ScenarioTitle, ScenarioSubtitle,
|
||||
ScenarioBlock, ScenarioBlockLabel, ScenarioBlockText,
|
||||
} from './survival-guide/styled'
|
||||
import { CASCADE_CHAINS, SCENARIOS } from './survival-guide/data'
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
interface ManaUpkeep { amount?: number; school?: string }
|
||||
|
||||
/** Safely read an optional `mana_upkeep` field that exists on some building JSON but not the typed schema yet. */
|
||||
function getManaUpkeep(b: object): ManaUpkeep | undefined {
|
||||
return 'mana_upkeep' in b ? (b as { mana_upkeep: ManaUpkeep }).mana_upkeep : undefined
|
||||
}
|
||||
|
||||
/** Safely read optional `variance` from an effect entry. */
|
||||
function getEffectVariance(effects: { type: string }[], effectType: string): number {
|
||||
const match = effects.find((e) => e.type === effectType)
|
||||
if (match && 'variance' in match) return (match as { variance: number }).variance
|
||||
return 0
|
||||
}
|
||||
|
||||
// ─── Cascade Flow Diagrams ────────────────────────────────────────────────────
|
||||
|
||||
const CascadesGrid = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
||||
const CascadeRow = styled.div<{ $color: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
padding: 0.75rem 1rem;
|
||||
background: ${({ theme }) => theme.colors.background.secondary};
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid ${({ $color }) => $color};
|
||||
`
|
||||
|
||||
const CascadeStep = styled.div<{ $color: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.625rem;
|
||||
background: ${({ $color }) => $color}18;
|
||||
border: 1px solid ${({ $color }) => $color}44;
|
||||
border-radius: 5px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: ${({ $color }) => $color};
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const CascadeArrow = styled.span`
|
||||
font-size: 0.875rem;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
padding: 0 0.375rem;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const CascadeLabel = styled.span<{ $color: string }>`
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: ${({ $color }) => $color};
|
||||
margin-right: 0.625rem;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
// ─── Aerosol section ─────────────────────────────────────────────────────────
|
||||
|
||||
const InfoGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
||||
const InfoTile = styled.div`
|
||||
background: ${({ theme }) => theme.colors.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
border-radius: 7px;
|
||||
padding: 0.75rem 0.875rem;
|
||||
`
|
||||
|
||||
const TileLabel = styled.div`
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
margin-bottom: 0.3rem;
|
||||
`
|
||||
|
||||
const TileValue = styled.div`
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
`
|
||||
|
||||
const TileSub = styled.div`
|
||||
font-size: 0.75rem;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
margin-top: 0.125rem;
|
||||
line-height: 1.4;
|
||||
`
|
||||
|
||||
// ─── Callout ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const Callout = styled.div<{ $variant?: 'warning' | 'info' | 'danger' }>`
|
||||
border-radius: 7px;
|
||||
border: 1px solid ${({ $variant }) =>
|
||||
$variant === 'danger' ? 'color-mix(in srgb, #8b1a1a 40%, transparent)' :
|
||||
$variant === 'warning' ? 'color-mix(in srgb, #c9a84c 35%, transparent)' :
|
||||
'color-mix(in srgb, #4080c0 30%, transparent)'};
|
||||
background: ${({ $variant }) =>
|
||||
$variant === 'danger' ? 'color-mix(in srgb, #8b1a1a 8%, transparent)' :
|
||||
$variant === 'warning' ? 'color-mix(in srgb, #c9a84c 7%, transparent)' :
|
||||
'color-mix(in srgb, #4080c0 6%, transparent)'};
|
||||
padding: 0.875rem 1rem;
|
||||
font-size: 0.8125rem;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
line-height: 1.65;
|
||||
margin: 1rem 0;
|
||||
`
|
||||
|
||||
const CalloutTitle = styled.div<{ $variant?: 'warning' | 'info' | 'danger' }>`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.4rem;
|
||||
color: ${({ $variant }) =>
|
||||
$variant === 'danger' ? '#e06060' :
|
||||
$variant === 'warning' ? '#d4a84c' :
|
||||
'#6090c0'};
|
||||
`
|
||||
|
||||
// ─── Protection tables ───────────────────────────────────────────────────────
|
||||
|
||||
const PathsRow = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.25rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
@media (max-width: 680px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`
|
||||
|
||||
const PathBlock = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
`
|
||||
|
||||
const PathTitle = styled.div<{ $color: string }>`
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: ${({ $color }) => $color};
|
||||
padding-bottom: 0.375rem;
|
||||
border-bottom: 2px solid ${({ $color }) => $color}44;
|
||||
`
|
||||
|
||||
const ProtTable = styled.table`
|
||||
border-collapse: collapse;
|
||||
font-size: 0.75rem;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const ProtTh = styled.th`
|
||||
text-align: left;
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
font-weight: 600;
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.4px;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const ProtTd = styled.td`
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
vertical-align: top;
|
||||
`
|
||||
|
||||
const ProtTdStrong = styled(ProtTd)`
|
||||
font-weight: 700;
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
`
|
||||
|
||||
// ─── Scenario playbook ───────────────────────────────────────────────────────
|
||||
|
||||
const ScenarioGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
||||
const ScenarioCard = styled.div<{ $color: string }>`
|
||||
border-radius: 8px;
|
||||
border: 1px solid ${({ $color }) => $color}40;
|
||||
background: ${({ $color }) => $color}0c;
|
||||
padding: 1rem 1.125rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
`
|
||||
|
||||
const ScenarioTitle = styled.div<{ $color: string }>`
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: ${({ $color }) => $color};
|
||||
`
|
||||
|
||||
const ScenarioSubtitle = styled.div`
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
margin-top: -0.375rem;
|
||||
`
|
||||
|
||||
const ScenarioBlock = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
`
|
||||
|
||||
const ScenarioBlockLabel = styled.div`
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
`
|
||||
|
||||
const ScenarioBlockText = styled.div`
|
||||
font-size: 0.8rem;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
line-height: 1.55;
|
||||
`
|
||||
|
||||
// ─── Data ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const VOLCANIC_COLOR = '#c04020'
|
||||
const IMPACT_COLOR = '#8060b0'
|
||||
const SOLAR_COLOR = '#c0a020'
|
||||
const PANDEMIC_COLOR = '#804040'
|
||||
|
||||
interface CascadeChain {
|
||||
label: string
|
||||
color: string
|
||||
steps: string[]
|
||||
}
|
||||
|
||||
const CASCADE_CHAINS: CascadeChain[] = [
|
||||
{
|
||||
label: 'Volcanic',
|
||||
color: VOLCANIC_COLOR,
|
||||
steps: ['Volcanic T5', 'Aerosol Cloud', 'Global Cooling', 'Terrain Degradation', 'Food Crisis', 'Population Collapse'],
|
||||
},
|
||||
{
|
||||
label: 'Impact',
|
||||
color: IMPACT_COLOR,
|
||||
steps: ['Impact T5', 'Global Aerosol', 'Biome Collapse', 'Famine', 'Extinction'],
|
||||
},
|
||||
{
|
||||
label: 'Solar/Glacial',
|
||||
color: SOLAR_COLOR,
|
||||
steps: ['Solar Minimum', 'Glacial ×2 freq', 'Ice Sheet T4', 'Warming Trigger', 'Runaway Melt T5', 'Tsunami: All Coasts'],
|
||||
},
|
||||
{
|
||||
label: 'Pandemic',
|
||||
color: PANDEMIC_COLOR,
|
||||
steps: ['Pandemic T3', 'Trade Spread', 'Multi-city Pop Loss', 'Production Collapse', 'Military Vulnerability'],
|
||||
},
|
||||
]
|
||||
|
||||
interface ScenarioData {
|
||||
title: string
|
||||
subtitle: string
|
||||
color: string
|
||||
trigger: string
|
||||
what: string
|
||||
response: string
|
||||
recovery: string
|
||||
}
|
||||
|
||||
const SCENARIOS: ScenarioData[] = [
|
||||
{
|
||||
title: '"The Volcanic Winter"',
|
||||
subtitle: 'Volcanic T5 — Supervolcano',
|
||||
color: VOLCANIC_COLOR,
|
||||
trigger: 'Random volcanic event rolls T5.',
|
||||
what: '5-hex radius scorched to desert. Aerosol 0.5 injected in 8-hex radius. Approximately 2 turns (20 years) of global cooling and drying. Rivers near the blast zone may freeze.',
|
||||
response: 'Shelters protect 1 pop per turn. Stockpile food reserves before winter arrives — once aerosol is injected you have 1 turn to prepare. Hardened Granary reserve buys 3 turns of buffer.',
|
||||
recovery: 'Obsidian spawns at the crater center (Chaos T3 ley anchor). The scorched radius recovers terrain quality slowly over 10–20 turns.',
|
||||
},
|
||||
{
|
||||
title: '"Extinction Event"',
|
||||
subtitle: 'Impact T5 — Extinction Asteroid',
|
||||
color: IMPACT_COLOR,
|
||||
trigger: 'Random impact event rolls T5. Astronomically rare.',
|
||||
what: '6-hex crater vaporized instantly. Global aerosol 1.0 — a 20-turn (200-year) impact winter. All biomes across the map lose 2 quality. A T5 Death+Chaos mithril vein spawns at the crater center.',
|
||||
response: 'Only Doomsday Vault (20–60% survival roll) or Energy Shield (complete nullification) matter. Bomb Shelter, Warding Circle, and Arcane Dome are insufficient. Pre-build before the event or accept catastrophic losses. There is no mid-event recovery path.',
|
||||
recovery: 'The mithril core is the rarest resource in the game. Whoever controls it dominates late-game unit production. Racing to claim the crater is the primary post-extinction strategic objective.',
|
||||
},
|
||||
{
|
||||
title: '"The Thwaites Cascade"',
|
||||
subtitle: 'Solar Minimum → Ice Sheet → T5 Melt Chain',
|
||||
color: SOLAR_COLOR,
|
||||
trigger: 'Solar T4 (Grand Minimum) doubles glacial frequency → Glacial T4 forms an ice sheet → minimum ends, solar output rebounds above the ice-sheet collapse threshold → Glacial T5 runaway melt.',
|
||||
what: 'A multi-turn chain disaster. The ice sheet forms slowly over ~10 turns, then suddenly collapses: simultaneous T3 tsunami on every coastal tile in the world, 0.20 moisture surge globally, and a warming feedback loop that can trigger further events.',
|
||||
response: 'During the ice sheet phase (T4 active), move coastal cities inland or fortify them. The ice sheet will collapse — it is a matter of when, not if, once temperatures begin to rise again. Tsunami-resistant coastal defenses reduce improvement damage.',
|
||||
recovery: 'Post-collapse warming means the coastlines quickly become fertile again. The moisture surge benefits inland agriculture. This chain is destructive in the short term but leaves the world warmer and wetter.',
|
||||
},
|
||||
{
|
||||
title: '"Trade Plague"',
|
||||
subtitle: 'Pandemic T3+',
|
||||
color: PANDEMIC_COLOR,
|
||||
trigger: 'Pandemic event rolls T3+. Spreads via road-connected and trade-route cities.',
|
||||
what: 'T3: −1 pop/turn per connected city for 8 turns. T4: spreads to ALL players via active trade routes, −2 pop/turn, kills garrisoned units. T5: global, population halved across all civilizations.',
|
||||
response: 'Cut trade routes immediately — pillage roads to sever spread vectors. The Life T3 quarantine spell blocks adjacency transmission. Hospital building reduces pop loss by 1 per turn. Isolationist governments have a natural advantage here.',
|
||||
recovery: 'Cities recover population slowly after the pandemic ends. T5 is generationally catastrophic — prioritize rebuilding food infrastructure and Granaries before military production.',
|
||||
},
|
||||
]
|
||||
|
||||
// ─── Page ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function SurvivalGuidePage(): ReactElement {
|
||||
const { mundane, magic, specialized } = classifyProtectionBuildings(allBuildings)
|
||||
const aerosol = getAerosolParams(climateSpec)
|
||||
|
||||
// Find the two aerosol-mitigating buildings for the dedicated callout tiles
|
||||
const arcaneDome = magic.find((b) => b.id === 'arcane_dome')
|
||||
const energyShield = magic.find((b) => b.id === 'energy_shield')
|
||||
|
||||
|
|
@ -362,7 +52,6 @@ export default function SurvivalGuidePage(): ReactElement {
|
|||
</PageSubtitle>
|
||||
</PageTitle>
|
||||
|
||||
|
||||
{/* Section 1: Cascade Model */}
|
||||
<Section>
|
||||
<SectionHeading>The Cascade Model</SectionHeading>
|
||||
|
|
@ -584,13 +273,13 @@ export default function SurvivalGuidePage(): ReactElement {
|
|||
<tbody>
|
||||
{magic.map((b) => {
|
||||
const popSave = getEffectValue(b, 'catastrophe_pop_save')
|
||||
const aerosol = getEffectValue(b, 'aerosol_mitigation')
|
||||
const aerosolMit = getEffectValue(b, 'aerosol_mitigation')
|
||||
return (
|
||||
<tr key={b.id}>
|
||||
<ProtTdStrong>{b.name}</ProtTdStrong>
|
||||
<ProtTd>{b.tech_required ? formatTech(b.tech_required) : '—'}</ProtTd>
|
||||
<ProtTd>{popSave != null ? popSave : '—'}</ProtTd>
|
||||
<ProtTd>{aerosol != null ? `${aerosol}%` : '—'}</ProtTd>
|
||||
<ProtTd>{aerosolMit != null ? `${aerosolMit}%` : '—'}</ProtTd>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue