mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 08:45:50 +00:00
feat: 集成 Battle tab 到 BuddyPanel,重命名 data/ 为 dex/ 规避 gitignore
- BuddyPanel 新增 Battle tab,BattleFlow 加 isActive 控制 - BattleFlow configSelect 阶段支持 ↑↓ 选择物种 - packages/pokemon/src/data/ → dex/,解决根 .gitignore 匹配问题 - 全量 Tab→2空格 缩进转换 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
"types": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@pkmn/client": "^0.7.2",
|
||||
"@pkmn/protocol": "^0.7.2",
|
||||
"@pkmn/view": "^0.7.2"
|
||||
"@pkmn/protocol": "^0.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import type { SpeciesId, Creature } from '../types'
|
||||
import { generateCreature, calculateStats, getCreatureName, getTotalEV, recalculateLevel, getActiveCreature } from '../core/creature'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
|
||||
describe('generateCreature', () => {
|
||||
test('creates a creature with correct defaults', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, test, expect, beforeEach } from 'bun:test'
|
||||
import { generateCreature } from '../core/creature'
|
||||
import { awardEV, awardTurnEV, getEVSummary, resetEVCooldowns } from '../core/effort'
|
||||
import { MAX_EV_PER_STAT, MAX_EV_TOTAL } from '../data/evMapping'
|
||||
import { MAX_EV_PER_STAT, MAX_EV_TOTAL } from '../dex/evMapping'
|
||||
|
||||
beforeEach(() => {
|
||||
resetEVCooldowns()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { getEVForTool, DEFAULT_EV_MAPPING, MAX_EV_PER_STAT, MAX_EV_TOTAL } from '../data/evMapping'
|
||||
import { getEVForTool, DEFAULT_EV_MAPPING, MAX_EV_PER_STAT, MAX_EV_TOTAL } from '../dex/evMapping'
|
||||
|
||||
describe('getEVForTool', () => {
|
||||
test('returns EV mapping for known tools', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { generateCreature } from '../core/creature'
|
||||
import { awardXP, getXpProgress } from '../core/experience'
|
||||
import { xpForLevel, levelFromXp, xpToNextLevel } from '../data/xpTable'
|
||||
import { xpForLevel, levelFromXp, xpToNextLevel } from '../dex/xpTable'
|
||||
|
||||
describe('xpForLevel', () => {
|
||||
test('level 1 requires 0 XP', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { determineGender, getGenderSymbol } from '../core/gender'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
|
||||
describe('determineGender', () => {
|
||||
test('genderless species', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { getDefaultMoveset, getDefaultAbility, getNewLearnableMoves } from '../data/learnsets'
|
||||
import { getDefaultMoveset, getDefaultAbility, getNewLearnableMoves } from '../dex/learnsets'
|
||||
import { EMPTY_MOVE } from '../types'
|
||||
|
||||
describe('getDefaultMoveset', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { SPECIES_NAMES, SPECIES_I18N, SPECIES_PERSONALITY } from '../data/names'
|
||||
import { SPECIES_NAMES, SPECIES_I18N, SPECIES_PERSONALITY } from '../dex/names'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
|
||||
describe('SPECIES_NAMES', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { getAllNatureNames, randomNature, getNatureEffect } from '../data/nature'
|
||||
import { getAllNatureNames, randomNature, getNatureEffect } from '../dex/nature'
|
||||
|
||||
describe('getAllNatureNames', () => {
|
||||
test('returns 25 nature names', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { FROM_DEX_STAT, TO_DEX_STAT, mapBaseStats, mapGenderRatio } from '../data/pkmn'
|
||||
import { FROM_DEX_STAT, TO_DEX_STAT, mapBaseStats, mapGenderRatio } from '../dex/pkmn'
|
||||
|
||||
describe('FROM_DEX_STAT', () => {
|
||||
test('maps all 6 stats', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { getSpeciesData, getAllSpeciesData, DEX_TO_SPECIES, ensureSpeciesData } from '../data/species'
|
||||
import { getSpeciesData, getAllSpeciesData, DEX_TO_SPECIES, ensureSpeciesData } from '../dex/species'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
import type { SpeciesId } from '../types'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { xpForLevel, levelFromXp, xpToNextLevel } from '../data/xpTable'
|
||||
import { xpForLevel, levelFromXp, xpToNextLevel } from '../dex/xpTable'
|
||||
|
||||
describe('xpForLevel', () => {
|
||||
test('returns 0 for level 1', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Battle, Teams, toID } from '@pkmn/sim'
|
||||
import { Dex } from '@pkmn/sim'
|
||||
import type { Creature, SpeciesId } from '../types'
|
||||
import { TO_DEX_STAT, FROM_DEX_STAT } from '../data/pkmn'
|
||||
import { TO_DEX_STAT, FROM_DEX_STAT } from '../dex/pkmn'
|
||||
import { STAT_NAMES } from '../types'
|
||||
import type { BattleState, BattlePokemon, BattleEvent, PlayerAction, StatusCondition } from './types'
|
||||
import { chooseAIMove } from './ai'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { StatName, SpeciesId } from '../types'
|
||||
import { STAT_NAMES } from '../types'
|
||||
import { TO_DEX_STAT } from '../data/pkmn'
|
||||
import { TO_DEX_STAT } from '../dex/pkmn'
|
||||
import type { BattleResult } from './types'
|
||||
import type { BuddyData } from '../types'
|
||||
import { levelFromXp } from '../data/xpTable'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { MAX_EV_PER_STAT, MAX_EV_TOTAL } from '../data/evMapping'
|
||||
import { levelFromXp } from '../dex/xpTable'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { MAX_EV_PER_STAT, MAX_EV_TOTAL } from '../dex/evMapping'
|
||||
import { Dex } from '@pkmn/sim'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { Creature, SpeciesId, StatName, StatsResult } from '../types'
|
||||
import { STAT_NAMES } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { determineGender } from './gender'
|
||||
import { levelFromXp } from '../data/xpTable'
|
||||
import { gen, TO_DEX_STAT } from '../data/pkmn'
|
||||
import { getDefaultMoveset, getDefaultAbility } from '../data/learnsets'
|
||||
import { randomNature } from '../data/nature'
|
||||
import { levelFromXp } from '../dex/xpTable'
|
||||
import { gen, TO_DEX_STAT } from '../dex/pkmn'
|
||||
import { getDefaultMoveset, getDefaultAbility } from '../dex/learnsets'
|
||||
import { randomNature } from '../dex/nature'
|
||||
|
||||
/**
|
||||
* Generate a new creature of the given species.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Creature, StatName } from '../types'
|
||||
import { STAT_NAMES } from '../types'
|
||||
import { getEVForTool, MAX_EV_PER_STAT, MAX_EV_TOTAL, EV_COOLDOWN_MS } from '../data/evMapping'
|
||||
import { getEVForTool, MAX_EV_PER_STAT, MAX_EV_TOTAL, EV_COOLDOWN_MS } from '../dex/evMapping'
|
||||
import { getTotalEV } from './creature'
|
||||
|
||||
// Track last EV award time per tool to enforce cooldown
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { BuddyData, Creature, Egg, SpeciesId } from '../types'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { generateCreature } from './creature'
|
||||
import { addToParty, depositToBox } from './storage'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Creature, EvolutionResult, SpeciesId } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getNextEvolution } from '../data/evolution'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { getNextEvolution } from '../dex/evolution'
|
||||
|
||||
/**
|
||||
* Check if a creature meets evolution conditions.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Creature } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { levelFromXp, xpForLevel } from '../data/xpTable'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { levelFromXp, xpForLevel } from '../dex/xpTable'
|
||||
|
||||
/**
|
||||
* Award XP to a creature. Returns updated creature and whether level up occurred.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type { SpeciesId, SpriteCache } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { getSpritesDir } from './storage'
|
||||
|
||||
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/HRKings/pokemonsay-newgenerations/master/pokemons'
|
||||
|
||||
@@ -4,9 +4,9 @@ import { homedir } from 'node:os'
|
||||
import type { BuddyData, Creature, SpeciesId, PCBox, Bag } from '../types'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
import { generateCreature } from './creature'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getDefaultMoveset, getDefaultAbility } from '../data/learnsets'
|
||||
import { randomNature } from '../data/nature'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { getDefaultMoveset, getDefaultAbility } from '../dex/learnsets'
|
||||
import { randomNature } from '../dex/nature'
|
||||
|
||||
const BUDDY_DATA_PATH = join(homedir(), '.claude', 'buddy-data.json')
|
||||
const BUDDY_SPRITES_DIR = join(homedir(), '.claude', 'buddy-sprites')
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Dex } from '@pkmn/sim'
|
||||
import type { SpeciesId, MoveSlot } from '../types'
|
||||
import { EMPTY_MOVE } from '../types'
|
||||
|
||||
const GEN = 9
|
||||
|
||||
/** Get the default moveset for a species at a given level (last 4 level-up moves) */
|
||||
export async function getDefaultMoveset(speciesId: SpeciesId, level: number): Promise<[MoveSlot, MoveSlot, MoveSlot, MoveSlot]> {
|
||||
const learnset = await Dex.learnsets.get(speciesId)
|
||||
if (!learnset?.learnset) return [EMPTY_MOVE, EMPTY_MOVE, EMPTY_MOVE, EMPTY_MOVE]
|
||||
|
||||
const levelUpMoves: { id: string; level: number }[] = []
|
||||
for (const [moveId, sources] of Object.entries(learnset.learnset)) {
|
||||
for (const src of sources as string[]) {
|
||||
if (src.startsWith(`${GEN}L`)) {
|
||||
levelUpMoves.push({ id: moveId, level: parseInt(src.slice(2)) })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
levelUpMoves.sort((a, b) => a.level - b.level)
|
||||
const available = levelUpMoves.filter(m => m.level <= level).slice(-4)
|
||||
|
||||
const slots: MoveSlot[] = available.map(m => {
|
||||
const dexMove = Dex.moves.get(m.id)
|
||||
return { id: m.id, pp: dexMove?.pp ?? 10, maxPp: dexMove?.pp ?? 10 }
|
||||
})
|
||||
|
||||
while (slots.length < 4) slots.push(EMPTY_MOVE)
|
||||
return slots as [MoveSlot, MoveSlot, MoveSlot, MoveSlot]
|
||||
}
|
||||
|
||||
/** Get the default ability for a species (first non-hidden ability) */
|
||||
export function getDefaultAbility(speciesId: SpeciesId): string {
|
||||
const species = Dex.species.get(speciesId)
|
||||
return species?.abilities?.['0']?.toLowerCase() ?? ''
|
||||
}
|
||||
|
||||
/** Get newly learnable moves when leveling up */
|
||||
export async function getNewLearnableMoves(speciesId: SpeciesId, oldLevel: number, newLevel: number): Promise<{ id: string; name: string }[]> {
|
||||
const learnset = await Dex.learnsets.get(speciesId)
|
||||
if (!learnset?.learnset) return []
|
||||
|
||||
const result: { id: string; name: string }[] = []
|
||||
for (const [moveId, sources] of Object.entries(learnset.learnset)) {
|
||||
for (const src of sources as string[]) {
|
||||
if (src.startsWith(`${GEN}L`)) {
|
||||
const moveLevel = parseInt(src.slice(2))
|
||||
if (moveLevel > oldLevel && moveLevel <= newLevel) {
|
||||
const dexMove = Dex.moves.get(moveId)
|
||||
result.push({ id: moveId, name: dexMove?.name ?? moveId })
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
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',
|
||||
}
|
||||
31
packages/pokemon/src/dex/evMapping.ts
Normal file
31
packages/pokemon/src/dex/evMapping.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { StatName } from '../types'
|
||||
|
||||
/**
|
||||
* Default EV mapping: tool name → EV gains per use.
|
||||
* Tools not in this mapping get random 1-2 EV points.
|
||||
*/
|
||||
export const DEFAULT_EV_MAPPING: Record<string, Record<StatName, number>> = {
|
||||
Bash: { hp: 0, attack: 2, defense: 0, spAtk: 0, spDef: 0, speed: 1 },
|
||||
Edit: { hp: 0, attack: 0, defense: 1, spAtk: 2, spDef: 0, speed: 0 },
|
||||
Write: { hp: 0, attack: 0, defense: 0, spAtk: 3, spDef: 0, speed: 0 },
|
||||
Read: { hp: 1, attack: 0, defense: 2, spAtk: 0, spDef: 0, speed: 0 },
|
||||
Grep: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 2, speed: 1 },
|
||||
Glob: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 2, speed: 1 },
|
||||
Agent: { hp: 0, attack: 1, defense: 0, spAtk: 0, spDef: 0, speed: 2 },
|
||||
WebSearch: { hp: 1, attack: 0, defense: 0, spAtk: 0, spDef: 2, speed: 0 },
|
||||
WebFetch: { hp: 1, attack: 0, defense: 0, spAtk: 0, spDef: 2, speed: 0 },
|
||||
}
|
||||
|
||||
// EV limits (matching original Pokémon)
|
||||
export const MAX_EV_PER_STAT = 252
|
||||
export const MAX_EV_TOTAL = 510
|
||||
|
||||
// EV cooldown: same tool type only counts once per 30 seconds
|
||||
export const EV_COOLDOWN_MS = 30_000
|
||||
|
||||
/**
|
||||
* Get EV gains for a tool. Returns undefined if not mapped (→ random).
|
||||
*/
|
||||
export function getEVForTool(toolName: string): Record<StatName, number> | undefined {
|
||||
return DEFAULT_EV_MAPPING[toolName]
|
||||
}
|
||||
37
packages/pokemon/src/dex/evolution.ts
Normal file
37
packages/pokemon/src/dex/evolution.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
59
packages/pokemon/src/dex/learnsets.ts
Normal file
59
packages/pokemon/src/dex/learnsets.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Dex } from '@pkmn/sim'
|
||||
import type { SpeciesId, MoveSlot } from '../types'
|
||||
import { EMPTY_MOVE } from '../types'
|
||||
|
||||
const GEN = 9
|
||||
|
||||
/** Get the default moveset for a species at a given level (last 4 level-up moves) */
|
||||
export async function getDefaultMoveset(speciesId: SpeciesId, level: number): Promise<[MoveSlot, MoveSlot, MoveSlot, MoveSlot]> {
|
||||
const learnset = await Dex.learnsets.get(speciesId)
|
||||
if (!learnset?.learnset) return [EMPTY_MOVE, EMPTY_MOVE, EMPTY_MOVE, EMPTY_MOVE]
|
||||
|
||||
const levelUpMoves: { id: string; level: number }[] = []
|
||||
for (const [moveId, sources] of Object.entries(learnset.learnset)) {
|
||||
for (const src of sources as string[]) {
|
||||
if (src.startsWith(`${GEN}L`)) {
|
||||
levelUpMoves.push({ id: moveId, level: parseInt(src.slice(2)) })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
levelUpMoves.sort((a, b) => a.level - b.level)
|
||||
const available = levelUpMoves.filter(m => m.level <= level).slice(-4)
|
||||
|
||||
const slots: MoveSlot[] = available.map(m => {
|
||||
const dexMove = Dex.moves.get(m.id)
|
||||
return { id: m.id, pp: dexMove?.pp ?? 10, maxPp: dexMove?.pp ?? 10 }
|
||||
})
|
||||
|
||||
while (slots.length < 4) slots.push(EMPTY_MOVE)
|
||||
return slots as [MoveSlot, MoveSlot, MoveSlot, MoveSlot]
|
||||
}
|
||||
|
||||
/** Get the default ability for a species (first non-hidden ability) */
|
||||
export function getDefaultAbility(speciesId: SpeciesId): string {
|
||||
const species = Dex.species.get(speciesId)
|
||||
return species?.abilities?.['0']?.toLowerCase() ?? ''
|
||||
}
|
||||
|
||||
/** Get newly learnable moves when leveling up */
|
||||
export async function getNewLearnableMoves(speciesId: SpeciesId, oldLevel: number, newLevel: number): Promise<{ id: string; name: string }[]> {
|
||||
const learnset = await Dex.learnsets.get(speciesId)
|
||||
if (!learnset?.learnset) return []
|
||||
|
||||
const result: { id: string; name: string }[] = []
|
||||
for (const [moveId, sources] of Object.entries(learnset.learnset)) {
|
||||
for (const src of sources as string[]) {
|
||||
if (src.startsWith(`${GEN}L`)) {
|
||||
const moveLevel = parseInt(src.slice(2))
|
||||
if (moveLevel > oldLevel && moveLevel <= newLevel) {
|
||||
const dexMove = Dex.moves.get(moveId)
|
||||
result.push({ id: moveId, name: dexMove?.name ?? moveId })
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
43
packages/pokemon/src/dex/names.ts
Normal file
43
packages/pokemon/src/dex/names.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { SpeciesId } from '../types'
|
||||
|
||||
/** Default names for each species (English) */
|
||||
export const SPECIES_NAMES: Record<SpeciesId, string> = {
|
||||
bulbasaur: 'Bulbasaur',
|
||||
ivysaur: 'Ivysaur',
|
||||
venusaur: 'Venusaur',
|
||||
charmander: 'Charmander',
|
||||
charmeleon: 'Charmeleon',
|
||||
charizard: 'Charizard',
|
||||
squirtle: 'Squirtle',
|
||||
wartortle: 'Wartortle',
|
||||
blastoise: 'Blastoise',
|
||||
pikachu: 'Pikachu',
|
||||
}
|
||||
|
||||
/** Multilingual names */
|
||||
export const SPECIES_I18N: Record<SpeciesId, Record<string, string>> = {
|
||||
bulbasaur: { en: 'Bulbasaur', ja: 'フシギダネ', zh: '妙蛙种子' },
|
||||
ivysaur: { en: 'Ivysaur', ja: 'フシギソウ', zh: '妙蛙草' },
|
||||
venusaur: { en: 'Venusaur', ja: 'フシギバナ', zh: '妙蛙花' },
|
||||
charmander: { en: 'Charmander', ja: 'ヒトカゲ', zh: '小火龙' },
|
||||
charmeleon: { en: 'Charmeleon', ja: 'リザード', zh: '火恐龙' },
|
||||
charizard: { en: 'Charizard', ja: 'リザードン', zh: '喷火龙' },
|
||||
squirtle: { en: 'Squirtle', ja: 'ゼニガメ', zh: '杰尼龟' },
|
||||
wartortle: { en: 'Wartortle', ja: 'カメール', zh: '卡咪龟' },
|
||||
blastoise: { en: 'Blastoise', ja: 'カメックス', zh: '水箭龟' },
|
||||
pikachu: { en: 'Pikachu', ja: 'ピカチュウ', zh: '皮卡丘' },
|
||||
}
|
||||
|
||||
/** Personality descriptions for each species */
|
||||
export const SPECIES_PERSONALITY: Record<SpeciesId, string> = {
|
||||
bulbasaur: 'Calm and collected, a reliable partner',
|
||||
ivysaur: 'Steady growth, patient and resilient',
|
||||
venusaur: 'Majestic and powerful, a natural leader',
|
||||
charmander: 'Energetic and curious, loves adventure',
|
||||
charmeleon: 'Fierce and determined, always pushing forward',
|
||||
charizard: 'Proud and strong-willed, a formidable ally',
|
||||
squirtle: 'Cheerful and playful, adapts easily',
|
||||
wartortle: 'Loyal and protective, wise beyond years',
|
||||
blastoise: 'Steadfast and powerful, a defensive fortress',
|
||||
pikachu: 'Friendly and energetic, always by your side',
|
||||
}
|
||||
191
packages/pokemon/src/dex/species.ts
Normal file
191
packages/pokemon/src/dex/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',
|
||||
}
|
||||
81
packages/pokemon/src/dex/xpTable.ts
Normal file
81
packages/pokemon/src/dex/xpTable.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { GrowthRate } from '../types'
|
||||
|
||||
/**
|
||||
* Calculate total XP required to reach a given level for a growth rate type.
|
||||
* Follows original Pokémon XP curve formulas.
|
||||
*/
|
||||
export function xpForLevel(level: number, growthRate: GrowthRate): number {
|
||||
if (level <= 1) return 0
|
||||
const n = level
|
||||
switch (growthRate) {
|
||||
case 'erratic':
|
||||
return xpErratic(n)
|
||||
case 'fast':
|
||||
return Math.floor((n * n * n * 4) / 5)
|
||||
case 'medium-fast':
|
||||
return n * n * n
|
||||
case 'medium-slow':
|
||||
return Math.floor((6 / 5) * n * n * n - 15 * n * n + 100 * n - 140)
|
||||
case 'slow':
|
||||
return Math.floor((5 * n * n * n) / 4)
|
||||
case 'fluctuating':
|
||||
return xpFluctuating(n)
|
||||
default:
|
||||
return n * n * n
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate level from total XP for a given growth rate.
|
||||
*/
|
||||
export function levelFromXp(totalXp: number, growthRate: GrowthRate): number {
|
||||
// Binary search for level
|
||||
let lo = 1
|
||||
let hi = 100
|
||||
while (lo < hi) {
|
||||
const mid = Math.ceil((lo + hi) / 2)
|
||||
if (xpForLevel(mid, growthRate) <= totalXp) {
|
||||
lo = mid
|
||||
} else {
|
||||
hi = mid - 1
|
||||
}
|
||||
}
|
||||
return Math.min(lo, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* XP needed to go from current level to next level.
|
||||
*/
|
||||
export function xpToNextLevel(currentLevel: number, totalXp: number, growthRate: GrowthRate): number {
|
||||
if (currentLevel >= 100) return 0
|
||||
const nextLevelXp = xpForLevel(currentLevel + 1, growthRate)
|
||||
return nextLevelXp - totalXp
|
||||
}
|
||||
|
||||
// Erratic growth rate (complex piecewise)
|
||||
function xpErratic(n: number): number {
|
||||
if (n <= 1) return 0
|
||||
if (n <= 50) {
|
||||
return Math.floor((n * n * n * (100 - n)) / 50)
|
||||
}
|
||||
if (n <= 68) {
|
||||
return Math.floor((n * n * n * (150 - n)) / 100)
|
||||
}
|
||||
if (n <= 98) {
|
||||
return Math.floor((n * n * n * Math.floor((1911 - 10 * n) / 3)) / 500)
|
||||
}
|
||||
// n 99-100
|
||||
return Math.floor((n * n * n * (160 - n)) / 100)
|
||||
}
|
||||
|
||||
// Fluctuating growth rate (complex piecewise)
|
||||
function xpFluctuating(n: number): number {
|
||||
if (n <= 1) return 0
|
||||
if (n <= 15) {
|
||||
return Math.floor((n * n * n * (Math.floor((n + 1) / 3) + 24)) / 50)
|
||||
}
|
||||
if (n <= 36) {
|
||||
return Math.floor((n * n * n * (n + 14)) / 50)
|
||||
}
|
||||
return Math.floor((n * n * n * (Math.floor(n / 2) + 32)) / 50)
|
||||
}
|
||||
@@ -27,14 +27,14 @@ export type {
|
||||
export { STAT_NAMES, STAT_LABELS, ALL_SPECIES_IDS, EMPTY_MOVE } from './types'
|
||||
|
||||
// Data
|
||||
export { SPECIES_DATA, DEX_TO_SPECIES, getSpeciesData, getAllSpeciesData, ensureSpeciesData, refreshAllSpeciesData } from './data/species'
|
||||
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 } from './data/evolution'
|
||||
export { getDefaultMoveset, getDefaultAbility, getNewLearnableMoves } from './data/learnsets'
|
||||
export { FROM_DEX_STAT, TO_DEX_STAT } from './data/pkmn'
|
||||
export { SPECIES_DATA, DEX_TO_SPECIES, getSpeciesData, getAllSpeciesData, ensureSpeciesData, refreshAllSpeciesData } from './dex/species'
|
||||
export { DEFAULT_EV_MAPPING, getEVForTool, MAX_EV_PER_STAT, MAX_EV_TOTAL } from './dex/evMapping'
|
||||
export { xpForLevel, levelFromXp, xpToNextLevel } from './dex/xpTable'
|
||||
export { SPECIES_NAMES, SPECIES_I18N, SPECIES_PERSONALITY } from './dex/names'
|
||||
export { getAllNatureNames, randomNature, getNatureEffect } from './dex/nature'
|
||||
export { getNextEvolution } from './dex/evolution'
|
||||
export { getDefaultMoveset, getDefaultAbility, getNewLearnableMoves } from './dex/learnsets'
|
||||
export { FROM_DEX_STAT, TO_DEX_STAT } from './dex/pkmn'
|
||||
|
||||
// Battle
|
||||
export type { BattleState, BattlePokemon, BattleEvent, BattleResult, PlayerAction, MoveOption, StatusCondition } from './battle/types'
|
||||
@@ -77,3 +77,4 @@ export { SwitchPanel } from './ui/SwitchPanel'
|
||||
export { ItemPanel } from './ui/ItemPanel'
|
||||
export { BattleResultPanel } from './ui/BattleResultPanel'
|
||||
export { MoveLearnPanel } from './ui/MoveLearnPanel'
|
||||
export { BattleFlow } from './ui/BattleFlow'
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import type { Creature, SpeciesId } from '../types'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { calculateStats, getCreatureName } from '../core/creature'
|
||||
|
||||
const CYAN = 'ansi:cyan'
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react'
|
||||
import { Box, Text, useInput } from '@anthropic/ink'
|
||||
import type { BuddyData, Creature, SpeciesId } from '../types'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { saveBuddyData } from '../core/storage'
|
||||
import { createBattle, executeTurn, type BattleInit } from '../battle/engine'
|
||||
import { settleBattle, applyMoveLearn, applyEvolution } from '../battle/settlement'
|
||||
@@ -28,9 +29,10 @@ type Phase =
|
||||
interface BattleFlowProps {
|
||||
buddyData: BuddyData
|
||||
onClose: () => void
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps) {
|
||||
export function BattleFlow({ buddyData: initialData, onClose, isActive = true }: BattleFlowProps) {
|
||||
const [phase, setPhase] = useState<Phase>('config')
|
||||
const [buddyData, setBuddyData] = useState(initialData)
|
||||
const [battleInit, setBattleInit] = useState<BattleInit | null>(null)
|
||||
@@ -40,11 +42,13 @@ export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps)
|
||||
const [pendingMoves, setPendingMoves] = useState<{ creatureId: string; moveId: string; moveName: string }[]>([])
|
||||
const [pendingEvos, setPendingEvos] = useState<{ creatureId: string; from: SpeciesId; to: SpeciesId }[]>([])
|
||||
const [replaceIndex, setReplaceIndex] = useState(0)
|
||||
const [speciesIndex, setSpeciesIndex] = useState(0)
|
||||
|
||||
// ─── Input handling ───
|
||||
|
||||
useInput((input: string, key: { escape?: boolean; return?: boolean; upArrow?: boolean; downArrow?: boolean }) => {
|
||||
// Config phase: Enter = random battle, ESC = cancel
|
||||
if (!isActive) return
|
||||
if (phase === 'config') {
|
||||
if (key.escape) {
|
||||
onClose()
|
||||
@@ -273,7 +277,6 @@ export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps)
|
||||
// Render by phase
|
||||
switch (phase) {
|
||||
case 'config':
|
||||
case 'configSelect':
|
||||
return (
|
||||
<BattleConfigPanel
|
||||
party={getPartyCreatures()}
|
||||
@@ -282,6 +285,34 @@ export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps)
|
||||
/>
|
||||
)
|
||||
|
||||
case 'configSelect': {
|
||||
const species = getSpeciesData(opponentSpeciesId)
|
||||
const selectedIdx = ALL_SPECIES_IDS.indexOf(opponentSpeciesId)
|
||||
const startIdx = Math.max(0, Math.min(selectedIdx, ALL_SPECIES_IDS.length - 5))
|
||||
const visibleSpecies = ALL_SPECIES_IDS.slice(startIdx, startIdx + 5)
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Text bold color="ansi:cyan"> 选择对手 </Text>
|
||||
{visibleSpecies.map((sid, i) => {
|
||||
const s = getSpeciesData(sid)
|
||||
const isSelected = sid === opponentSpeciesId
|
||||
return (
|
||||
<Box key={sid}>
|
||||
<Text color={isSelected ? 'ansi:yellow' : 'ansi:white'}>
|
||||
{isSelected ? ' ▶ ' : ' '}
|
||||
#{String(s.dexNumber).padStart(3, '0')} {s.names.zh ?? s.name}
|
||||
</Text>
|
||||
{isSelected && <Text color="ansi:cyan"> Lv.{getActiveCreatureLevel()}</Text>}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
<Box marginTop={1}>
|
||||
<Text color="ansi:white"> [↑↓] 选择 [Enter] 确认 [ESC] 返回</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
case 'battle': {
|
||||
if (!battleState) return null
|
||||
return (
|
||||
|
||||
@@ -2,14 +2,14 @@ import React from 'react'
|
||||
import { Box, Text, type Color } from '@anthropic/ink'
|
||||
import type { BuddyData, Creature, SpeciesId } from '../types'
|
||||
import { STAT_NAMES, STAT_LABELS } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { SPECIES_PERSONALITY } from '../data/names'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { SPECIES_PERSONALITY } from '../dex/names'
|
||||
import { calculateStats, getCreatureName, getTotalEV } from '../core/creature'
|
||||
import { getXpProgress } from '../core/experience'
|
||||
import { getEVSummary } from '../core/effort'
|
||||
import { getGenderSymbol } from '../core/gender'
|
||||
import { getStatColor } from './shared'
|
||||
import { getNextEvolution } from '../data/evolution'
|
||||
import { getNextEvolution } from '../dex/evolution'
|
||||
import { StatBar } from './StatBar'
|
||||
|
||||
interface CompanionCardProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Box, Text, type Color } from '@anthropic/ink'
|
||||
import type { SpeciesId } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { loadSprite } from '../core/spriteCache'
|
||||
import { getFallbackSprite } from '../sprites/fallback'
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react'
|
||||
import { Box, Text, type Color } from '@anthropic/ink'
|
||||
import type { BuddyData, SpeciesId } from '../types'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getNextEvolution } from '../data/evolution'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { getNextEvolution } from '../dex/evolution'
|
||||
|
||||
const CYAN: Color = 'ansi:cyan'
|
||||
const GREEN: Color = 'ansi:green'
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react'
|
||||
import { Box, Text, type Color } from '@anthropic/ink'
|
||||
import type { SpeciesId, StatName } from '../types'
|
||||
import { STAT_NAMES, STAT_LABELS, ALL_SPECIES_IDS } from '../types'
|
||||
import { getSpeciesData } from '../data/species'
|
||||
import { getNextEvolution } from '../data/evolution'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { getNextEvolution } from '../dex/evolution'
|
||||
import { StatBar } from './StatBar'
|
||||
import { getStatColor } from './shared'
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { getXpProgress } from '@claude-code-best/pokemon';
|
||||
|
||||
import { getGenderSymbol } from '@claude-code-best/pokemon';
|
||||
import { StatBar, SpriteAnimator, getFallbackSprite, loadSprite } from '@claude-code-best/pokemon';
|
||||
import { BattleFlow, loadBuddyData } from '@claude-code-best/pokemon';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
|
||||
const CYAN: Color = 'ansi:cyan';
|
||||
@@ -91,6 +92,13 @@ export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps)
|
||||
onClose={() => onClose('buddy panel closed')}
|
||||
/>
|
||||
</Tab>,
|
||||
<Tab key="battle" title="Battle">
|
||||
<BattleTab
|
||||
buddyData={data}
|
||||
isActive={selectedTab === 'Battle'}
|
||||
onUpdate={updateData}
|
||||
/>
|
||||
</Tab>,
|
||||
<Tab key="egg" title="Egg">
|
||||
<EggTab buddyData={data} />
|
||||
</Tab>,
|
||||
@@ -613,6 +621,35 @@ function DexTab({
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Battle Tab ──────────────────────────────────────────
|
||||
|
||||
function BattleTab({
|
||||
buddyData,
|
||||
isActive,
|
||||
onUpdate,
|
||||
}: {
|
||||
buddyData: BuddyData;
|
||||
isActive: boolean;
|
||||
onUpdate: (data: BuddyData) => void;
|
||||
}) {
|
||||
const [battleKey, setBattleKey] = useState(0);
|
||||
|
||||
const handleClose = async () => {
|
||||
const updated = await loadBuddyData();
|
||||
onUpdate(updated);
|
||||
setBattleKey(k => k + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<BattleFlow
|
||||
key={battleKey}
|
||||
buddyData={buddyData}
|
||||
onClose={handleClose}
|
||||
isActive={isActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Egg Tab ──────────────────────────────────────────
|
||||
|
||||
function EggTab({ buddyData }: { buddyData: BuddyData }) {
|
||||
|
||||
Reference in New Issue
Block a user