This commit is contained in:
Bastian Wagner
2026-08-19 23:42:18 +02:00
parent fd9852bfbf
commit 67237f5ad8
4 changed files with 175 additions and 5 deletions

View File

@@ -208,7 +208,7 @@
background-size: 600% 100%;
}
.sprite--hit {
.sprite--player.sprite--hit {
background-image: url('/images/combat/sprites/warrior-hit-sheet-384.png');
}
@@ -226,6 +226,126 @@
.sprite--monster {
justify-self: end;
block-size: calc(var(--sprite-scale, 0.6) * 100%);
// Every beat below scales and rotates the cut-out. Pivoting at the feet keeps
// it standing on the same spot instead of sinking behind the action bar.
transform-origin: bottom center;
}
/* The monster is a single cut-out, so every beat below is transform-only. All
of them animate translate/rotate/scale rather than `transform`, and any step
that touches `filter` has to repeat the base drop-shadow from `.sprite`,
because a keyframe filter replaces the whole list. */
/* Getting hit: a short recoil to the right, away from the player, with a red
glow pulse. 240ms so it finishes inside the 260ms riposte pause. */
@keyframes monster-flinch {
0% {
translate: 0 0;
rotate: 0deg;
filter: drop-shadow(0 0.5rem 0.9rem rgb(0 0 0 / 0.7)) drop-shadow(0 0 0 rgb(199 75 66 / 0));
}
20% {
translate: 0.45rem -0.12rem;
rotate: 1.1deg;
filter: drop-shadow(0 0.5rem 0.9rem rgb(0 0 0 / 0.7))
drop-shadow(0 0 0.65rem rgb(199 75 66 / 0.6));
}
45% {
translate: -0.28rem 0;
rotate: -1deg;
}
70% {
translate: 0.18rem 0;
rotate: 0.5deg;
}
100% {
translate: 0 0;
rotate: 0deg;
filter: drop-shadow(0 0.5rem 0.9rem rgb(0 0 0 / 0.7)) drop-shadow(0 0 0 rgb(199 75 66 / 0));
}
}
/* Striking back: a brief wind-up away from the player, then a lunge left. The
impact sits at 28% of 540ms so it reads against the warrior hit sheet. */
@keyframes monster-lunge {
0% {
translate: 0 0;
rotate: 0deg;
scale: 1;
}
12% {
translate: 0.55rem 0;
rotate: 1.4deg;
scale: 0.985;
}
28% {
translate: -2rem -0.45rem;
rotate: -2.4deg;
scale: 1.05;
}
55% {
translate: -0.8rem -0.1rem;
rotate: -0.8deg;
scale: 1.015;
}
100% {
translate: 0 0;
rotate: 0deg;
scale: 1;
}
}
/* Idle: deliberately tiny, just enough that the cut-out does not read as card-
board. The spec rules out permanent *strong* animation, not a slow breath. */
@keyframes monster-breathe {
from {
translate: 0 0;
scale: 1;
}
to {
translate: 0 -0.4%;
scale: 1.004;
}
}
/* Stage jolt when the player takes the blow. The slight scale gives the offset
somewhere to go: the stage clips its children, but its own border moves too,
so without it the shift would open a sliver at the frame. */
@keyframes stage-shake {
0%,
100% {
translate: 0 0;
scale: 1;
}
15% {
translate: -0.35% 0.08rem;
scale: 1.012;
}
35% {
translate: 0.3% -0.06rem;
scale: 1.012;
}
60% {
translate: -0.18% 0.04rem;
scale: 1.009;
}
80% {
translate: 0.1% 0;
scale: 1.004;
}
}
/* ---------- action bar ---------- */
@@ -439,11 +559,29 @@
}
@media (prefers-reduced-motion: no-preference) {
.sprite--attacking,
.sprite--hit {
.sprite--player.sprite--attacking,
.sprite--player.sprite--hit {
animation: warrior-frames 540ms steps(6) 1;
}
/* Order matters: the state classes below share translate/rotate/scale with
the breathing loop, so their `animation` shorthand has to come last. */
.sprite--monster {
animation: monster-breathe 5.5s ease-in-out infinite alternate;
}
.sprite--flinch {
animation: monster-flinch 240ms ease-out 1;
}
.sprite--lunge {
animation: monster-lunge 540ms cubic-bezier(0.22, 0.9, 0.3, 1) 1;
}
.combat__stage--shaken {
animation: stage-shake 200ms ease-out 1;
}
.bar__fill {
transition: inline-size var(--ar-motion-base);
}