mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
fix: Battle 快捷键通过 inputRef 转发至主 Ink 上下文
BattleFlow 不再直接调用 useInput(外部包的 Ink 上下文可能不同), 改为暴露 handleInput 方法,由 BuddyPanel 中的 BattleTab 通过 useInput + ref 转发键盘事件,确保在正确的 StdinContext 中工作。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -78,3 +78,4 @@ export { ItemPanel } from './ui/ItemPanel'
|
||||
export { BattleResultPanel } from './ui/BattleResultPanel'
|
||||
export { MoveLearnPanel } from './ui/MoveLearnPanel'
|
||||
export { BattleFlow } from './ui/BattleFlow'
|
||||
export type { BattleFlowHandle } from './ui/BattleFlow'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { Box, Text, useInput } from '@anthropic/ink'
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import type { BuddyData, Creature, SpeciesId } from '../types'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
@@ -26,13 +26,18 @@ type Phase =
|
||||
| 'evolution'
|
||||
| 'done'
|
||||
|
||||
export interface BattleFlowHandle {
|
||||
handleInput: (input: string, key: { escape?: boolean; return?: boolean; upArrow?: boolean; downArrow?: boolean }) => void
|
||||
}
|
||||
|
||||
interface BattleFlowProps {
|
||||
buddyData: BuddyData
|
||||
onClose: () => void
|
||||
isActive?: boolean
|
||||
inputRef?: React.MutableRefObject<BattleFlowHandle | null>
|
||||
}
|
||||
|
||||
export function BattleFlow({ buddyData: initialData, onClose, isActive = true }: BattleFlowProps) {
|
||||
export function BattleFlow({ buddyData: initialData, onClose, isActive = true, inputRef }: BattleFlowProps) {
|
||||
const [phase, setPhase] = useState<Phase>('config')
|
||||
const [buddyData, setBuddyData] = useState(initialData)
|
||||
const [battleInit, setBattleInit] = useState<BattleInit | null>(null)
|
||||
@@ -44,112 +49,6 @@ export function BattleFlow({ buddyData: initialData, onClose, isActive = true }:
|
||||
const [replaceIndex, setReplaceIndex] = useState(0)
|
||||
const [speciesIndex, setSpeciesIndex] = useState(0)
|
||||
|
||||
// ─── Input handling ───
|
||||
|
||||
useInput((input: string, key: { escape?: boolean; return?: boolean; upArrow?: boolean; downArrow?: boolean }) => {
|
||||
// Config phase: Enter = random battle, ESC = cancel
|
||||
if (!isActive) return
|
||||
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 {
|
||||
@@ -274,6 +173,111 @@ export function BattleFlow({ buddyData: initialData, onClose, isActive = true }:
|
||||
}
|
||||
}, [pendingEvos, buddyData, onClose])
|
||||
|
||||
// ─── Input handler (called externally via inputRef) ───
|
||||
|
||||
const handleInput = useCallback((input: string, key: { escape?: boolean; return?: boolean; upArrow?: boolean; downArrow?: boolean }) => {
|
||||
if (!isActive) return
|
||||
|
||||
if (phase === 'config') {
|
||||
if (key.escape) {
|
||||
onClose()
|
||||
} else if (key.return || input === '1') {
|
||||
handleRandomBattle()
|
||||
} else if (input === '2') {
|
||||
setPhase('configSelect')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (phase === 'configSelect') {
|
||||
if (key.escape) {
|
||||
setPhase('config')
|
||||
} else if (key.upArrow) {
|
||||
const idx = speciesIndex > 0 ? speciesIndex - 1 : ALL_SPECIES_IDS.length - 1
|
||||
setSpeciesIndex(idx)
|
||||
setOpponentSpeciesId(ALL_SPECIES_IDS[idx]!)
|
||||
} else if (key.downArrow) {
|
||||
const idx = speciesIndex < ALL_SPECIES_IDS.length - 1 ? speciesIndex + 1 : 0
|
||||
setSpeciesIndex(idx)
|
||||
setOpponentSpeciesId(ALL_SPECIES_IDS[idx]!)
|
||||
} else if (key.return) {
|
||||
handleStartBattle(opponentSpeciesId, buddyData.party[0] ? getActiveCreatureLevel() : 5)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (phase === 'battle') {
|
||||
if (key.escape) 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (phase === 'result') {
|
||||
if (key.return) handleResultContinue()
|
||||
return
|
||||
}
|
||||
|
||||
if (phase === 'learnMoves') {
|
||||
if (input.toLowerCase() === 's') {
|
||||
handleMoveSkip()
|
||||
} else if (input >= '1' && input <= '4') {
|
||||
const idx = parseInt(input) - 1
|
||||
setReplaceIndex(idx)
|
||||
handleMoveLearn(idx)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (phase === 'evolution') {
|
||||
if (key.return) handleEvolutionConfirm()
|
||||
return
|
||||
}
|
||||
}, [isActive, phase, speciesIndex, opponentSpeciesId, buddyData, battleState, battleInit, pendingMoves, pendingEvos, onClose, handleRandomBattle, handleStartBattle, handleAction, handleResultContinue, handleMoveLearn, handleMoveSkip, handleEvolutionConfirm])
|
||||
|
||||
// Expose handleInput via ref
|
||||
useEffect(() => {
|
||||
if (inputRef) inputRef.current = { handleInput }
|
||||
}, [handleInput, inputRef])
|
||||
|
||||
// Render by phase
|
||||
switch (phase) {
|
||||
case 'config':
|
||||
@@ -286,14 +290,13 @@ export function BattleFlow({ buddyData: initialData, onClose, isActive = true }:
|
||||
)
|
||||
|
||||
case 'configSelect': {
|
||||
const species = getSpeciesData(opponentSpeciesId)
|
||||
const selectedIdx = ALL_SPECIES_IDS.indexOf(opponentSpeciesId)
|
||||
const startIdx = Math.max(0, Math.min(selectedIdx, ALL_SPECIES_IDS.length - 5))
|
||||
const visibleSpecies = ALL_SPECIES_IDS.slice(startIdx, startIdx + 5)
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Text bold color="ansi:cyan"> 选择对手 </Text>
|
||||
{visibleSpecies.map((sid, i) => {
|
||||
{visibleSpecies.map((sid) => {
|
||||
const s = getSpeciesData(sid)
|
||||
const isSelected = sid === opponentSpeciesId
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { Box, Text, Pane, Tab, Tabs, useInput, type Color } from '@anthropic/ink';
|
||||
import { useSetAppState } from '../../state/AppState.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
@@ -22,6 +22,7 @@ import { getXpProgress } from '@claude-code-best/pokemon';
|
||||
import { getGenderSymbol } from '@claude-code-best/pokemon';
|
||||
import { StatBar, SpriteAnimator, getFallbackSprite, loadSprite } from '@claude-code-best/pokemon';
|
||||
import { BattleFlow, loadBuddyData } from '@claude-code-best/pokemon';
|
||||
import type { BattleFlowHandle } from '@claude-code-best/pokemon';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
|
||||
const CYAN: Color = 'ansi:cyan';
|
||||
@@ -633,6 +634,13 @@ function BattleTab({
|
||||
onUpdate: (data: BuddyData) => void;
|
||||
}) {
|
||||
const [battleKey, setBattleKey] = useState(0);
|
||||
const inputRef = useRef<BattleFlowHandle | null>(null);
|
||||
|
||||
// Handle input here (in main app's Ink context) and forward to BattleFlow via ref
|
||||
useInput((input, key) => {
|
||||
if (!isActive) return;
|
||||
inputRef.current?.handleInput(input, key);
|
||||
});
|
||||
|
||||
const handleClose = async () => {
|
||||
const updated = await loadBuddyData();
|
||||
@@ -646,6 +654,7 @@ function BattleTab({
|
||||
buddyData={buddyData}
|
||||
onClose={handleClose}
|
||||
isActive={isActive}
|
||||
inputRef={inputRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user