715 lines
16 KiB
SCSS
715 lines
16 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);
|
|
// The sprites are sized as a share of the stage, so the stage has to carry a
|
|
// height of its own. With only a min-block-size the tallest cut-out -- the
|
|
// road bandit -- sized the battlefield row from its intrinsic height and
|
|
// pushed the page into a scrollbar. The subtracted 12.5rem is the shell
|
|
// chrome around the main column: top bar, footer and its own padding.
|
|
block-size: max(24rem, calc(100dvh - 12.5rem));
|
|
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;
|
|
// The single row is spelled out rather than left implicit: only then is it a
|
|
// definite height, and only then do the sprites' percentage heights resolve
|
|
// against the battlefield instead of against their own artwork.
|
|
grid-template-columns: 1fr 1fr;
|
|
grid-template-rows: minmax(0, 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--player.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%);
|
|
// 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 ---------- */
|
|
|
|
.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 {
|
|
inline-size: min(32rem, 90%);
|
|
}
|
|
|
|
.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--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);
|
|
}
|
|
|
|
.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 {
|
|
block-size: clamp(20rem, 55dvh, 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;
|
|
}
|
|
}
|
|
|
|
/* Reward summary: dark stone and bronze, large readable values, restrained
|
|
rarity emphasis. No confetti, no popups, no slot-machine reveal (spec §50). */
|
|
.rewards {
|
|
display: grid;
|
|
gap: var(--ar-space-3);
|
|
justify-items: center;
|
|
inline-size: 100%;
|
|
margin-block-start: var(--ar-space-3);
|
|
padding-block-start: var(--ar-space-3);
|
|
border-block-start: 1px solid var(--ar-border);
|
|
}
|
|
|
|
.rewards__title {
|
|
margin: 0;
|
|
color: var(--ar-text-muted);
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|
font-size: var(--ar-font-sm);
|
|
font-weight: 400;
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.rewards__currencies {
|
|
display: flex;
|
|
gap: var(--ar-space-6);
|
|
margin: 0;
|
|
}
|
|
|
|
.rewards__currency {
|
|
display: grid;
|
|
gap: var(--ar-space-1);
|
|
justify-items: center;
|
|
}
|
|
|
|
.rewards__currency dt {
|
|
color: var(--ar-text-muted);
|
|
font-size: var(--ar-font-sm);
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.rewards__currency dd {
|
|
margin: 0;
|
|
color: var(--ar-gold);
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|
font-size: clamp(1.25rem, 2.5vw, 1.6rem);
|
|
}
|
|
|
|
.rewards__loot {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--ar-space-4);
|
|
justify-content: center;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|