diff --git a/packages/pokemon/src/__tests__/battle.test.ts b/packages/pokemon/src/__tests__/battle.test.ts index 241a04026..e0931e06f 100644 --- a/packages/pokemon/src/__tests__/battle.test.ts +++ b/packages/pokemon/src/__tests__/battle.test.ts @@ -23,7 +23,7 @@ function makeTestCreature(overrides: Partial = {}): Creature { ], ability: overrides.ability ?? 'blaze', heldItem: null, - friendship: 70, + friendship: overrides.friendship ?? 70, isShiny: false, hatchedAt: Date.now(), pokeball: 'pokeball', @@ -302,3 +302,168 @@ describe('settleBattle - advanced', () => { expect(settlement.data.stats.battlesWon).toBe(0) }) }) + +describe('createBattle - extended', () => { + test('battle state has turn initialized', () => { + const creature = makeTestCreature() + const init = createBattle([creature], 'squirtle', 50) + expect(init.state.turn).toBeGreaterThanOrEqual(1) + }) + + test('player pokemon has correct level', () => { + const creature = makeTestCreature({ level: 25 }) + const init = createBattle([creature], 'bulbasaur', 10) + expect(init.state.playerPokemon.level).toBe(25) + }) + + test('opponent pokemon has correct level', () => { + const creature = makeTestCreature() + const init = createBattle([creature], 'squirtle', 15) + expect(init.state.opponentPokemon.level).toBe(15) + }) + + test('battle state has player party', () => { + const creature = makeTestCreature() + const init = createBattle([creature], 'squirtle', 50) + expect(init.state.playerParty.length).toBeGreaterThan(0) + }) + + test('battle state has usable items (empty bag)', () => { + const creature = makeTestCreature() + const init = createBattle([creature], 'squirtle', 50) + expect(init.state.usableItems).toEqual([]) + }) +}) + +describe('executeTurn - extended', () => { + test('item action defaults to move 1', () => { + const creature = makeTestCreature() + const init = createBattle([creature], 'squirtle', 50) + const state = executeTurn(init, { type: 'item', itemId: 'potion' }) + expect(state).toBeDefined() + expect(state.events.length).toBeGreaterThan(0) + }) + + test('battle produces damage or heal events', () => { + const creature = makeTestCreature({ level: 100, ev: { hp: 252, attack: 252, defense: 0, spAtk: 0, spDef: 4, speed: 252 } }) + const init = createBattle([creature], 'squirtle', 5) + const state = executeTurn(init, { type: 'move', moveIndex: 0 }) + const hasDamageOrHeal = state.events.some(e => e.type === 'damage' || e.type === 'heal') + expect(hasDamageOrHeal).toBe(true) + }) +}) + +describe('settleBattle - EV limits', () => { + test('EV total cannot exceed 510', async () => { + const creature = makeTestCreature({ + level: 5, + ev: { hp: 250, attack: 250, defense: 10, spAtk: 0, spDef: 0, speed: 0 }, + }) + const data = makeTestBuddyData([creature]) + const result = { + winner: 'player' as const, + turns: 3, + xpGained: 0, + evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 }, + participantIds: [creature.id], + } + const settlement = await settleBattle(data, result, 'squirtle', 20) + const totalEV = Object.values(settlement.data.creatures[0]!.ev).reduce((a, b) => a + b, 0) + expect(totalEV).toBeLessThanOrEqual(510) + }) + + test('non-participant creatures are unchanged', async () => { + const participant = makeTestCreature({ id: 'p1', level: 5 }) + const bystander = makeTestCreature({ id: 'p2', level: 5, speciesId: 'bulbasaur' }) + const data = makeTestBuddyData([participant, bystander]) + data.party = [participant.id, bystander.id, null, null, null, null] + const result = { + winner: 'player' as const, + turns: 3, + xpGained: 0, + evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 }, + participantIds: [participant.id], + } + const settlement = await settleBattle(data, result, 'squirtle', 20) + const bystanderAfter = settlement.data.creatures.find(c => c.id === 'p2')! + expect(bystanderAfter.totalXp).toBe(bystander.totalXp) + }) + + test('uses all party members as participants when participantIds is empty', async () => { + const c1 = makeTestCreature({ id: 'p1', level: 5 }) + const c2 = makeTestCreature({ id: 'p2', level: 5, speciesId: 'bulbasaur' }) + const data = makeTestBuddyData([c1, c2]) + data.party = [c1.id, c2.id, null, null, null, null] + const result = { + winner: 'player' as const, + turns: 3, + xpGained: 0, + evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 }, + participantIds: [] as string[], + } + const settlement = await settleBattle(data, result, 'squirtle', 20) + expect(settlement.data.creatures.find(c => c.id === 'p1')!.totalXp).toBeGreaterThan(0) + expect(settlement.data.creatures.find(c => c.id === 'p2')!.totalXp).toBeGreaterThan(0) + }) + + test('player win increments battlesWon but not battlesLost', async () => { + const creature = makeTestCreature({ level: 5 }) + const data = makeTestBuddyData([creature]) + const result = { + winner: 'player' as const, + turns: 3, + xpGained: 0, + evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 }, + participantIds: [creature.id], + } + const settlement = await settleBattle(data, result, 'squirtle', 20) + expect(settlement.data.stats.battlesWon).toBe(1) + expect(settlement.data.stats.battlesLost).toBe(0) + }) +}) + +describe('applyMoveLearn - extended', () => { + test('new move has correct PP from Dex', () => { + const creature = makeTestCreature() + const data = makeTestBuddyData([creature]) + const updated = applyMoveLearn(data, creature.id, 'fireblast', 0) + const move = updated.creatures[0]!.moves[0]! + expect(move.id).toBe('fireblast') + expect(move.pp).toBeGreaterThan(0) + expect(move.maxPp).toBeGreaterThan(0) + }) + + test('non-target creatures are unchanged', () => { + const c1 = makeTestCreature({ id: 't1' }) + const c2 = makeTestCreature({ id: 't2', speciesId: 'bulbasaur' }) + const data = makeTestBuddyData([c1, c2]) + const updated = applyMoveLearn(data, 't1', 'fireblast', 0) + const unchanged = updated.creatures.find(c => c.id === 't2')! + expect(unchanged.moves[0]!.id).toBe('flamethrower') + }) +}) + +describe('applyEvolution - extended', () => { + test('friendship increases by 10', () => { + const creature = makeTestCreature({ speciesId: 'charmander', friendship: 70 }) + const data = makeTestBuddyData([creature]) + const updated = applyEvolution(data, creature.id, 'charmeleon') + expect(updated.creatures[0]!.friendship).toBe(80) + }) + + test('friendship capped at 255', () => { + const creature = makeTestCreature({ speciesId: 'charmander', friendship: 250 }) + const data = makeTestBuddyData([creature]) + const updated = applyEvolution(data, creature.id, 'charmeleon') + expect(updated.creatures[0]!.friendship).toBe(255) + }) + + test('multiple evolutions increment counter correctly', () => { + const c1 = makeTestCreature({ id: 't1', speciesId: 'charmander' }) + const c2 = makeTestCreature({ id: 't2', speciesId: 'bulbasaur' }) + const data = makeTestBuddyData([c1, c2]) + let updated = applyEvolution(data, 't1', 'charmeleon') + updated = applyEvolution(updated, 't2', 'ivysaur') + expect(updated.stats.totalEvolutions).toBe(2) + }) +}) diff --git a/packages/pokemon/src/__tests__/experience.test.ts b/packages/pokemon/src/__tests__/experience.test.ts index cbb54fd87..fe73e9ad9 100644 --- a/packages/pokemon/src/__tests__/experience.test.ts +++ b/packages/pokemon/src/__tests__/experience.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from 'bun:test' import { generateCreature } from '../core/creature' import { awardXP, getXpProgress } from '../core/experience' -import { xpForLevel, levelFromXp } from '../data/xpTable' +import { xpForLevel, levelFromXp, xpToNextLevel } from '../data/xpTable' describe('xpForLevel', () => { test('level 1 requires 0 XP', () => { @@ -81,4 +81,73 @@ describe('getXpProgress', () => { expect(progress.current).toBe(0) expect(progress.percentage).toBe(0) }) + + test('level 100 creature has 100% progress', async () => { + const c = await generateCreature('charmander') + c.level = 100 + c.totalXp = 1000000 + const progress = getXpProgress(c) + expect(progress.percentage).toBe(100) + }) + + test('needed is positive for sub-100 creatures', async () => { + const c = await generateCreature('bulbasaur') + c.level = 5 + c.totalXp = xpForLevel(5, 'medium-slow') + const progress = getXpProgress(c) + expect(progress.needed).toBeGreaterThan(0) + expect(progress.current).toBe(0) + }) +}) + +describe('xpToNextLevel', () => { + test('returns XP needed from current to next level', () => { + const xp10 = xpForLevel(10, 'medium-fast') + const xp11 = xpForLevel(11, 'medium-fast') + const needed = xpToNextLevel(10, xp10, 'medium-fast') + expect(needed).toBe(xp11 - xp10) + }) + + test('returns 0 at level 100', () => { + expect(xpToNextLevel(100, 1000000, 'medium-fast')).toBe(0) + }) + + test('accounts for partial XP already earned', () => { + const xp10 = xpForLevel(10, 'medium-fast') + const xp11 = xpForLevel(11, 'medium-fast') + const halfWay = xp10 + Math.floor((xp11 - xp10) / 2) + const needed = xpToNextLevel(10, halfWay, 'medium-fast') + expect(needed).toBe(xp11 - halfWay) + }) +}) + +describe('awardXP - extended', () => { + test('awarding 0 XP returns unchanged creature', async () => { + const c = await generateCreature('bulbasaur') + const result = awardXP(c, 0) + expect(result.creature.totalXp).toBe(c.totalXp) + expect(result.leveledUp).toBe(false) + }) + + test('XP progress is correctly calculated after award', async () => { + const c = await generateCreature('squirtle') + const xpNeeded = xpForLevel(2, 'medium-slow') + const result = awardXP(c, Math.floor(xpNeeded / 2)) + expect(result.creature.xp).toBeGreaterThanOrEqual(0) + }) + + test('multiple small XP awards equal one large award', async () => { + const c1 = await generateCreature('bulbasaur', 42) + const c2 = await generateCreature('bulbasaur', 42) + c2.totalXp = c1.totalXp + + let current = c1 + for (let i = 0; i < 10; i++) { + current = awardXP(current, 100).creature + } + const bigResult = awardXP(c2, 1000) + + expect(current.totalXp).toBe(bigResult.creature.totalXp) + expect(current.level).toBe(bigResult.creature.level) + }) }) diff --git a/packages/pokemon/src/__tests__/storage.test.ts b/packages/pokemon/src/__tests__/storage.test.ts index d5b8ec1e1..357fcf71e 100644 --- a/packages/pokemon/src/__tests__/storage.test.ts +++ b/packages/pokemon/src/__tests__/storage.test.ts @@ -325,3 +325,56 @@ describe('incrementTurns', () => { expect(updated.stats.totalTurns).toBe(data.stats.totalTurns + 1) }) }) + +// ─── Extended coverage ─── + +describe('depositToBox - full boxes', () => { + test('fails when all boxes are full', () => { + const data = makeData() + for (const box of data.boxes) { + for (let i = 0; i < 30; i++) { + box.slots[i] = `filler-${i}` + } + } + const result = depositToBox(data, 'test-id') + expect(result.deposited).toBe(false) + }) +}) + +describe('withdrawFromBox - roundtrip', () => { + test('deposit then withdraw leaves box empty', () => { + const data = makeData() + const deposited = depositToBox(data, 'test-id') + expect(deposited.deposited).toBe(true) + const result = withdrawFromBox(deposited.data, 'test-id') + expect(result.withdrawn).toBe(true) + const slot = result.data.boxes[0]!.slots.find(s => s === 'test-id') + expect(slot).toBeUndefined() + }) +}) + +describe('findCreatureLocation - deposit', () => { + test('finds creature after depositing to box', () => { + const data = makeData() + const deposited = depositToBox(data, 'box-mon') + const loc = findCreatureLocation(deposited.data, 'box-mon') + expect(loc).not.toBeNull() + expect(loc!.area).toBe('box') + }) +}) + +describe('releaseCreature - box', () => { + test('removes creature from box and creatures array', () => { + const data = makeData() + const deposited = depositToBox(data, 'box-mon') + const released = releaseCreature(deposited.data, 'box-mon') + expect(released.creatures.find(c => c.id === 'box-mon')).toBeUndefined() + }) + + test('clears party slot when releasing party member', () => { + const data = makeData(2) + const updated = releaseCreature(data, 'creature-1') + expect(updated.party[1]).toBeNull() + expect(updated.creatures.length).toBe(1) + }) +})