piclets / src /lib /battle-engine /ability-triggers.test.ts
Fraser's picture
battle battle
e78d70c
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);
});
});
});