piclets / src /lib /battle-engine /switching-system.test.ts
Fraser's picture
battle battle
e78d70c
import { describe, it, expect, beforeEach } from 'vitest';
import { BattleEngine } from './BattleEngine';
import type { PicletDefinition } from './types';
import { PicletType, AttackType } from './types';
describe('Switching System', () => {
let basicPiclet: PicletDefinition;
let reservePiclet: PicletDefinition;
let hazardSetter: PicletDefinition;
let switchTriggerPiclet: PicletDefinition;
beforeEach(() => {
// Basic piclet for primary battles
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' }]
}
]
};
// Reserve piclet for switching
reservePiclet = {
name: "Reserve Fighter",
description: "Backup piclet",
tier: 'medium',
primaryType: PicletType.FLORA,
baseStats: { hp: 90, attack: 50, defense: 70, speed: 40 },
nature: "Calm",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Leaf Strike",
type: AttackType.FLORA,
power: 45,
accuracy: 100,
pp: 25,
priority: 0,
flags: [],
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
}
]
};
// Piclet that can set entry hazards
hazardSetter = {
name: "Hazard Master",
description: "Sets entry hazards",
tier: 'medium',
primaryType: PicletType.MINERAL,
baseStats: { hp: 70, attack: 40, defense: 80, speed: 50 },
nature: "Bold",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Spike Trap",
type: AttackType.MINERAL,
power: 0,
accuracy: 100,
pp: 20,
priority: 0,
flags: [],
effects: [
{
type: 'fieldEffect',
effect: 'spikes',
target: 'opponentSide',
stackable: true
}
]
},
{
name: "Toxic Spikes",
type: AttackType.MINERAL,
power: 0,
accuracy: 100,
pp: 20,
priority: 0,
flags: [],
effects: [
{
type: 'fieldEffect',
effect: 'toxicSpikes',
target: 'opponentSide',
stackable: true
}
]
}
]
};
// Piclet with switch-triggered abilities
switchTriggerPiclet = {
name: "Switch Specialist",
description: "Has switch-in/out abilities",
tier: 'medium',
primaryType: PicletType.CULTURE,
baseStats: { hp: 85, attack: 55, defense: 65, speed: 75 },
nature: "Timid",
specialAbility: {
name: "Intimidate",
description: "Lowers opponent's attack on switch-in",
triggers: [
{
event: 'onSwitchIn',
condition: 'always',
effects: [
{
type: 'modifyStats',
target: 'opponent',
stats: {
attack: 'decrease'
}
}
]
}
]
},
movepool: [
{
name: "Quick Strike",
type: AttackType.CULTURE,
power: 40,
accuracy: 100,
pp: 30,
priority: 1,
flags: [],
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
}
]
};
});
describe('Basic Switch Actions', () => {
it('should allow switching to a different piclet', () => {
// Create engine with rosters
const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
const initialPlayerName = engine.getState().playerPiclet.definition.name;
expect(initialPlayerName).toBe("Basic Fighter");
// Execute switch action
engine.executeActions(
{ type: 'switch', piclet: 'player', newPicletIndex: 1 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const finalPlayerName = engine.getState().playerPiclet.definition.name;
const log = engine.getLog();
expect(finalPlayerName).toBe("Reserve Fighter");
expect(log.some(msg => msg.includes('switched') && msg.includes('Reserve Fighter'))).toBe(true);
});
it('should handle switch action priority correctly', () => {
const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
// Switch actions should have higher priority than moves
engine.executeActions(
{ type: 'switch', piclet: 'player', newPicletIndex: 1 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const log = engine.getLog();
// Switch should happen before the opponent's move
const switchIndex = log.findIndex(msg => msg.includes('switched'));
const moveIndex = log.findIndex(msg => msg.includes('used') && msg.includes('Basic Attack'));
expect(switchIndex).toBeLessThan(moveIndex);
});
it('should not allow switching to same piclet', () => {
const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
// Try to switch to the same piclet (index 0)
engine.executeActions(
{ type: 'switch', piclet: 'player', newPicletIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const log = engine.getLog();
expect(log.some(msg => msg.includes('already active') || msg.includes('cannot switch'))).toBe(true);
});
it('should not allow switching to fainted piclet', () => {
const faintedPiclet = { ...reservePiclet };
const engine = new BattleEngine([basicPiclet, faintedPiclet], [basicPiclet]);
// Mock fainted piclet by accessing private roster states
(engine as any).playerRosterStates[1].fainted = true;
(engine as any).playerRosterStates[1].currentHp = 0;
engine.executeActions(
{ type: 'switch', piclet: 'player', newPicletIndex: 1 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const log = engine.getLog();
expect(log.some(msg => msg.includes('fainted') || msg.includes('unable to battle'))).toBe(true);
});
});
describe('Entry Hazards', () => {
it('should apply spikes damage on switch-in', () => {
const engine = new BattleEngine([hazardSetter, basicPiclet], [basicPiclet, reservePiclet]);
// Set up spikes
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Spike Trap
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const log = engine.getLog();
expect(log.some(msg => msg.includes('spikes') || msg.includes('hazard'))).toBe(true);
// Switch opponent to trigger spikes
const initialHp = engine.getState().opponentPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'switch', piclet: 'opponent', newPicletIndex: 1 }
);
const finalLog = engine.getLog();
expect(finalLog.some(msg => msg.includes('hurt by spikes') || msg.includes('stepped on spikes'))).toBe(true);
});
it('should apply toxic spikes status on switch-in', () => {
const engine = new BattleEngine([hazardSetter], [basicPiclet, reservePiclet]);
// Set up toxic spikes
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 1 }, // Toxic Spikes
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
// Switch opponent to trigger toxic spikes
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'switch', piclet: 'opponent', newPicletIndex: 1 }
);
const finalState = engine.getState().opponentPiclet;
const log = engine.getLog();
expect(finalState.statusEffects).toContain('poison');
expect(log.some(msg => msg.includes('poisoned by toxic spikes'))).toBe(true);
});
it('should stack multiple layers of spikes', () => {
const engine = new BattleEngine([hazardSetter], [basicPiclet, reservePiclet]);
// Set up multiple spike layers
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // First Spike Trap
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Second Spike Trap
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const fieldEffects = engine.getState().fieldEffects;
const spikeCount = fieldEffects.filter(effect => effect.name === 'entryHazardSpikes').length;
expect(spikeCount).toBeGreaterThan(1);
});
});
describe('Switch-In/Out Ability Triggers', () => {
it('should trigger onSwitchIn ability when piclet enters battle', () => {
const engine = new BattleEngine([basicPiclet, switchTriggerPiclet], [basicPiclet]);
const initialOpponentAttack = engine.getState().opponentPiclet.attack;
// Switch in the intimidate piclet
engine.executeActions(
{ type: 'switch', piclet: 'player', newPicletIndex: 1 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const finalOpponentAttack = engine.getState().opponentPiclet.attack;
const log = engine.getLog();
expect(finalOpponentAttack).toBeLessThan(initialOpponentAttack);
expect(log.some(msg => msg.includes('Intimidate') && msg.includes('triggered'))).toBe(true);
});
it('should trigger onSwitchOut ability when piclet leaves battle', () => {
const switchOutPiclet: PicletDefinition = {
...switchTriggerPiclet,
specialAbility: {
name: "Parting Shot",
description: "Lowers opponent's stats on switch-out",
triggers: [
{
event: 'onSwitchOut',
condition: 'always',
effects: [
{
type: 'modifyStats',
target: 'opponent',
stats: {
attack: 'decrease',
defense: 'decrease'
}
}
]
}
]
}
};
const engine = new BattleEngine([switchOutPiclet, reservePiclet], [basicPiclet]);
const initialOpponentAttack = engine.getState().opponentPiclet.attack;
const initialOpponentDefense = engine.getState().opponentPiclet.defense;
// Switch out the parting shot piclet
engine.executeActions(
{ type: 'switch', piclet: 'player', newPicletIndex: 1 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const finalOpponentAttack = engine.getState().opponentPiclet.attack;
const finalOpponentDefense = engine.getState().opponentPiclet.defense;
const log = engine.getLog();
expect(finalOpponentAttack).toBeLessThan(initialOpponentAttack);
expect(finalOpponentDefense).toBeLessThan(initialOpponentDefense);
expect(log.some(msg => msg.includes('Parting Shot') && msg.includes('triggered'))).toBe(true);
});
});
describe('Forced Switching', () => {
it('should handle forced switch when active piclet faints', () => {
const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
// Damage player piclet to near-faint
engine['state'].playerPiclet.currentHp = 1;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Should KO player
);
const log = engine.getLog();
// Should prompt for forced switch or auto-switch if only one option
expect(log.some(msg =>
msg.includes('fainted') ||
msg.includes('must choose') ||
msg.includes('forced switch')
)).toBe(true);
});
it('should end battle if no valid switches remain', () => {
const engine = new BattleEngine([basicPiclet], [basicPiclet]); // Only one piclet each
// KO the only piclet
engine['state'].playerPiclet.currentHp = 1;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
expect(engine.isGameOver()).toBe(true);
expect(engine.getState().winner).toBe('opponent');
});
});
describe('Switch Action Integration', () => {
it('should preserve PP and status when switching back', () => {
const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
// Use a move to reduce PP
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const ppAfterMove = engine.getState().playerPiclet.moves[0].currentPP;
// Switch out and back
engine.executeActions(
{ type: 'switch', piclet: 'player', newPicletIndex: 1 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
engine.executeActions(
{ type: 'switch', piclet: 'player', newPicletIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const ppAfterReturn = engine.getState().playerPiclet.moves[0].currentPP;
// PP should be preserved
expect(ppAfterReturn).toBe(ppAfterMove);
});
it('should reset stat modifications when switching', () => {
const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
// Apply stat modification
engine['state'].playerPiclet.attack += 20; // Simulate boost
engine['state'].playerPiclet.statModifiers.attack = 1;
const boostedAttack = engine.getState().playerPiclet.attack;
// Switch out and back
engine.executeActions(
{ type: 'switch', piclet: 'player', newPicletIndex: 1 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
engine.executeActions(
{ type: 'switch', piclet: 'player', newPicletIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const finalAttack = engine.getState().playerPiclet.attack;
// Attack should be reset to base value
expect(finalAttack).toBeLessThan(boostedAttack);
expect(engine.getState().playerPiclet.statModifiers.attack).toBeFalsy();
});
});
});