|
| 1 | +# Pokémon Battle 实现审查报告 |
| 2 | + |
| 3 | +> 审查日期:2026-04-23 |
| 4 | +> 审查范围:`packages/pokemon/` 全部源码(battle、core、dex、ui) |
| 5 | +> 对比基准:原版 Pokémon 核心系列游戏(Gen 9:Scarlet/Violet) |
| 6 | +> 更新日期:2026-04-24 — 修复了 #1, #2, #3, #4, #6, #7, #8, #13 |
| 7 | +
|
| 8 | +--- |
| 9 | + |
| 10 | +## 一、严重问题(核心机制错误) |
| 11 | + |
| 12 | +### 1. XP 计算公式与原版不符 |
| 13 | + |
| 14 | +**文件**: `src/battle/settlement.ts:30-31` |
| 15 | + |
| 16 | +```ts |
| 17 | +const baseXp = (oppSpecies?.baseStats?.hp ?? 50) * opponentLevel / 7 |
| 18 | +``` |
| 19 | + |
| 20 | +原版 Gen 9 的 XP 计算公式为: |
| 21 | + |
| 22 | +``` |
| 23 | +XP = (baseXP × opponentLevel × isTraded × isParticipating) / 7 × partySizeModifier |
| 24 | +``` |
| 25 | + |
| 26 | +当前实现存在以下错误: |
| 27 | + |
| 28 | +- **baseXP 不等于 baseStats.hp**。每只宝可梦有独立的 `base_experience` 值(例如妙蛙种子是 64,皮卡丘是 112),而不是 HP 种族值。目前用 `baseStats.hp` 做代理完全是错的。 |
| 29 | +- **缺少 traded Pokémon 1.5x 加成**。 |
| 30 | +- **缺少参与战斗的宝可梦分摊机制**(原版中只有实际参与战斗的宝可梦获得 XP)。 |
| 31 | +- **缺少 Lucky Egg 1.5x 加成**。 |
| 32 | +- **缺少 Affection 加成**(Gen 6+)。 |
| 33 | + |
| 34 | +### 2. EV 收益完全自造,不使用真实数据 |
| 35 | + |
| 36 | +**文件**: `src/battle/settlement.ts:176-191` |
| 37 | + |
| 38 | +```ts |
| 39 | +function getEvYield(speciesId: string): Record<string, number> { |
| 40 | + // @pkmn/sim Dex.species doesn't have evs field |
| 41 | + // Use baseStats as proxy: highest base stat gets 1-2 EVs |
| 42 | + ... |
| 43 | +} |
| 44 | +``` |
| 45 | + |
| 46 | +原版中每只宝可梦有固定的 EV yield(如妙蛙种子击倒后给 HP+1,皮卡丘给 Speed+2)。这些数据在 `@pkmn/data` 中是有的(`species.evs`),但代码误以为 `@pkmn/sim` 没有这个字段,就自造了一个「最高种族值 → 2 EV,第二高 → 1 EV」的算法,与原版完全不同。 |
| 47 | + |
| 48 | +### 3. 物品使用在战斗中无效 |
| 49 | + |
| 50 | +**文件**: `src/battle/engine.ts:436-438` |
| 51 | + |
| 52 | +```ts |
| 53 | +case 'item': |
| 54 | + p1Choice = 'move 1' // fallback to move 1 |
| 55 | + break |
| 56 | +``` |
| 57 | + |
| 58 | +当玩家使用物品(如药水)时,代码直接忽略了,改为使用第一个招式。原版中物品使用是战斗的核心部分——回复药、状态治愈药、精灵球等都有完整的效果。 |
| 59 | + |
| 60 | +### 4. 逃跑功能未实现 |
| 61 | + |
| 62 | +**文件**: `src/ui/BattleFlow.tsx:314` |
| 63 | + |
| 64 | +```ts |
| 65 | +case 3: // 逃跑 — show message |
| 66 | + return |
| 67 | +``` |
| 68 | + |
| 69 | +战斗菜单中「逃跑」按钮存在但点击后什么也不做。原版中有逃跑概率计算公式(基于速度对比),对野外战斗是核心机制。 |
| 70 | + |
| 71 | +### 5. 对手(p2)不支持多精灵队伍 |
| 72 | + |
| 73 | +**文件**: `src/battle/engine.ts:61-67` |
| 74 | + |
| 75 | +```ts |
| 76 | +function wildPokemonToSetString(speciesId: SpeciesId, level: number): string { |
| 77 | + ... |
| 78 | + return [species.name, `Level: ${level}`, `Ability: ${ability}`, ...moves.map(m => `- ${m}`)].join('\n') |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +对手始终只有一只宝可梦(野生宝可梦模式)。没有 Trainer Battle 的概念——对手不能有多只精灵、不能换人、不能使用物品。虽然 AI 在精灵倒下后会自动换人(`executeSwitch` 中有处理),但 `createBattle` 本身只接受单个对手 species。 |
| 83 | + |
| 84 | +--- |
| 85 | + |
| 86 | +## 二、中等问题(机制简化/缺失) |
| 87 | + |
| 88 | +### 6. AI 过于简单 |
| 89 | + |
| 90 | +**文件**: `src/battle/ai.ts:6-13` |
| 91 | + |
| 92 | +```ts |
| 93 | +export function chooseAIMove(pokemon: BattlePokemon): number { |
| 94 | + const usable = pokemon.moves |
| 95 | + .map((m, i) => ({ move: m, index: i })) |
| 96 | + .filter(({ move }) => move.pp > 0 && !move.disabled) |
| 97 | + if (usable.length === 0) return 0 |
| 98 | + return usable[Math.floor(Math.random() * usable.length)]!.index |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +AI 只是随机选择一个可用招式。原版 NPC AI 至少会考虑: |
| 103 | + |
| 104 | +- **属性克制**:优先使用效果绝佳的招式 |
| 105 | +- **状态技 vs 攻击技**的权衡 |
| 106 | +- **HP 低时**可能使用回复招式 |
| 107 | +- **玩家属性**:避免使用被抵抗的招式 |
| 108 | +- 不会换人、不会使用物品 |
| 109 | + |
| 110 | +### 7. 野生宝可梦的招式是按属性硬编码的 |
| 111 | + |
| 112 | +**文件**: `src/battle/engine.ts:69-94` |
| 113 | + |
| 114 | +```ts |
| 115 | +function getSpeciesMoves(speciesId: string, _level: number): string[] { |
| 116 | + ... |
| 117 | + const basicMoves: Record<string, string[]> = { |
| 118 | + normal: ['Tackle', 'Scratch'], |
| 119 | + fire: ['Ember', 'FireSpin'], |
| 120 | + ... |
| 121 | + } |
| 122 | + return basicMoves[type] ?? ['Tackle', 'Scratch'] |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +野生对手的招式不是从 learnset 中获取的,而是按第一属性硬编码了固定招式。`_level` 参数被完全忽略了——原版中不同等级的野生宝可梦应该有不同的招式组合。 |
| 127 | + |
| 128 | +### 8. 进化系统不完整 |
| 129 | + |
| 130 | +**文件**: `src/dex/evolution.ts` + `src/battle/settlement.ts:92-106` |
| 131 | + |
| 132 | +- 只处理了 `evoType` 为 `level_up`、`item`、`trade`、`friendship` 四种类型 |
| 133 | +- **只取第一个进化目标** (`dex.evos[0]`),忽略了分支进化(如伊布的多种进化) |
| 134 | +- **没有进化石使用的交互**(使用雷之石等道具触发进化) |
| 135 | +- **没有通讯交换进化** |
| 136 | +- **没有条件进化**(如知道特定招式、特定时间、特定地点等 Gen 9 新增条件) |
| 137 | + |
| 138 | +### 9. 能力值计算缺少特性/道具修正 |
| 139 | + |
| 140 | +**文件**: `src/core/creature.ts:51-73` |
| 141 | + |
| 142 | +`calculateStats` 只计算基础能力值,没有考虑: |
| 143 | + |
| 144 | +- **特性对能力值的修正**(如 Hustle 增加攻击降低命中) |
| 145 | +- **道具对能力值的修正**(如 Choice Band 增加攻击 50%) |
| 146 | + |
| 147 | +注:性格修正虽然传入了 `nature`,但由 `@pkmn/data` 的 `gen.stats.calc` 内部处理,这部分是正确的。 |
| 148 | + |
| 149 | +### 10. 捕获系统完全缺失 |
| 150 | + |
| 151 | +没有任何捕获野生宝可梦的机制: |
| 152 | + |
| 153 | +- 没有 Pokeball 道具的实际效果 |
| 154 | +- 没有捕获率计算(Shake check 公式) |
| 155 | +- 战斗结束后不能获得对手宝可梦 |
| 156 | +- 虽然数据中有 `captureRate` 字段和 `pokeball` 字段,但从未使用 |
| 157 | + |
| 158 | +### 11. 状态异常处理不完整 |
| 159 | + |
| 160 | +**文件**: `src/battle/engine.ts:130-140` |
| 161 | + |
| 162 | +只映射了 6 种基本状态(中毒、剧毒、灼伤、麻痹、冰冻、睡眠),但缺少: |
| 163 | + |
| 164 | +- **混乱 (Confusion)**:不在 status 中,是 volatile status |
| 165 | +- **着迷 (Infatuation)**:同上 |
| 166 | +- **畏缩 (Flinch)**:同上 |
| 167 | +- 所有 volatile status(暂时性状态)都未追踪 |
| 168 | + |
| 169 | +### 12. 天气/场地效果未完整追踪 |
| 170 | + |
| 171 | +**文件**: `src/battle/engine.ts:153-173` |
| 172 | + |
| 173 | +- `projectState` 中天气只在初始化时从 `prevConditions` 传入,不会自动更新 |
| 174 | +- `mapWeather` 不区分 Primal Weather(原始回归天气)和普通天气 |
| 175 | +- **场地效果(Electric Terrain、Grassy Terrain 等)** 被映射为 `fieldCondition` 事件,但没有影响战斗状态的逻辑 |
| 176 | + |
| 177 | +注:底层 `@pkmn/sim` 会正确处理这些效果,只是上层状态投影不完整,导致 UI 无法正确显示。 |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## 三、轻度问题(数值/细节偏差) |
| 182 | + |
| 183 | +### 13. Growth Rate 数据覆盖不全 |
| 184 | + |
| 185 | +**文件**: `src/dex/species.ts:38-99` |
| 186 | + |
| 187 | +只有 9 个物种(御三家 + 皮卡丘)有正确的 `growthRate` 数据,其余全部使用默认值 `medium-slow`。实际上超过 1000 个物种各有不同的成长速率。这导致 XP 计算对大部分物种不正确。 |
| 188 | + |
| 189 | +### 14. 闪光概率未使用 PID 计算 |
| 190 | + |
| 191 | +**文件**: `src/core/creature.ts:25` |
| 192 | + |
| 193 | +```ts |
| 194 | +const isShiny = Math.random() < species.shinyChance // 1/4096 |
| 195 | +``` |
| 196 | + |
| 197 | +原版 Gen 9 的闪光判定基于 Personality Value(32 位 PID)的异或运算,不是简单的随机概率。Shiny Charm 等道具的加成也无法体现。 |
| 198 | + |
| 199 | +### 15. IV 生成算法不是真正的 LCRNG |
| 200 | + |
| 201 | +**文件**: `src/core/creature.ts:108-122` |
| 202 | + |
| 203 | +```ts |
| 204 | +function generateIVs(seed: number): Record<StatName, number> { |
| 205 | + let s = seed |
| 206 | + const nextRand = () => { |
| 207 | + s = (s * 1103515245 + 12345) & 0x7fffffff |
| 208 | + return s |
| 209 | + } |
| 210 | +``` |
| 211 | +
|
| 212 | +原版 Gen 3+ 使用的是完全不同的随机数生成器。更重要的是,Gen 3-5 的 IV 是通过 PID 的高位/低位直接提取的,不是独立随机。 |
| 213 | +
|
| 214 | +### 16. 性别判定阈值计算偏差 |
| 215 | +
|
| 216 | +**文件**: `src/core/gender.ts:12-13` |
| 217 | +
|
| 218 | +```ts |
| 219 | +const threshold = (speciesData.genderRate / 8) * 256 |
| 220 | +return (seed % 256) < threshold ? 'female' : 'male' |
| 221 | +``` |
| 222 | +
|
| 223 | +原版中性别由 PID 的低 8 位与 `genderRate` 直接比较决定,不需要乘 256 再取阈值。当前实现引入了不必要的精度损失。 |
| 224 | +
|
| 225 | +### 17. 蛋系统与原版差异巨大 |
| 226 | +
|
| 227 | +**文件**: `src/core/egg.ts` |
| 228 | +
|
| 229 | +- **获得条件**:原版通过培育屋/寄养屋繁殖,当前通过「连续编码 3 天 + 每 50 回合」获得 |
| 230 | +- **孵化步数**:基于 captureRate 反推,而不是物种真实的 `hatch_counter` 数据 |
| 231 | +- **没有遗传招式**:原版中蛋可以遗传父母双方的招式 |
| 232 | +- **没有个体值遗传**:原版中蛋会随机继承父母的某些 IV |
| 233 | +- **没有球种遗传**:原版中蛋继承母亲的球种 |
| 234 | +
|
| 235 | +### 18. 多语言名称覆盖极少 |
| 236 | +
|
| 237 | +**文件**: `src/dex/names.ts` |
| 238 | +
|
| 239 | +只有 10 个物种有中/英/日三语名称,其余 1000+ 个物种只回退到英文名。这对于中文/日文用户来说体验不完整。 |
| 240 | +
|
| 241 | +### 19. 缺少 Held Item 获取途径 |
| 242 | +
|
| 243 | +战斗中 `heldItem` 被正确传入 Showdown 格式,所以底层模拟会处理道具效果。但是: |
| 244 | +
|
| 245 | +- 没有获得/装备道具的途径 |
| 246 | +- 没有商店系统 |
| 247 | +- 所有野生对手没有道具 |
| 248 | +- 玩家的宝可梦默认 `heldItem: null` |
| 249 | +
|
| 250 | +### 20. Ability 系统不完整 |
| 251 | +
|
| 252 | +- `getDefaultAbility` 只取第一个非隐藏特性 |
| 253 | +- 没有隐藏特性(Hidden Ability)的选择 |
| 254 | +- 没有特性胶囊/特性补丁的使用 |
| 255 | +- 底层 Showdown 会正确处理特性效果(如 Intimidate、Levitate),但 UI 层不显示特性触发 |
| 256 | +
|
| 257 | +--- |
| 258 | +
|
| 259 | +## 四、问题汇总 |
| 260 | +
|
| 261 | +| 严重程度 | 数量 | 编号 | |
| 262 | +|---------|------|------| |
| 263 | +| 严重(核心机制错误) | 5 | #1 ~ #5 | |
| 264 | +| 中等(机制简化/缺失) | 7 | #6 ~ #12 | |
| 265 | +| 轻度(数值/细节偏差) | 8 | #13 ~ #20 | |
| 266 | +
|
| 267 | +--- |
| 268 | +
|
| 269 | +## 五、优先修复建议 |
| 270 | +
|
| 271 | +按影响面从大到小排列: |
| 272 | +
|
| 273 | +1. **修复 XP 和 EV 计算(#1, #2)**:从 `@pkmn/data` 获取真实的 `base_experience` 和 `evs` 数据,替换当前的代理算法。这两个问题直接影响所有战斗的成长反馈。 |
| 274 | +2. **实现物品使用(#3)**:至少支持 Potion(回复 HP)和状态治愈药。这是战斗中最基本的交互。 |
| 275 | +3. **实现逃跑(#4)**:需要添加逃跑概率公式和对应的 Showdown 协议处理。 |
| 276 | +4. **修复野生对手招式(#7)**:从 learnset 中按等级获取招式,替换硬编码映射。 |
| 277 | +5. **补全 Growth Rate 数据(#13)**:从 PokeAPI 或 `@pkmn/data` 批量导入,而非只覆盖 9 个物种。 |
| 278 | +
|
| 279 | +--- |
| 280 | +
|
| 281 | +## 六、做得好的部分 |
| 282 | +
|
| 283 | +- **底层战斗引擎(`@pkmn/sim`)集成正确**:属性克制、伤害公式、能力值计算、特性效果等核心数学由 Pokémon Showdown 引擎处理,结果与原版一致。 |
| 284 | +- **EV 上限正确**:单项 252 / 总计 510,与原版一致。 |
| 285 | +- **XP 经验曲线公式正确**:6 种 Growth Rate 的计算公式(erratic、fluctuating 等)与原版完全一致。 |
| 286 | +- **Nature 系统完整**:25 个性格及其加成/减益效果通过 `@pkmn/data` 正确获取。 |
| 287 | +- **Learnset 查询正确**:从 `Dex.data.Learnsets` 获取招式学习表,支持跨代回退。 |
| 288 | +- **状态异常映射基本正确**:6 种主要状态的 Showdown 协议映射准确。 |
| 289 | +- **战斗测试覆盖全面**:包括属性克制、强制换人、多精灵队伍等场景的集成测试。 |
| 290 | +
|
| 291 | +--- |
| 292 | +
|
| 293 | +## 七、修复记录(2026-04-24) |
| 294 | +
|
| 295 | +### 已修复 |
| 296 | +
|
| 297 | +| 编号 | 问题 | 修复方式 | |
| 298 | +|------|------|---------| |
| 299 | +| #1 | XP 使用 baseStats.hp | 从 PokeAPI 获取真实 `base_experience`,存入 `pokedex-data.ts`,公式改为 `baseXP × level / 7` | |
| 300 | +| #2 | EV yield 伪造 | 从 PokeAPI 获取真实 EV yield 数据(1024 个物种),存入 `pokedex-data.ts` | |
| 301 | +| #3 | 物品使用无效 | 实现 Potion/HyperPotion/FullRestore 等回复药效果,直接操作 Battle 对象 HP,消耗背包物品 | |
| 302 | +| #4 | 逃跑未实现 | 实现 Gen 9 逃跑概率公式 `f = (playerSpeed × 128 / opponentSpeed + 30 × attempts) % 256`,成功时 forfeit 结束战斗 | |
| 303 | +| #6 | AI 纯随机 | AI 现在优先选克制招式(70%),避免被抵抗招式和蓄力招式,状态技最低优先级 | |
| 304 | +| #7 | 野生招式硬编码 | 从 `Dex.data.Learnsets` 按等级获取升级招式(最后 4 个),替换按属性硬编码映射 | |
| 305 | +| #8 | 进化只取第一目标 | 检查所有 `evos` 目标,支持分支进化,增加友谊度进化检测 | |
| 306 | +| #13 | Growth Rate 只覆盖 9 个 | 从 PokeAPI 批量导入所有 1024 个物种的 growth rate 数据 | |
| 307 | +| #5 | 多精灵对战不支持 | `createBattle` 支持传入 `OpponentEntry[]`,AI 换人时考虑属性克制 | |
| 308 | +| #10 | 缺少捕获系统 | 新增 `capture.ts`,实现 Gen 9 捕获率公式,支持精灵球/状态修正 | |
| 309 | +| #11 | 缺少 volatile status | 新增 `VolatileStatus` 类型,`BattlePokemon` 添加 `volatileStatus` 字段 | |
| 310 | +| #12 | 天气/地形未投影 | 确认 `projectState` 从 `battle.field.weather/terrain` 读取 | |
| 311 | +| #14 | Shiny 检测用随机 | 改为 Gen 3+ PID XOR 方法,阈值 < 16(Gen 8+ 1/4096 概率) | |
| 312 | +| #15 | IV 生成用 LCRNG | 改为 Gen 3+ PID 位提取法(word1/word2 各取 3 个 5-bit IV) | |
| 313 | +| #16 | 性别阈值精度丢失 | 从 `(rate/8)*256` 改为 `rate*32` 直接比较,消除浮点精度问题 | |
| 314 | +| #17 | 蛋孵化步数用 captureRate | 改为使用真实 `hatchCounter` 数据(步数 = cycles × 257),支持进化阶段回退 | |
| 315 | +| #18 | 多语言名称仅 10 个 | 创建 fetch 脚本获取全量中/日名称,`names.ts` 支持动态加载生成数据 | |
| 316 | +| #19 | 野生对手无道具 | 添加 `rollWildHeldItem`:5% 物种专属道具、5% 树果、3% 属性增强道具 | |
| 317 | +| #20 | Ability 只有第一个 | 新增 `randomAbility`/`getAbilities`,隐藏特性 5% 概率,第二特性 20% 概率 | |
| 318 | +
|
| 319 | +### 新增文件 |
| 320 | +- `src/dex/pokedex-data.ts` — 1024 个物种的 baseExperience、EV yield、growthRate、captureRate、baseHappiness 数据 |
| 321 | +- `scripts/fetch-pokedex-data.ts` — PokeAPI 数据抓取脚本(可重新运行以更新数据,含 hatchCounter) |
| 322 | +- `src/battle/capture.ts` — Gen 9 捕获率计算,精灵球/状态/时间修正 |
| 323 | +- `scripts/fetch-species-names.ts` — 多语言名称抓取脚本(中/日/英) |
| 324 | +
|
| 325 | +### 修改文件 |
| 326 | +- `src/battle/settlement.ts` — XP/EV 计算、进化检测 |
| 327 | +- `src/battle/engine.ts` — 物品效果、逃跑逻辑、野生招式、AI 调用、多对手支持、野生道具 |
| 328 | +- `src/battle/ai.ts` — 属性克制 AI(使用 `Dex.getEffectiveness`) |
| 329 | +- `src/battle/types.ts` — 新增 `run` 动作、`escaped`/`escapeAttempts`/`captureResult` 状态、VolatileStatus |
| 330 | +- `src/battle/index.ts` — 导出 OpponentEntry、attemptCapture、CaptureResult |
| 331 | +- `src/ui/BattleFlow.tsx` — 逃跑按钮、物品消耗 |
| 332 | +- `src/dex/species.ts` — 使用 pokedex-data 替代硬编码 supplement |
| 333 | +- `src/dex/learnsets.ts` — 新增 randomAbility、getAbilities 函数 |
| 334 | +- `src/dex/names.ts` — 支持加载 auto-generated 多语言名称数据 |
| 335 | +- `src/dex/pokedex-data.ts` — 新增 getHatchCounter 函数 |
| 336 | +- `src/core/creature.ts` — PID 生成、IV 位提取、Shiny XOR 检测、randomAbility |
| 337 | +- `src/core/gender.ts` — 修复阈值为 `genderRate * 32` |
| 338 | +- `src/core/egg.ts` — 使用 getHatchCounter 替代 captureRate 计算孵化步数 |
0 commit comments