|
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(() => { |
|
|
|
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' }] |
|
} |
|
] |
|
}; |
|
|
|
|
|
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' }] |
|
} |
|
] |
|
}; |
|
|
|
|
|
fieldEffectUser = { |
|
name: "Field Controller", |
|
description: "Controls battlefield effects", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
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 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
|
|
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); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('barrier was raised to reduce contact move damage'))).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(25); |
|
expect(damage).toBeGreaterThan(0); |
|
}); |
|
|
|
it('should not reduce non-contact move damage', () => { |
|
const engine = new BattleEngine(fieldEffectUser, nonContactAttacker); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
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).toBeGreaterThan(25); |
|
}); |
|
}); |
|
|
|
describe('Non-Contact Damage Reduction (Light Screen)', () => { |
|
it('should reduce non-contact move damage by 50%', () => { |
|
const engine = new BattleEngine(fieldEffectUser, nonContactAttacker); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 1 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('barrier was raised to reduce non-contact move damage'))).toBe(true); |
|
|
|
|
|
const initialHp = engine.getState().playerPiclet.currentHp; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 1 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const finalHp = engine.getState().playerPiclet.currentHp; |
|
const damage = initialHp - finalHp; |
|
|
|
|
|
expect(damage).toBeLessThan(25); |
|
expect(damage).toBeGreaterThan(0); |
|
}); |
|
|
|
it('should not reduce contact move damage', () => { |
|
const engine = new BattleEngine(fieldEffectUser, contactAttacker); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 1 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
const initialHp = engine.getState().playerPiclet.currentHp; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 1 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const finalHp = engine.getState().playerPiclet.currentHp; |
|
const damage = initialHp - finalHp; |
|
|
|
|
|
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 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('Entry spikes were scattered'))).toBe(true); |
|
|
|
|
|
const state = engine.getState(); |
|
expect(state.fieldEffects.some(effect => effect.name === 'entryHazardSpikes')).toBe(true); |
|
}); |
|
|
|
it('should be stackable', () => { |
|
const engine = new BattleEngine(fieldEffectUser, basicOpponent); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 2 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 2 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const state = engine.getState(); |
|
const spikeEffects = state.fieldEffects.filter(effect => effect.name === 'entryHazardSpikes'); |
|
expect(spikeEffects.length).toBe(2); |
|
}); |
|
}); |
|
|
|
describe('Healing Field', () => { |
|
it('should create a healing field that affects both sides', () => { |
|
const engine = new BattleEngine(fieldEffectUser, basicOpponent); |
|
|
|
|
|
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 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('healing field was created'))).toBe(true); |
|
|
|
|
|
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); |
|
|
|
|
|
const playerInitialHp = engine.getState().playerPiclet.currentHp; |
|
const opponentInitialHp = engine.getState().opponentPiclet.currentHp; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 3 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
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); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
expect(engine.getState().fieldEffects.length).toBe(1); |
|
|
|
|
|
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('faded away'))).toBe(true); |
|
expect(engine.getState().fieldEffects.length).toBe(0); |
|
}); |
|
|
|
it('should not stack non-stackable effects', () => { |
|
const engine = new BattleEngine(fieldEffectUser, basicOpponent); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
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 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
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); |
|
|
|
|
|
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 } |
|
); |
|
|
|
const baselineDamage = baselineInitialHp - baselineEngine.getState().playerPiclet.currentHp; |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ 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 } |
|
); |
|
|
|
const protectedDamage = protectedInitialHp - engine.getState().playerPiclet.currentHp; |
|
|
|
|
|
expect(protectedDamage).toBeLessThan(baselineDamage * 0.75); |
|
}); |
|
|
|
it('should handle multiple field effects simultaneously', () => { |
|
const engine = new BattleEngine(fieldEffectUser, basicOpponent); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 3 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 2 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
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'); |
|
}); |
|
}); |
|
}); |