|
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest'; |
|
import { BattleEngine } from './BattleEngine'; |
|
import { PicletDefinition, Move, SpecialAbility } from './types'; |
|
import { PicletType, AttackType } from './types'; |
|
|
|
const STANDARD_STATS = { hp: 100, attack: 80, defense: 70, speed: 60 }; |
|
|
|
describe('Extreme Risk-Reward Moves - TDD Implementation', () => { |
|
describe('Self Destruct - Ultimate Sacrifice', () => { |
|
it('should handle Self Destruct move', () => { |
|
const selfDestruct: Move = { |
|
name: "Self Destruct", |
|
type: AttackType.NORMAL, |
|
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 bomberPiclet: PicletDefinition = { |
|
name: "Bomb Beast", |
|
description: "A creature that can self-destruct", |
|
tier: 'medium', |
|
primaryType: PicletType.MACHINA, |
|
baseStats: STANDARD_STATS, |
|
nature: "Brave", |
|
specialAbility: { name: "None", description: "No ability" }, |
|
movepool: [selfDestruct] |
|
}; |
|
|
|
const targetPiclet: PicletDefinition = { |
|
name: "Target", |
|
description: "Target dummy", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: STANDARD_STATS, |
|
nature: "Hardy", |
|
specialAbility: { name: "None", description: "No ability" }, |
|
movepool: [{ |
|
name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35, |
|
priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
}] |
|
}; |
|
|
|
|
|
expect(selfDestruct.effects).toHaveLength(2); |
|
expect(selfDestruct.effects[0].target).toBe('all'); |
|
expect(selfDestruct.effects[1].value).toBe(9999); |
|
}); |
|
}); |
|
|
|
describe('Berserker\'s End - Conditional Power', () => { |
|
it('should handle Berserker\'s End with conditional effects', () => { |
|
const berserkersEnd: Move = { |
|
name: "Berserker's End", |
|
type: AttackType.BEAST, |
|
power: 80, |
|
accuracy: 95, |
|
pp: 10, |
|
priority: 0, |
|
flags: ['contact', 'reckless'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'strong', |
|
condition: 'ifLowHp' |
|
}, |
|
{ |
|
type: 'mechanicOverride', |
|
target: 'self', |
|
mechanic: 'healingBlocked', |
|
value: true |
|
} |
|
] |
|
}; |
|
|
|
const berserkerPiclet: PicletDefinition = { |
|
name: "Berserker", |
|
description: "Fights with reckless abandon", |
|
tier: 'high', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 120, attack: 100, defense: 90, speed: 85 }, |
|
nature: "Reckless", |
|
specialAbility: { name: "None", description: "No ability" }, |
|
movepool: [berserkersEnd] |
|
}; |
|
|
|
|
|
expect(berserkersEnd.effects).toHaveLength(3); |
|
expect(berserkersEnd.effects[1].condition).toBe('ifLowHp'); |
|
expect(berserkersEnd.effects[2].mechanic).toBe('healingBlocked'); |
|
}); |
|
}); |
|
|
|
describe('Life Drain Overload - Massive Heal with Permanent Cost', () => { |
|
it('should handle Life Drain Overload move', () => { |
|
const lifeDrainOverload: Move = { |
|
name: "Life Drain Overload", |
|
type: AttackType.CULTURE, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 3, |
|
priority: 0, |
|
flags: ['draining'], |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
formula: 'percentage', |
|
value: 75 |
|
}, |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'greatly_decrease' }, |
|
condition: 'afterUse' |
|
} |
|
] |
|
}; |
|
|
|
expect(lifeDrainOverload.effects[0].formula).toBe('percentage'); |
|
expect(lifeDrainOverload.effects[0].value).toBe(75); |
|
expect(lifeDrainOverload.effects[1].stats.attack).toBe('greatly_decrease'); |
|
}); |
|
}); |
|
|
|
describe('Cursed Gambit - Random Extreme Outcome', () => { |
|
it('should handle Cursed Gambit with random effects', () => { |
|
const cursedGambit: Move = { |
|
name: "Cursed Gambit", |
|
type: AttackType.CULTURE, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 1, |
|
priority: 0, |
|
flags: ['gambling', 'cursed'], |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
formula: 'percentage', |
|
value: 100, |
|
condition: 'ifLucky50' |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'self', |
|
formula: 'fixed', |
|
value: 9999, |
|
condition: 'ifUnlucky50' |
|
} |
|
] |
|
}; |
|
|
|
expect(cursedGambit.effects).toHaveLength(2); |
|
expect(cursedGambit.effects[0].condition).toBe('ifLucky50'); |
|
expect(cursedGambit.effects[1].condition).toBe('ifUnlucky50'); |
|
expect(cursedGambit.flags).toContain('gambling'); |
|
}); |
|
}); |
|
|
|
describe('Blood Pact - Sacrifice HP for Permanent Power', () => { |
|
it('should handle Blood Pact move', () => { |
|
const bloodPact: Move = { |
|
name: "Blood Pact", |
|
type: AttackType.CULTURE, |
|
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' |
|
} |
|
] |
|
}; |
|
|
|
expect(bloodPact.effects[0].formula).toBe('percentage'); |
|
expect(bloodPact.effects[1].value).toBe(2.0); |
|
expect(bloodPact.flags).toContain('sacrifice'); |
|
}); |
|
}); |
|
|
|
describe('Soul Burn - PP Sacrifice for Power', () => { |
|
it('should handle Soul Burn move', () => { |
|
const soulBurn: Move = { |
|
name: "Soul Burn", |
|
type: AttackType.SPACE, |
|
power: 150, |
|
accuracy: 90, |
|
pp: 5, |
|
priority: 0, |
|
flags: ['burning'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'extreme' |
|
}, |
|
{ |
|
type: 'manipulatePP', |
|
target: 'self', |
|
action: 'drain', |
|
value: 3, |
|
targetMove: 'random', |
|
condition: 'afterUse' |
|
} |
|
] |
|
}; |
|
|
|
expect(soulBurn.effects[0].amount).toBe('extreme'); |
|
expect(soulBurn.effects[1].value).toBe(3); |
|
expect(soulBurn.effects[1].targetMove).toBe('random'); |
|
}); |
|
}); |
|
|
|
describe('Mirror Shatter - Damage Reflection with Cost', () => { |
|
it('should handle Mirror Shatter move', () => { |
|
const mirrorShatter: Move = { |
|
name: "Mirror Shatter", |
|
type: AttackType.MINERAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 5, |
|
priority: 4, |
|
flags: ['priority'], |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
target: 'self', |
|
mechanic: 'damageReflection', |
|
value: 'double', |
|
condition: 'thisTurn' |
|
}, |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { defense: 'greatly_decrease' }, |
|
condition: 'afterUse' |
|
} |
|
] |
|
}; |
|
|
|
expect(mirrorShatter.priority).toBe(4); |
|
expect(mirrorShatter.effects[0].value).toBe('double'); |
|
expect(mirrorShatter.effects[1].stats.defense).toBe('greatly_decrease'); |
|
}); |
|
}); |
|
|
|
describe('Apocalypse Strike - AoE Devastation with Vulnerability', () => { |
|
it('should handle Apocalypse Strike move', () => { |
|
const apocalypseStrike: Move = { |
|
name: "Apocalypse Strike", |
|
type: AttackType.SPACE, |
|
power: 120, |
|
accuracy: 85, |
|
pp: 1, |
|
priority: 0, |
|
flags: ['apocalyptic'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'all', |
|
formula: 'standard', |
|
multiplier: 1.3 |
|
}, |
|
{ |
|
type: 'mechanicOverride', |
|
target: 'self', |
|
mechanic: 'criticalHits', |
|
value: 'alwaysReceive', |
|
condition: 'restOfBattle' |
|
}, |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { defense: 'greatly_decrease' } |
|
} |
|
] |
|
}; |
|
|
|
expect(apocalypseStrike.effects).toHaveLength(3); |
|
expect(apocalypseStrike.effects[0].target).toBe('all'); |
|
expect(apocalypseStrike.effects[1].value).toBe('alwaysReceive'); |
|
expect(apocalypseStrike.pp).toBe(1); |
|
}); |
|
}); |
|
|
|
describe('Temporal Overload - Extra Turn with Cost', () => { |
|
it('should handle Temporal Overload move', () => { |
|
const temporalOverload: Move = { |
|
name: "Temporal Overload", |
|
type: AttackType.SPACE, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 2, |
|
priority: 0, |
|
flags: ['temporal'], |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
target: 'self', |
|
mechanic: 'extraTurn', |
|
value: true, |
|
condition: 'nextTurn' |
|
}, |
|
{ |
|
type: 'applyStatus', |
|
target: 'self', |
|
status: 'paralyze', |
|
chance: 100, |
|
condition: 'turnAfterNext' |
|
} |
|
] |
|
}; |
|
|
|
expect(temporalOverload.effects[0].mechanic).toBe('extraTurn'); |
|
expect(temporalOverload.effects[1].condition).toBe('turnAfterNext'); |
|
expect(temporalOverload.flags).toContain('temporal'); |
|
}); |
|
}); |
|
|
|
describe('Multi-Stage Effects - Charging Blast', () => { |
|
it('should handle Charging Blast with multi-stage effects', () => { |
|
const chargingBlast: Move = { |
|
name: "Charging Blast", |
|
type: AttackType.SPACE, |
|
power: 120, |
|
accuracy: 90, |
|
pp: 5, |
|
priority: 0, |
|
flags: ['charging'], |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { defense: 'increase' }, |
|
condition: 'onCharging' |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'extreme', |
|
condition: 'afterCharging' |
|
}, |
|
{ |
|
type: 'applyStatus', |
|
target: 'self', |
|
status: 'paralyze', |
|
condition: 'afterCharging' |
|
} |
|
] |
|
}; |
|
|
|
expect(chargingBlast.effects).toHaveLength(3); |
|
expect(chargingBlast.effects[0].condition).toBe('onCharging'); |
|
expect(chargingBlast.effects[1].condition).toBe('afterCharging'); |
|
expect(chargingBlast.flags).toContain('charging'); |
|
}); |
|
}); |
|
|
|
describe('Void Sacrifice - Field Effect with Self-Harm', () => { |
|
it('should handle Void Sacrifice from Tempest Wraith example', () => { |
|
const voidSacrifice: Move = { |
|
name: "Void Sacrifice", |
|
type: AttackType.SPACE, |
|
power: 130, |
|
accuracy: 85, |
|
pp: 1, |
|
priority: 0, |
|
flags: ['sacrifice', 'explosive'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'all', |
|
formula: 'standard', |
|
multiplier: 1.2 |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'self', |
|
formula: 'percentage', |
|
value: 75 |
|
}, |
|
{ |
|
type: 'fieldEffect', |
|
effect: 'voidStorm', |
|
target: 'field', |
|
stackable: false |
|
} |
|
] |
|
}; |
|
|
|
expect(voidSacrifice.effects).toHaveLength(3); |
|
expect(voidSacrifice.effects[2].effect).toBe('voidStorm'); |
|
expect(voidSacrifice.effects[2].stackable).toBe(false); |
|
}); |
|
}); |
|
|
|
describe('Integration Test - Complex Battle with Extreme Moves', () => { |
|
it('should handle a battle with multiple extreme moves', () => { |
|
const extremePiclet: PicletDefinition = { |
|
name: "Chaos Incarnate", |
|
description: "Master of extreme techniques", |
|
tier: 'legendary', |
|
primaryType: PicletType.SPACE, |
|
secondaryType: PicletType.CULTURE, |
|
baseStats: { hp: 150, attack: 120, defense: 80, speed: 100 }, |
|
nature: "Reckless", |
|
specialAbility: { |
|
name: "Chaos Heart", |
|
description: "Gains power from desperation", |
|
triggers: [ |
|
{ |
|
event: 'onLowHP', |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'damageMultiplier', |
|
value: 1.5 |
|
} |
|
] |
|
} |
|
] |
|
}, |
|
movepool: [ |
|
{ |
|
name: "Cursed Gambit", |
|
type: AttackType.CULTURE, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 1, |
|
priority: 0, |
|
flags: ['gambling'], |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
formula: 'percentage', |
|
value: 100, |
|
condition: 'ifLucky50' |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'self', |
|
formula: 'fixed', |
|
value: 9999, |
|
condition: 'ifUnlucky50' |
|
} |
|
] |
|
}, |
|
{ |
|
name: "Blood Pact", |
|
type: AttackType.CULTURE, |
|
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' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const standardPiclet: PicletDefinition = { |
|
name: "Standard Fighter", |
|
description: "Uses normal moves", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: STANDARD_STATS, |
|
nature: "Hardy", |
|
specialAbility: { name: "None", description: "No ability" }, |
|
movepool: [{ |
|
name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35, |
|
priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
}] |
|
}; |
|
|
|
const engine = new BattleEngine(extremePiclet, standardPiclet); |
|
|
|
|
|
expect(engine.getState().playerPiclet.definition.name).toBe("Chaos Incarnate"); |
|
expect(engine.getState().playerPiclet.moves).toHaveLength(2); |
|
expect(engine.getState().playerPiclet.moves[0].move.name).toBe("Cursed Gambit"); |
|
expect(engine.getState().playerPiclet.moves[1].move.name).toBe("Blood Pact"); |
|
|
|
|
|
expect(extremePiclet.specialAbility.triggers).toHaveLength(1); |
|
expect(extremePiclet.specialAbility.triggers![0].event).toBe('onLowHP'); |
|
}); |
|
}); |
|
}); |