From df61bf3852a9e3e500836c863820d34688d82a64 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Wed, 22 Apr 2026 03:02:15 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E5=B7=B2=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E7=9A=84=E8=AE=A1=E5=88=92=E6=96=87=E4=BB=B6=20(Phase?= =?UTF-8?q?=200-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- plans/phase-0.md | 189 ----------------------------------------- plans/phase-1.md | 215 ----------------------------------------------- plans/phase-2.md | 188 ----------------------------------------- plans/phase-3.md | 182 --------------------------------------- 4 files changed, 774 deletions(-) delete mode 100644 plans/phase-0.md delete mode 100644 plans/phase-1.md delete mode 100644 plans/phase-2.md delete mode 100644 plans/phase-3.md diff --git a/plans/phase-0.md b/plans/phase-0.md deleted file mode 100644 index fa3ce9fce..000000000 --- a/plans/phase-0.md +++ /dev/null @@ -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 = { - hp: 'hp', atk: 'attack', def: 'defense', - spa: 'spAtk', spd: 'spDef', spe: 'speed', -} - -// Stat key 映射:我们的 StatName → @pkmn 缩写 -export const TO_DEX_STAT: Record = { - 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 → Charmeleon,Squirtle Lv16 → Wartortle -6. stat 计算结果与旧实现数值一致 - -## 代码量 - -- 删除:~55 行(NATURES 27 行 + EVOLUTION_CHAINS 12 行 + calculateStats 手写公式 18 行 - 2 行) -- 新增:~40 行(委托代码) -- 净变化:-15 行 diff --git a/plans/phase-1.md b/plans/phase-1.md deleted file mode 100644 index cc2ed03ef..000000000 --- a/plans/phase-1.md +++ /dev/null @@ -1,215 +0,0 @@ -# Phase 1: 数据模型升级 — Creature v2 + 存储迁移 - -## 前置 - -Phase 0 完成(@pkmn 数据源已就位,Natures/Evolution/Stats 已委托) - -## 目标 - -扩展 Creature 类型(Nature/Moves/Ability/HeldItem),新增 PCBox/Bag,BuddyData v2 存储迁移。 - -## 新类型定义 - -### types.ts 新增 - -```typescript -// 招式槽位 -export type MoveSlot = { id: string; pp: number; maxPp: number } -export const EMPTY_MOVE: MoveSlot = { id: '', pp: 0, maxPp: 0 } - -// 道具 ID(Showdown 格式字符串) -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 // 努力值 - iv: Record // 个体值 (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()` 填充 nature(randomNature)、moves(getDefaultMoveset)、ability(getDefaultAbility) | -| `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 数据自动迁移为 v2:boxes 生成、空 moves/ability 被回填 -4. PC 箱操作:存入/取出/移动/释放均正确 -5. 背包操作:添加/消耗/查询均正确 -6. `bun run dev` → `/buddy` 现有功能正常(pet、dex、egg、switch) - -## 代码量 - -新增 ~200 行(类型 + learnsets + PCBox/Bag 操作 + 迁移) diff --git a/plans/phase-2.md b/plans/phase-2.md deleted file mode 100644 index a315a0205..000000000 --- a/plans/phase-2.md +++ /dev/null @@ -1,188 +0,0 @@ -# Phase 2: 战斗引擎 — @pkmn 薄适配层 - -## 前置 - -Phase 1 完成(Creature 含 moves/ability/nature/heldItem,PCBox/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 // -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 - 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.calc,Phase 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 行手写引擎 diff --git a/plans/phase-3.md b/plans/phase-3.md deleted file mode 100644 index 63ed780a8..000000000 --- a/plans/phase-3.md +++ /dev/null @@ -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 |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 组件 + 状态机 + 命令集成)