piclets / src /lib /battle-engine /missing-features.test.ts
Fraser's picture
simple counters
1bb845a
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);
// Use the PP restore move multiple times to drain it
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);
// Apply reflect
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);
// Check if reflect reduces physical damage in subsequent turns
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;
// Reflect should reduce physical damage
expect(damage).toBeLessThan(30); // Should be reduced from normal ~40-50 damage
});
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);
// TODO: Test spikes damage when switching (requires switching mechanics)
});
});
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, // High priority to set up counter before opponent attacks
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);
// Damage the priority user to trigger low HP condition
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'));
// When at low HP, priority user should go first despite lower speed
expect(playerMoveIndex).toBeLessThan(opponentMoveIndex);
});
});
describe('removeStatus Effects', () => {
it('should remove status effects from target', () => {
// Simple test: create a move that only removes poison status
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;
// Test the removeStatus effect by directly calling it
// This bypasses any turn/timing issues
const mockEffect = { status: 'poison' };
// First add poison manually
playerPiclet.statusEffects.push('poison');
expect(playerPiclet.statusEffects.includes('poison')).toBe(true);
// Call the removeStatus effect processor directly
engine['processRemoveStatusEffect'](mockEffect, playerPiclet);
// Check if poison was removed
expect(playerPiclet.statusEffects.includes('poison')).toBe(false);
});
});
});