From ea0eee05d06ffb6bf647fe0f27298ad9c4dfffb3 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Wed, 22 Apr 2026 08:51:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20/pokemon-battle=20?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E6=88=98=E6=96=97=E5=91=BD=E4=BB=A4=EF=BC=8C?= =?UTF-8?q?=E4=BB=8E=20BuddyPanel=20=E7=A7=BB=E9=99=A4=20Battle=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /pokemon-battle 命令,独立全屏战斗面板 - BattlePanel 在主 app Ink 上下文中使用 useInput,通过 inputRef 转发事件 - BuddyPanel 恢复为 Buddy/Pokédex/Egg 三 tab - BattleFlow 移除内部 useInput,改为暴露 handleInput 方法 Co-Authored-By: Claude Opus 4.6 --- src/commands.ts | 6 ++ src/commands/buddy/BuddyPanel.tsx | 48 +------------ src/commands/pokemon-battle/index.ts | 15 ++++ src/commands/pokemon-battle/pokemon-battle.ts | 71 +++++++++++++++++++ 4 files changed, 93 insertions(+), 47 deletions(-) create mode 100644 src/commands/pokemon-battle/index.ts create mode 100644 src/commands/pokemon-battle/pokemon-battle.ts diff --git a/src/commands.ts b/src/commands.ts index f0fc6675a..98abcc23b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -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] : []), diff --git a/src/commands/buddy/BuddyPanel.tsx b/src/commands/buddy/BuddyPanel.tsx index 962fdeb6a..3438215bf 100644 --- a/src/commands/buddy/BuddyPanel.tsx +++ b/src/commands/buddy/BuddyPanel.tsx @@ -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')} /> , - - - , , @@ -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(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 ( - - ); -} - // ─── Egg Tab ────────────────────────────────────────── function EggTab({ buddyData }: { buddyData: BuddyData }) { diff --git a/src/commands/pokemon-battle/index.ts b/src/commands/pokemon-battle/index.ts new file mode 100644 index 000000000..d2acd7662 --- /dev/null +++ b/src/commands/pokemon-battle/index.ts @@ -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 diff --git a/src/commands/pokemon-battle/pokemon-battle.ts b/src/commands/pokemon-battle/pokemon-battle.ts new file mode 100644 index 000000000..d4cddbadc --- /dev/null +++ b/src/commands/pokemon-battle/pokemon-battle.ts @@ -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 { + let data = await loadBuddyData() + if (!getActiveCreature(data)) { + data = await loadBuddyData() + } + return data +} + +export async function call( + onDone: LocalJSXCommandOnDone, + _context: ToolUseContext & LocalJSXCommandContext, + _args: string, +): Promise { + 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(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, + }) +}