feat(combat): implement HEAVY_STRIKE, SHIELD_BASH, DEFEND, POTION, and monster telegraphing
This commit is contained in:
@@ -101,7 +101,169 @@ describe('CombatEngineService', () => {
|
||||
|
||||
it('throws UnsupportedCombatActionError for an action it does not implement', () => {
|
||||
expect(() =>
|
||||
engine.resolveAction(baseState(), { action: 'HEAVY_STRIKE' as CombatAction }),
|
||||
engine.resolveAction(baseState(), { action: 'FLEE' as CombatAction }),
|
||||
).toThrow(UnsupportedCombatActionError);
|
||||
});
|
||||
|
||||
it('HEAVY_STRIKE deals 160% damage to the monster', () => {
|
||||
const result = engine.resolveAction(baseState(), { action: CombatAction.HEAVY_STRIKE });
|
||||
|
||||
// raw = 14; 14 * 1.6 = 22.4 -> rounds to 22
|
||||
expect(result.events[0]).toEqual({
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.MONSTER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 22,
|
||||
});
|
||||
expect(result.state.monster.currentHp).toBe(45 - 22);
|
||||
});
|
||||
|
||||
it('SHIELD_BASH deals 70% damage and does not emit INTERRUPT when nothing is pending', () => {
|
||||
const result = engine.resolveAction(baseState(), { action: CombatAction.SHIELD_BASH });
|
||||
|
||||
// raw = 14; 14 * 0.7 = 9.8 -> rounds to 10
|
||||
expect(result.events[0]).toEqual({
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.MONSTER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 10,
|
||||
});
|
||||
expect(result.events.some((event) => event.type === CombatEventType.INTERRUPT)).toBe(false);
|
||||
});
|
||||
|
||||
it('SHIELD_BASH interrupts a pending Heavy Attack and the monster does not act this round', () => {
|
||||
const state = baseState({
|
||||
monster: {
|
||||
currentHp: 45,
|
||||
maxHp: 45,
|
||||
stats: { attack: 5, armor: 0, pendingAction: 'HEAVY_ATTACK' },
|
||||
},
|
||||
});
|
||||
|
||||
const result = engine.resolveAction(state, { action: CombatAction.SHIELD_BASH });
|
||||
|
||||
expect(result.events).toEqual([
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.DAMAGE, amount: 10 },
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.INTERRUPT },
|
||||
]);
|
||||
expect(result.state.monster.stats.pendingAction).toBeUndefined();
|
||||
expect(result.state.player.currentHp).toBe(100);
|
||||
expect(result.state.round).toBe(2);
|
||||
});
|
||||
|
||||
it('DEFEND deals no damage and halves the monster normal attack this round', () => {
|
||||
const result = engine.resolveAction(baseState(), { action: CombatAction.DEFEND });
|
||||
|
||||
expect(result.state.monster.currentHp).toBe(45);
|
||||
// raw = 5; mitigated = 4.545...; * 0.5 = 2.27 -> rounds to 2
|
||||
expect(result.events).toEqual([
|
||||
{ source: Combatant.PLAYER, target: Combatant.PLAYER, type: CombatEventType.DEFEND },
|
||||
{ source: Combatant.MONSTER, target: Combatant.PLAYER, type: CombatEventType.DAMAGE, amount: 2 },
|
||||
]);
|
||||
expect(result.state.player.currentHp).toBe(98);
|
||||
});
|
||||
|
||||
it('POTION heals 35% of max HP, decrements potionsRemaining, and the monster still acts', () => {
|
||||
const state = baseState({
|
||||
player: {
|
||||
currentHp: 60,
|
||||
maxHp: 100,
|
||||
stats: { attack: 6, weaponDamage: 8, armor: 6, potionsRemaining: 2 },
|
||||
},
|
||||
});
|
||||
|
||||
const result = engine.resolveAction(state, { action: CombatAction.POTION });
|
||||
|
||||
// 35% of 100 = 35
|
||||
expect(result.events[0]).toEqual({
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.HEAL,
|
||||
amount: 35,
|
||||
});
|
||||
expect(result.events[1]).toEqual({
|
||||
source: Combatant.MONSTER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 5,
|
||||
});
|
||||
// 60 + 35 healed - 5 monster hit = 90
|
||||
expect(result.state.player.currentHp).toBe(90);
|
||||
expect(result.state.player.stats.potionsRemaining).toBe(1);
|
||||
});
|
||||
|
||||
it('caps POTION healing at the maximum HP', () => {
|
||||
const state = baseState({
|
||||
player: {
|
||||
currentHp: 90,
|
||||
maxHp: 100,
|
||||
stats: { attack: 6, weaponDamage: 8, armor: 6, potionsRemaining: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
const result = engine.resolveAction(state, { action: CombatAction.POTION });
|
||||
|
||||
// 35% of 100 = 35, but only 10 HP is missing
|
||||
expect(result.events[0]).toEqual({
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.HEAL,
|
||||
amount: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('telegraphs a Heavy Attack instead of attacking on the third round, and resolves it the round after', () => {
|
||||
const round3State = baseState({ round: 3 });
|
||||
|
||||
const telegraphResult = engine.resolveAction(round3State, { action: CombatAction.ATTACK });
|
||||
|
||||
expect(telegraphResult.state.player.currentHp).toBe(100);
|
||||
expect(telegraphResult.state.monster.stats.pendingAction).toBe('HEAVY_ATTACK');
|
||||
expect(telegraphResult.events[1]).toEqual({
|
||||
source: Combatant.MONSTER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.TELEGRAPH,
|
||||
});
|
||||
expect(telegraphResult.state.round).toBe(4);
|
||||
|
||||
const resolveResult = engine.resolveAction(telegraphResult.state, { action: CombatAction.ATTACK });
|
||||
|
||||
// raw = 5; mitigated = 4.545...; heavy * 1.6 = 7.27 -> rounds to 7
|
||||
expect(resolveResult.events[1]).toEqual({
|
||||
source: Combatant.MONSTER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 7,
|
||||
});
|
||||
expect(resolveResult.state.monster.stats.pendingAction).toBeUndefined();
|
||||
expect(resolveResult.state.player.currentHp).toBe(100 - 7);
|
||||
});
|
||||
|
||||
it('does not resolve a pending Heavy Attack when the monster is killed this round', () => {
|
||||
const state = baseState({
|
||||
monster: {
|
||||
currentHp: 10,
|
||||
maxHp: 45,
|
||||
stats: { attack: 5, armor: 0, pendingAction: 'HEAVY_ATTACK' },
|
||||
},
|
||||
});
|
||||
|
||||
const result = engine.resolveAction(state, { action: CombatAction.ATTACK });
|
||||
|
||||
expect(result.state.status).toBe(CombatStatus.WON);
|
||||
expect(result.state.player.currentHp).toBe(100);
|
||||
expect(result.events).toEqual([
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.DAMAGE, amount: 14 },
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.COMBAT_WON },
|
||||
]);
|
||||
});
|
||||
|
||||
it('is deterministic for HEAVY_STRIKE as well', () => {
|
||||
const state = baseState();
|
||||
|
||||
const first = engine.resolveAction(state, { action: CombatAction.HEAVY_STRIKE });
|
||||
const second = engine.resolveAction(state, { action: CombatAction.HEAVY_STRIKE });
|
||||
|
||||
expect(first).toEqual(second);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user