chore: 删除已完成的计划文件 (Phase 0-3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-22 03:02:15 +08:00
parent 98284a5908
commit df61bf3852
4 changed files with 0 additions and 774 deletions

View File

@@ -1,189 +0,0 @@
# Phase 0: 清除重复 — 委托 @pkmn
## 目标
删除所有与 @pkmn 重复的硬编码数据和手写公式,统一委托给 @pkmn 生态。**零新功能,纯重构。**
## 设计原则
先清后建。先消除重复代码,再在干净基础上添加新功能。此 Phase 不引入任何新类型或新功能。
## 改动
| 文件 | 操作 | 说明 |
|------|------|------|
| `types.ts` | 删除 `NATURES` 常量27 行) | `Dex.natures` 已有完整数据 |
| `data/nature.ts` | 重写 | `ALL_NATURE_NAMES` → 遍历 `Dex.natures``getNatureEffect()` → 查询 `Dex.natures.get()` |
| `data/evolution.ts` | 重写 | `EVOLUTION_CHAINS``Dex.species.get(id).evos`/`.evoLevel` 动态查询 |
| `core/creature.ts` | 重写 `calculateStats()` | 手写公式 → `gen.stats.calc()`Nature ±10% 自动处理) |
| `data/species.ts` | 简化 `buildEvolutionChain()` | 已部分使用 Dex.evos统一用新 `getNextEvolution()` |
| `data/pkmn.ts` | 统一 `gens` 单例 | 导出 `gen``TO_DEX_STAT` 映射,消除多处重复创建 `Generations` |
## 使用 @pkmn 包
- `@pkmn/sim`Dex.natures, Dex.species
- `@pkmn/data`stats.calc, Generations
## 保留不变
- `types.ts` 中的 `NatureName`/`NatureStat`/`NatureEffect` 类型定义Creature 类型约束需要)
- `data/species.ts``SUPPLEMENT`growthRate/captureRate/flavorText 不在 Dex 中)
- `data/names.ts`i18n 多语言名称)
- `data/xpTable.ts``data/evMapping.ts`(完全自定义)
- `core/` 其他文件、`ui/``sprites/`
## 详细实现
### 1. types.ts — 删除 NATURES
删除 `types.ts``NATURES` 常量(约 27 行手写数据)。保留 `NatureName``NatureStat``NatureEffect` 类型。
### 2. data/nature.ts — 委托 Dex.natures
```typescript
import { Dex } from '@pkmn/sim'
import type { NatureName, NatureEffect, NatureStat } from '../types'
export function getAllNatureNames(): NatureName[] {
const names: NatureName[] = []
for (const nature of Dex.natures) {
if (nature.exists) names.push(nature.id as NatureName)
}
return names
}
export function randomNature(): NatureName {
const names = getAllNatureNames()
return names[Math.floor(Math.random() * names.length)]!
}
export function getNatureEffect(nature: NatureName): NatureEffect {
const n = Dex.natures.get(nature)
if (!n?.exists) return { plus: null, minus: null }
return {
plus: (n.plus as NatureStat | undefined) ?? null,
minus: (n.minus as NatureStat | undefined) ?? null,
}
}
```
### 3. data/evolution.ts — 委托 Dex.species
```typescript
import { Dex } from '@pkmn/sim'
import type { SpeciesId } from '../types'
export interface EvolutionChainStep {
from: SpeciesId
to: SpeciesId
trigger: 'level_up' | 'item' | 'trade' | 'friendship'
minLevel?: number
}
/** 查找物种的下一个进化(从 Dex 动态获取) */
export function getNextEvolution(speciesId: SpeciesId): EvolutionChainStep | undefined {
const dex = Dex.species.get(speciesId)
if (!dex?.evos?.length) return undefined
const target = dex.evos[0]!.toLowerCase()
const targetDex = Dex.species.get(target)
if (!targetDex?.exists) return undefined
const trigger = dex.evoType === 'trade' ? 'trade'
: dex.evoType === 'useItem' ? 'item'
: dex.evoType === 'levelFriendship' ? 'friendship'
: 'level_up'
return {
from: speciesId,
to: target as SpeciesId,
trigger,
minLevel: targetDex.evoLevel,
}
}
```
### 4. data/pkmn.ts — 统一 gens 单例 + 导出 stat 映射
```typescript
import { Dex } from '@pkmn/sim'
import { Generations } from '@pkmn/data'
import type { StatName } from '../types'
// 统一单例(全包唯一入口)
const gens = new Generations(Dex as unknown as import('@pkmn/data').Dex)
export const gen = gens.get(9)
// Stat key 映射:@pkmn 缩写 → 我们的 StatName
export const FROM_DEX_STAT: Record<string, StatName> = {
hp: 'hp', atk: 'attack', def: 'defense',
spa: 'spAtk', spd: 'spDef', spe: 'speed',
}
// Stat key 映射:我们的 StatName → @pkmn 缩写
export const TO_DEX_STAT: Record<StatName, string> = {
hp: 'hp', attack: 'atk', defense: 'def',
spAtk: 'spa', spDef: 'spd', speed: 'spe',
}
// ...保留现有 getSpecies, getMove, getAbility, getType, mapBaseStats, mapGenderRatio, getPrimaryAbility ...
```
### 5. core/creature.ts — calculateStats 委托 stats.calc
```typescript
import { gen, TO_DEX_STAT } from '../data/pkmn'
import { STAT_NAMES } from '../types'
import type { Creature, StatsResult } from '../types'
export function calculateStats(creature: Creature): StatsResult {
const species = gen.species.get(creature.speciesId)
if (!species) throw new Error(`Species ${creature.speciesId} not found`)
const nature = creature.nature ? gen.natures.get(creature.nature) : undefined
const result = {} as StatsResult
for (const stat of STAT_NAMES) {
const dexKey = TO_DEX_STAT[stat] as 'hp' | 'atk' | 'def' | 'spa' | 'spd' | 'spe'
result[stat] = gen.stats.calc(
dexKey,
species.baseStats[dexKey],
creature.iv[stat],
creature.ev[stat],
creature.level,
nature ?? undefined,
)
}
return result
}
```
**注意**`gen.stats.calc()` 内部已处理 Nature ±10% 修正。Phase -1 计划中的手写 Nature 修正代码不再需要。
### 6. data/species.ts — 简化 buildEvolutionChain
现有的 `buildEvolutionChain()` 已使用 `dex.evos`,只需确保它与新的 `getNextEvolution()` 一致。可简化为:
```typescript
function buildEvolutionChain(speciesId: SpeciesId): SpeciesData['evolutionChain'] {
const evo = getNextEvolution(speciesId)
if (!evo) return undefined
return [{ trigger: evo.trigger, level: evo.minLevel, into: evo.to }]
}
```
## 验证
1. `bun run typecheck` 零错误
2. `bun test packages/pokemon/` 全部通过
3. `bun run dev``/buddy` 所有现有功能正常pet、dex、egg、switch
4. 性格效果正确Adamant Charmander Lv50 ATK 应比 Hardy 高 ~10%SPA 低 ~10%
5. 进化判定正确Charmander Lv16 → CharmeleonSquirtle Lv16 → Wartortle
6. stat 计算结果与旧实现数值一致
## 代码量
- 删除:~55 行NATURES 27 行 + EVOLUTION_CHAINS 12 行 + calculateStats 手写公式 18 行 - 2 行)
- 新增:~40 行(委托代码)
- 净变化:-15 行

