piclets / src /lib /battle-engine /advanced-status-effects.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('Advanced Status Effects System', () => {
let basicPiclet: PicletDefinition;
let statusInflicter: PicletDefinition;
beforeEach(() => {
// Basic piclet without special abilities
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' }]
}
]
};
// Piclet that can inflict status effects
statusInflicter = {
name: "Status Master",
description: "Can inflict various status effects",
tier: 'medium',
primaryType: PicletType.CULTURE,
baseStats: { hp: 90, attack: 50, defense: 70, speed: 80 },
nature: "Timid",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Freeze Ray",
type: AttackType.AQUATIC,
power: 40,
accuracy: 90,
pp: 15,
priority: 0,
flags: [],
effects: [
{ type: 'damage', target: 'opponent', amount: 'normal' },
{ type: 'applyStatus', target: 'opponent', status: 'freeze', chance: 30 }
]
},
{
name: "Paralyzing Shock",
type: AttackType.MACHINA,
power: 45,
accuracy: 100,
pp: 20,
priority: 0,
flags: [],
effects: [
{ type: 'damage', target: 'opponent', amount: 'normal' },
{ type: 'applyStatus', target: 'opponent', status: 'paralyze', chance: 25 }
]
},
{
name: "Sleep Powder",
type: AttackType.FLORA,
power: 0,
accuracy: 85,
pp: 15,
priority: 0,
flags: [],
effects: [
{ type: 'applyStatus', target: 'opponent', status: 'sleep', chance: 100 }
]
},
{
name: "Confuse Ray",
type: AttackType.SPACE,
power: 0,
accuracy: 100,
pp: 10,
priority: 0,
flags: [],
effects: [
{ type: 'applyStatus', target: 'opponent', status: 'confuse', chance: 100 }
]
}
]
};
});
describe('Freeze Status Effect', () => {
it('should prevent the frozen piclet from acting', () => {
const engine = new BattleEngine(statusInflicter, basicPiclet);
// Force freeze to trigger by mocking Math.random
const originalRandom = Math.random;
Math.random = () => 0.1; // 10% < 30% chance, should trigger freeze
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Freeze Ray
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Should be prevented if frozen
);
// Restore Math.random
Math.random = originalRandom;
const log = engine.getLog();
const opponentState = engine.getState().opponentPiclet;
// Check that freeze was applied
expect(opponentState.statusEffects).toContain('freeze');
expect(log.some(msg => msg.includes('was frozen'))).toBe(true);
// Execute another turn to test freeze preventing action
if (!engine.isGameOver()) {
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const secondTurnLog = engine.getLog();
expect(secondTurnLog.some(msg => msg.includes('is frozen solid') || msg.includes('cannot move'))).toBe(true);
}
});
it('should have a chance to thaw each turn', () => {
const engine = new BattleEngine(statusInflicter, basicPiclet);
// Manually apply freeze status
engine['state'].opponentPiclet.statusEffects.push('freeze');
// Force thaw with low random number
const originalRandom = Math.random;
Math.random = () => 0.1; // Should trigger thaw (usually 20% chance)
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
// Restore Math.random
Math.random = originalRandom;
const log = engine.getLog();
const opponentState = engine.getState().opponentPiclet;
// Should thaw and be able to act
expect(log.some(msg => msg.includes('thawed out') || msg.includes('is no longer frozen'))).toBe(true);
expect(opponentState.statusEffects).not.toContain('freeze');
});
});
describe('Paralysis Status Effect', () => {
it('should reduce speed by 50%', () => {
const engine = new BattleEngine(statusInflicter, basicPiclet);
const initialSpeed = engine.getState().opponentPiclet.speed;
// Force paralysis to trigger
const originalRandom = Math.random;
Math.random = () => 0.1; // 10% < 25% chance
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 1 }, // Paralyzing Shock
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
// Restore Math.random
Math.random = originalRandom;
const finalSpeed = engine.getState().opponentPiclet.speed;
const log = engine.getLog();
expect(engine.getState().opponentPiclet.statusEffects).toContain('paralyze');
expect(log.some(msg => msg.includes('was paralyzed'))).toBe(true);
expect(finalSpeed).toBe(Math.floor(initialSpeed * 0.5)); // 50% speed reduction
});
it('should have 25% chance to prevent action', () => {
const engine = new BattleEngine(statusInflicter, basicPiclet);
// Manually apply paralysis
engine['state'].opponentPiclet.statusEffects.push('paralyze');
engine['state'].opponentPiclet.speed = Math.floor(engine['state'].opponentPiclet.speed * 0.5);
// Force paralysis to prevent action
const originalRandom = Math.random;
Math.random = () => 0.1; // Should trigger paralysis prevention (25% chance)
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Should be prevented
);
// Restore Math.random
Math.random = originalRandom;
const log = engine.getLog();
expect(log.some(msg =>
msg.includes('is fully paralyzed') ||
msg.includes('cannot move due to paralysis')
)).toBe(true);
});
});
describe('Sleep Status Effect', () => {
it('should prevent action and last 1-3 turns', () => {
const engine = new BattleEngine(statusInflicter, basicPiclet);
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 2 }, // Sleep Powder
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Should be prevented if asleep
);
const log = engine.getLog();
const opponentState = engine.getState().opponentPiclet;
expect(opponentState.statusEffects).toContain('sleep');
expect(log.some(msg => msg.includes('fell asleep'))).toBe(true);
// Sleep should prevent action
if (!engine.isGameOver()) {
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const secondLog = engine.getLog();
expect(secondLog.some(msg =>
msg.includes('is fast asleep') ||
msg.includes('cannot wake up')
)).toBe(true);
}
});
it('should wake up when attacked', () => {
const engine = new BattleEngine(basicPiclet, statusInflicter);
// Put player to sleep
engine['state'].playerPiclet.statusEffects.push('sleep');
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Should be prevented by sleep
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Attack should wake up player
);
const log = engine.getLog();
const playerState = engine.getState().playerPiclet;
// Should wake up when damaged
expect(log.some(msg => msg.includes('woke up'))).toBe(true);
expect(playerState.statusEffects).not.toContain('sleep');
});
});
describe('Confusion Status Effect', () => {
it('should last 2-5 turns and cause self-damage 33% of the time', () => {
const engine = new BattleEngine(statusInflicter, basicPiclet);
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 3 }, // Confuse Ray
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const log = engine.getLog();
const opponentState = engine.getState().opponentPiclet;
expect(opponentState.statusEffects).toContain('confuse');
expect(log.some(msg => msg.includes('became confused'))).toBe(true);
// Test confusion self-damage
const initialHp = engine.getState().opponentPiclet.currentHp;
// Force confusion self-damage
const originalRandom = Math.random;
Math.random = () => 0.2; // Should trigger self-damage (33% chance)
if (!engine.isGameOver()) {
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const confusedLog = engine.getLog();
const finalHp = engine.getState().opponentPiclet.currentHp;
expect(confusedLog.some(msg =>
msg.includes('hurt itself in confusion') ||
msg.includes('attacked itself')
)).toBe(true);
}
// Restore Math.random
Math.random = originalRandom;
});
it('should wear off after 2-5 turns', () => {
const engine = new BattleEngine(statusInflicter, basicPiclet);
// Manually apply confusion with duration
engine['state'].opponentPiclet.statusEffects.push('confuse');
(engine['state'].opponentPiclet as any).confusionTurns = 1; // Set to expire next turn
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const log = engine.getLog();
const opponentState = engine.getState().opponentPiclet;
expect(log.some(msg => msg.includes('is no longer confused') || msg.includes('snapped out of confusion'))).toBe(true);
expect(opponentState.statusEffects).not.toContain('confuse');
});
});
describe('Status Effect Interactions', () => {
it('should not allow multiple major status effects simultaneously', () => {
const engine = new BattleEngine(statusInflicter, basicPiclet);
// Apply freeze first
engine['state'].opponentPiclet.statusEffects.push('freeze');
// Try to apply paralysis
const originalRandom = Math.random;
Math.random = () => 0.1; // Should trigger paralysis normally
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 1 }, // Paralyzing Shock
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
Math.random = originalRandom;
const opponentState = engine.getState().opponentPiclet;
const majorStatuses = opponentState.statusEffects.filter(status =>
['freeze', 'paralyze', 'sleep'].includes(status)
);
// Should only have one major status effect
expect(majorStatuses.length).toBeLessThanOrEqual(1);
});
it('should allow confusion alongside other status effects', () => {
const engine = new BattleEngine(statusInflicter, basicPiclet);
// Apply paralysis first
engine['state'].opponentPiclet.statusEffects.push('paralyze');
// Apply confusion
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 3 }, // Confuse Ray
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const opponentState = engine.getState().opponentPiclet;
// Should have both paralysis and confusion
expect(opponentState.statusEffects).toContain('paralyze');
expect(opponentState.statusEffects).toContain('confuse');
});
});
});