mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 16:55:51 +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",
|
"types": "./src/index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pkmn/client": "^0.7.2",
|
"@pkmn/client": "^0.7.2",
|
||||||
"@pkmn/protocol": "^0.7.2",
|
"@pkmn/protocol": "^0.7.2"
|
||||||
"@pkmn/view": "^0.7.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import type { SpeciesId, Creature } from '../types'
|
import type { SpeciesId, Creature } from '../types'
|
||||||
import { generateCreature, calculateStats, getCreatureName, getTotalEV, recalculateLevel, getActiveCreature } from '../core/creature'
|
import { generateCreature, calculateStats, getCreatureName, getTotalEV, recalculateLevel, getActiveCreature } from '../core/creature'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
|
|
||||||
describe('generateCreature', () => {
|
describe('generateCreature', () => {
|
||||||
test('creates a creature with correct defaults', async () => {
|
test('creates a creature with correct defaults', async () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, test, expect, beforeEach } from 'bun:test'
|
import { describe, test, expect, beforeEach } from 'bun:test'
|
||||||
import { generateCreature } from '../core/creature'
|
import { generateCreature } from '../core/creature'
|
||||||
import { awardEV, awardTurnEV, getEVSummary, resetEVCooldowns } from '../core/effort'
|
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(() => {
|
beforeEach(() => {
|
||||||
resetEVCooldowns()
|
resetEVCooldowns()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
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', () => {
|
describe('getEVForTool', () => {
|
||||||
test('returns EV mapping for known tools', () => {
|
test('returns EV mapping for known tools', () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import { generateCreature } from '../core/creature'
|
import { generateCreature } from '../core/creature'
|
||||||
import { awardXP, getXpProgress } from '../core/experience'
|
import { awardXP, getXpProgress } from '../core/experience'
|
||||||
import { xpForLevel, levelFromXp, xpToNextLevel } from '../data/xpTable'
|
import { xpForLevel, levelFromXp, xpToNextLevel } from '../dex/xpTable'
|
||||||
|
|
||||||
describe('xpForLevel', () => {
|
describe('xpForLevel', () => {
|
||||||
test('level 1 requires 0 XP', () => {
|
test('level 1 requires 0 XP', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import { determineGender, getGenderSymbol } from '../core/gender'
|
import { determineGender, getGenderSymbol } from '../core/gender'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
|
|
||||||
describe('determineGender', () => {
|
describe('determineGender', () => {
|
||||||
test('genderless species', () => {
|
test('genderless species', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
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'
|
import { EMPTY_MOVE } from '../types'
|
||||||
|
|
||||||
describe('getDefaultMoveset', () => {
|
describe('getDefaultMoveset', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
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'
|
import { ALL_SPECIES_IDS } from '../types'
|
||||||
|
|
||||||
describe('SPECIES_NAMES', () => {
|
describe('SPECIES_NAMES', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import { getAllNatureNames, randomNature, getNatureEffect } from '../data/nature'
|
import { getAllNatureNames, randomNature, getNatureEffect } from '../dex/nature'
|
||||||
|
|
||||||
describe('getAllNatureNames', () => {
|
describe('getAllNatureNames', () => {
|
||||||
test('returns 25 nature names', () => {
|
test('returns 25 nature names', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
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', () => {
|
describe('FROM_DEX_STAT', () => {
|
||||||
test('maps all 6 stats', () => {
|
test('maps all 6 stats', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
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 { ALL_SPECIES_IDS } from '../types'
|
||||||
import type { SpeciesId } from '../types'
|
import type { SpeciesId } from '../types'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import { xpForLevel, levelFromXp, xpToNextLevel } from '../data/xpTable'
|
import { xpForLevel, levelFromXp, xpToNextLevel } from '../dex/xpTable'
|
||||||
|
|
||||||
describe('xpForLevel', () => {
|
describe('xpForLevel', () => {
|
||||||
test('returns 0 for level 1', () => {
|
test('returns 0 for level 1', () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Battle, Teams, toID } from '@pkmn/sim'
|
import { Battle, Teams, toID } from '@pkmn/sim'
|
||||||
import { Dex } from '@pkmn/sim'
|
import { Dex } from '@pkmn/sim'
|
||||||
import type { Creature, SpeciesId } from '../types'
|
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 { STAT_NAMES } from '../types'
|
||||||
import type { BattleState, BattlePokemon, BattleEvent, PlayerAction, StatusCondition } from './types'
|
import type { BattleState, BattlePokemon, BattleEvent, PlayerAction, StatusCondition } from './types'
|
||||||
import { chooseAIMove } from './ai'
|
import { chooseAIMove } from './ai'
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { StatName, SpeciesId } from '../types'
|
import type { StatName, SpeciesId } from '../types'
|
||||||
import { STAT_NAMES } 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 { BattleResult } from './types'
|
||||||
import type { BuddyData } from '../types'
|
import type { BuddyData } from '../types'
|
||||||
import { levelFromXp } from '../data/xpTable'
|
import { levelFromXp } from '../dex/xpTable'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { MAX_EV_PER_STAT, MAX_EV_TOTAL } from '../data/evMapping'
|
import { MAX_EV_PER_STAT, MAX_EV_TOTAL } from '../dex/evMapping'
|
||||||
import { Dex } from '@pkmn/sim'
|
import { Dex } from '@pkmn/sim'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
import type { Creature, SpeciesId, StatName, StatsResult } from '../types'
|
import type { Creature, SpeciesId, StatName, StatsResult } from '../types'
|
||||||
import { STAT_NAMES } from '../types'
|
import { STAT_NAMES } from '../types'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { determineGender } from './gender'
|
import { determineGender } from './gender'
|
||||||
import { levelFromXp } from '../data/xpTable'
|
import { levelFromXp } from '../dex/xpTable'
|
||||||
import { gen, TO_DEX_STAT } from '../data/pkmn'
|
import { gen, TO_DEX_STAT } from '../dex/pkmn'
|
||||||
import { getDefaultMoveset, getDefaultAbility } from '../data/learnsets'
|
import { getDefaultMoveset, getDefaultAbility } from '../dex/learnsets'
|
||||||
import { randomNature } from '../data/nature'
|
import { randomNature } from '../dex/nature'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a new creature of the given species.
|
* Generate a new creature of the given species.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Creature, StatName } from '../types'
|
import type { Creature, StatName } from '../types'
|
||||||
import { STAT_NAMES } 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'
|
import { getTotalEV } from './creature'
|
||||||
|
|
||||||
// Track last EV award time per tool to enforce cooldown
|
// Track last EV award time per tool to enforce cooldown
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
import type { BuddyData, Creature, Egg, SpeciesId } from '../types'
|
import type { BuddyData, Creature, Egg, SpeciesId } from '../types'
|
||||||
import { ALL_SPECIES_IDS } from '../types'
|
import { ALL_SPECIES_IDS } from '../types'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { generateCreature } from './creature'
|
import { generateCreature } from './creature'
|
||||||
import { addToParty, depositToBox } from './storage'
|
import { addToParty, depositToBox } from './storage'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Creature, EvolutionResult, SpeciesId } from '../types'
|
import type { Creature, EvolutionResult, SpeciesId } from '../types'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { getNextEvolution } from '../data/evolution'
|
import { getNextEvolution } from '../dex/evolution'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a creature meets evolution conditions.
|
* Check if a creature meets evolution conditions.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Creature } from '../types'
|
import type { Creature } from '../types'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { levelFromXp, xpForLevel } from '../data/xpTable'
|
import { levelFromXp, xpForLevel } from '../dex/xpTable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Award XP to a creature. Returns updated creature and whether level up occurred.
|
* 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 { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import type { SpeciesId, SpriteCache } from '../types'
|
import type { SpeciesId, SpriteCache } from '../types'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { getSpritesDir } from './storage'
|
import { getSpritesDir } from './storage'
|
||||||
|
|
||||||
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/HRKings/pokemonsay-newgenerations/master/pokemons'
|
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 type { BuddyData, Creature, SpeciesId, PCBox, Bag } from '../types'
|
||||||
import { ALL_SPECIES_IDS } from '../types'
|
import { ALL_SPECIES_IDS } from '../types'
|
||||||
import { generateCreature } from './creature'
|
import { generateCreature } from './creature'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { getDefaultMoveset, getDefaultAbility } from '../data/learnsets'
|
import { getDefaultMoveset, getDefaultAbility } from '../dex/learnsets'
|
||||||
import { randomNature } from '../data/nature'
|
import { randomNature } from '../dex/nature'
|
||||||
|
|
||||||
const BUDDY_DATA_PATH = join(homedir(), '.claude', 'buddy-data.json')
|
const BUDDY_DATA_PATH = join(homedir(), '.claude', 'buddy-data.json')
|
||||||
const BUDDY_SPRITES_DIR = join(homedir(), '.claude', 'buddy-sprites')
|
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'
|
export { STAT_NAMES, STAT_LABELS, ALL_SPECIES_IDS, EMPTY_MOVE } from './types'
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
export { SPECIES_DATA, DEX_TO_SPECIES, getSpeciesData, getAllSpeciesData, ensureSpeciesData, refreshAllSpeciesData } from './data/species'
|
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 './data/evMapping'
|
export { DEFAULT_EV_MAPPING, getEVForTool, MAX_EV_PER_STAT, MAX_EV_TOTAL } from './dex/evMapping'
|
||||||
export { xpForLevel, levelFromXp, xpToNextLevel } from './data/xpTable'
|
export { xpForLevel, levelFromXp, xpToNextLevel } from './dex/xpTable'
|
||||||
export { SPECIES_NAMES, SPECIES_I18N, SPECIES_PERSONALITY } from './data/names'
|
export { SPECIES_NAMES, SPECIES_I18N, SPECIES_PERSONALITY } from './dex/names'
|
||||||
export { getAllNatureNames, randomNature, getNatureEffect } from './data/nature'
|
export { getAllNatureNames, randomNature, getNatureEffect } from './dex/nature'
|
||||||
export { getNextEvolution } from './data/evolution'
|
export { getNextEvolution } from './dex/evolution'
|
||||||
export { getDefaultMoveset, getDefaultAbility, getNewLearnableMoves } from './data/learnsets'
|
export { getDefaultMoveset, getDefaultAbility, getNewLearnableMoves } from './dex/learnsets'
|
||||||
export { FROM_DEX_STAT, TO_DEX_STAT } from './data/pkmn'
|
export { FROM_DEX_STAT, TO_DEX_STAT } from './dex/pkmn'
|
||||||
|
|
||||||
// Battle
|
// Battle
|
||||||
export type { BattleState, BattlePokemon, BattleEvent, BattleResult, PlayerAction, MoveOption, StatusCondition } from './battle/types'
|
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 { ItemPanel } from './ui/ItemPanel'
|
||||||
export { BattleResultPanel } from './ui/BattleResultPanel'
|
export { BattleResultPanel } from './ui/BattleResultPanel'
|
||||||
export { MoveLearnPanel } from './ui/MoveLearnPanel'
|
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 { Box, Text } from '@anthropic/ink'
|
||||||
import type { Creature, SpeciesId } from '../types'
|
import type { Creature, SpeciesId } from '../types'
|
||||||
import { ALL_SPECIES_IDS } from '../types'
|
import { ALL_SPECIES_IDS } from '../types'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { calculateStats, getCreatureName } from '../core/creature'
|
import { calculateStats, getCreatureName } from '../core/creature'
|
||||||
|
|
||||||
const CYAN = 'ansi:cyan'
|
const CYAN = 'ansi:cyan'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react'
|
|||||||
import { Box, Text, useInput } from '@anthropic/ink'
|
import { Box, Text, useInput } from '@anthropic/ink'
|
||||||
import type { BuddyData, Creature, SpeciesId } from '../types'
|
import type { BuddyData, Creature, SpeciesId } from '../types'
|
||||||
import { ALL_SPECIES_IDS } from '../types'
|
import { ALL_SPECIES_IDS } from '../types'
|
||||||
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { saveBuddyData } from '../core/storage'
|
import { saveBuddyData } from '../core/storage'
|
||||||
import { createBattle, executeTurn, type BattleInit } from '../battle/engine'
|
import { createBattle, executeTurn, type BattleInit } from '../battle/engine'
|
||||||
import { settleBattle, applyMoveLearn, applyEvolution } from '../battle/settlement'
|
import { settleBattle, applyMoveLearn, applyEvolution } from '../battle/settlement'
|
||||||
@@ -28,9 +29,10 @@ type Phase =
|
|||||||
interface BattleFlowProps {
|
interface BattleFlowProps {
|
||||||
buddyData: BuddyData
|
buddyData: BuddyData
|
||||||
onClose: () => void
|
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 [phase, setPhase] = useState<Phase>('config')
|
||||||
const [buddyData, setBuddyData] = useState(initialData)
|
const [buddyData, setBuddyData] = useState(initialData)
|
||||||
const [battleInit, setBattleInit] = useState<BattleInit | null>(null)
|
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 [pendingMoves, setPendingMoves] = useState<{ creatureId: string; moveId: string; moveName: string }[]>([])
|
||||||
const [pendingEvos, setPendingEvos] = useState<{ creatureId: string; from: SpeciesId; to: SpeciesId }[]>([])
|
const [pendingEvos, setPendingEvos] = useState<{ creatureId: string; from: SpeciesId; to: SpeciesId }[]>([])
|
||||||
const [replaceIndex, setReplaceIndex] = useState(0)
|
const [replaceIndex, setReplaceIndex] = useState(0)
|
||||||
|
const [speciesIndex, setSpeciesIndex] = useState(0)
|
||||||
|
|
||||||
// ─── Input handling ───
|
// ─── Input handling ───
|
||||||
|
|
||||||
useInput((input: string, key: { escape?: boolean; return?: boolean; upArrow?: boolean; downArrow?: boolean }) => {
|
useInput((input: string, key: { escape?: boolean; return?: boolean; upArrow?: boolean; downArrow?: boolean }) => {
|
||||||
// Config phase: Enter = random battle, ESC = cancel
|
// Config phase: Enter = random battle, ESC = cancel
|
||||||
|
if (!isActive) return
|
||||||
if (phase === 'config') {
|
if (phase === 'config') {
|
||||||
if (key.escape) {
|
if (key.escape) {
|
||||||
onClose()
|
onClose()
|
||||||
@@ -273,7 +277,6 @@ export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps)
|
|||||||
// Render by phase
|
// Render by phase
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case 'config':
|
case 'config':
|
||||||
case 'configSelect':
|
|
||||||
return (
|
return (
|
||||||
<BattleConfigPanel
|
<BattleConfigPanel
|
||||||
party={getPartyCreatures()}
|
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': {
|
case 'battle': {
|
||||||
if (!battleState) return null
|
if (!battleState) return null
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import React from 'react'
|
|||||||
import { Box, Text, type Color } from '@anthropic/ink'
|
import { Box, Text, type Color } from '@anthropic/ink'
|
||||||
import type { BuddyData, Creature, SpeciesId } from '../types'
|
import type { BuddyData, Creature, SpeciesId } from '../types'
|
||||||
import { STAT_NAMES, STAT_LABELS } from '../types'
|
import { STAT_NAMES, STAT_LABELS } from '../types'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { SPECIES_PERSONALITY } from '../data/names'
|
import { SPECIES_PERSONALITY } from '../dex/names'
|
||||||
import { calculateStats, getCreatureName, getTotalEV } from '../core/creature'
|
import { calculateStats, getCreatureName, getTotalEV } from '../core/creature'
|
||||||
import { getXpProgress } from '../core/experience'
|
import { getXpProgress } from '../core/experience'
|
||||||
import { getEVSummary } from '../core/effort'
|
import { getEVSummary } from '../core/effort'
|
||||||
import { getGenderSymbol } from '../core/gender'
|
import { getGenderSymbol } from '../core/gender'
|
||||||
import { getStatColor } from './shared'
|
import { getStatColor } from './shared'
|
||||||
import { getNextEvolution } from '../data/evolution'
|
import { getNextEvolution } from '../dex/evolution'
|
||||||
import { StatBar } from './StatBar'
|
import { StatBar } from './StatBar'
|
||||||
|
|
||||||
interface CompanionCardProps {
|
interface CompanionCardProps {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Box, Text, type Color } from '@anthropic/ink'
|
import { Box, Text, type Color } from '@anthropic/ink'
|
||||||
import type { SpeciesId } from '../types'
|
import type { SpeciesId } from '../types'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { loadSprite } from '../core/spriteCache'
|
import { loadSprite } from '../core/spriteCache'
|
||||||
import { getFallbackSprite } from '../sprites/fallback'
|
import { getFallbackSprite } from '../sprites/fallback'
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React from 'react'
|
|||||||
import { Box, Text, type Color } from '@anthropic/ink'
|
import { Box, Text, type Color } from '@anthropic/ink'
|
||||||
import type { BuddyData, SpeciesId } from '../types'
|
import type { BuddyData, SpeciesId } from '../types'
|
||||||
import { ALL_SPECIES_IDS } from '../types'
|
import { ALL_SPECIES_IDS } from '../types'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { getNextEvolution } from '../data/evolution'
|
import { getNextEvolution } from '../dex/evolution'
|
||||||
|
|
||||||
const CYAN: Color = 'ansi:cyan'
|
const CYAN: Color = 'ansi:cyan'
|
||||||
const GREEN: Color = 'ansi:green'
|
const GREEN: Color = 'ansi:green'
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React from 'react'
|
|||||||
import { Box, Text, type Color } from '@anthropic/ink'
|
import { Box, Text, type Color } from '@anthropic/ink'
|
||||||
import type { SpeciesId, StatName } from '../types'
|
import type { SpeciesId, StatName } from '../types'
|
||||||
import { STAT_NAMES, STAT_LABELS, ALL_SPECIES_IDS } from '../types'
|
import { STAT_NAMES, STAT_LABELS, ALL_SPECIES_IDS } from '../types'
|
||||||
import { getSpeciesData } from '../data/species'
|
import { getSpeciesData } from '../dex/species'
|
||||||
import { getNextEvolution } from '../data/evolution'
|
import { getNextEvolution } from '../dex/evolution'
|
||||||
import { StatBar } from './StatBar'
|
import { StatBar } from './StatBar'
|
||||||
import { getStatColor } from './shared'
|
import { getStatColor } from './shared'
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { getXpProgress } from '@claude-code-best/pokemon';
|
|||||||
|
|
||||||
import { getGenderSymbol } from '@claude-code-best/pokemon';
|
import { getGenderSymbol } from '@claude-code-best/pokemon';
|
||||||
import { StatBar, SpriteAnimator, getFallbackSprite, loadSprite } 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';
|
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||||
|
|
||||||
const CYAN: Color = 'ansi:cyan';
|
const CYAN: Color = 'ansi:cyan';
|
||||||
@@ -91,6 +92,13 @@ export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps)
|
|||||||
onClose={() => onClose('buddy panel closed')}
|
onClose={() => onClose('buddy panel closed')}
|
||||||
/>
|
/>
|
||||||
</Tab>,
|
</Tab>,
|
||||||
|
<Tab key="battle" title="Battle">
|
||||||
|
<BattleTab
|
||||||
|
buddyData={data}
|
||||||
|
isActive={selectedTab === 'Battle'}
|
||||||
|
onUpdate={updateData}
|
||||||
|
/>
|
||||||
|
</Tab>,
|
||||||
<Tab key="egg" title="Egg">
|
<Tab key="egg" title="Egg">
|
||||||
<EggTab buddyData={data} />
|
<EggTab buddyData={data} />
|
||||||
</Tab>,
|
</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 ──────────────────────────────────────────
|
// ─── Egg Tab ──────────────────────────────────────────
|
||||||
|
|
||||||
function EggTab({ buddyData }: { buddyData: BuddyData }) {
|
function EggTab({ buddyData }: { buddyData: BuddyData }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user