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

216 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 1: 数据模型升级 — Creature v2 + 存储迁移
## 前置
Phase 0 完成(@pkmn 数据源已就位Natures/Evolution/Stats 已委托)
## 目标
扩展 Creature 类型Nature/Moves/Ability/HeldItem新增 PCBox/BagBuddyData v2 存储迁移。
## 新类型定义
### types.ts 新增
```typescript
// 招式槽位
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 扩展
```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()` 填充 naturerandomNature、movesgetDefaultMoveset、abilitygetDefaultAbility |
| `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 数据自动迁移为 v2boxes 生成、空 moves/ability 被回填
4. PC 箱操作:存入/取出/移动/释放均正确
5. 背包操作:添加/消耗/查询均正确
6. `bun run dev``/buddy` 现有功能正常pet、dex、egg、switch
## 代码量
新增 ~200 行(类型 + learnsets + PCBox/Bag 操作 + 迁移)