feat(@projects/@magic-civilization): add game scope hygiene guide

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-18 00:15:27 -07:00
parent 00fbad0a42
commit b31cf586e4
10 changed files with 440 additions and 15 deletions

View file

@ -15,10 +15,10 @@
| Priority | ✅ | 🟡 | 🔴 | ❌ | ⚫ | Total |
|---|---|---|---|---|---|---|
| **P0** | 27 | 5 | 3 | 0 | 0 | 35 |
| **P1** | 13 | 3 | 2 | 0 | 1 | 19 |
| **P2** | 9 | 6 | 0 | 9 | 0 | 24 |
| **P1** | 13 | 3 | 3 | 1 | 1 | 21 |
| **P2** | 9 | 6 | 0 | 12 | 0 | 27 |
| **P3 (oos)** | 0 | 0 | 0 | 0 | 17 | 17 |
| **total** | **49** | **14** | **5** | **9** | **18** | **95** |
| **total** | **49** | **14** | **6** | **13** | **18** | **100** |
</td><td valign='top' style='padding-left:2em'>
@ -26,10 +26,10 @@
| Team Lead | Remaining |
|---|---|
| [tourguide](../team-leads/tourguide.md) | 7 |
| [asset-sprite](../team-leads/asset-sprite.md) | 7 |
| [warcouncil](../team-leads/warcouncil.md) | 6 |
| [wireguard](../team-leads/wireguard.md) | 4 |
| [tourguide](../team-leads/tourguide.md) | 3 |
| [wireguard](../team-leads/wireguard.md) | 5 |
| [shipwright](../team-leads/shipwright.md) | 2 |
| [testwright](../team-leads/testwright.md) | 2 |
| [asset-audio](../team-leads/asset-audio.md) | 1 |
@ -95,6 +95,7 @@
| [p1-12](p1-12-build-output-docs-alignment.md) | ✅ done | Align every doc reference to the relocated wasm-pack output | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
| [p1-13](p1-13-guide-dev-route-coverage.md) | ✅ done | Guide dev server boots on plum with zero-error route coverage | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
| [p1-15](p1-15-guide-next-deploy-infra.md) | ✅ done | Deploy dev guide to https://mc.next.black.local | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
| [p1-16](p1-16-guide-game1-scope-hygiene.md) | ❌ missing | Purge Game 2/3 scope bleed from user-visible Game 1 guide copy | [tourguide](../team-leads/tourguide.md) | 2026-04-18 |
| [p1-17](p1-17-guide-next-auto-deploy.md) | 🟡 partial | Forgejo workflow auto-deploys dev guide on push to main | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
| [p1-18](p1-18-village-discovery-feedback.md) | 🔴 stub | Village discovery — world-map feedback (notification, reward popup, minimap ping) | [wireguard](../team-leads/wireguard.md) | 2026-04-17 |
| [p1-19](p1-19-tutorial-opt-in.md) | 🔴 stub | Tutorial opt-in — HUD button, disappears after turn 5, starts from Step 1 | [wireguard](../team-leads/wireguard.md) | 2026-04-17 |
@ -128,6 +129,9 @@
| [p2-27](p2-27-city-population-tier-sprites.md) | ❌ missing | City population-tier sprites — city_q1 through city_q5 | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 |
| [p2-28](p2-28-sprite-provenance-ledger.md) | ❌ missing | Sprite provenance ledger — LICENSES.md per-file attribution | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 |
| [p2-29](p2-29-guide-welcome-homepage-theme-alignment.md) | ❌ missing | Welcome modal + HomePage lore + guide theme align to the player's chosen race/gender | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
| [p2-30](p2-30-guide-shared-primitives.md) | ❌ missing | Consolidate duplicate page styled-components into shared PagePrimitives | [tourguide](../team-leads/tourguide.md) | 2026-04-18 |
| [p2-31](p2-31-guide-url-bound-state.md) | ❌ missing | Migrate guide filter + tab state from useState to URL search params | [tourguide](../team-leads/tourguide.md) | 2026-04-18 |
| [p2-32](p2-32-guide-data-driven-enums.md) | ❌ missing | Replace hardcoded page enums with JSON data reads | [tourguide](../team-leads/tourguide.md) | 2026-04-18 |
## Out of Scope (Game 2 / Game 3)

View file

