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

7.0 KiB
Raw Blame History

Phase 1: 数据模型升级 — Creature v2 + 存储迁移

前置

Phase 0 完成(@pkmn 数据源已就位Natures/Evolution/Stats 已委托)

目标

扩展 Creature 类型Nature/Moves/Ability/HeldItem新增 PCBox/BagBuddyData v2 存储迁移。

新类型定义

types.ts 新增

// 招式槽位
export type MoveSlot = { id: string; pp: number; maxPp: number }
export const EMPTY_MOVE: MoveSlot = { id: '', pp: 0, maxPp: 0 }

// 道具 IDShowdown 格式字符串)
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 扩展

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

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

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() 填充 naturerandomNature、movesgetDefaultMoveset、abilitygetDefaultAbility
core/storage.ts BuddyData v2 默认值v1→v2 迁移PCBox 操作Bag 操作
core/egg.ts hatchEgg() 返回的 creature 自动放入 party 空位或 PC 箱
index.ts 新增所有导出

storage.ts 新增函数

// 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(随机)、movesgetDefaultMovesetabilitygetDefaultAbilityheldItem: nullpokeball: '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 数据自动迁移为 v2boxes 生成、空 moves/ability 被回填
  4. PC 箱操作:存入/取出/移动/释放均正确
  5. 背包操作:添加/消耗/查询均正确
  6. bun run dev/buddy 现有功能正常pet、dex、egg、switch

代码量

新增 ~200 行(类型 + learnsets + PCBox/Bag 操作 + 迁移)