|
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest'; |
|
import { BattleEngine } from './BattleEngine'; |
|
import { |
|
STELLAR_WOLF, |
|
TOXIC_CRAWLER, |
|
BERSERKER_BEAST, |
|
AQUA_GUARDIAN, |
|
BASIC_TACKLE, |
|
FLAME_BURST, |
|
HEALING_LIGHT, |
|
POWER_UP, |
|
BERSERKER_END, |
|
TOXIC_STING |
|
} from './test-data'; |
|
import { BattleAction } from './types'; |
|
|
|
describe('BattleEngine', () => { |
|
let engine: BattleEngine; |
|
|
|
beforeEach(() => { |
|
engine = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER); |
|
}); |
|
|
|
describe('Battle Initialization', () => { |
|
it('should initialize battle state correctly', () => { |
|
const state = engine.getState(); |
|
|
|
expect(state.turn).toBe(1); |
|
expect(state.phase).toBe('selection'); |
|
expect(state.playerPiclet.definition.name).toBe('Stellar Wolf'); |
|
expect(state.opponentPiclet.definition.name).toBe('Toxic Crawler'); |
|
expect(state.winner).toBeUndefined(); |
|
expect(state.log.length).toBe(0); |
|
}); |
|
|
|
it('should calculate battle stats correctly', () => { |
|
const state = engine.getState(); |
|
const player = state.playerPiclet; |
|
|
|
|
|
expect(player.maxHp).toBe(STELLAR_WOLF.baseStats.hp); |
|
expect(player.attack).toBe(STELLAR_WOLF.baseStats.attack); |
|
expect(player.defense).toBe(STELLAR_WOLF.baseStats.defense); |
|
expect(player.speed).toBe(STELLAR_WOLF.baseStats.speed); |
|
expect(player.currentHp).toBe(player.maxHp); |
|
}); |
|
|
|
it('should initialize moves with correct PP', () => { |
|
const state = engine.getState(); |
|
const playerMoves = state.playerPiclet.moves; |
|
|
|
expect(playerMoves).toHaveLength(4); |
|
expect(playerMoves[0].move.name).toBe('Tackle'); |
|
expect(playerMoves[0].currentPP).toBe(35); |
|
expect(playerMoves[1].move.name).toBe('Flame Burst'); |
|
expect(playerMoves[1].currentPP).toBe(15); |
|
}); |
|
}); |
|
|
|
describe('Basic Battle Flow', () => { |
|
it('should execute a basic turn', () => { |
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
engine.executeActions(playerAction, opponentAction); |
|
|
|
const state = engine.getState(); |
|
expect(state.turn).toBe(2); |
|
expect(state.phase).toBe('selection'); |
|
expect(state.log.length).toBeGreaterThan(2); |
|
}); |
|
|
|
it('should consume PP when moves are used', () => { |
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
const initialPP = engine.getState().playerPiclet.moves[0].currentPP; |
|
engine.executeActions(playerAction, opponentAction); |
|
const finalPP = engine.getState().playerPiclet.moves[0].currentPP; |
|
|
|
expect(finalPP).toBe(initialPP - 1); |
|
}); |
|
|
|
it('should handle moves with no PP', () => { |
|
|
|
const state = engine.getState(); |
|
engine['state'].playerPiclet.moves[0].currentPP = 0; |
|
|
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
engine.executeActions(playerAction, opponentAction); |
|
|
|
const log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('no PP left'))).toBe(true); |
|
}); |
|
}); |
|
|
|
describe('Damage Calculation', () => { |
|
it('should calculate basic damage correctly', () => { |
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
const initialHp = engine.getState().opponentPiclet.currentHp; |
|
engine.executeActions(playerAction, opponentAction); |
|
const finalHp = engine.getState().opponentPiclet.currentHp; |
|
|
|
expect(finalHp).toBeLessThan(initialHp); |
|
expect(finalHp).toBeGreaterThan(0); |
|
}); |
|
|
|
it('should apply type effectiveness correctly', () => { |
|
|
|
const spaceVsBug = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER); |
|
|
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
const initialHp = spaceVsBug.getState().opponentPiclet.currentHp; |
|
spaceVsBug.executeActions(playerAction, opponentAction); |
|
|
|
const log = spaceVsBug.getLog(); |
|
expect(log.some(msg => msg.includes("It's super effective!"))).toBe(true); |
|
}); |
|
|
|
it('should apply STAB (Same Type Attack Bonus)', () => { |
|
|
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
const initialHp = engine.getState().opponentPiclet.currentHp; |
|
engine.executeActions(playerAction, opponentAction); |
|
const finalHp = engine.getState().opponentPiclet.currentHp; |
|
|
|
|
|
expect(finalHp).toBeLessThan(initialHp); |
|
}); |
|
}); |
|
|
|
describe('Status Effects', () => { |
|
it('should apply poison status', () => { |
|
const toxicEngine = new BattleEngine(TOXIC_CRAWLER, STELLAR_WOLF); |
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
toxicEngine.executeActions(playerAction, opponentAction); |
|
|
|
const state = toxicEngine.getState(); |
|
expect(state.opponentPiclet.statusEffects).toContain('poison'); |
|
}); |
|
|
|
it('should process poison damage at turn end', () => { |
|
const toxicEngine = new BattleEngine(TOXIC_CRAWLER, STELLAR_WOLF); |
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
toxicEngine.executeActions(playerAction, opponentAction); |
|
|
|
const hpAfterPoison = toxicEngine.getState().opponentPiclet.currentHp; |
|
|
|
|
|
toxicEngine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const hpAfterSecondTurn = toxicEngine.getState().opponentPiclet.currentHp; |
|
expect(hpAfterSecondTurn).toBeLessThan(hpAfterPoison); |
|
|
|
const log = toxicEngine.getLog(); |
|
expect(log.some(msg => msg.includes('hurt by poison'))).toBe(true); |
|
}); |
|
}); |
|
|
|
describe('Stat Modifications', () => { |
|
it('should increase attack stat', () => { |
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 3 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
const initialAttack = engine.getState().playerPiclet.attack; |
|
engine.executeActions(playerAction, opponentAction); |
|
const finalAttack = engine.getState().playerPiclet.attack; |
|
|
|
expect(finalAttack).toBeGreaterThan(initialAttack); |
|
|
|
const log = engine.getLog(); |
|
expect(log.some(msg => msg.includes("attack rose"))).toBe(true); |
|
}); |
|
}); |
|
|
|
describe('Healing Effects', () => { |
|
it('should heal HP correctly', () => { |
|
|
|
engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.5); |
|
|
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 2 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
const hpBeforeHeal = engine.getState().playerPiclet.currentHp; |
|
engine.executeActions(playerAction, opponentAction); |
|
const hpAfterHeal = engine.getState().playerPiclet.currentHp; |
|
|
|
expect(hpAfterHeal).toBeGreaterThan(hpBeforeHeal); |
|
|
|
const log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('recovered') && msg.includes('HP'))).toBe(true); |
|
}); |
|
|
|
it('should not heal above max HP', () => { |
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 2 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
engine.executeActions(playerAction, opponentAction); |
|
|
|
const state = engine.getState(); |
|
expect(state.playerPiclet.currentHp).toBeLessThanOrEqual(state.playerPiclet.maxHp); |
|
}); |
|
}); |
|
|
|
describe('Conditional Effects', () => { |
|
it('should trigger conditional effects when conditions are met', () => { |
|
const berserkerEngine = new BattleEngine(BERSERKER_BEAST, STELLAR_WOLF); |
|
|
|
|
|
berserkerEngine['state'].playerPiclet.currentHp = Math.floor(berserkerEngine['state'].playerPiclet.maxHp * 0.2); |
|
|
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
const initialDefense = berserkerEngine.getState().playerPiclet.defense; |
|
berserkerEngine.executeActions(playerAction, opponentAction); |
|
const finalDefense = berserkerEngine.getState().playerPiclet.defense; |
|
|
|
|
|
expect(finalDefense).toBeLessThan(initialDefense); |
|
}); |
|
|
|
it('should not trigger conditional effects when conditions are not met', () => { |
|
const berserkerEngine = new BattleEngine(BERSERKER_BEAST, STELLAR_WOLF); |
|
|
|
|
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
const initialDefense = berserkerEngine.getState().playerPiclet.defense; |
|
berserkerEngine.executeActions(playerAction, opponentAction); |
|
const finalDefense = berserkerEngine.getState().playerPiclet.defense; |
|
|
|
|
|
expect(finalDefense).toBe(initialDefense); |
|
}); |
|
}); |
|
|
|
describe('Battle End Conditions', () => { |
|
it('should end battle when player Piclet faints', () => { |
|
|
|
engine['state'].playerPiclet.currentHp = 0; |
|
|
|
|
|
engine['checkBattleEnd'](); |
|
|
|
expect(engine.isGameOver()).toBe(true); |
|
expect(engine.getWinner()).toBe('opponent'); |
|
}); |
|
|
|
it('should end battle when opponent Piclet faints', () => { |
|
engine['state'].opponentPiclet.currentHp = 1; |
|
|
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
engine.executeActions(playerAction, opponentAction); |
|
|
|
expect(engine.isGameOver()).toBe(true); |
|
expect(engine.getWinner()).toBe('player'); |
|
}); |
|
|
|
it('should handle draw when both Piclets faint', () => { |
|
|
|
engine['state'].playerPiclet.currentHp = 0; |
|
engine['state'].opponentPiclet.currentHp = 0; |
|
|
|
|
|
engine['checkBattleEnd'](); |
|
|
|
expect(engine.isGameOver()).toBe(true); |
|
expect(engine.getWinner()).toBe('draw'); |
|
}); |
|
}); |
|
|
|
describe('Move Accuracy', () => { |
|
it('should handle move misses', () => { |
|
|
|
const originalRandom = Math.random; |
|
Math.random = () => 0.99; |
|
|
|
const berserkerEngine = new BattleEngine(BERSERKER_BEAST, STELLAR_WOLF); |
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
const initialHp = berserkerEngine.getState().opponentPiclet.currentHp; |
|
berserkerEngine.executeActions(playerAction, opponentAction); |
|
const finalHp = berserkerEngine.getState().opponentPiclet.currentHp; |
|
|
|
|
|
expect(finalHp).toBe(initialHp); |
|
|
|
const log = berserkerEngine.getLog(); |
|
expect(log.some(msg => msg.includes('attack missed'))).toBe(true); |
|
|
|
|
|
Math.random = originalRandom; |
|
}); |
|
}); |
|
|
|
describe('Action Priority', () => { |
|
it('should execute higher priority moves first', () => { |
|
|
|
const highPriorityMove = { |
|
...BASIC_TACKLE, |
|
name: "Quick Attack", |
|
priority: 1 |
|
}; |
|
|
|
const customWolf = { |
|
...STELLAR_WOLF, |
|
movepool: [highPriorityMove, BASIC_TACKLE, HEALING_LIGHT, POWER_UP] |
|
}; |
|
|
|
const priorityEngine = new BattleEngine(customWolf, TOXIC_CRAWLER); |
|
|
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
priorityEngine.executeActions(playerAction, opponentAction); |
|
|
|
const log = priorityEngine.getLog(); |
|
const playerMoveIndex = log.findIndex(msg => msg.includes('used Quick Attack')); |
|
const opponentMoveIndex = log.findIndex(msg => msg.includes('used Tackle')); |
|
|
|
expect(playerMoveIndex).toBeLessThan(opponentMoveIndex); |
|
}); |
|
|
|
it('should use speed for same priority moves', () => { |
|
|
|
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; |
|
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; |
|
|
|
engine.executeActions(playerAction, opponentAction); |
|
|
|
const log = engine.getLog(); |
|
const stellarWolfIndex = log.findIndex(msg => msg.includes('Stellar Wolf used')); |
|
const toxicCrawlerIndex = log.findIndex(msg => msg.includes('Toxic Crawler used')); |
|
|
|
|
|
expect(stellarWolfIndex).toBeLessThan(toxicCrawlerIndex); |
|
}); |
|
}); |
|
}); |