|
|
|
|
|
|
|
|
|
|
|
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('Mechanic Override System - TDD Implementation', () => { |
|
describe('Critical Hit Mechanics', () => { |
|
it('should handle Shell Armor - cannot be critically hit', () => { |
|
const shellArmor: SpecialAbility = { |
|
name: "Shell Armor", |
|
description: "Hard shell prevents critical hits", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'criticalHits', |
|
condition: 'always', |
|
value: false |
|
} |
|
] |
|
}; |
|
|
|
const shellPiclet: PicletDefinition = { |
|
name: "Shell Defender", |
|
description: "Protected by a hard shell", |
|
tier: 'medium', |
|
primaryType: PicletType.MINERAL, |
|
baseStats: STANDARD_STATS, |
|
nature: "Bold", |
|
specialAbility: shellArmor, |
|
movepool: [{ |
|
name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35, |
|
priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
}] |
|
}; |
|
|
|
|
|
expect(shellArmor.effects![0].mechanic).toBe('criticalHits'); |
|
expect(shellArmor.effects![0].value).toBe(false); |
|
}); |
|
|
|
it('should handle Super Luck - always critical hits', () => { |
|
const superLuck: SpecialAbility = { |
|
name: "Super Luck", |
|
description: "Extremely lucky, always lands critical hits", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'criticalHits', |
|
condition: 'always', |
|
value: true |
|
} |
|
] |
|
}; |
|
|
|
expect(superLuck.effects![0].value).toBe(true); |
|
}); |
|
|
|
it('should handle Scope Lens - double critical hit rate', () => { |
|
const scopeLens: SpecialAbility = { |
|
name: "Scope Lens", |
|
description: "Enhanced precision doubles critical hit rate", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'criticalHits', |
|
condition: 'always', |
|
value: 'double' |
|
} |
|
] |
|
}; |
|
|
|
expect(scopeLens.effects![0].value).toBe('double'); |
|
}); |
|
}); |
|
|
|
describe('Status Immunity', () => { |
|
it('should handle Insomnia - sleep immunity', () => { |
|
const insomnia: SpecialAbility = { |
|
name: "Insomnia", |
|
description: "Prevents sleep status", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'statusImmunity', |
|
value: ['sleep'] |
|
} |
|
] |
|
}; |
|
|
|
const insomniaPiclet: PicletDefinition = { |
|
name: "Sleepless Guardian", |
|
description: "Never sleeps", |
|
tier: 'medium', |
|
primaryType: PicletType.CULTURE, |
|
baseStats: STANDARD_STATS, |
|
nature: "Alert", |
|
specialAbility: insomnia, |
|
movepool: [{ |
|
name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35, |
|
priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
}] |
|
}; |
|
|
|
expect(insomnia.effects![0].value).toContain('sleep'); |
|
}); |
|
|
|
it('should handle multi-status immunity', () => { |
|
const immunity: SpecialAbility = { |
|
name: "Pure Body", |
|
description: "Immune to poison and burn", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'statusImmunity', |
|
value: ['poison', 'burn'] |
|
} |
|
] |
|
}; |
|
|
|
expect(immunity.effects![0].value).toEqual(['poison', 'burn']); |
|
}); |
|
}); |
|
|
|
describe('Damage Reflection', () => { |
|
it('should handle Rough Skin - contact damage reflection', () => { |
|
const roughSkin: SpecialAbility = { |
|
name: "Rough Skin", |
|
description: "Rough skin damages attackers on contact", |
|
triggers: [ |
|
{ |
|
event: 'onContactDamage', |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'attacker', |
|
formula: 'fixed', |
|
value: 12 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const roughPiclet: PicletDefinition = { |
|
name: "Spike Beast", |
|
description: "Covered in rough spikes", |
|
tier: 'medium', |
|
primaryType: PicletType.MINERAL, |
|
baseStats: STANDARD_STATS, |
|
nature: "Hardy", |
|
specialAbility: roughSkin, |
|
movepool: [{ |
|
name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35, |
|
priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] |
|
}] |
|
}; |
|
|
|
expect(roughSkin.triggers![0].event).toBe('onContactDamage'); |
|
expect(roughSkin.triggers![0].effects[0].target).toBe('attacker'); |
|
}); |
|
|
|
it('should handle damage reflection percentage', () => { |
|
const reflectArmor: SpecialAbility = { |
|
name: "Mirror Armor", |
|
description: "Reflects 50% of damage back", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'damageReflection', |
|
value: 0.5 |
|
} |
|
] |
|
}; |
|
|
|
expect(reflectArmor.effects![0].value).toBe(0.5); |
|
}); |
|
}); |
|
|
|
describe('Type Mechanics', () => { |
|
it('should handle Wonder Guard - only super-effective moves hit', () => { |
|
const wonderGuard: SpecialAbility = { |
|
name: "Wonder Guard", |
|
description: "Only super-effective moves deal damage", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'damageCalculation', |
|
condition: 'ifNotSuperEffective', |
|
value: false |
|
} |
|
] |
|
}; |
|
|
|
expect(wonderGuard.effects![0].mechanic).toBe('damageCalculation'); |
|
expect(wonderGuard.effects![0].condition).toBe('ifNotSuperEffective'); |
|
}); |
|
|
|
it('should handle Levitate - ground type immunity', () => { |
|
const levitate: SpecialAbility = { |
|
name: "Levitate", |
|
description: "Floating ability makes ground moves miss", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'typeImmunity', |
|
value: ['ground'] |
|
} |
|
] |
|
}; |
|
|
|
expect(levitate.effects![0].value).toContain('ground'); |
|
}); |
|
|
|
it('should handle Protean - type changes to match move', () => { |
|
const protean: SpecialAbility = { |
|
name: "Protean", |
|
description: "Changes type to match the move being used", |
|
triggers: [ |
|
{ |
|
event: 'beforeMoveUse', |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'typeChange', |
|
value: 'matchMoveType' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(protean.triggers![0].event).toBe('beforeMoveUse'); |
|
expect(protean.triggers![0].effects[0].value).toBe('matchMoveType'); |
|
}); |
|
}); |
|
|
|
describe('Healing Mechanics', () => { |
|
it('should handle Poison Heal - poison heals instead of damages', () => { |
|
const poisonHeal: SpecialAbility = { |
|
name: "Poison Heal", |
|
description: "Poison heals instead of damages", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'healingInversion', |
|
value: 'invert' |
|
} |
|
] |
|
}; |
|
|
|
expect(poisonHeal.effects![0].mechanic).toBe('healingInversion'); |
|
expect(poisonHeal.effects![0].value).toBe('invert'); |
|
}); |
|
|
|
it('should handle healing blocked', () => { |
|
const healBlock: SpecialAbility = { |
|
name: "Cursed Body", |
|
description: "Cannot be healed by any means", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'healingBlocked', |
|
value: true |
|
} |
|
] |
|
}; |
|
|
|
expect(healBlock.effects![0].value).toBe(true); |
|
}); |
|
}); |
|
|
|
describe('Damage Absorption', () => { |
|
it('should handle Photosynthesis - absorbs flora moves', () => { |
|
const photosynthesis: SpecialAbility = { |
|
name: "Photosynthesis", |
|
description: "Absorbs flora-type moves to restore HP", |
|
triggers: [ |
|
{ |
|
event: 'onDamageTaken', |
|
condition: 'ifMoveType:flora', |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'damageAbsorption', |
|
value: 'absorb' |
|
}, |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
formula: 'percentage', |
|
value: 25 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(photosynthesis.triggers![0].condition).toBe('ifMoveType:flora'); |
|
expect(photosynthesis.triggers![0].effects[0].mechanic).toBe('damageAbsorption'); |
|
}); |
|
}); |
|
|
|
describe('Stat Modification Mechanics', () => { |
|
it('should handle Contrary - stat changes are reversed', () => { |
|
const contrary: SpecialAbility = { |
|
name: "Contrary", |
|
description: "Stat changes have the opposite effect", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'statModification', |
|
value: 'invert' |
|
} |
|
] |
|
}; |
|
|
|
expect(contrary.effects![0].value).toBe('invert'); |
|
}); |
|
}); |
|
|
|
describe('Flag-Based Immunities', () => { |
|
it('should handle Sky Dancer - immune to ground-flagged attacks', () => { |
|
const skyDancer: SpecialAbility = { |
|
name: "Sky Dancer", |
|
description: "Floating in air, immune to ground-based attacks", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'flagImmunity', |
|
value: ['ground'] |
|
} |
|
] |
|
}; |
|
|
|
expect(skyDancer.effects![0].value).toContain('ground'); |
|
}); |
|
|
|
it('should handle Sound Barrier - immune to sound attacks', () => { |
|
const soundBarrier: SpecialAbility = { |
|
name: "Sound Barrier", |
|
description: "Natural sound dampening prevents sound-based moves", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'flagImmunity', |
|
value: ['sound'] |
|
} |
|
] |
|
}; |
|
|
|
expect(soundBarrier.effects![0].value).toContain('sound'); |
|
}); |
|
|
|
it('should handle Soft Body - immune to explosive, weak to punch', () => { |
|
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'] |
|
} |
|
] |
|
}; |
|
|
|
expect(softBody.effects![0].value).toContain('explosive'); |
|
expect(softBody.effects![1].value).toContain('punch'); |
|
}); |
|
|
|
it('should handle flag resistance', () => { |
|
const thickHide: SpecialAbility = { |
|
name: "Thick Hide", |
|
description: "Tough skin reduces impact from physical contact", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'flagResistance', |
|
value: ['contact'] |
|
} |
|
] |
|
}; |
|
|
|
expect(thickHide.effects![0].value).toContain('contact'); |
|
}); |
|
}); |
|
|
|
describe('Priority Override', () => { |
|
it('should handle Prankster - status moves get priority', () => { |
|
const prankster: SpecialAbility = { |
|
name: "Prankster", |
|
description: "Status moves gain priority", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'priorityOverride', |
|
condition: 'ifStatusMove', |
|
value: 1 |
|
} |
|
] |
|
}; |
|
|
|
expect(prankster.effects![0].condition).toBe('ifStatusMove'); |
|
expect(prankster.effects![0].value).toBe(1); |
|
}); |
|
}); |
|
|
|
describe('Drain Inversion', () => { |
|
it('should handle Vampiric - drain moves damage the drainer', () => { |
|
const vampiric: SpecialAbility = { |
|
name: "Vampiric", |
|
description: "Cursed blood damages those who try to drain it", |
|
triggers: [ |
|
{ |
|
event: 'onHPDrained', |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'drainInversion', |
|
value: true |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'attacker', |
|
formula: 'fixed', |
|
value: 20 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(vampiric.triggers![0].event).toBe('onHPDrained'); |
|
expect(vampiric.triggers![0].effects[0].mechanic).toBe('drainInversion'); |
|
}); |
|
}); |
|
|
|
describe('Target Redirection', () => { |
|
it('should handle Magic Bounce - reflects status moves', () => { |
|
const magicBounce: SpecialAbility = { |
|
name: "Magic Bounce", |
|
description: "Reflects status moves back at the user", |
|
triggers: [ |
|
{ |
|
event: 'onStatusMoveTargeted', |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'targetRedirection', |
|
value: 'reflect' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(magicBounce.triggers![0].event).toBe('onStatusMoveTargeted'); |
|
expect(magicBounce.triggers![0].effects[0].value).toBe('reflect'); |
|
}); |
|
}); |
|
|
|
describe('Status Replacement', () => { |
|
it('should handle Frost Walker - freeze becomes attack boost', () => { |
|
const frostWalker: SpecialAbility = { |
|
name: "Frost Walker", |
|
description: "Instead of being frozen, gains +50% attack", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'statusReplacement', |
|
value: { |
|
status: 'freeze', |
|
replacement: { |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'greatly_increase' } |
|
} |
|
} |
|
} |
|
] |
|
}; |
|
|
|
expect(frostWalker.effects![0].mechanic).toBe('statusReplacement'); |
|
expect(frostWalker.effects![0].value.status).toBe('freeze'); |
|
}); |
|
}); |
|
|
|
describe('Damage Multiplier', () => { |
|
it('should handle damage multiplication abilities', () => { |
|
const damageBoost: SpecialAbility = { |
|
name: "Rage Mode", |
|
description: "All damage dealt is doubled when at low HP", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'damageMultiplier', |
|
condition: 'ifLowHp', |
|
value: 2.0 |
|
} |
|
] |
|
}; |
|
|
|
expect(damageBoost.effects![0].value).toBe(2.0); |
|
expect(damageBoost.effects![0].condition).toBe('ifLowHp'); |
|
}); |
|
}); |
|
|
|
describe('Extra Turn Mechanics', () => { |
|
it('should handle extra turn abilities', () => { |
|
const extraTurn: SpecialAbility = { |
|
name: "Time Distortion", |
|
description: "Gets an extra turn when switching in", |
|
triggers: [ |
|
{ |
|
event: 'onSwitchIn', |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'extraTurn', |
|
value: true, |
|
condition: 'nextTurn' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
expect(extraTurn.triggers![0].effects[0].mechanic).toBe('extraTurn'); |
|
expect(extraTurn.triggers![0].effects[0].condition).toBe('nextTurn'); |
|
}); |
|
}); |
|
}); |