import { describe, it, expect, beforeEach } from 'vitest'; import { BattleEngine } from './BattleEngine'; import type { PicletDefinition } from './types'; import { PicletType, AttackType } from './types'; describe('Advanced Status Effects System', () => { let basicPiclet: PicletDefinition; let statusInflicter: PicletDefinition; beforeEach(() => { // Basic piclet without special abilities 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' }] } ] }; // Piclet that can inflict status effects statusInflicter = { name: "Status Master", description: "Can inflict various status effects", tier: 'medium', primaryType: PicletType.CULTURE, baseStats: { hp: 90, attack: 50, defense: 70, speed: 80 }, nature: "Timid", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Freeze Ray", type: AttackType.AQUATIC, power: 40, accuracy: 90, pp: 15, priority: 0, flags: [], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' }, { type: 'applyStatus', target: 'opponent', status: 'freeze', chance: 30 } ] }, { name: "Paralyzing Shock", type: AttackType.MACHINA, power: 45, accuracy: 100, pp: 20, priority: 0, flags: [], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' }, { type: 'applyStatus', target: 'opponent', status: 'paralyze', chance: 25 } ] }, { name: "Sleep Powder", type: AttackType.FLORA, power: 0, accuracy: 85, pp: 15, priority: 0, flags: [], effects: [ { type: 'applyStatus', target: 'opponent', status: 'sleep', chance: 100 } ] }, { name: "Confuse Ray", type: AttackType.SPACE, power: 0, accuracy: 100, pp: 10, priority: 0, flags: [], effects: [ { type: 'applyStatus', target: 'opponent', status: 'confuse', chance: 100 } ] } ] }; }); describe('Freeze Status Effect', () => { it('should prevent the frozen piclet from acting', () => { const engine = new BattleEngine(statusInflicter, basicPiclet); // Force freeze to trigger by mocking Math.random const originalRandom = Math.random; Math.random = () => 0.1; // 10% < 30% chance, should trigger freeze engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Freeze Ray { type: 'move', piclet: 'opponent', moveIndex: 0 } // Should be prevented if frozen ); // Restore Math.random Math.random = originalRandom; const log = engine.getLog(); const opponentState = engine.getState().opponentPiclet; // Check that freeze was applied expect(opponentState.statusEffects).toContain('freeze'); expect(log.some(msg => msg.includes('was frozen'))).toBe(true); // Execute another turn to test freeze preventing action if (!engine.isGameOver()) { engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const secondTurnLog = engine.getLog(); expect(secondTurnLog.some(msg => msg.includes('is frozen solid') || msg.includes('cannot move'))).toBe(true); } }); it('should have a chance to thaw each turn', () => { const engine = new BattleEngine(statusInflicter, basicPiclet); // Manually apply freeze status engine['state'].opponentPiclet.statusEffects.push('freeze'); // Force thaw with low random number const originalRandom = Math.random; Math.random = () => 0.1; // Should trigger thaw (usually 20% chance) engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); // Restore Math.random Math.random = originalRandom; const log = engine.getLog(); const opponentState = engine.getState().opponentPiclet; // Should thaw and be able to act expect(log.some(msg => msg.includes('thawed out') || msg.includes('is no longer frozen'))).toBe(true); expect(opponentState.statusEffects).not.toContain('freeze'); }); }); describe('Paralysis Status Effect', () => { it('should reduce speed by 50%', () => { const engine = new BattleEngine(statusInflicter, basicPiclet); const initialSpeed = engine.getState().opponentPiclet.speed; // Force paralysis to trigger const originalRandom = Math.random; Math.random = () => 0.1; // 10% < 25% chance engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 1 }, // Paralyzing Shock { type: 'move', piclet: 'opponent', moveIndex: 0 } ); // Restore Math.random Math.random = originalRandom; const finalSpeed = engine.getState().opponentPiclet.speed; const log = engine.getLog(); expect(engine.getState().opponentPiclet.statusEffects).toContain('paralyze'); expect(log.some(msg => msg.includes('was paralyzed'))).toBe(true); expect(finalSpeed).toBe(Math.floor(initialSpeed * 0.5)); // 50% speed reduction }); it('should have 25% chance to prevent action', () => { const engine = new BattleEngine(statusInflicter, basicPiclet); // Manually apply paralysis engine['state'].opponentPiclet.statusEffects.push('paralyze'); engine['state'].opponentPiclet.speed = Math.floor(engine['state'].opponentPiclet.speed * 0.5); // Force paralysis to prevent action const originalRandom = Math.random; Math.random = () => 0.1; // Should trigger paralysis prevention (25% chance) engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Should be prevented ); // Restore Math.random Math.random = originalRandom; const log = engine.getLog(); expect(log.some(msg => msg.includes('is fully paralyzed') || msg.includes('cannot move due to paralysis') )).toBe(true); }); }); describe('Sleep Status Effect', () => { it('should prevent action and last 1-3 turns', () => { const engine = new BattleEngine(statusInflicter, basicPiclet); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 2 }, // Sleep Powder { type: 'move', piclet: 'opponent', moveIndex: 0 } // Should be prevented if asleep ); const log = engine.getLog(); const opponentState = engine.getState().opponentPiclet; expect(opponentState.statusEffects).toContain('sleep'); expect(log.some(msg => msg.includes('fell asleep'))).toBe(true); // Sleep should prevent action if (!engine.isGameOver()) { engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const secondLog = engine.getLog(); expect(secondLog.some(msg => msg.includes('is fast asleep') || msg.includes('cannot wake up') )).toBe(true); } }); it('should wake up when attacked', () => { const engine = new BattleEngine(basicPiclet, statusInflicter); // Put player to sleep engine['state'].playerPiclet.statusEffects.push('sleep'); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Should be prevented by sleep { type: 'move', piclet: 'opponent', moveIndex: 0 } // Attack should wake up player ); const log = engine.getLog(); const playerState = engine.getState().playerPiclet; // Should wake up when damaged expect(log.some(msg => msg.includes('woke up'))).toBe(true); expect(playerState.statusEffects).not.toContain('sleep'); }); }); describe('Confusion Status Effect', () => { it('should last 2-5 turns and cause self-damage 33% of the time', () => { const engine = new BattleEngine(statusInflicter, basicPiclet); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 3 }, // Confuse Ray { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const log = engine.getLog(); const opponentState = engine.getState().opponentPiclet; expect(opponentState.statusEffects).toContain('confuse'); expect(log.some(msg => msg.includes('became confused'))).toBe(true); // Test confusion self-damage const initialHp = engine.getState().opponentPiclet.currentHp; // Force confusion self-damage const originalRandom = Math.random; Math.random = () => 0.2; // Should trigger self-damage (33% chance) if (!engine.isGameOver()) { engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const confusedLog = engine.getLog(); const finalHp = engine.getState().opponentPiclet.currentHp; expect(confusedLog.some(msg => msg.includes('hurt itself in confusion') || msg.includes('attacked itself') )).toBe(true); } // Restore Math.random Math.random = originalRandom; }); it('should wear off after 2-5 turns', () => { const engine = new BattleEngine(statusInflicter, basicPiclet); // Manually apply confusion with duration engine['state'].opponentPiclet.statusEffects.push('confuse'); (engine['state'].opponentPiclet as any).confusionTurns = 1; // Set to expire next turn engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const log = engine.getLog(); const opponentState = engine.getState().opponentPiclet; expect(log.some(msg => msg.includes('is no longer confused') || msg.includes('snapped out of confusion'))).toBe(true); expect(opponentState.statusEffects).not.toContain('confuse'); }); }); describe('Status Effect Interactions', () => { it('should not allow multiple major status effects simultaneously', () => { const engine = new BattleEngine(statusInflicter, basicPiclet); // Apply freeze first engine['state'].opponentPiclet.statusEffects.push('freeze'); // Try to apply paralysis const originalRandom = Math.random; Math.random = () => 0.1; // Should trigger paralysis normally engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 1 }, // Paralyzing Shock { type: 'move', piclet: 'opponent', moveIndex: 0 } ); Math.random = originalRandom; const opponentState = engine.getState().opponentPiclet; const majorStatuses = opponentState.statusEffects.filter(status => ['freeze', 'paralyze', 'sleep'].includes(status) ); // Should only have one major status effect expect(majorStatuses.length).toBeLessThanOrEqual(1); }); it('should allow confusion alongside other status effects', () => { const engine = new BattleEngine(statusInflicter, basicPiclet); // Apply paralysis first engine['state'].opponentPiclet.statusEffects.push('paralyze'); // Apply confusion engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 3 }, // Confuse Ray { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const opponentState = engine.getState().opponentPiclet; // Should have both paralysis and confusion expect(opponentState.statusEffects).toContain('paralyze'); expect(opponentState.statusEffects).toContain('confuse'); }); }); });