From 62d66772984234835b08eb73c6d83556fc7cb776 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Thu, 20 Aug 2026 23:54:02 +0200 Subject: [PATCH] fix(combat): show the potion heal immediately instead of folding it into the monster's reply --- .../combat-page/combat-page.component.spec.ts | 45 +++++++++++++++++++ .../combat-page/combat-page.component.ts | 16 ++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts b/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts index b2ebffc..880c1ad 100644 --- a/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts +++ b/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts @@ -339,6 +339,51 @@ describe('CombatPageComponent', () => { 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('[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 () => { const fixture = await setup(activeCombat); const won: Combat = { diff --git a/apps/web/src/app/features/combat/combat-page/combat-page.component.ts b/apps/web/src/app/features/combat/combat-page/combat-page.component.ts index 8869c33..e4cea29 100644 --- a/apps/web/src/app/features/combat/combat-page/combat-page.component.ts +++ b/apps/web/src/app/features/combat/combat-page/combat-page.component.ts @@ -126,9 +126,23 @@ export class CombatPageComponent implements OnInit { // Show what the player's own action produced, holding back the // 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({ ...after, - player: before.player, + player: intermediatePlayer, monster: { ...(dealtDamage ? after.monster : before.monster), pendingIntent: before.monster.pendingIntent,