|
import { describe, it, expect, beforeEach } from 'vitest'; |
|
import { BattleEngine } from './BattleEngine'; |
|
import type { PicletDefinition } from './types'; |
|
import { PicletType, AttackType } from './types'; |
|
|
|
describe('Extreme Risk-Reward Moves', () => { |
|
describe('Self-Destruct Moves', () => { |
|
it('should deal massive damage to all but KO the user', () => { |
|
const bomber: PicletDefinition = { |
|
name: "Suicide Bomber", |
|
description: "Sacrifices itself for massive damage", |
|
tier: 'medium', |
|
primaryType: PicletType.MACHINA, |
|
baseStats: { hp: 60, attack: 40, defense: 60, speed: 50 }, |
|
nature: "Brave", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Self Destruct", |
|
type: AttackType.MACHINA, |
|
power: 200, |
|
accuracy: 100, |
|
pp: 1, |
|
priority: 0, |
|
flags: ['explosive', 'contact'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'all', |
|
formula: 'standard', |
|
multiplier: 1.5 |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'self', |
|
formula: 'fixed', |
|
value: 9999, |
|
condition: 'afterUse' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const opponent: PicletDefinition = { |
|
name: "Sturdy Opponent", |
|
description: "Tanky opponent", |
|
tier: 'high', |
|
primaryType: PicletType.MINERAL, |
|
baseStats: { hp: 100, attack: 60, defense: 100, speed: 40 }, |
|
nature: "Impish", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Tackle", |
|
type: AttackType.NORMAL, |
|
power: 40, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(bomber, opponent); |
|
const initialOpponentHp = engine.getState().opponentPiclet.currentHp; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const finalOpponentHp = engine.getState().opponentPiclet.currentHp; |
|
const playerHp = engine.getState().playerPiclet.currentHp; |
|
|
|
|
|
expect(playerHp).toBe(0); |
|
|
|
|
|
expect(finalOpponentHp).toBeLessThan(initialOpponentHp); |
|
const damage = initialOpponentHp - finalOpponentHp; |
|
expect(damage).toBeGreaterThan(45); |
|
|
|
const log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('Self Destruct') || msg.includes('exploded'))).toBe(true); |
|
expect(engine.isGameOver()).toBe(true); |
|
}); |
|
}); |
|
|
|
describe('Gambling Moves', () => { |
|
it('should have random success/failure outcomes', () => { |
|
const gambler: PicletDefinition = { |
|
name: "Lucky Gambler", |
|
description: "Relies on luck for power", |
|
tier: 'medium', |
|
primaryType: PicletType.CULTURE, |
|
baseStats: { hp: 70, attack: 60, defense: 60, speed: 80 }, |
|
nature: "Hasty", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Cursed Gambit", |
|
type: AttackType.CULTURE, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 1, |
|
priority: 0, |
|
flags: ['gambling', 'cursed'], |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
amount: 'percentage', |
|
value: 100, |
|
condition: 'ifLucky50' |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'self', |
|
formula: 'fixed', |
|
value: 9999, |
|
condition: 'ifUnlucky50' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const opponent: PicletDefinition = { |
|
name: "Opponent", |
|
description: "Standard opponent", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 }, |
|
nature: "Hardy", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Do Nothing", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [] |
|
} |
|
] |
|
}; |
|
|
|
|
|
let healedCount = 0; |
|
let faintedCount = 0; |
|
|
|
for (let i = 0; i < 20; i++) { |
|
const engine = new BattleEngine(gambler, opponent); |
|
|
|
engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.5); |
|
const preGambitHp = engine.getState().playerPiclet.currentHp; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const postGambitHp = engine.getState().playerPiclet.currentHp; |
|
const maxHp = engine.getState().playerPiclet.maxHp; |
|
|
|
|
|
if (postGambitHp === 0) { |
|
faintedCount++; |
|
} else if (postGambitHp > preGambitHp) { |
|
healedCount++; |
|
} |
|
} |
|
|
|
|
|
expect(healedCount + faintedCount).toBeGreaterThan(0); |
|
expect(healedCount).toBeGreaterThan(0); |
|
expect(faintedCount).toBeGreaterThan(0); |
|
}); |
|
}); |
|
|
|
describe('Sacrifice Moves', () => { |
|
it('should provide powerful effects at great personal cost', () => { |
|
const sacrificer: PicletDefinition = { |
|
name: "Blood Warrior", |
|
description: "Sacrifices HP for power", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 100, attack: 70, defense: 60, speed: 60 }, |
|
nature: "Brave", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Blood Pact", |
|
type: AttackType.BEAST, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 3, |
|
priority: 0, |
|
flags: ['sacrifice'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'self', |
|
formula: 'percentage', |
|
value: 50 |
|
}, |
|
{ |
|
type: 'mechanicOverride', |
|
target: 'self', |
|
mechanic: 'damageMultiplier', |
|
value: 2.0, |
|
condition: 'restOfBattle' |
|
} |
|
] |
|
}, |
|
{ |
|
name: "Strike", |
|
type: AttackType.BEAST, |
|
power: 60, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const opponent: PicletDefinition = { |
|
name: "Opponent", |
|
description: "Standard opponent", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 }, |
|
nature: "Hardy", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Tackle", |
|
type: AttackType.NORMAL, |
|
power: 40, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(sacrificer, opponent); |
|
const initialHp = engine.getState().playerPiclet.currentHp; |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const hpAfterSacrifice = engine.getState().playerPiclet.currentHp; |
|
expect(hpAfterSacrifice).toBeLessThan(initialHp); |
|
|
|
|
|
const initialOpponentHp = engine.getState().opponentPiclet.currentHp; |
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 1 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const finalOpponentHp = engine.getState().opponentPiclet.currentHp; |
|
const damage = initialOpponentHp - finalOpponentHp; |
|
|
|
|
|
expect(damage).toBeGreaterThan(60); |
|
|
|
const log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('Blood Pact') || msg.includes('sacrifice'))).toBe(true); |
|
}); |
|
}); |
|
|
|
describe('Conditional Power Scaling', () => { |
|
it('should scale damage based on conditions', () => { |
|
const revengeUser: PicletDefinition = { |
|
name: "Revenge Fighter", |
|
description: "Gets stronger when damaged", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 90, attack: 70, defense: 80, speed: 50 }, |
|
nature: "Brave", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Revenge Strike", |
|
type: AttackType.BEAST, |
|
power: 60, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'strong', |
|
condition: 'ifDamagedThisTurn' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const attacker: PicletDefinition = { |
|
name: "Fast Attacker", |
|
description: "Quick attacker", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 200, attack: 80, defense: 60, speed: 100 }, |
|
nature: "Hasty", |
|
specialAbility: { name: "No Ability", description: "" }, |
|
movepool: [ |
|
{ |
|
name: "Quick Strike", |
|
type: AttackType.BEAST, |
|
power: 50, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 1, |
|
flags: ['contact', 'priority'], |
|
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
} |
|
] |
|
}; |
|
|
|
|
|
const engine1 = new BattleEngine(revengeUser, attacker); |
|
const initialOpponentHp = engine1.getState().opponentPiclet.currentHp; |
|
engine1.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const hpAfterNormalRevenge = engine1.getState().opponentPiclet.currentHp; |
|
const normalRevengeDamage = initialOpponentHp - hpAfterNormalRevenge; |
|
|
|
|
|
const engine2 = new BattleEngine(revengeUser, attacker); |
|
|
|
engine2['state'].playerPiclet.currentHp = Math.floor(engine2['state'].playerPiclet.maxHp * 0.5); |
|
|
|
const initialOpponentHp2 = engine2.getState().opponentPiclet.currentHp; |
|
engine2.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const hpAfterPoweredRevenge = engine2.getState().opponentPiclet.currentHp; |
|
const poweredRevengeDamage = initialOpponentHp2 - hpAfterPoweredRevenge; |
|
|
|
|
|
const damageMessages = engine2.getLog().filter(msg => msg.includes('took') && msg.includes('damage')); |
|
expect(damageMessages.length).toBeGreaterThanOrEqual(3); |
|
|
|
|
|
expect(poweredRevengeDamage).toBeGreaterThan(100); |
|
}); |
|
}); |
|
}); |