|
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest'; |
|
import { BattleEngine } from './BattleEngine'; |
|
import { PicletDefinition, Move, SpecialAbility } from './types'; |
|
import { PicletType, AttackType } from './types'; |
|
|
|
const STANDARD_STATS = { hp: 100, attack: 80, defense: 70, speed: 60 }; |
|
|
|
describe('Special Ability Trigger System - TDD Implementation', () => { |
|
describe('Damage Triggers', () => { |
|
it('should handle onDamageTaken triggers', () => { |
|
const berserkAbility: SpecialAbility = { |
|
name: "Berserker", |
|
description: "Attack increases when taking damage", |
|
triggers: [ |
|
{ |
|
event: 'onDamageTaken', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'increase' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const berserkerPiclet: PicletDefinition = { |
|
name: "Rage Beast", |
|
description: "Gets stronger when hurt", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: STANDARD_STATS, |
|
nature: "Brave", |
|
specialAbility: berserkAbility, |
|
movepool: [{ |
|
name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35, |
|
priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
}] |
|
}; |
|
|
|
expect(berserkAbility.triggers![0].event).toBe('onDamageTaken'); |
|
}); |
|
|
|
it('should handle onDamageDealt triggers', () => { |
|
const lifeStealAbility: SpecialAbility = { |
|
name: "Life Steal", |
|
description: "Heals when dealing damage", |
|
triggers: [ |
|
{ |
|
event: 'onDamageDealt', |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
amount: 'small' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(lifeStealAbility.triggers![0].event).toBe('onDamageDealt'); |
|
}); |
|
|
|
it('should handle onContactDamage triggers', () => { |
|
const toxicSkin: SpecialAbility = { |
|
name: "Toxic Skin", |
|
description: "Physical contact poisons the attacker", |
|
triggers: [ |
|
{ |
|
event: 'onContactDamage', |
|
effects: [ |
|
{ |
|
type: 'applyStatus', |
|
target: 'attacker', |
|
status: 'poison', |
|
chance: 50 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(toxicSkin.triggers![0].event).toBe('onContactDamage'); |
|
expect(toxicSkin.triggers![0].effects[0].target).toBe('attacker'); |
|
}); |
|
}); |
|
|
|
describe('Status Triggers', () => { |
|
it('should handle onStatusInflicted triggers', () => { |
|
const burnBoost: SpecialAbility = { |
|
name: "Burn Boost", |
|
description: "Fire damage energizes this Piclet, increasing attack power", |
|
triggers: [ |
|
{ |
|
event: 'onStatusInflicted', |
|
condition: 'ifStatus:burn', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'greatly_increase' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(burnBoost.triggers![0].event).toBe('onStatusInflicted'); |
|
expect(burnBoost.triggers![0].condition).toBe('ifStatus:burn'); |
|
}); |
|
|
|
it('should handle onStatusMove triggers', () => { |
|
const statusReflect: SpecialAbility = { |
|
name: "Status Shield", |
|
description: "Reflects status moves back to user", |
|
triggers: [ |
|
{ |
|
event: 'onStatusMove', |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'targetRedirection', |
|
value: 'reflect' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(statusReflect.triggers![0].event).toBe('onStatusMove'); |
|
}); |
|
|
|
it('should handle onStatusMoveTargeted triggers', () => { |
|
const statusCounter: SpecialAbility = { |
|
name: "Status Counter", |
|
description: "When targeted by status moves, counter with damage", |
|
triggers: [ |
|
{ |
|
event: 'onStatusMoveTargeted', |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'attacker', |
|
formula: 'fixed', |
|
value: 30 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(statusCounter.triggers![0].event).toBe('onStatusMoveTargeted'); |
|
}); |
|
}); |
|
|
|
describe('Critical Hit Triggers', () => { |
|
it('should handle onCriticalHit triggers', () => { |
|
const criticalMomentum: SpecialAbility = { |
|
name: "Critical Momentum", |
|
description: "Critical hits increase speed", |
|
triggers: [ |
|
{ |
|
event: 'onCriticalHit', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { speed: 'increase' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(criticalMomentum.triggers![0].event).toBe('onCriticalHit'); |
|
}); |
|
}); |
|
|
|
describe('HP Drain Triggers', () => { |
|
it('should handle onHPDrained triggers', () => { |
|
const drainPunish: SpecialAbility = { |
|
name: "Drain Punishment", |
|
description: "Damages opponents who try to drain HP", |
|
triggers: [ |
|
{ |
|
event: 'onHPDrained', |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'attacker', |
|
formula: 'fixed', |
|
value: 25 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(drainPunish.triggers![0].event).toBe('onHPDrained'); |
|
}); |
|
}); |
|
|
|
describe('KO Triggers', () => { |
|
it('should handle onKO triggers', () => { |
|
const koBoost: SpecialAbility = { |
|
name: "Victory Rush", |
|
description: "Gets stronger after knocking out an opponent", |
|
triggers: [ |
|
{ |
|
event: 'onKO', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'greatly_increase', speed: 'increase' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(koBoost.triggers![0].event).toBe('onKO'); |
|
}); |
|
}); |
|
|
|
describe('Switch Triggers', () => { |
|
it('should handle onSwitchIn triggers', () => { |
|
const intimidate: SpecialAbility = { |
|
name: "Intimidate", |
|
description: "Lowers opponent's attack when entering battle", |
|
triggers: [ |
|
{ |
|
event: 'onSwitchIn', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'opponent', |
|
stats: { attack: 'decrease' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(intimidate.triggers![0].event).toBe('onSwitchIn'); |
|
expect(intimidate.triggers![0].effects[0].target).toBe('opponent'); |
|
}); |
|
|
|
it('should handle onSwitchOut triggers', () => { |
|
const regenerator: SpecialAbility = { |
|
name: "Regenerator", |
|
description: "Restores HP when switching out", |
|
triggers: [ |
|
{ |
|
event: 'onSwitchOut', |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
amount: 'small' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(regenerator.triggers![0].event).toBe('onSwitchOut'); |
|
}); |
|
|
|
it('should handle conditional switch-in triggers', () => { |
|
const stormCaller: SpecialAbility = { |
|
name: "Storm Caller", |
|
description: "Boosts attack when entering during storm weather", |
|
triggers: [ |
|
{ |
|
event: 'onSwitchIn', |
|
condition: 'ifWeather:storm', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'increase' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(stormCaller.triggers![0].condition).toBe('ifWeather:storm'); |
|
}); |
|
}); |
|
|
|
describe('Weather Triggers', () => { |
|
it('should handle onWeatherChange triggers', () => { |
|
const weatherAdapt: SpecialAbility = { |
|
name: "Weather Adaptation", |
|
description: "Adapts stats based on weather changes", |
|
triggers: [ |
|
{ |
|
event: 'onWeatherChange', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { speed: 'increase' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(weatherAdapt.triggers![0].event).toBe('onWeatherChange'); |
|
}); |
|
}); |
|
|
|
describe('Move Use Triggers', () => { |
|
it('should handle beforeMoveUse triggers', () => { |
|
const movePrep: SpecialAbility = { |
|
name: "Move Preparation", |
|
description: "Boosts accuracy before using moves", |
|
triggers: [ |
|
{ |
|
event: 'beforeMoveUse', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { accuracy: 'increase' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(movePrep.triggers![0].event).toBe('beforeMoveUse'); |
|
}); |
|
|
|
it('should handle afterMoveUse triggers', () => { |
|
const moveRecovery: SpecialAbility = { |
|
name: "Move Recovery", |
|
description: "Heals slightly after using any move", |
|
triggers: [ |
|
{ |
|
event: 'afterMoveUse', |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
formula: 'fixed', |
|
value: 5 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(moveRecovery.triggers![0].event).toBe('afterMoveUse'); |
|
}); |
|
}); |
|
|
|
describe('HP Threshold Triggers', () => { |
|
it('should handle onLowHP triggers', () => { |
|
const emergencyMode: SpecialAbility = { |
|
name: "Emergency Mode", |
|
description: "Activates emergency protocols when HP is low", |
|
triggers: [ |
|
{ |
|
event: 'onLowHP', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { speed: 'greatly_increase', attack: 'increase' } |
|
}, |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'statusImmunity', |
|
value: ['burn', 'poison', 'paralyze'] |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(emergencyMode.triggers![0].event).toBe('onLowHP'); |
|
expect(emergencyMode.triggers![0].effects).toHaveLength(2); |
|
}); |
|
|
|
it('should handle onFullHP triggers', () => { |
|
const fullPower: SpecialAbility = { |
|
name: "Full Power", |
|
description: "Maximum power when at full health", |
|
triggers: [ |
|
{ |
|
event: 'onFullHP', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'greatly_increase' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(fullPower.triggers![0].event).toBe('onFullHP'); |
|
}); |
|
}); |
|
|
|
describe('Turn-Based Triggers', () => { |
|
it('should handle endOfTurn triggers', () => { |
|
const turnRegeneration: SpecialAbility = { |
|
name: "Slow Regeneration", |
|
description: "Heals at the end of each turn", |
|
triggers: [ |
|
{ |
|
event: 'endOfTurn', |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
formula: 'percentage', |
|
value: 10 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(turnRegeneration.triggers![0].event).toBe('endOfTurn'); |
|
}); |
|
|
|
it('should handle conditional turn triggers', () => { |
|
const sleepHeal: SpecialAbility = { |
|
name: "Slumber Heal", |
|
description: "Restores HP while sleeping instead of being unable to act", |
|
triggers: [ |
|
{ |
|
event: 'endOfTurn', |
|
condition: 'ifStatus:sleep', |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
formula: 'percentage', |
|
value: 15 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(sleepHeal.triggers![0].condition).toBe('ifStatus:sleep'); |
|
}); |
|
}); |
|
|
|
describe('Opponent Move Triggers', () => { |
|
it('should handle onOpponentContactMove triggers', () => { |
|
const contactPunish: SpecialAbility = { |
|
name: "Contact Punishment", |
|
description: "Damages opponents who use contact moves", |
|
triggers: [ |
|
{ |
|
event: 'onOpponentContactMove', |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'attacker', |
|
formula: 'fixed', |
|
value: 15 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(contactPunish.triggers![0].event).toBe('onOpponentContactMove'); |
|
}); |
|
|
|
it('should handle wind currents ability from design doc', () => { |
|
const windCurrents: SpecialAbility = { |
|
name: "Wind Currents", |
|
description: "Gains +25% speed when opponent uses a contact move", |
|
triggers: [ |
|
{ |
|
event: 'onOpponentContactMove', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { speed: 'increase' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const zephyrSprite: PicletDefinition = { |
|
name: "Zephyr Sprite", |
|
description: "A mysterious floating creature that manipulates wind currents", |
|
tier: 'medium', |
|
primaryType: PicletType.SPACE, |
|
baseStats: { hp: 65, attack: 85, defense: 40, speed: 90 }, |
|
nature: "hasty", |
|
specialAbility: windCurrents, |
|
movepool: [{ |
|
name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35, |
|
priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
}] |
|
}; |
|
|
|
expect(windCurrents.triggers![0].event).toBe('onOpponentContactMove'); |
|
expect(zephyrSprite.specialAbility.name).toBe('Wind Currents'); |
|
}); |
|
}); |
|
|
|
describe('Complex Multi-Trigger Abilities', () => { |
|
it('should handle abilities with multiple triggers', () => { |
|
const complexAbility: SpecialAbility = { |
|
name: "Adaptive Guardian", |
|
description: "Complex ability with multiple trigger conditions", |
|
triggers: [ |
|
{ |
|
event: 'onSwitchIn', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { defense: 'increase' } |
|
} |
|
] |
|
}, |
|
{ |
|
event: 'onDamageTaken', |
|
condition: 'ifLowHp', |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'damageReflection', |
|
value: 0.3 |
|
} |
|
] |
|
}, |
|
{ |
|
event: 'endOfTurn', |
|
condition: 'ifStatus:burn', |
|
effects: [ |
|
{ |
|
type: 'removeStatus', |
|
target: 'self', |
|
status: 'burn' |
|
}, |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'increase' } |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(complexAbility.triggers).toHaveLength(3); |
|
expect(complexAbility.triggers![1].condition).toBe('ifLowHp'); |
|
expect(complexAbility.triggers![2].effects).toHaveLength(2); |
|
}); |
|
}); |
|
|
|
describe('Status-Specific Ability Examples', () => { |
|
it('should handle Glacial Birth - starts battle frozen', () => { |
|
const glacialBirth: SpecialAbility = { |
|
name: "Glacial Birth", |
|
description: "Enters battle in a frozen state but gains defensive bonuses", |
|
triggers: [ |
|
{ |
|
event: 'onSwitchIn', |
|
effects: [ |
|
{ |
|
type: 'applyStatus', |
|
target: 'self', |
|
status: 'freeze', |
|
chance: 100 |
|
}, |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { defense: 'greatly_increase' }, |
|
condition: 'whileFrozen' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(glacialBirth.triggers![0].effects[0].status).toBe('freeze'); |
|
expect(glacialBirth.triggers![0].effects[1].condition).toBe('whileFrozen'); |
|
}); |
|
|
|
it('should handle Cryogenic Touch - freezes on contact', () => { |
|
const cryogenicTouch: SpecialAbility = { |
|
name: "Cryogenic Touch", |
|
description: "Contact moves have a chance to freeze the attacker", |
|
triggers: [ |
|
{ |
|
event: 'onContactDamage', |
|
effects: [ |
|
{ |
|
type: 'applyStatus', |
|
target: 'attacker', |
|
status: 'freeze', |
|
chance: 30 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(cryogenicTouch.triggers![0].effects[0].chance).toBe(30); |
|
}); |
|
|
|
it('should handle Paralytic Aura - paralyzes on entry', () => { |
|
const paralyticAura: SpecialAbility = { |
|
name: "Paralytic Aura", |
|
description: "Intimidating presence paralyzes the opponent upon entry", |
|
triggers: [ |
|
{ |
|
event: 'onSwitchIn', |
|
effects: [ |
|
{ |
|
type: 'applyStatus', |
|
target: 'opponent', |
|
status: 'paralyze', |
|
chance: 75 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(paralyticAura.triggers![0].effects[0].target).toBe('opponent'); |
|
expect(paralyticAura.triggers![0].effects[0].chance).toBe(75); |
|
}); |
|
|
|
it('should handle Confusion Clarity - team status removal', () => { |
|
const confusionClarity: SpecialAbility = { |
|
name: "Confusion Clarity", |
|
description: "Clear mind prevents confusion and helps allies focus", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'statusImmunity', |
|
value: ['confuse'] |
|
} |
|
], |
|
triggers: [ |
|
{ |
|
event: 'onSwitchIn', |
|
effects: [ |
|
{ |
|
type: 'removeStatus', |
|
target: 'allies', |
|
status: 'confuse' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(confusionClarity.effects![0].value).toContain('confuse'); |
|
expect(confusionClarity.triggers![0].effects[0].target).toBe('allies'); |
|
}); |
|
}); |
|
}); |