import { describe, it, expect, beforeEach } from 'vitest'; import { BattleEngine } from './BattleEngine'; import type { PicletDefinition } from './types'; import { PicletType, AttackType } from './types'; describe('Switching System', () => { let basicPiclet: PicletDefinition; let reservePiclet: PicletDefinition; let hazardSetter: PicletDefinition; let switchTriggerPiclet: PicletDefinition; beforeEach(() => { // Basic piclet for primary battles basicPiclet = { name: "Basic Fighter", description: "Standard test piclet", tier: 'medium', primaryType: PicletType.BEAST, baseStats: { hp: 80, attack: 60, defense: 60, speed: 60 }, nature: "Hardy", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Basic Attack", type: AttackType.BEAST, power: 50, accuracy: 100, pp: 20, priority: 0, flags: ['contact'], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] } ] }; // Reserve piclet for switching reservePiclet = { name: "Reserve Fighter", description: "Backup piclet", tier: 'medium', primaryType: PicletType.FLORA, baseStats: { hp: 90, attack: 50, defense: 70, speed: 40 }, nature: "Calm", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Leaf Strike", type: AttackType.FLORA, power: 45, accuracy: 100, pp: 25, priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] } ] }; // Piclet that can set entry hazards hazardSetter = { name: "Hazard Master", description: "Sets entry hazards", tier: 'medium', primaryType: PicletType.MINERAL, baseStats: { hp: 70, attack: 40, defense: 80, speed: 50 }, nature: "Bold", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Spike Trap", type: AttackType.MINERAL, power: 0, accuracy: 100, pp: 20, priority: 0, flags: [], effects: [ { type: 'fieldEffect', effect: 'spikes', target: 'opponentSide', stackable: true } ] }, { name: "Toxic Spikes", type: AttackType.MINERAL, power: 0, accuracy: 100, pp: 20, priority: 0, flags: [], effects: [ { type: 'fieldEffect', effect: 'toxicSpikes', target: 'opponentSide', stackable: true } ] } ] }; // Piclet with switch-triggered abilities switchTriggerPiclet = { name: "Switch Specialist", description: "Has switch-in/out abilities", tier: 'medium', primaryType: PicletType.CULTURE, baseStats: { hp: 85, attack: 55, defense: 65, speed: 75 }, nature: "Timid", specialAbility: { name: "Intimidate", description: "Lowers opponent's attack on switch-in", triggers: [ { event: 'onSwitchIn', condition: 'always', effects: [ { type: 'modifyStats', target: 'opponent', stats: { attack: 'decrease' } } ] } ] }, movepool: [ { name: "Quick Strike", type: AttackType.CULTURE, power: 40, accuracy: 100, pp: 30, priority: 1, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] } ] }; }); describe('Basic Switch Actions', () => { it('should allow switching to a different piclet', () => { // Create engine with rosters const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]); const initialPlayerName = engine.getState().playerPiclet.definition.name; expect(initialPlayerName).toBe("Basic Fighter"); // Execute switch action engine.executeActions( { type: 'switch', piclet: 'player', newPicletIndex: 1 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const finalPlayerName = engine.getState().playerPiclet.definition.name; const log = engine.getLog(); expect(finalPlayerName).toBe("Reserve Fighter"); expect(log.some(msg => msg.includes('switched') && msg.includes('Reserve Fighter'))).toBe(true); }); it('should handle switch action priority correctly', () => { const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]); // Switch actions should have higher priority than moves engine.executeActions( { type: 'switch', piclet: 'player', newPicletIndex: 1 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const log = engine.getLog(); // Switch should happen before the opponent's move const switchIndex = log.findIndex(msg => msg.includes('switched')); const moveIndex = log.findIndex(msg => msg.includes('used') && msg.includes('Basic Attack')); expect(switchIndex).toBeLessThan(moveIndex); }); it('should not allow switching to same piclet', () => { const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]); // Try to switch to the same piclet (index 0) engine.executeActions( { type: 'switch', piclet: 'player', newPicletIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const log = engine.getLog(); expect(log.some(msg => msg.includes('already active') || msg.includes('cannot switch'))).toBe(true); }); it('should not allow switching to fainted piclet', () => { const faintedPiclet = { ...reservePiclet }; const engine = new BattleEngine([basicPiclet, faintedPiclet], [basicPiclet]); // Mock fainted piclet by accessing private roster states (engine as any).playerRosterStates[1].fainted = true; (engine as any).playerRosterStates[1].currentHp = 0; engine.executeActions( { type: 'switch', piclet: 'player', newPicletIndex: 1 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const log = engine.getLog(); expect(log.some(msg => msg.includes('fainted') || msg.includes('unable to battle'))).toBe(true); }); }); describe('Entry Hazards', () => { it('should apply spikes damage on switch-in', () => { const engine = new BattleEngine([hazardSetter, basicPiclet], [basicPiclet, reservePiclet]); // Set up spikes engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Spike Trap { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const log = engine.getLog(); expect(log.some(msg => msg.includes('spikes') || msg.includes('hazard'))).toBe(true); // Switch opponent to trigger spikes const initialHp = engine.getState().opponentPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'switch', piclet: 'opponent', newPicletIndex: 1 } ); const finalLog = engine.getLog(); expect(finalLog.some(msg => msg.includes('hurt by spikes') || msg.includes('stepped on spikes'))).toBe(true); }); it('should apply toxic spikes status on switch-in', () => { const engine = new BattleEngine([hazardSetter], [basicPiclet, reservePiclet]); // Set up toxic spikes engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 1 }, // Toxic Spikes { type: 'move', piclet: 'opponent', moveIndex: 0 } ); // Switch opponent to trigger toxic spikes engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'switch', piclet: 'opponent', newPicletIndex: 1 } ); const finalState = engine.getState().opponentPiclet; const log = engine.getLog(); expect(finalState.statusEffects).toContain('poison'); expect(log.some(msg => msg.includes('poisoned by toxic spikes'))).toBe(true); }); it('should stack multiple layers of spikes', () => { const engine = new BattleEngine([hazardSetter], [basicPiclet, reservePiclet]); // Set up multiple spike layers engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // First Spike Trap { type: 'move', piclet: 'opponent', moveIndex: 0 } ); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Second Spike Trap { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const fieldEffects = engine.getState().fieldEffects; const spikeCount = fieldEffects.filter(effect => effect.name === 'entryHazardSpikes').length; expect(spikeCount).toBeGreaterThan(1); }); }); describe('Switch-In/Out Ability Triggers', () => { it('should trigger onSwitchIn ability when piclet enters battle', () => { const engine = new BattleEngine([basicPiclet, switchTriggerPiclet], [basicPiclet]); const initialOpponentAttack = engine.getState().opponentPiclet.attack; // Switch in the intimidate piclet engine.executeActions( { type: 'switch', piclet: 'player', newPicletIndex: 1 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const finalOpponentAttack = engine.getState().opponentPiclet.attack; const log = engine.getLog(); expect(finalOpponentAttack).toBeLessThan(initialOpponentAttack); expect(log.some(msg => msg.includes('Intimidate') && msg.includes('triggered'))).toBe(true); }); it('should trigger onSwitchOut ability when piclet leaves battle', () => { const switchOutPiclet: PicletDefinition = { ...switchTriggerPiclet, specialAbility: { name: "Parting Shot", description: "Lowers opponent's stats on switch-out", triggers: [ { event: 'onSwitchOut', condition: 'always', effects: [ { type: 'modifyStats', target: 'opponent', stats: { attack: 'decrease', defense: 'decrease' } } ] } ] } }; const engine = new BattleEngine([switchOutPiclet, reservePiclet], [basicPiclet]); const initialOpponentAttack = engine.getState().opponentPiclet.attack; const initialOpponentDefense = engine.getState().opponentPiclet.defense; // Switch out the parting shot piclet engine.executeActions( { type: 'switch', piclet: 'player', newPicletIndex: 1 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const finalOpponentAttack = engine.getState().opponentPiclet.attack; const finalOpponentDefense = engine.getState().opponentPiclet.defense; const log = engine.getLog(); expect(finalOpponentAttack).toBeLessThan(initialOpponentAttack); expect(finalOpponentDefense).toBeLessThan(initialOpponentDefense); expect(log.some(msg => msg.includes('Parting Shot') && msg.includes('triggered'))).toBe(true); }); }); describe('Forced Switching', () => { it('should handle forced switch when active piclet faints', () => { const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]); // Damage player piclet to near-faint engine['state'].playerPiclet.currentHp = 1; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Should KO player ); const log = engine.getLog(); // Should prompt for forced switch or auto-switch if only one option expect(log.some(msg => msg.includes('fainted') || msg.includes('must choose') || msg.includes('forced switch') )).toBe(true); }); it('should end battle if no valid switches remain', () => { const engine = new BattleEngine([basicPiclet], [basicPiclet]); // Only one piclet each // KO the only piclet engine['state'].playerPiclet.currentHp = 1; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); expect(engine.isGameOver()).toBe(true); expect(engine.getState().winner).toBe('opponent'); }); }); describe('Switch Action Integration', () => { it('should preserve PP and status when switching back', () => { const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]); // Use a move to reduce PP engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const ppAfterMove = engine.getState().playerPiclet.moves[0].currentPP; // Switch out and back engine.executeActions( { type: 'switch', piclet: 'player', newPicletIndex: 1 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); engine.executeActions( { type: 'switch', piclet: 'player', newPicletIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const ppAfterReturn = engine.getState().playerPiclet.moves[0].currentPP; // PP should be preserved expect(ppAfterReturn).toBe(ppAfterMove); }); it('should reset stat modifications when switching', () => { const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]); // Apply stat modification engine['state'].playerPiclet.attack += 20; // Simulate boost engine['state'].playerPiclet.statModifiers.attack = 1; const boostedAttack = engine.getState().playerPiclet.attack; // Switch out and back engine.executeActions( { type: 'switch', piclet: 'player', newPicletIndex: 1 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); engine.executeActions( { type: 'switch', piclet: 'player', newPicletIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const finalAttack = engine.getState().playerPiclet.attack; // Attack should be reset to base value expect(finalAttack).toBeLessThan(boostedAttack); expect(engine.getState().playerPiclet.statModifiers.attack).toBeFalsy(); }); }); });