diff --git a/mcp_wave_b_c_d_home.png b/mcp_wave_b_c_d_home.png new file mode 100644 index 00000000..19f3fcb4 Binary files /dev/null and b/mcp_wave_b_c_d_home.png differ diff --git a/public/games/age-of-dwarves/guide/src/App.tsx b/public/games/age-of-dwarves/guide/src/App.tsx index beff1b1f..6b8a6e39 100644 --- a/public/games/age-of-dwarves/guide/src/App.tsx +++ b/public/games/age-of-dwarves/guide/src/App.tsx @@ -45,6 +45,18 @@ const DEV_GUIDE_ALL_EPISODES = 999 const ACTIVE_EPISODE: number = import.meta.env.VITE_DEV_GUIDE === '1' ? DEV_GUIDE_ALL_EPISODES : 1 +// Races playable in the current episode. Every race JSON carries an +// `episode: number` field = the episode where that race first becomes a +// valid leader choice (cumulative). Game 1 = Dwarves only; Game 3 = +// Dwarves + Kzzkyt + four Elf races; etc. Dev bundle +// (`VITE_DEV_GUIDE=1` → episode=999) unlocks every race whose episode +// field is set. Races without an `episode` field (stubs, wrapper JSONs +// like strategic_axes) are excluded. +const playableRaces = allRaces.filter( + (r): r is typeof r & { episode: number } => + typeof r.episode === 'number' && r.episode <= ACTIVE_EPISODE, +) + export default function App(): ReactElement { const { preferences, isFirstVisit, save } = usePlayerPreferences() const [searchParams] = useSearchParams() @@ -183,7 +195,7 @@ export default function App(): ReactElement { return ( - + - + {routes} diff --git a/public/games/age-of-dwarves/guide/src/components/welcome/WelcomeModal.tsx b/public/games/age-of-dwarves/guide/src/components/welcome/WelcomeModal.tsx index e75c261f..44bfd9c5 100644 --- a/public/games/age-of-dwarves/guide/src/components/welcome/WelcomeModal.tsx +++ b/public/games/age-of-dwarves/guide/src/components/welcome/WelcomeModal.tsx @@ -1,4 +1,5 @@ -import { useState, useEffect, useRef, type ReactElement } from 'react' +import { useState, useEffect, useMemo, useRef, type ReactElement } from 'react' +import { useEpisode } from '@magic-civ/guide-engine' import type { PlayerPreferences, ColorMode, FontSize } from '@/hooks/usePlayerPreferences' import { allRaces } from '@/data' import { resolveRace, resolveGender, type ConcreteRace, type ConcreteGender } from '@/contexts/PreferencesContext' @@ -23,13 +24,21 @@ const RULER_NAMES: Record = {} interface RaceOption { id: ConcreteRace label: string + episode: number } -const RACE_OPTIONS: RaceOption[] = allRaces - .filter((r) => r.id in RACE_ICONS) +/** Every race whose JSON carries an `episode: number` field + a sidebar icon + * in `RACE_ICONS`. The active-episode filter (inside the component) trims + * the list down to races playable in the current game — Game 1 = Dwarves + * only, Game 2 adds Kzzkyt, Game 3 unlocks the four Elf races, etc. */ +const ALL_ELIGIBLE_RACES: RaceOption[] = allRaces + .filter((r): r is typeof r & { episode: number } => + typeof r.episode === 'number' && r.id in RACE_ICONS, + ) .map((r): RaceOption => ({ id: r.id as ConcreteRace, label: r.name, + episode: r.episode, })) const GENDER_OPTIONS: ReadonlyArray<{ id: ConcreteGender; label: string }> = [ @@ -64,6 +73,16 @@ export function WelcomeModal({ onConfirm, onClose, onPreview, initialPreferences const initRaceRandom = !initialPreferences || initialPreferences.race === 'random' const initGenderRandom = !initialPreferences || initialPreferences.gender === 'random' + // Race grid filtered by the active episode — every race's `episode` field + // declares the game where it first becomes a valid leader choice + // (cumulative). Game 1 = Dwarves only; Game 3 would offer Dwarves + + // Kzzkyt + four Elf races; `VITE_DEV_GUIDE=1` unlocks the full roster. + const activeEpisode = useEpisode() + const raceOptions = useMemo( + () => ALL_ELIGIBLE_RACES.filter((r) => r.episode <= activeEpisode), + [activeEpisode], + ) + const [raceIsRandom, setRaceIsRandom] = useState(initRaceRandom) const [genderIsRandom, setGenderIsRandom] = useState(initGenderRandom) @@ -146,8 +165,8 @@ export function WelcomeModal({ onConfirm, onClose, onPreview, initialPreferences Shuffle - - {RACE_OPTIONS.map((opt) => { + + {raceOptions.map((opt) => { const RaceIcon = RACE_ICONS[opt.id][visibleGender] return (