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.
506 lines
10 KiB
SCSS
506 lines
10 KiB
SCSS
:host {
|
|
display: block;
|
|
block-size: 100%;
|
|
}
|
|
|
|
.combat {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) clamp(13rem, 17vw, 17rem);
|
|
gap: var(--ar-space-4);
|
|
block-size: 100%;
|
|
}
|
|
|
|
/* ---------- 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;
|
|
padding: var(--ar-space-4);
|
|
overflow: hidden;
|
|
border: 1px solid var(--ar-border);
|
|
background-color: var(--ar-bg);
|
|
background-image: url('/images/backgrounds/Aschestrasse.png');
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-size: cover;
|
|
box-shadow: var(--ar-shadow-raised);
|
|
}
|
|
|
|
@supports (
|
|
background-image: image-set(
|
|
url('/images/backgrounds/runtime/Aschestrasse-960.jpg') type('image/jpeg') 1x
|
|
)
|
|
) {
|
|
.combat__stage {
|
|
background-image: image-set(
|
|
url('/images/backgrounds/runtime/Aschestrasse-960.jpg') type('image/jpeg') 1x
|
|
);
|
|
}
|
|
}
|
|
|
|
.combat__stage::before {
|
|
position: absolute;
|
|
z-index: 0;
|
|
inset: 0;
|
|
content: '';
|
|
background:
|
|
linear-gradient(180deg, rgb(4 6 8 / 0.72) 0%, rgb(4 6 8 / 0.1) 26%, rgb(4 6 8 / 0.66) 100%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ---------- combatant status ---------- */
|
|
|
|
.combat__status {
|
|
position: relative;
|
|
z-index: 2;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
|
align-items: start;
|
|
gap: var(--ar-space-4);
|
|
}
|
|
|
|
.fighter {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--ar-space-3);
|
|
min-inline-size: 0;
|
|
}
|
|
|
|
.fighter--monster {
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.fighter__icon {
|
|
flex: 0 0 auto;
|
|
inline-size: clamp(2.75rem, 5vw, 3.75rem);
|
|
block-size: clamp(2.75rem, 5vw, 3.75rem);
|
|
object-fit: cover;
|
|
border-radius: 50%;
|
|
box-shadow:
|
|
0 0 0 1px var(--ar-border-highlight),
|
|
0 0.3rem 0.9rem rgb(0 0 0 / 0.8);
|
|
}
|
|
|
|
.fighter__meter {
|
|
display: grid;
|
|
gap: 0.3rem;
|
|
min-inline-size: 0;
|
|
flex: 1 1 auto;
|
|
max-inline-size: 22rem;
|
|
}
|
|
|
|
.fighter--monster .fighter__meter {
|
|
justify-items: end;
|
|
text-align: end;
|
|
}
|
|
|
|
.fighter__name {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
color: var(--ar-text);
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|
font-size: clamp(0.95rem, 1.5vw, 1.15rem);
|
|
letter-spacing: 0.02em;
|
|
text-overflow: ellipsis;
|
|
text-shadow: 0 0.1rem 0.5rem rgb(0 0 0 / 0.95);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.fighter__level {
|
|
color: var(--ar-gold);
|
|
font-family: system-ui, sans-serif;
|
|
font-size: var(--ar-font-sm);
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* ---------- hp bars ---------- */
|
|
|
|
.bar {
|
|
position: relative;
|
|
display: block;
|
|
inline-size: 100%;
|
|
block-size: 1.25rem;
|
|
overflow: hidden;
|
|
border: 1px solid var(--ar-border);
|
|
background: rgb(0 0 0 / 0.72);
|
|
box-shadow: inset 0 0 0.6rem rgb(0 0 0 / 0.9);
|
|
}
|
|
|
|
.bar__fill {
|
|
position: absolute;
|
|
z-index: 0;
|
|
inset-block: 0;
|
|
inset-inline-start: 0;
|
|
display: block;
|
|
background: linear-gradient(180deg, #d0564a, #8e2b23);
|
|
}
|
|
|
|
.bar--monster .bar__fill {
|
|
inset-inline-start: auto;
|
|
inset-inline-end: 0;
|
|
}
|
|
|
|
.bar__text {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: grid;
|
|
block-size: 100%;
|
|
place-items: center;
|
|
color: var(--ar-text);
|
|
font-size: var(--ar-font-sm);
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: 0.04em;
|
|
text-shadow: 0 0.05rem 0.25rem rgb(0 0 0 / 1);
|
|
}
|
|
|
|
/* ---------- round marker ---------- */
|
|
|
|
.combat__round {
|
|
margin: 0;
|
|
align-self: center;
|
|
padding: var(--ar-space-1) var(--ar-space-5);
|
|
border-block: 1px solid var(--ar-border-highlight);
|
|
color: var(--ar-gold);
|
|
background: linear-gradient(90deg, transparent, rgb(9 11 13 / 0.9) 18%, rgb(9 11 13 / 0.9) 82%, transparent);
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|
font-size: clamp(0.95rem, 1.4vw, 1.1rem);
|
|
letter-spacing: 0.14em;
|
|
text-transform: uppercase;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ---------- battlefield ---------- */
|
|
|
|
.combat__field {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
align-items: end;
|
|
min-block-size: 0;
|
|
}
|
|
|
|
.sprite {
|
|
display: block;
|
|
max-inline-size: 100%;
|
|
inline-size: auto;
|
|
object-fit: contain;
|
|
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: 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: calc(var(--sprite-scale, 0.6) * 100%);
|
|
}
|
|
|
|
/* ---------- action bar ---------- */
|
|
|
|
.combat__actions {
|
|
position: relative;
|
|
z-index: 2;
|
|
display: flex;
|
|
gap: var(--ar-space-3);
|
|
min-block-size: clamp(6.5rem, 11vw, 8.5rem);
|
|
}
|
|
|
|
.action {
|
|
position: relative;
|
|
inline-size: clamp(6.5rem, 11vw, 8.5rem);
|
|
aspect-ratio: 1;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent url('/images/hud/runtime/fight-action-frame-360.png') center / 100% 100%
|
|
no-repeat;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.action__icon,
|
|
.action__label,
|
|
.action__key {
|
|
position: absolute;
|
|
inset-inline-start: 50%;
|
|
translate: -50% -50%;
|
|
}
|
|
|
|
.action__icon {
|
|
inset-block-start: 32%;
|
|
inline-size: 34%;
|
|
border-radius: 50%;
|
|
opacity: 0.92;
|
|
}
|
|
|
|
.action__label {
|
|
inset-block-start: 62%;
|
|
color: var(--ar-text);
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|
font-size: clamp(0.85rem, 1.2vw, 1rem);
|
|
letter-spacing: 0.03em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.action__key {
|
|
inset-block-start: 90%;
|
|
color: var(--ar-text-muted);
|
|
font-size: var(--ar-font-sm);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.action:hover:not(:disabled) .action__label,
|
|
.action:focus-visible .action__label {
|
|
color: var(--ar-gold);
|
|
}
|
|
|
|
.action:focus-visible {
|
|
outline: 2px solid var(--ar-gold);
|
|
outline-offset: 3px;
|
|
}
|
|
|
|
.action:disabled {
|
|
cursor: progress;
|
|
opacity: 0.55;
|
|
}
|
|
|
|
/* ---------- outcome overlay ---------- */
|
|
|
|
.outcome {
|
|
position: absolute;
|
|
z-index: 3;
|
|
inset-block-start: 50%;
|
|
inset-inline-start: 50%;
|
|
display: grid;
|
|
gap: var(--ar-space-2);
|
|
justify-items: center;
|
|
inline-size: min(26rem, 80%);
|
|
padding: var(--ar-space-5);
|
|
translate: -50% -50%;
|
|
border: 1px solid var(--ar-border-highlight);
|
|
background: rgb(9 11 13 / 0.94);
|
|
box-shadow: var(--ar-shadow-raised);
|
|
text-align: center;
|
|
}
|
|
|
|
.outcome p {
|
|
margin: 0;
|
|
color: var(--ar-text-muted);
|
|
}
|
|
|
|
.outcome__title {
|
|
margin: 0;
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|
font-size: clamp(1.5rem, 3vw, 2rem);
|
|
font-weight: 400;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.outcome--won .outcome__title {
|
|
color: var(--ar-gold);
|
|
}
|
|
|
|
.outcome--lost .outcome__title {
|
|
color: var(--ar-danger);
|
|
}
|
|
|
|
.outcome__hint {
|
|
font-size: var(--ar-font-sm);
|
|
font-style: italic;
|
|
}
|
|
|
|
.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);
|
|
color: var(--ar-text);
|
|
background: linear-gradient(180deg, #23282c, #14181b);
|
|
cursor: pointer;
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
/* ---------- combat log ---------- */
|
|
|
|
.combat__log {
|
|
display: grid;
|
|
grid-template-rows: auto minmax(0, 1fr);
|
|
min-block-size: 0;
|
|
border: 1px solid var(--ar-border);
|
|
background: var(--ar-panel-muted);
|
|
box-shadow: var(--ar-shadow-raised);
|
|
}
|
|
|
|
.combat__log-title {
|
|
margin: 0;
|
|
padding: var(--ar-space-3) var(--ar-space-4);
|
|
border-block-end: 1px solid var(--ar-border);
|
|
color: var(--ar-gold);
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|
font-size: 1.05rem;
|
|
font-weight: 400;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.combat__log-body {
|
|
padding: var(--ar-space-3) var(--ar-space-4) var(--ar-space-4);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.combat__log-round {
|
|
margin: var(--ar-space-4) 0 var(--ar-space-2);
|
|
color: var(--ar-gold);
|
|
font-size: var(--ar-font-sm);
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.combat__log-round:first-child {
|
|
margin-block-start: 0;
|
|
}
|
|
|
|
.combat__log-entry {
|
|
margin: 0 0 var(--ar-space-1);
|
|
padding-inline-start: var(--ar-space-3);
|
|
border-inline-start: 2px solid var(--ar-danger);
|
|
color: var(--ar-text-muted);
|
|
font-size: var(--ar-font-sm);
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.combat__log-entry--player {
|
|
border-inline-start-color: var(--ar-border-highlight);
|
|
}
|
|
|
|
/* ---------- notices ---------- */
|
|
|
|
.combat__notice {
|
|
grid-column: 1 / -1;
|
|
padding: var(--ar-space-4);
|
|
border: 1px solid var(--ar-border);
|
|
background: var(--ar-panel);
|
|
box-shadow: var(--ar-shadow-raised);
|
|
}
|
|
|
|
.combat__notice--error {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--ar-space-4);
|
|
border-color: var(--ar-danger);
|
|
}
|
|
|
|
.combat__notice--error button {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
.action__label {
|
|
transition: color var(--ar-motion-fast);
|
|
}
|
|
}
|
|
|
|
@media (width < 60rem) {
|
|
:host,
|
|
.combat {
|
|
block-size: auto;
|
|
}
|
|
|
|
.combat {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
|
|
.combat__stage {
|
|
min-block-size: clamp(24rem, 55vh, 34rem);
|
|
}
|
|
|
|
.combat__log {
|
|
max-block-size: 18rem;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
.combat__round {
|
|
order: -1;
|
|
justify-self: center;
|
|
}
|
|
|
|
.fighter--monster {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.fighter--monster .fighter__meter {
|
|
justify-items: start;
|
|
text-align: start;
|
|
}
|
|
|
|
.bar--monster .bar__fill {
|
|
inset-inline-start: 0;
|
|
inset-inline-end: auto;
|
|
}
|
|
|
|
.fighter__meter {
|
|
max-inline-size: none;
|
|
}
|
|
}
|