feat: 新增 /pokemon-battle 独立战斗命令,从 BuddyPanel 移除 Battle tab

- 新增 /pokemon-battle 命令,独立全屏战斗面板
- BattlePanel 在主 app Ink 上下文中使用 useInput,通过 inputRef 转发事件
- BuddyPanel 恢复为 Buddy/Pokédex/Egg 三 tab
- BattleFlow 移除内部 useInput,改为暴露 handleInput 方法

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-22 08:51:04 +08:00
parent bd70971632
commit ea0eee05d0
4 changed files with 93 additions and 47 deletions

View File

@@ -155,6 +155,11 @@ const buddy = feature('BUDDY')
require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js')
).default
: null
const pokemonBattle = feature('BUDDY')
? (
require('./commands/pokemon-battle/index.js') as typeof import('./commands/pokemon-battle/index.js')
).default
: null
const poor = feature('POOR')
? (
require('./commands/poor/index.js') as typeof import('./commands/poor/index.js')
@@ -364,6 +369,7 @@ const COMMANDS = memoize((): Command[] => [
...(webCmd ? [webCmd] : []),
...(forkCmd ? [forkCmd] : []),
...(buddy ? [buddy] : []),
...(pokemonBattle ? [pokemonBattle] : []),
...(poor ? [poor] : []),
...(proactive ? [proactive] : []),
...(monitorCmd ? [monitorCmd] : []),

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useState, useRef } from 'react';
import { useState } 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';
@@ -21,8 +21,6 @@ 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';
@@ -93,13 +91,6 @@ export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps)
onClose={() => onClose('buddy panel closed')}
/>
</Tab>,
<Tab key="battle" title="Battle">
<BattleTab
buddyData={data}
isActive={selectedTab === 'Battle'}
onUpdate={updateData}
/>
</Tab>,
<Tab key="egg" title="Egg">
<EggTab buddyData={data} />
</Tab>,
@@ -622,43 +613,6 @@ function DexTab({
);
}
// ─── Battle Tab ──────────────────────────────────────────
function BattleTab({
buddyData,
isActive,
onUpdate,
}: {
buddyData: BuddyData;
isActive: boolean;
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();
onUpdate(updated);
setBattleKey(k => k + 1);
};
return (
<BattleFlow
key={battleKey}
buddyData={buddyData}
onClose={handleClose}
isActive={isActive}
inputRef={inputRef}
/>
);
}
// ─── Egg Tab ──────────────────────────────────────────
function EggTab({ buddyData }: { buddyData: BuddyData }) {

View File

@@ -0,0 +1,15 @@
import type { Command } from '../../commands.js'
import { isBuddyLive } from '../../buddy/useBuddyNotification.js'
const pokemonBattle = {
type: 'local-jsx',
name: 'pokemon-battle',
description: 'Start a Pokémon battle',
immediate: true,
get isHidden() {
return !isBuddyLive()
},
load: () => import('./pokemon-battle.js'),
} satisfies Command
export default pokemonBattle

View File

@@ -0,0 +1,71 @@
import React, { useState, useRef } from 'react'
import { useInput } from '@anthropic/ink'
import {
loadBuddyData,
saveBuddyData,
getActiveCreature,
BattleFlow,
type BuddyData,
type BattleFlowHandle,
} from '@claude-code-best/pokemon'
import type { ToolUseContext } from '../../Tool.js'
import type {
LocalJSXCommandContext,
LocalJSXCommandOnDone,
} from '../../types/command.js'
async function getOrInitBuddyData(): Promise<BuddyData> {
let data = await loadBuddyData()
if (!getActiveCreature(data)) {
data = await loadBuddyData()
}
return data
}
export async function call(
onDone: LocalJSXCommandOnDone,
_context: ToolUseContext & LocalJSXCommandContext,
_args: string,
): Promise<React.ReactNode> {
const data = await getOrInitBuddyData()
if (!getActiveCreature(data)) {
onDone('No companion yet · run /buddy first', { display: 'system' })
return null
}
return React.createElement(BattlePanel, {
buddyData: data,
onClose: () => {
onDone('battle closed', { display: 'system' })
},
})
}
function BattlePanel({
buddyData,
onClose,
}: {
buddyData: BuddyData
onClose: () => void
}) {
const [battleKey, setBattleKey] = useState(0)
const inputRef = useRef<BattleFlowHandle | null>(null)
useInput((input, key) => {
inputRef.current?.handleInput(input, key)
})
const handleClose = async () => {
const updated = await loadBuddyData()
setBattleKey(k => k + 1)
}
return React.createElement(BattleFlow, {
key: battleKey,
buddyData,
onClose,
isActive: true,
inputRef,
})
}