mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 16:55:51 +00:00
feat: 添加 BattleFlow 战斗状态机组件 (Phase 3)
实现完整的战斗 UI 流程:配置 → 战斗 → 换人/道具 → 结果 → 学招 → 进化 → 完成 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
246
packages/pokemon/src/ui/BattleFlow.tsx
Normal file
246
packages/pokemon/src/ui/BattleFlow.tsx
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user