feat(combat): play the round a beat at a time with attack and hit frames

The server resolves a whole round in one call, so both blows used to land
at the same instant. The page now keeps its own view of the combat and
plays the round back: the swing animates, the monster's HP and log line
land, then after a beat the monster strikes and the player recoils.

Both animations are six-frame sprite sheets driven by steps(6), which is
why the phase durations mirror the stylesheet.

The status row reflows on the stage's own width via a container query —
the side rails can squeeze it narrow while the viewport is still wide,
which previously overlapped the round marker with the player's name. The
component-style budget moves to 12kB to fit this screen's stylesheet.
This commit is contained in:
Bastian Wagner
2026-08-19 22:46:17 +02:00
parent 54f9d59ef4
commit 6c4b7d29d0
5 changed files with 265 additions and 52 deletions

View File

@@ -1,5 +1,5 @@
<section class="combat" aria-label="Kampf">
@if (combatStore.combat(); as combat) {
@if (combat(); as combat) {
<div class="combat__stage">
<header class="combat__status">
<div class="fighter fighter--player">
@@ -49,9 +49,16 @@
</header>
<div class="combat__field">
<img class="sprite sprite--player" [src]="playerSprite" [alt]="combat.player.name" />
<div
class="sprite sprite--player"
[class.sprite--attacking]="phase() === 'attacking'"
[class.sprite--hit]="phase() === 'hit'"
role="img"
[attr.aria-label]="combat.player.name"
></div>
<img
class="sprite sprite--monster"
[style.--sprite-scale]="monsterSpriteScale(combat.monster.key)"
[src]="monsterSprite(combat.monster.key, combat.monster.artworkPath)"
[alt]="combat.monster.name"
/>
@@ -63,7 +70,7 @@
type="button"
class="action"
data-combat-attack
[disabled]="combatStore.actionPending()"
[disabled]="busy()"
(click)="attack()"
>
<img class="action__icon" src="/images/hud/runtime/AttackIcon-96.png" alt="" />