feat(combat): rebuild the combat screen around the reference layout
Move both health bars to a status row at the top of the scene with a circular portrait beside each, and place full-body sprites for the player and the monster standing on the location background instead of square portraits. The Angriff action now uses the ornate HUD frame art, with the keybind in the frame's own tab. Sprite and icon derivatives are keyed by monster key so both seeded monsters resolve; the Aschenratte body art still carries its original backdrop until a cut-out version replaces it at the same path.
BIN
apps/web/public/images/combat/icons/ash-rat-128.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
apps/web/public/images/combat/icons/road-bandit-128.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
apps/web/public/images/combat/sprites/ash-rat-760.png
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
apps/web/public/images/combat/sprites/road-bandit-620.png
Normal file
|
After Width: | Height: | Size: 348 KiB |
BIN
apps/web/public/images/combat/sprites/warrior-attack-512.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
apps/web/public/images/hud/runtime/AttackIcon-96.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
apps/web/public/images/hud/runtime/fight-action-frame-360.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
@@ -1,93 +1,117 @@
|
||||
<section class="combat-page" aria-label="Kampf">
|
||||
<section class="combat" aria-label="Kampf">
|
||||
@if (combatStore.combat(); as combat) {
|
||||
<div class="combat-page__scene">
|
||||
<div class="combat-page__combatant combat-page__combatant--player">
|
||||
<div class="combat__stage">
|
||||
<header class="combat__status">
|
||||
<div class="fighter fighter--player">
|
||||
<img class="fighter__icon" [src]="playerIcon" alt="" />
|
||||
<div class="fighter__meter">
|
||||
<p class="fighter__name">{{ combat.player.name }}</p>
|
||||
<div
|
||||
class="bar bar--player"
|
||||
role="progressbar"
|
||||
[attr.aria-label]="'Lebenspunkte ' + combat.player.name"
|
||||
[attr.aria-valuenow]="combat.player.currentHp"
|
||||
[attr.aria-valuemin]="0"
|
||||
[attr.aria-valuemax]="combat.player.maxHp"
|
||||
>
|
||||
<span class="bar__fill" [style.inline-size.%]="playerHpPercent()"></span>
|
||||
<span class="bar__text">{{ combat.player.currentHp }} / {{ combat.player.maxHp }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="combat__round" data-combat-round>Runde {{ combat.round }}</p>
|
||||
|
||||
<div class="fighter fighter--monster">
|
||||
<div class="fighter__meter">
|
||||
<p class="fighter__name">
|
||||
{{ combat.monster.name }}
|
||||
<span class="fighter__level">Stufe {{ combat.monster.level }}</span>
|
||||
</p>
|
||||
<div
|
||||
class="bar bar--monster"
|
||||
role="progressbar"
|
||||
[attr.aria-label]="'Lebenspunkte ' + combat.monster.name"
|
||||
[attr.aria-valuenow]="combat.monster.currentHp"
|
||||
[attr.aria-valuemin]="0"
|
||||
[attr.aria-valuemax]="combat.monster.maxHp"
|
||||
>
|
||||
<span class="bar__fill" [style.inline-size.%]="monsterHpPercent()"></span>
|
||||
<span class="bar__text">{{ combat.monster.currentHp }} / {{ combat.monster.maxHp }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
class="combat-page__portrait"
|
||||
src="/images/hud/runtime/CharacterIcon-128.png"
|
||||
[alt]="combat.player.name"
|
||||
class="fighter__icon"
|
||||
[src]="monsterIcon(combat.monster.key, combat.monster.artworkPath)"
|
||||
alt=""
|
||||
/>
|
||||
<div class="combat-page__info">
|
||||
<span class="combat-page__name">{{ combat.player.name }}</span>
|
||||
<div class="combat-page__hp" aria-label="Lebenspunkte">
|
||||
<span>{{ combat.player.currentHp }} / {{ combat.player.maxHp }}</span>
|
||||
<span class="combat-page__hp-track" aria-hidden="true">
|
||||
<span class="combat-page__hp-value" [style.inline-size.%]="playerHpPercent()"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="combat-page__round" data-combat-round>Runde {{ combat.round }}</div>
|
||||
|
||||
<div class="combat-page__combatant combat-page__combatant--monster">
|
||||
<picture>
|
||||
@if (runtimeMonsterArtwork(combat.monster.artworkPath); as runtimeArtwork) {
|
||||
<source [srcset]="runtimeArtwork" type="image/jpeg" />
|
||||
}
|
||||
<div class="combat__field">
|
||||
<img class="sprite sprite--player" [src]="playerSprite" [alt]="combat.player.name" />
|
||||
<img
|
||||
class="combat-page__portrait"
|
||||
[src]="combat.monster.artworkPath"
|
||||
class="sprite sprite--monster"
|
||||
[src]="monsterSprite(combat.monster.key, combat.monster.artworkPath)"
|
||||
[alt]="combat.monster.name"
|
||||
/>
|
||||
</picture>
|
||||
<div class="combat-page__info">
|
||||
<span class="combat-page__name">{{ combat.monster.name }}</span>
|
||||
<span class="combat-page__level">Stufe {{ combat.monster.level }}</span>
|
||||
<div class="combat-page__hp" aria-label="Lebenspunkte des Gegners">
|
||||
<span>{{ combat.monster.currentHp }} / {{ combat.monster.maxHp }}</span>
|
||||
<span class="combat-page__hp-track" aria-hidden="true">
|
||||
<span
|
||||
class="combat-page__hp-value combat-page__hp-value--monster"
|
||||
[style.inline-size.%]="monsterHpPercent()"
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="combat-page__actions">
|
||||
<footer class="combat__actions">
|
||||
@if (combat.status === 'ACTIVE') {
|
||||
<button
|
||||
type="button"
|
||||
class="combat-page__attack"
|
||||
class="action"
|
||||
data-combat-attack
|
||||
[disabled]="combatStore.actionPending()"
|
||||
(click)="attack()"
|
||||
>
|
||||
Angriff
|
||||
<img class="action__icon" src="/images/hud/runtime/AttackIcon-96.png" alt="" />
|
||||
<span class="action__label">Angriff</span>
|
||||
<span class="action__key">1</span>
|
||||
</button>
|
||||
} @else if (combat.status === 'WON') {
|
||||
<section class="combat-page__result combat-page__result--won" data-combat-result="WON">
|
||||
<h2>Sieg</h2>
|
||||
}
|
||||
</footer>
|
||||
|
||||
@if (combat.status === 'WON') {
|
||||
<div class="outcome outcome--won" data-combat-result="WON">
|
||||
<h2 class="outcome__title">Sieg</h2>
|
||||
<p>{{ combat.monster.name }} wurde besiegt.</p>
|
||||
<p class="combat-page__result-hint">Belohnungen werden im nächsten Schritt verarbeitet.</p>
|
||||
<button type="button" data-combat-to-hunt (click)="goToHunt()">Zur Jagd</button>
|
||||
</section>
|
||||
} @else {
|
||||
<section class="combat-page__result combat-page__result--lost" data-combat-result="LOST">
|
||||
<h2>Niederlage</h2>
|
||||
<p class="outcome__hint">Belohnungen werden im nächsten Schritt verarbeitet.</p>
|
||||
<button type="button" class="outcome__button" data-combat-to-hunt (click)="goToHunt()">
|
||||
Zur Jagd
|
||||
</button>
|
||||
</div>
|
||||
} @else if (combat.status === 'LOST') {
|
||||
<div class="outcome outcome--lost" data-combat-result="LOST">
|
||||
<h2 class="outcome__title">Niederlage</h2>
|
||||
<p>{{ combat.player.name }} wurde im Kampf besiegt.</p>
|
||||
<button type="button" data-combat-to-hunt (click)="goToHunt()">Zur Jagd</button>
|
||||
</section>
|
||||
<button type="button" class="outcome__button" data-combat-to-hunt (click)="goToHunt()">
|
||||
Zur Jagd
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<aside class="combat-page__log" aria-label="Kampfprotokoll">
|
||||
<aside class="combat__log" aria-label="Kampfprotokoll">
|
||||
<h2 class="combat__log-title">Kampflog</h2>
|
||||
<div class="combat__log-body">
|
||||
@for (round of logRounds(); track round.round) {
|
||||
<p class="combat-page__log-round">Runde {{ round.round }}</p>
|
||||
<p class="combat__log-round">Runde {{ round.round }}</p>
|
||||
@for (event of round.events; track event.sequence) {
|
||||
<p class="combat-page__log-entry">{{ formatEvent(event) }}</p>
|
||||
<p class="combat__log-entry" [class.combat__log-entry--player]="event.source === 'PLAYER'">
|
||||
{{ formatEvent(event) }}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
} @else if (combatStore.loading()) {
|
||||
<p class="combat-page__loading" role="status">Kampf wird geladen…</p>
|
||||
<p class="combat__notice" role="status">Kampf wird geladen…</p>
|
||||
}
|
||||
|
||||
@if (combatStore.error(); as error) {
|
||||
<section class="combat-page__error" role="alert">
|
||||
<section class="combat__notice combat__notice--error" role="alert">
|
||||
<p>{{ error }}</p>
|
||||
<button type="button" data-combat-retry (click)="retry()">Erneut versuchen</button>
|
||||
</section>
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
:host {
|
||||
display: block;
|
||||
min-block-size: 100%;
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
.combat-page {
|
||||
.combat {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) clamp(13rem, 17vw, 17rem);
|
||||
gap: var(--ar-space-4);
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
.combat-page__scene {
|
||||
/* ---------- stage ---------- */
|
||||
|
||||
.combat__stage {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: end;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
gap: var(--ar-space-4);
|
||||
min-block-size: clamp(18rem, 40vh, 26rem);
|
||||
padding: var(--ar-space-5);
|
||||
min-block-size: 26rem;
|
||||
padding: var(--ar-space-4);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--ar-border);
|
||||
background-color: #151718;
|
||||
background-color: var(--ar-bg);
|
||||
background-image: url('/images/backgrounds/Aschestrasse.png');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
@@ -31,201 +34,370 @@
|
||||
url('/images/backgrounds/runtime/Aschestrasse-960.jpg') type('image/jpeg') 1x
|
||||
)
|
||||
) {
|
||||
.combat-page__scene {
|
||||
.combat__stage {
|
||||
background-image: image-set(
|
||||
url('/images/backgrounds/runtime/Aschestrasse-960.jpg') type('image/jpeg') 1x
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.combat-page__scene::before {
|
||||
.combat__stage::before {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
inset: 0;
|
||||
content: '';
|
||||
background: linear-gradient(180deg, rgb(4 6 8 / 0.15), rgb(4 6 8 / 0.6));
|
||||
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;
|
||||
}
|
||||
|
||||
.combat-page__combatant {
|
||||
/* ---------- combatant status ---------- */
|
||||
|
||||
.combat__status {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
gap: var(--ar-space-2);
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: var(--ar-space-4);
|
||||
}
|
||||
|
||||
.combat-page__combatant--player {
|
||||
justify-self: start;
|
||||
.fighter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ar-space-3);
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.combat-page__combatant--monster {
|
||||
justify-self: end;
|
||||
.fighter--monster {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.combat-page__portrait {
|
||||
inline-size: clamp(6rem, 14vw, 10rem);
|
||||
block-size: clamp(6rem, 14vw, 10rem);
|
||||
.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: 2px solid var(--ar-border-highlight);
|
||||
border-radius: var(--ar-radius-md);
|
||||
box-shadow: var(--ar-shadow-raised);
|
||||
border-radius: 50%;
|
||||
box-shadow:
|
||||
0 0 0 1px var(--ar-border-highlight),
|
||||
0 0.3rem 0.9rem rgb(0 0 0 / 0.8);
|
||||
}
|
||||
|
||||
.combat-page__info {
|
||||
.fighter__meter {
|
||||
display: grid;
|
||||
gap: var(--ar-space-1);
|
||||
min-inline-size: 10rem;
|
||||
color: var(--ar-text);
|
||||
text-shadow: 0 0.1rem 0.4rem rgb(0 0 0 / 0.85);
|
||||
gap: 0.3rem;
|
||||
min-inline-size: 0;
|
||||
flex: 1 1 auto;
|
||||
max-inline-size: 22rem;
|
||||
}
|
||||
|
||||
.combat-page__name {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 1.15rem;
|
||||
.fighter--monster .fighter__meter {
|
||||
justify-items: end;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.combat-page__level {
|
||||
color: var(--ar-gold);
|
||||
font-size: var(--ar-font-sm);
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.combat-page__hp {
|
||||
display: grid;
|
||||
gap: var(--ar-space-1);
|
||||
font-size: var(--ar-font-sm);
|
||||
}
|
||||
|
||||
.combat-page__hp-track {
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
block-size: 0.5rem;
|
||||
.fighter__name {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--ar-border);
|
||||
border-radius: var(--ar-radius-sm);
|
||||
background: rgb(0 0 0 / 0.5);
|
||||
}
|
||||
|
||||
.combat-page__hp-value {
|
||||
display: block;
|
||||
block-size: 100%;
|
||||
background: var(--ar-success);
|
||||
}
|
||||
|
||||
.combat-page__hp-value--monster {
|
||||
background: var(--ar-danger);
|
||||
}
|
||||
|
||||
.combat-page__round {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
align-self: start;
|
||||
justify-self: center;
|
||||
padding: var(--ar-space-1) var(--ar-space-3);
|
||||
border: 1px solid var(--ar-border-highlight);
|
||||
border-radius: var(--ar-radius-sm);
|
||||
color: var(--ar-gold);
|
||||
background: rgb(9 11 13 / 0.75);
|
||||
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;
|
||||
}
|
||||
|
||||
.combat-page__actions {
|
||||
/* ---------- hp bars ---------- */
|
||||
|
||||
.bar {
|
||||
position: relative;
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
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));
|
||||
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;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.combat-page__attack {
|
||||
padding: var(--ar-space-3) var(--ar-space-6);
|
||||
border: 1px solid var(--ar-border-highlight);
|
||||
border-radius: var(--ar-radius-sm);
|
||||
block-size: 100%;
|
||||
place-items: center;
|
||||
color: var(--ar-text);
|
||||
background: linear-gradient(180deg, #263b4b, #17232d);
|
||||
cursor: pointer;
|
||||
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: 1.1rem;
|
||||
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-block-size: 100%;
|
||||
inline-size: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 1rem 1.5rem rgb(0 0 0 / 0.75));
|
||||
}
|
||||
|
||||
.sprite--player {
|
||||
justify-self: start;
|
||||
block-size: clamp(11rem, 30vh, 19rem);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.sprite--monster {
|
||||
justify-self: end;
|
||||
block-size: clamp(9rem, 24vh, 15rem);
|
||||
}
|
||||
|
||||
/* ---------- 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 {
|
||||
position: absolute;
|
||||
inset-block-start: 32%;
|
||||
inset-inline-start: 50%;
|
||||
inline-size: 34%;
|
||||
translate: -50% -50%;
|
||||
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);
|
||||
letter-spacing: 0.03em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.combat-page__attack:hover:not(:disabled) {
|
||||
border-color: #d6b26b;
|
||||
background: linear-gradient(180deg, #315067, #1a2c3a);
|
||||
.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;
|
||||
}
|
||||
|
||||
.combat-page__attack:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
.action:hover:not(:disabled) .action__label,
|
||||
.action:focus-visible .action__label {
|
||||
color: var(--ar-gold);
|
||||
}
|
||||
|
||||
.combat-page__result {
|
||||
.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);
|
||||
max-inline-size: 30rem;
|
||||
justify-items: center;
|
||||
inline-size: min(26rem, 80%);
|
||||
padding: var(--ar-space-5);
|
||||
translate: -50% -50%;
|
||||
border: 1px solid var(--ar-border-highlight);
|
||||
background: var(--ar-panel);
|
||||
background: rgb(9 11 13 / 0.94);
|
||||
box-shadow: var(--ar-shadow-raised);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.combat-page__result h2 {
|
||||
.outcome p {
|
||||
margin: 0;
|
||||
color: var(--ar-text-muted);
|
||||
}
|
||||
|
||||
.outcome__title {
|
||||
margin: 0;
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 1.6rem;
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.combat-page__result--won h2 {
|
||||
color: var(--ar-success);
|
||||
.outcome--won .outcome__title {
|
||||
color: var(--ar-gold);
|
||||
}
|
||||
|
||||
.combat-page__result--lost h2 {
|
||||
.outcome--lost .outcome__title {
|
||||
color: var(--ar-danger);
|
||||
}
|
||||
|
||||
.combat-page__result-hint {
|
||||
color: var(--ar-text-muted);
|
||||
.outcome__hint {
|
||||
font-size: var(--ar-font-sm);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.combat-page__log {
|
||||
.outcome__button {
|
||||
margin-block-start: var(--ar-space-2);
|
||||
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:hover {
|
||||
border-color: var(--ar-gold);
|
||||
color: var(--ar-gold);
|
||||
}
|
||||
|
||||
/* ---------- combat log ---------- */
|
||||
|
||||
.combat__log {
|
||||
display: grid;
|
||||
gap: var(--ar-space-1);
|
||||
max-block-size: 16rem;
|
||||
padding: var(--ar-space-4);
|
||||
overflow-y: auto;
|
||||
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-page__log-round {
|
||||
margin: var(--ar-space-2) 0 0;
|
||||
.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-size: var(--ar-font-sm);
|
||||
letter-spacing: 0.06em;
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.combat-page__log-entry {
|
||||
margin: 0;
|
||||
color: var(--ar-text-muted);
|
||||
.combat__log-body {
|
||||
padding: var(--ar-space-3) var(--ar-space-4) var(--ar-space-4);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.combat-page__loading,
|
||||
.combat-page__error {
|
||||
.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-page__error {
|
||||
.combat__notice--error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -233,7 +405,7 @@
|
||||
border-color: var(--ar-danger);
|
||||
}
|
||||
|
||||
.combat-page__error button {
|
||||
.combat__notice--error button {
|
||||
flex: 0 0 auto;
|
||||
padding: var(--ar-space-2) var(--ar-space-3);
|
||||
border: 1px solid var(--ar-border-highlight);
|
||||
@@ -244,20 +416,64 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.combat-page__hp-value {
|
||||
.bar__fill {
|
||||
transition: inline-size var(--ar-motion-base);
|
||||
}
|
||||
|
||||
.action__label {
|
||||
transition: color var(--ar-motion-fast);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 720px) {
|
||||
.combat-page__scene {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
@media (width < 60rem) {
|
||||
:host,
|
||||
.combat {
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
.combat-page__combatant--player,
|
||||
.combat-page__combatant--monster {
|
||||
.combat {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.combat__stage {
|
||||
min-block-size: clamp(24rem, 55vh, 34rem);
|
||||
}
|
||||
|
||||
.combat__log {
|
||||
max-block-size: 18rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 40rem) {
|
||||
.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;
|
||||
}
|
||||
|
||||
.sprite--player {
|
||||
block-size: clamp(8rem, 22vh, 12rem);
|
||||
}
|
||||
|
||||
.sprite--monster {
|
||||
block-size: clamp(7rem, 18vh, 10rem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import type { CombatEvent } from '../../../core/api/game-api.models';
|
||||
import { runtimeMonsterArtworkPath } from '../../../shared/monster-artwork';
|
||||
import {
|
||||
combatMonsterIconPath,
|
||||
combatMonsterSpritePath,
|
||||
runtimeMonsterArtworkPath,
|
||||
} from '../../../shared/monster-artwork';
|
||||
import { CombatStore } from '../combat.store';
|
||||
|
||||
interface CombatLogRound {
|
||||
@@ -9,6 +13,9 @@ interface CombatLogRound {
|
||||
events: CombatEvent[];
|
||||
}
|
||||
|
||||
const PLAYER_SPRITE = '/images/combat/sprites/warrior-attack-512.png';
|
||||
const PLAYER_ICON = '/images/hud/runtime/CharacterIcon-128.png';
|
||||
|
||||
@Component({
|
||||
selector: 'app-combat-page',
|
||||
templateUrl: './combat-page.component.html',
|
||||
@@ -35,8 +42,15 @@ export class CombatPageComponent implements OnInit {
|
||||
void this.router.navigate(['/hunt']);
|
||||
}
|
||||
|
||||
protected runtimeMonsterArtwork(artworkPath: string): string | undefined {
|
||||
return runtimeMonsterArtworkPath(artworkPath);
|
||||
protected readonly playerSprite = PLAYER_SPRITE;
|
||||
protected readonly playerIcon = PLAYER_ICON;
|
||||
|
||||
protected monsterSprite(monsterKey: string, artworkPath: string): string {
|
||||
return combatMonsterSpritePath(monsterKey) ?? runtimeMonsterArtworkPath(artworkPath) ?? artworkPath;
|
||||
}
|
||||
|
||||
protected monsterIcon(monsterKey: string, artworkPath: string): string {
|
||||
return combatMonsterIconPath(monsterKey) ?? runtimeMonsterArtworkPath(artworkPath) ?? artworkPath;
|
||||
}
|
||||
|
||||
protected playerHpPercent(): number {
|
||||
|
||||
@@ -6,3 +6,21 @@ const RUNTIME_MONSTER_ARTWORK: Readonly<Record<string, string>> = {
|
||||
export function runtimeMonsterArtworkPath(artworkPath: string): string | undefined {
|
||||
return RUNTIME_MONSTER_ARTWORK[artworkPath];
|
||||
}
|
||||
|
||||
const COMBAT_MONSTER_SPRITE: Readonly<Record<string, string>> = {
|
||||
'ash-rat': '/images/combat/sprites/ash-rat-760.png',
|
||||
'road-bandit': '/images/combat/sprites/road-bandit-620.png',
|
||||
};
|
||||
|
||||
const COMBAT_MONSTER_ICON: Readonly<Record<string, string>> = {
|
||||
'ash-rat': '/images/combat/icons/ash-rat-128.png',
|
||||
'road-bandit': '/images/combat/icons/road-bandit-128.png',
|
||||
};
|
||||
|
||||
export function combatMonsterSpritePath(monsterKey: string): string | undefined {
|
||||
return COMBAT_MONSTER_SPRITE[monsterKey];
|
||||
}
|
||||
|
||||
export function combatMonsterIconPath(monsterKey: string): string | undefined {
|
||||
return COMBAT_MONSTER_ICON[monsterKey];
|
||||
}
|
||||
|
||||
BIN
art/hud/fight-action-background.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |