|
|
|
|
|
|
|
|
|
|
|
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('Advanced Battle Effects - TDD Implementation', () => { |
|
describe('Damage Formula System', () => { |
|
it('should handle recoil damage moves', () => { |
|
const recoilMove: Move = { |
|
name: "Reckless Dive", |
|
type: AttackType.SPACE, |
|
power: 120, |
|
accuracy: 100, |
|
pp: 5, |
|
priority: 0, |
|
flags: ['contact', 'reckless'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'strong' |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'self', |
|
formula: 'recoil', |
|
value: 0.25 |
|
} |
|
] |
|
}; |
|
|
|
const testPiclet: PicletDefinition = { |
|
name: "Recoil Tester", |
|
description: "Tests recoil moves", |
|
tier: 'medium', |
|
primaryType: PicletType.SPACE, |
|
baseStats: STANDARD_STATS, |
|
nature: "Bold", |
|
specialAbility: { name: "None", description: "No ability" }, |
|
movepool: [recoilMove] |
|
}; |
|
|
|
const targetPiclet: PicletDefinition = { |
|
name: "Target", |
|
description: "Target dummy", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: STANDARD_STATS, |
|
nature: "Hardy", |
|
specialAbility: { name: "None", description: "No ability" }, |
|
movepool: [{ |
|
name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35, |
|
priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
}] |
|
}; |
|
|
|
const engine = new BattleEngine(testPiclet, targetPiclet); |
|
const initialHp = engine.getState().playerPiclet.currentHp; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const finalHp = engine.getState().playerPiclet.currentHp; |
|
expect(finalHp).toBeLessThan(initialHp); |
|
}); |
|
|
|
it('should handle drain damage moves', () => { |
|
const drainMove: Move = { |
|
name: "Spectral Drain", |
|
type: AttackType.CULTURE, |
|
power: 60, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['draining'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
formula: 'drain', |
|
value: 0.5 |
|
} |
|
] |
|
}; |
|
|
|
const testPiclet: PicletDefinition = { |
|
name: "Drain Tester", |
|
description: "Tests drain moves", |
|
tier: 'medium', |
|
primaryType: PicletType.CULTURE, |
|
baseStats: STANDARD_STATS, |
|
nature: "Bold", |
|
specialAbility: { name: "None", description: "No ability" }, |
|
movepool: [drainMove] |
|
}; |
|
|
|
const targetPiclet: PicletDefinition = { |
|
name: "Target", |
|
description: "Target dummy", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: STANDARD_STATS, |
|
nature: "Hardy", |
|
specialAbility: { name: "None", description: "No ability" }, |
|
movepool: [{ |
|
name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35, |
|
priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
}] |
|
}; |
|
|
|
const engine = new BattleEngine(testPiclet, targetPiclet); |
|
|
|
|
|
engine['state'].playerPiclet.currentHp = 50; |
|
const initialHp = engine.getState().playerPiclet.currentHp; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const log = engine.getLog(); |
|
const hasHealingMessage = log.some(msg => msg.includes('recovered') && msg.includes('HP from draining')); |
|
expect(hasHealingMessage).toBe(true); |
|
}); |
|
|
|
it('should handle fixed damage moves', () => { |
|
const fixedMove: Move = { |
|
name: "Fixed Strike", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
formula: 'fixed', |
|
value: 50 |
|
} |
|
] |
|
}; |
|
|
|
|
|
expect(fixedMove.effects[0].formula).toBe('fixed'); |
|
expect(fixedMove.effects[0].value).toBe(50); |
|
}); |
|
|
|
it('should handle percentage damage moves', () => { |
|
const percentMove: Move = { |
|
name: "Percentage Strike", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 5, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
formula: 'percentage', |
|
value: 25 |
|
} |
|
] |
|
}; |
|
|
|
|
|
expect(percentMove.effects[0].formula).toBe('percentage'); |
|
expect(percentMove.effects[0].value).toBe(25); |
|
}); |
|
}); |
|
|
|
describe('PP Manipulation System', () => { |
|
it('should handle PP drain moves', () => { |
|
const ppDrainMove: Move = { |
|
name: "Mind Drain", |
|
type: AttackType.CULTURE, |
|
power: 40, |
|
accuracy: 100, |
|
pp: 15, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
}, |
|
{ |
|
type: 'manipulatePP', |
|
target: 'opponent', |
|
action: 'drain', |
|
amount: 'medium' |
|
} |
|
] |
|
}; |
|
|
|
|
|
expect(ppDrainMove.effects[1].type).toBe('manipulatePP'); |
|
expect(ppDrainMove.effects[1].action).toBe('drain'); |
|
}); |
|
|
|
it('should handle PP restore moves', () => { |
|
const ppRestoreMove: Move = { |
|
name: "Restore Energy", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 5, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'manipulatePP', |
|
target: 'self', |
|
action: 'restore', |
|
amount: 'large' |
|
} |
|
] |
|
}; |
|
|
|
|
|
expect(ppRestoreMove.effects[0].type).toBe('manipulatePP'); |
|
expect(ppRestoreMove.effects[0].action).toBe('restore'); |
|
}); |
|
|
|
it('should handle specific PP manipulation', () => { |
|
const specificPPMove: Move = { |
|
name: "Soul Burn", |
|
type: AttackType.SPACE, |
|
power: 150, |
|
accuracy: 90, |
|
pp: 5, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'extreme' |
|
}, |
|
{ |
|
type: 'manipulatePP', |
|
target: 'self', |
|
action: 'drain', |
|
value: 3, |
|
targetMove: 'random', |
|
condition: 'afterUse' |
|
} |
|
] |
|
}; |
|
|
|
|
|
expect(specificPPMove.effects[1].value).toBe(3); |
|
expect(specificPPMove.effects[1].targetMove).toBe('random'); |
|
}); |
|
}); |
|
|
|
describe('Field Effects System', () => { |
|
it('should handle field-wide effects', () => { |
|
const fieldMove: Move = { |
|
name: "Void Storm", |
|
type: AttackType.SPACE, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 5, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'fieldEffect', |
|
effect: 'voidStorm', |
|
target: 'field', |
|
stackable: false |
|
} |
|
] |
|
}; |
|
|
|
|
|
expect(fieldMove.effects[0].type).toBe('fieldEffect'); |
|
expect(fieldMove.effects[0].target).toBe('field'); |
|
}); |
|
|
|
it('should handle side-specific effects', () => { |
|
const sideMove: Move = { |
|
name: "Healing Mist", |
|
type: AttackType.FLORA, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'fieldEffect', |
|
effect: 'healingMist', |
|
target: 'playerSide', |
|
stackable: true |
|
} |
|
] |
|
}; |
|
|
|
|
|
expect(sideMove.effects[0].target).toBe('playerSide'); |
|
expect(sideMove.effects[0].stackable).toBe(true); |
|
}); |
|
}); |
|
|
|
describe('Counter Move System', () => { |
|
it('should handle physical counter moves', () => { |
|
const counterMove: Move = { |
|
name: "Counter Strike", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 20, |
|
priority: -5, |
|
flags: ['lowPriority'], |
|
effects: [ |
|
{ |
|
type: 'counter', |
|
strength: 'strong' |
|
} |
|
] |
|
}; |
|
|
|
|
|
expect(counterMove.effects[0].type).toBe('counter'); |
|
expect(counterMove.effects[0].strength).toBe('strong'); |
|
}); |
|
|
|
it('should handle special counter moves', () => { |
|
const specialCounterMove: Move = { |
|
name: "Mirror Coat", |
|
type: AttackType.CULTURE, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 20, |
|
priority: -5, |
|
flags: ['lowPriority'], |
|
effects: [ |
|
{ |
|
type: 'counter', |
|
strength: 'strong' |
|
} |
|
] |
|
}; |
|
|
|
expect(specialCounterMove.effects[0].strength).toBe('strong'); |
|
}); |
|
}); |
|
|
|
describe('Priority Manipulation', () => { |
|
it('should handle priority-changing effects', () => { |
|
const priorityMove: Move = { |
|
name: "Quick Strike", |
|
type: AttackType.NORMAL, |
|
power: 40, |
|
accuracy: 100, |
|
pp: 30, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'weak' |
|
}, |
|
{ |
|
type: 'priority', |
|
target: 'self', |
|
value: 1, |
|
condition: 'ifLowHp' |
|
} |
|
] |
|
}; |
|
|
|
|
|
expect(priorityMove.effects[1].type).toBe('priority'); |
|
expect(priorityMove.effects[1].value).toBe(1); |
|
}); |
|
}); |
|
|
|
describe('Status Chance System', () => { |
|
it('should handle status moves with specific chances', () => { |
|
const chanceStatusMove: Move = { |
|
name: "Thunder Wave", |
|
type: AttackType.SPACE, |
|
power: 0, |
|
accuracy: 90, |
|
pp: 20, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'applyStatus', |
|
target: 'opponent', |
|
status: 'paralyze', |
|
chance: 100 |
|
} |
|
] |
|
}; |
|
|
|
expect(chanceStatusMove.effects[0].chance).toBe(100); |
|
}); |
|
|
|
it('should handle partial chance status effects', () => { |
|
const partialChanceMove: Move = { |
|
name: "Ice Touch", |
|
type: AttackType.MINERAL, |
|
power: 60, |
|
accuracy: 100, |
|
pp: 20, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
}, |
|
{ |
|
type: 'applyStatus', |
|
target: 'opponent', |
|
status: 'freeze', |
|
chance: 30 |
|
} |
|
] |
|
}; |
|
|
|
expect(partialChanceMove.effects[1].chance).toBe(30); |
|
}); |
|
}); |
|
|
|
describe('Percentage-based Healing', () => { |
|
it('should handle percentage healing moves', () => { |
|
const percentHealMove: Move = { |
|
name: "Recovery", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
formula: 'percentage', |
|
value: 50 |
|
} |
|
] |
|
}; |
|
|
|
expect(percentHealMove.effects[0].formula).toBe('percentage'); |
|
expect(percentHealMove.effects[0].value).toBe(50); |
|
}); |
|
|
|
it('should handle fixed healing moves', () => { |
|
const fixedHealMove: Move = { |
|
name: "First Aid", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 15, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
formula: 'fixed', |
|
value: 25 |
|
} |
|
] |
|
}; |
|
|
|
expect(fixedHealMove.effects[0].formula).toBe('fixed'); |
|
expect(fixedHealMove.effects[0].value).toBe(25); |
|
}); |
|
}); |
|
|
|
describe('Extended Condition System', () => { |
|
it('should handle type-specific conditions', () => { |
|
const typeConditionMove: Move = { |
|
name: "Flora Boost", |
|
type: AttackType.FLORA, |
|
power: 60, |
|
accuracy: 100, |
|
pp: 15, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
}, |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'increase' }, |
|
condition: 'ifMoveType:flora' |
|
} |
|
] |
|
}; |
|
|
|
expect(typeConditionMove.effects[1].condition).toBe('ifMoveType:flora'); |
|
}); |
|
|
|
it('should handle status-specific conditions', () => { |
|
const statusConditionMove: Move = { |
|
name: "Burn Power", |
|
type: AttackType.SPACE, |
|
power: 80, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'strong', |
|
condition: 'ifStatus:burn' |
|
} |
|
] |
|
}; |
|
|
|
expect(statusConditionMove.effects[0].condition).toBe('ifStatus:burn'); |
|
}); |
|
|
|
it('should handle weather-specific conditions', () => { |
|
const weatherConditionMove: Move = { |
|
name: "Storm Strike", |
|
type: AttackType.SPACE, |
|
power: 70, |
|
accuracy: 95, |
|
pp: 15, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'strong', |
|
condition: 'ifWeather:storm' |
|
} |
|
] |
|
}; |
|
|
|
expect(weatherConditionMove.effects[0].condition).toBe('ifWeather:storm'); |
|
}); |
|
}); |
|
|
|
describe('Remove Status Effects', () => { |
|
it('should handle status removal moves', () => { |
|
const removeStatusMove: Move = { |
|
name: "Cleanse", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 15, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'removeStatus', |
|
target: 'self', |
|
status: 'poison' |
|
} |
|
] |
|
}; |
|
|
|
expect(removeStatusMove.effects[0].type).toBe('removeStatus'); |
|
expect(removeStatusMove.effects[0].status).toBe('poison'); |
|
}); |
|
|
|
it('should handle multi-target status removal', () => { |
|
const teamCleanseMove: Move = { |
|
name: "Team Cleanse", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 5, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'removeStatus', |
|
target: 'allies', |
|
status: 'confuse' |
|
} |
|
] |
|
}; |
|
|
|
expect(teamCleanseMove.effects[0].target).toBe('allies'); |
|
}); |
|
}); |
|
}); |