import { describe, it, expect, beforeEach } from 'vitest'; import { BattleEngine } from './BattleEngine'; import type { PicletDefinition, SpecialAbility } from './types'; import { PicletType, AttackType } from './types'; describe('Move Flags and Flag-Based Interactions', () => { describe('Flag-Based Immunities', () => { it('should provide immunity to contact moves with Ethereal Form', () => { const etherealForm: SpecialAbility = { name: "Ethereal Form", description: "Ghostly body cannot be touched by physical contact", effects: [ { type: 'mechanicOverride', mechanic: 'flagImmunity', value: ['contact'] } ] }; const ghostly: PicletDefinition = { name: "Ghost Fighter", description: "Ethereal being immune to contact", tier: 'medium', primaryType: PicletType.CULTURE, baseStats: { hp: 70, attack: 80, defense: 50, speed: 90 }, nature: "Timid", specialAbility: etherealForm, movepool: [ { name: "Shadow Ball", type: AttackType.CULTURE, power: 80, accuracy: 100, pp: 10, priority: 0, flags: [], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const contactUser: PicletDefinition = { name: "Physical Fighter", description: "Uses contact moves", tier: 'medium', primaryType: PicletType.BEAST, baseStats: { hp: 80, attack: 90, defense: 70, speed: 60 }, nature: "Adamant", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Punch", type: AttackType.BEAST, power: 75, accuracy: 100, pp: 10, priority: 0, flags: ['contact', 'punch'], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] }, { name: "Energy Blast", type: AttackType.SPACE, power: 75, accuracy: 100, pp: 10, priority: 0, flags: [], // No contact flag effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const engine = new BattleEngine(ghostly, contactUser); // Test contact move immunity const initialHp = engine.getState().playerPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Contact move ); const hpAfterContact = engine.getState().playerPiclet.currentHp; expect(hpAfterContact).toBe(initialHp); // No damage from contact move expect(engine.getLog().some(msg => msg.includes('had no effect') || msg.includes('immune'))).toBe(true); // Test non-contact move still works engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 1 } // Non-contact move ); const hpAfterNonContact = engine.getState().playerPiclet.currentHp; expect(hpAfterNonContact).toBeLessThan(hpAfterContact); // Should take damage }); it('should provide immunity to sound moves with Sound Barrier', () => { const soundBarrier: SpecialAbility = { name: "Sound Barrier", description: "Natural sound dampening prevents sound-based moves", effects: [ { type: 'mechanicOverride', mechanic: 'flagImmunity', value: ['sound'] } ] }; const soundProof: PicletDefinition = { name: "Silent Fighter", description: "Cannot be affected by sound attacks", tier: 'medium', primaryType: PicletType.MACHINA, baseStats: { hp: 85, attack: 70, defense: 85, speed: 50 }, nature: "Bold", specialAbility: soundBarrier, movepool: [ { name: "Laser Beam", type: AttackType.MACHINA, power: 70, accuracy: 100, pp: 10, priority: 0, flags: [], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const soundUser: PicletDefinition = { name: "Sound Fighter", description: "Uses sound-based attacks", tier: 'medium', primaryType: PicletType.CULTURE, baseStats: { hp: 75, attack: 80, defense: 60, speed: 85 }, nature: "Modest", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Sonic Boom", type: AttackType.CULTURE, power: 80, accuracy: 100, pp: 10, priority: 0, flags: ['sound'], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const engine = new BattleEngine(soundProof, soundUser); 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; expect(finalHp).toBe(initialHp); expect(engine.getLog().some(msg => msg.includes('had no effect') || msg.includes('immune'))).toBe(true); }); it('should provide immunity to explosive moves with Soft Body', () => { const softBody: SpecialAbility = { name: "Soft Body", description: "Gelatinous form absorbs explosions but vulnerable to direct hits", effects: [ { type: 'mechanicOverride', mechanic: 'flagImmunity', value: ['explosive'] }, { type: 'mechanicOverride', mechanic: 'flagWeakness', value: ['punch'] } ] }; const gelatinous: PicletDefinition = { name: "Gel Fighter", description: "Soft gelatinous body", tier: 'medium', primaryType: PicletType.AQUATIC, baseStats: { hp: 90, attack: 60, defense: 80, speed: 60 }, nature: "Bold", specialAbility: softBody, movepool: [ { name: "Water Gun", type: AttackType.AQUATIC, power: 60, accuracy: 100, pp: 10, priority: 0, flags: [], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const explosiveUser: PicletDefinition = { name: "Bomber", description: "Uses explosive attacks", tier: 'medium', primaryType: PicletType.MACHINA, baseStats: { hp: 120, attack: 90, defense: 60, speed: 70 }, nature: "Hasty", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Explosion", type: AttackType.MACHINA, power: 120, accuracy: 100, pp: 5, priority: 0, flags: ['explosive'], effects: [ { type: 'damage', target: 'opponent', amount: 'strong' } ] }, { name: "Mega Punch", type: AttackType.BEAST, power: 80, accuracy: 85, pp: 10, priority: 0, flags: ['contact', 'punch'], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const engine = new BattleEngine(gelatinous, explosiveUser); const initialHp = engine.getState().playerPiclet.currentHp; // Test explosive immunity engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Explosive move ); const hpAfterExplosive = engine.getState().playerPiclet.currentHp; expect(hpAfterExplosive).toBe(initialHp); expect(engine.getLog().some(msg => msg.includes('had no effect') || msg.includes('absorbed'))).toBe(true); // Test punch weakness (should take extra damage) - only if battle hasn't ended if (!engine.isGameOver()) { engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 1 } // Punch move ); const hpAfterPunch = engine.getState().playerPiclet.currentHp; expect(hpAfterPunch).toBeLessThan(hpAfterExplosive); // Should take more damage than normal due to weakness } }); }); describe('Flag-Based Weaknesses', () => { it('should take extra damage from specific flagged moves', () => { const fragileShell: SpecialAbility = { name: "Fragile Shell", description: "Hard shell provides defense but shatters from explosions", effects: [ { type: 'modifyStats', target: 'self', stats: { defense: 'increase' } }, { type: 'mechanicOverride', mechanic: 'flagWeakness', value: ['explosive'] } ] }; const shelledCreature: PicletDefinition = { name: "Shell Fighter", description: "Protected by fragile shell", tier: 'medium', primaryType: PicletType.MINERAL, baseStats: { hp: 80, attack: 60, defense: 90, speed: 50 }, nature: "Impish", specialAbility: fragileShell, movepool: [ { name: "Rock Throw", type: AttackType.MINERAL, power: 50, accuracy: 90, pp: 10, priority: 0, flags: [], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const explosiveUser: PicletDefinition = { name: "Bomber", description: "Uses explosive attacks", tier: 'medium', primaryType: PicletType.MACHINA, baseStats: { hp: 70, attack: 80, defense: 60, speed: 70 }, nature: "Modest", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Normal Attack", type: AttackType.NORMAL, power: 60, accuracy: 100, pp: 10, priority: 0, flags: [], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] }, { name: "Bomb Blast", type: AttackType.MACHINA, power: 60, accuracy: 100, pp: 10, priority: 0, flags: ['explosive'], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const engine = new BattleEngine(shelledCreature, explosiveUser); // Test normal damage const initialHp = engine.getState().playerPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Normal attack ); const hpAfterNormal = engine.getState().playerPiclet.currentHp; const normalDamage = initialHp - hpAfterNormal; // Test explosive weakness (should do more damage) - only if battle hasn't ended if (!engine.isGameOver()) { const preExplosiveHp = engine.getState().playerPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 1 } // Explosive attack ); const hpAfterExplosive = engine.getState().playerPiclet.currentHp; const explosiveDamage = preExplosiveHp - hpAfterExplosive; // Explosive should do more damage due to weakness expect(explosiveDamage).toBeGreaterThan(normalDamage); expect(engine.getLog().some(msg => msg.includes('It\'s super effective') || msg.includes('weakness'))).toBe(true); } }); }); describe('Flag-Based Resistances', () => { it('should take reduced damage from specific flagged moves', () => { const thickHide: SpecialAbility = { name: "Thick Hide", description: "Tough skin reduces impact from physical contact", effects: [ { type: 'mechanicOverride', mechanic: 'flagResistance', value: ['contact'] } ] }; const toughCreature: PicletDefinition = { name: "Tough Fighter", description: "Has thick, resistant hide", tier: 'medium', primaryType: PicletType.BEAST, baseStats: { hp: 150, attack: 70, defense: 90, speed: 40 }, nature: "Impish", specialAbility: thickHide, movepool: [ { name: "Bite", type: AttackType.BEAST, power: 60, accuracy: 100, pp: 10, priority: 0, flags: ['contact', 'bite'], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const attacker: PicletDefinition = { name: "Mixed Attacker", description: "Uses various attack types", tier: 'medium', primaryType: PicletType.BEAST, baseStats: { hp: 120, attack: 85, defense: 60, speed: 70 }, nature: "Adamant", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Scratch", type: AttackType.BEAST, power: 60, accuracy: 100, pp: 10, priority: 0, flags: ['contact'], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] }, { name: "Energy Beam", type: AttackType.SPACE, power: 60, accuracy: 100, pp: 10, priority: 0, flags: [], // No contact effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const engine = new BattleEngine(toughCreature, attacker); // Test contact move (should be resisted) const initialHp = engine.getState().playerPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Contact move ); const hpAfterContact = engine.getState().playerPiclet.currentHp; const contactDamage = initialHp - hpAfterContact; // For now, just verify that resistance is working through the log message // The actual damage comparison requires battle to continue, which depends on HP balance expect(engine.getLog().some(msg => msg.includes('not very effective'))).toBe(true); // Verify that some damage was actually reduced (contact damage should be less than normal) // This is a basic sanity check - contact damage with resistance should be reasonable expect(contactDamage).toBeGreaterThan(0); expect(contactDamage).toBeLessThan(60); // Should be less than normal damage due to resistance }); }); describe('Multi-Flag Interactions', () => { it('should handle creatures with multiple flag interactions', () => { const liquidBody: SpecialAbility = { name: "Liquid Body", description: "Fluid form flows around physical attacks but resonates with sound", effects: [ { type: 'mechanicOverride', mechanic: 'flagImmunity', value: ['punch', 'bite'] }, { type: 'mechanicOverride', mechanic: 'flagWeakness', value: ['sound'] } ] }; const liquidCreature: PicletDefinition = { name: "Liquid Fighter", description: "Made of flowing liquid", tier: 'medium', primaryType: PicletType.AQUATIC, baseStats: { hp: 85, attack: 70, defense: 60, speed: 75 }, nature: "Calm", specialAbility: liquidBody, movepool: [ { name: "Water Pulse", type: AttackType.AQUATIC, power: 60, accuracy: 100, pp: 10, priority: 0, flags: [], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const multiAttacker: PicletDefinition = { name: "Multi Attacker", description: "Uses different types of attacks", tier: 'medium', primaryType: PicletType.BEAST, baseStats: { hp: 200, attack: 80, defense: 65, speed: 70 }, nature: "Hardy", specialAbility: { name: "No Ability", description: "" }, movepool: [ { name: "Punch", type: AttackType.BEAST, power: 70, accuracy: 100, pp: 10, priority: 0, flags: ['contact', 'punch'], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] }, { name: "Bite", type: AttackType.BEAST, power: 70, accuracy: 100, pp: 10, priority: 0, flags: ['contact', 'bite'], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] }, { name: "Sonic Roar", type: AttackType.CULTURE, power: 70, accuracy: 100, pp: 10, priority: 0, flags: ['sound'], effects: [ { type: 'damage', target: 'opponent', amount: 'normal' } ] } ] }; const engine = new BattleEngine(liquidCreature, multiAttacker); const initialHp = engine.getState().playerPiclet.currentHp; // Test punch immunity engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } // Punch (should be immune) ); const hpAfterPunch = engine.getState().playerPiclet.currentHp; expect(hpAfterPunch).toBe(initialHp); // Test bite immunity - only if battle hasn't ended let hpAfterBite = hpAfterPunch; if (!engine.isGameOver()) { engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 1 } // Bite (should be immune) ); hpAfterBite = engine.getState().playerPiclet.currentHp; expect(hpAfterBite).toBe(hpAfterPunch); } // Test sound weakness - only if battle hasn't ended if (!engine.isGameOver()) { engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 2 } // Sound (should be weak) ); const hpAfterSound = engine.getState().playerPiclet.currentHp; expect(hpAfterSound).toBeLessThan(hpAfterBite); } const log = engine.getLog(); expect(log.some(msg => msg.includes('had no effect'))).toBe(true); expect(log.some(msg => msg.includes('super effective') || msg.includes('weakness'))).toBe(true); }); }); });