mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
189 lines
6.4 KiB
Markdown
189 lines
6.4 KiB
Markdown
# 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<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.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 行手写引擎
|