diff --git a/packages/pokemon/src/ui/BattleFlow.tsx b/packages/pokemon/src/ui/BattleFlow.tsx new file mode 100644 index 000000000..c829eeade --- /dev/null +++ b/packages/pokemon/src/ui/BattleFlow.tsx @@ -0,0 +1,246 @@ +import React, { useState, useCallback } from 'react' +import { Box, Text, useInput } from '@anthropic/ink' +import type { BuddyData, Creature, SpeciesId } from '../types' +import { getActiveCreature, getCreatureName } from '../core/creature' +import { saveBuddyData } from '../core/storage' +import { createBattle, executeTurn, type BattleInit } from '../battle/engine' +import { settleBattle, applyMoveLearn, applyEvolution } from '../battle/settlement' +import { BattleConfigPanel } from './BattleConfigPanel' +import { BattleView } from './BattleView' +import { SwitchPanel } from './SwitchPanel' +import { ItemPanel } from './ItemPanel' +import { BattleResultPanel } from './BattleResultPanel' +import { MoveLearnPanel } from './MoveLearnPanel' +import type { BattleState, PlayerAction } from '../battle/types' + +type Phase = + | 'config' + | 'battle' + | 'switch' + | 'item' + | 'result' + | 'learnMoves' + | 'evolution' + | 'done' + +interface BattleFlowProps { + buddyData: BuddyData + onClose: () => void +} + +export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps) { + const [phase, setPhase] = useState('config') + const [buddyData, setBuddyData] = useState(initialData) + const [battleInit, setBattleInit] = useState(null) + const [battleState, setBattleState] = useState(null) + const [opponentSpeciesId, setOpponentSpeciesId] = useState('pikachu') + const [opponentLevel, setOpponentLevel] = useState(5) + const [pendingMoves, setPendingMoves] = useState<{ creatureId: string; moveId: string; moveName: string }[]>([]) + const [pendingEvos, setPendingEvos] = useState<{ creatureId: string; from: SpeciesId; to: SpeciesId }[]>([]) + const [replaceIndex, setReplaceIndex] = useState(0) + + // Config phase: start battle + const handleStartBattle = useCallback((speciesId: SpeciesId, level: number) => { + setOpponentSpeciesId(speciesId) + setOpponentLevel(level) + + const creatures = 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) + + if (creatures.length === 0) return + + const bagItems = buddyData.bag.items + const init = createBattle(creatures, speciesId, level, bagItems) + setBattleInit(init) + setBattleState(init.state) + setPhase('battle') + }, [buddyData]) + + // Battle phase: handle action + const handleAction = useCallback((action: PlayerAction) => { + if (!battleInit) return + const state = executeTurn(battleInit, action) + setBattleState(state) + + 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) + + setBuddyData(settled.data) + setPendingMoves(settled.learnableMoves) + setPendingEvos(settled.pendingEvolutions) + setBattleState({ ...state, result }) + setPhase('result') + } + }, [battleInit, buddyData, opponentSpeciesId, opponentLevel]) + + // Result phase: continue to move learning + const handleResultContinue = useCallback(() => { + if (pendingMoves.length > 0) { + setPhase('learnMoves') + } else if (pendingEvos.length > 0) { + setPhase('evolution') + } else { + saveBuddyData(buddyData) + setPhase('done') + onClose() + } + }, [pendingMoves, pendingEvos, buddyData, onClose]) + + // Move learning + const handleMoveLearn = useCallback((idx: number) => { + if (pendingMoves.length === 0) return + const move = pendingMoves[0]! + const updated = applyMoveLearn(buddyData, move.creatureId, move.moveId, idx) + setBuddyData(updated) + const remaining = pendingMoves.slice(1) + setPendingMoves(remaining) + if (remaining.length === 0) { + if (pendingEvos.length > 0) { + setPhase('evolution') + } else { + saveBuddyData(updated) + setPhase('done') + onClose() + } + } + }, [pendingMoves, pendingEvos, buddyData, onClose]) + + const handleMoveSkip = useCallback(() => { + const remaining = pendingMoves.slice(1) + setPendingMoves(remaining) + if (remaining.length === 0) { + if (pendingEvos.length > 0) { + setPhase('evolution') + } else { + saveBuddyData(buddyData) + setPhase('done') + onClose() + } + } + }, [pendingMoves, pendingEvos, buddyData, onClose]) + + // Evolution + const handleEvolutionConfirm = useCallback(() => { + if (pendingEvos.length === 0) return + const evo = pendingEvos[0]! + const updated = applyEvolution(buddyData, evo.creatureId, evo.to) + setBuddyData(updated) + const remaining = pendingEvos.slice(1) + setPendingEvos(remaining) + if (remaining.length === 0) { + saveBuddyData(updated) + setPhase('done') + onClose() + } + }, [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': + return ( + + ) + + case 'battle': { + if (!battleState) return null + return ( + + ) + } + + case 'switch': { + if (!battleState) return null + return ( + { + handleAction({ type: 'switch', creatureId }) + setPhase('battle') + }} + onCancel={() => setPhase('battle')} + /> + ) + } + + case 'item': { + if (!battleState) return null + return ( + { + handleAction({ type: 'item', itemId }) + setPhase('battle') + }} + onCancel={() => setPhase('battle')} + /> + ) + } + + case 'result': { + if (!battleState?.result) return null + return ( + + ) + } + + case 'learnMoves': { + if (pendingMoves.length === 0) return null + const move = pendingMoves[0]! + const creature = buddyData.creatures.find(c => c.id === move.creatureId) + if (!creature) return null + return ( + + ) + } + + case 'evolution': { + if (pendingEvos.length === 0) return null + const evo = pendingEvos[0]! + useInput(() => { + handleEvolutionConfirm() + }) + return ( + + 进化! + {evo.from} 正在进化为 {evo.to}! + [Enter] 继续 + + ) + } + + case 'done': + return null + + default: + return null + } +}