feat: 添加 BattleFlow 战斗状态机组件 (Phase 3)

实现完整的战斗 UI 流程:配置 → 战斗 → 换人/道具 → 结果 → 学招 → 进化 → 完成

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-22 00:38:17 +08:00
parent f5a97011e8
commit e3570f8cdb

View File

@@ -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<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)
// 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
}
}