- 新增 MoveSlot, PCBox, Bag, ItemId 类型 - Creature 扩展 nature/moves/ability/heldItem/pokeball 字段 - BuddyData 升级 v2: 新增 boxes, bag, battlesWon/battlesLost - 新建 data/learnsets.ts: getDefaultMoveset/getDefaultAbility/getNewLearnableMoves - storage.ts v1→v2 迁移: 回填 nature/moves/ability,新增 PCBox/Bag - 新增 PCBox 操作: deposit/withdraw/move/rename/findLocation/release - 新增 Bag 操作: add/remove/getCount - generateCreature/loadBuddyData/hatchEgg 改为 async (Dex.learnsets.get 异步) - 修复 PokedexView: activeCreatureId → party[0] - 更新测试文件: async/await + v2 BuddyData fixtures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
112 lines
3.7 KiB
TypeScript
112 lines
3.7 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import type { BuddyData, Creature, Egg, SpeciesId } from '../types'
|
|
import { ALL_SPECIES_IDS } from '../types'
|
|
import { getSpeciesData } from '../data/species'
|
|
import { generateCreature } from './creature'
|
|
import { addToParty, depositToBox } from './storage'
|
|
|
|
/** Days of consecutive coding needed to be eligible for an egg */
|
|
export const EGG_REQUIRED_DAYS = 3
|
|
|
|
/**
|
|
* Check if the player is eligible to receive an egg.
|
|
* Conditions: consecutiveDays >= EGG_REQUIRED_DAYS AND totalTurns % 50 === 0 AND eggs.length < 1
|
|
*/
|
|
export function checkEggEligibility(buddyData: BuddyData): boolean {
|
|
if (buddyData.eggs.length >= 1) return false
|
|
if (buddyData.stats.consecutiveDays < EGG_REQUIRED_DAYS) return false
|
|
if (buddyData.stats.totalTurns % 50 !== 0) return false
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Generate a new egg with a species the player hasn't collected yet.
|
|
* Priority: uncollected species > random from all species.
|
|
*/
|
|
export function generateEgg(buddyData: BuddyData): Egg {
|
|
// Find uncollected species
|
|
const collectedSpecies = new Set(buddyData.creatures.map((c) => c.speciesId))
|
|
const uncollected = ALL_SPECIES_IDS.filter((id) => !collectedSpecies.has(id))
|
|
|
|
// Pick species (prefer uncollected, fall back to random starter)
|
|
const starters: SpeciesId[] = ['bulbasaur', 'charmander', 'squirtle', 'pikachu']
|
|
const speciesId = uncollected.length > 0
|
|
? uncollected[Math.floor(Math.random() * uncollected.length)]
|
|
: starters[Math.floor(Math.random() * starters.length)]
|
|
|
|
// Steps based on rarity (capture rate: lower = rarer = more steps)
|
|
const species = getSpeciesData(speciesId)
|
|
const baseSteps = Math.floor(2000 + ((255 - species.captureRate) / 255) * 3000)
|
|
|
|
return {
|
|
id: randomUUID(),
|
|
obtainedAt: Date.now(),
|
|
stepsRemaining: baseSteps,
|
|
totalSteps: baseSteps,
|
|
speciesId,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Advance egg steps by a given amount.
|
|
* Returns updated egg or null if egg hatched.
|
|
*/
|
|
export function advanceEggSteps(egg: Egg, steps: number): Egg {
|
|
const newSteps = Math.max(0, egg.stepsRemaining - steps)
|
|
return { ...egg, stepsRemaining: newSteps }
|
|
}
|
|
|
|
/**
|
|
* Check if an egg is ready to hatch.
|
|
*/
|
|
export function isEggReadyToHatch(egg: Egg): boolean {
|
|
return egg.stepsRemaining <= 0
|
|
}
|
|
|
|
/**
|
|
* Hatch an egg, creating a new creature and updating buddy data.
|
|
* Tries to add to party first, then deposits to PC box.
|
|
*/
|
|
export async function hatchEgg(buddyData: BuddyData, egg: Egg): Promise<{ buddyData: BuddyData; creature: Creature }> {
|
|
const creature = await generateCreature(egg.speciesId)
|
|
creature.hatchedAt = Date.now()
|
|
|
|
// Add creature to list
|
|
let updatedData: BuddyData = {
|
|
...buddyData,
|
|
creatures: [...buddyData.creatures, creature],
|
|
eggs: buddyData.eggs.filter((e) => e.id !== egg.id),
|
|
dex: updateDexEntry(buddyData.dex, egg.speciesId, creature.level),
|
|
stats: {
|
|
...buddyData.stats,
|
|
totalEggsObtained: buddyData.stats.totalEggsObtained + 1,
|
|
},
|
|
}
|
|
|
|
// Place in party or PC box
|
|
const partyResult = addToParty(updatedData, creature.id)
|
|
if (partyResult.added) {
|
|
updatedData = partyResult.data
|
|
} else {
|
|
const boxResult = depositToBox(updatedData, creature.id)
|
|
if (boxResult.deposited) updatedData = boxResult.data
|
|
}
|
|
|
|
return { buddyData: updatedData, creature }
|
|
}
|
|
|
|
/**
|
|
* Update or create a dex entry for a species.
|
|
*/
|
|
function updateDexEntry(dex: BuddyData['dex'], speciesId: SpeciesId, level: number): BuddyData['dex'] {
|
|
const existing = dex.find((d) => d.speciesId === speciesId)
|
|
if (existing) {
|
|
return dex.map((d) =>
|
|
d.speciesId === speciesId
|
|
? { ...d, caughtCount: d.caughtCount + 1, bestLevel: Math.max(d.bestLevel, level) }
|
|
: d,
|
|
)
|
|
}
|
|
return [...dex, { speciesId, discoveredAt: Date.now(), caughtCount: 1, bestLevel: level }]
|
|
}
|