File size: 15,198 Bytes
1ecc382 ba9896a 1ecc382 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
/**
* Test suite for the Battle Engine
* Tests battle flow, damage calculation, effects, and type effectiveness
*/
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;
// Level 50 should have base stats (no modifier)
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', () => {
// Manually set PP to 0 by getting mutable state
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 }; // Tackle
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); // Should not be a one-hit KO
});
it('should apply type effectiveness correctly', () => {
// Create engine with type advantage: Space vs Bug (Space is 2x effective vs Bug)
const spaceVsBug = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER);
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; // Flame Burst (Space type)
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)', () => {
// Stellar Wolf using Flame Burst (Space type move, matches primary type)
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;
// With STAB, damage should be higher than without
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 }; // Toxic Sting
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 }; // Toxic Sting
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
toxicEngine.executeActions(playerAction, opponentAction);
const hpAfterPoison = toxicEngine.getState().opponentPiclet.currentHp;
// Execute another turn to trigger poison damage
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 }; // Power Up
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', () => {
// Damage the player first by directly modifying the internal state
engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.5);
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 2 }; // Healing Light
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 }; // Healing Light
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);
// Set player to low HP to trigger condition
berserkerEngine['state'].playerPiclet.currentHp = Math.floor(berserkerEngine['state'].playerPiclet.maxHp * 0.2);
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; // Berserker's End
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;
// Defense should be greatly decreased due to low HP condition
expect(finalDefense).toBeLessThan(initialDefense);
});
it('should not trigger conditional effects when conditions are not met', () => {
const berserkerEngine = new BattleEngine(BERSERKER_BEAST, STELLAR_WOLF);
// Player at full HP - condition not met
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; // Berserker's End
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;
// Defense should remain unchanged
expect(finalDefense).toBe(initialDefense);
});
});
describe('Battle End Conditions', () => {
it('should end battle when player Piclet faints', () => {
// Set player HP to 0 to guarantee fainting
engine['state'].playerPiclet.currentHp = 0;
// Force battle end check
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; // Set to very low HP
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', () => {
// Set both HP to 0 to guarantee draw
engine['state'].playerPiclet.currentHp = 0;
engine['state'].opponentPiclet.currentHp = 0;
// Force battle end check
engine['checkBattleEnd']();
expect(engine.isGameOver()).toBe(true);
expect(engine.getWinner()).toBe('draw');
});
});
describe('Move Accuracy', () => {
it('should handle move misses', () => {
// Mock Math.random to force a miss
const originalRandom = Math.random;
Math.random = () => 0.99; // Force miss for 90% accuracy moves
const berserkerEngine = new BattleEngine(BERSERKER_BEAST, STELLAR_WOLF);
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; // Berserker's End (90% accuracy)
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;
// HP should be unchanged due to miss
expect(finalHp).toBe(initialHp);
const log = berserkerEngine.getLog();
expect(log.some(msg => msg.includes('attack missed'))).toBe(true);
// Restore original Math.random
Math.random = originalRandom;
});
});
describe('Action Priority', () => {
it('should execute higher priority moves first', () => {
// Create a custom high-priority move for testing
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 }; // Quick Attack (priority 1)
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; // Tackle (priority 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', () => {
// Both using same priority moves, faster should go first
const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; // Tackle
const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; // Tackle
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'));
// Stellar Wolf has higher speed (70 vs 55), so should go first
expect(stellarWolfIndex).toBeLessThan(toxicCrawlerIndex);
});
});
}); |