feat: 添加 BattleFlow 完整键盘输入处理

所有 phase 现在都支持键盘交互:
- config: Enter/1=随机战斗, 2=指定对手, ESC=取消
- configSelect: Enter=确认, ESC=返回
- battle: 1-4=选招, S=换人, I=道具
- switch: 1-6=选队友, ESC=取消
- item: 1-9=选道具, ESC=取消
- result: Enter=继续
- learnMoves: 1-4=替换招式, S=跳过
- evolution: Enter=确认进化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-22 06:07:01 +08:00
parent 0777e1a1f9
commit 3c64113d77

View File

@@ -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 (
<BattleConfigPanel
party={partyCreatures}
party={getPartyCreatures()}
onSubmit={handleStartBattle}
onCancel={onClose}
/>
@@ -175,7 +296,7 @@ export function BattleFlow({ buddyData: initialData, onClose }: BattleFlowProps)
if (!battleState) return null
return (
<SwitchPanel
party={partyCreatures}
party={getPartyCreatures()}
activeId={battleState.playerPokemon.id}
onSelect={(creatureId) => {
handleAction({ type: 'switch', creatureId })