diff --git a/packages/pokemon/src/core/spriteCache.ts b/packages/pokemon/src/core/spriteCache.ts index d4b98e8f9..59b3f7f67 100644 --- a/packages/pokemon/src/core/spriteCache.ts +++ b/packages/pokemon/src/core/spriteCache.ts @@ -4,20 +4,14 @@ import type { SpeciesId, SpriteCache } from '../types' import { getSpeciesData } from '../dex/species' import { getSpritesDir } from './storage' -const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/HRKings/pokemonsay-newgenerations/master/pokemons' +const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/claude-code-best/pokemonsay-newgenerations/master/pokemons' -/** Mapping of speciesId to cow file prefix */ -const COW_FILE_MAP: Record = { - bulbasaur: '001_bulbasaur', - ivysaur: '002_ivysaur', - venusaur: '003_venusaur', - charmander: '004_charmander', - charmeleon: '005_charmeleon', - charizard: '006_charizard', - squirtle: '007_squirtle', - wartortle: '008_wartortle', - blastoise: '009_blastoise', - pikachu: '025_pikachu', +/** + * Build cow file name from dex number: NNN.cow + */ +function cowFileName(speciesId: SpeciesId): string { + const { dexNumber } = getSpeciesData(speciesId) + return `${String(dexNumber).padStart(3, '0')}.cow` } /** @@ -32,8 +26,7 @@ export function loadSprite(speciesId: SpeciesId): SpriteCache | null { try { const raw = readFileSync(filePath, 'utf-8') return JSON.parse(raw) as SpriteCache - } catch (e) { - console.error(`[buddy] Failed to load sprite cache for ${speciesId}:`, e) + } catch { return null } } @@ -47,10 +40,8 @@ export async function fetchAndCacheSprite(speciesId: SpeciesId): Promise stripAnsi(l).length)), + width: Math.max(...lines.map(l => stripAnsi(l).length)), height: lines.length, fetchedAt: Date.now(), } @@ -74,8 +65,7 @@ export async function fetchAndCacheSprite(speciesId: SpeciesId): Promise line.trimEnd()) + lines = lines.map(line => line.trimEnd()) return lines } diff --git a/src/commands/buddy/BuddyPanel.tsx b/src/commands/buddy/BuddyPanel.tsx index ade1be720..e0038430a 100644 --- a/src/commands/buddy/BuddyPanel.tsx +++ b/src/commands/buddy/BuddyPanel.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { useState } from 'react'; -import { Box, Text, Pane, Tab, Tabs, useInput, useTabHeaderFocus, type Color } from '@anthropic/ink'; +import { Box, Text, Pane, Tab, Tabs, useInput, type Color } from '@anthropic/ink'; import { useSetAppState } from '../../state/AppState.js'; import { useKeybinding } from '../../keybindings/useKeybinding.js'; import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; @@ -72,6 +72,20 @@ export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps) isActive: true, }); + // Tab / Shift+Tab to switch between tabs + const TAB_ORDER = ['Buddy', 'PC Box', 'Pokédex', 'Egg'] + useInput((_input, key) => { + if (key.tab) { + setSelectedTab(prev => { + const idx = TAB_ORDER.indexOf(prev) + if (key.shift) { + return TAB_ORDER[(idx - 1 + TAB_ORDER.length) % TAB_ORDER.length]! + } + return TAB_ORDER[(idx + 1) % TAB_ORDER.length]! + }) + } + }); + const updateData = (updated: BuddyData) => { setData(updated); saveBuddyData(updated); @@ -100,7 +114,7 @@ export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps) return ( - + {tabs} @@ -125,11 +139,11 @@ function PartyView({ useInput((_input, key) => { if (!isActive) return; - if (_input === 'a' || _input === 'A') { + if (key.leftArrow) { setFocusedSlot(prev => (prev > 0 ? prev - 1 : 5)); setTick(t => t + 1); setStatusMsg(null); - } else if (_input === 'd' || _input === 'D') { + } else if (key.rightArrow) { setFocusedSlot(prev => (prev < 5 ? prev + 1 : 0)); setTick(t => t + 1); setStatusMsg(null); @@ -205,7 +219,7 @@ function PartyView({ {/* Hint */} - a/d navigate · Enter swap · X remove + ←/→ navigate · Enter swap · X remove {/* Selected creature detail — key forces remount on slot change */} @@ -817,7 +831,6 @@ const PARTY_SLOTS = 6 type Panel = 'party' | 'box' function PcBoxTab({ data, onUpdate, isActive }: { data: BuddyData; onUpdate: (d: BuddyData) => void; isActive: boolean }) { - const { headerFocused, blurHeader, focusHeader } = useTabHeaderFocus() const [boxIdx, setBoxIdx] = useState(0) const [panel, setPanel] = useState('box') const [partyCursor, setPartyCursor] = useState(0) // 0-5 @@ -825,9 +838,6 @@ function PcBoxTab({ data, onUpdate, isActive }: { data: BuddyData; onUpdate: (d: const [held, setHeld] = useState<{ id: string; from: Panel; partySlot?: number; boxSlot?: number } | null>(null) const [statusMsg, setStatusMsg] = useState(null) - // Blur header on mount so left/right goes to box grid, not tab switching - React.useEffect(() => { blurHeader() }, [blurHeader]) - const partySet = new Set(data.party.filter((id): id is string => id !== null)) const box = data.boxes[boxIdx]! const boxRows = Math.ceil(BOX_SIZE / BOX_COLS) @@ -839,11 +849,6 @@ function PcBoxTab({ data, onUpdate, isActive }: { data: BuddyData; onUpdate: (d: useInput((_input, key) => { if (!isActive) return - // When header is focused, only handle Tab to give focus back to content - if (headerFocused) { - if (key.tab) { blurHeader() } - return // let Tabs handle left/right - } // Switch box with ,/. regardless of panel if (_input === ',' || _input === '<') {