| |
| |
| |
| |
|
|
| 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); |
| }); |
| }); |
| }); |