@ -0,0 +1,103 @@
---
id: p1-16
title: Purge Game 2/3 scope bleed from user-visible Game 1 guide copy
priority: p1
status: missing
scope: game1
owner: tourguide
updated_at: 2026-04-18
evidence:
- public/games/age-of-dwarves/guide/src/pages/HomePage.tsx
- public/games/age-of-dwarves/guide/src/pages/CommunicationsPage.tsx
- public/games/age-of-dwarves/guide/src/pages/PromotionsPage.tsx
- public/games/age-of-dwarves/guide/src/pages/survival-guide/data.ts
- public/games/age-of-dwarves/guide/src/pages/progress-report/OverviewTab.tsx
- public/games/age-of-dwarves/guide/e2e/scope-hygiene.spec.ts
---
## Summary
CLAUDE.md's Game 1 scope rule is clear: Age of Dwarves Early Access
ships with **no magic** — no magic schools, no Archons, no leylines,
no mana. The `<EpisodeGate min={2}>` component + `VITE_DEV_GUIDE=1`
dev-bundle flag (p1-15) are the enforcement mechanism. But a 2026-04-18
Explore sweep catalogued six user-visible surfaces in the default Game 1
build that still advertise or document Game 2/3 cosmology:
| File:Line | Failure |
|---|---|
| `HomePage.tsx:189190` (FEATURES) | "5 Magic Schools" card with "cross-school fusions, 10 hybrid disciplines" |
| `HomePage.tsx:246251` (Pitch) | "16 asymmetric races, 5 magic schools … pursue arcane power" |
| `HomePage.tsx:256275` (LoreSection) | Two paragraphs of mana-nodes + ley-lines + school-aligned energy |
| `CommunicationsPage.tsx:9798` | "Archon Telepathy (Magic civs)" row in mundane radio-tower rules |
| `PromotionsPage.tsx:5,1215,156193` | Magic-data imports + "Mana Infusions" section + "Dispellable by Aether" / "High Archon dies" body text |
| `SurvivalGuidePage` data.ts:85 | "Life T3 quarantine spell blocks adjacency transmission" in mundane survival scenario |
These are **RED** — visible to someone opening the staging / production
build without `VITE_DEV_GUIDE=1`. A player launching Early Access with
one dwarf race should not be told the game has "5 magic schools" or
read Archon mechanics in the communications tower rules.
YELLOW items (dev-bundle-only, flagged for opportunistic cleanup):
- `progress-report/OverviewTab.tsx:137,183` — hardcoded "5 trees,
30 policies" + "12 additional races" roadmap rows
- `GovernmentPage.tsx:48` (SKIP_MODS) — defensive filter for the
Game-2 `no_spell_pact_opposing_school` modifier
## Why P1 (not P2)
This is the inverse of p1-14 (which tried to ADD magic-school gating
work to Game 1 and was rightly OOS'd). Here the work is **removing**
Game 2/3 copy from Game 1 UI — exactly the spirit of p2-09's
scope-narrow pass, but at the copy level. Early Access cannot ship
with a homepage advertising mechanics that don't exist. Gated-behind-
EpisodeGate OR pure Dwarf rewrite both count as "done"; the win is
that `grep` stops finding magic terminology in default-build output.
## Acceptance
- **HomePage**`Hero` / `Pitch` / `FEATURES` / `LoreSection` no
longer mention `magic schools`, `Archons`, `mana`, `ley lines`,
`arcane`, or cross-race counts ("16 races") in the default build.
Either the prose is rewritten Dwarves-first (craft, industry,
fortress cities, subterranean engineering, steam) or the magic
paragraphs are wrapped in `<EpisodeGate min={2}>` so they reappear
cleanly in the `VITE_DEV_GUIDE=1` dev bundle.
- **CommunicationsPage** — the "Archon Telepathy (Magic civs)" rules
row is `<EpisodeGate min={2}>`-wrapped. The rest of the Radio Tower
section reads mundane (wireless infra only).
- **PromotionsPage**`disciplinesData` / `infusionTrees` imports
removed (they were empty stubs anyway). The "Mana Infusions" body
block (lines 156193) + its `Dispellable by Aether` /
`High Archon dies` text all wrap in `<EpisodeGate min={2}>`. The XP
+ promotion-tree body above is mundane and unchanged.
- **SurvivalGuidePage data.ts** — the Pandemic scenario's "Life T3
quarantine spell" mitigation replaced with a mundane equivalent
(e.g. "Quarantine House building" once that building lands, or
"isolate infected cities" as a pure game mechanic until then).
- **Scope-rule grep gate** — from the repo root, the command
`grep -RE "magic schools|High Archon|mana nodes|ley lines" public/games/age-of-dwarves/guide/src/ 2>/dev/null | grep -v 'EpisodeGate\|episode *>= *2\|VITE_DEV_GUIDE'`
returns **zero** matches. Mechanically-enforced scope integrity.
- **New e2e spec**`public/games/age-of-dwarves/guide/e2e/scope-hygiene.spec.ts`
walks the five key Game 1 routes (home, communications, promotions,
survival, combat) and asserts the rendered DOM contains none of the
forbidden terms. Included in the `pnpm test:e2e` default run.
## Non-goals
- Rewriting the WelcomeModal — that's p2-29.
- Converting the FEATURES array to a JSON data file — that's p2-32
(data-driven enums). p1-16 just gates / rewrites the magic cards;
the data-drive pass comes later.
- Fixing `GovernmentPage.tsx:48` SKIP_MODS defensive filter — YELLOW,
dev-bundle-only, addressed opportunistically.
- Shared-primitive consolidation on the same pages — that's p2-30.
Keep edits minimal and scope-only here.
## Why tourguide owns it
Mechanical scope enforcement on the dev-preview + staging deploy
(`mc.next.black.local`) is squarely in Tourguide's mandate. The grep
gate + `scope-hygiene.spec.ts` extend the Wave-1 e2e harness from
p1-13, the same substrate Tourguide authored.

View file

@ -0,0 +1,98 @@
---
id: p2-30
title: Consolidate duplicate page styled-components into shared PagePrimitives
priority: p2
status: missing
scope: game1
owner: tourguide
updated_at: 2026-04-18
evidence:
- src/packages/guide/src/components/ui/PagePrimitives.tsx
- public/games/age-of-dwarves/guide/src/pages/BiomeBrowserPage.tsx
- public/games/age-of-dwarves/guide/src/pages/SpeciesBrowserPage.tsx
- public/games/age-of-dwarves/guide/src/pages/MapTypesPage.tsx
- public/games/age-of-dwarves/guide/src/pages/ExpansionsPage.tsx
- public/games/age-of-dwarves/guide/src/pages/TeamPage.tsx
- public/games/age-of-dwarves/guide/src/pages/EpisodeDwarvesPage.tsx
---
## Summary
An Explore sweep on 2026-04-18 counted ~180 redundant styled-component
declarations across 15 guide pages. Each page declares its own
`Card`, `CardHeader`, `CardTitle`, `StatsGrid`, `Stat`, `Badge`,
`SectionLabel`, `Subtitle`, etc. that already exist (or could exist) in
the shared `src/packages/guide/src/components/ui/PagePrimitives.tsx`.
The duplication:
- Makes theme-token changes N-way rather than one-way (change the Dwarf
copper accent → audit 15 pages).
- Hides inconsistencies. `SpeciesBrowserPage`'s `Card` has 12 px
border-radius; `BiomeBrowserPage`'s has 10 px. Nobody notices until
screenshots diverge.
- Inflates each page's file size toward the 500-LOC cap, forcing
awkward splits (`BiomeBrowserPage.tsx` is already 355 LoC of styled
alone, ignoring JSX).
- Forecloses easy swap-to-Markdown + swap-to-data adoption because
each page has its own local idiom.
Highest-ROI candidates (counted by Explore):
- `BiomeBrowserPage.tsx:191355` — 23 custom styled, largest footprint.
- `SpeciesBrowserPage.tsx:236330` — 16 custom styled.
- `MapTypesPage.tsx:9103` — Name / StatsGrid / Stat / SectionLabel /
TopologyCard / TopologyBadge / TopologyDesc restate PagePrimitives.
- `ExpansionsPage.tsx:10132`, `TeamPage.tsx:630` — near-total overlap.
- `EpisodeDwarvesPage.tsx` — inline `INCLUDED_SYSTEMS` array + own
styled.
## Acceptance
- **Extend PagePrimitives** with the missing shared shapes:
- `<DataCard>` — the surface+border+radius+padding base used by
Species / Biome / MapTypes / Expansions. Takes an optional
`$variant` prop (`"base"` default, `"compact"`, `"topology"`).
- `<StatsGrid>` + `<StatCell>` — keyed label/value grid used in
MapTypes + Species + Biome.
- `<QualityIndicator>` — shared dot/range element used by Species
+ Biome for quality tiers.
- `<StackedBarChart>` — extracted from BiomeBrowserPage's FloraBar;
theme-token-driven (no hardcoded hex).
- **Per-page migration**, in order:
1. BiomeBrowserPage (biggest win). Delete duplicates; swap to shared.
2. SpeciesBrowserPage (closest cousin of Biome).
3. MapTypesPage.
4. ExpansionsPage + TeamPage (trivial two-liner migrations).
5. EpisodeDwarvesPage (also touches p2-32 data-drive work).
- **Metric gate** — from the guide directory,
`grep -RE "^const (Card|Badge|CardHeader|CardTitle|Stat|StatsGrid) = styled" src/pages/ | wc -l`
drops by ≥60% from the pre-pass baseline. Measure + record the
baseline in the closure entry.
- **No regressions**`pnpm typecheck` clean after each page
migration; `pnpm test:e2e --grep all-routes` stays 51/51 green;
MCP / curl screenshots of migrated pages match the pre-migration
visuals (no per-page theme drift).
- **Each page's `styled.ts` shrinks** to only genuinely page-specific
variants (e.g. a `<BiomeColorDot>` that only the biome page ever
uses stays local).
## Non-goals
- Migrating the non-listed pages (KeywordsPage, ItemsPage, etc.) in
this objective. They get swept when someone else touches them; the
goal here is to establish the shared primitives + migrate the
highest-duplication offenders, not a total rewrite.
- Changing visual design — colors / padding / border-radius of the
new shared primitives should match the existing `SpeciesBrowserPage`
values as the canonical reference (any visible visual change is a
regression).
- FloraBar color tokens — promoting the hardcoded `#1a9928` /
`#8cc634` / `#9040a0` to theme palette entries is a prerequisite
the shared `<StackedBarChart>` assumes; the palette additions land
in the same commit as the primitive.
## Dependency
- p2-29 (welcome/theme alignment) and p1-16 (scope hygiene) edit
HomePage + some of the same pages. Sequence after both of those
close or rebase on top cleanly. Prefer after.

View file

@ -0,0 +1,76 @@
---
id: p2-31
title: Migrate guide filter + tab state from useState to URL search params
priority: p2
status: missing
scope: game1
owner: tourguide
updated_at: 2026-04-18
evidence:
- public/games/age-of-dwarves/guide/src/pages/SpeciesBrowserPage.tsx
- public/games/age-of-dwarves/guide/src/pages/BiomeBrowserPage.tsx
- public/games/age-of-dwarves/guide/src/pages/ClimateEventsPage.tsx
- public/games/age-of-dwarves/guide/src/pages/progress-report/ObjectivesTab.tsx
- public/games/age-of-dwarves/guide/src/pages/ProgressReportPage.tsx
---
## Summary
The Progress Report's Details tab set a precedent: its filter chip
state + per-objective modal round-trip through `useSearchParams()`
(see `progress-report/ObjectivesTab.tsx` + `ProgressReportPage.tsx`).
Bookmarking `?tab=details&filter=partial&objective=p0-01` restores the
exact view.
Three other browsable pages still keep their filter / tab state in
`useState`, so deep links don't work and users can't share a filtered
view by URL:
- `SpeciesBrowserPage.tsx:57` — role / biome / quality filters
- `BiomeBrowserPage.tsx:121` — category filter + potentially a
highlighted biome
- `ClimateEventsPage.tsx` — category tabs
## Acceptance
- **Shared pattern** — copy (or better: extract as a helper hook) the
`ObjectivesTab.tsx` pattern:
- `const [searchParams, setSearchParams] = useSearchParams()`
- `const filter = coerceFilter(searchParams.get('filter'))`
- `setFilter(next)` updates `searchParams` and writes back
(`replace: true` for filter changes — don't pollute history).
- A small `coerceFilter` narrowing that whitelists known values
and falls back to the default on unknown input.
- **Per-page migration**:
- `SpeciesBrowserPage``?role=…&biome=…&quality=…&species=<id>`.
Clicking a species opens a modal (or inline detail) bound to the
`species` param.
- `BiomeBrowserPage``?category=…&biome=<id>` with an inline or
modal highlight.
- `ClimateEventsPage``?category=<tab>` for the tabs.
- **Back button** — closing a modal or navigating between tabs
registers as a history entry; Back restores the previous view.
- **Deep-link smoke** — a new e2e spec `shareable-urls.spec.ts` hits
five representative URLs (e.g. `/climate/ecosystem/fauna?role=grazer`,
`/climate/ecosystem/biomes?category=aquatic&biome=shallow-coast`,
`/climate/weather?category=pandemic`) and asserts the filter state
is visibly applied in the first render.
- **No regressions** — route-coverage e2e stays 51/51 green (the
new URL shapes don't break the default no-param render).
## Reusable helper
Extract a small `useUrlFilter<T extends string>(key: string, values: readonly T[], fallback: T)` hook in `public/games/age-of-dwarves/guide/src/hooks/useUrlFilter.ts` so each page isn't copy-pasting the same 15 lines. Three concrete call sites qualify: Species, Biome, ClimateEvents.
## Non-goals
- Migrating welcome-modal preference state — that's a long-lived
cross-session concern handled by `usePlayerPreferences` + localStorage;
URL params are orthogonal.
- Porting OBJECTIVES tab patterns to ClimateSimulation (its URL state
is already custom and tuned for the sim worker — leave it alone).
## Dependency
Cleanest after p2-30 (shared primitives) since some of the same pages
are touched, but not blocking. Can land independently.

View file

@ -0,0 +1,75 @@
---
id: p2-32
title: Replace hardcoded page enums with JSON data reads
priority: p2
status: missing
scope: game1
owner: tourguide
updated_at: 2026-04-18
evidence:
- public/games/age-of-dwarves/guide/src/pages/MapTypesPage.tsx
- public/games/age-of-dwarves/guide/src/pages/EpisodeDwarvesPage.tsx
- public/games/age-of-dwarves/guide/src/pages/HomePage.tsx
- public/games/age-of-dwarves/guide/src/pages/progress-report/OverviewTab.tsx
- public/games/age-of-dwarves/data/map-topologies.json
- public/games/age-of-dwarves/data/episodes/ep1-systems.json
- public/games/age-of-dwarves/data/homepage-features.json
---
## Summary
Rail #2 in CLAUDE.md says "JSON game packs are the canonical content
store — neither Rust nor GDScript hardcodes game content." Several
guide pages still violate the spirit of that rule by hand-typing
data arrays in `.tsx` files. When the design changes ("we decided
it's 20 races, not 16" — or "Arcana tree got merged into Scholarship"),
the fix has to hop four files in TypeScript rather than editing one JSON.
Hardcoded arrays the Explore sweep found:
| File:Line | Array | Target JSON |
|---|---|---|
| `MapTypesPage.tsx:105124` | `TOPOLOGY_MODES` — 3 topologies with `{id, label, desc, math, isDefault}` | `public/games/age-of-dwarves/data/map-topologies.json` (new) |
| `EpisodeDwarvesPage.tsx:820` | `INCLUDED_SYSTEMS` — 10 system-name strings | `public/games/age-of-dwarves/data/episodes/ep1-systems.json` (new) |
| `HomePage.tsx:180210` (FEATURES) | "What makes this game different" cards | `public/games/age-of-dwarves/data/homepage-features.json` (new) — Episode 1 cards first, Episode 2+ behind an EpisodeGate at render time |
| `progress-report/OverviewTab.tsx:114165` | Hand-typed "Coming in v1.0.0" + "After Full Release" roadmap tables | `public/games/age-of-dwarves/data/shipping-roadmap.json` (new) |
## Acceptance
- **New JSON files** — each created at the path above with the exact
field shape the current page consumes (no data migration on the
reader side beyond swapping `const X = [...]` to
`import X from '@data/...'`).
- **Consistent loader pattern** — single-file reads go through the
`@data/` alias that already exists in `vite.config.ts`. No new
`import.meta.glob` patterns unless a directory layout is warranted
(episode-systems maybe, since there will be ep2+ siblings).
- **HomePage FEATURES** — each card carries an optional `min_episode`
field (default 1). The page filters `feature.min_episode <= activeEpisode`
so the Episode 2+ cards (if any) appear in dev bundle only. Source
of truth: the JSON, not the gate.
- **OverviewTab roadmap tables** — same pattern; each row carries a
priority + labels, rendered from the JSON.
- **No regressions**`pnpm typecheck` clean, e2e 51/51 green,
visual parity with the pre-pass rendering.
- **JSON schema validation** — the game-data schema validator
(`tools/validate-game-data.py`) gains entries for the new files so
`./run verify` step 0 catches schema drift.
## Non-goals
- Converting other hand-typed arrays that were not in the Explore
sweep (e.g. `promotions-data` is already partly JSON-sourced; other
pages may have inline arrays of <4 items that don't warrant
migration).
- Adding a general-purpose `useGameData<T>(path)` abstraction —
overkill for 4 new reads. Each page imports its JSON directly via
`@data/` alias.
- Game 2+ episode-systems files — those land when the Kzzykt guide
shell does.
## Dependency
Best after p1-16 (scope hygiene) so the homepage-features JSON can
be authored knowing which cards should ship Game 1-only vs gated.
Independent of p2-30 and p2-31.

View file

@ -7,10 +7,14 @@ objectives:
- p1-12
- p1-13
- p1-15
- p1-16
- p1-17
- p2-20
- p2-21
- p2-29
- p2-30
- p2-31
- p2-32
---
## Mandate

View file

@ -1,12 +1,12 @@
{
"generated_at": "2026-04-18T07:05:04Z",
"generated_at": "2026-04-18T07:13:06Z",
"totals": {
"stub": 5,
"done": 49,
"oos": 18,
"missing": 13,
"partial": 14,
"missing": 9,
"total": 95
"done": 49,
"stub": 6,
"oos": 18,
"total": 100
},
"objectives": [
{
@ -519,6 +519,16 @@
"updated_at": "2026-04-17",
"summary": ""
},
{
"id": "p1-16",
"title": "Purge Game 2/3 scope bleed from user-visible Game 1 guide copy",
"priority": "p1",
"status": "missing",
"scope": "game1",
"owner": "tourguide",
"updated_at": "2026-04-18",
"summary": "CLAUDE.md's Game 1 scope rule is clear: Age of Dwarves Early Access\nships with **no magic** — no magic schools, no Archons, no leylines,\nno mana. The `<EpisodeGate min={2}>` component + `VITE_DEV_GUIDE=1`\ndev-bundle flag (p1-15) are the enforcement mechanism. But a 2026-04-18\nExplore sweep catalogued six user-visible surfaces in the default Game 1\nbuild that still advertise or document Game 2/3 cosmology:\n\n| File:Line | Failure |\n|---|---|\n| `HomePage.tsx:189190` (FEATURES) | \"5 Magic Schools\" card with \"cross-school fusions, 10 hybrid disciplines\" |\n| `HomePage.tsx:246251` (Pitch) | \"16 asymmetric races, 5 magic schools … pursue arcane power\" |\n| `HomePage.tsx:256275` (LoreSection) | Two paragraphs of mana-nodes + ley-lines + school-aligned energy |\n| `CommunicationsPage.tsx:9798` | \"Archon Telepathy (Magic civs)\" row in mundane radio-tower rules |\n| `PromotionsPage.tsx:5,1215,156193` | Magic-data imports + \"Mana Infusions\" section + \"Dispellable by Aether\" / \"High Archon dies\" body text |\n| `SurvivalGuidePage` data.ts:85 | \"Life T3 quarantine spell blocks adjacency transmission\" in mundane survival scenario |\n\nThese are **RED** — visible to someone opening the staging / production\nbuild without `VITE_DEV_GUIDE=1`. A player launching Early Access with\none dwarf race should not be told the game has \"5 magic schools\" or\nread Archon mechanics in the communications tower rules.\n\nYELLOW items (dev-bundle-only, flagged for opportunistic cleanup):\n\n- `progress-report/OverviewTab.tsx:137,183` — hardcoded \"5 trees,\n 30 policies\" + \"12 additional races\" roadmap rows\n- `GovernmentPage.tsx:48` (SKIP_MODS) — defensive filter for the\n Game-2 `no_spell_pact_opposing_school` modifier"
},
{
"id": "p1-17",
"title": "Forgejo workflow auto-deploys dev guide on push to main",
@ -549,6 +559,16 @@
"updated_at": "2026-04-17",
"summary": "The first-run tutorial currently auto-shows on game start (gated by\n`TutorialOverlay.should_show_on_first_run()`). This is hostile to returning\nplayers and to playtesting — the tutorial interrupts real gameplay every fresh\nboot. Assume the player doesn't need a tutorial by default. Offer it as a\nbutton on the world-map HUD that disappears after turn 5.\n\nAdditionally, when the tutorial IS started, it begins at **Step 1** (camera\npan), not Step 2. This is already the authoritative step order in\n`tutorial_overlay.gd:_STEPS` — the fix is only to make sure `_current_step`\ninitializes to `1` (it does) and that no code skips ahead."
},
{
"id": "p1-20",
"title": "Unit patrol orders — assign a unit to loop between waypoint tiles",
"priority": "p1",
"status": "stub",
"scope": "game1",
"owner": "wireguard",
"updated_at": "2026-04-18",
"summary": "Both the human player and the AI clans need a *standing order* that keeps a\nunit moving along a fixed route turn after turn without per-turn micro-management.\nCanonical use cases: escorting a worker loop, covering a chokepoint, sweeping\nscout fog between two outposts.\n\nToday a unit has two durable states: idle-on-tile, or fortified (via\n`unit.gd:250`). `Skip` ends the turn but does not persist. A player who wants\na scout to pace between two tiles must hand-move it every single turn — which\nbreaks down entirely once the empire has more than a few units, and which the\nAI cannot express at all because `mc-ai/tactical/movement.rs` re-plans from\nscratch each turn.\n\nThis objective adds a third durable state — **patrol** — with a small\nwaypoint list and a direction cursor. While patrolling, the unit auto-advances\nalong its route during the turn processor before the player's input phase, so\nturn N+1 opens with the unit already at the next step on its loop."
},
{
"id": "p2-01",
"title": "Minimap — fog reflection and unit markers",
@ -789,6 +809,36 @@
"updated_at": "2026-04-17",
"summary": "The guide already exposes a welcome modal that lets the player pick a race\n(Dwarf in Game 1 — `CONCRETE_RACES = ['dwarf']`) and a gender, plus a\n`RaceThemeProvider` that merges a per-race/per-gender palette into the\nstyled-components theme. But the three surfaces don't line up:\n\n- **WelcomeModal copy** reads like a settings dialog (\"Settings\", field\n labels) rather than an invitation into the story, so the player's first\n impression is admin-UI-shaped.\n- **HomePage `<LoreSection>`** *does* pull the player's race + name via\n `usePreferences()`, but the surrounding `<Hero>` / `<Tagline>` / `<Pitch>`\n hardcode out-of-scope narrative (\"16 asymmetric races, 5 magic schools\")\n that is Game 2/3 territory and does NOT update when the player picks a\n dwarf leader.\n- **Theme application** — the palette change from `RaceThemeProvider` fires\n on confirm, but a browser already on the HomePage may not re-derive the\n Hero/Pitch colors in a visually coherent way (Cinzel serif, Dwarf copper\n `#c07040`, etc.). The \"align with welcome\" contract is not exercised.\n\nWhen the player picks **Dwarf + Female** in the modal and clicks Begin,\nall three surfaces should read as one piece: a dwarf-themed guide,\nreferring to the named dwarf leader, in Dwarf scope language (no \"5\nmagic schools\" pitch, no generic cross-race framing)."
},
{
"id": "p2-30",
"title": "Consolidate duplicate page styled-components into shared PagePrimitives",
"priority": "p2",
"status": "missing",
"scope": "game1",
"owner": "tourguide",
"updated_at": "2026-04-18",
"summary": "An Explore sweep on 2026-04-18 counted ~180 redundant styled-component\ndeclarations across 15 guide pages. Each page declares its own\n`Card`, `CardHeader`, `CardTitle`, `StatsGrid`, `Stat`, `Badge`,\n`SectionLabel`, `Subtitle`, etc. that already exist (or could exist) in\nthe shared `src/packages/guide/src/components/ui/PagePrimitives.tsx`.\nThe duplication:\n\n- Makes theme-token changes N-way rather than one-way (change the Dwarf\n copper accent → audit 15 pages).\n- Hides inconsistencies. `SpeciesBrowserPage`'s `Card` has 12 px\n border-radius; `BiomeBrowserPage`'s has 10 px. Nobody notices until\n screenshots diverge.\n- Inflates each page's file size toward the 500-LOC cap, forcing\n awkward splits (`BiomeBrowserPage.tsx` is already 355 LoC of styled\n alone, ignoring JSX).\n- Forecloses easy swap-to-Markdown + swap-to-data adoption because\n each page has its own local idiom.\n\nHighest-ROI candidates (counted by Explore):\n\n- `BiomeBrowserPage.tsx:191355` — 23 custom styled, largest footprint.\n- `SpeciesBrowserPage.tsx:236330` — 16 custom styled.\n- `MapTypesPage.tsx:9103` — Name / StatsGrid / Stat / SectionLabel /\n TopologyCard / TopologyBadge / TopologyDesc restate PagePrimitives.\n- `ExpansionsPage.tsx:10132`, `TeamPage.tsx:630` — near-total overlap.\n- `EpisodeDwarvesPage.tsx` — inline `INCLUDED_SYSTEMS` array + own\n styled."
},
{
"id": "p2-31",
"title": "Migrate guide filter + tab state from useState to URL search params",
"priority": "p2",
"status": "missing",
"scope": "game1",
"owner": "tourguide",
"updated_at": "2026-04-18",
"summary": "The Progress Report's Details tab set a precedent: its filter chip\nstate + per-objective modal round-trip through `useSearchParams()`\n(see `progress-report/ObjectivesTab.tsx` + `ProgressReportPage.tsx`).\nBookmarking `?tab=details&filter=partial&objective=p0-01` restores the\nexact view.\n\nThree other browsable pages still keep their filter / tab state in\n`useState`, so deep links don't work and users can't share a filtered\nview by URL:\n\n- `SpeciesBrowserPage.tsx:57` — role / biome / quality filters\n- `BiomeBrowserPage.tsx:121` — category filter + potentially a\n highlighted biome\n- `ClimateEventsPage.tsx` — category tabs"
},
{
"id": "p2-32",
"title": "Replace hardcoded page enums with JSON data reads",
"priority": "p2",
"status": "missing",
"scope": "game1",
"owner": "tourguide",
"updated_at": "2026-04-18",
"summary": "Rail #2 in CLAUDE.md says \"JSON game packs are the canonical content\nstore — neither Rust nor GDScript hardcodes game content.\" Several\nguide pages still violate the spirit of that rule by hand-typing\ndata arrays in `.tsx` files. When the design changes (\"we decided\nit's 20 races, not 16\" — or \"Arcana tree got merged into Scholarship\"),\nthe fix has to hop four files in TypeScript rather than editing one JSON.\n\nHardcoded arrays the Explore sweep found:\n\n| File:Line | Array | Target JSON |\n|---|---|---|\n| `MapTypesPage.tsx:105124` | `TOPOLOGY_MODES` — 3 topologies with `{id, label, desc, math, isDefault}` | `public/games/age-of-dwarves/data/map-topologies.json` (new) |\n| `EpisodeDwarvesPage.tsx:820` | `INCLUDED_SYSTEMS` — 10 system-name strings | `public/games/age-of-dwarves/data/episodes/ep1-systems.json` (new) |\n| `HomePage.tsx:180210` (FEATURES) | \"What makes this game different\" cards | `public/games/age-of-dwarves/data/homepage-features.json` (new) — Episode 1 cards first, Episode 2+ behind an EpisodeGate at render time |\n| `progress-report/OverviewTab.tsx:114165` | Hand-typed \"Coming in v1.0.0\" + \"After Full Release\" roadmap tables | `public/games/age-of-dwarves/data/shipping-roadmap.json` (new) |"
},
{
"id": "g2-01",
"title": "Ley lines — Game 2 (Age of Kzzykt)",

View file

@ -261,6 +261,12 @@ static func _player_to_dict(p: RefCounted) -> Dictionary:
"hp_max": maxi(1, int(u.max_hp)),
"moves_left": maxi(0, int(u.movement_remaining)),
"fortified": bool(u.is_fortified),
# Data-driven founder flag — clan-themed units like
# "dwarf_tribe" aren't recognized by string match alone,
# so we pass the engine's already-computed boolean
# through to the Rust port. Matches the settle.rs
# `is_settler()` fallback.
"can_found_city": bool(u.get("can_found_city") == true),
})
ui += 1
var cities: Array = []

View file

@ -104,11 +104,13 @@ pub(crate) fn decide_settle(
actions
}
/// A settler is any unit whose `kind` encodes a founder role. Age-of-Dwarves
/// uses `"settler"`; `"founder"` is accepted as a legacy alias so the port
/// doesn't depend on a particular data-pack version shipping the rename.
/// A settler is any unit flagged `can_found_city` by the engine's data pack.
/// Prefer the data-driven flag over string matching on `kind` — clan-themed
/// founders like `"dwarf_tribe"` wouldn't match a hardcoded id set.
/// Falls back to string match on legacy fixtures (where `can_found_city` is
/// default-false) so existing tests continue to pass.
fn is_settler(unit: &TacticalUnit) -> bool {
matches!(unit.kind.as_str(), "settler" | "founder")
unit.can_found_city || matches!(unit.kind.as_str(), "settler" | "founder")
}
/// Return the action for a single settler, or `None` if the settler cannot

View file

@ -105,6 +105,13 @@ pub struct TacticalUnit {
pub moves_left: u32,
/// Fortify-in-place flag.
pub fortified: bool,
/// True when this unit can found a city — data-driven from the engine's
/// `unit.can_found_city` flag. NEVER match on `kind` string alone because
/// clan-themed founder units (e.g. `"dwarf_tribe"`) DO NOT literally spell
/// "settler" or "founder". Default `false` for serde back-compat with
/// fixtures that predate this field.
#[serde(default)]
pub can_found_city: bool,
}
/// A city.