mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 08:15:53 +00:00
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:
133
packages/pokemon/scripts/fetch-pokedex-data.ts
Normal file
133
packages/pokemon/scripts/fetch-pokedex-data.ts
Normal 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)
|
||||||
90
packages/pokemon/scripts/fetch-species-names.ts
Normal file
90
packages/pokemon/scripts/fetch-species-names.ts
Normal 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)
|
||||||
1093
packages/pokemon/src/dex/pokedex-data.ts
Normal file
1093
packages/pokemon/src/dex/pokedex-data.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import type { SpeciesData, SpeciesId, GrowthRate } from '../types'
|
|||||||
import { getSpecies, mapBaseStats, mapGenderRatio } from './pkmn'
|
import { getSpecies, mapBaseStats, mapGenderRatio } from './pkmn'
|
||||||
import { getNextEvolution } from './evolution'
|
import { getNextEvolution } from './evolution'
|
||||||
import { SPECIES_PERSONALITY } from './names'
|
import { SPECIES_PERSONALITY } from './names'
|
||||||
|
import { getGrowthRate, getCaptureRate, getBaseHappiness } from './pokedex-data'
|
||||||
|
|
||||||
// ─── Dynamic species list from @pkmn/sim Dex ───
|
// ─── Dynamic species list from @pkmn/sim Dex ───
|
||||||
|
|
||||||
@@ -22,78 +23,38 @@ export const ALL_SPECIES_IDS: SpeciesId[] = _ids
|
|||||||
// Only curated entries for species with known data; defaults used for others.
|
// Only curated entries for species with known data; defaults used for others.
|
||||||
|
|
||||||
interface SupplementEntry {
|
interface SupplementEntry {
|
||||||
growthRate: GrowthRate
|
|
||||||
captureRate: number
|
|
||||||
baseHappiness: number
|
|
||||||
flavorText: string
|
flavorText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SUPPLEMENT: SupplementEntry = {
|
|
||||||
growthRate: 'medium-slow',
|
|
||||||
captureRate: 45,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
const SUPPLEMENT: Partial<Record<string, SupplementEntry>> = {
|
const SUPPLEMENT: Partial<Record<string, SupplementEntry>> = {
|
||||||
bulbasaur: {
|
bulbasaur: {
|
||||||
growthRate: 'medium-slow',
|
|
||||||
captureRate: 45,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: 'A strange seed was planted on its back at birth. The plant sprouts and grows with this Pokémon.',
|
flavorText: 'A strange seed was planted on its back at birth. The plant sprouts and grows with this Pokémon.',
|
||||||
},
|
},
|
||||||
ivysaur: {
|
ivysaur: {
|
||||||
growthRate: 'medium-slow',
|
|
||||||
captureRate: 45,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: 'When the bulb on its back grows large, it appears to lose the ability to stand on its hind legs.',
|
flavorText: 'When the bulb on its back grows large, it appears to lose the ability to stand on its hind legs.',
|
||||||
},
|
},
|
||||||
venusaur: {
|
venusaur: {
|
||||||
growthRate: 'medium-slow',
|
|
||||||
captureRate: 45,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: 'The plant blooms when it is absorbing solar energy. It stays on the move to seek sunlight.',
|
flavorText: 'The plant blooms when it is absorbing solar energy. It stays on the move to seek sunlight.',
|
||||||
},
|
},
|
||||||
charmander: {
|
charmander: {
|
||||||
growthRate: 'medium-slow',
|
|
||||||
captureRate: 45,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: 'Obviously prefers hot places. When it rains, steam is said to spout from the tip of its tail.',
|
flavorText: 'Obviously prefers hot places. When it rains, steam is said to spout from the tip of its tail.',
|
||||||
},
|
},
|
||||||
charmeleon: {
|
charmeleon: {
|
||||||
growthRate: 'medium-slow',
|
|
||||||
captureRate: 45,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: 'Tough fights could excite this Pokémon. When excited, it may blow out bluish-white flames.',
|
flavorText: 'Tough fights could excite this Pokémon. When excited, it may blow out bluish-white flames.',
|
||||||
},
|
},
|
||||||
charizard: {
|
charizard: {
|
||||||
growthRate: 'medium-slow',
|
|
||||||
captureRate: 45,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: 'Spits fire that is hot enough to melt boulders. Known to cause forest fires unintentionally.',
|
flavorText: 'Spits fire that is hot enough to melt boulders. Known to cause forest fires unintentionally.',
|
||||||
},
|
},
|
||||||
squirtle: {
|
squirtle: {
|
||||||
growthRate: 'medium-slow',
|
|
||||||
captureRate: 45,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: 'After birth, its back swells and hardens into a shell. Powerfully sprays foam from its mouth.',
|
flavorText: 'After birth, its back swells and hardens into a shell. Powerfully sprays foam from its mouth.',
|
||||||
},
|
},
|
||||||
wartortle: {
|
wartortle: {
|
||||||
growthRate: 'medium-slow',
|
|
||||||
captureRate: 45,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: 'Often hides in water to stalk unwary prey. For swimming fast, it moves its ears to maintain balance.',
|
flavorText: 'Often hides in water to stalk unwary prey. For swimming fast, it moves its ears to maintain balance.',
|
||||||
},
|
},
|
||||||
blastoise: {
|
blastoise: {
|
||||||
growthRate: 'medium-slow',
|
|
||||||
captureRate: 45,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: 'It crushes its foe under its heavy body to cause fainting. In a pinch, it will withdraw inside its shell.',
|
flavorText: 'It crushes its foe under its heavy body to cause fainting. In a pinch, it will withdraw inside its shell.',
|
||||||
},
|
},
|
||||||
pikachu: {
|
pikachu: {
|
||||||
growthRate: 'medium-fast',
|
|
||||||
captureRate: 190,
|
|
||||||
baseHappiness: 70,
|
|
||||||
flavorText: 'When several of these Pokémon gather, their electricity can build and cause lightning storms.',
|
flavorText: 'When several of these Pokémon gather, their electricity can build and cause lightning storms.',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -110,7 +71,6 @@ function buildEvolutionChain(speciesId: SpeciesId): SpeciesData['evolutionChain'
|
|||||||
|
|
||||||
function buildSpeciesData(id: SpeciesId): SpeciesData {
|
function buildSpeciesData(id: SpeciesId): SpeciesData {
|
||||||
const dex = getSpecies(id)
|
const dex = getSpecies(id)
|
||||||
const sup = SUPPLEMENT[id] ?? DEFAULT_SUPPLEMENT
|
|
||||||
const personality = SPECIES_PERSONALITY[id]
|
const personality = SPECIES_PERSONALITY[id]
|
||||||
|
|
||||||
if (!dex) {
|
if (!dex) {
|
||||||
@@ -125,13 +85,13 @@ function buildSpeciesData(id: SpeciesId): SpeciesData {
|
|||||||
genderRate: mapGenderRatio(dex.genderRatio as { M: number; F: number } | undefined),
|
genderRate: mapGenderRatio(dex.genderRatio as { M: number; F: number } | undefined),
|
||||||
baseStats: mapBaseStats(dex.baseStats),
|
baseStats: mapBaseStats(dex.baseStats),
|
||||||
types: dex.types.map((t: string) => t.toLowerCase()) as [string, string?],
|
types: dex.types.map((t: string) => t.toLowerCase()) as [string, string?],
|
||||||
baseHappiness: sup.baseHappiness,
|
baseHappiness: getBaseHappiness(id),
|
||||||
growthRate: sup.growthRate,
|
growthRate: getGrowthRate(id) as GrowthRate,
|
||||||
captureRate: sup.captureRate,
|
captureRate: getCaptureRate(id),
|
||||||
personality: personality ?? '',
|
personality: personality ?? '',
|
||||||
evolutionChain: buildEvolutionChain(id),
|
evolutionChain: buildEvolutionChain(id),
|
||||||
shinyChance: 1 / 4096,
|
shinyChance: 1 / 4096,
|
||||||
flavorText: sup.flavorText,
|
flavorText: SUPPLEMENT[id]?.flavorText ?? '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user