diff --git a/packages/pokemon/src/ui/PokedexView.tsx b/packages/pokemon/src/ui/PokedexView.tsx index 15b7a5844..aca628c48 100644 --- a/packages/pokemon/src/ui/PokedexView.tsx +++ b/packages/pokemon/src/ui/PokedexView.tsx @@ -3,105 +3,140 @@ import { Box, Text, type Color } from '@anthropic/ink' import type { BuddyData, SpeciesId } from '../types' import { ALL_SPECIES_IDS } from '../types' import { getSpeciesData } from '../dex/species' -import { getNextEvolution } from '../dex/evolution' const CYAN: Color = 'ansi:cyan' const GREEN: Color = 'ansi:green' const GRAY: Color = 'ansi:white' const YELLOW: Color = 'ansi:yellow' const WHITE: Color = 'ansi:whiteBright' -const RED: Color = 'ansi:red' -const BLUE: Color = 'ansi:blue' + +const BAR_WIDTH = 30 + +/** Gen ranges for stats */ +const GEN_RANGES = [ + { label: 'Gen I', start: 1, end: 151 }, + { label: 'Gen II', start: 152, end: 251 }, + { label: 'Gen III', start: 252, end: 386 }, + { label: 'Gen IV', start: 387, end: 493 }, + { label: 'Gen V', start: 494, end: 649 }, + { label: 'Gen VI', start: 650, end: 721 }, + { label: 'Gen VII', start: 722, end: 809 }, + { label: 'Gen VIII',start: 810, end: 905 }, + { label: 'Gen IX', start: 906, end: 1025 }, +] interface PokedexViewProps { buddyData: BuddyData } /** - * Pokédex view — shows all species with collection status, - * evolution chains, and active creature indicator. + * Pokédex view — shows collection progress, per-gen stats, + * and discovered species list. */ export function PokedexView({ buddyData }: PokedexViewProps) { const dexMap = new Map(buddyData.dex.map((d) => [d.speciesId, d])) const collected = buddyData.dex.length const total = ALL_SPECIES_IDS.length + const percent = total > 0 ? collected / total : 0 - // Group species by evolution chain - const chains = groupByChain() + // Build dex number set for quick lookup + const collectedNums = new Set() + for (const entry of buddyData.dex) { + const data = getSpeciesData(entry.speciesId) + collectedNums.add(data.dexNumber) + } + + // Per-gen stats + const genStats = GEN_RANGES.map(g => { + const genTotal = ALL_SPECIES_IDS.filter(id => { + const n = getSpeciesData(id).dexNumber + return n >= g.start && n <= g.end + }).length + const genCollected = [...collectedNums].filter(n => n >= g.start && n <= g.end).length + return { ...g, total: genTotal, collected: genCollected } + }) + + // Discovered species (for compact display) + const discovered = buddyData.dex + .map(entry => { + const species = getSpeciesData(entry.speciesId) + return { entry, species } + }) + .sort((a, b) => a.species.dexNumber - b.species.dexNumber) return ( - {/* Header */} - + {/* Header with percentage */} + Pokédex {collected} /{total} - collected + {(percent * 100).toFixed(1)}% - {/* Progress bar */} + {/* Fixed-width progress bar */} - {'█'.repeat(collected)} - {'░'.repeat(total - collected)} - {Math.floor((collected / total) * 100)}% + {'█'.repeat(Math.round(percent * BAR_WIDTH))} + {'░'.repeat(BAR_WIDTH - Math.round(percent * BAR_WIDTH))} + {Math.floor(percent * 100)}% - {/* Species list grouped by evolution chains */} - {chains.map((chain, ci) => ( - 0 ? 0 : 0}> - {chain.map((speciesId, si) => { - const species = getSpeciesData(speciesId) - const entry = dexMap.get(speciesId) - const discovered = !!entry - const isActive = buddyData.party[0] - ? buddyData.creatures.some((c) => c.id === buddyData.party[0] && c.speciesId === speciesId) - : false - const nextEvo = getNextEvolution(speciesId) + {/* Per-gen stats */} + + ─── 分代统计 ─── + {genStats.map(g => { + const p = g.total > 0 ? g.collected / g.total : 0 + const miniBar = '█'.repeat(Math.round(p * 10)) + '░'.repeat(10 - Math.round(p * 10)) + return ( + + {g.label.padEnd(8)} + = 1 ? GREEN : p > 0 ? YELLOW : GRAY}>{miniBar} + {g.collected}/{g.total} + + ) + })} + + {/* Discovered species list */} + {discovered.length > 0 && ( + + ─── 已发现 ({discovered.length}) ─── + {discovered.map(({ entry, species }) => { + const isActive = buddyData.party[0] + ? buddyData.creatures.some(c => c.id === buddyData.party[0] && c.speciesId === species.id) + : false return ( - - - {/* Chain connector */} - {si === 0 ? ' ' : '├'} - {/* Active indicator */} - {isActive ? : ' '} - {/* Dex number */} - #{String(species.dexNumber).padStart(3, '0')} - {/* Name */} - - {discovered - ? (species.names.zh ?? species.name) - : '???'} - - {/* Type badges */} - {discovered && ( - - {' '} - {species.types.filter((t): t is string => Boolean(t)).map((t, ti) => ( - - {ti > 0 ? '/' : ''}{t.slice(0, 3).toUpperCase()} - - ))} + + {isActive ? : ' '} + #{String(species.dexNumber).padStart(3, '0')} + + {(species.names as Record).zh ?? species.name} + + + {' '} + {species.types.filter((t): t is string => Boolean(t)).map((t, ti) => ( + + {ti > 0 ? '/' : ''}{t.slice(0, 3).toUpperCase()} - )} - {/* Level / unknown indicator */} - {discovered && entry ? ( - Lv.{entry.bestLevel} - ) : ( - ─── - )} - {/* Evolution arrow */} - {nextEvo && ( - Lv.{nextEvo.minLevel} - )} - + ))} + + Lv.{entry.bestLevel} + {entry.caughtCount > 1 && ( + x{entry.caughtCount} + )} ) })} - ))} + )} + + {discovered.length === 0 && ( + + 还没有发现任何精灵,开始冒险吧! + + )} {/* Stats row */} @@ -151,36 +186,3 @@ function getTypeColor(type: string): Color { } return colors[type] ?? 'ansi:white' } - -/** Group species by evolution chain for visual display */ -function groupByChain(): SpeciesId[][] { - const visited = new Set() - const chains: SpeciesId[][] = [] - - for (const id of ALL_SPECIES_IDS) { - if (visited.has(id)) continue - - // Walk back to find chain head - let head: SpeciesId = id - for (const candidate of ALL_SPECIES_IDS) { - const evo = getNextEvolution(candidate) - if (evo?.to === head) { - head = candidate - break - } - } - - // Walk forward to build chain - const chain: SpeciesId[] = [] - let current: SpeciesId | undefined = head - while (current && !visited.has(current)) { - chain.push(current) - visited.add(current) - current = getNextEvolution(current)?.to - } - - if (chain.length > 0) chains.push(chain) - } - - return chains -}