275 lines
10 KiB
HTML
275 lines
10 KiB
HTML
<section class="combat" aria-label="Combat">
|
||
@if (combat(); as combat) {
|
||
<div class="combat__stage" [class.combat__stage--shaken]="stageShake()">
|
||
<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]="'Health points ' + 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>
|
||
|
||
@if (combat.player.statusEffects.length) {
|
||
<ul class="statuses" data-combat-statuses aria-label="Active effects">
|
||
@for (effect of combat.player.statusEffects; track effect.type) {
|
||
<li
|
||
class="status"
|
||
[class]="'status--' + effect.type.toLowerCase()"
|
||
[attr.data-combat-status]="effect.type"
|
||
>
|
||
<span class="status__glyph" aria-hidden="true"></span>
|
||
<span class="status__label">
|
||
{{ statusEffectLabel(effect.type) }} · {{ effect.remainingRounds }}
|
||
</span>
|
||
</li>
|
||
}
|
||
</ul>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<p class="combat__round" data-combat-round>Round {{ combat.round }}</p>
|
||
|
||
<div class="fighter fighter--monster">
|
||
<div class="fighter__meter">
|
||
<p class="fighter__name">
|
||
{{ combat.monster.name }}
|
||
<span class="fighter__level">Level {{ combat.monster.level }}</span>
|
||
</p>
|
||
<div
|
||
class="bar bar--monster"
|
||
role="progressbar"
|
||
[attr.aria-label]="'Health points ' + 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="fighter__icon"
|
||
[src]="monsterIcon(combat.monster.key, combat.monster.artworkPath)"
|
||
alt=""
|
||
/>
|
||
</div>
|
||
</header>
|
||
|
||
@if (monsterIntentLabel(); as intent) {
|
||
<p class="combat__telegraph" data-combat-telegraph role="status">{{ intent }}</p>
|
||
}
|
||
|
||
@if (monsterGuardLabel(); as guard) {
|
||
<p class="combat__telegraph combat__guard" data-combat-guard role="status">{{ guard }}</p>
|
||
}
|
||
|
||
@if (monsterIsEnraged()) {
|
||
<p class="combat__telegraph combat__enraged" data-combat-enraged role="status">
|
||
{{ combat.monster.name }} is enraged.
|
||
</p>
|
||
}
|
||
|
||
<div class="combat__field">
|
||
<div
|
||
class="sprite sprite--player"
|
||
[class.sprite--attacking]="phase() === 'attacking'"
|
||
[class.sprite--hit]="phase() === 'hit'"
|
||
role="img"
|
||
[attr.aria-label]="combat.player.name"
|
||
></div>
|
||
<img
|
||
class="sprite sprite--monster"
|
||
[class.sprite--flinch]="monsterPhase() === 'flinch'"
|
||
[class.sprite--lunge]="monsterPhase() === 'lunge'"
|
||
[style.--sprite-scale]="monsterSpriteScale(combat.monster.key)"
|
||
[src]="monsterSprite(combat.monster.key, combat.monster.artworkPath)"
|
||
[alt]="combat.monster.name"
|
||
/>
|
||
</div>
|
||
|
||
<footer class="combat__actions">
|
||
@if (combat.status === 'ACTIVE') {
|
||
<button
|
||
type="button"
|
||
class="action"
|
||
data-combat-attack
|
||
[disabled]="busy()"
|
||
(click)="performAction('ATTACK')"
|
||
>
|
||
<img class="action__icon" src="/images/hud/runtime/AttackIcon-96.png" alt="" />
|
||
<span class="action__label">Attack</span>
|
||
<span class="action__key">1</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="action"
|
||
data-combat-heavy-strike
|
||
[disabled]="busy()"
|
||
(click)="performAction('HEAVY_STRIKE')"
|
||
>
|
||
<img class="action__icon" src="/images/hud/runtime/AttackIcon-96.png" alt="" />
|
||
<span class="action__label">Heavy Strike</span>
|
||
<span class="action__key">2</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="action"
|
||
data-combat-shield-bash
|
||
[disabled]="busy()"
|
||
(click)="performAction('SHIELD_BASH')"
|
||
>
|
||
<img class="action__icon" src="/images/hud/runtime/CharacterIcon-128.png" alt="" />
|
||
<span class="action__label">Shield Bash</span>
|
||
<span class="action__key">3</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="action"
|
||
data-combat-defend
|
||
[disabled]="busy()"
|
||
(click)="performAction('DEFEND')"
|
||
>
|
||
<img class="action__icon" src="/images/hud/runtime/CharacterIcon-128.png" alt="" />
|
||
<span class="action__label">Defend</span>
|
||
<span class="action__key">4</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="action"
|
||
data-combat-potion
|
||
[disabled]="busy() || combat.player.potionsRemaining <= 0"
|
||
(click)="performAction('POTION')"
|
||
>
|
||
<img class="action__icon" src="/images/items/small-healing-potion.png" alt="" />
|
||
<span class="action__label">Potion {{ combat.player.potionsRemaining }}/{{ combat.player.potionsMax }}</span>
|
||
<span class="action__key">5</span>
|
||
</button>
|
||
}
|
||
</footer>
|
||
|
||
@if (combat.status === 'WON') {
|
||
<div class="outcome outcome--won" data-combat-result="WON">
|
||
<h2 class="outcome__title">Victory</h2>
|
||
<p>{{ combat.monster.name }} has been defeated.</p>
|
||
|
||
@if (combat.rewards; as rewards) {
|
||
<section class="rewards" data-combat-rewards aria-label="Rewards">
|
||
@if (lootGroups(); as groups) {
|
||
@if (groups.length) {
|
||
@for (group of groups; track group.key) {
|
||
<h3 class="rewards__title" [attr.data-reward-group]="group.key">
|
||
{{ group.title }}
|
||
</h3>
|
||
<ul class="rewards__loot">
|
||
@for (reward of group.items; track reward.characterItemId) {
|
||
<li>
|
||
<app-item-card [item]="reward.item" [quantity]="reward.quantity" />
|
||
</li>
|
||
}
|
||
</ul>
|
||
}
|
||
} @else {
|
||
<p class="outcome__hint" data-reward-empty>No notable loot found.</p>
|
||
}
|
||
}
|
||
|
||
@if (leftBehind(); as refused) {
|
||
@if (refused.length) {
|
||
<h3 class="rewards__title rewards__title--refused">Left Behind</h3>
|
||
<ul class="rewards__refused" data-reward-left-behind>
|
||
@for (entry of refused; track entry.key) {
|
||
<li [attr.data-reward-refused]="entry.key">
|
||
{{ entry.name }} ×{{ entry.quantity }} — no room left
|
||
</li>
|
||
}
|
||
</ul>
|
||
}
|
||
}
|
||
|
||
@if (rewards.capacities.length) {
|
||
<app-loot-capacity-strip
|
||
class="rewards__capacities"
|
||
[capacities]="rewards.capacities"
|
||
/>
|
||
}
|
||
</section>
|
||
}
|
||
|
||
<div class="outcome__buttons">
|
||
<button type="button" class="outcome__button" data-combat-to-hunt (click)="goToHunt()">
|
||
Keep Hunting
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="outcome__button outcome__button--secondary"
|
||
data-combat-to-location
|
||
(click)="goToLocation()"
|
||
>
|
||
To Location
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="outcome__button outcome__button--secondary"
|
||
data-combat-to-inventory
|
||
(click)="goToInventory()"
|
||
>
|
||
Open Inventory
|
||
</button>
|
||
</div>
|
||
</div>
|
||
} @else if (combat.status === 'LOST') {
|
||
<div class="outcome outcome--lost" data-combat-result="LOST">
|
||
<h2 class="outcome__title">Defeat</h2>
|
||
<p>{{ combat.player.name }} has been defeated.</p>
|
||
<div class="outcome__buttons">
|
||
<button type="button" class="outcome__button" data-combat-to-hunt (click)="goToHunt()">
|
||
Keep Hunting
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="outcome__button outcome__button--secondary"
|
||
data-combat-to-location
|
||
(click)="goToLocation()"
|
||
>
|
||
To Location
|
||
</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<aside class="combat__log" aria-label="Combat Log">
|
||
<h2 class="combat__log-title">Combat Log</h2>
|
||
<div class="combat__log-body">
|
||
@for (round of logRounds(); track round.round) {
|
||
<p class="combat__log-round">Round {{ 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__notice" role="status">Loading combat…</p>
|
||
}
|
||
|
||
@if (combatStore.error(); as error) {
|
||
<section class="combat__notice combat__notice--error" role="alert">
|
||
<p>{{ error }}</p>
|
||
<button type="button" data-combat-retry (click)="retry()">Retry</button>
|
||
</section>
|
||
}
|
||
</section>
|