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.
This commit is contained in:
Bastian Wagner
2026-08-19 22:11:07 +02:00
parent 6affb9eddc
commit 92b9f1cd35
12 changed files with 475 additions and 203 deletions

View File

@@ -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">
<img
class="combat-page__portrait"
src="/images/hud/runtime/CharacterIcon-128.png"
[alt]="combat.player.name"
/>
<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 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>
</div>
<div class="combat-page__round" data-combat-round>Runde {{ combat.round }}</div>
<p class="combat__round" data-combat-round>Runde {{ combat.round }}</p>
<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="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]="combat.monster.artworkPath"
[alt]="combat.monster.name"
class="fighter__icon"
[src]="monsterIcon(combat.monster.key, combat.monster.artworkPath)"
alt=""
/>
</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>
</header>
<div class="combat__field">
<img class="sprite sprite--player" [src]="playerSprite" [alt]="combat.player.name" />
<img
class="sprite sprite--monster"
[src]="monsterSprite(combat.monster.key, combat.monster.artworkPath)"
[alt]="combat.monster.name"
/>
</div>
</div>
<div class="combat-page__actions">
@if (combat.status === 'ACTIVE') {
<button
type="button"
class="combat-page__attack"
data-combat-attack
[disabled]="combatStore.actionPending()"
(click)="attack()"
>
Angriff
</button>
} @else if (combat.status === 'WON') {
<section class="combat-page__result combat-page__result--won" data-combat-result="WON">
<h2>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>{{ combat.player.name }} wurde im Kampf besiegt.</p>
<button type="button" data-combat-to-hunt (click)="goToHunt()">Zur Jagd</button>
</section>
}
</div>
<aside class="combat-page__log" aria-label="Kampfprotokoll">
@for (round of logRounds(); track round.round) {
<p class="combat-page__log-round">Runde {{ round.round }}</p>
@for (event of round.events; track event.sequence) {
<p class="combat-page__log-entry">{{ formatEvent(event) }}</p>
<footer class="combat__actions">
@if (combat.status === 'ACTIVE') {
<button
type="button"
class="action"
data-combat-attack
[disabled]="combatStore.actionPending()"
(click)="attack()"
>
<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>
}
</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="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" class="outcome__button" data-combat-to-hunt (click)="goToHunt()">
Zur Jagd
</button>
</div>
}
</div>
<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__log-round">Runde {{ round.round }}</p>
@for (event of round.events; track event.sequence) {
<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>

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -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];
}