View File

@@ -1,215 +0,0 @@
# Phase 1: 数据模型升级 — Creature v2 + 存储迁移
## 前置
Phase 0 完成(@pkmn 数据源已就位Natures/Evolution/Stats 已委托)
## 目标
扩展 Creature 类型Nature/Moves/Ability/HeldItem新增 PCBox/BagBuddyData v2 存储迁移。
## 新类型定义
### types.ts 新增
```typescript
// 招式槽位
export type MoveSlot = { id: string; pp: number; maxPp: number }
export const EMPTY_MOVE: MoveSlot = { id: '', pp: 0, maxPp: 0 }
// 道具 IDShowdown 格式字符串)
export type ItemId = string
// PC 箱(固定 30 槽)
export type PCBox = {
name: string
slots: (string | null)[] // 固定长度 30存 creature ID
}
// 背包条目
export type BagEntry = { id: ItemId; count: number }
export type Bag = { items: BagEntry[] }
```
### Creature 扩展
```typescript
export type Creature = {
// ─── 标识 ───
id: string // UUID
speciesId: SpeciesId
nickname?: string
// ─── 基础属性 ───
gender: Gender
level: number
xp: number // 当前等级进度 XP
totalXp: number // 累计总 XP
nature: NatureName // NEW: 性格
isShiny: boolean
// ─── 能力值 ───
ev: Record<StatName, number> // 努力值
iv: Record<StatName, number> // 个体值 (0-31)
// ─── 战斗 ───
moves: [MoveSlot, MoveSlot, MoveSlot, MoveSlot] // NEW: 4 招式槽位
ability: string // NEW: Showdown 特性 ID
heldItem: ItemId | null // NEW: 携带道具
// ─── 关系 ───
friendship: number // 亲密度 (0-255)
hatchedAt: number // 获得时间戳
pokeball: string // NEW: 捕获球
}
```
### BuddyData v2
```typescript
export type BuddyData = {
version: 2
party: (string | null)[] // 固定 6 槽party[0] = 活跃精灵
boxes: PCBox[] // NEW: PC 箱(默认 8 箱 × 30 槽)
creatures: Creature[] // 主表
eggs: Egg[]
dex: DexEntry[]
bag: Bag // NEW: 玩家背包
stats: {
totalTurns: number
consecutiveDays: number
lastActiveDate: string
totalEggsObtained: number
totalEvolutions: number
battlesWon: number // NEW
battlesLost: number // NEW
}
}
```
## 设计决策
- Party 和 Box 只存 `id`,不嵌套对象 → 单一数据源,移动只改引用
- Creature 主表用数组 → JSON 友好
- 8 箱 × 30 槽 = 240 + 6 party = 246 上限 → 足够 MVP
- `activeCreatureId` 彻底删除 → Party slot 0 即活跃精灵
- 战后自动恢复HP/PP 满值Creature 不存 currentHp
- 异常状态/能力阶级仅战斗内Phase 2 BattleState不持久化
## 新建文件
### data/learnsets.ts
```typescript
import { Dex } from '@pkmn/sim'
import type { SpeciesId, MoveSlot } from '../types'
import { EMPTY_MOVE } from '../types'
const GEN = 9
/** 获取物种在指定等级的默认招式(最后 4 个 level-up 招式) */
export function getDefaultMoveset(speciesId: SpeciesId, level: number): [MoveSlot, MoveSlot, MoveSlot, MoveSlot] {
const learnset = Dex.learnsets.get(speciesId)
if (!learnset?.learnset) return [EMPTY_MOVE, EMPTY_MOVE, EMPTY_MOVE, EMPTY_MOVE]
const levelUpMoves: { id: string; level: number }[] = []
for (const [moveId, sources] of Object.entries(learnset.learnset)) {
for (const src of sources) {
if (src.startsWith(`${GEN}L`)) {
levelUpMoves.push({ id: moveId, level: parseInt(src.slice(2)) })
break
}
}
}
levelUpMoves.sort((a, b) => a.level - b.level)
const available = levelUpMoves.filter(m => m.level <= level).slice(-4)
const slots: MoveSlot[] = available.map(m => {
const dexMove = Dex.moves.get(m.id)
return { id: m.id, pp: dexMove?.pp ?? 10, maxPp: dexMove?.pp ?? 10 }
})
while (slots.length < 4) slots.push(EMPTY_MOVE)
return slots as [MoveSlot, MoveSlot, MoveSlot, MoveSlot]
}
/** 获取物种的默认特性(第一个非隐藏特性) */
export function getDefaultAbility(speciesId: SpeciesId): string {
const species = Dex.species.get(speciesId)
return species?.abilities?.['0']?.toLowerCase() ?? ''
}
/** 获取物种在指定等级新可学的招式列表(用于升级检测) */
export function getNewLearnableMoves(speciesId: SpeciesId, oldLevel: number, newLevel: number): { id: string; name: string }[] {
const learnset = Dex.learnsets.get(speciesId)
if (!learnset?.learnset) return []
const result: { id: string; name: string }[] = []
for (const [moveId, sources] of Object.entries(learnset.learnset)) {
for (const src of sources) {
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
}
```
## 改动
| 文件 | 操作 |
|------|------|
| `types.ts` | 新增 MoveSlot/PCBox/Bag 类型Creature 扩展字段BuddyData v2删除 `activeCreatureId` |
| `core/creature.ts` | `generateCreature()` 填充 naturerandomNature、movesgetDefaultMoveset、abilitygetDefaultAbility |
| `core/storage.ts` | BuddyData v2 默认值v1→v2 迁移PCBox 操作Bag 操作 |
| `core/egg.ts` | `hatchEgg()` 返回的 creature 自动放入 party 空位或 PC 箱 |
| `index.ts` | 新增所有导出 |
### storage.ts 新增函数
```typescript
// PC 箱操作
depositToBox(data, creatureId): { data, deposited }
withdrawFromBox(data, creatureId): { data, withdrawn }
moveInBox(data, fromBox, fromSlot, toBox, toSlot): BuddyData
renameBox(data, boxIndex, name): BuddyData
findCreatureLocation(data, creatureId): { area, slot } | null
releaseCreature(data, creatureId): BuddyData
getTotalCreatureCount(data): number
getAllCreatureIds(data): string[]
// 背包操作
addItemToBag(data, itemId, count?): BuddyData
removeItemFromBag(data, itemId, count?): { data, removed }
getItemCount(data, itemId): number
```
### v1 → v2 迁移
- 保留 `party` 数组
- 新增默认 8 箱空 `boxes`
- 不在 party 的精灵放入 Box 1 前几个槽位
- 每个 creature 补全新字段:`nature`(随机)、`moves`getDefaultMoveset`ability`getDefaultAbility`heldItem: null``pokeball: 'pokeball'`
- 新增 `bag: { items: [] }`
- `stats` 补全 `battlesWon: 0`, `battlesLost: 0`
- 删除 `activeCreatureId`
## 验证
1. `bun run typecheck` + `bun test packages/pokemon/`
2. 新 creature 带有随机 nature + 正确的 4 招(含 PP+ 默认 ability
3. 旧 v1 数据自动迁移为 v2boxes 生成、空 moves/ability 被回填
4. PC 箱操作:存入/取出/移动/释放均正确
5. 背包操作:添加/消耗/查询均正确
6. `bun run dev``/buddy` 现有功能正常pet、dex、egg、switch
## 代码量
新增 ~200 行(类型 + learnsets + PCBox/Bag 操作 + 迁移)

View File

@@ -1,188 +0,0 @@
# Phase 2: 战斗引擎 — @pkmn 薄适配层
## 前置
Phase 1 完成Creature 含 moves/ability/nature/heldItemPCBox/Bag 就位)
## 目标
安装 `@pkmn/protocol``@pkmn/client``@pkmn/view`,构建战斗引擎作为 @pkmn 的薄适配层。纯逻辑层,不涉及 UI。
## 安装依赖
```bash
cd packages/pokemon && bun add @pkmn/protocol @pkmn/client @pkmn/view
```
## 战斗设计原则
- **战后自动恢复**:每场战斗结束后 HP/PP 自动恢复满值Creature 不存 currentHp
- **不可逃跑**:野生战斗必须打到一方倒下
- **状态不持久化**:异常状态、能力阶级仅存在于 BattleState
- **支持换人**:战斗中可切换 party 其他精灵(消耗回合)
- **支持道具**:战斗中使用背包道具(消耗回合)
- **特性/物品自动处理**@pkmn/sim 内置
## 核心架构
```
@pkmn/sim Battle ──(log)──→ @pkmn/protocol ──(Handler)──→ BattleEvent[]
──(protocol)──→ @pkmn/client Battle ──(投影)──→ BattleState
```
## 新建文件
```
packages/pokemon/src/battle/
├── types.ts # BattleState, BattleEvent, BattleResult, PlayerAction, BattlePokemon
├── adapter.ts # Creature → PokemonSet 转换stat key + gender 映射)
├── engine.ts # createBattle(), executeTurn() — 薄封装 @pkmn/sim Battle
├── handler.ts # @pkmn/protocol Handler → BattleEvent 转换
├── ai.ts # AI 决策(随机合法招式)
├── settlement.ts # 战后结算XP/EV/升级/进化/招式学习/背包扣减)
└── index.ts # 导出
```
## 详细实现
### 1. battle/types.ts — 战斗类型
```typescript
export type StatusCondition = 'poison' | 'bad_poison' | 'burn' | 'paralysis' | 'freeze' | 'sleep' | 'none'
export type BattlePokemon = {
id: string // creature ID
speciesId: string
name: string
level: number
hp: number // 战斗中当前 HP
maxHp: number
types: string[]
moves: MoveOption[]
ability: string
heldItem: string | null
status: StatusCondition
statStages: Record<string, number> // -6 到 +6
}
export type MoveOption = {
id: string; name: string; type: string
pp: number; maxPp: number; disabled: boolean
}
export type PlayerAction =
| { type: 'move'; moveIndex: number }
| { type: 'switch'; creatureId: string }
| { type: 'item'; itemId: string }
export type BattleEvent =
| { type: 'move'; side: 'player' | 'opponent'; move: string; user: string }
| { type: 'damage'; side: 'player' | 'opponent'; amount: number; percentage: number }
| { type: 'heal'; side: 'player' | 'opponent'; amount: number; percentage: number }
| { type: 'faint'; side: 'player' | 'opponent'; speciesId: string }
| { type: 'switch'; side: 'player' | 'opponent'; speciesId: string; name: string }
| { type: 'effectiveness'; multiplier: number }
| { type: 'crit' }
| { type: 'miss' }
| { type: 'status'; side: 'player' | 'opponent'; status: StatusCondition }
| { type: 'statChange'; side: 'player' | 'opponent'; stat: string; stages: number }
| { type: 'ability'; side: 'player' | 'opponent'; ability: string }
| { type: 'item'; side: 'player' | 'opponent'; item: string }
| { type: 'fail'; reason: string }
| { type: 'turn'; number: number }
export type BattleResult = {
winner: 'player' | 'opponent'
turns: number
xpGained: number
evGained: Record<string, number>
participantIds: string[]
}
export type BattleState = {
_sim: any // @pkmn/sim Battle 实例(内部)
_client: any // @pkmn/client Battle 实例(内部)
playerPokemon: BattlePokemon // 从 _client.p1.active[0] 投影
opponentPokemon: BattlePokemon // 从 _client.p2.active[0] 投影
playerParty: BattlePokemon[] // 从 _client.p1.team 投影
turn: number
events: BattleEvent[]
finished: boolean
result?: BattleResult
usableItems: { id: string; name: string; count: number }[]
}
```
### 2. battle/adapter.ts — 格式转换
- `creatureToSet(creature)``PokemonSet`stat key 映射 attack→atk, spAtk→spa 等)
- `wildPokemonToSet(speciesId, level)` → 野生对手 `PokemonSet`
- 复用 `TO_DEX_STAT` 映射(来自 `data/pkmn.ts`
### 3. battle/engine.ts — 核心引擎
- `createBattle(partyCreatures, opponentSpeciesId, opponentLevel, bagItems?)` → BattleState
- 创建 `@pkmn/sim.Battle` 实例(`gen9customgame` 格式)
- 创建 `@pkmn/client.Battle` 实例追踪状态
- `executeTurn(state, action)` → BattleState
- 构建 choice 字符串(`move 1` / `switch 2`
- AI 选招(随机合法招式)
- `battle.makeChoices(p1, p2)`
- 新 log → `@pkmn/protocol` Handler → BattleEvent[]
-`_client` 投影 BattleState
### 4. battle/handler.ts — 协议处理
- 实现 `Protocol.Handler` 接口
- 每个 `|move|``|-damage|``|faint|` 等回调产出 `BattleEvent`
- 替代手写 switch-case 解析器
### 5. battle/ai.ts — AI 决策
初期实现从对手可用招式中随机选一个PP > 0。后续可增加属性克制优先。
### 6. battle/settlement.ts — 战后结算
```typescript
settleBattle(data, battleState) {
data: BuddyData
learnableMoves: { creatureId, moveId, moveName }[]
pendingEvolutions: { creatureId, from, to }[]
}
applyMoveLearn(data, creatureId, newMoveId, replaceIndex) BuddyData
applyEvolution(data, creatureId) BuddyData
```
结算流程XP → EV → 升级 → 新招式检测 → 进化检测 → 背包扣减 → 统计更新。
## 使用 @pkmn 包
- `@pkmn/sim`Battle 类、Dex.teams.pack
- `@pkmn/protocol`Protocol.Handler、Protocol.parse
- `@pkmn/client`Battle 状态追踪)
- `@pkmn/sets`PokemonSet 类型)
- `@pkmn/view`LogFormatter可选
- `@pkmn/data`stats.calcPhase 0 已集成)
## 测试
**新文件**: `packages/pokemon/src/__tests__/battle.test.ts`
- 创建战斗BattleState 初始化正确
- 选招回合HP 更新 + 事件生成
- 异常状态:状态附加/恢复
- 换人:正确切换 + 对手出招
- 道具HP 恢复 + 背包扣减
- 特性/物品:事件生成
- 击倒 → 结束 → XP/EV 奖励
- 战后不写入 Creature 持久化数据
## 验证
1. `bun test packages/pokemon/src/__tests__/battle.test.ts` 通过
2. 可在 Node REPL 中完成完整战斗流程
3. `bun run typecheck` 零错误
## 代码量
新增 ~300 行(适配层),避免 ~500 行手写引擎

