Files
claude-code/packages/pokemon/src/ui/BattleFlow.tsx
claude-code-best 100b1589f2 fix: 修复 BattleFlow 进化阶段输入处理 + 清理无用文件
- BattleFlow.tsx: useInput hook 提升到顶层避免 React hooks 规则违规
- 删除未使用的 battle/adapter.ts 和 battle/handler.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 00:47:05 +08:00

254 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Phase>('config')
const [buddyData, setBuddyData] = useState(initialData)
const [battleInit, setBattleInit] = useState<BattleInit | null>(null)
const [battleState, setBattleState] = useState<BattleState | null>(null)
const [opponentSpeciesId, setOpponentSpeciesId] = useState<SpeciesId>('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)
// Evolution phase input — must be at top level (React hooks rule)
useInput((_input: string, key: { return?: boolean }) => {
if (phase === 'evolution' && key.return) {
handleEvolutionConfirm()
}
})
// 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 (
<BattleConfigPanel
party={partyCreatures}
onSubmit={handleStartBattle}
onCancel={onClose}
/>
)
case 'battle': {
if (!battleState) return null
return (
<BattleView
state={battleState}
onAction={handleAction}
/>
)
}
case 'switch': {
if (!battleState) return null
return (
<SwitchPanel
party={partyCreatures}
activeId={battleState.playerPokemon.id}
onSelect={(creatureId) => {
handleAction({ type: 'switch', creatureId })
setPhase('battle')
}}
onCancel={() => setPhase('battle')}
/>
)
}
case 'item': {
if (!battleState) return null
return (
<ItemPanel
items={battleState.usableItems}
onSelect={(itemId) => {
handleAction({ type: 'item', itemId })
setPhase('battle')
}}
onCancel={() => setPhase('battle')}
/>
)
}
case 'result': {
if (!battleState?.result) return null
return (
<BattleResultPanel
result={battleState.result}
playerPokemon={battleState.playerPokemon}
onContinue={handleResultContinue}
/>
)
}
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 (
<MoveLearnPanel
creature={creature}
newMoveId={move.moveId}
replaceIndex={replaceIndex}
onLearn={handleMoveLearn}
onSkip={handleMoveSkip}
onSelectReplace={setReplaceIndex}
/>
)
}
case 'evolution': {
if (pendingEvos.length === 0) return null
const evo = pendingEvos[0]!
useInput(() => {
handleEvolutionConfirm()
})
return (
<Box flexDirection="column" borderStyle="round" paddingX={1}>
<Text bold color="ansi:yellow"> </Text>
<Text> {evo.from} {evo.to}</Text>
<Text color="ansi:white"> [Enter] </Text>
</Box>
)
}
case 'done':
return null
default:
return null
}
}