diff --git a/public/games/age-of-dwarves/guide/src/nav.tsx b/public/games/age-of-dwarves/guide/src/nav.tsx index ef4df37b..4717602b 100644 --- a/public/games/age-of-dwarves/guide/src/nav.tsx +++ b/public/games/age-of-dwarves/guide/src/nav.tsx @@ -1,14 +1,8 @@ import type { NavGroup } from '@magic-civ/guide-engine' import episodes from '@resources/episodes.json' +import { EPISODE_COLORS } from '@magic-civ/guide-engine' -// Episode accent colors — app navigation branding (UI-only, stays in TypeScript) -const EP1_COLOR = '#c07040' // dwarf copper -const EP2_COLOR = '#4a7c59' // kzzykt green -const EP3_COLOR = '#7c5cbf' // elven violet -const EP4_COLOR = '#3a6ea8' // terran blue (psionics) -const EP5_COLOR = '#9b6b9b' // ethereal violet - -const [ep1, ep2, ep3, ep4, ep5] = episodes +const [ep1] = episodes export const NAV: NavGroup[] = [ // ─── Common (cross-episode) ─────────────────────────────────────────────── @@ -34,7 +28,7 @@ export const NAV: NavGroup[] = [ // ─── Episode 1: Age of Dwarves — Khazad Prime ──────────────────────────── { - episodeHeader: { number: 1, name: ep1.display_name, color: EP1_COLOR, link: ep1.route! }, + episodeHeader: { number: 1, name: ep1.display_name, color: EPISODE_COLORS.ep1, link: ep1.route! }, title: 'Craft & Khazad Prime', items: [ { to: ep1.route!, icon: '📋', label: ep1.name }, @@ -117,89 +111,4 @@ export const NAV: NavGroup[] = [ { to: '/playing/lenses', icon: '🔍', label: 'Lenses' }, ], }, - - // ─── Episode 2: Age of Kzzykt — The Hive ──────────────────────────────── - { - episodeHeader: { number: 2, name: ep2.display_name, color: EP2_COLOR, link: ep2.route! }, - title: 'Kzzykt & The Hive', - items: [ - { to: ep2.route!, icon: '📋', label: ep2.name }, - { to: '/worlds/the-hive', icon: '🐛', label: 'The Hive' }, - ], - }, - { - title: 'Ley Lines & Magic', - items: [ - { to: '/magic', icon: '🌿', label: 'Magic Overview' }, - { to: '/magic/ley-lines', icon: '〰', label: 'Ley Lines' }, - ], - }, - - // ─── Episode 3: Age of Elves — Silvandel ───────────────────────────────── - { - episodeHeader: { number: 3, name: ep3.display_name, color: EP3_COLOR, link: ep3.route! }, - title: 'Elves & Silvandel', - items: [ - { to: ep3.route!, icon: '📋', label: ep3.name }, - { to: '/worlds/silvandel', icon: '🌲', label: 'Silvandel' }, - ], - }, - { - title: 'Schools of Magic', - items: [ - { to: '/magic/schools', icon: '📚', label: 'All Schools' }, - { to: '/magic/schools/life', icon: '💚', label: 'Life' }, - { to: '/magic/schools/death', icon: '💀', label: 'Death' }, - { to: '/magic/schools/chaos', icon: '🔥', label: 'Chaos' }, - { to: '/magic/schools/aether', icon: '✨', label: 'Aether' }, - ], - }, - { - title: 'Archons & Ascension', - items: [ - { to: '/magic/archons', icon: '👁', label: 'Archons' }, - { to: '/magic/ascension', icon: '🌟', label: 'Arcane Ascension' }, - ], - }, - - // ─── Episode 4: Age of Terrans — Terra ─────────────────────────────────── - { - episodeHeader: { number: 4, name: ep4.display_name, color: EP4_COLOR, link: ep4.route! }, - title: 'Terrans & Terra', - items: [ - { to: ep4.route!, icon: '📋', label: ep4.name }, - { to: '/worlds/terra', icon: '🌍', label: 'Terra' }, - ], - }, - { - title: 'Psionics', - items: [ - { to: '/psionics', icon: '🧠', label: 'Psionics Overview' }, - ], - }, - { - title: 'Religion', - items: [ - { to: '/religion', icon: '⛪', label: 'Religious Victory' }, - ], - }, - - // ─── Episode 5: Age of Ascension — Ethereal Plane ──────────────────────── - { - episodeHeader: { number: 5, name: ep5.display_name, color: EP5_COLOR, link: ep5.route! }, - title: 'The Ethereal Plane', - items: [ - { to: ep5.route!, icon: '📋', label: ep5.name }, - { to: '/worlds/ethereal-plane', icon: '🌌', label: 'Ethereal Plane' }, - ], - }, - { - title: 'Ethereal Species', - items: [ - { to: '/species/phantasma', icon: '👻', label: 'Phantasma' }, - { to: '/species/flugel', icon: '🪽', label: 'Flügel' }, - { to: '/species/gith', icon: '⚔', label: 'Gith' }, - { to: '/species/demonia', icon: '😈', label: 'Demonia' }, - ], - }, ] diff --git a/public/games/age-of-dwarves/guide/src/pages/ProgressReportPage.tsx b/public/games/age-of-dwarves/guide/src/pages/ProgressReportPage.tsx index 9606850a..10982c79 100644 --- a/public/games/age-of-dwarves/guide/src/pages/ProgressReportPage.tsx +++ b/public/games/age-of-dwarves/guide/src/pages/ProgressReportPage.tsx @@ -66,6 +66,7 @@ const PRIORITY_LABEL: Record = { p0: 'P0 — Blockers for completely playable', p1: 'P1 — Ship readiness', p2: 'P2 — Polish', + p3: 'P3 — Future-game scope', } // ─── Asset-presence detection via Vite glob ───────────────────────────────── @@ -126,7 +127,7 @@ export default function ProgressReportPage(): ReactElement { [data.objectives], ) const deferred = useMemo( - () => data.objectives.filter((o) => o.scope === 'game2'), + () => data.objectives.filter((o) => o.scope !== 'game1'), [data.objectives], ) @@ -230,11 +231,12 @@ export default function ProgressReportPage(): ReactElement { {deferred.length > 0 && ( - Deferred to Game 2 — Age of Kzzykt ({deferred.length}) + Deferred to future games ({deferred.length}) - These objectives are explicitly future-scope. They are not part of - the Age of Dwarves Early Access release. + These objectives are explicitly future-scope (Age of Kzzykt and + later episodes). They are not part of the Age of Dwarves Early + Access release. diff --git a/public/games/age-of-dwarves/guide/src/pages/progress-report/PriorityModal.tsx b/public/games/age-of-dwarves/guide/src/pages/progress-report/PriorityModal.tsx index 5dba9403..c00efc0b 100644 --- a/public/games/age-of-dwarves/guide/src/pages/progress-report/PriorityModal.tsx +++ b/public/games/age-of-dwarves/guide/src/pages/progress-report/PriorityModal.tsx @@ -33,6 +33,7 @@ const PRIORITY_LABEL: Record = { p0: 'P0 — Blockers for completely playable', p1: 'P1 — Ship readiness', p2: 'P2 — Polish', + p3: 'P3 — Future-game scope', } const BREAKDOWN_STATUSES: readonly ObjectiveStatus[] = [ diff --git a/public/games/age-of-dwarves/guide/src/pages/progress-report/__tests__/objectives-json.test.ts b/public/games/age-of-dwarves/guide/src/pages/progress-report/__tests__/objectives-json.test.ts index 0ea5c0d5..64a93171 100644 --- a/public/games/age-of-dwarves/guide/src/pages/progress-report/__tests__/objectives-json.test.ts +++ b/public/games/age-of-dwarves/guide/src/pages/progress-report/__tests__/objectives-json.test.ts @@ -25,20 +25,20 @@ describe('objectives.json (generated by tools/objectives-report.py)', () => { it('every objective has the required frontmatter fields', () => { for (const o of data.objectives) { expect(typeof o.id).toBe('string') - expect(o.id).toMatch(/^p[012]-\d{2}$/) + expect(o.id).toMatch(/^(p[0-3]|g[2-5])-\d{2}$/) expect(typeof o.title).toBe('string') expect(o.title.length).toBeGreaterThan(0) - expect(['p0', 'p1', 'p2']).toContain(o.priority) + expect(['p0', 'p1', 'p2', 'p3']).toContain(o.priority) expect(VALID_STATUSES.has(o.status)).toBe(true) - expect(['game1', 'game2']).toContain(o.scope) + expect(['game1', 'game2', 'game3', 'game4', 'game5']).toContain(o.scope) expect(typeof o.updated_at).toBe('string') expect(typeof o.summary).toBe('string') } }) - it('all game2-scoped objectives are out-of-scope', () => { + it('all non-game1-scoped objectives are out-of-scope', () => { for (const o of data.objectives) { - if (o.scope === 'game2') expect(o.status).toBe('oos') + if (o.scope !== 'game1') expect(o.status).toBe('oos') } }) diff --git a/public/games/age-of-dwarves/guide/src/pages/progress-report/filter.ts b/public/games/age-of-dwarves/guide/src/pages/progress-report/filter.ts index c745ac73..208ef4ef 100644 --- a/public/games/age-of-dwarves/guide/src/pages/progress-report/filter.ts +++ b/public/games/age-of-dwarves/guide/src/pages/progress-report/filter.ts @@ -16,7 +16,9 @@ export function filterObjectives( export function groupByPriority( objectives: ObjectiveRecord[], ): Record { - const groups: Record = { p0: [], p1: [], p2: [] } + const groups: Record = { + p0: [], p1: [], p2: [], p3: [], + } for (const o of objectives) groups[o.priority].push(o) return groups } diff --git a/public/games/age-of-dwarves/guide/src/pages/progress-report/styled.ts b/public/games/age-of-dwarves/guide/src/pages/progress-report/styled.ts index 8a834d28..1d4ce38e 100644 --- a/public/games/age-of-dwarves/guide/src/pages/progress-report/styled.ts +++ b/public/games/age-of-dwarves/guide/src/pages/progress-report/styled.ts @@ -112,6 +112,7 @@ const PRIORITY_COLOR: Record = { p0: '#f87171', p1: '#facc15', p2: '#60a5fa', + p3: '#94a3b8', } export const PriorityChip = styled.span<{ $priority: ObjectivePriority }>` diff --git a/public/games/age-of-dwarves/guide/src/pages/progress-report/types.ts b/public/games/age-of-dwarves/guide/src/pages/progress-report/types.ts index 39a70b26..ee1fa06d 100644 --- a/public/games/age-of-dwarves/guide/src/pages/progress-report/types.ts +++ b/public/games/age-of-dwarves/guide/src/pages/progress-report/types.ts @@ -1,6 +1,6 @@ export type ObjectiveStatus = 'done' | 'partial' | 'stub' | 'missing' | 'oos' -export type ObjectivePriority = 'p0' | 'p1' | 'p2' -export type ObjectiveScope = 'game1' | 'game2' +export type ObjectivePriority = 'p0' | 'p1' | 'p2' | 'p3' +export type ObjectiveScope = 'game1' | 'game2' | 'game3' | 'game4' | 'game5' export interface ObjectiveRecord { id: string diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index b0a62201..cfadc839 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -1089,6 +1089,32 @@ func _manage_production(city: Variant) -> void: var built: String = _next_building(city, player, city_count, has_founder) if built.is_empty(): built = "warrior" + # Once a city has enough buildings, prefer a world wonder in the capital. + if built == "warrior" and city_count >= 1 and Array(city.buildings).size() >= 6: + var existing: Array = Array(city.buildings) + var has_wonder: bool = false + for bld_id: String in existing: + var bd: Dictionary = DataLoader.get_building(bld_id) + if bd.get("wonder_type") != null: + has_wonder = true + break + if not has_wonder: + var best_wonder: String = "" + var best_era: int = 999 + for b: Dictionary in DataLoader.get_all_buildings(): + var wid: String = str(b.get("id", "")) + if wid.is_empty() or wid in existing: + continue + if b.get("wonder_type") == null: + continue + if not city.can_build(wid, player): + continue + var era: int = int(b.get("era", 999)) + if era < best_era: + best_era = era + best_wonder = wid + if not best_wonder.is_empty(): + built = best_wonder var unit_ids: Array[String] = ["warrior", "founder", "worker"] if built in unit_ids: city.add_to_queue("unit", built) diff --git a/src/packages/guide/src/index.ts b/src/packages/guide/src/index.ts index 132d808b..0a00b39f 100644 --- a/src/packages/guide/src/index.ts +++ b/src/packages/guide/src/index.ts @@ -1,7 +1,7 @@ // @magic-civ/guide-engine — shared UI components, types, utils -// Scope: Game 1 (Age of Dwarves). Game 2/3 content (magic schools, spells, -// archons, ley-lines, elven/hive episodes and worlds) has been removed per -// the CLAUDE.md scope rule: "do NOT ship Game 2 features into Game 1". + +// ─── Universe nav (episodes 2–5) — import in each episode's guide app ──────── +export { EP2_NAV, EP3_NAV, EP4_NAV, EP5_NAV, EPISODE_COLORS } from './nav' // ─── Data context ──────────────────────────────────────────────────────────── export { GuideDataProvider, useGuideData } from './contexts/GuideDataContext' diff --git a/src/packages/guide/src/nav.ts b/src/packages/guide/src/nav.ts new file mode 100644 index 00000000..4bce5929 --- /dev/null +++ b/src/packages/guide/src/nav.ts @@ -0,0 +1,109 @@ +import type { NavGroup } from './types/navigation' +import episodes from '@resources/episodes.json' + +const [, ep2, ep3, ep4, ep5] = episodes + +export const EPISODE_COLORS = { + ep1: '#c07040', // dwarf copper + ep2: '#4a7c59', // kzzykt green + ep3: '#7c5cbf', // elven violet + ep4: '#3a6ea8', // terran blue (psionics) + ep5: '#9b6b9b', // ethereal violet +} as const + +// ─── Episode 2: Age of Kzzykt — The Hive ───────────────────────────────────── + +export const EP2_NAV: NavGroup[] = [ + { + episodeHeader: { number: 2, name: ep2.display_name, color: EPISODE_COLORS.ep2, link: ep2.route! }, + title: 'Kzzykt & The Hive', + items: [ + { to: ep2.route!, icon: '📋', label: ep2.name }, + { to: '/worlds/the-hive', icon: '🐛', label: 'The Hive' }, + ], + }, + { + title: 'Ley Lines & Magic', + items: [ + { to: '/magic', icon: '🌿', label: 'Magic Overview' }, + { to: '/magic/ley-lines', icon: '〰', label: 'Ley Lines' }, + ], + }, +] + +// ─── Episode 3: Age of Elves — Silvandel ───────────────────────────────────── + +export const EP3_NAV: NavGroup[] = [ + { + episodeHeader: { number: 3, name: ep3.display_name, color: EPISODE_COLORS.ep3, link: ep3.route! }, + title: 'Elves & Silvandel', + items: [ + { to: ep3.route!, icon: '📋', label: ep3.name }, + { to: '/worlds/silvandel', icon: '🌲', label: 'Silvandel' }, + ], + }, + { + title: 'Schools of Magic', + items: [ + { to: '/magic/schools', icon: '📚', label: 'All Schools' }, + { to: '/magic/schools/life', icon: '💚', label: 'Life' }, + { to: '/magic/schools/death', icon: '💀', label: 'Death' }, + { to: '/magic/schools/chaos', icon: '🔥', label: 'Chaos' }, + { to: '/magic/schools/aether', icon: '✨', label: 'Aether' }, + ], + }, + { + title: 'Archons & Ascension', + items: [ + { to: '/magic/archons', icon: '👁', label: 'Archons' }, + { to: '/magic/ascension', icon: '🌟', label: 'Arcane Ascension' }, + ], + }, +] + +// ─── Episode 4: Age of Terrans — Terra ─────────────────────────────────────── + +export const EP4_NAV: NavGroup[] = [ + { + episodeHeader: { number: 4, name: ep4.display_name, color: EPISODE_COLORS.ep4, link: ep4.route! }, + title: 'Terrans & Terra', + items: [ + { to: ep4.route!, icon: '📋', label: ep4.name }, + { to: '/worlds/terra', icon: '🌍', label: 'Terra' }, + ], + }, + { + title: 'Psionics', + items: [ + { to: '/psionics', icon: '🧠', label: 'Psionics Overview' }, + ], + }, + { + title: 'Religion', + items: [ + { to: '/religion', icon: '⛪', label: 'Religious Victory' }, + ], + }, +] + +// ─── Episode 5: Age of Ascension — Ethereal Plane ──────────────────────────── + +export const EP5_NAV: NavGroup[] = [ + { + episodeHeader: { number: 5, name: ep5.display_name, color: EPISODE_COLORS.ep5, link: ep5.route! }, + title: 'The Ethereal Plane', + items: [ + { to: ep5.route!, icon: '📋', label: ep5.name }, + { to: '/worlds/ethereal-plane', icon: '🌌', label: 'Ethereal Plane' }, + ], + }, + { + title: 'Ethereal Species', + items: [ + { to: '/species/phantasma', icon: '👻', label: 'Phantasma' }, + { to: '/species/flugel', icon: '🪽', label: 'Flügel' }, + { to: '/species/gith', icon: '⚔', label: 'Gith' }, + { to: '/species/demonia', icon: '😈', label: 'Demonia' }, + ], + }, +]