import { describe, it, expect, beforeEach } from 'vitest'; import { BattleEngine } from './BattleEngine'; import type { PicletDefinition } from './types'; import { PicletType, AttackType } from './types'; describe('Field Effects System', () => { let contactAttacker: PicletDefinition; let nonContactAttacker: PicletDefinition; let fieldEffectUser: PicletDefinition; let basicOpponent: PicletDefinition; beforeEach(() => { // Piclet that uses contact moves contactAttacker = { name: "Contact Fighter", description: "Uses contact moves", tier: 'medium', primaryType: PicletType.BEAST, baseStats: { hp: 80, attack: 80, defense: 60, speed: 70 }, nature: "Adamant", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Physical Strike", type: AttackType.BEAST, power: 60, accuracy: 100, pp: 15, priority: 0, flags: ['contact'], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] } ] }; // Piclet that uses non-contact moves nonContactAttacker = { name: "Ranged Fighter", description: "Uses non-contact moves", tier: 'medium', primaryType: PicletType.SPACE, baseStats: { hp: 80, attack: 80, defense: 60, speed: 70 }, nature: "Modest", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Energy Blast", type: AttackType.SPACE, power: 60, accuracy: 100, pp: 15, priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] } ] }; // Piclet that can create field effects fieldEffectUser = { name: "Field Controller", description: "Controls battlefield effects", tier: 'medium', primaryType: PicletType.BEAST, // Changed to BEAST so opponent can damage it baseStats: { hp: 100, attack: 60, defense: 80, speed: 60 }, nature: "Calm", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Contact Barrier", type: AttackType.SPACE, power: 0, accuracy: 100, pp: 10, priority: 0, flags: [], effects: [ { type: 'fieldEffect', effect: 'reflect', target: 'playerSide', stackable: false } ] }, { name: "Non-Contact Barrier", type: AttackType.SPACE, power: 0, accuracy: 100, pp: 10, priority: 0, flags: [], effects: [ { type: 'fieldEffect', effect: 'lightScreen', target: 'playerSide', stackable: false } ] }, { name: "Entry Spikes", type: AttackType.MINERAL, power: 0, accuracy: 100, pp: 10, priority: 0, flags: [], effects: [ { type: 'fieldEffect', effect: 'spikes', target: 'opponentSide', stackable: true } ] }, { name: "Healing Field", type: AttackType.FLORA, power: 0, accuracy: 100, pp: 10, priority: 0, flags: [], effects: [ { type: 'fieldEffect', effect: 'healingMist', target: 'field', stackable: false } ] } ] }; // Basic opponent for testing basicOpponent = { name: "Basic Opponent", description: "Standard test opponent", 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.NORMAL, power: 50, accuracy: 100, pp: 20, priority: 0, flags: ['contact'], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] } ] }; }); describe('Contact Damage Reduction (Reflect)', () => { it('should reduce contact move damage by 50%', () => { const engine = new BattleEngine(fieldEffectUser, contactAttacker); // Set up barrier first engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier { type: 'move', piclet: 'opponent', moveIndex: 0 } // Basic Attack (will be reduced) ); const log = engine.getLog(); expect(log.some(msg => msg.includes('barrier was raised to reduce contact move damage'))).toBe(true); // Test that subsequent contact moves are reduced const initialHp = engine.getState().playerPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier (no effect, already active) { type: 'move', piclet: 'opponent', moveIndex: 0 } // Basic Attack (should be reduced) ); const finalHp = engine.getState().playerPiclet.currentHp; const damage = initialHp - finalHp; // Damage should be significantly reduced (less than normal ~30-40 damage) expect(damage).toBeLessThan(25); expect(damage).toBeGreaterThan(0); // But still some damage }); it('should not reduce non-contact move damage', () => { const engine = new BattleEngine(fieldEffectUser, nonContactAttacker); // Set up contact barrier engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier { type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (non-contact) ); // Test that non-contact moves are NOT reduced const initialHp = engine.getState().playerPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (should not be reduced) ); const finalHp = engine.getState().playerPiclet.currentHp; const damage = initialHp - finalHp; // Damage should be normal (around 30-50) expect(damage).toBeGreaterThan(25); }); }); describe('Non-Contact Damage Reduction (Light Screen)', () => { it('should reduce non-contact move damage by 50%', () => { const engine = new BattleEngine(fieldEffectUser, nonContactAttacker); // Set up non-contact barrier engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 1 }, // Non-Contact Barrier { type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (should be reduced) ); const log = engine.getLog(); expect(log.some(msg => msg.includes('barrier was raised to reduce non-contact move damage'))).toBe(true); // Test reduction on subsequent turn const initialHp = engine.getState().playerPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 1 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (should be reduced) ); const finalHp = engine.getState().playerPiclet.currentHp; const damage = initialHp - finalHp; // Damage should be reduced expect(damage).toBeLessThan(25); expect(damage).toBeGreaterThan(0); }); it('should not reduce contact move damage', () => { const engine = new BattleEngine(fieldEffectUser, contactAttacker); // Set up non-contact barrier engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 1 }, // Non-Contact Barrier { type: 'move', piclet: 'opponent', moveIndex: 0 } // Physical Strike (contact) ); // Test that contact moves are NOT reduced const initialHp = engine.getState().playerPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 1 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Physical Strike (should not be reduced) ); const finalHp = engine.getState().playerPiclet.currentHp; const damage = initialHp - finalHp; // Damage should be normal expect(damage).toBeGreaterThan(25); }); }); describe('Entry Hazards (Spikes)', () => { it('should set up entry spikes on opponent side', () => { const engine = new BattleEngine(fieldEffectUser, basicOpponent); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const log = engine.getLog(); expect(log.some(msg => msg.includes('Entry spikes were scattered'))).toBe(true); // Check that field effect was applied const state = engine.getState(); expect(state.fieldEffects.some(effect => effect.name === 'entryHazardSpikes')).toBe(true); }); it('should be stackable', () => { const engine = new BattleEngine(fieldEffectUser, basicOpponent); // Apply spikes twice engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes { type: 'move', piclet: 'opponent', moveIndex: 0 } ); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes again { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const state = engine.getState(); const spikeEffects = state.fieldEffects.filter(effect => effect.name === 'entryHazardSpikes'); expect(spikeEffects.length).toBe(2); // Should stack }); }); describe('Healing Field', () => { it('should create a healing field that affects both sides', () => { const engine = new BattleEngine(fieldEffectUser, basicOpponent); // Damage both piclets first engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.5); engine['state'].opponentPiclet.currentHp = Math.floor(engine['state'].opponentPiclet.maxHp * 0.5); const playerInitialHp = engine.getState().playerPiclet.currentHp; const opponentInitialHp = engine.getState().opponentPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 3 }, // Healing Field { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const log = engine.getLog(); expect(log.some(msg => msg.includes('healing field was created'))).toBe(true); // Both piclets should be healed at end of turn const playerFinalHp = engine.getState().playerPiclet.currentHp; const opponentFinalHp = engine.getState().opponentPiclet.currentHp; expect(playerFinalHp).toBeGreaterThan(playerInitialHp); expect(opponentFinalHp).toBeGreaterThan(opponentInitialHp); expect(log.some(msg => msg.includes('healed by the healing field'))).toBe(true); }); it('should not heal piclets at full HP', () => { const engine = new BattleEngine(fieldEffectUser, basicOpponent); // Both piclets at full HP const playerInitialHp = engine.getState().playerPiclet.currentHp; const opponentInitialHp = engine.getState().opponentPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 3 }, // Healing Field { type: 'move', piclet: 'opponent', moveIndex: 0 } ); // HP should remain the same const playerFinalHp = engine.getState().playerPiclet.currentHp; const opponentFinalHp = engine.getState().opponentPiclet.currentHp; expect(playerFinalHp).toBe(playerInitialHp); expect(opponentFinalHp).toBe(opponentInitialHp); }); }); describe('Field Effect Duration and Management', () => { it('should expire field effects after 5 turns', () => { const engine = new BattleEngine(fieldEffectUser, basicOpponent); // Create a barrier engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier { type: 'move', piclet: 'opponent', moveIndex: 0 } ); // Verify it exists expect(engine.getState().fieldEffects.length).toBe(1); // Pass 5 turns for (let i = 0; i < 5; i++) { engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); } // Effect should have expired const log = engine.getLog(); expect(log.some(msg => msg.includes('faded away'))).toBe(true); expect(engine.getState().fieldEffects.length).toBe(0); }); it('should not stack non-stackable effects', () => { const engine = new BattleEngine(fieldEffectUser, basicOpponent); // Apply same barrier twice engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier { type: 'move', piclet: 'opponent', moveIndex: 0 } ); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier again { type: 'move', piclet: 'opponent', moveIndex: 0 } ); // Should only have one effect (refreshed duration) const contactBarriers = engine.getState().fieldEffects.filter( effect => effect.name === 'contactDamageReduction' ); expect(contactBarriers.length).toBe(1); }); it('should properly format field effect names in logs', () => { const engine = new BattleEngine(fieldEffectUser, basicOpponent); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier { type: 'move', piclet: 'opponent', moveIndex: 0 } ); // Pass enough turns for effect to fade for (let i = 0; i < 5; i++) { engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); } const log = engine.getLog(); expect(log.some(msg => msg.includes('Contact damage barrier faded away') || msg.includes('contact damage barrier faded away') )).toBe(true); }); }); describe('Field Effect Integration with Battle Flow', () => { it('should apply field effects during damage calculation', () => { const engine = new BattleEngine(fieldEffectUser, contactAttacker); // Measure baseline damage first const baselineEngine = new BattleEngine(fieldEffectUser, contactAttacker); const baselineInitialHp = baselineEngine.getState().playerPiclet.currentHp; baselineEngine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // No barrier ); const baselineDamage = baselineInitialHp - baselineEngine.getState().playerPiclet.currentHp; // Now test with barrier engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const protectedInitialHp = engine.getState().playerPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Attack against barrier ); const protectedDamage = protectedInitialHp - engine.getState().playerPiclet.currentHp; // Protected damage should be significantly less expect(protectedDamage).toBeLessThan(baselineDamage * 0.75); }); it('should handle multiple field effects simultaneously', () => { const engine = new BattleEngine(fieldEffectUser, basicOpponent); // Apply multiple field effects engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier { type: 'move', piclet: 'opponent', moveIndex: 0 } ); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 3 }, // Healing Field { type: 'move', piclet: 'opponent', moveIndex: 0 } ); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes { type: 'move', piclet: 'opponent', moveIndex: 0 } ); // Should have 3 different field effects const state = engine.getState(); expect(state.fieldEffects.length).toBe(3); const effectNames = state.fieldEffects.map(effect => effect.name); expect(effectNames).toContain('contactDamageReduction'); expect(effectNames).toContain('healingField'); expect(effectNames).toContain('entryHazardSpikes'); }); }); });