mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
chore: 删除已完成的计划文件 (Phase 0-3)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
189
plans/phase-0.md
189
plans/phase-0.md
@@ -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 → Charmeleon,Squirtle Lv16 → Wartortle
|
||||
6. stat 计算结果与旧实现数值一致
|
||||
|
||||
## 代码量
|
||||
|
||||
- 删除:~55 行(NATURES 27 行 + EVOLUTION_CHAINS 12 行 + calculateStats 手写公式 18 行 - 2 行)
|
||||
- 新增:~40 行(委托代码)
|
||||
- 净变化:-15 行
|
||||
215
plans/phase-1.md
215
plans/phase-1.md
@@ -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<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()` 填充 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 操作 + 迁移)
|
||||
188
plans/phase-2.md
188
plans/phase-2.md
@@ -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<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 行手写引擎
|
||||
182
plans/phase-3.md
182
plans/phase-3.md
@@ -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 组件 + 状态机 + 命令集成)
|
||||
Reference in New Issue
Block a user