View File

@@ -1,182 +0,0 @@
# Phase 3: 战斗 UI — 终端交互
## 前置
Phase 2 完成(引擎 API 就绪createBattle, executeTurn, settleBattle, applyMoveLearn, applyEvolution
## 目标
基于 Phase 2 引擎 API构建终端战斗交互界面。纯 UI 层,无引擎逻辑。
## 引擎 API 依赖
```
createBattle(party, opponent, level, bagItems) → BattleState
executeTurn(state, action) → BattleState
settleBattle(data, state) → { data, learnableMoves, pendingEvolutions }
applyMoveLearn(data, creatureId, moveId, replaceIndex) → BuddyData
applyEvolution(data, creatureId) → BuddyData
```
## 新建文件
```
packages/pokemon/src/ui/
├── BattleConfigPanel.tsx # 战斗前配置
├── BattleView.tsx # 战斗主界面
├── SwitchPanel.tsx # 换人选择
├── ItemPanel.tsx # 道具使用
├── BattleResultPanel.tsx # 战斗结算
├── MoveLearnPanel.tsx # 招式学习
src/commands/buddy/
└── BattleFlow.tsx # 状态机编排
```
## 详细设计
### 1. BattleConfigPanel — 战斗配置
```
┌── 战斗配置 ──────────────────────────────────┐
│ 队伍: │
│ ▶ Charizard Lv.36 🔥🦅 HP ██████ 100% │
│ Venusaur Lv.28 🌿☢ HP ██████ 100% │
│ [空] [空] [空] │
│ │
│ 对手: │
│ [1] 随机遇战(等级自动匹配) │
│ [2] 指定对手: _________ │
│ │
│ [Enter] 开始战斗 [ESC] 取消 │
└──────────────────────────────────────────────┘
```
Props: `{ party, onSubmit, onCancel }`
交互:[1] 随机遇战 → ±5 级匹配;[2] 模糊搜索物种名Enter 确认ESC 取消。
### 2. BattleView — 战斗主界面
```
┌────────────────────────────────────────────┐
│ 野生 Blastoise (Lv.32) 💧 │
│ HP ████░░░░ 78% [烧伤] │
│ (对手精灵 ASCII) │
│ ── vs ── │
│ (我方精灵 ASCII) │
│ Charizard (Lv.36) ♂ 🔥🦅 │
│ HP ████████ 95% [特性: 猛火] │
│ │
│ > 选择行动: │
│ [1] Flamethrower PP 15/15 │
│ [2] Air Slash PP 15/15 │
│ [3] Dragon Rage PP 10/10 │
│ [4] Slash PP 20/20 │
│ [S] 换人 [I] 道具 │
│ │
│ 效果拔群! Blastoise 受到 23 点伤害! │
└────────────────────────────────────────────┘
```
Props: `{ state: BattleState, onAction: (action) => void }`
视觉HP 条 >50% 绿/25-50% 黄/<25% 红状态标签彩色PP=0 灰显;最近 8 条事件日志。
### 3. SwitchPanel — 换人选择
```
┌── 换人 ──────────────────────────┐
│ [1] Venusaur (Lv.28) HP 100% │
│ [2] Pikachu (Lv.25) HP 72% │
│ [3] Wartortle (Lv.22) HP 45% ⚠ │
│ [ESC] 取消 │
└──────────────────────────────────┘
```
Props: `{ party, activeId, onSelect, onCancel }`
HP=0 灰显不可选;当前场上精灵标注 ▶ 不可选ESC 取消。
### 4. ItemPanel — 道具使用
```
┌── 道具 ──────────────────────────┐
│ [1] 伤药 ×3 恢复 20 HP │
│ [2] 好伤药 ×1 恢复 50 HP │
│ [ESC] 取消 │
└──────────────────────────────────┘
```
Props: `{ items, onSelect, onCancel }`
### 5. BattleResultPanel — 战斗结算
```
┌── 战斗结束!胜利! ──────────────────────┐
│ Charizard 获得了 340 经验值! │
│ ████████████░░ 75% → 82% │
│ ⬆ 升到了 Lv.37
│ 努力值获得: ATK +2 DEF +1 │
│ [Enter] 继续 │
└─────────────────────────────────────────┘
```
### 6. MoveLearnPanel — 招式学习
```
┌── 新招式! ──────────────────────────────────┐
│ Charizard 升到了 Lv.37
│ 可以学习: Flamethrower 🔥 │
│ 当前招式: │
│ [1] Ember PP 35/35 │
│ [2] Air Slash PP 15/15 ← 替换目标 │
│ [3] Dragon Rage PP 10/10 │
│ [4] Slash PP 20/20 │
│ [Y] 学习 [N] 跳过 [← →] 切换替换目标 │
└──────────────────────────────────────────────┘
```
### 7. BattleFlow — 状态机编排
```
config → battle ⇌ switch/item (子状态) → result → learnMoves → evolution → 完成
```
- `config`:渲染 BattleConfigPanel → `createBattle()`
- `battle`:渲染 BattleView
- 选招 → `executeTurn({ type: 'move' })`
- S → SwitchPanel → `executeTurn({ type: 'switch' })`
- I → ItemPanel → `executeTurn({ type: 'item' })`
- `finished``settleBattle()``result`
- `result`BattleResultPanel → learnMoves / evolution
- `learnMoves`:循环 MoveLearnPanel → `applyMoveLearn()`
- `evolution`:循环 EvolutionAnim → `applyEvolution()`
- 完成 → `saveBuddyData()`
### 8. 命令集成
`/buddy battle` → 打开 BattleConfigPanel
`/buddy battle pikachu` → 快捷指定对手
`src/commands/buddy/index.ts` 更新 argumentHint`'[pet|dex|egg|battle|switch|rename <name>|on|off]'`
## 使用 @pkmn 包
- `@pkmn/view`LogFormatter可选用于战斗日志格式化
## 验证
1. `bun run typecheck` 零错误
2. `/buddy battle` 完整流程:
- 配置 → 选对手 → 开始战斗
- 选招 → HP 变化 + 事件日志
- S → 换人 → 对手出招
- I → 选道具 → HP 恢复
- 胜利 → 结算 → 升级提示
- 新招式 → 确认学习/跳过
- 进化 → 动画
3. 战后数据持久化正确XP/EV/招式/进化HP/PP 下次战斗满值
## 代码量
新增 ~400 行6 个 UI 组件 + 状态机 + 命令集成)