diff --git a/public/games/age-of-dwarves/guide/src/App.tsx b/public/games/age-of-dwarves/guide/src/App.tsx
index 18ac20e2..a1e07a9d 100644
--- a/public/games/age-of-dwarves/guide/src/App.tsx
+++ b/public/games/age-of-dwarves/guide/src/App.tsx
@@ -18,7 +18,7 @@ import { getThemeForPreferences } from '@/theme/fantasy-theme'
import { allRaces } from '@/data/game'
import { guideData } from '@/app/guide-data'
import {
- TerrainPage, ResourcesPage, MapTypesPage,
+ TerrainPage, ResourcesPage, MapTypesPage, EconomyResourcesPage,
ClimateOverviewPage, ClimateEventsPage, ClimateTerrainPage, ClimateSimulationPage,
SurvivalGuidePage, EcosystemPage, BiomeBrowserPage, FloraPage,
FaunaFloraConnectionsPage, SpeciesBrowserPage, FoodWebPage,
@@ -96,6 +96,9 @@ export default function App(): ReactElement {
} />
} />
+ {/* Economy */}
+ } />
+
{/* Climate & Survival */}
} />
} />
diff --git a/public/games/age-of-dwarves/guide/src/app/lazy-pages.ts b/public/games/age-of-dwarves/guide/src/app/lazy-pages.ts
index 574693de..ac38110c 100644
--- a/public/games/age-of-dwarves/guide/src/app/lazy-pages.ts
+++ b/public/games/age-of-dwarves/guide/src/app/lazy-pages.ts
@@ -7,6 +7,9 @@ import { lazy } from 'react'
// The Map
export const TerrainPage = lazy(() => import('@/pages/TerrainPage'))
export const ResourcesPage = lazy(() => import('@/pages/ResourcesPage'))
+
+// Economy
+export const EconomyResourcesPage = lazy(() => import('@/pages/EconomyResourcesPage'))
export const MapTypesPage = lazy(() => import('@/pages/MapTypesPage'))
// Climate & Survival
diff --git a/public/games/age-of-dwarves/guide/src/nav.tsx b/public/games/age-of-dwarves/guide/src/nav.tsx
index 1db44f04..6f85c488 100644
--- a/public/games/age-of-dwarves/guide/src/nav.tsx
+++ b/public/games/age-of-dwarves/guide/src/nav.tsx
@@ -76,6 +76,12 @@ export const NAV: NavGroup[] = [
{ to: '/empire/victory', icon: 'π', label: 'Victory' },
],
},
+ {
+ title: 'Economy',
+ items: [
+ { to: '/economy/resources', icon: 'β', label: 'Resources' },
+ ],
+ },
{
title: 'Research',
items: [
diff --git a/public/games/age-of-dwarves/guide/src/pages/EconomyResourcesPage.tsx b/public/games/age-of-dwarves/guide/src/pages/EconomyResourcesPage.tsx
new file mode 100644
index 00000000..e95511f2
--- /dev/null
+++ b/public/games/age-of-dwarves/guide/src/pages/EconomyResourcesPage.tsx
@@ -0,0 +1,488 @@
+import { useMemo, type ReactElement } from 'react'
+import styled from 'styled-components'
+import { FadeIn, PageTitle, Section, SectionHeading, Prose, Card, Callout, CalloutText } from '@magic-civ/guide-engine'
+import { Heading, Text } from '@lilith/ui-typography'
+
+// βββ Data imports βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+// Deposit JSON files β source of truth for resource data
+const depositMods = import.meta.glob(
+ '../../../../../resources/deposits/*.json',
+ { eager: true, import: 'default' },
+) as Record
+
+// Biome JSON files β source of truth for biomeβcollectible mapping
+const biomeMods = import.meta.glob(
+ '../../../../../resources/biomes/*/biomes.json',
+ { eager: true, import: 'default' },
+) as Record
+
+// βββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+interface QualityLevel {
+ multiplier: number
+ label: string
+}
+
+interface DepositRecord {
+ id: string
+ name: string
+ category: 'bonus' | 'luxury' | 'strategic'
+ description: string
+ terrains: string[]
+ revealed_by_tech: string | null
+ yields: { food: number; production: number; trade: number; culture: number }
+ food_bonus?: number
+ production_bonus?: number
+ trade_bonus?: number
+ culture_bonus?: number
+ happiness_per_unique_copy?: number
+ gates_units?: string[]
+ gates_buildings?: string[]
+ improvement_required?: string
+ quality_scale?: Record
+ tier?: number
+}
+
+interface BiomeCollectible {
+ resource: string
+ base_quantity: number
+ quality_range: [number, number]
+}
+
+interface Biome {
+ id: string
+ name: string
+ category?: string
+ collectibles?: BiomeCollectible[]
+}
+
+interface BiomeCollection {
+ collection: string
+ biomes: Biome[]
+}
+
+// βββ Derived data βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function buildDerivedData() {
+ // Filter out schema/category files, collect only deposit records
+ const deposits = Object.values(depositMods).filter(
+ (d): d is DepositRecord => d != null && typeof d.id === 'string' && typeof d.category === 'string',
+ )
+
+ const byCategory = {
+ bonus: deposits.filter(d => d.category === 'bonus'),
+ luxury: deposits.filter(d => d.category === 'luxury'),
+ strategic: deposits.filter(d => d.category === 'strategic'),
+ }
+
+ // Biome β collectible resource IDs
+ const biomeCollectibles = new Map()
+ for (const collection of Object.values(biomeMods)) {
+ if (!collection?.biomes) continue
+ for (const biome of collection.biomes) {
+ if (!biome.collectibles?.length) continue
+ biomeCollectibles.set(biome.id, {
+ name: biome.name,
+ collection: collection.collection,
+ resources: biome.collectibles.map(c => c.resource),
+ })
+ }
+ }
+
+ // Resource β biomes that contain it
+ const resourceToBiomes = new Map()
+ for (const [, biomeData] of biomeCollectibles) {
+ for (const res of biomeData.resources) {
+ const existing = resourceToBiomes.get(res) ?? []
+ existing.push(biomeData.name)
+ resourceToBiomes.set(res, existing)
+ }
+ }
+
+ // Strategic resources that gate units
+ const gatingSummary = deposits
+ .filter(d => d.gates_units && d.gates_units.length > 0)
+ .map(d => ({ id: d.id, name: d.name, gates: d.gates_units! }))
+
+ // Representative quality scale (same schema for all deposits β use first one found)
+ const sampleDeposit = deposits.find(d => d.quality_scale != null)
+ const qualityScale = sampleDeposit?.quality_scale ?? null
+
+ return { deposits, byCategory, biomeCollectibles, resourceToBiomes, gatingSummary, qualityScale }
+}
+
+// βββ Styled components ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const CategoryGrid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 1rem;
+`
+
+const CategoryCard = styled(Card)<{ $accent: string }>`
+ border-top: 3px solid ${({ $accent }) => $accent};
+ padding: 1.125rem;
+ gap: 0.625rem;
+`
+
+const CategoryLabel = styled.h3`
+ font-size: 0.875rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ color: ${({ theme }) => theme.colors.primary.light};
+ margin: 0;
+`
+
+const CategoryStat = styled.div`
+ font-size: 0.8125rem;
+ color: ${({ theme }) => theme.colors.text.secondary};
+ display: flex;
+ gap: 0.375rem;
+
+ span:first-child {
+ color: ${({ theme }) => theme.colors.text.muted};
+ min-width: 5rem;
+ flex-shrink: 0;
+ }
+`
+
+const QualityTable = styled.div`
+ display: grid;
+ grid-template-columns: 4rem 6rem 3.5rem 1fr;
+ gap: 0;
+ border: 1px solid ${({ theme }) => theme.colors.border.default};
+ border-radius: 6px;
+ overflow: hidden;
+ font-size: 0.8125rem;
+`
+
+const QRow = styled.div<{ $header?: boolean }>`
+ display: contents;
+
+ > * {
+ padding: 0.5rem 0.75rem;
+ background: ${({ $header, theme }) =>
+ $header ? theme.colors.background.tertiary : 'transparent'};
+ border-bottom: 1px solid ${({ theme }) => theme.colors.border.default};
+ color: ${({ $header, theme }) =>
+ $header ? theme.colors.text.muted : theme.colors.text.secondary};
+ font-weight: ${({ $header }) => ($header ? 700 : 400)};
+ font-size: ${({ $header }) => ($header ? '0.6875rem' : '0.8125rem')};
+ text-transform: ${({ $header }) => ($header ? 'uppercase' : 'none')};
+ letter-spacing: ${({ $header }) => ($header ? '0.5px' : 'normal')};
+ }
+
+ &:last-child > * {
+ border-bottom: none;
+ }
+`
+
+const QualityDot = styled.span<{ $color: string }>`
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: ${({ $color }) => $color};
+ margin-right: 0.375rem;
+ vertical-align: middle;
+`
+
+const BiomeTable = styled.div`
+ display: grid;
+ grid-template-columns: minmax(140px, 1fr) minmax(100px, auto) 1fr;
+ gap: 0;
+ border: 1px solid ${({ theme }) => theme.colors.border.default};
+ border-radius: 6px;
+ overflow: hidden;
+ font-size: 0.8125rem;
+`
+
+const BRow = styled.div<{ $header?: boolean }>`
+ display: contents;
+
+ > * {
+ padding: 0.4rem 0.75rem;
+ background: ${({ $header, theme }) =>
+ $header ? theme.colors.background.tertiary : 'transparent'};
+ border-bottom: 1px solid ${({ theme }) => theme.colors.border.default};
+ color: ${({ $header, theme }) =>
+ $header ? theme.colors.text.muted : theme.colors.text.secondary};
+ font-weight: ${({ $header }) => ($header ? 700 : 400)};
+ font-size: ${({ $header }) => ($header ? '0.6875rem' : '0.8125rem')};
+ text-transform: ${({ $header }) => ($header ? 'uppercase' : 'none')};
+ letter-spacing: ${({ $header }) => ($header ? '0.5px' : 'normal')};
+ align-self: start;
+ }
+
+ &:last-child > * {
+ border-bottom: none;
+ }
+`
+
+const CollectibleChip = styled.span`
+ display: inline-block;
+ background: ${({ theme }) => theme.colors.background.tertiary};
+ border: 1px solid ${({ theme }) => theme.colors.border.default};
+ border-radius: 4px;
+ padding: 0.1rem 0.35rem;
+ font-size: 0.6875rem;
+ margin: 0.1rem 0.15rem;
+ color: ${({ theme }) => theme.colors.text.secondary};
+ white-space: nowrap;
+`
+
+const GatingGrid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 0.625rem;
+`
+
+const GatingCard = styled(Card)`
+ padding: 0.875rem;
+ gap: 0.375rem;
+`
+
+const ResourceName = styled.h4`
+ font-size: 0.875rem;
+ font-weight: 700;
+ color: ${({ theme }) => theme.colors.primary.light};
+ margin: 0;
+`
+
+const UnitList = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+`
+
+const UnitChip = styled.span`
+ font-size: 0.6875rem;
+ font-weight: 600;
+ padding: 0.15rem 0.4rem;
+ border-radius: 4px;
+ background: ${({ theme }) => theme.colors.primary.main}18;
+ color: ${({ theme }) => theme.colors.primary.light};
+ border: 1px solid ${({ theme }) => theme.colors.primary.main}40;
+`
+
+const TechBadge = styled.span`
+ font-size: 0.6875rem;
+ color: ${({ theme }) => theme.colors.text.muted};
+`
+
+const HappinessBadge = styled.span`
+ font-size: 0.6875rem;
+ font-weight: 600;
+ color: #e8b84b;
+`
+
+// βββ Quality colors ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const QUALITY_COLORS: Record = {
+ '1': '#c8c0c8',
+ '2': '#BCC6CC',
+ '3': '#00B8E6',
+ '4': '#b8960c',
+ '5': '#8b2be2',
+}
+
+const CATEGORY_ACCENTS: Record = {
+ bonus: '#4caf50',
+ luxury: '#ffd700',
+ strategic: '#e07030',
+}
+
+// βββ Page ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export default function EconomyResourcesPage(): ReactElement {
+ const { byCategory, biomeCollectibles, gatingSummary, qualityScale } = useMemo(
+ () => buildDerivedData(),
+ [],
+ )
+
+ const qualityRows = qualityScale
+ ? Object.entries(qualityScale).sort(([a], [b]) => Number(a) - Number(b))
+ : []
+
+ // Sort biomes: show ones with more collectibles first
+ const sortedBiomes = Array.from(biomeCollectibles.values())
+ .sort((a, b) => b.resources.length - a.resources.length)
+
+ return (
+
+
+ Resources
+
+ Resources are tile-bound goods that enrich yields, fuel happiness, and gate military units.
+ Every resource on the map belongs to one of three categories with different economic roles.
+
+
+
+
+
+ Resources are visible immediately (bonus) or revealed by technology (luxury and strategic).
+ Improving a tile with the required improvement β mine, farm, pasture, etc. β activates the resource
+ and adds its yields to the city working that tile.
+
+
+
+ {/* ββ Category overview ββ */}
+
+ Three categories
+
+ Each category has a distinct economic purpose. Bonus resources are the backbone of early-game
+ tile value. Luxury resources drive happiness and diplomacy. Strategic resources are prerequisites
+ for military units β no resource means no unit, full stop.
+
+
+
+
+ Bonus
+ Always visible. No tech required to see or improve. Provide food and production only.
+ Never tradeable and never generate happiness. The foundation of early city growth.
+ Count{byCategory.bonus.length} resources
+ HappinessNone (happiness_per_copy: 0)
+ TradeNot tradeable to other civs
+
+
+
+ Luxury
+ Tech-gated. Primary diplomatic trade currency. Each unique type owned adds happiness
+ per turn β owning duplicates adds no extra happiness but increases trade value to neighbors.
+ Count{byCategory.luxury.length} resources
+ Happiness+3 to +6 per unique type
+ Trade1 copy = happiness delivered to trading partner
+
+
+
+ Strategic
+ Tech-gated. Required to train specific military units. Without the resource, the unit
+ cannot be trained at all. Tradeable to supply allies. Strategic value is military, not comfort.
+ Count{byCategory.strategic.length} resources
+ HappinessNone β strategic value is military
+ TradeSupply allies who lack the resource
+
+
+
+
+ {/* ββ Quality scale ββ */}
+
+ Quality scale
+
+ Every deposit spawns at quality 1β5. Quality acts as a yield multiplier: a quality-3 Iron Ore
+ is the baseline all listed yield values are calibrated against. Map generation targets quality 3
+ as the median β quality 5 "mother lode" deposits make up roughly 5% of spawns and are worth
+ fighting over.
+
+ {qualityRows.length > 0 && (
+
+
+
Tier
+
Label
+
Multiplier
+
Design intent
+
+ {qualityRows.map(([tier, level]) => (
+
+
+
+ Q{tier}
+
+
{level.label}
+
{level.multiplier}Γ
+
+ {tier === '1' && 'Marginal vein β barely worth improvement investment'}
+ {tier === '2' && 'Workable seam β cities accept it but won\'t prioritize it'}
+ {tier === '3' && 'Standard β all listed yields calibrated at this level'}
+ {tier === '4' && 'Rich β notable enough to reroute borders and prioritize'}
+ {tier === '5' && 'Mother lode β rarest 5% of spawns, justifies conflict'}
+
+
+ ))}
+
+ )}
+
+
+ {/* ββ Luxury happiness ββ */}
+
+ Luxury happiness
+
+ Happiness from luxuries applies once per unique type β owning six copies of gold yields the same
+ happiness as owning one. Duplicates have value only as trade goods: sell or gift them to neighboring
+ clans to earn diplomatic goodwill or reduce their military budget.
+
+
+ The happiness-per-copy values across current luxury resources:
+
+
+ {byCategory.luxury
+ .filter(r => (r.happiness_per_unique_copy ?? 0) > 0)
+ .sort((a, b) => (b.happiness_per_unique_copy ?? 0) - (a.happiness_per_unique_copy ?? 0))
+ .map(r => (
+
+ {r.name}
+ +{r.happiness_per_unique_copy} happiness / unique copy
+ {r.revealed_by_tech && (
+ Requires: {r.revealed_by_tech.replace(/_/g, ' ')}
+ )}
+
+ ))}
+
+
+
+ {/* ββ Strategic gating ββ */}
+
+ Strategic gating
+
+ Strategic resources are hard prerequisites. The units listed below simply cannot be trained
+ if the required resource is not improved and connected to a city. Losing access β through
+ trade cancellation, city capture, or tile pillaging β immediately suspends production of
+ gated units until supply is restored.
+
+
+ {gatingSummary.map(r => (
+
+ {r.name}
+
+ {r.gates.map(u => (
+ {u.replace(/_/g, ' ')}
+ ))}
+
+
+ ))}
+
+
+
+ {/* ββ Biome collectibles ββ */}
+
+ Biome collectibles
+
+ Resources don't spawn uniformly. Each biome has a collectibles list β resources that
+ map generation can place there, each with a base quantity and quality range. The biome
+ you settle near determines which resources you can access early and what improvement
+ tech you need to exploit them.
+
+
+
+