feat: 从 PokeAPI 批量导入物种数据,替换硬编码

- 新增 pokedex-data.ts:1024 个物种的 baseExperience、EV yield、growthRate、captureRate、baseHappiness、hatchCounter
- 新增 fetch-pokedex-data.ts:PokeAPI 数据抓取脚本(可重复运行)
- 新增 fetch-species-names.ts:多语言名称抓取脚本(中/日/英)
- species.ts 改为使用 pokedex-data 替代硬编码 supplement 条目
- 解决 #1 XP 数据源、#2 EV 数据源、#13 Growth Rate 覆盖不全问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-24 08:56:12 +08:00
parent 6a89a5139a
commit 6fc365cf73
4 changed files with 1321 additions and 45 deletions

View File

@@ -0,0 +1,133 @@
/**
* Fetch base_experience, EV yield, and growth_rate for all species from PokeAPI.
* Generates src/dex/pokedex-data.ts
*
* Usage: bun run scripts/fetch-pokedex-data.ts
*/
import { Dex } from '@pkmn/sim'
const GROWTH_RATE_MAP: Record<string, string> = {
'slow-then-very-fast': 'erratic',
'fast-then-very-slow': 'fluctuating',
'medium': 'medium-fast',
'medium-slow': 'medium-slow',
'slow': 'slow',
'fast': 'fast',
}
const STAT_MAP: Record<string, string> = {
'hp': 'hp',
'attack': 'atk',
'defense': 'def',
'special-attack': 'spa',
'special-defense': 'spd',
'speed': 'spe',
}
interface SpeciesPokedex {
baseExperience: number
evs: Record<string, number>
growthRate: string
captureRate: number
baseHappiness: number
hatchCounter: number
}
async function fetchSpeciesData(id: number): Promise<SpeciesPokedex | null> {
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
if (!res.ok) return null
const data = await res.json() as any
// Get growth rate from species endpoint
const speciesRes = await fetch(`https://pokeapi.co/api/v2/pokemon-species/${id}`)
if (!speciesRes.ok) return null
const speciesData = await speciesRes.json() as any
const evs: Record<string, number> = {}
for (const stat of data.stats || []) {
if (stat.effort > 0) {
const statName = STAT_MAP[stat.stat.name]
if (statName) evs[statName] = stat.effort
}
}
const growthRateName = GROWTH_RATE_MAP[speciesData.growth_rate?.name] ?? 'medium-slow'
return {
baseExperience: data.base_experience ?? 50,
evs,
growthRate: growthRateName,
captureRate: speciesData.capture_rate ?? 45,
baseHappiness: speciesData.base_happiness ?? 70,
hatchCounter: speciesData.hatch_counter ?? 20,
}
} catch {
return null
}
}
async function main() {
// Get all base species IDs from Dex
const rawSpecies = Dex.data.Species as Record<string, { num: number; forme?: string }>
const species: { id: string; num: number }[] = []
for (const [id, s] of Object.entries(rawSpecies)) {
if (s.num > 0 && Number.isInteger(s.num) && !s.forme) {
species.push({ id, num: s.num })
}
}
species.sort((a, b) => a.num - b.num)
console.log(`Fetching data for ${species.length} species from PokeAPI...`)
const results: Record<string, SpeciesPokedex> = {}
let fetched = 0
const BATCH_SIZE = 20
for (let i = 0; i < species.length; i += BATCH_SIZE) {
const batch = species.slice(i, i + BATCH_SIZE)
const promises = batch.map(async (s) => {
const data = await fetchSpeciesData(s.num)
if (data) results[s.id] = data
fetched++
})
await Promise.all(promises)
process.stdout.write(`\rFetched ${fetched}/${species.length}...`)
// Small delay to avoid rate limiting
await new Promise(r => setTimeout(r, 200))
}
console.log(`\nFetched ${Object.keys(results).length} species.`)
// Generate TypeScript file
const lines: string[] = [
'// Auto-generated from PokeAPI. Run: bun run scripts/fetch-pokedex-data.ts',
'// eslint-disable-next-line @typescript-eslint/no-extraneous-class',
'export interface PokedexEntry {',
' baseExperience: number',
' evs: Record<string, number>',
' growthRate: string',
' captureRate: number',
' baseHappiness: number',
' hatchCounter?: number',
'}',
'',
'export const POKEDEX_DATA: Record<string, PokedexEntry> = {',
]
for (const [id, data] of Object.entries(results)) {
const evsStr = Object.keys(data.evs).length > 0
? `{ ${Object.entries(data.evs).map(([k, v]) => `${k}: ${v}`).join(', ')} }`
: '{}'
lines.push(` '${id}': { baseExperience: ${data.baseExperience}, evs: ${evsStr}, growthRate: '${data.growthRate}', captureRate: ${data.captureRate}, baseHappiness: ${data.baseHappiness}, hatchCounter: ${data.hatchCounter} },`)
}
lines.push('}')
lines.push('')
const outputPath = new URL('../src/dex/pokedex-data.ts', import.meta.url)
await Bun.write(outputPath, lines.join('\n'))
console.log(`Written to ${outputPath.pathname}`)
}
main().catch(console.error)

