import { describe, it, expect, beforeEach } from 'vitest'; import { BattleEngine } from './BattleEngine'; import type { PicletDefinition, SpecialAbility } from './types'; import { PicletType, AttackType } from './types'; describe('Special Ability Triggers System', () => { let basicPiclet: PicletDefinition; let abilityPiclet: 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 with special abilities for testing abilityPiclet = { name: "Ability User", description: "Has special abilities", tier: 'medium', primaryType: PicletType.BEAST, baseStats: { hp: 100, attack: 70, defense: 70, speed: 50 }, nature: "Bold", specialAbility: { name: "Test Ability", description: "Triggers on various events", triggers: [ { event: 'onDamageTaken', condition: 'always', effects: [ { type: 'modifyStats', target: 'self', stats: { attack: 'increase' } } ] } ] }, movepool: [ { name: "Power Strike", type: AttackType.BEAST, power: 60, accuracy: 100, pp: 15, priority: 0, flags: ['contact'], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] } ] }; }); describe('onDamageTaken Trigger', () => { it('should trigger when piclet takes damage', () => { const engine = new BattleEngine(abilityPiclet, basicPiclet); const initialAttack = engine.getState().playerPiclet.attack; // Opponent attacks, should trigger onDamageTaken engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // This should damage player and trigger ability ); const finalAttack = engine.getState().playerPiclet.attack; const log = engine.getLog(); // Attack should have increased due to ability trigger expect(finalAttack).toBeGreaterThan(initialAttack); expect(log.some(msg => msg.includes('Test Ability') && msg.includes('triggered'))).toBe(true); }); }); describe('endOfTurn Trigger', () => { it('should trigger at the end of every turn', () => { const endTurnAbility: PicletDefinition = { ...abilityPiclet, specialAbility: { name: "Regeneration", description: "Heals at end of turn", triggers: [ { event: 'endOfTurn', condition: 'always', effects: [ { type: 'heal', target: 'self', amount: 'small' } ] } ] } }; const engine = new BattleEngine(endTurnAbility, basicPiclet); // Damage the piclet first so healing is visible, but not too much engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.9); const initialHp = engine.getState().playerPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const log = engine.getLog(); console.log('Regeneration test log:', log); console.log('Initial HP:', initialHp, 'Final HP:', engine.getState().playerPiclet.currentHp); // The ability should trigger (check log message) expect(log.some(msg => msg.includes('Regeneration') && msg.includes('triggered'))).toBe(true); // HP might decrease due to damage taken, but healing should have occurred expect(log.some(msg => msg.includes('recovered') || msg.includes('healed'))).toBe(true); }); }); describe('onDamageDealt Trigger', () => { it('should trigger when piclet deals damage to opponent', () => { const damageDealer: PicletDefinition = { ...abilityPiclet, specialAbility: { name: "Combat High", description: "Gains speed when dealing damage", triggers: [ { event: 'onDamageDealt', condition: 'always', effects: [ { type: 'modifyStats', target: 'self', stats: { speed: 'increase' } } ] } ] } }; const engine = new BattleEngine(damageDealer, basicPiclet); const initialSpeed = engine.getState().playerPiclet.speed; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Player deals damage { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const finalSpeed = engine.getState().playerPiclet.speed; const log = engine.getLog(); expect(finalSpeed).toBeGreaterThan(initialSpeed); expect(log.some(msg => msg.includes('Combat High') && msg.includes('triggered'))).toBe(true); }); }); describe('onCriticalHit Trigger', () => { it('should trigger when dealing a critical hit', () => { const criticalHitter: PicletDefinition = { ...abilityPiclet, specialAbility: { name: "Critical Momentum", description: "Gains attack on critical hits", triggers: [ { event: 'onCriticalHit', condition: 'always', effects: [ { type: 'modifyStats', target: 'self', stats: { attack: 'increase' } } ] } ] } }; const engine = new BattleEngine(criticalHitter, basicPiclet); // Force a critical hit for testing const originalRandom = Math.random; Math.random = () => 0.01; // Force critical hit (< 0.0625) const initialAttack = engine.getState().playerPiclet.attack; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Should crit and trigger ability { type: 'move', piclet: 'opponent', moveIndex: 0 } ); // Restore original Math.random Math.random = originalRandom; const finalAttack = engine.getState().playerPiclet.attack; const log = engine.getLog(); expect(log.some(msg => msg.includes('A critical hit!'))).toBe(true); expect(finalAttack).toBeGreaterThan(initialAttack); expect(log.some(msg => msg.includes('Critical Momentum') && msg.includes('triggered'))).toBe(true); }); }); describe('onContactDamage Trigger', () => { it('should trigger only when hit by contact moves', () => { const contactSensitive: PicletDefinition = { ...abilityPiclet, specialAbility: { name: "Spiky Skin", description: "Hurts attackers that make contact", triggers: [ { event: 'onContactDamage', condition: 'always', effects: [ { type: 'damage', target: 'opponent', amount: 'small' } ] } ] } }; const engine = new BattleEngine(contactSensitive, basicPiclet); const initialOpponentHp = engine.getState().opponentPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Contact move should trigger Spiky Skin ); const finalOpponentHp = engine.getState().opponentPiclet.currentHp; const log = engine.getLog(); // Opponent should take extra damage from Spiky Skin expect(log.some(msg => msg.includes('Spiky Skin') && msg.includes('triggered'))).toBe(true); // The opponent should have taken damage from both the regular attack and the ability expect(finalOpponentHp).toBeLessThan(initialOpponentHp); }); }); describe('Conditional Triggers', () => { it('should respect ifLowHp condition', () => { const conditionalAbility: PicletDefinition = { ...abilityPiclet, specialAbility: { name: "Desperation", description: "Only triggers when HP is low", triggers: [ { event: 'onDamageTaken', condition: 'ifLowHp', effects: [ { type: 'modifyStats', target: 'self', stats: { attack: 'greatly_increase' } } ] } ] } }; const engine = new BattleEngine(conditionalAbility, basicPiclet); const initialAttack = engine.getState().playerPiclet.attack; // At high HP, condition should not be met engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const midAttack = engine.getState().playerPiclet.attack; expect(midAttack).toBe(initialAttack); // No trigger due to condition // Create a new engine for the low HP test const lowHpEngine = new BattleEngine(conditionalAbility, basicPiclet); // Set HP low and trigger the ability lowHpEngine['state'].playerPiclet.currentHp = Math.floor(lowHpEngine['state'].playerPiclet.maxHp * 0.15); lowHpEngine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const finalAttack = lowHpEngine.getState().playerPiclet.attack; const log = lowHpEngine.getLog(); expect(finalAttack).toBeGreaterThan(initialAttack); expect(log.some(msg => msg.includes('Desperation') && msg.includes('triggered'))).toBe(true); }); }); describe('Multiple Triggers on Same Ability', () => { it('should handle multiple triggers on the same ability', () => { const multiTriggerAbility: PicletDefinition = { ...abilityPiclet, specialAbility: { name: "Adaptive Fighter", description: "Multiple trigger conditions", triggers: [ { event: 'onDamageTaken', condition: 'always', effects: [ { type: 'modifyStats', target: 'self', stats: { defense: 'increase' } } ] }, { event: 'onDamageDealt', condition: 'always', effects: [ { type: 'modifyStats', target: 'self', stats: { attack: 'increase' } } ] } ] } }; const engine = new BattleEngine(multiTriggerAbility, basicPiclet); const initialAttack = engine.getState().playerPiclet.attack; const initialDefense = engine.getState().playerPiclet.defense; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Should trigger onDamageDealt { type: 'move', piclet: 'opponent', moveIndex: 0 } // Should trigger onDamageTaken ); const finalAttack = engine.getState().playerPiclet.attack; const finalDefense = engine.getState().playerPiclet.defense; const log = engine.getLog(); // Both stats should increase expect(finalAttack).toBeGreaterThan(initialAttack); expect(finalDefense).toBeGreaterThan(initialDefense); expect(log.some(msg => msg.includes('Adaptive Fighter') && msg.includes('triggered'))).toBe(true); }); }); });