piclets / src /lib /battle-engine /field-effects.test.ts
Fraser's picture
battle battle
e78d70c
raw
history blame
17.3 kB
import { describe, it, expect, beforeEach } from 'vitest';
import { BattleEngine } from './BattleEngine';
import type { PicletDefinition } from './types';
import { PicletType, AttackType } from './types';
describe('Field Effects System', () => {
let contactAttacker: PicletDefinition;
let nonContactAttacker: PicletDefinition;
let fieldEffectUser: PicletDefinition;
let basicOpponent: PicletDefinition;
beforeEach(() => {
// Piclet that uses contact moves
contactAttacker = {
name: "Contact Fighter",
description: "Uses contact 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: 60,
accuracy: 100,
pp: 15,
priority: 0,
flags: ['contact'],
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
}
]
};
// Piclet that uses non-contact moves
nonContactAttacker = {
name: "Ranged Fighter",
description: "Uses non-contact moves",
tier: 'medium',
primaryType: PicletType.SPACE,
baseStats: { hp: 80, attack: 80, defense: 60, speed: 70 },
nature: "Modest",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Energy Blast",
type: AttackType.SPACE,
power: 60,
accuracy: 100,
pp: 15,
priority: 0,
flags: [],
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
}
]
};
// Piclet that can create field effects
fieldEffectUser = {
name: "Field Controller",
description: "Controls battlefield effects",
tier: 'medium',
primaryType: PicletType.BEAST, // Changed to BEAST so opponent can damage it
baseStats: { hp: 100, attack: 60, defense: 80, speed: 60 },
nature: "Calm",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Contact Barrier",
type: AttackType.SPACE,
power: 0,
accuracy: 100,
pp: 10,
priority: 0,
flags: [],
effects: [
{
type: 'fieldEffect',
effect: 'reflect',
target: 'playerSide',
stackable: false
}
]
},
{
name: "Non-Contact Barrier",
type: AttackType.SPACE,
power: 0,
accuracy: 100,
pp: 10,
priority: 0,
flags: [],
effects: [
{
type: 'fieldEffect',
effect: 'lightScreen',
target: 'playerSide',
stackable: false
}
]
},
{
name: "Entry Spikes",
type: AttackType.MINERAL,
power: 0,
accuracy: 100,
pp: 10,
priority: 0,
flags: [],
effects: [
{
type: 'fieldEffect',
effect: 'spikes',
target: 'opponentSide',
stackable: true
}
]
},
{
name: "Healing Field",
type: AttackType.FLORA,
power: 0,
accuracy: 100,
pp: 10,
priority: 0,
flags: [],
effects: [
{
type: 'fieldEffect',
effect: 'healingMist',
target: 'field',
stackable: false
}
]
}
]
};
// Basic opponent for testing
basicOpponent = {
name: "Basic Opponent",
description: "Standard test opponent",
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.NORMAL,
power: 50,
accuracy: 100,
pp: 20,
priority: 0,
flags: ['contact'],
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
}
]
};
});
describe('Contact Damage Reduction (Reflect)', () => {
it('should reduce contact move damage by 50%', () => {
const engine = new BattleEngine(fieldEffectUser, contactAttacker);
// Set up barrier first
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Basic Attack (will be reduced)
);
const log = engine.getLog();
expect(log.some(msg => msg.includes('barrier was raised to reduce contact move damage'))).toBe(true);
// Test that subsequent contact moves are reduced
const initialHp = engine.getState().playerPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier (no effect, already active)
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Basic Attack (should be reduced)
);
const finalHp = engine.getState().playerPiclet.currentHp;
const damage = initialHp - finalHp;
// Damage should be significantly reduced (less than normal ~30-40 damage)
expect(damage).toBeLessThan(25);
expect(damage).toBeGreaterThan(0); // But still some damage
});
it('should not reduce non-contact move damage', () => {
const engine = new BattleEngine(fieldEffectUser, nonContactAttacker);
// Set up contact barrier
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (non-contact)
);
// Test that non-contact moves are NOT reduced
const initialHp = engine.getState().playerPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (should not be reduced)
);
const finalHp = engine.getState().playerPiclet.currentHp;
const damage = initialHp - finalHp;
// Damage should be normal (around 30-50)
expect(damage).toBeGreaterThan(25);
});
});
describe('Non-Contact Damage Reduction (Light Screen)', () => {
it('should reduce non-contact move damage by 50%', () => {
const engine = new BattleEngine(fieldEffectUser, nonContactAttacker);
// Set up non-contact barrier
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 1 }, // Non-Contact Barrier
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (should be reduced)
);
const log = engine.getLog();
expect(log.some(msg => msg.includes('barrier was raised to reduce non-contact move damage'))).toBe(true);
// Test reduction on subsequent turn
const initialHp = engine.getState().playerPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 1 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (should be reduced)
);
const finalHp = engine.getState().playerPiclet.currentHp;
const damage = initialHp - finalHp;
// Damage should be reduced
expect(damage).toBeLessThan(25);
expect(damage).toBeGreaterThan(0);
});
it('should not reduce contact move damage', () => {
const engine = new BattleEngine(fieldEffectUser, contactAttacker);
// Set up non-contact barrier
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 1 }, // Non-Contact Barrier
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Physical Strike (contact)
);
// Test that contact moves are NOT reduced
const initialHp = engine.getState().playerPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 1 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Physical Strike (should not be reduced)
);
const finalHp = engine.getState().playerPiclet.currentHp;
const damage = initialHp - finalHp;
// Damage should be normal
expect(damage).toBeGreaterThan(25);
});
});
describe('Entry Hazards (Spikes)', () => {
it('should set up entry spikes on opponent side', () => {
const engine = new BattleEngine(fieldEffectUser, basicOpponent);
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const log = engine.getLog();
expect(log.some(msg => msg.includes('Entry spikes were scattered'))).toBe(true);
// Check that field effect was applied
const state = engine.getState();
expect(state.fieldEffects.some(effect => effect.name === 'entryHazardSpikes')).toBe(true);
});
it('should be stackable', () => {
const engine = new BattleEngine(fieldEffectUser, basicOpponent);
// Apply spikes twice
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes again
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const state = engine.getState();
const spikeEffects = state.fieldEffects.filter(effect => effect.name === 'entryHazardSpikes');
expect(spikeEffects.length).toBe(2); // Should stack
});
});
describe('Healing Field', () => {
it('should create a healing field that affects both sides', () => {
const engine = new BattleEngine(fieldEffectUser, basicOpponent);
// Damage both piclets first
engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.5);
engine['state'].opponentPiclet.currentHp = Math.floor(engine['state'].opponentPiclet.maxHp * 0.5);
const playerInitialHp = engine.getState().playerPiclet.currentHp;
const opponentInitialHp = engine.getState().opponentPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 3 }, // Healing Field
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const log = engine.getLog();
expect(log.some(msg => msg.includes('healing field was created'))).toBe(true);
// Both piclets should be healed at end of turn
const playerFinalHp = engine.getState().playerPiclet.currentHp;
const opponentFinalHp = engine.getState().opponentPiclet.currentHp;
expect(playerFinalHp).toBeGreaterThan(playerInitialHp);
expect(opponentFinalHp).toBeGreaterThan(opponentInitialHp);
expect(log.some(msg => msg.includes('healed by the healing field'))).toBe(true);
});
it('should not heal piclets at full HP', () => {
const engine = new BattleEngine(fieldEffectUser, basicOpponent);
// Both piclets at full HP
const playerInitialHp = engine.getState().playerPiclet.currentHp;
const opponentInitialHp = engine.getState().opponentPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 3 }, // Healing Field
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
// HP should remain the same
const playerFinalHp = engine.getState().playerPiclet.currentHp;
const opponentFinalHp = engine.getState().opponentPiclet.currentHp;
expect(playerFinalHp).toBe(playerInitialHp);
expect(opponentFinalHp).toBe(opponentInitialHp);
});
});
describe('Field Effect Duration and Management', () => {
it('should expire field effects after 5 turns', () => {
const engine = new BattleEngine(fieldEffectUser, basicOpponent);
// Create a barrier
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
// Verify it exists
expect(engine.getState().fieldEffects.length).toBe(1);
// Pass 5 turns
for (let i = 0; i < 5; i++) {
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
}
// Effect should have expired
const log = engine.getLog();
expect(log.some(msg => msg.includes('faded away'))).toBe(true);
expect(engine.getState().fieldEffects.length).toBe(0);
});
it('should not stack non-stackable effects', () => {
const engine = new BattleEngine(fieldEffectUser, basicOpponent);
// Apply same barrier twice
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier again
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
// Should only have one effect (refreshed duration)
const contactBarriers = engine.getState().fieldEffects.filter(
effect => effect.name === 'contactDamageReduction'
);
expect(contactBarriers.length).toBe(1);
});
it('should properly format field effect names in logs', () => {
const engine = new BattleEngine(fieldEffectUser, basicOpponent);
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
// Pass enough turns for effect to fade
for (let i = 0; i < 5; i++) {
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
}
const log = engine.getLog();
expect(log.some(msg =>
msg.includes('Contact damage barrier faded away') ||
msg.includes('contact damage barrier faded away')
)).toBe(true);
});
});
describe('Field Effect Integration with Battle Flow', () => {
it('should apply field effects during damage calculation', () => {
const engine = new BattleEngine(fieldEffectUser, contactAttacker);
// Measure baseline damage first
const baselineEngine = new BattleEngine(fieldEffectUser, contactAttacker);
const baselineInitialHp = baselineEngine.getState().playerPiclet.currentHp;
baselineEngine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // No barrier
);
const baselineDamage = baselineInitialHp - baselineEngine.getState().playerPiclet.currentHp;
// Now test with barrier
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const protectedInitialHp = engine.getState().playerPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 } // Attack against barrier
);
const protectedDamage = protectedInitialHp - engine.getState().playerPiclet.currentHp;
// Protected damage should be significantly less
expect(protectedDamage).toBeLessThan(baselineDamage * 0.75);
});
it('should handle multiple field effects simultaneously', () => {
const engine = new BattleEngine(fieldEffectUser, basicOpponent);
// Apply multiple field effects
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 3 }, // Healing Field
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
// Should have 3 different field effects
const state = engine.getState();
expect(state.fieldEffects.length).toBe(3);
const effectNames = state.fieldEffects.map(effect => effect.name);
expect(effectNames).toContain('contactDamageReduction');
expect(effectNames).toContain('healingField');
expect(effectNames).toContain('entryHazardSpikes');
});
});
});