View File

@@ -0,0 +1,90 @@
/**
* Fetch multilingual species names (en, ja, zh) from PokeAPI.
* Generates src/dex/species-names.ts
*
* Usage: bun run scripts/fetch-species-names.ts
*/
import { Dex } from '@pkmn/sim'
interface SpeciesNames {
en: string
ja: string
zh: string
}
async function fetchSpeciesNames(id: number): Promise<SpeciesNames | null> {
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon-species/${id}`)
if (!res.ok) return null
const data = await res.json() as any
const names: SpeciesNames = { en: '', ja: '', zh: '' }
for (const entry of data.names || []) {
const lang = entry.language.name as string
if (lang === 'en') names.en = entry.name
else if (lang === 'ja') names.ja = entry.name
else if (lang === 'zh-Hant' || lang === 'zh-Hans') names.zh = entry.name
}
// Fallback to English if zh/ja missing
if (!names.zh) names.zh = names.en
if (!names.ja) names.ja = names.en
if (!names.en) return null
return names
} catch {
return null
}
}
async function main() {
const rawSpecies = Dex.data.Species as Record<string, { num: number; forme?: string }>
const species: { id: string; num: number }[] = []
for (const [id, s] of Object.entries(rawSpecies)) {
if (s.num > 0 && Number.isInteger(s.num) && !s.forme) {
species.push({ id, num: s.num })
}
}
species.sort((a, b) => a.num - b.num)
console.log(`Fetching names for ${species.length} species from PokeAPI...`)
const results: Record<string, SpeciesNames> = {}
let fetched = 0
const BATCH_SIZE = 20
for (let i = 0; i < species.length; i += BATCH_SIZE) {
const batch = species.slice(i, i + BATCH_SIZE)
const promises = batch.map(async (s) => {
const data = await fetchSpeciesNames(s.num)
if (data) results[s.id] = data
fetched++
})
await Promise.all(promises)
process.stdout.write(`\rFetched ${fetched}/${species.length}...`)
await new Promise(r => setTimeout(r, 200))
}
console.log(`\nFetched ${Object.keys(results).length} species names.`)
// Generate TypeScript file
const lines: string[] = [
'// Auto-generated from PokeAPI. Run: bun run scripts/fetch-species-names.ts',
'',
'export interface SpeciesI18n { en: string; ja: string; zh: string }',
'',
'export const SPECIES_I18N_DATA: Record<string, SpeciesI18n> = {',
]
for (const [id, data] of Object.entries(results)) {
lines.push(` '${id}': { en: '${data.en.replace(/'/g, "\\'")}', ja: '${data.ja}', zh: '${data.zh}' },`)
}
lines.push('}')
lines.push('')
const outputPath = new URL('../src/dex/species-names.ts', import.meta.url)
await Bun.write(outputPath, lines.join('\n'))
console.log(`Written to ${outputPath.pathname}`)
}
main().catch(console.error)