|
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: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(ghostly, contactUser); |
|
|
|
|
|
const initialHp = engine.getState().playerPiclet.currentHp; |
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const hpAfterContact = engine.getState().playerPiclet.currentHp; |
|
expect(hpAfterContact).toBe(initialHp); |
|
expect(engine.getLog().some(msg => msg.includes('had no effect') || msg.includes('immune'))).toBe(true); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 1 } |
|
); |
|
|
|
const hpAfterNonContact = engine.getState().playerPiclet.currentHp; |
|
expect(hpAfterNonContact).toBeLessThan(hpAfterContact); |
|
}); |
|
|
|
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; |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
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); |
|
|
|
|
|
if (!engine.isGameOver()) { |
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 1 } |
|
); |
|
|
|
const hpAfterPunch = engine.getState().playerPiclet.currentHp; |
|
expect(hpAfterPunch).toBeLessThan(hpAfterExplosive); |
|
|
|
} |
|
}); |
|
}); |
|
|
|
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); |
|
|
|
|
|
const initialHp = engine.getState().playerPiclet.currentHp; |
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const hpAfterNormal = engine.getState().playerPiclet.currentHp; |
|
const normalDamage = initialHp - hpAfterNormal; |
|
|
|
|
|
if (!engine.isGameOver()) { |
|
const preExplosiveHp = engine.getState().playerPiclet.currentHp; |
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 1 } |
|
); |
|
|
|
const hpAfterExplosive = engine.getState().playerPiclet.currentHp; |
|
const explosiveDamage = preExplosiveHp - hpAfterExplosive; |
|
|
|
|
|
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: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(toughCreature, attacker); |
|
|
|
|
|
const initialHp = engine.getState().playerPiclet.currentHp; |
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const hpAfterContact = engine.getState().playerPiclet.currentHp; |
|
const contactDamage = initialHp - hpAfterContact; |
|
|
|
|
|
|
|
expect(engine.getLog().some(msg => msg.includes('not very effective'))).toBe(true); |
|
|
|
|
|
|
|
expect(contactDamage).toBeGreaterThan(0); |
|
expect(contactDamage).toBeLessThan(60); |
|
}); |
|
}); |
|
|
|
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; |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const hpAfterPunch = engine.getState().playerPiclet.currentHp; |
|
expect(hpAfterPunch).toBe(initialHp); |
|
|
|
|
|
let hpAfterBite = hpAfterPunch; |
|
if (!engine.isGameOver()) { |
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 1 } |
|
); |
|
|
|
hpAfterBite = engine.getState().playerPiclet.currentHp; |
|
expect(hpAfterBite).toBe(hpAfterPunch); |
|
} |
|
|
|
|
|
if (!engine.isGameOver()) { |
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 2 } |
|
); |
|
|
|
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); |
|
}); |
|
}); |
|
}); |