diff --git a/bun.lock b/bun.lock index 07c4c1afe..18b951d06 100644 --- a/bun.lock +++ b/bun.lock @@ -267,6 +267,11 @@ "packages/pokemon": { "name": "@claude-code-best/pokemon", "version": "1.0.0", + "dependencies": { + "@pkmn/client": "^0.7.2", + "@pkmn/protocol": "^0.7.2", + "@pkmn/view": "^0.7.2", + }, }, "packages/remote-control-server": { "name": "@anthropic/remote-control-server", @@ -975,6 +980,18 @@ "@pinojs/redact": ["@pinojs/redact@0.4.0", "https://registry.npmmirror.com/@pinojs/redact/-/redact-0.4.0.tgz", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + "@pkmn/client": ["@pkmn/client@0.7.2", "", { "dependencies": { "@pkmn/data": "^0.10.2", "@pkmn/protocol": "^0.7.2" } }, "sha512-Jf40Nqp+ZAcM5wNEIrMKNdU2G0OKELtGNXmq2QYRcVsutDF/g9+Xm5Y9nK+mIw+bAgSVSmZKD5/Y1MpwxHgX9A=="], + + "@pkmn/data": ["@pkmn/data@0.10.7", "", { "dependencies": { "@pkmn/dex-types": "^0.10.7" } }, "sha512-csUX8BU4RMHxl3nEF3gIOp1eq9+q3xh+ZaX+Nr1RZBqWF4L5PkBzUG36KKgU+lw92BmncDYm7S8t52lPhAmoXA=="], + + "@pkmn/dex-types": ["@pkmn/dex-types@0.10.7", "", { "dependencies": { "@pkmn/types": "^4.0.0" } }, "sha512-ewinxJHyeLwYSOcDsy+2pq9e1mMYNXwBB9B3CZG2fvonrkxAyycR5AtLKPqE61480jPPxaZocOh+SLtUm648SA=="], + + "@pkmn/protocol": ["@pkmn/protocol@0.7.2", "", { "dependencies": { "@pkmn/types": "^4.0.0" }, "bin": { "generate-handler": "generate-handler", "protocol-verifier": "protocol-verifier" } }, "sha512-3zRY9B4YN8aeA/jypPgW1hh/SiEIY6lNg9xOqIgox0m4sW1kMhGoNCygJ1Qvx8x33xSRD/AVtH+VtsCGn+LcQg=="], + + "@pkmn/types": ["@pkmn/types@4.0.0", "", {}, "sha512-gR2s/pxJYEegek1TtsYCQupNR3d5hMlcJFsiD+2LyfKr4tc+gETTql47tWLX5mFSbPcbXh7f4+7txlMIDoZx/g=="], + + "@pkmn/view": ["@pkmn/view@0.7.2", "", { "dependencies": { "@pkmn/protocol": "^0.7.2", "@pkmn/types": "^4.0.0" }, "bin": { "format-battle": "format-battle" } }, "sha512-SBaBIAuyJ/iGfYQxfzQ6jXv64Qz1/pIo5gjCXT9AfsErkHT27VwIhEEd2vUlD0bdfx802sPNvzHvLYJWMtss1w=="], + "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "https://registry.npmmirror.com/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="], "@prisma/instrumentation": ["@prisma/instrumentation@7.6.0", "https://registry.npmmirror.com/@prisma/instrumentation/-/instrumentation-7.6.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ=="], diff --git a/packages/pokemon/package.json b/packages/pokemon/package.json index fb91826fe..96d105f33 100644 --- a/packages/pokemon/package.json +++ b/packages/pokemon/package.json @@ -5,5 +5,9 @@ "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", - "dependencies": {} + "dependencies": { + "@pkmn/client": "^0.7.2", + "@pkmn/protocol": "^0.7.2", + "@pkmn/view": "^0.7.2" + } } diff --git a/packages/pokemon/src/battle/ai.ts b/packages/pokemon/src/battle/ai.ts new file mode 100644 index 000000000..899b8f8e1 --- /dev/null +++ b/packages/pokemon/src/battle/ai.ts @@ -0,0 +1,13 @@ +import type { BattlePokemon } from './types' + +/** + * Simple AI: pick a random usable move. + */ +export function chooseAIMove(pokemon: BattlePokemon): number { + const usable = pokemon.moves + .map((m, i) => ({ move: m, index: i })) + .filter(({ move }) => move.pp > 0 && !move.disabled) + + if (usable.length === 0) return 0 // Struggle + return usable[Math.floor(Math.random() * usable.length)]!.index +} diff --git a/packages/pokemon/src/battle/engine.ts b/packages/pokemon/src/battle/engine.ts new file mode 100644 index 000000000..742cd8838 --- /dev/null +++ b/packages/pokemon/src/battle/engine.ts @@ -0,0 +1,285 @@ +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 { STAT_NAMES } from '../types' +import type { BattleState, BattlePokemon, BattleEvent, BattleResult, PlayerAction, MoveOption, StatusCondition } from './types' +import { chooseAIMove } from './ai' + +// ─── Adapter: Creature → Showdown Set ─── + +function creatureToSetString(creature: Creature): string { + const species = Dex.species.get(creature.speciesId) + if (!species) throw new Error(`Species ${creature.speciesId} not found`) + + const natureName = creature.nature.charAt(0).toUpperCase() + creature.nature.slice(1) + const abilityName = creature.ability ? (Dex.abilities.get(creature.ability)?.name ?? creature.ability) : '' + + const moves = creature.moves + .filter(m => m.id) + .map(m => Dex.moves.get(m.id)?.name ?? m.id) + + const ivs = STAT_NAMES.map(s => `${creature.iv[s]} ${TO_DEX_STAT[s].toUpperCase().replace('SPA', 'SpA').replace('SPD', 'SpD')}`).join(' / ') + const evs = STAT_NAMES.map(s => `${creature.ev[s]} ${TO_DEX_STAT[s].toUpperCase().replace('SPA', 'SpA').replace('SPD', 'SpD')}`).join(' / ') + + const lines = [ + species.name, + `Level: ${creature.level}`, + `Ability: ${abilityName}`, + `Nature: ${natureName}`, + `IVs: ${ivs}`, + `EVs: ${evs}`, + ] + if (creature.heldItem) lines.push(`Item: ${Dex.items.get(creature.heldItem)?.name ?? creature.heldItem}`) + for (const move of moves) lines.push(`- ${move}`) + + return lines.join('\n') +} + +function wildPokemonToSetString(speciesId: SpeciesId, level: number): string { + const species = Dex.species.get(speciesId) + if (!species) throw new Error(`Species ${speciesId} not found`) + const ability = species.abilities['0'] ?? '' + // Get first 4 level-up moves (from species data) + const moves = getSpeciesMoves(speciesId, level) + return [species.name, `Level: ${level}`, `Ability: ${ability}`, ...moves.map(m => `- ${m}`)].join('\n') +} + +function getSpeciesMoves(speciesId: string, level: number): string[] { + // Use @pkmn/sim move pool - get natural level-up moves + // This is a simplified approach for the sim + const species = Dex.species.get(speciesId) + if (!species) return ['Tackle'] + // For sim battles, just return basic moves + return ['Tackle', 'Splash'] +} + +// ─── State Projection ─── + +function projectPokemon(pkm: any): BattlePokemon { + if (!pkm) throw new Error('No active pokemon') + const species = pkm.species + const hp = pkm.hp ?? 0 + const maxHp = pkm.maxhp ?? 1 + + return { + id: pkm.name, // sim doesn't store our UUID, use name as temp id + speciesId: toID(species.name) as SpeciesId, + name: species.name, + level: pkm.level, + hp, + maxHp, + types: species.types?.map((t: string) => t.toLowerCase()) ?? [], + moves: (pkm.moveSlots ?? pkm.baseMoveset ?? []).filter(Boolean).map((m: any) => ({ + id: toID(m.name ?? m), + name: m.name ?? m, + type: m.type ?? 'Normal', + pp: m.pp ?? 0, + maxPp: m.maxPp ?? m.pp ?? 0, + disabled: m.disabled ?? false, + })), + ability: pkm.ability ?? '', + heldItem: pkm.item ?? null, + status: mapStatus(pkm.status), + statStages: projectBoosts(pkm.boosts), + } +} + +function mapStatus(status: string): StatusCondition { + if (!status) return 'none' + const s = status.toLowerCase() + if (s === 'psn') return 'poison' + if (s === 'tox') return 'bad_poison' + if (s === 'brn') return 'burn' + if (s === 'par') return 'paralysis' + if (s === 'frz') return 'freeze' + if (s === 'slp') return 'sleep' + return 'none' +} + +function projectBoosts(boosts: Record | undefined): Record { + if (!boosts) return {} + const result: Record = {} + for (const [k, v] of Object.entries(boosts)) { + const mapped = FROM_DEX_STAT[k] + if (mapped) result[mapped] = v + else result[k] = v + } + return result +} + +// ─── Log Parsing ─── + +function parseLogToEvents(log: string[]): BattleEvent[] { + const events: BattleEvent[] = [] + for (const line of log) { + if (line.startsWith('|move|')) { + const parts = line.split('|') + const side = parts[2]?.startsWith('p1a') ? 'player' : 'opponent' + const moveName = parts[3] + events.push({ type: 'move', side, move: moveName, user: parts[2] }) + } else if (line.startsWith('|-damage|')) { + const parts = line.split('|') + const side = parts[2]?.startsWith('p1a') ? 'player' : 'opponent' + const hpStr = parts[3] // e.g. "16/20" or "16/20[1]" + const [cur, max] = parseHpString(hpStr) + events.push({ type: 'damage', side, amount: 0, percentage: Math.round((1 - cur / max) * 100) }) + } else if (line.startsWith('|-heal|')) { + const parts = line.split('|') + const side = parts[2]?.startsWith('p1a') ? 'player' : 'opponent' + const hpStr = parts[3] + const [cur, max] = parseHpString(hpStr) + events.push({ type: 'heal', side, amount: 0, percentage: Math.round(cur / max * 100) }) + } else if (line.startsWith('|faint|')) { + const parts = line.split('|') + const side = parts[2]?.startsWith('p1a') ? 'player' : 'opponent' + events.push({ type: 'faint', side, speciesId: toID(parts[2]?.split(': ')?.[1] ?? '') }) + } else if (line.startsWith('|switch|')) { + const parts = line.split('|') + const side = parts[2]?.startsWith('p1a') ? 'player' : 'opponent' + const speciesPart = parts[3]?.split(',')[0]?.split(': ') + events.push({ type: 'switch', side, speciesId: toID(speciesPart?.[1] ?? ''), name: speciesPart?.[1] ?? '' }) + } else if (line.startsWith('|-supereffective|')) { + events.push({ type: 'effectiveness', multiplier: 2 }) + } else if (line.startsWith('|-resisted|')) { + events.push({ type: 'effectiveness', multiplier: 0.5 }) + } else if (line.startsWith('|-crit|')) { + events.push({ type: 'crit' }) + } else if (line.startsWith('|-miss|')) { + const parts = line.split('|') + const side = parts[2]?.startsWith('p1a') ? 'player' : 'opponent' + events.push({ type: 'miss', side } as any) + } else if (line.startsWith('|-status|')) { + const parts = line.split('|') + const side = parts[2]?.startsWith('p1a') ? 'player' : 'opponent' + events.push({ type: 'status', side, status: mapStatus(parts[3]) }) + } else if (line.startsWith('|-boost|') || line.startsWith('|-unboost|')) { + const parts = line.split('|') + const side = parts[2]?.startsWith('p1a') ? 'player' : 'opponent' + const stages = line.startsWith('|-boost|') ? parseInt(parts[4]) : -parseInt(parts[4]) + events.push({ type: 'statChange', side, stat: parts[3], stages }) + } else if (line.startsWith('|-ability|')) { + const parts = line.split('|') + const side = parts[2]?.startsWith('p1a') ? 'player' : 'opponent' + events.push({ type: 'ability', side, ability: parts[3] }) + } else if (line.startsWith('|turn|')) { + events.push({ type: 'turn', number: parseInt(line.split('|')[2]) }) + } + } + return events +} + +function parseHpString(hpStr: string): [number, number] { + if (!hpStr) return [0, 1] + // Remove status suffix like "[1]" + const clean = hpStr.replace(/\[.*\]/, '') + const parts = clean.split('/') + if (parts.length !== 2) return [0, 1] + return [parseInt(parts[0]) || 0, parseInt(parts[1]) || 1] +} + +// ─── Engine ─── + +export type BattleInit = { + battle: any // @pkmn/sim Battle instance + state: BattleState +} + +export function createBattle( + partyCreatures: Creature[], + opponentSpeciesId: SpeciesId, + opponentLevel: number, + _bagItems?: { id: string; count: number }[], +): BattleInit { + const p1Sets = partyCreatures.map(c => creatureToSetString(c)) + const p2Set = wildPokemonToSetString(opponentSpeciesId, opponentLevel) + + const p1Team = Teams.import(p1Sets.join('\n\n')) + const p2Team = Teams.import(p2Set) + + // Create battle + const battle = new Battle({ + formatid: 'gen9customgame' as any, + p1: { name: 'Player', team: p1Team }, + p2: { name: 'Opponent', team: p2Team }, + }) + + // Handle team preview → auto-select leads + battle.makeChoices('team 1', 'team 1') + + // Project initial state + const state = projectState(battle, _bagItems) + return { battle, state } +} + +export function executeTurn( + battleInit: BattleInit, + action: PlayerAction, +): BattleState { + const { battle } = battleInit + const prevLogLen = battle.log.length + + // Build choice string + let p1Choice: string + switch (action.type) { + case 'move': + p1Choice = `move ${action.moveIndex + 1}` + break + case 'switch': + p1Choice = `switch ${action.creatureId}` + break + case 'item': + p1Choice = 'move 1' // Items handled via settlement + break + default: + p1Choice = 'move 1' + } + + // AI choice + const aiPokemon = projectPokemon(battle.p2.active[0]) + const aiMoveIndex = chooseAIMove(aiPokemon) + const p2Choice = `move ${aiMoveIndex + 1}` + + // Execute + battle.makeChoices(p1Choice, p2Choice) + + // Parse new log entries + const newLog = battle.log.slice(prevLogLen) + const newEvents = parseLogToEvents(newLog) + + // Project new state + const state = projectState(battle, battleInit.state.usableItems) + state.events = [...battleInit.state.events, ...newEvents] + + // Check for battle end + if (battle.ended) { + state.finished = true + const winner = battle.winner === 'Player' ? 'player' : 'opponent' + state.result = { + winner, + turns: state.turn, + xpGained: 0, // calculated in settlement + evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 }, + participantIds: [], + } + } + + battleInit.state = state + return state +} + +function projectState(battle: any, bagItems?: { id: string; count: number }[]): BattleState { + const p1 = battle.p1 + const p2 = battle.p2 + + return { + playerPokemon: projectPokemon(p1.active[0]), + opponentPokemon: projectPokemon(p2.active[0]), + playerParty: p1.pokemon.map((p: any) => projectPokemon(p)), + opponentParty: p2.pokemon.map((p: any) => projectPokemon(p)), + turn: battle.turn ?? 1, + events: [], + finished: battle.ended, + usableItems: bagItems?.filter(i => i.count > 0).map(i => ({ id: i.id, name: i.id, count: i.count })) ?? [], + } +} diff --git a/packages/pokemon/src/battle/index.ts b/packages/pokemon/src/battle/index.ts new file mode 100644 index 000000000..807204d59 --- /dev/null +++ b/packages/pokemon/src/battle/index.ts @@ -0,0 +1,4 @@ +export type { BattleState, BattlePokemon, BattleEvent, BattleResult, PlayerAction, MoveOption, StatusCondition } from './types' +export { createBattle, executeTurn, type BattleInit } from './engine' +export { settleBattle, applyMoveLearn, applyEvolution } from './settlement' +export { chooseAIMove } from './ai' diff --git a/packages/pokemon/src/battle/settlement.ts b/packages/pokemon/src/battle/settlement.ts new file mode 100644 index 000000000..63cf22517 --- /dev/null +++ b/packages/pokemon/src/battle/settlement.ts @@ -0,0 +1,167 @@ +import type { Creature, StatName, SpeciesId } from '../types' +import { STAT_NAMES } from '../types' +import { TO_DEX_STAT } from '../data/pkmn' +import type { BattleResult } from './types' +import type { BuddyData } from '../types' +import { addItemToBag, removeItemFromBag } from '../core/storage' +import { xpForLevel, levelFromXp } from '../data/xpTable' +import { getSpeciesData } from '../data/species' +import { Dex } from '@pkmn/sim' + +/** + * Settle battle results: XP, EV, level ups, move learning, evolution detection. + */ +export function settleBattle( + data: BuddyData, + result: BattleResult, + opponentSpeciesId: SpeciesId, + opponentLevel: number, +): { + data: BuddyData + learnableMoves: { creatureId: string; moveId: string; moveName: string }[] + pendingEvolutions: { creatureId: string; from: SpeciesId; to: SpeciesId }[] +} { + if (result.winner !== 'player') { + return { data, learnableMoves: [], pendingEvolutions: [] } + } + + // Calculate XP reward (simplified: base XP from species) + const species = Dex.species.get(opponentSpeciesId) + const baseXp = (species?.baseStats?.hp ?? 50) * opponentLevel / 7 + const xpGained = Math.max(1, Math.floor(baseXp)) + + // Calculate EV reward + const evGained: Record = { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 } + const evYield = getEvYield(opponentSpeciesId) + for (const stat of STAT_NAMES) { + evGained[stat] = evYield[TO_DEX_STAT[stat]] ?? 0 + } + + // Award XP/EV to participant creatures + const learnableMoves: { creatureId: string; moveId: string; moveName: string }[] = [] + const pendingEvolutions: { creatureId: string; from: SpeciesId; to: SpeciesId }[] = [] + const participantIds = new Set(result.participantIds.length > 0 ? result.participantIds : data.party.filter(Boolean)) + + const updatedCreatures = data.creatures.map(creature => { + if (!participantIds.has(creature.id)) return creature + + // Award EVs (capped) + const newEv = { ...creature.ev } + let totalEV = STAT_NAMES.reduce((sum, s) => sum + newEv[s], 0) + for (const stat of STAT_NAMES) { + if (totalEV >= 510) break + const gain = Math.min(evGained[stat], 252 - newEv[stat], 510 - totalEV) + newEv[stat] += gain + totalEV += gain + } + + // Award XP + const oldLevel = creature.level + const newTotalXp = creature.totalXp + xpGained + const species = getSpeciesData(creature.speciesId) + const newLevel = Math.min(100, levelFromXp(newTotalXp, species.growthRate)) + + // Detect new learnable moves (async in real code, but for settlement we check synchronously) + // This will be handled at the UI level with getNewLearnableMoves + + // Detect evolution + if (newLevel > oldLevel) { + const species = Dex.species.get(creature.speciesId) + if (species?.evos?.length) { + const targetId = species.evos[0]!.toLowerCase() + const target = Dex.species.get(targetId) + if (target?.evoLevel && newLevel >= target.evoLevel) { + pendingEvolutions.push({ + creatureId: creature.id, + from: creature.speciesId, + to: targetId as SpeciesId, + }) + } + } + } + + return { + ...creature, + level: newLevel, + totalXp: newTotalXp, + ev: newEv, + } + }) + + // Update data + const updatedData: BuddyData = { + ...data, + creatures: updatedCreatures, + stats: { + ...data.stats, + battlesWon: data.stats.battlesWon + (result.winner === 'player' ? 1 : 0), + battlesLost: data.stats.battlesLost + (result.winner !== 'player' ? 1 : 0), + }, + } + + return { data: updatedData, learnableMoves, pendingEvolutions } +} + +/** + * Apply move learning - replace a move at the given index. + */ +export function applyMoveLearn( + data: BuddyData, + creatureId: string, + moveId: string, + replaceIndex: number, +): BuddyData { + return { + ...data, + creatures: data.creatures.map(c => { + if (c.id !== creatureId) return c + const dexMove = Dex.moves.get(moveId) + const newMoves = [...c.moves] as typeof c.moves + newMoves[replaceIndex] = { + id: moveId, + pp: dexMove?.pp ?? 10, + maxPp: dexMove?.pp ?? 10, + } + return { ...c, moves: newMoves as typeof c.moves } + }), + } +} + +/** + * Apply evolution to a creature. + */ +export function applyEvolution( + data: BuddyData, + creatureId: string, + newSpeciesId: SpeciesId, +): BuddyData { + return { + ...data, + creatures: data.creatures.map(c => + c.id === creatureId + ? { ...c, speciesId: newSpeciesId, friendship: Math.min(255, c.friendship + 10) } + : c, + ), + stats: { + ...data.stats, + totalEvolutions: data.stats.totalEvolutions + 1, + }, + } +} + +function getEvYield(speciesId: string): Record { + // @pkmn/sim Dex.species doesn't have evs field + // Use baseStats as proxy: highest base stat gets 1-2 EVs + const species = Dex.species.get(speciesId) + if (!species?.baseStats) return {} + const stats = species.baseStats as Record + const entries = Object.entries(stats) + if (entries.length === 0) return {} + // Sort by value descending, give 1-2 EV to top stats + entries.sort((a, b) => b[1] - a[1]) + const result: Record = {} + // Top stat gets 2 EVs, second gets 1 + if (entries[0]) result[entries[0][0]] = 2 + if (entries[1]) result[entries[1][0]] = 1 + return result +} diff --git a/packages/pokemon/src/battle/types.ts b/packages/pokemon/src/battle/types.ts new file mode 100644 index 000000000..557a0a395 --- /dev/null +++ b/packages/pokemon/src/battle/types.ts @@ -0,0 +1,68 @@ +import type { StatName, SpeciesId } from '../types' + +export type StatusCondition = 'poison' | 'bad_poison' | 'burn' | 'paralysis' | 'freeze' | 'sleep' | 'none' + +export type BattlePokemon = { + id: string // creature ID + speciesId: SpeciesId + name: string + level: number + hp: number // current HP in battle + maxHp: number + types: string[] + moves: MoveOption[] + ability: string + heldItem: string | null + status: StatusCondition + statStages: Record // -6 to +6 +} + +export type MoveOption = { + id: string + name: string + type: string + pp: number + maxPp: number + disabled: boolean +} + +export type PlayerAction = + | { type: 'move'; moveIndex: number } + | { type: 'switch'; creatureId: string } + | { type: 'item'; itemId: string } + +export type BattleEvent = + | { type: 'move'; side: 'player' | 'opponent'; move: string; user: string } + | { type: 'damage'; side: 'player' | 'opponent'; amount: number; percentage: number } + | { type: 'heal'; side: 'player' | 'opponent'; amount: number; percentage: number } + | { type: 'faint'; side: 'player' | 'opponent'; speciesId: string } + | { type: 'switch'; side: 'player' | 'opponent'; speciesId: string; name: string } + | { type: 'effectiveness'; multiplier: number } + | { type: 'crit' } + | { type: 'miss' } + | { type: 'status'; side: 'player' | 'opponent'; status: StatusCondition } + | { type: 'statChange'; side: 'player' | 'opponent'; stat: string; stages: number } + | { type: 'ability'; side: 'player' | 'opponent'; ability: string } + | { type: 'item'; side: 'player' | 'opponent'; item: string } + | { type: 'fail'; reason: string } + | { type: 'turn'; number: number } + +export type BattleResult = { + winner: 'player' | 'opponent' + turns: number + xpGained: number + evGained: Record + participantIds: string[] +} + +export type BattleState = { + playerPokemon: BattlePokemon + opponentPokemon: BattlePokemon + playerParty: BattlePokemon[] + opponentParty: BattlePokemon[] + turn: number + events: BattleEvent[] + finished: boolean + result?: BattleResult + usableItems: { id: string; name: string; count: number }[] +} diff --git a/packages/pokemon/src/index.ts b/packages/pokemon/src/index.ts index 7967d50af..5465f0e0c 100644 --- a/packages/pokemon/src/index.ts +++ b/packages/pokemon/src/index.ts @@ -36,6 +36,12 @@ export { getNextEvolution, EVOLUTION_CHAINS } from './data/evolution' export { getDefaultMoveset, getDefaultAbility, getNewLearnableMoves } from './data/learnsets' export { FROM_DEX_STAT, TO_DEX_STAT } from './data/pkmn' +// Battle +export type { BattleState, BattlePokemon, BattleEvent, BattleResult, PlayerAction, MoveOption, StatusCondition } from './battle/types' +export { createBattle, executeTurn, type BattleInit } from './battle/engine' +export { settleBattle, applyMoveLearn, applyEvolution } from './battle/settlement' +export { chooseAIMove } from './battle/ai' + // Core export { generateCreature, calculateStats, getCreatureName, recalculateLevel, getActiveCreature, getTotalEV } from './core/creature' export { determineGender, getGenderSymbol } from './core/gender'