From a8a7fb6f632824b53172fa5ad2be718334654ce9 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 7 Apr 2026 21:28:48 -0700 Subject: [PATCH] =?UTF-8?q?ui(guide):=20=F0=9F=92=84=20Update=20SurvivalGu?= =?UTF-8?q?idePage=20component=20layout,=20styling,=20and=20functionality?= =?UTF-8?q?=20for=20better=20UX=20and=20new=20content=20in=20Age=20of=20Dw?= =?UTF-8?q?arves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../guide/src/pages/SurvivalGuidePage.tsx | 333 +----------------- 1 file changed, 11 insertions(+), 322 deletions(-) diff --git a/public/games/age-of-dwarves/guide/src/pages/SurvivalGuidePage.tsx b/public/games/age-of-dwarves/guide/src/pages/SurvivalGuidePage.tsx index 5858cc5f..116cfb54 100644 --- a/public/games/age-of-dwarves/guide/src/pages/SurvivalGuidePage.tsx +++ b/public/games/age-of-dwarves/guide/src/pages/SurvivalGuidePage.tsx @@ -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 { - {/* Section 1: Cascade Model */}
The Cascade Model @@ -584,13 +273,13 @@ export default function SurvivalGuidePage(): ReactElement { {magic.map((b) => { const popSave = getEffectValue(b, 'catastrophe_pop_save') - const aerosol = getEffectValue(b, 'aerosol_mitigation') + const aerosolMit = getEffectValue(b, 'aerosol_mitigation') return ( {b.name} {b.tech_required ? formatTech(b.tech_required) : '—'} {popSave != null ? popSave : '—'} - {aerosol != null ? `${aerosol}%` : '—'} + {aerosolMit != null ? `${aerosolMit}%` : '—'} ) })}