diff --git a/packages/pokemon/src/__tests__/battle.test.ts b/packages/pokemon/src/__tests__/battle.test.ts index 80fc9d191..241a04026 100644 --- a/packages/pokemon/src/__tests__/battle.test.ts +++ b/packages/pokemon/src/__tests__/battle.test.ts @@ -99,7 +99,7 @@ describe('executeTurn', () => { }) describe('settleBattle', () => { - test('player win increments battlesWon', () => { + test('player win increments battlesWon', async () => { const creature = makeTestCreature() const data: BuddyData = { version: 2, @@ -127,11 +127,11 @@ describe('settleBattle', () => { participantIds: [creature.id], } - const settlement = settleBattle(data, result, 'squirtle', 20) + const settlement = await settleBattle(data, result, 'squirtle', 20) expect(settlement.data.stats.battlesWon).toBe(1) }) - test('player loss returns unchanged data', () => { + test('player loss returns unchanged data', async () => { const creature = makeTestCreature() const data: BuddyData = { version: 2, @@ -159,7 +159,7 @@ describe('settleBattle', () => { participantIds: [creature.id], } - const settlement = settleBattle(data, result, 'squirtle', 20) + const settlement = await settleBattle(data, result, 'squirtle', 20) // Loss early-returns unchanged data expect(settlement.data.creatures[0]!.totalXp).toBe(creature.totalXp) expect(settlement.learnableMoves).toEqual([]) @@ -255,7 +255,7 @@ describe('chooseAIMove', () => { }) describe('settleBattle - advanced', () => { - test('player win awards XP to creature', () => { + test('player win awards XP to creature', async () => { const creature = makeTestCreature({ level: 5 }) const data = makeTestBuddyData([creature]) const result = { @@ -265,11 +265,11 @@ describe('settleBattle - advanced', () => { evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 }, participantIds: [creature.id], } - const settlement = settleBattle(data, result, 'squirtle', 20) + const settlement = await settleBattle(data, result, 'squirtle', 20) expect(settlement.data.creatures[0]!.totalXp).toBeGreaterThan(0) }) - test('player win awards EVs (capped at 252 per stat)', () => { + test('player win awards EVs (capped at 252 per stat)', async () => { const creature = makeTestCreature({ level: 5, ev: { hp: 250, attack: 250, defense: 250, spAtk: 250, spDef: 250, speed: 250 }, @@ -282,13 +282,13 @@ describe('settleBattle - advanced', () => { evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 }, participantIds: [creature.id], } - const settlement = settleBattle(data, result, 'squirtle', 20) + const settlement = await settleBattle(data, result, 'squirtle', 20) for (const stat of ['hp', 'attack', 'defense', 'spAtk', 'spDef', 'speed'] as const) { expect(settlement.data.creatures[0]!.ev[stat]).toBeLessThanOrEqual(252) } }) - test('player loss does not increment battlesWon', () => { + test('player loss does not increment battlesWon', async () => { const creature = makeTestCreature() const data = makeTestBuddyData([creature]) const result = { @@ -298,7 +298,7 @@ describe('settleBattle - advanced', () => { evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 }, participantIds: [creature.id], } - const settlement = settleBattle(data, result, 'squirtle', 20) + const settlement = await settleBattle(data, result, 'squirtle', 20) expect(settlement.data.stats.battlesWon).toBe(0) }) }) diff --git a/packages/pokemon/src/battle/engine.ts b/packages/pokemon/src/battle/engine.ts index c16e57510..15ba5f260 100644 --- a/packages/pokemon/src/battle/engine.ts +++ b/packages/pokemon/src/battle/engine.ts @@ -164,7 +164,7 @@ function parseLogToEvents(log: string[]): BattleEvent[] { } else if (line.startsWith('|-crit|')) { events.push({ type: 'crit' }) } else if (line.startsWith('|-miss|')) { - events.push({ type: 'miss', side } as any) + events.push({ type: 'miss', side }) } else if (line.startsWith('|-status|')) { events.push({ type: 'status', side, status: mapStatus(parts[3]) }) } else if (line.startsWith('|-boost|') || line.startsWith('|-unboost|')) { @@ -235,8 +235,13 @@ export function executeTurn( case 'move': p1Choice = `move ${action.moveIndex + 1}` break - case 'switch': - p1Choice = `switch ${action.creatureId}` + case 'switch': { + // Find the party slot number for this creature (sim uses 1-based index) + const p1Pokemon: any[] = battle.p1.pokemon + const switchIdx = p1Pokemon.findIndex((p: any) => toID(p.name) === action.creatureId || p.name === action.creatureId) + p1Choice = switchIdx >= 0 ? `switch ${switchIdx + 1}` : 'move 1' + break + } break case 'item': p1Choice = 'move 1' // Items handled via settlement diff --git a/packages/pokemon/src/battle/settlement.ts b/packages/pokemon/src/battle/settlement.ts index 9449a6e96..63c578a99 100644 --- a/packages/pokemon/src/battle/settlement.ts +++ b/packages/pokemon/src/battle/settlement.ts @@ -11,16 +11,16 @@ import { Dex } from '@pkmn/sim' /** * Settle battle results: XP, EV, level ups, move learning, evolution detection. */ -export function settleBattle( +export async function settleBattle( data: BuddyData, result: BattleResult, opponentSpeciesId: SpeciesId, opponentLevel: number, -): { +): Promise<{ data: BuddyData learnableMoves: { creatureId: string; moveId: string; moveName: string }[] pendingEvolutions: { creatureId: string; from: SpeciesId; to: SpeciesId }[] -} { +}> { if (result.winner !== 'player') { return { data, learnableMoves: [], pendingEvolutions: [] } } @@ -40,10 +40,14 @@ export function settleBattle( // 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 participantIds = new Set(result.participantIds.length > 0 ? result.participantIds : data.party.filter((id): id is string => id !== null)) - const updatedCreatures = data.creatures.map(creature => { - if (!participantIds.has(creature.id)) return creature + const updatedCreatures: typeof data.creatures = [] + for (const creature of data.creatures) { + if (!participantIds.has(creature.id)) { + updatedCreatures.push(creature) + continue + } // Award EVs (capped) const newEv = { ...creature.ev } @@ -61,8 +65,28 @@ export function settleBattle( 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 new learnable moves on level up + if (newLevel > oldLevel) { + const learnset = await Dex.learnsets.get(creature.speciesId) + if (learnset?.learnset) { + for (const [moveId, sources] of Object.entries(learnset.learnset)) { + for (const src of sources as string[]) { + if (src.startsWith('9L')) { + const moveLevel = parseInt(src.slice(2)) + if (moveLevel > oldLevel && moveLevel <= newLevel) { + const dexMove = Dex.moves.get(moveId) + learnableMoves.push({ + creatureId: creature.id, + moveId, + moveName: dexMove?.name ?? moveId, + }) + } + break + } + } + } + } + } // Detect evolution if (newLevel > oldLevel) { @@ -80,13 +104,13 @@ export function settleBattle( } } - return { + updatedCreatures.push({ ...creature, level: newLevel, totalXp: newTotalXp, ev: newEv, - } - }) + }) + } // Update data const updatedData: BuddyData = { diff --git a/packages/pokemon/src/battle/types.ts b/packages/pokemon/src/battle/types.ts index 557a0a395..1ed735e14 100644 --- a/packages/pokemon/src/battle/types.ts +++ b/packages/pokemon/src/battle/types.ts @@ -39,7 +39,7 @@ export type BattleEvent = | { type: 'switch'; side: 'player' | 'opponent'; speciesId: string; name: string } | { type: 'effectiveness'; multiplier: number } | { type: 'crit' } - | { type: 'miss' } + | { type: 'miss'; side: 'player' | 'opponent' } | { type: 'status'; side: 'player' | 'opponent'; status: StatusCondition } | { type: 'statChange'; side: 'player' | 'opponent'; stat: string; stages: number } | { type: 'ability'; side: 'player' | 'opponent'; ability: string } diff --git a/packages/pokemon/src/ui/BattleFlow.tsx b/packages/pokemon/src/ui/BattleFlow.tsx index f6a0f5f69..04cb20d6e 100644 --- a/packages/pokemon/src/ui/BattleFlow.tsx +++ b/packages/pokemon/src/ui/BattleFlow.tsx @@ -65,7 +65,7 @@ export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps) }, [buddyData]) // Battle phase: handle action - const handleAction = useCallback((action: PlayerAction) => { + const handleAction = useCallback(async (action: PlayerAction) => { if (!battleInit) return const state = executeTurn(battleInit, action) setBattleState(state) @@ -73,7 +73,7 @@ export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps) if (state.finished && state.result) { const participants = buddyData.party.filter((id): id is string => id !== null) const result = { ...state.result, participantIds: participants } - const settled = settleBattle(buddyData, result, opponentSpeciesId, opponentLevel) + const settled = await settleBattle(buddyData, result, opponentSpeciesId, opponentLevel) setBuddyData(settled.data) setPendingMoves(settled.learnableMoves)