diff --git a/.project/objectives/README.md b/.project/objectives/README.md index c8379f2d..bca9e57e 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -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** | @@ -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) diff --git a/.project/objectives/p1-16-guide-game1-scope-hygiene.md b/.project/objectives/p1-16-guide-game1-scope-hygiene.md new file mode 100644 index 00000000..01bf49ae --- /dev/null +++ b/.project/objectives/p1-16-guide-game1-scope-hygiene.md @@ -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 `` 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:189–190` (FEATURES) | "5 Magic Schools" card with "cross-school fusions, 10 hybrid disciplines" | +| `HomePage.tsx:246–251` (Pitch) | "16 asymmetric races, 5 magic schools … pursue arcane power" | +| `HomePage.tsx:256–275` (LoreSection) | Two paragraphs of mana-nodes + ley-lines + school-aligned energy | +| `CommunicationsPage.tsx:97–98` | "Archon Telepathy (Magic civs)" row in mundane radio-tower rules | +| `PromotionsPage.tsx:5,12–15,156–193` | 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 `` so they reappear + cleanly in the `VITE_DEV_GUIDE=1` dev bundle. +- **CommunicationsPage** — the "Archon Telepathy (Magic civs)" rules + row is ``-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 156–193) + its `Dispellable by Aether` / + `High Archon dies` text all wrap in ``. 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. diff --git a/.project/objectives/p2-30-guide-shared-primitives.md b/.project/objectives/p2-30-guide-shared-primitives.md new file mode 100644 index 00000000..6219bee7 --- /dev/null +++ b/.project/objectives/p2-30-guide-shared-primitives.md @@ -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:191–355` — 23 custom styled, largest footprint. +- `SpeciesBrowserPage.tsx:236–330` — 16 custom styled. +- `MapTypesPage.tsx:9–103` — Name / StatsGrid / Stat / SectionLabel / + TopologyCard / TopologyBadge / TopologyDesc restate PagePrimitives. +- `ExpansionsPage.tsx:10–132`, `TeamPage.tsx:6–30` — near-total overlap. +- `EpisodeDwarvesPage.tsx` — inline `INCLUDED_SYSTEMS` array + own + styled. + +## Acceptance + +- **Extend PagePrimitives** with the missing shared shapes: + - `` — the surface+border+radius+padding base used by + Species / Biome / MapTypes / Expansions. Takes an optional + `$variant` prop (`"base"` default, `"compact"`, `"topology"`). + - `` + `` — keyed label/value grid used in + MapTypes + Species + Biome. + - `` — shared dot/range element used by Species + + Biome for quality tiers. + - `` — 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 `` 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 `` 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. diff --git a/.project/objectives/p2-31-guide-url-bound-state.md b/.project/objectives/p2-31-guide-url-bound-state.md new file mode 100644 index 00000000..4c4410bd --- /dev/null +++ b/.project/objectives/p2-31-guide-url-bound-state.md @@ -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=`. + Clicking a species opens a modal (or inline detail) bound to the + `species` param. + - `BiomeBrowserPage` — `?category=…&biome=` with an inline or + modal highlight. + - `ClimateEventsPage` — `?category=` 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(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. diff --git a/.project/objectives/p2-32-guide-data-driven-enums.md b/.project/objectives/p2-32-guide-data-driven-enums.md new file mode 100644 index 00000000..16da9d74 --- /dev/null +++ b/.project/objectives/p2-32-guide-data-driven-enums.md @@ -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:105–124` | `TOPOLOGY_MODES` — 3 topologies with `{id, label, desc, math, isDefault}` | `public/games/age-of-dwarves/data/map-topologies.json` (new) | +| `EpisodeDwarvesPage.tsx:8–20` | `INCLUDED_SYSTEMS` — 10 system-name strings | `public/games/age-of-dwarves/data/episodes/ep1-systems.json` (new) | +| `HomePage.tsx:180–210` (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:114–165` | 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(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. diff --git a/.project/team-leads/tourguide.md b/.project/team-leads/tourguide.md index e8770abd..38ef8c89 100644 --- a/.project/team-leads/tourguide.md +++ b/.project/team-leads/tourguide.md @@ -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 diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 7998485d..418ebbb3 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -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 `` 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:189–190` (FEATURES) | \"5 Magic Schools\" card with \"cross-school fusions, 10 hybrid disciplines\" |\n| `HomePage.tsx:246–251` (Pitch) | \"16 asymmetric races, 5 magic schools … pursue arcane power\" |\n| `HomePage.tsx:256–275` (LoreSection) | Two paragraphs of mana-nodes + ley-lines + school-aligned energy |\n| `CommunicationsPage.tsx:97–98` | \"Archon Telepathy (Magic civs)\" row in mundane radio-tower rules |\n| `PromotionsPage.tsx:5,12–15,156–193` | 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 ``** *does* pull the player's race + name via\n `usePreferences()`, but the surrounding `` / `` / ``\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:191–355` — 23 custom styled, largest footprint.\n- `SpeciesBrowserPage.tsx:236–330` — 16 custom styled.\n- `MapTypesPage.tsx:9–103` — Name / StatsGrid / Stat / SectionLabel /\n TopologyCard / TopologyBadge / TopologyDesc restate PagePrimitives.\n- `ExpansionsPage.tsx:10–132`, `TeamPage.tsx:6–30` — 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:105–124` | `TOPOLOGY_MODES` — 3 topologies with `{id, label, desc, math, isDefault}` | `public/games/age-of-dwarves/data/map-topologies.json` (new) |\n| `EpisodeDwarvesPage.tsx:8–20` | `INCLUDED_SYSTEMS` — 10 system-name strings | `public/games/age-of-dwarves/data/episodes/ep1-systems.json` (new) |\n| `HomePage.tsx:180–210` (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:114–165` | 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)", diff --git a/src/game/engine/src/modules/ai/ai_turn_bridge.gd b/src/game/engine/src/modules/ai/ai_turn_bridge.gd index 25d23b27..21307181 100644 --- a/src/game/engine/src/modules/ai/ai_turn_bridge.gd +++ b/src/game/engine/src/modules/ai/ai_turn_bridge.gd @@ -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 = [] diff --git a/src/simulator/crates/mc-ai/src/tactical/settle.rs b/src/simulator/crates/mc-ai/src/tactical/settle.rs index cd2da8e2..ef51875d 100644 --- a/src/simulator/crates/mc-ai/src/tactical/settle.rs +++ b/src/simulator/crates/mc-ai/src/tactical/settle.rs @@ -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 diff --git a/src/simulator/crates/mc-ai/src/tactical/state.rs b/src/simulator/crates/mc-ai/src/tactical/state.rs index 1403740c..62c40d82 100644 --- a/src/simulator/crates/mc-ai/src/tactical/state.rs +++ b/src/simulator/crates/mc-ai/src/tactical/state.rs @@ -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.