feat(@projects/@magic-civilization): add race episode filtering logic

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-18 03:38:28 -07:00
parent 77181f8808
commit 916dcac056
3 changed files with 38 additions and 7 deletions

BIN
mcp_wave_b_c_d_home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

View file

@ -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 (
<EpisodeProvider episode={ACTIVE_EPISODE}>
<GuideDataProvider data={guideData}>
<PreferencesProvider preferences={activePrefs} races={allRaces}>
<PreferencesProvider preferences={activePrefs} races={playableRaces}>
<RaceThemeProvider
colorMode={activePrefs.colorMode}
dyslexicFont={activePrefs.dyslexicFont}
@ -201,7 +213,7 @@ export default function App(): ReactElement {
return (
<EpisodeProvider episode={ACTIVE_EPISODE}>
<GuideDataProvider data={guideData}>
<PreferencesProvider preferences={activePrefs} races={allRaces}>
<PreferencesProvider preferences={activePrefs} races={playableRaces}>
<AppShell save={save} showWelcome={showWelcome} setShowWelcome={setShowWelcome} preferences={activePrefs}>
{routes}
</AppShell>

View file

@ -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<string, string[]> = {}
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<RaceOption[]>(
() => 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
</ShuffleButton>
</SectionRow>
<OptionGrid $cols={RACE_OPTIONS.length}>
{RACE_OPTIONS.map((opt) => {
<OptionGrid $cols={raceOptions.length}>
{raceOptions.map((opt) => {
const RaceIcon = RACE_ICONS[opt.id][visibleGender]
return (
<OptionButton