fix(combat): show the potion heal immediately instead of folding it into the monster's reply

This commit is contained in:
Bastian Wagner
2026-08-20 23:54:02 +02:00
parent 1aac3416fa
commit 62d6677298
2 changed files with 60 additions and 1 deletions

View File

@@ -339,6 +339,51 @@ describe('CombatPageComponent', () => {
expect(monster?.classList.contains('sprite--lunge')).toBe(false); expect(monster?.classList.contains('sprite--lunge')).toBe(false);
}); });
it('shows the potion heal at the first checkpoint instead of waiting for the riposte reveal', async () => {
const fixture = await setup({
...activeCombat,
player: { ...activeCombat.player, currentHp: 70 },
});
const healed: Combat = {
...activeCombat,
round: 3,
player: { ...activeCombat.player, currentHp: 80, potionsRemaining: 1 },
events: [
...activeCombat.events,
{ round: 2, sequence: 3, type: 'HEAL', source: 'PLAYER', target: 'PLAYER', amount: 15 },
{ round: 2, sequence: 4, type: 'DAMAGE', source: 'MONSTER', target: 'PLAYER', amount: 5 },
],
};
combatStore.performAction.mockImplementation(async () => {
combatStore.combat.set(healed);
});
vi.useFakeTimers();
const element = fixture.nativeElement as HTMLElement;
element.querySelector<HTMLButtonElement>('[data-combat-potion]')?.click();
fixture.detectChanges();
// Before the checkpoint: the pre-heal HP and potion count still show.
expect(element.textContent).toContain('70 / 100');
expect(element.querySelector('[data-combat-potion]')?.textContent).toContain('Trank 2/2');
// First checkpoint: the heal already landed from the player's own action,
// so the HP bar and potion count update here -- well before the monster's
// held-back reply resolves.
await vi.advanceTimersByTimeAsync(540);
fixture.detectChanges();
expect(element.textContent).toContain('85 / 100');
expect(element.querySelector('[data-combat-potion]')?.textContent).toContain('Trank 1/2');
expect(element.textContent).toContain('Aric Duskwalker trinkt einen Trank und heilt 15 Lebenspunkte.');
// The monster's reply is still held back at this point.
expect(countOccurrences(element.textContent, monsterHitLine)).toBe(1);
await vi.advanceTimersByTimeAsync(1260);
fixture.detectChanges();
expect(element.textContent).toContain('80 / 100');
});
it('skips the recoil when the round ends without the monster striking back', async () => { it('skips the recoil when the round ends without the monster striking back', async () => {
const fixture = await setup(activeCombat); const fixture = await setup(activeCombat);
const won: Combat = { const won: Combat = {

View File

@@ -126,9 +126,23 @@ export class CombatPageComponent implements OnInit {
// Show what the player's own action produced, holding back the // Show what the player's own action produced, holding back the
// monster's reply -- including whether it just started telegraphing. // monster's reply -- including whether it just started telegraphing.
// A POTION heal lands from the player's own action, before the
// monster's reply, so it must show up here rather than being folded
// into the delayed riposte reveal.
const healEvent = roundEvents.find(
(event) => event.type === 'HEAL' && event.sequence < monsterEvent.sequence,
);
const intermediatePlayer = healEvent
? {
...before.player,
currentHp: Math.min(before.player.maxHp, before.player.currentHp + (healEvent.amount ?? 0)),
potionsRemaining: after.player.potionsRemaining,
}
: before.player;
this.displayed.set({ this.displayed.set({
...after, ...after,
player: before.player, player: intermediatePlayer,
monster: { monster: {
...(dealtDamage ? after.monster : before.monster), ...(dealtDamage ? after.monster : before.monster),
pendingIntent: before.monster.pendingIntent, pendingIntent: before.monster.pendingIntent,