feat(@projects/@magic-civilization): ✨ update guide navigation structure
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
862b5a4817
commit
406ec8cc21
10 changed files with 159 additions and 109 deletions
|
|
@ -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' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ const PRIORITY_LABEL: Record<ObjectivePriority, string> = {
|
|||
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 && (
|
||||
<DeferredDisclosure>
|
||||
<summary>
|
||||
Deferred to Game 2 — Age of Kzzykt ({deferred.length})
|
||||
Deferred to future games ({deferred.length})
|
||||
</summary>
|
||||
<Prose style={{ marginTop: '0.75rem' }}>
|
||||
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.
|
||||
</Prose>
|
||||
<ObjectivesTable>
|
||||
<thead>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ const PRIORITY_LABEL: Record<ObjectivePriority, string> = {
|
|||
p0: 'P0 — Blockers for completely playable',
|
||||
p1: 'P1 — Ship readiness',
|
||||
p2: 'P2 — Polish',
|
||||
p3: 'P3 — Future-game scope',
|
||||
}
|
||||
|
||||
const BREAKDOWN_STATUSES: readonly ObjectiveStatus[] = [
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ export function filterObjectives(
|
|||
export function groupByPriority(
|
||||
objectives: ObjectiveRecord[],
|
||||
): Record<ObjectivePriority, ObjectiveRecord[]> {
|
||||
const groups: Record<ObjectivePriority, ObjectiveRecord[]> = { p0: [], p1: [], p2: [] }
|
||||
const groups: Record<ObjectivePriority, ObjectiveRecord[]> = {
|
||||
p0: [], p1: [], p2: [], p3: [],
|
||||
}
|
||||
for (const o of objectives) groups[o.priority].push(o)
|
||||
return groups
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ const PRIORITY_COLOR: Record<ObjectivePriority, string> = {
|
|||
p0: '#f87171',
|
||||
p1: '#facc15',
|
||||
p2: '#60a5fa',
|
||||
p3: '#94a3b8',
|
||||
}
|
||||
|
||||
export const PriorityChip = styled.span<{ $priority: ObjectivePriority }>`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
109
src/packages/guide/src/nav.ts
Normal file
109
src/packages/guide/src/nav.ts
Normal file
|
|
@ -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' },
|
||||
],
|
||||
},
|
||||
]
|
||||
Loading…
Add table
Reference in a new issue