mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
feat: 又一大波改动
This commit is contained in:
@@ -29,10 +29,15 @@ function creatureToSetString(creature: Creature): string {
|
||||
const natureName = creature.nature.charAt(0).toUpperCase() + creature.nature.slice(1)
|
||||
const abilityName = creature.ability ? (Dex.abilities.get(creature.ability)?.name ?? creature.ability) : ''
|
||||
|
||||
const moves = creature.moves
|
||||
let moves = creature.moves
|
||||
.filter(m => m.id)
|
||||
.map(m => Dex.moves.get(m.id)?.name ?? m.id)
|
||||
|
||||
// Fallback: if no valid moves, use type-based defaults
|
||||
if (moves.length === 0) {
|
||||
moves = getSpeciesMoves(creature.speciesId, creature.level)
|
||||
}
|
||||
|
||||
const DEX_DISPLAY: Record<string, string> = { hp: 'HP', atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe' }
|
||||
const formatStatLine = (vals: Record<string, number>) =>
|
||||
STAT_NAMES.map(s => `${vals[s]} ${DEX_DISPLAY[TO_DEX_STAT[s]]}`).join(' / ')
|
||||
|
||||
@@ -4,7 +4,7 @@ import { STAT_NAMES } from '../types'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { determineGender } from './gender'
|
||||
import { levelFromXp } from '../dex/xpTable'
|
||||
import { gen, TO_DEX_STAT } from '../dex/pkmn'
|
||||
import { gen, TO_DEX_STAT, getSpecies } from '../dex/pkmn'
|
||||
import { getDefaultMoveset, getDefaultAbility } from '../dex/learnsets'
|
||||
import { randomNature } from '../dex/nature'
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function generateCreature(speciesId: SpeciesId, seed?: number): Pro
|
||||
* Handles base stats, IV, EV, level, and nature correction internally.
|
||||
*/
|
||||
export function calculateStats(creature: Creature): StatsResult {
|
||||
const species = gen.species.get(creature.speciesId)
|
||||
const species = getSpecies(creature.speciesId)
|
||||
if (!species) throw new Error(`Species ${creature.speciesId} not found`)
|
||||
|
||||
// Get nature if creature has one (Phase 1 adds nature field)
|
||||
|
||||
@@ -4,22 +4,41 @@ import { EMPTY_MOVE } from '../types'
|
||||
|
||||
const GEN = 9
|
||||
|
||||
/** Get the default moveset for a species at a given level (last 4 level-up moves) */
|
||||
export async function getDefaultMoveset(speciesId: SpeciesId, level: number): Promise<[MoveSlot, MoveSlot, MoveSlot, MoveSlot]> {
|
||||
const learnset = await Dex.learnsets.get(speciesId)
|
||||
if (!learnset?.learnset) return [EMPTY_MOVE, EMPTY_MOVE, EMPTY_MOVE, EMPTY_MOVE]
|
||||
/** Get raw learnset data from Dex.data (synchronous, always available) */
|
||||
function getLearnsetData(speciesId: SpeciesId): Record<string, string[]> | null {
|
||||
const entry = Dex.data.Learnsets[speciesId]
|
||||
return entry?.learnset ?? null
|
||||
}
|
||||
|
||||
const levelUpMoves: { id: string; level: number }[] = []
|
||||
for (const [moveId, sources] of Object.entries(learnset.learnset)) {
|
||||
for (const src of sources as string[]) {
|
||||
if (src.startsWith(`${GEN}L`)) {
|
||||
levelUpMoves.push({ id: moveId, level: parseInt(src.slice(2)) })
|
||||
break
|
||||
/**
|
||||
* Get level-up moves for a species.
|
||||
* Prefers the current gen (9L), falls back to the latest available gen.
|
||||
*/
|
||||
function getLevelUpMoves(learnset: Record<string, string[]>): { id: string; level: number }[] {
|
||||
// Collect level-up moves, preferring highest-gen data per move
|
||||
const moveMap = new Map<string, { id: string; level: number; gen: number }>()
|
||||
for (const [moveId, sources] of Object.entries(learnset)) {
|
||||
for (const src of sources) {
|
||||
const match = src.match(/^(\d+)L(\d+)$/)
|
||||
if (match) {
|
||||
const gen = parseInt(match[1]!)
|
||||
const level = parseInt(match[2]!)
|
||||
const existing = moveMap.get(moveId)
|
||||
if (!existing || gen > existing.gen) {
|
||||
moveMap.set(moveId, { id: moveId, level, gen })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(moveMap.values()).sort((a, b) => a.level - b.level)
|
||||
}
|
||||
|
||||
levelUpMoves.sort((a, b) => a.level - b.level)
|
||||
/** Get the default moveset for a species at a given level (last 4 level-up moves) */
|
||||
export async function getDefaultMoveset(speciesId: SpeciesId, level: number): Promise<[MoveSlot, MoveSlot, MoveSlot, MoveSlot]> {
|
||||
const learnset = getLearnsetData(speciesId)
|
||||
if (!learnset) return [EMPTY_MOVE, EMPTY_MOVE, EMPTY_MOVE, EMPTY_MOVE]
|
||||
|
||||
const levelUpMoves = getLevelUpMoves(learnset)
|
||||
const available = levelUpMoves.filter(m => m.level <= level).slice(-4)
|
||||
|
||||
const slots: MoveSlot[] = available.map(m => {
|
||||
@@ -39,21 +58,14 @@ export function getDefaultAbility(speciesId: SpeciesId): string {
|
||||
|
||||
/** Get newly learnable moves when leveling up */
|
||||
export async function getNewLearnableMoves(speciesId: SpeciesId, oldLevel: number, newLevel: number): Promise<{ id: string; name: string }[]> {
|
||||
const learnset = await Dex.learnsets.get(speciesId)
|
||||
if (!learnset?.learnset) return []
|
||||
const learnset = getLearnsetData(speciesId)
|
||||
if (!learnset) return []
|
||||
|
||||
const result: { id: string; name: string }[] = []
|
||||
for (const [moveId, sources] of Object.entries(learnset.learnset)) {
|
||||
for (const src of sources as string[]) {
|
||||
if (src.startsWith(`${GEN}L`)) {
|
||||
const moveLevel = parseInt(src.slice(2))
|
||||
if (moveLevel > oldLevel && moveLevel <= newLevel) {
|
||||
const dexMove = Dex.moves.get(moveId)
|
||||
result.push({ id: moveId, name: dexMove?.name ?? moveId })
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
const levelUpMoves = getLevelUpMoves(learnset)
|
||||
return levelUpMoves
|
||||
.filter(m => m.level > oldLevel && m.level <= newLevel)
|
||||
.map(m => {
|
||||
const dexMove = Dex.moves.get(m.id)
|
||||
return { id: m.id, name: dexMove?.name ?? m.id }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import type { BattleState, WeatherKind } from '../battle/types'
|
||||
import type { SpeciesId } from '../types'
|
||||
import { loadSprite } from '../core/spriteCache'
|
||||
import { loadSprite, fetchAndCacheSprite } from '../core/spriteCache'
|
||||
import { getFallbackSprite } from '../sprites/fallback'
|
||||
import { HpCard } from './HpCard'
|
||||
import { BattleMenu } from './BattleMenu'
|
||||
@@ -12,11 +12,16 @@ import type { StatusCondition } from '../battle/types'
|
||||
|
||||
export type MenuPhase = 'main' | 'fight' | 'bag' | 'pokemon'
|
||||
|
||||
/** Get sprite lines: try cache → fallback */
|
||||
function getSpriteLines(speciesId: SpeciesId): string[] {
|
||||
/** Hook: get sprite lines with async fetch fallback */
|
||||
function useSpriteLines(speciesId: SpeciesId): string[] {
|
||||
const [tick, setTick] = useState(0)
|
||||
useEffect(() => {
|
||||
if (loadSprite(speciesId)) return
|
||||
fetchAndCacheSprite(speciesId).then(s => { if (s) setTick(t => t + 1) })
|
||||
}, [speciesId])
|
||||
void tick
|
||||
const cached = loadSprite(speciesId)
|
||||
if (cached) return cached.lines
|
||||
return getFallbackSprite(speciesId)
|
||||
return cached?.lines ?? getFallbackSprite(speciesId)
|
||||
}
|
||||
|
||||
interface BattleSceneProps {
|
||||
@@ -51,9 +56,9 @@ export function BattleScene({
|
||||
const opp = state.opponentPokemon
|
||||
const player = state.playerPokemon
|
||||
|
||||
// Load sprite lines (memoized by speciesId)
|
||||
const oppSpriteLines = useMemo(() => getSpriteLines(opp.speciesId as SpeciesId), [opp.speciesId])
|
||||
const playerSpriteLines = useMemo(() => getSpriteLines(player.speciesId as SpeciesId), [player.speciesId])
|
||||
// Load sprite lines (with async fetch for uncached species)
|
||||
const oppSpriteLines = useSpriteLines(opp.speciesId as SpeciesId)
|
||||
const playerSpriteLines = useSpriteLines(player.speciesId as SpeciesId)
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" width="100%">
|
||||
@@ -78,8 +83,8 @@ export function BattleScene({
|
||||
overlay
|
||||
) : (
|
||||
<>
|
||||
{/* Opponent: HP card left, sprite right */}
|
||||
<Box flexDirection="row" justifyContent="space-between">
|
||||
{/* Opponent info */}
|
||||
<Box flexDirection="row" justifyContent="flex-start">
|
||||
<HpCard
|
||||
name={opp.name}
|
||||
level={opp.level}
|
||||
@@ -89,20 +94,33 @@ export function BattleScene({
|
||||
align="left"
|
||||
isOpponent
|
||||
/>
|
||||
<BattleSprite
|
||||
lines={oppSpriteLines}
|
||||
animEnabled={animEnabled}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Player: overlaps opponent area by pulling up */}
|
||||
<Box flexDirection="row" justifyContent="space-between" alignItems="flex-end" marginTop={-10}>
|
||||
<BattleSprite
|
||||
lines={playerSpriteLines}
|
||||
flip
|
||||
phaseOffset={2}
|
||||
animEnabled={animEnabled}
|
||||
/>
|
||||
{/*
|
||||
Keep the overlapping sprites inside a fixed-height battlefield with absolute positioning.
|
||||
Do NOT switch this back to negative margins or normal-flow overlap: Ink/Yoga reflow can leave
|
||||
visual ghosting above the player sprite during animation when overlap affects outer layout.
|
||||
*/}
|
||||
{/* Overlapped battlefield: fixed-height container so overlap won't disturb outer layout */}
|
||||
<Box height={18} marginTop={1} marginBottom={1} overflow="hidden">
|
||||
<Box position="absolute" top={0} right={0}>
|
||||
<BattleSprite
|
||||
lines={oppSpriteLines}
|
||||
animEnabled={animEnabled}
|
||||
/>
|
||||
</Box>
|
||||
<Box position="absolute" bottom={0} left={0}>
|
||||
<BattleSprite
|
||||
lines={playerSpriteLines}
|
||||
flip
|
||||
phaseOffset={2}
|
||||
animEnabled={animEnabled}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Player info */}
|
||||
<Box flexDirection="row" justifyContent="flex-end">
|
||||
<HpCard
|
||||
name={player.name}
|
||||
level={player.level}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
|
||||
import { Box, Text, type Color } from '@anthropic/ink'
|
||||
import type { SpeciesId } from '../types'
|
||||
import { getSpeciesData } from '../dex/species'
|
||||
import { loadSprite } from '../core/spriteCache'
|
||||
import { loadSprite, fetchAndCacheSprite } from '../core/spriteCache'
|
||||
import { getFallbackSprite } from '../sprites/fallback'
|
||||
|
||||
const YELLOW: Color = 'ansi:yellow'
|
||||
@@ -22,8 +22,19 @@ interface EvolutionAnimProps {
|
||||
*/
|
||||
export function EvolutionAnim({ fromSpecies, toSpecies, onComplete }: EvolutionAnimProps) {
|
||||
const [tick, setTick] = useState(0)
|
||||
const [spriteTick, setSpriteTick] = useState(0)
|
||||
const totalFrames = 8
|
||||
|
||||
// Prefetch sprites for both species
|
||||
useEffect(() => {
|
||||
for (const id of [fromSpecies, toSpecies]) {
|
||||
if (!loadSprite(id)) {
|
||||
fetchAndCacheSprite(id).then(s => { if (s) setSpriteTick(t => t + 1) })
|
||||
}
|
||||
}
|
||||
}, [fromSpecies, toSpecies])
|
||||
void spriteTick
|
||||
|
||||
useEffect(() => {
|
||||
if (tick >= totalFrames) {
|
||||
onComplete()
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getCreatureName,
|
||||
getXpProgress,
|
||||
loadSprite,
|
||||
fetchAndCacheSprite,
|
||||
getFallbackSprite,
|
||||
renderAnimatedSprite,
|
||||
getIdleAnimMode,
|
||||
@@ -165,10 +166,19 @@ export function CompanionSprite(): React.ReactNode {
|
||||
const xpInfo = useAppState(s => s.companionXpInfo);
|
||||
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 creatureChangedAt = useAppState(s => s.companionCreatureChangedAt);
|
||||
const setAppState = useSetAppState();
|
||||
const { columns } = useTerminalSize();
|
||||
const [tick, setTick] = useState(0);
|
||||
const [spriteTick, setSpriteTick] = useState(0);
|
||||
|
||||
// Prefetch sprite when creature changes
|
||||
useEffect(() => {
|
||||
const c = getPokemonCreature();
|
||||
if (!c || loadSprite(c.speciesId)) return;
|
||||
fetchAndCacheSprite(c.speciesId).then(s => { if (s) setSpriteTick(t => t + 1) });
|
||||
}, [creatureChangedAt]);
|
||||
void spriteTick;
|
||||
const lastSpokeTick = useRef(0);
|
||||
const [{ petStartTick, forPetAt }, setPetStart] = useState({
|
||||
petStartTick: 0,
|
||||
|
||||
@@ -19,7 +19,7 @@ import { calculateStats, getCreatureName, getTotalEV, getActiveCreature, saveBud
|
||||
import { getXpProgress } from '@claude-code-best/pokemon';
|
||||
|
||||
import { getGenderSymbol } from '@claude-code-best/pokemon';
|
||||
import { StatBar, SpriteAnimator, getFallbackSprite, loadSprite, SpeciesPicker } from '@claude-code-best/pokemon';
|
||||
import { StatBar, SpriteAnimator, getFallbackSprite, loadSprite, fetchAndCacheSprite, SpeciesPicker } from '@claude-code-best/pokemon';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
|
||||
const CYAN: Color = 'ansi:cyan';
|
||||
@@ -136,6 +136,7 @@ function PartyView({
|
||||
const [focusedSlot, setFocusedSlot] = useState(0);
|
||||
const [statusMsg, setStatusMsg] = useState<string | null>(null);
|
||||
const [tick, setTick] = useState(0); // force re-render on navigation
|
||||
const [spriteTick, setSpriteTick] = useState(0); // force re-render after sprite fetch
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (!isActive) return;
|
||||
@@ -171,6 +172,31 @@ function PartyView({
|
||||
? data.creatures.find(c => c.id === focusedCreatureId) ?? null
|
||||
: null;
|
||||
|
||||
// Async-fetch sprite for focused creature if not cached
|
||||
React.useEffect(() => {
|
||||
if (!focusedCreature) return;
|
||||
if (loadSprite(focusedCreature.speciesId)) return;
|
||||
fetchAndCacheSprite(focusedCreature.speciesId).then((sprite) => {
|
||||
if (sprite) setSpriteTick(t => t + 1);
|
||||
});
|
||||
}, [focusedCreature?.speciesId]);
|
||||
|
||||
// Also prefetch sprites for all party members on mount
|
||||
React.useEffect(() => {
|
||||
for (const id of data.party) {
|
||||
if (!id) continue;
|
||||
const c = data.creatures.find(cr => cr.id === id);
|
||||
if (c && !loadSprite(c.speciesId)) {
|
||||
fetchAndCacheSprite(c.speciesId).then((sprite) => {
|
||||
if (sprite) setSpriteTick(t => t + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Consume spriteTick to avoid unused warning
|
||||
void spriteTick;
|
||||
|
||||
// Load sprite for focused creature (not just active)
|
||||
const focusedSprite = focusedCreature
|
||||
? (loadSprite(focusedCreature.speciesId)?.lines ?? getFallbackSprite(focusedCreature.speciesId))
|
||||
@@ -417,6 +443,15 @@ function DexTab({
|
||||
const [focusedId, setFocusedId] = useState<SpeciesId>(buddyData.dex[0]?.speciesId ?? 'bulbasaur');
|
||||
const [dexCursor, setDexCursor] = useState(0);
|
||||
const [statusMsg, setStatusMsg] = useState<string | null>(null);
|
||||
const [dexSpriteTick, setDexSpriteTick] = useState(0);
|
||||
|
||||
// Prefetch sprite for focused dex species
|
||||
React.useEffect(() => {
|
||||
if (!loadSprite(focusedId)) {
|
||||
fetchAndCacheSprite(focusedId).then(s => { if (s) setDexSpriteTick(t => t + 1) });
|
||||
}
|
||||
}, [focusedId]);
|
||||
void dexSpriteTick;
|
||||
|
||||
// Sorted discovered species
|
||||
const discovered = buddyData.dex
|
||||
|
||||
Reference in New Issue
Block a user