| 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'); |
| }); |
| }); |
| }); |