|
import { describe, it, expect, beforeEach } from 'vitest'; |
|
import { BattleEngine } from './BattleEngine'; |
|
import type { PicletDefinition, Move, SpecialAbility } from './types'; |
|
import { PicletType, AttackType } from './types'; |
|
|
|
describe('Missing Battle System Features', () => { |
|
describe('manipulatePP Effects', () => { |
|
it('should drain opponent PP', () => { |
|
const ppDrainer: PicletDefinition = { |
|
name: "PP Drainer", |
|
description: "Drains opponent's PP", |
|
tier: 'medium', |
|
primaryType: PicletType.CULTURE, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 }, |
|
nature: "Calm", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Mind Drain", |
|
type: AttackType.CULTURE, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'manipulatePP', |
|
target: 'opponent', |
|
action: 'drain', |
|
amount: 'medium' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const opponent: PicletDefinition = { |
|
name: "Opponent", |
|
description: "Standard opponent", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 }, |
|
nature: "Hardy", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Tackle", |
|
type: AttackType.NORMAL, |
|
power: 40, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(ppDrainer, opponent); |
|
const initialPP = engine.getState().opponentPiclet.moves[0].currentPP; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const finalPP = engine.getState().opponentPiclet.moves[0].currentPP; |
|
expect(finalPP).toBeLessThan(initialPP); |
|
expect(engine.getLog().some(msg => msg.includes('PP was drained'))).toBe(true); |
|
}); |
|
|
|
it('should restore own PP', () => { |
|
const ppRestorer: PicletDefinition = { |
|
name: "PP Restorer", |
|
description: "Restores own PP", |
|
tier: 'medium', |
|
primaryType: PicletType.FLORA, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 }, |
|
nature: "Calm", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "PP Restore", |
|
type: AttackType.FLORA, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 5, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'manipulatePP', |
|
target: 'self', |
|
action: 'restore', |
|
amount: 'large' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const opponent: PicletDefinition = { |
|
name: "Opponent", |
|
description: "Standard opponent", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 }, |
|
nature: "Hardy", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Tackle", |
|
type: AttackType.NORMAL, |
|
power: 40, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(ppRestorer, opponent); |
|
|
|
|
|
engine['state'].playerPiclet.moves[0].currentPP = 1; |
|
const initialPP = engine['state'].playerPiclet.moves[0].currentPP; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const finalPP = engine.getState().playerPiclet.moves[0].currentPP; |
|
expect(finalPP).toBeGreaterThan(initialPP); |
|
expect(engine.getLog().some(msg => msg.includes('PP was restored'))).toBe(true); |
|
}); |
|
}); |
|
|
|
describe('fieldEffect System', () => { |
|
it('should apply field effects that persist across turns', () => { |
|
const fieldEffectUser: PicletDefinition = { |
|
name: "Field Controller", |
|
description: "Controls battlefield effects", |
|
tier: 'medium', |
|
primaryType: PicletType.SPACE, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 }, |
|
nature: "Calm", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Reflect", |
|
type: AttackType.SPACE, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'fieldEffect', |
|
effect: 'reflect', |
|
target: 'playerSide', |
|
stackable: false |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const opponent: PicletDefinition = { |
|
name: "Opponent", |
|
description: "Standard opponent", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 }, |
|
nature: "Hardy", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Physical Attack", |
|
type: AttackType.BEAST, |
|
power: 60, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(fieldEffectUser, opponent); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
expect(engine.getLog().some(msg => msg.includes('Reflect') && msg.includes('applied'))).toBe(true); |
|
|
|
|
|
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; |
|
const damage = initialHp - finalHp; |
|
|
|
|
|
expect(damage).toBeLessThan(30); |
|
}); |
|
|
|
it('should handle spikes field effect', () => { |
|
const spikesUser: PicletDefinition = { |
|
name: "Spikes User", |
|
description: "Sets entry hazards", |
|
tier: 'medium', |
|
primaryType: PicletType.MINERAL, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 }, |
|
nature: "Impish", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Spikes", |
|
type: AttackType.MINERAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'fieldEffect', |
|
effect: 'spikes', |
|
target: 'opponentSide', |
|
stackable: true |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const opponent: PicletDefinition = { |
|
name: "Opponent", |
|
description: "Standard opponent", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 }, |
|
nature: "Hardy", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Tackle", |
|
type: AttackType.NORMAL, |
|
power: 40, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(spikesUser, opponent); |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
expect(engine.getLog().some(msg => msg.includes('Spikes') && msg.includes('set'))).toBe(true); |
|
|
|
|
|
}); |
|
}); |
|
|
|
describe('counter Effects', () => { |
|
it('should counter physical attacks', () => { |
|
const counterUser: PicletDefinition = { |
|
name: "Counter Fighter", |
|
description: "Counters physical attacks", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 100, attack: 60, defense: 80, speed: 50 }, |
|
nature: "Brave", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Counter", |
|
type: AttackType.BEAST, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 1, |
|
flags: ['lowPriority'], |
|
effects: [ |
|
{ |
|
type: 'counter', |
|
strength: 'strong' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const opponent: PicletDefinition = { |
|
name: "Physical Attacker", |
|
description: "Uses physical 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: 80, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(counterUser, opponent); |
|
const initialOpponentHp = engine.getState().opponentPiclet.currentHp; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const finalOpponentHp = engine.getState().opponentPiclet.currentHp; |
|
expect(finalOpponentHp).toBeLessThan(initialOpponentHp); |
|
expect(engine.getLog().some(msg => msg.includes('countered') || msg.includes('Counter'))).toBe(true); |
|
}); |
|
}); |
|
|
|
describe('priority Effects', () => { |
|
it('should modify move priority conditionally', () => { |
|
const priorityUser: PicletDefinition = { |
|
name: "Priority User", |
|
description: "Uses priority moves based on conditions", |
|
tier: 'medium', |
|
primaryType: PicletType.SPACE, |
|
baseStats: { hp: 60, attack: 70, defense: 50, speed: 40 }, |
|
nature: "Quiet", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Desperation Strike", |
|
type: AttackType.SPACE, |
|
power: 60, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
}, |
|
{ |
|
type: 'priority', |
|
target: 'self', |
|
value: 1, |
|
condition: 'ifLowHp' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const fastOpponent: PicletDefinition = { |
|
name: "Fast Opponent", |
|
description: "Very fast opponent", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 100 }, |
|
nature: "Timid", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Quick Attack", |
|
type: AttackType.NORMAL, |
|
power: 40, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(priorityUser, fastOpponent); |
|
|
|
|
|
engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.2); |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const log = engine.getLog(); |
|
const playerMoveIndex = log.findIndex(msg => msg.includes('Priority User used Desperation Strike')); |
|
const opponentMoveIndex = log.findIndex(msg => msg.includes('Fast Opponent used Quick Attack')); |
|
|
|
|
|
expect(playerMoveIndex).toBeLessThan(opponentMoveIndex); |
|
}); |
|
}); |
|
|
|
describe('removeStatus Effects', () => { |
|
it('should remove status effects from target', () => { |
|
|
|
const cleanser: PicletDefinition = { |
|
name: "Cleanser", |
|
description: "Can remove poison", |
|
tier: 'medium', |
|
primaryType: PicletType.FLORA, |
|
baseStats: { hp: 100, attack: 50, defense: 50, speed: 50 }, |
|
nature: "Calm", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Cleanse", |
|
type: AttackType.FLORA, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'removeStatus', |
|
target: 'self', |
|
status: 'poison' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const dummy: PicletDefinition = { |
|
name: "Dummy", |
|
description: "Does nothing", |
|
tier: 'low', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 50, attack: 30, defense: 30, speed: 30 }, |
|
nature: "Docile", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Do Nothing", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 20, |
|
priority: 0, |
|
flags: [], |
|
effects: [] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(cleanser, dummy); |
|
const playerPiclet = engine.getState().playerPiclet; |
|
|
|
|
|
|
|
const mockEffect = { status: 'poison' }; |
|
|
|
|
|
playerPiclet.statusEffects.push('poison'); |
|
expect(playerPiclet.statusEffects.includes('poison')).toBe(true); |
|
|
|
|
|
engine['processRemoveStatusEffect'](mockEffect, playerPiclet); |
|
|
|
|
|
expect(playerPiclet.statusEffects.includes('poison')).toBe(false); |
|
}); |
|
}); |
|
}); |