|
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(() => { |
|
|
|
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' }] |
|
} |
|
] |
|
}; |
|
|
|
|
|
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); |
|
|
|
|
|
const originalRandom = Math.random; |
|
Math.random = () => 0.1; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
Math.random = originalRandom; |
|
|
|
const log = engine.getLog(); |
|
const opponentState = engine.getState().opponentPiclet; |
|
|
|
|
|
expect(opponentState.statusEffects).toContain('freeze'); |
|
expect(log.some(msg => msg.includes('was frozen'))).toBe(true); |
|
|
|
|
|
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); |
|
|
|
|
|
engine['state'].opponentPiclet.statusEffects.push('freeze'); |
|
|
|
|
|
const originalRandom = Math.random; |
|
Math.random = () => 0.1; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
Math.random = originalRandom; |
|
|
|
const log = engine.getLog(); |
|
const opponentState = engine.getState().opponentPiclet; |
|
|
|
|
|
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; |
|
|
|
|
|
const originalRandom = Math.random; |
|
Math.random = () => 0.1; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 1 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
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)); |
|
}); |
|
|
|
it('should have 25% chance to prevent action', () => { |
|
const engine = new BattleEngine(statusInflicter, basicPiclet); |
|
|
|
|
|
engine['state'].opponentPiclet.statusEffects.push('paralyze'); |
|
engine['state'].opponentPiclet.speed = Math.floor(engine['state'].opponentPiclet.speed * 0.5); |
|
|
|
|
|
const originalRandom = Math.random; |
|
Math.random = () => 0.1; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
|
|
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 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const log = engine.getLog(); |
|
const opponentState = engine.getState().opponentPiclet; |
|
|
|
expect(opponentState.statusEffects).toContain('sleep'); |
|
expect(log.some(msg => msg.includes('fell asleep'))).toBe(true); |
|
|
|
|
|
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); |
|
|
|
|
|
engine['state'].playerPiclet.statusEffects.push('sleep'); |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const log = engine.getLog(); |
|
const playerState = engine.getState().playerPiclet; |
|
|
|
|
|
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 }, |
|
{ 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); |
|
|
|
|
|
const initialHp = engine.getState().opponentPiclet.currentHp; |
|
|
|
|
|
const originalRandom = Math.random; |
|
Math.random = () => 0.2; |
|
|
|
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); |
|
} |
|
|
|
|
|
Math.random = originalRandom; |
|
}); |
|
|
|
it('should wear off after 2-5 turns', () => { |
|
const engine = new BattleEngine(statusInflicter, basicPiclet); |
|
|
|
|
|
engine['state'].opponentPiclet.statusEffects.push('confuse'); |
|
(engine['state'].opponentPiclet as any).confusionTurns = 1; |
|
|
|
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); |
|
|
|
|
|
engine['state'].opponentPiclet.statusEffects.push('freeze'); |
|
|
|
|
|
const originalRandom = Math.random; |
|
Math.random = () => 0.1; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 1 }, |
|
{ 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) |
|
); |
|
|
|
|
|
expect(majorStatuses.length).toBeLessThanOrEqual(1); |
|
}); |
|
|
|
it('should allow confusion alongside other status effects', () => { |
|
const engine = new BattleEngine(statusInflicter, basicPiclet); |
|
|
|
|
|
engine['state'].opponentPiclet.statusEffects.push('paralyze'); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 3 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const opponentState = engine.getState().opponentPiclet; |
|
|
|
|
|
expect(opponentState.statusEffects).toContain('paralyze'); |
|
expect(opponentState.statusEffects).toContain('confuse'); |
|
}); |
|
}); |
|
}); |