|
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect } from 'vitest'; |
|
import { BattleEngine } from './BattleEngine'; |
|
import { PicletDefinition } from './types'; |
|
import { PicletType, AttackType } from './types'; |
|
|
|
describe('Complete Tempest Wraith Implementation', () => { |
|
it('should handle the complete Tempest Wraith from design document', () => { |
|
const tempestWraith: PicletDefinition = { |
|
name: "Tempest Wraith", |
|
description: "A ghostly creature born from violent storms, wielding cosmic energy and shadowy illusions", |
|
tier: 'high', |
|
primaryType: PicletType.SPACE, |
|
secondaryType: PicletType.CULTURE, |
|
baseStats: { |
|
hp: 75, |
|
attack: 95, |
|
defense: 45, |
|
speed: 85 |
|
}, |
|
nature: "timid", |
|
specialAbility: { |
|
name: "Storm Caller", |
|
description: "When HP drops below 25%, gains immunity to status effects and +50% speed", |
|
triggers: [ |
|
{ |
|
event: 'onLowHP', |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'statusImmunity', |
|
value: ['burn', 'freeze', 'paralyze', 'poison', 'sleep', 'confuse'] |
|
}, |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { speed: 'greatly_increase' } |
|
} |
|
] |
|
}, |
|
{ |
|
event: 'onSwitchIn', |
|
condition: 'ifWeather:storm', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'increase' } |
|
} |
|
] |
|
} |
|
] |
|
}, |
|
movepool: [ |
|
{ |
|
name: "Shadow Pulse", |
|
type: AttackType.CULTURE, |
|
power: 70, |
|
accuracy: 100, |
|
pp: 15, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
}, |
|
{ |
|
type: 'applyStatus', |
|
target: 'opponent', |
|
status: 'confuse' |
|
} |
|
] |
|
}, |
|
{ |
|
name: "Cosmic Strike", |
|
type: AttackType.SPACE, |
|
power: 85, |
|
accuracy: 90, |
|
pp: 10, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
}, |
|
{ |
|
type: 'applyStatus', |
|
target: 'opponent', |
|
status: 'paralyze' |
|
} |
|
] |
|
}, |
|
{ |
|
name: "Spectral Drain", |
|
type: AttackType.CULTURE, |
|
power: 60, |
|
accuracy: 95, |
|
pp: 12, |
|
priority: 0, |
|
flags: ['draining'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
formula: 'drain', |
|
value: 0.5 |
|
} |
|
] |
|
}, |
|
{ |
|
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 |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const opponent: PicletDefinition = { |
|
name: "Standard Fighter", |
|
description: "A basic opponent", |
|
tier: 'medium', |
|
primaryType: PicletType.BEAST, |
|
baseStats: { hp: 100, attack: 80, defense: 70, speed: 60 }, |
|
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(tempestWraith, opponent); |
|
|
|
|
|
const state = engine.getState(); |
|
expect(state.playerPiclet.definition.name).toBe("Tempest Wraith"); |
|
expect(state.playerPiclet.definition.primaryType).toBe(PicletType.SPACE); |
|
expect(state.playerPiclet.definition.secondaryType).toBe(PicletType.CULTURE); |
|
expect(state.playerPiclet.moves).toHaveLength(4); |
|
|
|
|
|
const ability = tempestWraith.specialAbility; |
|
expect(ability.name).toBe("Storm Caller"); |
|
expect(ability.triggers).toHaveLength(2); |
|
expect(ability.triggers![0].event).toBe('onLowHP'); |
|
expect(ability.triggers![1].event).toBe('onSwitchIn'); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 0 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
let log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('used Shadow Pulse'))).toBe(true); |
|
|
|
|
|
if (!engine.isGameOver()) { |
|
|
|
engine['state'].playerPiclet.currentHp = 30; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 2 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('recovered') && msg.includes('HP from draining'))).toBe(true); |
|
} |
|
|
|
|
|
if (!engine.isGameOver()) { |
|
const preVoidHp = engine.getState().playerPiclet.currentHp; |
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 3 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
log = engine.getLog(); |
|
expect(log.some(msg => msg.includes('used Void Sacrifice'))).toBe(true); |
|
|
|
const hasFieldEffect = log.some(msg => msg.includes('applied') || msg.includes('effect')); |
|
expect(hasFieldEffect).toBe(true); |
|
|
|
|
|
const postVoidHp = engine.getState().playerPiclet.currentHp; |
|
expect(postVoidHp).toBeLessThan(preVoidHp); |
|
} |
|
}); |
|
|
|
it('should demonstrate strategic depth with different movesets', () => { |
|
const tempestWraith: PicletDefinition = { |
|
name: "Tempest Wraith", |
|
description: "A ghostly creature born from violent storms", |
|
tier: 'high', |
|
primaryType: PicletType.SPACE, |
|
secondaryType: PicletType.CULTURE, |
|
baseStats: { hp: 75, attack: 95, defense: 45, speed: 85 }, |
|
nature: "timid", |
|
specialAbility: { |
|
name: "Storm Caller", |
|
description: "Complex multi-trigger ability", |
|
triggers: [ |
|
{ |
|
event: 'onLowHP', |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'statusImmunity', |
|
value: ['burn', 'freeze', 'paralyze', 'poison', 'sleep', 'confuse'] |
|
} |
|
] |
|
} |
|
] |
|
}, |
|
movepool: [ |
|
{ |
|
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 |
|
} |
|
] |
|
}, |
|
{ |
|
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' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(tempestWraith, { |
|
name: "Opponent", description: "Test opponent", tier: 'medium', |
|
primaryType: PicletType.BEAST, baseStats: { hp: 100, attack: 80, defense: 70, speed: 60 }, |
|
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 state = engine.getState(); |
|
expect(state.playerPiclet.moves[0].move.name).toBe("Berserker's End"); |
|
expect(state.playerPiclet.moves[1].move.name).toBe("Cursed Gambit"); |
|
|
|
|
|
const berserkersEnd = state.playerPiclet.moves[0].move; |
|
expect(berserkersEnd.effects).toHaveLength(3); |
|
expect(berserkersEnd.effects[1].condition).toBe('ifLowHp'); |
|
|
|
|
|
const cursedGambit = state.playerPiclet.moves[1].move; |
|
expect(cursedGambit.effects).toHaveLength(2); |
|
expect(cursedGambit.effects[0].condition).toBe('ifLucky50'); |
|
expect(cursedGambit.effects[1].condition).toBe('ifUnlucky50'); |
|
}); |
|
|
|
it('should handle complete battle with advanced mechanics', () => { |
|
const advancedPiclet: PicletDefinition = { |
|
name: "Master of All Trades", |
|
description: "Demonstrates every major battle system feature", |
|
tier: 'legendary', |
|
primaryType: PicletType.SPACE, |
|
secondaryType: PicletType.CULTURE, |
|
baseStats: { hp: 100, attack: 100, defense: 80, speed: 90 }, |
|
nature: "Adaptive", |
|
specialAbility: { |
|
name: "Omni-Adaptation", |
|
description: "Multiple triggers for different situations", |
|
effects: [ |
|
{ |
|
type: 'mechanicOverride', |
|
mechanic: 'criticalHits', |
|
value: 'double' |
|
} |
|
], |
|
triggers: [ |
|
{ |
|
event: 'onDamageTaken', |
|
effects: [ |
|
{ |
|
type: 'modifyStats', |
|
target: 'self', |
|
stats: { attack: 'increase' } |
|
} |
|
] |
|
}, |
|
{ |
|
event: 'onSwitchIn', |
|
effects: [ |
|
{ |
|
type: 'removeStatus', |
|
target: 'self', |
|
status: 'poison' |
|
} |
|
] |
|
}, |
|
{ |
|
event: 'endOfTurn', |
|
condition: 'ifStatus:burn', |
|
effects: [ |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
formula: 'percentage', |
|
value: 10 |
|
} |
|
] |
|
} |
|
] |
|
}, |
|
movepool: [ |
|
{ |
|
name: "Adaptive Strike", |
|
type: AttackType.NORMAL, |
|
power: 70, |
|
accuracy: 100, |
|
pp: 20, |
|
priority: 0, |
|
flags: ['contact'], |
|
effects: [ |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'normal' |
|
}, |
|
{ |
|
type: 'damage', |
|
target: 'opponent', |
|
amount: 'strong', |
|
condition: 'ifStatus:burn' |
|
} |
|
] |
|
}, |
|
{ |
|
name: "Field Manipulator", |
|
type: AttackType.SPACE, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: 1, |
|
flags: ['priority'], |
|
effects: [ |
|
{ |
|
type: 'fieldEffect', |
|
effect: 'gravityField', |
|
target: 'field', |
|
stackable: false |
|
}, |
|
{ |
|
type: 'modifyStats', |
|
target: 'opponent', |
|
stats: { speed: 'decrease' } |
|
} |
|
] |
|
}, |
|
{ |
|
name: "Status Cleanse", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 15, |
|
priority: 0, |
|
flags: [], |
|
effects: [ |
|
{ |
|
type: 'removeStatus', |
|
target: 'self', |
|
status: 'poison' |
|
}, |
|
{ |
|
type: 'removeStatus', |
|
target: 'self', |
|
status: 'burn' |
|
}, |
|
{ |
|
type: 'heal', |
|
target: 'self', |
|
amount: 'medium' |
|
} |
|
] |
|
}, |
|
{ |
|
name: "Counter Protocol", |
|
type: AttackType.NORMAL, |
|
power: 0, |
|
accuracy: 100, |
|
pp: 10, |
|
priority: -5, |
|
flags: ['lowPriority'], |
|
effects: [ |
|
{ |
|
type: 'counter', |
|
strength: 'strong' |
|
} |
|
] |
|
} |
|
] |
|
}; |
|
|
|
const engine = new BattleEngine(advancedPiclet, { |
|
name: "Test Opponent", description: "Basic opponent", tier: 'medium', |
|
primaryType: PicletType.BEAST, baseStats: { hp: 80, attack: 70, defense: 60, speed: 50 }, |
|
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 ability = advancedPiclet.specialAbility; |
|
expect(ability.effects).toHaveLength(1); |
|
expect(ability.triggers).toHaveLength(3); |
|
expect(ability.triggers![0].event).toBe('onDamageTaken'); |
|
expect(ability.triggers![2].condition).toBe('ifStatus:burn'); |
|
|
|
|
|
const moves = advancedPiclet.movepool; |
|
expect(moves).toHaveLength(4); |
|
expect(moves[1].priority).toBe(1); |
|
expect(moves[3].priority).toBe(-5); |
|
expect(moves[2].effects).toHaveLength(3); |
|
|
|
|
|
engine.executeActions( |
|
{ type: 'move', piclet: 'player', moveIndex: 1 }, |
|
{ type: 'move', piclet: 'opponent', moveIndex: 0 } |
|
); |
|
|
|
const log = engine.getLog(); |
|
const hasFieldEffect = log.some(msg => msg.includes('applied') || msg.includes('effect')); |
|
expect(hasFieldEffect).toBe(true); |
|
expect(log.some(msg => msg.includes('speed fell'))).toBe(true); |
|
}); |
|
}); |