mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 00:35:51 +00:00
feat: 又是更新了一大堆
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import type { SpeciesId, Creature } from '../types'
|
import type { SpeciesId, Creature } from '../types'
|
||||||
import { generateCreature, calculateStats, getCreatureName, getTotalEV, recalculateLevel } from '../core/creature'
|
import { generateCreature, calculateStats, getCreatureName, getTotalEV, recalculateLevel } from '../core/creature'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
|
|
||||||
describe('generateCreature', () => {
|
describe('generateCreature', () => {
|
||||||
test('creates a creature with correct defaults', () => {
|
test('creates a creature with correct defaults', () => {
|
||||||
@@ -10,7 +10,7 @@ describe('generateCreature', () => {
|
|||||||
expect(c.level).toBe(1)
|
expect(c.level).toBe(1)
|
||||||
expect(c.xp).toBe(0)
|
expect(c.xp).toBe(0)
|
||||||
expect(c.totalXp).toBe(0)
|
expect(c.totalXp).toBe(0)
|
||||||
expect(c.friendship).toBe(SPECIES_DATA.bulbasaur.baseHappiness)
|
expect(c.friendship).toBe(getSpeciesData('bulbasaur').baseHappiness)
|
||||||
expect(c.isShiny).toBeDefined()
|
expect(c.isShiny).toBeDefined()
|
||||||
expect(c.id).toBeTruthy()
|
expect(c.id).toBeTruthy()
|
||||||
expect(Object.values(c.iv).every((v) => v >= 0 && v <= 31)).toBe(true)
|
expect(Object.values(c.iv).every((v) => v >= 0 && v <= 31)).toBe(true)
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import type { BuddyData } from '../types'
|
|||||||
import { generateCreature } from '../core/creature'
|
import { generateCreature } from '../core/creature'
|
||||||
|
|
||||||
function makeBuddyData(overrides: Partial<BuddyData['stats']> = {}): BuddyData {
|
function makeBuddyData(overrides: Partial<BuddyData['stats']> = {}): BuddyData {
|
||||||
|
const creature = generateCreature('bulbasaur')
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
activeCreatureId: 'test',
|
party: [creature.id, null, null, null, null, null],
|
||||||
creatures: [generateCreature('bulbasaur')],
|
creatures: [creature],
|
||||||
eggs: [],
|
eggs: [],
|
||||||
dex: [{ speciesId: 'bulbasaur', discoveredAt: Date.now(), caughtCount: 1, bestLevel: 1 }],
|
dex: [{ speciesId: 'bulbasaur', discoveredAt: Date.now(), caughtCount: 1, bestLevel: 1 }],
|
||||||
stats: {
|
stats: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import { determineGender, getGenderSymbol } from '../core/gender'
|
import { determineGender, getGenderSymbol } from '../core/gender'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
|
|
||||||
describe('determineGender', () => {
|
describe('determineGender', () => {
|
||||||
test('genderless species', () => {
|
test('genderless species', () => {
|
||||||
@@ -8,12 +8,12 @@ describe('determineGender', () => {
|
|||||||
// Venusaur has genderRate 1 (12.5% female)
|
// Venusaur has genderRate 1 (12.5% female)
|
||||||
// For testing genderless, we'd need a species with genderRate -1
|
// For testing genderless, we'd need a species with genderRate -1
|
||||||
// None in MVP are genderless, so test the basic logic
|
// None in MVP are genderless, so test the basic logic
|
||||||
const pikachu = SPECIES_DATA.pikachu
|
const pikachu = getSpeciesData('pikachu')
|
||||||
expect(pikachu.genderRate).toBe(4)
|
expect(pikachu.genderRate).toBe(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('pikachu 50% female ratio', () => {
|
test('pikachu 50% female ratio', () => {
|
||||||
const pikachu = SPECIES_DATA.pikachu
|
const pikachu = getSpeciesData('pikachu')
|
||||||
let males = 0
|
let males = 0
|
||||||
let females = 0
|
let females = 0
|
||||||
for (let seed = 0; seed < 1000; seed++) {
|
for (let seed = 0; seed < 1000; seed++) {
|
||||||
@@ -27,7 +27,7 @@ describe('determineGender', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('starters are ~12.5% female', () => {
|
test('starters are ~12.5% female', () => {
|
||||||
const bulbasaur = SPECIES_DATA.bulbasaur
|
const bulbasaur = getSpeciesData('bulbasaur')
|
||||||
let females = 0
|
let females = 0
|
||||||
for (let seed = 0; seed < 1000; seed++) {
|
for (let seed = 0; seed < 1000; seed++) {
|
||||||
if (determineGender(bulbasaur, seed) === 'female') females++
|
if (determineGender(bulbasaur, seed) === 'female') females++
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
import type { Creature, SpeciesId, StatName, StatsResult } from '../types'
|
import type { Creature, SpeciesId, StatName, StatsResult } from '../types'
|
||||||
import { STAT_NAMES } from '../types'
|
import { STAT_NAMES } from '../types'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
import { determineGender } from './gender'
|
import { determineGender } from './gender'
|
||||||
import { levelFromXp } from '../data/xpTable'
|
import { levelFromXp } from '../data/xpTable'
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import { levelFromXp } from '../data/xpTable'
|
|||||||
* Generate a new creature of the given species.
|
* Generate a new creature of the given species.
|
||||||
*/
|
*/
|
||||||
export function generateCreature(speciesId: SpeciesId, seed?: number): Creature {
|
export function generateCreature(speciesId: SpeciesId, seed?: number): Creature {
|
||||||
const species = SPECIES_DATA[speciesId]
|
const species = getSpeciesData(speciesId)
|
||||||
const actualSeed = seed ?? Math.floor(Math.random() * 0xffffffff)
|
const actualSeed = seed ?? Math.floor(Math.random() * 0xffffffff)
|
||||||
|
|
||||||
// Generate IVs (0-31) using simple hash from seed
|
// Generate IVs (0-31) using simple hash from seed
|
||||||
@@ -42,7 +42,7 @@ export function generateCreature(speciesId: SpeciesId, seed?: number): Creature
|
|||||||
* Other: floor((2 * base + iv + floor(ev/4)) * level / 100) + 5
|
* Other: floor((2 * base + iv + floor(ev/4)) * level / 100) + 5
|
||||||
*/
|
*/
|
||||||
export function calculateStats(creature: Creature): StatsResult {
|
export function calculateStats(creature: Creature): StatsResult {
|
||||||
const species = SPECIES_DATA[creature.speciesId]
|
const species = getSpeciesData(creature.speciesId)
|
||||||
const level = creature.level
|
const level = creature.level
|
||||||
const result: StatsResult = {} as StatsResult
|
const result: StatsResult = {} as StatsResult
|
||||||
|
|
||||||
@@ -67,14 +67,14 @@ export function calculateStats(creature: Creature): StatsResult {
|
|||||||
*/
|
*/
|
||||||
export function getCreatureName(creature: Creature): string {
|
export function getCreatureName(creature: Creature): string {
|
||||||
if (creature.nickname) return creature.nickname
|
if (creature.nickname) return creature.nickname
|
||||||
return SPECIES_DATA[creature.speciesId].name
|
return getSpeciesData(creature.speciesId).name
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recalculate level from total XP (e.g. after XP gain).
|
* Recalculate level from total XP (e.g. after XP gain).
|
||||||
*/
|
*/
|
||||||
export function recalculateLevel(creature: Creature): Creature {
|
export function recalculateLevel(creature: Creature): Creature {
|
||||||
const species = SPECIES_DATA[creature.speciesId]
|
const species = getSpeciesData(creature.speciesId)
|
||||||
const newLevel = levelFromXp(creature.totalXp, species.growthRate)
|
const newLevel = levelFromXp(creature.totalXp, species.growthRate)
|
||||||
if (newLevel !== creature.level) {
|
if (newLevel !== creature.level) {
|
||||||
return { ...creature, level: newLevel }
|
return { ...creature, level: newLevel }
|
||||||
@@ -84,10 +84,12 @@ export function recalculateLevel(creature: Creature): Creature {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the active creature from buddy data.
|
* Get the active creature from buddy data.
|
||||||
|
* Reads from party[0] (new) with fallback to activeCreatureId (legacy).
|
||||||
*/
|
*/
|
||||||
export function getActiveCreature(buddyData: { activeCreatureId: string | null; creatures: Creature[] }): Creature | null {
|
export function getActiveCreature(buddyData: { party?: (string | null)[]; activeCreatureId?: string | null; creatures: Creature[] }): Creature | null {
|
||||||
if (!buddyData.activeCreatureId) return null
|
const activeId = buddyData.party?.[0] ?? buddyData.activeCreatureId ?? null
|
||||||
return buddyData.creatures.find((c) => c.id === buddyData.activeCreatureId) ?? null
|
if (!activeId) return null
|
||||||
|
return buddyData.creatures.find((c) => c.id === activeId) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
import type { BuddyData, Creature, Egg, SpeciesId } from '../types'
|
import type { BuddyData, Creature, Egg, SpeciesId } from '../types'
|
||||||
import { ALL_SPECIES_IDS } from '../types'
|
import { ALL_SPECIES_IDS } from '../types'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
import { generateCreature } from './creature'
|
import { generateCreature } from './creature'
|
||||||
|
|
||||||
/** Days of consecutive coding needed to be eligible for an egg */
|
/** Days of consecutive coding needed to be eligible for an egg */
|
||||||
@@ -34,7 +34,7 @@ export function generateEgg(buddyData: BuddyData): Egg {
|
|||||||
: starters[Math.floor(Math.random() * starters.length)]
|
: starters[Math.floor(Math.random() * starters.length)]
|
||||||
|
|
||||||
// Steps based on rarity (capture rate: lower = rarer = more steps)
|
// Steps based on rarity (capture rate: lower = rarer = more steps)
|
||||||
const species = SPECIES_DATA[speciesId]
|
const species = getSpeciesData(speciesId)
|
||||||
const baseSteps = Math.floor(2000 + ((255 - species.captureRate) / 255) * 3000)
|
const baseSteps = Math.floor(2000 + ((255 - species.captureRate) / 255) * 3000)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Creature, EvolutionResult, SpeciesId } from '../types'
|
import type { Creature, EvolutionResult, SpeciesId } from '../types'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
import { getNextEvolution } from '../data/evolution'
|
import { getNextEvolution } from '../data/evolution'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,7 +29,7 @@ export function checkEvolution(creature: Creature): EvolutionResult | null {
|
|||||||
* Returns the updated creature with new species and recalculated data.
|
* Returns the updated creature with new species and recalculated data.
|
||||||
*/
|
*/
|
||||||
export function evolve(creature: Creature, targetSpeciesId: SpeciesId): Creature {
|
export function evolve(creature: Creature, targetSpeciesId: SpeciesId): Creature {
|
||||||
const newSpecies = SPECIES_DATA[targetSpeciesId]
|
const newSpecies = getSpeciesData(targetSpeciesId)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...creature,
|
...creature,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { Creature } from '../types'
|
import type { Creature } from '../types'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
import { levelFromXp, xpForLevel } from '../data/xpTable'
|
import { levelFromXp, xpForLevel } from '../data/xpTable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Award XP to a creature. Returns updated creature and whether level up occurred.
|
* Award XP to a creature. Returns updated creature and whether level up occurred.
|
||||||
*/
|
*/
|
||||||
export function awardXP(creature: Creature, amount: number): { creature: Creature; leveledUp: boolean; newLevel: number } {
|
export function awardXP(creature: Creature, amount: number): { creature: Creature; leveledUp: boolean; newLevel: number } {
|
||||||
const species = SPECIES_DATA[creature.speciesId]
|
const species = getSpeciesData(creature.speciesId)
|
||||||
if (creature.level >= 100) {
|
if (creature.level >= 100) {
|
||||||
return { creature, leveledUp: false, newLevel: creature.level }
|
return { creature, leveledUp: false, newLevel: creature.level }
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ export function awardXP(creature: Creature, amount: number): { creature: Creatur
|
|||||||
* Get XP needed to reach next level from current state.
|
* Get XP needed to reach next level from current state.
|
||||||
*/
|
*/
|
||||||
export function getXpProgress(creature: Creature): { current: number; needed: number; percentage: number } {
|
export function getXpProgress(creature: Creature): { current: number; needed: number; percentage: number } {
|
||||||
const species = SPECIES_DATA[creature.speciesId]
|
const species = getSpeciesData(creature.speciesId)
|
||||||
const currentLevelXp = xpForLevel(creature.level, species.growthRate)
|
const currentLevelXp = xpForLevel(creature.level, species.growthRate)
|
||||||
const nextLevelXp = creature.level < 100 ? xpForLevel(creature.level + 1, species.growthRate) : currentLevelXp
|
const nextLevelXp = creature.level < 100 ? xpForLevel(creature.level + 1, species.growthRate) : currentLevelXp
|
||||||
const needed = nextLevelXp - currentLevelXp
|
const needed = nextLevelXp - currentLevelXp
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import type { SpeciesId, SpriteCache } from '../types'
|
import type { SpeciesId, SpriteCache } from '../types'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
import { getSpritesDir } from './storage'
|
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/HRKings/pokemonsay-newgenerations/master/pokemons'
|
||||||
@@ -134,6 +134,6 @@ function stripAnsi(str: string): string {
|
|||||||
* Get species name with dex number for display.
|
* Get species name with dex number for display.
|
||||||
*/
|
*/
|
||||||
export function getSpeciesDisplay(speciesId: SpeciesId): string {
|
export function getSpeciesDisplay(speciesId: SpeciesId): string {
|
||||||
const data = SPECIES_DATA[speciesId]
|
const data = getSpeciesData(speciesId)
|
||||||
return `#${String(data.dexNumber).padStart(3, '0')} ${data.name}`
|
return `#${String(data.dexNumber).padStart(3, '0')} ${data.name}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { homedir } from 'node:os'
|
|||||||
import type { BuddyData, SpeciesId } from '../types'
|
import type { BuddyData, SpeciesId } from '../types'
|
||||||
import { ALL_SPECIES_IDS } from '../types'
|
import { ALL_SPECIES_IDS } from '../types'
|
||||||
import { generateCreature } from './creature'
|
import { generateCreature } from './creature'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
|
|
||||||
const BUDDY_DATA_PATH = join(homedir(), '.claude', 'buddy-data.json')
|
const BUDDY_DATA_PATH = join(homedir(), '.claude', 'buddy-data.json')
|
||||||
const BUDDY_SPRITES_DIR = join(homedir(), '.claude', 'buddy-sprites')
|
const BUDDY_SPRITES_DIR = join(homedir(), '.claude', 'buddy-sprites')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load buddy data from disk. Returns default data if file doesn't exist.
|
* Load buddy data from disk. Returns default data if file doesn't exist.
|
||||||
|
* Auto-migrates legacy data without `party` field.
|
||||||
*/
|
*/
|
||||||
export function loadBuddyData(): BuddyData {
|
export function loadBuddyData(): BuddyData {
|
||||||
if (!existsSync(BUDDY_DATA_PATH)) {
|
if (!existsSync(BUDDY_DATA_PATH)) {
|
||||||
@@ -22,7 +23,8 @@ export function loadBuddyData(): BuddyData {
|
|||||||
if (data.version !== 1) {
|
if (data.version !== 1) {
|
||||||
return migrateData(data)
|
return migrateData(data)
|
||||||
}
|
}
|
||||||
return data
|
// Migrate legacy data without party field
|
||||||
|
return ensurePartyField(data)
|
||||||
} catch {
|
} catch {
|
||||||
return getDefaultBuddyData()
|
return getDefaultBuddyData()
|
||||||
}
|
}
|
||||||
@@ -51,7 +53,7 @@ export function getDefaultBuddyData(): BuddyData {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
activeCreatureId: creature.id,
|
party: [creature.id, null, null, null, null, null],
|
||||||
creatures: [creature],
|
creatures: [creature],
|
||||||
eggs: [],
|
eggs: [],
|
||||||
dex: [
|
dex: [
|
||||||
@@ -122,14 +124,14 @@ export function migrateFromLegacy(
|
|||||||
creature.friendship = 120 // Existing partner bonus
|
creature.friendship = 120 // Existing partner bonus
|
||||||
|
|
||||||
// Preserve nickname if it's not the default
|
// Preserve nickname if it's not the default
|
||||||
const speciesInfo = SPECIES_DATA[speciesId]
|
const speciesInfo = getSpeciesData(speciesId)
|
||||||
if (storedCompanion.name && storedCompanion.name !== speciesInfo.name) {
|
if (storedCompanion.name && storedCompanion.name !== speciesInfo.name) {
|
||||||
creature.nickname = storedCompanion.name
|
creature.nickname = storedCompanion.name
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
activeCreatureId: creature.id,
|
party: [creature.id, null, null, null, null, null],
|
||||||
creatures: [creature],
|
creatures: [creature],
|
||||||
eggs: [],
|
eggs: [],
|
||||||
dex: [
|
dex: [
|
||||||
@@ -204,3 +206,85 @@ export function incrementTurns(data: BuddyData): BuddyData {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure buddy data has a `party` field.
|
||||||
|
* Migrates from legacy `activeCreatureId` if needed.
|
||||||
|
*/
|
||||||
|
function ensurePartyField(data: BuddyData): BuddyData {
|
||||||
|
if (data.party && data.party.length === 6) return data
|
||||||
|
|
||||||
|
// Build party from existing creatures
|
||||||
|
const party: (string | null)[] = new Array(6).fill(null)
|
||||||
|
|
||||||
|
// Put active creature first
|
||||||
|
const activeId = data.activeCreatureId ?? data.party?.[0]
|
||||||
|
if (activeId) {
|
||||||
|
party[0] = activeId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill remaining slots with other creatures
|
||||||
|
let slot = 1
|
||||||
|
for (const c of data.creatures) {
|
||||||
|
if (c.id === activeId) continue
|
||||||
|
if (slot >= 6) break
|
||||||
|
party[slot] = c.id
|
||||||
|
slot++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...data, party }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a creature to the party. Finds first empty slot.
|
||||||
|
* Returns true if added, false if party is full.
|
||||||
|
*/
|
||||||
|
export function addToParty(data: BuddyData, creatureId: string): { data: BuddyData; added: boolean } {
|
||||||
|
const party = [...data.party]
|
||||||
|
const emptyIdx = party.findIndex(p => p === null)
|
||||||
|
if (emptyIdx === -1) {
|
||||||
|
return { data, added: false }
|
||||||
|
}
|
||||||
|
party[emptyIdx] = creatureId
|
||||||
|
return { data: { ...data, party }, added: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a creature from party by slot index.
|
||||||
|
*/
|
||||||
|
export function removeFromParty(data: BuddyData, slotIndex: number): BuddyData {
|
||||||
|
if (slotIndex < 0 || slotIndex >= 6) return data
|
||||||
|
const party = [...data.party]
|
||||||
|
party[slotIndex] = null
|
||||||
|
return { ...data, party }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swap two party slots by index.
|
||||||
|
*/
|
||||||
|
export function swapPartySlots(data: BuddyData, indexA: number, indexB: number): BuddyData {
|
||||||
|
const party = [...data.party]
|
||||||
|
const a = party[indexA]
|
||||||
|
const b = party[indexB]
|
||||||
|
party[indexA] = b
|
||||||
|
party[indexB] = a
|
||||||
|
return { ...data, party }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set party[0] to the given creature ID (by swapping if already in party).
|
||||||
|
*/
|
||||||
|
export function setActivePartyMember(data: BuddyData, creatureId: string): BuddyData {
|
||||||
|
const party = [...data.party]
|
||||||
|
const existingIdx = party.findIndex(id => id === creatureId)
|
||||||
|
if (existingIdx === 0) return data // Already active
|
||||||
|
if (existingIdx > 0) {
|
||||||
|
// Swap with slot 0
|
||||||
|
party[0] = creatureId
|
||||||
|
party[existingIdx] = data.party[0]
|
||||||
|
} else {
|
||||||
|
// Not in party — put in slot 0
|
||||||
|
party[0] = creatureId
|
||||||
|
}
|
||||||
|
return { ...data, party }
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export type {
|
|||||||
export { STAT_NAMES, STAT_LABELS, ALL_SPECIES_IDS } from './types'
|
export { STAT_NAMES, STAT_LABELS, ALL_SPECIES_IDS } from './types'
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
export { SPECIES_DATA, DEX_TO_SPECIES } from './data/species'
|
export { SPECIES_DATA, DEX_TO_SPECIES, getSpeciesData, getAllSpeciesData, ensureSpeciesData, refreshAllSpeciesData } from './data/species'
|
||||||
export { DEFAULT_EV_MAPPING, getEVForTool, MAX_EV_PER_STAT, MAX_EV_TOTAL } from './data/evMapping'
|
export { DEFAULT_EV_MAPPING, getEVForTool, MAX_EV_PER_STAT, MAX_EV_TOTAL } from './data/evMapping'
|
||||||
export { xpForLevel, levelFromXp, xpToNextLevel } from './data/xpTable'
|
export { xpForLevel, levelFromXp, xpToNextLevel } from './data/xpTable'
|
||||||
export { SPECIES_NAMES, SPECIES_I18N, SPECIES_PERSONALITY } from './data/names'
|
export { SPECIES_NAMES, SPECIES_I18N, SPECIES_PERSONALITY } from './data/names'
|
||||||
@@ -32,7 +32,7 @@ export { awardXP, getXpProgress } from './core/experience'
|
|||||||
export { awardEV, awardTurnEV, getEVSummary, resetEVCooldowns } from './core/effort'
|
export { awardEV, awardTurnEV, getEVSummary, resetEVCooldowns } from './core/effort'
|
||||||
export { checkEvolution, evolve, canEvolveFurther } from './core/evolution'
|
export { checkEvolution, evolve, canEvolveFurther } from './core/evolution'
|
||||||
export { checkEggEligibility, generateEgg, advanceEggSteps, isEggReadyToHatch, hatchEgg, EGG_REQUIRED_DAYS } from './core/egg'
|
export { checkEggEligibility, generateEgg, advanceEggSteps, isEggReadyToHatch, hatchEgg, EGG_REQUIRED_DAYS } from './core/egg'
|
||||||
export { loadBuddyData, saveBuddyData, getDefaultBuddyData, migrateFromLegacy, updateDailyStats, incrementTurns } from './core/storage'
|
export { loadBuddyData, saveBuddyData, getDefaultBuddyData, migrateFromLegacy, updateDailyStats, incrementTurns, addToParty, removeFromParty, swapPartySlots, setActivePartyMember } from './core/storage'
|
||||||
export { loadSprite, fetchAndCacheSprite, getSpeciesDisplay } from './core/spriteCache'
|
export { loadSprite, fetchAndCacheSprite, getSpeciesDisplay } from './core/spriteCache'
|
||||||
|
|
||||||
// Sprites
|
// Sprites
|
||||||
|
|||||||
@@ -107,7 +107,9 @@ export type DexEntry = {
|
|||||||
// buddy-data.json complete structure
|
// buddy-data.json complete structure
|
||||||
export type BuddyData = {
|
export type BuddyData = {
|
||||||
version: 1
|
version: 1
|
||||||
activeCreatureId: string | null
|
/** @deprecated Use party[0] instead. Kept for backward compat during migration. */
|
||||||
|
activeCreatureId?: string | null
|
||||||
|
party: (string | null)[] // Always length 6, party[0] = active buddy
|
||||||
creatures: Creature[]
|
creatures: Creature[]
|
||||||
eggs: Egg[]
|
eggs: Egg[]
|
||||||
dex: DexEntry[]
|
dex: DexEntry[]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import { Box, Text, type Color } from '@anthropic/ink'
|
import { Box, Text, type Color } from '@anthropic/ink'
|
||||||
import type { BuddyData, Creature, SpeciesId } from '../types'
|
import type { BuddyData, Creature, SpeciesId } from '../types'
|
||||||
import { STAT_NAMES, STAT_LABELS } from '../types'
|
import { STAT_NAMES, STAT_LABELS } from '../types'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
import { SPECIES_I18N, SPECIES_PERSONALITY } from '../data/names'
|
import { SPECIES_I18N, SPECIES_PERSONALITY } from '../data/names'
|
||||||
import { calculateStats, getCreatureName, getTotalEV } from '../core/creature'
|
import { calculateStats, getCreatureName, getTotalEV } from '../core/creature'
|
||||||
import { getXpProgress } from '../core/experience'
|
import { getXpProgress } from '../core/experience'
|
||||||
@@ -42,7 +42,7 @@ const TYPE_COLORS: Record<string, Color> = {
|
|||||||
* Redesigned companion card with Pokémon-style stats display.
|
* Redesigned companion card with Pokémon-style stats display.
|
||||||
*/
|
*/
|
||||||
export function CompanionCard({ creature, buddyData, spriteLines }: CompanionCardProps) {
|
export function CompanionCard({ creature, buddyData, spriteLines }: CompanionCardProps) {
|
||||||
const species = SPECIES_DATA[creature.speciesId]
|
const species = getSpeciesData(creature.speciesId)
|
||||||
const stats = calculateStats(creature)
|
const stats = calculateStats(creature)
|
||||||
const xp = getXpProgress(creature)
|
const xp = getXpProgress(creature)
|
||||||
const genderSymbol = getGenderSymbol(creature.gender)
|
const genderSymbol = getGenderSymbol(creature.gender)
|
||||||
@@ -66,7 +66,7 @@ export function CompanionCard({ creature, buddyData, spriteLines }: CompanionCar
|
|||||||
|
|
||||||
// Evolution hint
|
// Evolution hint
|
||||||
const evoHint = nextEvo ? (
|
const evoHint = nextEvo ? (
|
||||||
<Text color={GRAY}> → <Text color={CYAN}>{SPECIES_DATA[nextEvo.to].names.zh ?? SPECIES_DATA[nextEvo.to].name}</Text> Lv.{nextEvo.minLevel}</Text>
|
<Text color={GRAY}> → <Text color={CYAN}>{getSpeciesData(nextEvo.to).names.zh ?? getSpeciesData(nextEvo.to).name}</Text> Lv.{nextEvo.minLevel}</Text>
|
||||||
) : null
|
) : null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Box, Text, type Color } from '@anthropic/ink'
|
import { Box, Text, type Color } from '@anthropic/ink'
|
||||||
import type { SpeciesId } from '../types'
|
import type { SpeciesId } from '../types'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
import { loadSprite } from '../core/spriteCache'
|
import { loadSprite } from '../core/spriteCache'
|
||||||
import { getFallbackSprite } from '../sprites/fallback'
|
import { getFallbackSprite } from '../sprites/fallback'
|
||||||
|
|
||||||
@@ -35,8 +35,8 @@ export function EvolutionAnim({ fromSpecies, toSpecies, onComplete }: EvolutionA
|
|||||||
|
|
||||||
const fromSprite = getSpriteLines(fromSpecies)
|
const fromSprite = getSpriteLines(fromSpecies)
|
||||||
const toSprite = getSpriteLines(toSpecies)
|
const toSprite = getSpriteLines(toSpecies)
|
||||||
const fromName = SPECIES_DATA[fromSpecies].name
|
const fromName = getSpeciesData(fromSpecies).name
|
||||||
const toName = SPECIES_DATA[toSpecies].name
|
const toName = getSpeciesData(toSpecies).name
|
||||||
|
|
||||||
// Frame logic:
|
// Frame logic:
|
||||||
// 0-3: old sprite with flash (alternate blank)
|
// 0-3: old sprite with flash (alternate blank)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import { Box, Text, type Color } from '@anthropic/ink'
|
import { Box, Text, type Color } from '@anthropic/ink'
|
||||||
import type { BuddyData, SpeciesId } from '../types'
|
import type { BuddyData, SpeciesId } from '../types'
|
||||||
import { ALL_SPECIES_IDS } from '../types'
|
import { ALL_SPECIES_IDS } from '../types'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
import { getNextEvolution } from '../data/evolution'
|
import { getNextEvolution } from '../data/evolution'
|
||||||
|
|
||||||
const CYAN: Color = 'ansi:cyan'
|
const CYAN: Color = 'ansi:cyan'
|
||||||
@@ -52,7 +52,7 @@ export function PokedexView({ buddyData }: PokedexViewProps) {
|
|||||||
{chains.map((chain, ci) => (
|
{chains.map((chain, ci) => (
|
||||||
<Box key={ci} flexDirection="column" marginTop={ci > 0 ? 0 : 0}>
|
<Box key={ci} flexDirection="column" marginTop={ci > 0 ? 0 : 0}>
|
||||||
{chain.map((speciesId, si) => {
|
{chain.map((speciesId, si) => {
|
||||||
const species = SPECIES_DATA[speciesId]
|
const species = getSpeciesData(speciesId)
|
||||||
const entry = dexMap.get(speciesId)
|
const entry = dexMap.get(speciesId)
|
||||||
const discovered = !!entry
|
const discovered = !!entry
|
||||||
const isActive = buddyData.activeCreatureId
|
const isActive = buddyData.activeCreatureId
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import { Box, Text, type Color } from '@anthropic/ink'
|
import { Box, Text, type Color } from '@anthropic/ink'
|
||||||
import type { SpeciesId, StatName } from '../types'
|
import type { SpeciesId, StatName } from '../types'
|
||||||
import { STAT_NAMES, STAT_LABELS } from '../types'
|
import { STAT_NAMES, STAT_LABELS } from '../types'
|
||||||
import { SPECIES_DATA } from '../data/species'
|
import { getSpeciesData } from '../data/species'
|
||||||
import { SPECIES_PERSONALITY } from '../data/names'
|
import { SPECIES_PERSONALITY } from '../data/names'
|
||||||
import { getNextEvolution } from '../data/evolution'
|
import { getNextEvolution } from '../data/evolution'
|
||||||
import { StatBar } from './StatBar'
|
import { StatBar } from './StatBar'
|
||||||
@@ -32,7 +32,7 @@ interface SpeciesDetailProps {
|
|||||||
* Detailed species info page — base stats, evolution chain, flavor text.
|
* Detailed species info page — base stats, evolution chain, flavor text.
|
||||||
*/
|
*/
|
||||||
export function SpeciesDetail({ speciesId, caughtLevel, spriteLines }: SpeciesDetailProps) {
|
export function SpeciesDetail({ speciesId, caughtLevel, spriteLines }: SpeciesDetailProps) {
|
||||||
const species = SPECIES_DATA[speciesId]
|
const species = getSpeciesData(speciesId)
|
||||||
const nextEvo = getNextEvolution(speciesId)
|
const nextEvo = getNextEvolution(speciesId)
|
||||||
|
|
||||||
// Type badges
|
// Type badges
|
||||||
@@ -163,7 +163,7 @@ function EvolutionChain({ speciesId }: { speciesId: SpeciesId }) {
|
|||||||
<React.Fragment key={sid}>
|
<React.Fragment key={sid}>
|
||||||
{i > 0 && <Text color={GRAY}> → </Text>}
|
{i > 0 && <Text color={GRAY}> → </Text>}
|
||||||
<Text color={sid === speciesId ? CYAN : GRAY} bold={sid === speciesId}>
|
<Text color={sid === speciesId ? CYAN : GRAY} bold={sid === speciesId}>
|
||||||
{SPECIES_DATA[sid].names.zh ?? SPECIES_DATA[sid].name}
|
{getSpeciesData(sid).names.zh ?? getSpeciesData(sid).name}
|
||||||
</Text>
|
</Text>
|
||||||
{i < chain.length - 1 && getNextEvolution(sid) && (
|
{i < chain.length - 1 && getNextEvolution(sid) && (
|
||||||
<Text color={GRAY}> Lv.{getNextEvolution(sid)!.minLevel}</Text>
|
<Text color={GRAY}> Lv.{getNextEvolution(sid)!.minLevel}</Text>
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import {
|
|||||||
loadBuddyData,
|
loadBuddyData,
|
||||||
getActiveCreature,
|
getActiveCreature,
|
||||||
getCreatureName,
|
getCreatureName,
|
||||||
|
getXpProgress,
|
||||||
loadSprite,
|
loadSprite,
|
||||||
getFallbackSprite,
|
getFallbackSprite,
|
||||||
renderAnimatedSprite,
|
renderAnimatedSprite,
|
||||||
getIdleAnimMode,
|
getIdleAnimMode,
|
||||||
SPECIES_DATA,
|
getSpeciesData,
|
||||||
type Creature,
|
type Creature,
|
||||||
type AnimMode,
|
type AnimMode,
|
||||||
} from '@claude-code-best/pokemon';
|
} from '@claude-code-best/pokemon';
|
||||||
@@ -138,7 +139,10 @@ function getAnimatedSpriteLines(creature: Creature, tick: number, mode: AnimMode
|
|||||||
export function CompanionSprite(): React.ReactNode {
|
export function CompanionSprite(): React.ReactNode {
|
||||||
const reaction = useAppState(s => s.companionReaction);
|
const reaction = useAppState(s => s.companionReaction);
|
||||||
const petAt = useAppState(s => s.companionPetAt);
|
const petAt = useAppState(s => s.companionPetAt);
|
||||||
|
const xpInfo = useAppState(s => s.companionXpInfo);
|
||||||
const focused = useAppState(s => s.footerSelection === 'companion');
|
const focused = useAppState(s => s.footerSelection === 'companion');
|
||||||
|
// Subscribe to creature changes so we re-render immediately after switch
|
||||||
|
const _creatureChangedAt = useAppState(s => s.companionCreatureChangedAt);
|
||||||
const setAppState = useSetAppState();
|
const setAppState = useSetAppState();
|
||||||
const { columns } = useTerminalSize();
|
const { columns } = useTerminalSize();
|
||||||
const [tick, setTick] = useState(0);
|
const [tick, setTick] = useState(0);
|
||||||
@@ -179,7 +183,7 @@ export function CompanionSprite(): React.ReactNode {
|
|||||||
const creature = getPokemonCreature();
|
const creature = getPokemonCreature();
|
||||||
if (!creature || getGlobalConfig().companionMuted) return null;
|
if (!creature || getGlobalConfig().companionMuted) return null;
|
||||||
|
|
||||||
const species = SPECIES_DATA[creature.speciesId];
|
const species = getSpeciesData(creature.speciesId);
|
||||||
const name = getCreatureName(creature);
|
const name = getCreatureName(creature);
|
||||||
const color = creature.isShiny ? 'warning' : 'claude';
|
const color = creature.isShiny ? 'warning' : 'claude';
|
||||||
const colWidth = spriteColWidth(stringWidth(name));
|
const colWidth = spriteColWidth(stringWidth(name));
|
||||||
@@ -195,6 +199,13 @@ export function CompanionSprite(): React.ReactNode {
|
|||||||
const quip =
|
const quip =
|
||||||
reaction && reaction.length > NARROW_QUIP_CAP ? reaction.slice(0, NARROW_QUIP_CAP - 1) + '…' : reaction;
|
reaction && reaction.length > NARROW_QUIP_CAP ? reaction.slice(0, NARROW_QUIP_CAP - 1) + '…' : reaction;
|
||||||
const label = quip ? `"${quip}"` : focused ? ` ${name} ` : name;
|
const label = quip ? `"${quip}"` : focused ? ` ${name} ` : name;
|
||||||
|
const xpLabel = xpInfo
|
||||||
|
? xpInfo.leveledUp
|
||||||
|
? ` ↑Lv.${xpInfo.level}`
|
||||||
|
: ` Lv.${xpInfo.level} +${xpInfo.xpGained}xp`
|
||||||
|
: creature.level > 1
|
||||||
|
? ` Lv.${creature.level}`
|
||||||
|
: '';
|
||||||
return (
|
return (
|
||||||
<Box paddingX={1} alignSelf="flex-end">
|
<Box paddingX={1} alignSelf="flex-end">
|
||||||
<Text>
|
<Text>
|
||||||
@@ -211,6 +222,11 @@ export function CompanionSprite(): React.ReactNode {
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
|
{xpLabel && (
|
||||||
|
<Text dimColor bold={xpInfo?.leveledUp} color={xpInfo?.leveledUp ? 'warning' : 'inactive'}>
|
||||||
|
{xpLabel}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -229,6 +245,12 @@ export function CompanionSprite(): React.ReactNode {
|
|||||||
const heartFrame = petting ? PET_HEARTS[petAge % PET_HEARTS.length] : null;
|
const heartFrame = petting ? PET_HEARTS[petAge % PET_HEARTS.length] : null;
|
||||||
const displayLines = heartFrame ? [heartFrame, ...spriteLines] : spriteLines;
|
const displayLines = heartFrame ? [heartFrame, ...spriteLines] : spriteLines;
|
||||||
|
|
||||||
|
const xpStatus = xpInfo
|
||||||
|
? xpInfo.leveledUp
|
||||||
|
? `↑Lv.${xpInfo.level}`
|
||||||
|
: `+${xpInfo.xpGained}xp`
|
||||||
|
: null;
|
||||||
|
|
||||||
const spriteColumn = (
|
const spriteColumn = (
|
||||||
<Box flexDirection="column" flexShrink={0} alignItems="center" width={colWidth}>
|
<Box flexDirection="column" flexShrink={0} alignItems="center" width={colWidth}>
|
||||||
{displayLines.map((line, i) => (
|
{displayLines.map((line, i) => (
|
||||||
@@ -239,6 +261,9 @@ export function CompanionSprite(): React.ReactNode {
|
|||||||
<Text italic bold={focused} dimColor={!focused} color={focused ? color : undefined} inverse={focused}>
|
<Text italic bold={focused} dimColor={!focused} color={focused ? color : undefined} inverse={focused}>
|
||||||
{focused ? ` ${name} ` : name}
|
{focused ? ` ${name} ` : name}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text dimColor color={xpInfo?.leveledUp ? 'warning' : 'inactive'}>
|
||||||
|
Lv.{creature.level} {xpStatus ?? ''}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -260,6 +285,7 @@ export function CompanionSprite(): React.ReactNode {
|
|||||||
// Floating bubble overlay for fullscreen mode
|
// Floating bubble overlay for fullscreen mode
|
||||||
export function CompanionFloatingBubble(): React.ReactNode {
|
export function CompanionFloatingBubble(): React.ReactNode {
|
||||||
const reaction = useAppState(s => s.companionReaction);
|
const reaction = useAppState(s => s.companionReaction);
|
||||||
|
const _creatureChangedAt = useAppState(s => s.companionCreatureChangedAt);
|
||||||
const [{ tick, forReaction }, setTick] = useState({
|
const [{ tick, forReaction }, setTick] = useState({
|
||||||
tick: 0,
|
tick: 0,
|
||||||
forReaction: reaction,
|
forReaction: reaction,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
getActiveCreature,
|
getActiveCreature,
|
||||||
getCreatureName,
|
getCreatureName,
|
||||||
calculateStats,
|
calculateStats,
|
||||||
SPECIES_DATA,
|
getSpeciesData,
|
||||||
type Creature,
|
type Creature,
|
||||||
} from '@claude-code-best/pokemon'
|
} from '@claude-code-best/pokemon'
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ async function callBuddyReactAPI(
|
|||||||
const orgId = getGlobalConfig().oauthAccount?.organizationUuid
|
const orgId = getGlobalConfig().oauthAccount?.organizationUuid
|
||||||
if (!orgId) return null
|
if (!orgId) return null
|
||||||
|
|
||||||
const species = SPECIES_DATA[creature.speciesId]
|
const species = getSpeciesData(creature.speciesId)
|
||||||
const name = getCreatureName(creature)
|
const name = getCreatureName(creature)
|
||||||
const stats = calculateStats(creature)
|
const stats = calculateStats(creature)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
loadBuddyData,
|
loadBuddyData,
|
||||||
getActiveCreature,
|
getActiveCreature,
|
||||||
getCreatureName,
|
getCreatureName,
|
||||||
SPECIES_DATA,
|
getSpeciesData,
|
||||||
} from '@claude-code-best/pokemon'
|
} from '@claude-code-best/pokemon'
|
||||||
|
|
||||||
export function companionIntroText(name: string, species: string): string {
|
export function companionIntroText(name: string, species: string): string {
|
||||||
@@ -26,7 +26,7 @@ export function getCompanionIntroAttachment(
|
|||||||
if (!creature || getGlobalConfig().companionMuted) return []
|
if (!creature || getGlobalConfig().companionMuted) return []
|
||||||
|
|
||||||
const name = getCreatureName(creature)
|
const name = getCreatureName(creature)
|
||||||
const species = SPECIES_DATA[creature.speciesId]
|
const species = getSpeciesData(creature.speciesId)
|
||||||
|
|
||||||
// Skip if already announced for this companion.
|
// Skip if already announced for this companion.
|
||||||
for (const msg of messages ?? []) {
|
for (const msg of messages ?? []) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Box, Text, Pane, Tab, Tabs, 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 { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
||||||
import { Select } from '../../components/CustomSelect/select.js';
|
import { Select } from '../../components/CustomSelect/select.js';
|
||||||
@@ -12,15 +13,15 @@ import {
|
|||||||
type Creature,
|
type Creature,
|
||||||
type SpeciesId,
|
type SpeciesId,
|
||||||
} from '@claude-code-best/pokemon';
|
} from '@claude-code-best/pokemon';
|
||||||
import { SPECIES_DATA } from '@claude-code-best/pokemon';
|
import { getSpeciesData, ensureSpeciesData } from '@claude-code-best/pokemon';
|
||||||
|
|
||||||
import { getNextEvolution } from '@claude-code-best/pokemon';
|
import { getNextEvolution } from '@claude-code-best/pokemon';
|
||||||
import { calculateStats, getCreatureName, getTotalEV, getActiveCreature, saveBuddyData, EGG_REQUIRED_DAYS } from '@claude-code-best/pokemon';
|
import { calculateStats, getCreatureName, getTotalEV, getActiveCreature, saveBuddyData, EGG_REQUIRED_DAYS, addToParty, swapPartySlots, removeFromParty } from '@claude-code-best/pokemon';
|
||||||
import { getXpProgress } from '@claude-code-best/pokemon';
|
import { getXpProgress } from '@claude-code-best/pokemon';
|
||||||
|
|
||||||
import { getGenderSymbol } from '@claude-code-best/pokemon';
|
import { getGenderSymbol } from '@claude-code-best/pokemon';
|
||||||
import { StatBar, SpriteAnimator, getFallbackSprite, loadSprite } from '@claude-code-best/pokemon';
|
import { StatBar, SpriteAnimator, getFallbackSprite, loadSprite } from '@claude-code-best/pokemon';
|
||||||
import type { LocalJSXCommandContext, LocalJSXCommandOnDone } from '../../types/command.js';
|
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||||
|
|
||||||
const CYAN: Color = 'ansi:cyan';
|
const CYAN: Color = 'ansi:cyan';
|
||||||
const YELLOW: Color = 'ansi:yellow';
|
const YELLOW: Color = 'ansi:yellow';
|
||||||
@@ -54,9 +55,15 @@ interface BuddyPanelProps {
|
|||||||
export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps) {
|
export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps) {
|
||||||
const [selectedTab, setSelectedTab] = useState('Buddy');
|
const [selectedTab, setSelectedTab] = useState('Buddy');
|
||||||
const [data, setData] = useState(buddyData);
|
const [data, setData] = useState(buddyData);
|
||||||
|
const setAppState = useSetAppState();
|
||||||
|
|
||||||
useExitOnCtrlCDWithKeybindings();
|
useExitOnCtrlCDWithKeybindings();
|
||||||
|
|
||||||
|
// Trigger species data refresh from API (fire-and-forget)
|
||||||
|
React.useEffect(() => {
|
||||||
|
ensureSpeciesData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
onClose('buddy panel closed');
|
onClose('buddy panel closed');
|
||||||
};
|
};
|
||||||
@@ -66,27 +73,21 @@ export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps)
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const creature = getActiveCreature(data);
|
const updateData = (updated: BuddyData) => {
|
||||||
|
|
||||||
const handleSwitchCreature = (creatureId: string) => {
|
|
||||||
const updated = { ...data, activeCreatureId: creatureId };
|
|
||||||
setData(updated);
|
setData(updated);
|
||||||
saveBuddyData(updated);
|
saveBuddyData(updated);
|
||||||
|
setAppState(prev => ({ ...prev, companionCreatureChangedAt: Date.now() }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
<Tab key="buddy" title="Buddy">
|
<Tab key="buddy" title="Buddy">
|
||||||
{creature ? (
|
<PartyView data={data} onUpdate={updateData} isActive={selectedTab === 'Buddy'} />
|
||||||
<BuddyTab creature={creature} buddyData={data} spriteLines={spriteLines} />
|
|
||||||
) : (
|
|
||||||
<Text color={GRAY}>No buddy yet. Keep coding!</Text>
|
|
||||||
)}
|
|
||||||
</Tab>,
|
</Tab>,
|
||||||
<Tab key="dex" title="Pokédex">
|
<Tab key="dex" title="Pokédex">
|
||||||
<DexTab
|
<DexTab
|
||||||
buddyData={data}
|
buddyData={data}
|
||||||
isActive={selectedTab === 'Pokédex'}
|
isActive={selectedTab === 'Pokédex'}
|
||||||
onSwitchCreature={handleSwitchCreature}
|
onUpdate={updateData}
|
||||||
onClose={() => onClose('buddy panel closed')}
|
onClose={() => onClose('buddy panel closed')}
|
||||||
/>
|
/>
|
||||||
</Tab>,
|
</Tab>,
|
||||||
@@ -104,18 +105,131 @@ export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Buddy Tab ────────────────────────────────────────
|
// ─── Party View (replaces BuddyTab) ─────────────────────
|
||||||
|
|
||||||
function BuddyTab({
|
function PartyView({
|
||||||
|
data,
|
||||||
|
onUpdate,
|
||||||
|
isActive,
|
||||||
|
}: {
|
||||||
|
data: BuddyData;
|
||||||
|
onUpdate: (data: BuddyData) => void;
|
||||||
|
spriteLines?: string[];
|
||||||
|
isActive: boolean;
|
||||||
|
}) {
|
||||||
|
const [focusedSlot, setFocusedSlot] = useState(0);
|
||||||
|
const [statusMsg, setStatusMsg] = useState<string | null>(null);
|
||||||
|
const [tick, setTick] = useState(0); // force re-render on navigation
|
||||||
|
|
||||||
|
useInput((_input, key) => {
|
||||||
|
if (!isActive) return;
|
||||||
|
if (_input === 'a' || _input === 'A') {
|
||||||
|
setFocusedSlot(prev => (prev > 0 ? prev - 1 : 5));
|
||||||
|
setTick(t => t + 1);
|
||||||
|
setStatusMsg(null);
|
||||||
|
} else if (_input === 'd' || _input === 'D') {
|
||||||
|
setFocusedSlot(prev => (prev < 5 ? prev + 1 : 0));
|
||||||
|
setTick(t => t + 1);
|
||||||
|
setStatusMsg(null);
|
||||||
|
} else if (key.return) {
|
||||||
|
if (focusedSlot === 0) {
|
||||||
|
setStatusMsg('This is your active buddy!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updated = swapPartySlots(data, 0, focusedSlot);
|
||||||
|
onUpdate(updated);
|
||||||
|
setStatusMsg('Swapped with active buddy!');
|
||||||
|
} else if (_input === 'x' || _input === 'X') {
|
||||||
|
const creatureId = data.party[focusedSlot];
|
||||||
|
if (!creatureId) return;
|
||||||
|
const updated = removeFromParty(data, focusedSlot);
|
||||||
|
onUpdate(updated);
|
||||||
|
setStatusMsg('Removed from party.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resolve creature for the focused slot (tick forces re-read)
|
||||||
|
const _tick = tick; // reference tick to avoid unused warning
|
||||||
|
const focusedCreatureId = data.party[focusedSlot];
|
||||||
|
const focusedCreature = focusedCreatureId
|
||||||
|
? data.creatures.find(c => c.id === focusedCreatureId) ?? null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Load sprite for focused creature (not just active)
|
||||||
|
const focusedSprite = focusedCreature
|
||||||
|
? (loadSprite(focusedCreature.speciesId)?.lines ?? getFallbackSprite(focusedCreature.speciesId))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{/* Party slots row */}
|
||||||
|
<Box flexDirection="row" justifyContent="center">
|
||||||
|
{data.party.map((creatureId, i) => {
|
||||||
|
const creature = creatureId ? data.creatures.find(c => c.id === creatureId) : null;
|
||||||
|
const isActiveSlot = i === 0;
|
||||||
|
const isFocused = i === focusedSlot;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={i} flexDirection="column" alignItems="center" width={14} marginX={0}>
|
||||||
|
<Box borderStyle={isFocused ? 'round' : undefined} borderColor={isFocused ? CYAN : undefined} paddingX={1}>
|
||||||
|
<Text>
|
||||||
|
{isActiveSlot && !isFocused && <Text color={YELLOW}>★</Text>}
|
||||||
|
{isFocused && <Text color={CYAN}>▸</Text>}
|
||||||
|
{creature ? (
|
||||||
|
<Text bold={isFocused} color={isFocused ? CYAN : GRAY}>
|
||||||
|
{getCreatureName(creature).length > 8
|
||||||
|
? getCreatureName(creature).slice(0, 7) + '…'
|
||||||
|
: getCreatureName(creature)}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text color={GRAY}>---</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Text color={creature ? GRAY : undefined} dimColor={!creature}>
|
||||||
|
{creature ? `Lv.${creature.level}` : ' '}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Status message */}
|
||||||
|
{statusMsg && (
|
||||||
|
<Box justifyContent="center">
|
||||||
|
<Text color={GRAY} italic>{statusMsg}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Hint */}
|
||||||
|
<Box justifyContent="center">
|
||||||
|
<Text color={GRAY} dimColor>a/d navigate · Enter swap · X remove</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Selected creature detail — key forces remount on slot change */}
|
||||||
|
{focusedCreature ? (
|
||||||
|
<CreatureDetail key={focusedCreature.id} creature={focusedCreature} spriteLines={focusedSprite} isActive={data.party[0] === focusedCreature.id} />
|
||||||
|
) : (
|
||||||
|
<Box flexDirection="column" alignItems="center" marginTop={1}>
|
||||||
|
<Text color={GRAY} italic>Empty slot — add from Pokédex tab</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Creature Detail ─────────────────────────────────────
|
||||||
|
|
||||||
|
function CreatureDetail({
|
||||||
creature,
|
creature,
|
||||||
buddyData,
|
|
||||||
spriteLines,
|
spriteLines,
|
||||||
|
isActive,
|
||||||
}: {
|
}: {
|
||||||
creature: Creature;
|
creature: Creature;
|
||||||
buddyData: BuddyData;
|
|
||||||
spriteLines?: string[];
|
spriteLines?: string[];
|
||||||
|
isActive: boolean;
|
||||||
}) {
|
}) {
|
||||||
const species = SPECIES_DATA[creature.speciesId];
|
const species = getSpeciesData(creature.speciesId);
|
||||||
const stats = calculateStats(creature);
|
const stats = calculateStats(creature);
|
||||||
const xp = getXpProgress(creature);
|
const xp = getXpProgress(creature);
|
||||||
const genderSymbol = getGenderSymbol(creature.gender);
|
const genderSymbol = getGenderSymbol(creature.gender);
|
||||||
@@ -137,7 +251,7 @@ function BuddyTab({
|
|||||||
const evoHint = nextEvo ? (
|
const evoHint = nextEvo ? (
|
||||||
<Text color={GRAY}>
|
<Text color={GRAY}>
|
||||||
{' '}
|
{' '}
|
||||||
→ <Text color={CYAN}>{SPECIES_DATA[nextEvo.to].names.zh ?? SPECIES_DATA[nextEvo.to].name}</Text> Lv.
|
→ <Text color={CYAN}>{getSpeciesData(nextEvo.to).names.zh ?? getSpeciesData(nextEvo.to).name}</Text> Lv.
|
||||||
{nextEvo.minLevel}
|
{nextEvo.minLevel}
|
||||||
</Text>
|
</Text>
|
||||||
) : null;
|
) : null;
|
||||||
@@ -151,6 +265,7 @@ function BuddyTab({
|
|||||||
<Text color={GRAY}> #{String(species.dexNumber).padStart(3, '0')}</Text>
|
<Text color={GRAY}> #{String(species.dexNumber).padStart(3, '0')}</Text>
|
||||||
{shinyBadge}
|
{shinyBadge}
|
||||||
<Text bold> Lv.{creature.level}</Text>
|
<Text bold> Lv.{creature.level}</Text>
|
||||||
|
{isActive && <Text color={YELLOW}> ★ Active</Text>}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
@@ -253,38 +368,38 @@ function BuddyTab({
|
|||||||
function DexTab({
|
function DexTab({
|
||||||
buddyData,
|
buddyData,
|
||||||
isActive,
|
isActive,
|
||||||
onSwitchCreature,
|
onUpdate,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
buddyData: BuddyData;
|
buddyData: BuddyData;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
onSwitchCreature: (creatureId: string) => void;
|
onUpdate: (data: BuddyData) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const dexMap = new Map(buddyData.dex.map(d => [d.speciesId, d]));
|
const dexMap = new Map(buddyData.dex.map(d => [d.speciesId, d]));
|
||||||
const collected = buddyData.dex.length;
|
const collected = buddyData.dex.length;
|
||||||
const total = ALL_SPECIES_IDS.length;
|
const total = ALL_SPECIES_IDS.length;
|
||||||
const flatSpecies = groupByChain().flat();
|
const flatSpecies = groupByChain().flat();
|
||||||
|
const partySet = new Set(buddyData.party.filter((id): id is string => id !== null));
|
||||||
|
|
||||||
const [focusedId, setFocusedId] = useState<SpeciesId>(flatSpecies[0]);
|
const [focusedId, setFocusedId] = useState<SpeciesId>(flatSpecies[0]);
|
||||||
|
const [statusMsg, setStatusMsg] = useState<string | null>(null);
|
||||||
|
|
||||||
// Build options for the Select component
|
// Build options for the Select component
|
||||||
const options = flatSpecies.map(speciesId => {
|
const options = flatSpecies.map(speciesId => {
|
||||||
const species = SPECIES_DATA[speciesId];
|
const species = getSpeciesData(speciesId);
|
||||||
const entry = dexMap.get(speciesId);
|
const entry = dexMap.get(speciesId);
|
||||||
const discovered = !!entry;
|
const discovered = !!entry;
|
||||||
const isActiveCreature = buddyData.activeCreatureId
|
const inParty = buddyData.creatures.some(c => partySet.has(c.id) && c.speciesId === speciesId);
|
||||||
? buddyData.creatures.some(c => c.id === buddyData.activeCreatureId && c.speciesId === speciesId)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: (
|
label: (
|
||||||
<Text>
|
<Text>
|
||||||
<Text color={GRAY}>#{String(species.dexNumber).padStart(3, '0')} </Text>
|
<Text color={GRAY}>#{String(species.dexNumber).padStart(3, '0')} </Text>
|
||||||
<Text color={discovered ? WHITE : GRAY} bold={isActiveCreature}>
|
<Text color={discovered ? WHITE : GRAY} bold={inParty}>
|
||||||
{discovered ? (species.names.zh ?? species.name) : '???'}
|
{discovered ? (species.names.zh ?? species.name) : '???'}
|
||||||
</Text>
|
</Text>
|
||||||
{isActiveCreature && <Text color={YELLOW}> ★</Text>}
|
{inParty && <Text color={YELLOW}> ★</Text>}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
value: speciesId,
|
value: speciesId,
|
||||||
@@ -293,13 +408,11 @@ function DexTab({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Right panel data
|
// Right panel data
|
||||||
const focusedSpecies = SPECIES_DATA[focusedId];
|
const focusedSpecies = getSpeciesData(focusedId);
|
||||||
const focusedEntry = dexMap.get(focusedId);
|
const focusedEntry = dexMap.get(focusedId);
|
||||||
const focusedDiscovered = !!focusedEntry;
|
const focusedDiscovered = !!focusedEntry;
|
||||||
const focusedOwned = buddyData.creatures.find(c => c.speciesId === focusedId);
|
const focusedOwned = buddyData.creatures.find(c => c.speciesId === focusedId);
|
||||||
const focusedIsActive = buddyData.activeCreatureId
|
const focusedInParty = focusedOwned ? partySet.has(focusedOwned.id) : false;
|
||||||
? buddyData.creatures.some(c => c.id === buddyData.activeCreatureId && c.speciesId === focusedId)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
const spriteLines = focusedDiscovered
|
const spriteLines = focusedDiscovered
|
||||||
? (loadSprite(focusedId)?.lines ?? getFallbackSprite(focusedId))
|
? (loadSprite(focusedId)?.lines ?? getFallbackSprite(focusedId))
|
||||||
@@ -307,6 +420,25 @@ function DexTab({
|
|||||||
|
|
||||||
const maxBase = 130;
|
const maxBase = 130;
|
||||||
|
|
||||||
|
const handleAddToParty = (speciesId: SpeciesId) => {
|
||||||
|
const creature = buddyData.creatures.find(c => c.speciesId === speciesId);
|
||||||
|
if (!creature) return;
|
||||||
|
|
||||||
|
// Already in party?
|
||||||
|
if (partySet.has(creature.id)) {
|
||||||
|
setStatusMsg('Already in party!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = addToParty(buddyData, creature.id);
|
||||||
|
if (result.added) {
|
||||||
|
onUpdate(result.data);
|
||||||
|
setStatusMsg(`Added ${getCreatureName(creature)} to party!`);
|
||||||
|
} else {
|
||||||
|
setStatusMsg('Party is full! Remove a member first.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -328,13 +460,8 @@ function DexTab({
|
|||||||
<Box width={20}>
|
<Box width={20}>
|
||||||
<Select
|
<Select
|
||||||
options={options}
|
options={options}
|
||||||
onFocus={(value: SpeciesId) => setFocusedId(value)}
|
onFocus={(value: SpeciesId) => { setFocusedId(value); setStatusMsg(null); }}
|
||||||
onChange={(value: SpeciesId) => {
|
onChange={(value: SpeciesId) => handleAddToParty(value)}
|
||||||
const creature = buddyData.creatures.find(c => c.speciesId === value);
|
|
||||||
if (creature && creature.id !== buddyData.activeCreatureId) {
|
|
||||||
onSwitchCreature(creature.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
visibleOptionCount={flatSpecies.length}
|
visibleOptionCount={flatSpecies.length}
|
||||||
hideIndexes
|
hideIndexes
|
||||||
@@ -421,7 +548,7 @@ function DexTab({
|
|||||||
<React.Fragment key={sid}>
|
<React.Fragment key={sid}>
|
||||||
{i > 0 && <Text color={GRAY}> → </Text>}
|
{i > 0 && <Text color={GRAY}> → </Text>}
|
||||||
<Text color={sid === focusedId ? CYAN : GRAY} bold={sid === focusedId}>
|
<Text color={sid === focusedId ? CYAN : GRAY} bold={sid === focusedId}>
|
||||||
{SPECIES_DATA[sid].names.zh ?? SPECIES_DATA[sid].name}
|
{getSpeciesData(sid).names.zh ?? getSpeciesData(sid).name}
|
||||||
</Text>
|
</Text>
|
||||||
{next && <Text color={GRAY}> Lv.{next.minLevel}</Text>}
|
{next && <Text color={GRAY}> Lv.{next.minLevel}</Text>}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@@ -441,11 +568,13 @@ function DexTab({
|
|||||||
|
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<Box marginTop={0}>
|
<Box marginTop={0}>
|
||||||
{focusedOwned ? (
|
{statusMsg ? (
|
||||||
focusedIsActive ? (
|
<Text color={GREEN} italic>{statusMsg}</Text>
|
||||||
<Text color={GREEN}>★ Current buddy</Text>
|
) : focusedOwned ? (
|
||||||
|
focusedInParty ? (
|
||||||
|
<Text color={GREEN}>★ In party</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text color={CYAN}>Enter → switch to this buddy</Text>
|
<Text color={CYAN}>Enter → add to party</Text>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Text color={GRAY}>Not owned</Text>
|
<Text color={GRAY}>Not owned</Text>
|
||||||
|
|||||||
@@ -23,9 +23,13 @@ import {
|
|||||||
fetchAndCacheSprite,
|
fetchAndCacheSprite,
|
||||||
loadSprite,
|
loadSprite,
|
||||||
getFallbackSprite,
|
getFallbackSprite,
|
||||||
SPECIES_DATA,
|
getSpeciesData,
|
||||||
|
generateCreature,
|
||||||
|
addToParty,
|
||||||
|
ALL_SPECIES_IDS,
|
||||||
type BuddyData,
|
type BuddyData,
|
||||||
type Creature,
|
type Creature,
|
||||||
|
type SpeciesId,
|
||||||
} from '@claude-code-best/pokemon'
|
} from '@claude-code-best/pokemon'
|
||||||
import { BuddyPanel } from './BuddyPanel.js'
|
import { BuddyPanel } from './BuddyPanel.js'
|
||||||
|
|
||||||
@@ -36,8 +40,8 @@ import { BuddyPanel } from './BuddyPanel.js'
|
|||||||
function getOrInitBuddyData(): BuddyData {
|
function getOrInitBuddyData(): BuddyData {
|
||||||
let data = loadBuddyData()
|
let data = loadBuddyData()
|
||||||
|
|
||||||
// If no active creature, check for legacy companion to migrate
|
// If no active creature (party empty), check for legacy companion to migrate
|
||||||
if (!data.activeCreatureId || data.creatures.length === 0) {
|
if (!getActiveCreature(data) || data.creatures.length === 0) {
|
||||||
const legacyCompanion = getGlobalConfig().companion
|
const legacyCompanion = getGlobalConfig().companion
|
||||||
if (legacyCompanion) {
|
if (legacyCompanion) {
|
||||||
data = migrateFromLegacy(legacyCompanion)
|
data = migrateFromLegacy(legacyCompanion)
|
||||||
@@ -147,39 +151,63 @@ export async function call(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── /buddy switch — switch active creature ──
|
// ── /buddy give-me-pokemon <species> [level] — admin: grant any Pokémon ──
|
||||||
if (sub === 'switch') {
|
if (sub.startsWith('give-me-pokemon')) {
|
||||||
const data = getOrInitBuddyData()
|
const parts = sub.split(/\s+/)
|
||||||
if (data.creatures.length <= 1) {
|
const speciesArg = parts[1]?.toLowerCase()
|
||||||
onDone('You only have one buddy!', { display: 'system' })
|
const levelArg = parts[2] ? parseInt(parts[2], 10) : undefined
|
||||||
return null
|
|
||||||
}
|
if (!speciesArg) {
|
||||||
const lines = data.creatures.map((c, i) => {
|
const available = ALL_SPECIES_IDS.join(', ')
|
||||||
const name = getCreatureName(c)
|
onDone(`Usage: /buddy give-me-pokemon <species> [level]\nAvailable: ${available}`, { display: 'system' })
|
||||||
const species = SPECIES_DATA[c.speciesId]
|
|
||||||
const active = c.id === data.activeCreatureId ? ' ← active' : ''
|
|
||||||
return `${i + 1}. ${name} (${species.names.zh ?? species.name}) Lv.${c.level}${active}`
|
|
||||||
})
|
|
||||||
onDone(
|
|
||||||
['Switch buddy:', ...lines, '', 'Use: /buddy switch <number>'].join('\n'),
|
|
||||||
{ display: 'system' },
|
|
||||||
)
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub.startsWith('switch ')) {
|
// Validate species (match by partial name or full id)
|
||||||
const num = parseInt(sub.slice(7).trim(), 10)
|
const match = ALL_SPECIES_IDS.find(id =>
|
||||||
const data = getOrInitBuddyData()
|
id === speciesArg || id.includes(speciesArg),
|
||||||
if (isNaN(num) || num < 1 || num > data.creatures.length) {
|
)
|
||||||
onDone('Invalid number. Use /buddy switch to see list.', {
|
if (!match) {
|
||||||
display: 'system',
|
onDone(`Unknown species "${speciesArg}". Available: ${ALL_SPECIES_IDS.join(', ')}`, { display: 'system' })
|
||||||
})
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const creature = data.creatures[num - 1]!
|
|
||||||
data.activeCreatureId = creature.id
|
const data = getOrInitBuddyData()
|
||||||
|
|
||||||
|
// Create the creature
|
||||||
|
const creature = generateCreature(match)
|
||||||
|
if (levelArg && !isNaN(levelArg) && levelArg >= 1 && levelArg <= 100) {
|
||||||
|
creature.level = levelArg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to creatures and dex
|
||||||
|
data.creatures.push(creature)
|
||||||
|
const existingDex = data.dex.find(d => d.speciesId === match)
|
||||||
|
if (existingDex) {
|
||||||
|
existingDex.caughtCount++
|
||||||
|
existingDex.bestLevel = Math.max(existingDex.bestLevel, creature.level)
|
||||||
|
} else {
|
||||||
|
data.dex.push({
|
||||||
|
speciesId: match,
|
||||||
|
discoveredAt: Date.now(),
|
||||||
|
caughtCount: 1,
|
||||||
|
bestLevel: creature.level,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to add to party (first empty slot)
|
||||||
|
const partyResult = addToParty(data, creature.id)
|
||||||
|
if (partyResult.added) {
|
||||||
|
Object.assign(data, partyResult.data)
|
||||||
|
}
|
||||||
|
// If party full, creature stays in creatures[] but not in party
|
||||||
|
|
||||||
saveBuddyData(data)
|
saveBuddyData(data)
|
||||||
onDone(`Switched to ${getCreatureName(creature)}!`, { display: 'system' })
|
setState?.(prev => ({ ...prev, companionCreatureChangedAt: Date.now() }))
|
||||||
|
|
||||||
|
const species = getSpeciesData(match)
|
||||||
|
const name = creature.nickname ?? species.name
|
||||||
|
onDone(`Got ${name} (${species.names.zh ?? species.name}) Lv.${creature.level}!`, { display: 'system' })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3482,6 +3482,21 @@ export function REPL({
|
|||||||
// 3. Award conversation XP
|
// 3. Award conversation XP
|
||||||
const _xpResult = _awardXP(_evolved, 5 + _toolNames.length);
|
const _xpResult = _awardXP(_evolved, 5 + _toolNames.length);
|
||||||
_data.creatures = _data.creatures.map((c: any) => (c.id === _creature.id ? _xpResult.creature : c));
|
_data.creatures = _data.creatures.map((c: any) => (c.id === _creature.id ? _xpResult.creature : c));
|
||||||
|
// 3b. Update companion XP info for status display
|
||||||
|
{
|
||||||
|
const { getXpProgress: _getXp } = await import('@claude-code-best/pokemon');
|
||||||
|
const _prog = _getXp(_xpResult.creature);
|
||||||
|
setAppState(prev => ({
|
||||||
|
...prev,
|
||||||
|
companionXpInfo: {
|
||||||
|
level: _xpResult.newLevel,
|
||||||
|
xpGained: 5 + _toolNames.length,
|
||||||
|
xpCurrent: _prog.current,
|
||||||
|
xpNeeded: _prog.needed,
|
||||||
|
leveledUp: _xpResult.leveledUp,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
// 4. Advance egg steps
|
// 4. Advance egg steps
|
||||||
if (_data.eggs.length > 0) {
|
if (_data.eggs.length > 0) {
|
||||||
_data.eggs = _data.eggs.map((e: any) => _advSteps(e, 3));
|
_data.eggs = _data.eggs.map((e: any) => _advSteps(e, 3));
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ export type AppState = DeepImmutable<{
|
|||||||
companionEvolving?: { from: string; to: string }
|
companionEvolving?: { from: string; to: string }
|
||||||
// Egg steps update counter (triggers UI refresh)
|
// Egg steps update counter (triggers UI refresh)
|
||||||
companionEggSteps?: number
|
companionEggSteps?: number
|
||||||
|
// XP info for companion status display (set after each turn)
|
||||||
|
companionXpInfo?: { level: number; xpGained: number; xpCurrent: number; xpNeeded: number; leveledUp: boolean }
|
||||||
|
// Timestamp when active creature was switched — triggers CompanionSprite refresh
|
||||||
|
companionCreatureChangedAt?: number
|
||||||
// TODO (ashwin): see if we can use utility-types DeepReadonly for this
|
// TODO (ashwin): see if we can use utility-types DeepReadonly for this
|
||||||
mcp: {
|
mcp: {
|
||||||
clients: MCPServerConnection[]
|
clients: MCPServerConnection[]
|
||||||
|
|||||||
Reference in New Issue
Block a user