feat(web): show monster guard and rage on the combat screen
This commit is contained in:
@@ -170,7 +170,10 @@ export type CombatEventType =
|
||||
| 'STATUS_DAMAGE'
|
||||
| 'STATUS_EXPIRED'
|
||||
| 'COMBAT_WON'
|
||||
| 'COMBAT_LOST';
|
||||
| 'COMBAT_LOST'
|
||||
| 'GUARD_RAISED'
|
||||
| 'GUARD_ENDED'
|
||||
| 'ENRAGED';
|
||||
export type StatusEffectType = 'BLEED';
|
||||
export type CombatSide = 'PLAYER' | 'MONSTER';
|
||||
export type CombatAction = 'ATTACK' | 'HEAVY_STRIKE' | 'SHIELD_BASH' | 'DEFEND' | 'POTION';
|
||||
@@ -209,6 +212,9 @@ export interface CombatMonster {
|
||||
currentHp: number;
|
||||
artworkPath: string;
|
||||
pendingIntent: CombatMonsterIntent | null;
|
||||
/** Rounds the monster's raised guard still covers, or null when open. */
|
||||
guardRemainingRounds: number | null;
|
||||
enraged: boolean;
|
||||
}
|
||||
|
||||
export interface Combat {
|
||||
|
||||
@@ -25,6 +25,8 @@ const runningCombat: Combat = {
|
||||
currentHp: 30,
|
||||
artworkPath: '/images/enemies/RoadBandit.png',
|
||||
pendingIntent: null,
|
||||
guardRemainingRounds: null,
|
||||
enraged: false,
|
||||
},
|
||||
events: [],
|
||||
rewards: null,
|
||||
|
||||
@@ -69,6 +69,16 @@
|
||||
<p class="combat__telegraph" data-combat-telegraph role="status">{{ intent }}</p>
|
||||
}
|
||||
|
||||
@if (monsterGuardLabel(); as guard) {
|
||||
<p class="combat__telegraph" data-combat-guard role="status">{{ guard }}</p>
|
||||
}
|
||||
|
||||
@if (monsterIsEnraged()) {
|
||||
<p class="combat__telegraph" data-combat-enraged role="status">
|
||||
{{ combat.monster.name }} is enraged.
|
||||
</p>
|
||||
}
|
||||
|
||||
<div class="combat__field">
|
||||
<div
|
||||
class="sprite sprite--player"
|
||||
|
||||
@@ -413,7 +413,7 @@
|
||||
z-index: 2;
|
||||
margin: 0;
|
||||
padding: var(--ar-space-2) var(--ar-space-4);
|
||||
border: 1px solid var(--ar-gold);
|
||||
border: 1px solid currentColor;
|
||||
border-radius: var(--ar-radius-sm);
|
||||
background: rgb(9 11 13 / 0.85);
|
||||
color: var(--ar-gold);
|
||||
@@ -423,6 +423,17 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// The guard and enrage banners are the same shape and role as the telegraph
|
||||
// above -- same class, same rule -- only the accent changes, picked off the
|
||||
// data attribute the tests already hook into.
|
||||
[data-combat-guard] {
|
||||
color: var(--ar-blue);
|
||||
}
|
||||
|
||||
[data-combat-enraged] {
|
||||
color: var(--ar-danger);
|
||||
}
|
||||
|
||||
.action {
|
||||
position: relative;
|
||||
inline-size: clamp(6.5rem, 11vw, 8.5rem);
|
||||
@@ -709,45 +720,6 @@
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* ---------- ongoing effects ---------- */
|
||||
|
||||
/* Sits directly under the health bar it is eating away at, so the cause of
|
||||
the drain is next to the number that drops (spec §4). */
|
||||
.statuses {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ar-space-2);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border: 1px solid var(--ar-border);
|
||||
border-radius: 0.15rem;
|
||||
background: rgb(0 0 0 / 0.35);
|
||||
font-size: var(--ar-font-sm);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
/* Drawn rather than loaded: a 12px bitmap of a blood drop would be mush, and
|
||||
the shape carries the meaning on its own. */
|
||||
.status__glyph {
|
||||
inline-size: 0.55rem;
|
||||
block-size: 0.7rem;
|
||||
background: currentcolor;
|
||||
clip-path: polygon(50% 0%, 100% 62%, 82% 95%, 18% 95%, 0% 62%);
|
||||
}
|
||||
|
||||
.status--bleed {
|
||||
border-color: rgb(158 46 42 / 0.75);
|
||||
color: #d8736c;
|
||||
}
|
||||
|
||||
.fighter--monster .fighter__meter {
|
||||
justify-items: start;
|
||||
text-align: start;
|
||||
|
||||
@@ -37,6 +37,8 @@ const activeCombat: Combat = {
|
||||
currentHp: 31,
|
||||
artworkPath: '/images/monsters/ash-rat.png',
|
||||
pendingIntent: null,
|
||||
guardRemainingRounds: null,
|
||||
enraged: false,
|
||||
},
|
||||
events: [
|
||||
{ round: 1, sequence: 1, type: 'DAMAGE', source: 'PLAYER', target: 'MONSTER', amount: 14 },
|
||||
@@ -197,6 +199,61 @@ describe('CombatPageComponent', () => {
|
||||
expect(element.querySelector('[data-combat-telegraph]')).toBeNull();
|
||||
});
|
||||
|
||||
const veteran: Combat = {
|
||||
...activeCombat,
|
||||
monster: { ...activeCombat.monster, name: 'Raider Veteran' },
|
||||
};
|
||||
|
||||
it('announces a raised guard with the rounds it still covers', async () => {
|
||||
const fixture = await setup({
|
||||
...veteran,
|
||||
monster: { ...veteran.monster, guardRemainingRounds: 2 },
|
||||
});
|
||||
|
||||
const banner: HTMLElement | null = fixture.nativeElement.querySelector(
|
||||
'[data-combat-guard]',
|
||||
);
|
||||
expect(banner?.textContent).toContain('Raider Veteran');
|
||||
expect(banner?.textContent).toContain('2');
|
||||
});
|
||||
|
||||
it('says nothing about a guard when the monster is open', async () => {
|
||||
const fixture = await setup(activeCombat);
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('[data-combat-guard]'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('marks an enraged monster', async () => {
|
||||
const fixture = await setup({
|
||||
...activeCombat,
|
||||
monster: { ...activeCombat.monster, enraged: true },
|
||||
});
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('[data-combat-enraged]'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('reads the new events back in the log', async () => {
|
||||
const fixture = await setup({
|
||||
...veteran,
|
||||
events: [
|
||||
{ round: 1, sequence: 1, type: 'GUARD_RAISED', source: 'MONSTER', target: 'MONSTER', amount: 2 },
|
||||
{ round: 1, sequence: 2, type: 'ENRAGED', source: 'MONSTER', target: 'MONSTER' },
|
||||
{ round: 2, sequence: 1, type: 'GUARD_ENDED', source: 'MONSTER', target: 'MONSTER' },
|
||||
],
|
||||
});
|
||||
|
||||
const log = (
|
||||
fixture.nativeElement as HTMLElement
|
||||
).querySelector('.combat__log')?.textContent;
|
||||
expect(log).toContain('Raider Veteran raises its guard.');
|
||||
expect(log).toContain('Raider Veteran turns savage.');
|
||||
expect(log).toContain("Raider Veteran's guard drops.");
|
||||
});
|
||||
|
||||
it('renders HEAL, DEFEND, TELEGRAPH, and INTERRUPT log lines', async () => {
|
||||
const fixture = await setup({
|
||||
...activeCombat,
|
||||
|
||||
@@ -323,6 +323,20 @@ export class CombatPageComponent implements OnInit {
|
||||
return `${combat.monster.name} is winding up a Heavy Strike.`;
|
||||
}
|
||||
|
||||
protected monsterGuardLabel(): string | null {
|
||||
const combat = this.displayed();
|
||||
const rounds = combat?.monster.guardRemainingRounds;
|
||||
if (!combat || !rounds) {
|
||||
return null;
|
||||
}
|
||||
const roundWord = rounds === 1 ? 'round' : 'rounds';
|
||||
return `${combat.monster.name} is covering — ${rounds} ${roundWord}. Shield Bash breaks it.`;
|
||||
}
|
||||
|
||||
protected monsterIsEnraged(): boolean {
|
||||
return this.displayed()?.monster.enraged ?? false;
|
||||
}
|
||||
|
||||
protected logRounds(): CombatLogRound[] {
|
||||
const combat = this.displayed();
|
||||
if (!combat) {
|
||||
@@ -366,6 +380,18 @@ export class CombatPageComponent implements OnInit {
|
||||
return `${playerName} interrupts ${monsterName}'s attack.`;
|
||||
}
|
||||
|
||||
if (event.type === 'GUARD_RAISED') {
|
||||
return `${monsterName} raises its guard.`;
|
||||
}
|
||||
|
||||
if (event.type === 'GUARD_ENDED') {
|
||||
return `${monsterName}'s guard drops.`;
|
||||
}
|
||||
|
||||
if (event.type === 'ENRAGED') {
|
||||
return `${monsterName} turns savage.`;
|
||||
}
|
||||
|
||||
const effect = event.statusEffect ? STATUS_EFFECT_LABELS[event.statusEffect] : 'An effect';
|
||||
|
||||
if (event.type === 'STATUS_APPLIED') {
|
||||
|
||||
@@ -26,6 +26,8 @@ const startedCombat: Combat = {
|
||||
currentHp: 45,
|
||||
artworkPath: '/images/monsters/ash-rat.png',
|
||||
pendingIntent: null,
|
||||
guardRemainingRounds: null,
|
||||
enraged: false,
|
||||
},
|
||||
events: [],
|
||||
rewards: null,
|
||||
|
||||
@@ -88,6 +88,8 @@ const startedCombat: Combat = {
|
||||
currentHp: 75,
|
||||
artworkPath: '/images/enemies/RoadBandit.png',
|
||||
pendingIntent: null,
|
||||
guardRemainingRounds: null,
|
||||
enraged: false,
|
||||
},
|
||||
events: [],
|
||||
rewards: null,
|
||||
|
||||
Reference in New Issue
Block a user