diff --git a/packages/pokemon/src/ui/BattleFlow.tsx b/packages/pokemon/src/ui/BattleFlow.tsx index 04cb20d6e..1de91b5aa 100644 --- a/packages/pokemon/src/ui/BattleFlow.tsx +++ b/packages/pokemon/src/ui/BattleFlow.tsx @@ -1,6 +1,7 @@ import React, { useState, useCallback } from 'react' import { Box, Text, useInput } from '@anthropic/ink' import type { BuddyData, Creature, SpeciesId } from '../types' +import { ALL_SPECIES_IDS } from '../types' import { saveBuddyData } from '../core/storage' import { createBattle, executeTurn, type BattleInit } from '../battle/engine' import { settleBattle, applyMoveLearn, applyEvolution } from '../battle/settlement' @@ -10,10 +11,12 @@ import { SwitchPanel } from './SwitchPanel' import { ItemPanel } from './ItemPanel' import { BattleResultPanel } from './BattleResultPanel' import { MoveLearnPanel } from './MoveLearnPanel' +import { chooseAIMove } from '../battle/ai' import type { BattleState, PlayerAction } from '../battle/types' type Phase = | 'config' + | 'configSelect' | 'battle' | 'switch' | 'item' @@ -38,13 +41,136 @@ export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps) const [pendingEvos, setPendingEvos] = useState<{ creatureId: string; from: SpeciesId; to: SpeciesId }[]>([]) const [replaceIndex, setReplaceIndex] = useState(0) - // Evolution phase input — must be at top level (React hooks rule) - useInput((_input: string, key: { return?: boolean }) => { - if (phase === 'evolution' && key.return) { - handleEvolutionConfirm() + // ─── Input handling ─── + + useInput((input: string, key: { escape?: boolean; return?: boolean; upArrow?: boolean; downArrow?: boolean }) => { + // Config phase: Enter = random battle, ESC = cancel + if (phase === 'config') { + if (key.escape) { + onClose() + } else if (key.return || input === '1') { + handleRandomBattle() + } else if (input === '2') { + setPhase('configSelect') + } + return + } + + // Config select: pick species by number + if (phase === 'configSelect') { + if (key.escape) { + setPhase('config') + } else if (key.return) { + handleStartBattle(opponentSpeciesId, buddyData.party[0] ? getActiveCreatureLevel() : 5) + } + return + } + + // Battle phase: 1-4 = move, S = switch, I = item, ESC = cancel + if (phase === 'battle') { + if (key.escape) { + // Can't flee from wild battle - do nothing + return + } + if (input >= '1' && input <= '4') { + const idx = parseInt(input) - 1 + if (battleState && idx < battleState.playerPokemon.moves.length) { + handleAction({ type: 'move', moveIndex: idx }) + } + } else if (input.toLowerCase() === 's') { + setPhase('switch') + } else if (input.toLowerCase() === 'i') { + setPhase('item') + } + return + } + + // Switch phase: 1-6 = select, ESC = cancel + if (phase === 'switch') { + if (key.escape) { + setPhase('battle') + } else if (input >= '1' && input <= '6') { + const idx = parseInt(input) - 1 + const partyCreatures = getPartyCreatures() + if (battleState && partyCreatures[idx] && partyCreatures[idx]!.id !== battleState.playerPokemon.id) { + handleAction({ type: 'switch', creatureId: partyCreatures[idx]!.id }) + setPhase('battle') + } + } + return + } + + // Item phase: 1-9 = select item, ESC = cancel + if (phase === 'item') { + if (key.escape) { + setPhase('battle') + } else if (input >= '1' && input <= '9') { + if (battleState) { + const idx = parseInt(input) - 1 + const items = battleState.usableItems + if (items[idx]) { + handleAction({ type: 'item', itemId: items[idx]!.id }) + setPhase('battle') + } + } + } + return + } + + // Result phase: Enter = continue + if (phase === 'result') { + if (key.return) { + handleResultContinue() + } + return + } + + // Move learn phase: 1-4 = replace, S = skip + if (phase === 'learnMoves') { + if (input.toLowerCase() === 's') { + handleMoveSkip() + } else if (input >= '1' && input <= '4') { + const idx = parseInt(input) - 1 + setReplaceIndex(idx) + handleMoveLearn(idx) + } + return + } + + // Evolution phase: Enter = confirm + if (phase === 'evolution') { + if (key.return) { + handleEvolutionConfirm() + } + return } }) + // ─── Helpers ─── + + function getActiveCreatureLevel(): number { + const id = buddyData.party[0] + if (!id) return 5 + const c = buddyData.creatures.find(cr => cr.id === id) + return c?.level ?? 5 + } + + function getPartyCreatures(): Creature[] { + return buddyData.party + .filter((id): id is string => id !== null) + .map(id => buddyData.creatures.find(c => c.id === id)) + .filter((c): c is Creature => c !== undefined) + } + + // ─── Actions ─── + + const handleRandomBattle = useCallback(() => { + const opponentLevel = getActiveCreatureLevel() + const speciesList = ALL_SPECIES_IDS + const randomSpecies = speciesList[Math.floor(Math.random() * speciesList.length)]! + handleStartBattle(randomSpecies, opponentLevel) + }, [buddyData]) + // Config phase: start battle const handleStartBattle = useCallback((speciesId: SpeciesId, level: number) => { setOpponentSpeciesId(speciesId) @@ -144,18 +270,13 @@ export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps) } }, [pendingEvos, buddyData, onClose]) - // Switch: convert BattlePokemon to Creature for SwitchPanel - const partyCreatures = buddyData.party - .filter((id): id is string => id !== null) - .map(id => buddyData.creatures.find(c => c.id === id)) - .filter((c): c is Creature => c !== undefined) - // Render by phase switch (phase) { case 'config': + case 'configSelect': return ( @@ -175,7 +296,7 @@ export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps) if (!battleState) return null return ( { handleAction({ type: 'switch', creatureId })