mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
refactor: Phase 0 — 清除重复,委托 @pkmn 生态
- 删除硬编码 NATURES 常量,nature.ts 委托 Dex.natures - 删除硬编码 EVOLUTION_CHAINS,evolution.ts 委托 Dex.species.evos - calculateStats() 手写公式替换为 gen.stats.calc() - 统一 TO_DEX_STAT/FROM_DEX_STAT 映射到 pkmn.ts - 简化 species.ts buildEvolutionChain() 复用 getNextEvolution() - 添加 NatureName/NatureEffect 类型定义 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { STAT_NAMES } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { determineGender } from './gender'
|
||||
import { levelFromXp } from '../data/xpTable'
|
||||
import { gen, TO_DEX_STAT } from '../data/pkmn'
|
||||
|
||||
/**
|
||||
* Generate a new creature of the given species.
|
||||
@@ -37,28 +38,30 @@ export function generateCreature(speciesId: SpeciesId, seed?: number): Creature
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate actual stats for a creature using Pokémon stat formulas.
|
||||
* HP: floor((2 * base + iv + floor(ev/4)) * level / 100) + level + 10
|
||||
* Other: floor((2 * base + iv + floor(ev/4)) * level / 100) + 5
|
||||
* Calculate actual stats for a creature using @pkmn/data stats.calc().
|
||||
* Handles base stats, IV, EV, level, and nature correction internally.
|
||||
*/
|
||||
export function calculateStats(creature: Creature): StatsResult {
|
||||
const species = getSpeciesData(creature.speciesId)
|
||||
const level = creature.level
|
||||
const result: StatsResult = {} as StatsResult
|
||||
const species = gen.species.get(creature.speciesId)
|
||||
if (!species) throw new Error(`Species ${creature.speciesId} not found`)
|
||||
|
||||
// Get nature if creature has one (Phase 1 adds nature field)
|
||||
const nature = 'nature' in creature && creature.nature
|
||||
? gen.natures.get(creature.nature as string)
|
||||
: undefined
|
||||
|
||||
const result = {} as StatsResult
|
||||
for (const stat of STAT_NAMES) {
|
||||
const base = species.baseStats[stat]
|
||||
const iv = creature.iv[stat]
|
||||
const ev = creature.ev[stat]
|
||||
const raw = Math.floor((2 * base + iv + Math.floor(ev / 4)) * level / 100)
|
||||
|
||||
if (stat === 'hp') {
|
||||
result[stat] = raw + level + 10
|
||||
} else {
|
||||
result[stat] = raw + 5
|
||||
}
|
||||
const dexKey = TO_DEX_STAT[stat] as 'hp' | 'atk' | 'def' | 'spa' | 'spd' | 'spe'
|
||||
result[stat] = gen.stats.calc(
|
||||
dexKey,
|
||||
species.baseStats[dexKey],
|
||||
creature.iv[stat],
|
||||
creature.ev[stat],
|
||||
creature.level,
|
||||
nature ?? undefined,
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export function checkEvolution(creature: Creature): EvolutionResult | null {
|
||||
if (!nextEvo) return null
|
||||
|
||||
// Check level-up conditions
|
||||
if (nextEvo.trigger === 'level_up' && creature.level >= nextEvo.minLevel) {
|
||||
if (nextEvo.trigger === 'level_up' && nextEvo.minLevel != null && creature.level >= nextEvo.minLevel) {
|
||||
return {
|
||||
from: creature.speciesId,
|
||||
to: nextEvo.to,
|
||||
|
||||
45
packages/pokemon/src/data/evolution.ts
Normal file
45
packages/pokemon/src/data/evolution.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Dex } from '@pkmn/sim'
|
||||
import type { SpeciesId } from '../types'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
|
||||
export interface EvolutionChainStep {
|
||||
from: SpeciesId
|
||||
to: SpeciesId
|
||||
trigger: 'level_up' | 'item' | 'trade' | 'friendship'
|
||||
minLevel?: number
|
||||
}
|
||||
|
||||
/** Find the next evolution for a species, dynamically from Dex */
|
||||
export function getNextEvolution(speciesId: SpeciesId): EvolutionChainStep | undefined {
|
||||
const dex = Dex.species.get(speciesId)
|
||||
if (!dex?.evos?.length) return undefined
|
||||
|
||||
// Take the first evolution target (most species have single evo path)
|
||||
const target = dex.evos[0]!.toLowerCase()
|
||||
if (!ALL_SPECIES_IDS.includes(target as SpeciesId)) return undefined
|
||||
|
||||
const targetDex = Dex.species.get(target)
|
||||
if (!targetDex?.exists) return undefined
|
||||
|
||||
const trigger = dex.evoType === 'trade' ? 'trade'
|
||||
: dex.evoType === 'useItem' ? 'item'
|
||||
: dex.evoType === 'levelFriendship' ? 'friendship'
|
||||
: 'level_up'
|
||||
|
||||
return {
|
||||
from: speciesId,
|
||||
to: target as SpeciesId,
|
||||
trigger,
|
||||
minLevel: targetDex.evoLevel ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use getNextEvolution() instead. Kept for backward compat. */
|
||||
export const EVOLUTION_CHAINS: EvolutionChainStep[] = (() => {
|
||||
const chains: EvolutionChainStep[] = []
|
||||
for (const id of ALL_SPECIES_IDS) {
|
||||
const evo = getNextEvolution(id)
|
||||
if (evo) chains.push(evo)
|
||||
}
|
||||
return chains
|
||||
})()
|
||||
32
packages/pokemon/src/data/nature.ts
Normal file
32
packages/pokemon/src/data/nature.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Dex } from '@pkmn/sim'
|
||||
import type { NatureName, NatureEffect, NatureStat } from '../types'
|
||||
|
||||
// All 25 canonical nature names (Dex.natures is not iterable, so we list them)
|
||||
const NATURE_IDS: NatureName[] = [
|
||||
'hardy', 'lonely', 'brave', 'adamant', 'naughty',
|
||||
'bold', 'docile', 'relaxed', 'impish', 'lax',
|
||||
'timid', 'hasty', 'serious', 'jolly', 'naive',
|
||||
'modest', 'mild', 'quiet', 'bashful', 'rash',
|
||||
'calm', 'gentle', 'sassy', 'careful', 'quirky',
|
||||
]
|
||||
|
||||
/** Get all nature names */
|
||||
export function getAllNatureNames(): NatureName[] {
|
||||
return NATURE_IDS.filter(name => Dex.natures.get(name)?.exists)
|
||||
}
|
||||
|
||||
/** Randomly assign a nature */
|
||||
export function randomNature(): NatureName {
|
||||
const names = getAllNatureNames()
|
||||
return names[Math.floor(Math.random() * names.length)]!
|
||||
}
|
||||
|
||||
/** Get nature effect (plus/minus stat, or null for neutral) — delegates to Dex.natures */
|
||||
export function getNatureEffect(nature: NatureName): NatureEffect {
|
||||
const n = Dex.natures.get(nature)
|
||||
if (!n?.exists) return { plus: null, minus: null }
|
||||
return {
|
||||
plus: (n.plus as NatureStat | undefined) ?? null,
|
||||
minus: (n.minus as NatureStat | undefined) ?? null,
|
||||
}
|
||||
}
|
||||
59
packages/pokemon/src/data/pkmn.ts
Normal file
59
packages/pokemon/src/data/pkmn.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Dex } from '@pkmn/sim'
|
||||
import { Generations } from '@pkmn/data'
|
||||
import type { StatName } from '../types'
|
||||
|
||||
// Singleton Gen 9 data source
|
||||
const gens = new Generations(Dex as unknown as import('@pkmn/data').Dex)
|
||||
export const gen = gens.get(9)
|
||||
|
||||
// Stat name mapping: @pkmn/sim → our StatName
|
||||
export const FROM_DEX_STAT: Record<string, StatName> = {
|
||||
hp: 'hp', atk: 'attack', def: 'defense',
|
||||
spa: 'spAtk', spd: 'spDef', spe: 'speed',
|
||||
}
|
||||
|
||||
// Stat name mapping: our StatName → @pkmn/sim abbreviation
|
||||
export const TO_DEX_STAT: Record<StatName, string> = {
|
||||
hp: 'hp', attack: 'atk', defense: 'def',
|
||||
spAtk: 'spa', spDef: 'spd', speed: 'spe',
|
||||
}
|
||||
|
||||
/** Query species from Dex */
|
||||
export function getSpecies(id: string) {
|
||||
return gen.species.get(id)
|
||||
}
|
||||
|
||||
/** Query move from Dex */
|
||||
export function getMove(id: string) {
|
||||
return gen.moves.get(id)
|
||||
}
|
||||
|
||||
/** Query ability from Dex */
|
||||
export function getAbility(id: string) {
|
||||
return gen.abilities.get(id)
|
||||
}
|
||||
|
||||
/** Query type from Dex */
|
||||
export function getType(id: string) {
|
||||
return gen.types.get(id)
|
||||
}
|
||||
|
||||
/** Map Dex baseStats to our StatName format */
|
||||
export function mapBaseStats(dexStats: { hp: number; atk: number; def: number; spa: number; spd: number; spe: number }): Record<StatName, number> {
|
||||
const result = {} as Record<StatName, number>
|
||||
for (const [dexKey, ourKey] of Object.entries(FROM_DEX_STAT)) {
|
||||
result[ourKey] = dexStats[dexKey as keyof typeof dexStats] ?? 0
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Get gender rate from Dex genderRatio (M/F ratio → our genderRate 0-8) */
|
||||
export function mapGenderRatio(genderRatio?: { M: number; F: number } | string): number {
|
||||
if (!genderRatio || typeof genderRatio === 'string') return -1 // genderless
|
||||
return Math.round(genderRatio.F * 8)
|
||||
}
|
||||
|
||||
/** Get primary ability ID from Dex abilities object */
|
||||
export function getPrimaryAbility(abilities: Record<string, string>): string {
|
||||
return abilities['0']?.toLowerCase() ?? ''
|
||||
}
|
||||
191
packages/pokemon/src/data/species.ts
Normal file
191
packages/pokemon/src/data/species.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import type { SpeciesData, SpeciesId, GrowthRate } from '../types'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
import { getSpecies, mapBaseStats, mapGenderRatio } from './pkmn'
|
||||
import { getNextEvolution } from './evolution'
|
||||
import { SPECIES_I18N, SPECIES_PERSONALITY } from './names'
|
||||
|
||||
// ─── Supplementary data (fields not provided by @pkmn/sim) ───
|
||||
|
||||
const SUPPLEMENT: Record<SpeciesId, {
|
||||
growthRate: GrowthRate
|
||||
captureRate: number
|
||||
baseHappiness: number
|
||||
flavorText: string
|
||||
}> = {
|
||||
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.',
|
||||
},
|
||||
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.',
|
||||
},
|
||||
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.',
|
||||
},
|
||||
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.',
|
||||
},
|
||||
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.',
|
||||
},
|
||||
charizard: {
|
||||
growthRate: 'medium-slow',
|
||||
captureRate: 45,
|
||||
baseHappiness: 70,
|
||||
flavorText: 'Spits fire that is hot enough to melt boulders. Known to cause forest fires unintentionally.',
|
||||
},
|
||||
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.',
|
||||
},
|
||||
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.',
|
||||
},
|
||||
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.',
|
||||
},
|
||||
pikachu: {
|
||||
growthRate: 'medium-fast',
|
||||
captureRate: 190,
|
||||
baseHappiness: 70,
|
||||
flavorText: 'When several of these Pokémon gather, their electricity can build and cause lightning storms.',
|
||||
},
|
||||
}
|
||||
|
||||
// ─── Evolution chain builder (from Dex evos field) ───
|
||||
|
||||
function buildEvolutionChain(speciesId: SpeciesId): SpeciesData['evolutionChain'] {
|
||||
const evo = getNextEvolution(speciesId)
|
||||
if (!evo) return undefined
|
||||
return [{ trigger: evo.trigger, level: evo.minLevel, into: evo.to }]
|
||||
}
|
||||
|
||||
// ─── Build SpeciesData from Dex + supplement ───
|
||||
|
||||
function buildSpeciesData(id: SpeciesId): SpeciesData {
|
||||
const dex = getSpecies(id)
|
||||
const sup = SUPPLEMENT[id]
|
||||
const i18n = SPECIES_I18N[id]
|
||||
const personality = SPECIES_PERSONALITY[id]
|
||||
|
||||
if (!dex) {
|
||||
// Fallback if Dex somehow doesn't have the species (shouldn't happen for MVP)
|
||||
throw new Error(`Species ${id} not found in @pkmn/sim Dex`)
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name: dex.name,
|
||||
names: i18n ?? { en: dex.name },
|
||||
dexNumber: dex.num,
|
||||
genderRate: mapGenderRatio(dex.genderRatio as { M: number; F: number } | undefined),
|
||||
baseStats: mapBaseStats(dex.baseStats),
|
||||
types: dex.types.map((t: string) => t.toLowerCase()) as [string, string?],
|
||||
baseHappiness: sup.baseHappiness,
|
||||
growthRate: sup.growthRate,
|
||||
captureRate: sup.captureRate,
|
||||
personality: personality ?? '',
|
||||
evolutionChain: buildEvolutionChain(id),
|
||||
shinyChance: 1 / 4096,
|
||||
flavorText: sup.flavorText,
|
||||
}
|
||||
}
|
||||
|
||||
// ─── In-memory cache (built once, immutable) ───
|
||||
|
||||
const speciesCache = new Map<SpeciesId, SpeciesData>()
|
||||
|
||||
function getCached(id: SpeciesId): SpeciesData {
|
||||
let data = speciesCache.get(id)
|
||||
if (!data) {
|
||||
data = buildSpeciesData(id)
|
||||
speciesCache.set(id, data)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// ─── Sync getters (used by all consumers) ───
|
||||
|
||||
/** Get species data by ID. */
|
||||
export function getSpeciesData(id: SpeciesId): SpeciesData {
|
||||
return getCached(id)
|
||||
}
|
||||
|
||||
/** Get all species data as a Record. */
|
||||
export function getAllSpeciesData(): Record<SpeciesId, SpeciesData> {
|
||||
const result = {} as Record<SpeciesId, SpeciesData>
|
||||
for (const id of ALL_SPECIES_IDS) {
|
||||
result[id] = getCached(id)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous getter that returns the full map.
|
||||
* @deprecated Use getSpeciesData / getAllSpeciesData
|
||||
*/
|
||||
export const SPECIES_DATA: Record<SpeciesId, SpeciesData> = new Proxy({} as Record<SpeciesId, SpeciesData>, {
|
||||
get(_, prop: string) {
|
||||
return getSpeciesData(prop as SpeciesId)
|
||||
},
|
||||
ownKeys() {
|
||||
return ALL_SPECIES_IDS as unknown as string[]
|
||||
},
|
||||
has(_, prop) {
|
||||
return ALL_SPECIES_IDS.includes(prop as SpeciesId)
|
||||
},
|
||||
getOwnPropertyDescriptor(_, prop) {
|
||||
if (ALL_SPECIES_IDS.includes(prop as SpeciesId)) {
|
||||
return { configurable: true, enumerable: true, value: getSpeciesData(prop as SpeciesId) }
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
|
||||
/** No-op — data is now built-in from @pkmn/sim */
|
||||
export function ensureSpeciesData(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
/** No-op — data is now built-in from @pkmn/sim */
|
||||
export async function refreshAllSpeciesData(): Promise<void> {
|
||||
// Clear cache to force rebuild
|
||||
speciesCache.clear()
|
||||
}
|
||||
|
||||
// ─── Dex number mapping ───
|
||||
|
||||
export const DEX_TO_SPECIES: Record<number, SpeciesId> = {
|
||||
1: 'bulbasaur',
|
||||
2: 'ivysaur',
|
||||
3: 'venusaur',
|
||||
4: 'charmander',
|
||||
5: 'charmeleon',
|
||||
6: 'charizard',
|
||||
7: 'squirtle',
|
||||
8: 'wartortle',
|
||||
9: 'blastoise',
|
||||
25: 'pikachu',
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
// Types
|
||||
export type {
|
||||
StatName,
|
||||
NatureName,
|
||||
NatureStat,
|
||||
NatureEffect,
|
||||
SpeciesId,
|
||||
Gender,
|
||||
EvolutionTrigger,
|
||||
@@ -23,7 +26,9 @@ export { SPECIES_DATA, DEX_TO_SPECIES, getSpeciesData, getAllSpeciesData, ensure
|
||||
export { DEFAULT_EV_MAPPING, getEVForTool, MAX_EV_PER_STAT, MAX_EV_TOTAL } from './data/evMapping'
|
||||
export { xpForLevel, levelFromXp, xpToNextLevel } from './data/xpTable'
|
||||
export { SPECIES_NAMES, SPECIES_I18N, SPECIES_PERSONALITY } from './data/names'
|
||||
export { getAllNatureNames, randomNature, getNatureEffect } from './data/nature'
|
||||
export { getNextEvolution, EVOLUTION_CHAINS } from './data/evolution'
|
||||
export { FROM_DEX_STAT, TO_DEX_STAT } from './data/pkmn'
|
||||
|
||||
// Core
|
||||
export { generateCreature, calculateStats, getCreatureName, recalculateLevel, getActiveCreature, getTotalEV } from './core/creature'
|
||||
|
||||
@@ -36,6 +36,11 @@ export const ALL_SPECIES_IDS: SpeciesId[] = [
|
||||
'pikachu',
|
||||
]
|
||||
|
||||
// Nature (delegated to @pkmn/sim Dex.natures)
|
||||
export type NatureName = string
|
||||
export type NatureStat = 'attack' | 'defense' | 'spAtk' | 'spDef' | 'speed'
|
||||
export type NatureEffect = { plus: NatureStat | null; minus: NatureStat | null }
|
||||
|
||||
// Gender
|
||||
export type Gender = 'male' | 'female' | 'genderless'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user