Files
claude-code/plans/phase-2.md
claude-code-best 336159ee18 feat: 计划完成
2026-04-21 23:56:03 +08:00

6.4 KiB
Raw Blame History

Phase 2: 战斗引擎 — @pkmn 薄适配层

前置

Phase 1 完成Creature 含 moves/ability/nature/heldItemPCBox/Bag 就位)

目标

安装 @pkmn/protocol@pkmn/client@pkmn/view,构建战斗引擎作为 @pkmn 的薄适配层。纯逻辑层,不涉及 UI。

安装依赖

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 — 战斗类型

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)PokemonSetstat 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 — 战后结算

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/simBattle 类、Dex.teams.pack
  • @pkmn/protocolProtocol.Handler、Protocol.parse
  • @pkmn/clientBattle 状态追踪)
  • @pkmn/setsPokemonSet 类型)
  • @pkmn/viewLogFormatter可选
  • @pkmn/datastats.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 行手写引擎