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:
@@ -12,9 +12,12 @@
|
||||
|
||||
/* ---------- stage ---------- */
|
||||
|
||||
// The stage is a container so the status row reflows on its own width: the
|
||||
// surrounding rails can squeeze it narrow while the viewport is still wide.
|
||||
.combat__stage {
|
||||
position: relative;
|
||||
display: grid;
|
||||
container-type: inline-size;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
gap: var(--ar-space-4);
|
||||
min-block-size: 26rem;
|
||||
@@ -126,7 +129,7 @@
|
||||
block-size: 1.25rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--ar-border);
|
||||
background: linear-gradient(180deg, rgb(0 0 0 / 0.85), rgb(0 0 0 / 0.6));
|
||||
background: rgb(0 0 0 / 0.72);
|
||||
box-shadow: inset 0 0 0.6rem rgb(0 0 0 / 0.9);
|
||||
}
|
||||
|
||||
@@ -186,21 +189,43 @@
|
||||
|
||||
.sprite {
|
||||
display: block;
|
||||
max-block-size: 100%;
|
||||
max-inline-size: 100%;
|
||||
inline-size: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 1rem 1.5rem rgb(0 0 0 / 0.75));
|
||||
object-position: bottom;
|
||||
filter: drop-shadow(0 0.5rem 0.9rem rgb(0 0 0 / 0.7));
|
||||
}
|
||||
|
||||
/* Six 384px frames laid out horizontally; frame 0 is the resting stance. */
|
||||
.sprite--player {
|
||||
justify-self: start;
|
||||
block-size: clamp(11rem, 30vh, 19rem);
|
||||
block-size: 80%;
|
||||
aspect-ratio: 1;
|
||||
transform: scaleX(-1);
|
||||
background-image: url('/images/combat/sprites/warrior-attack-sheet-384.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0% 0;
|
||||
background-size: 600% 100%;
|
||||
}
|
||||
|
||||
.sprite--hit {
|
||||
background-image: url('/images/combat/sprites/warrior-hit-sheet-384.png');
|
||||
}
|
||||
|
||||
/* 6 frames across a 600%-wide sheet land on 0/20/40/60/80/100%. */
|
||||
@keyframes warrior-frames {
|
||||
from {
|
||||
background-position: 0% 0;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 120% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sprite--monster {
|
||||
justify-self: end;
|
||||
block-size: clamp(9rem, 24vh, 15rem);
|
||||
block-size: calc(var(--sprite-scale, 0.6) * 100%);
|
||||
}
|
||||
|
||||
/* ---------- action bar ---------- */
|
||||
@@ -224,21 +249,23 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action__icon {
|
||||
.action__icon,
|
||||
.action__label,
|
||||
.action__key {
|
||||
position: absolute;
|
||||
inset-block-start: 32%;
|
||||
inset-inline-start: 50%;
|
||||
inline-size: 34%;
|
||||
translate: -50% -50%;
|
||||
}
|
||||
|
||||
.action__icon {
|
||||
inset-block-start: 32%;
|
||||
inline-size: 34%;
|
||||
border-radius: 50%;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.action__label {
|
||||
position: absolute;
|
||||
inset-block-start: 62%;
|
||||
inset-inline-start: 50%;
|
||||
translate: -50% -50%;
|
||||
color: var(--ar-text);
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: clamp(0.85rem, 1.2vw, 1rem);
|
||||
@@ -247,10 +274,7 @@
|
||||
}
|
||||
|
||||
.action__key {
|
||||
position: absolute;
|
||||
inset-block-start: 90%;
|
||||
inset-inline-start: 50%;
|
||||
translate: -50% -50%;
|
||||
color: var(--ar-text-muted);
|
||||
font-size: var(--ar-font-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
@@ -317,8 +341,8 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.outcome__button {
|
||||
margin-block-start: var(--ar-space-2);
|
||||
.outcome__button,
|
||||
.combat__notice--error button {
|
||||
padding: var(--ar-space-2) var(--ar-space-5);
|
||||
border: 1px solid var(--ar-border-highlight);
|
||||
border-radius: var(--ar-radius-sm);
|
||||
@@ -329,7 +353,12 @@
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.outcome__button:hover {
|
||||
.outcome__button {
|
||||
margin-block-start: var(--ar-space-2);
|
||||
}
|
||||
|
||||
.outcome__button:hover,
|
||||
.combat__notice--error button:hover {
|
||||
border-color: var(--ar-gold);
|
||||
color: var(--ar-gold);
|
||||
}
|
||||
@@ -407,15 +436,14 @@
|
||||
|
||||
.combat__notice--error button {
|
||||
flex: 0 0 auto;
|
||||
padding: var(--ar-space-2) var(--ar-space-3);
|
||||
border: 1px solid var(--ar-border-highlight);
|
||||
border-radius: var(--ar-radius-sm);
|
||||
color: var(--ar-text);
|
||||
background: #1a2023;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.sprite--attacking,
|
||||
.sprite--hit {
|
||||
animation: warrior-frames 540ms steps(6) 1;
|
||||
}
|
||||
|
||||
.bar__fill {
|
||||
transition: inline-size var(--ar-motion-base);
|
||||
}
|
||||
@@ -444,7 +472,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 40rem) {
|
||||
// Below this the three-column status row cannot hold two names and two bars
|
||||
// side by side, so the fighters stack under a centred round marker.
|
||||
@container (width < 38rem) {
|
||||
.combat__status {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--ar-space-2);
|
||||
@@ -469,11 +499,7 @@
|
||||
inset-inline-end: auto;
|
||||
}
|
||||
|
||||
.sprite--player {
|
||||
block-size: clamp(8rem, 22vh, 12rem);
|
||||
}
|
||||
|
||||
.sprite--monster {
|
||||
block-size: clamp(7rem, 18vh, 10rem);
|
||||
.fighter__meter {
|
||||
max-inline-size: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user