sprites
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 2.3 MiB |
BIN
apps/web/public/images/items/raider-warband-mark.png
Normal file
BIN
apps/web/public/images/items/raider-warband-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
BIN
apps/web/public/images/items/scorched-hide.png
Normal file
BIN
apps/web/public/images/items/scorched-hide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -1,5 +1,5 @@
|
||||
<div class="app-shell">
|
||||
<app-top-bar [character]="worldStore.displayedCharacter()" />
|
||||
<app-top-bar [character]="topBarCharacter()" />
|
||||
|
||||
<div class="app-shell__content" [class.app-shell__content--no-context]="!showContextPanel()">
|
||||
<app-side-navigation />
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Router, provideRouter } from '@angular/router';
|
||||
import type { CharacterResponse, Combat } from '../../core/api/game-api.models';
|
||||
import { CombatStore } from '../../features/combat/combat.store';
|
||||
import { WorldStore } from '../../features/world/world.store';
|
||||
import { AppShellComponent } from './app-shell.component';
|
||||
|
||||
function characterFixture(overrides: Partial<CharacterResponse> = {}): CharacterResponse {
|
||||
return {
|
||||
id: 'character-1',
|
||||
name: 'Aric Duskwalker',
|
||||
renown: 3,
|
||||
silver: 120,
|
||||
currentHp: 40,
|
||||
maxHp: 100,
|
||||
attack: 10,
|
||||
hpRegenPerSecond: 1,
|
||||
// Regenerating, as if the fight started mid-heal -- the world snapshot's
|
||||
// own HP would keep climbing toward maxHp if the HUD trusted it.
|
||||
hpRegenSince: new Date().toISOString(),
|
||||
currentLocation: { id: 'location-1', key: 'aschenfelder', name: 'Ashen Fields' },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const activeCombat: Combat = {
|
||||
id: 'combat-1',
|
||||
status: 'ACTIVE',
|
||||
round: 2,
|
||||
player: {
|
||||
name: 'Aric Duskwalker',
|
||||
maxHp: 100,
|
||||
currentHp: 62,
|
||||
potionsRemaining: 2,
|
||||
potionsMax: 2,
|
||||
statusEffects: [],
|
||||
},
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Ash Rat',
|
||||
level: 1,
|
||||
maxHp: 45,
|
||||
currentHp: 31,
|
||||
artworkPath: '/images/monsters/ash-rat.png',
|
||||
pendingIntent: null,
|
||||
guardRemainingRounds: null,
|
||||
enraged: false,
|
||||
},
|
||||
events: [],
|
||||
rewards: null,
|
||||
};
|
||||
|
||||
@Component({ template: '' })
|
||||
class BlankComponent {}
|
||||
|
||||
describe('AppShellComponent', () => {
|
||||
async function setup(url: string, combat: Combat | null): Promise<HTMLElement> {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AppShellComponent],
|
||||
providers: [
|
||||
provideRouter([{ path: '**', component: BlankComponent }]),
|
||||
{ provide: WorldStore, useValue: { displayedCharacter: signal(characterFixture()) } },
|
||||
{ provide: CombatStore, useValue: { combat: signal(combat) } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
await router.navigateByUrl(url);
|
||||
|
||||
const fixture = TestBed.createComponent(AppShellComponent);
|
||||
fixture.detectChanges();
|
||||
return fixture.nativeElement as HTMLElement;
|
||||
}
|
||||
|
||||
it('shows the live combat HP, not the regen-ticked world snapshot, while in a fight', async () => {
|
||||
const element = await setup('/combat/combat-1', activeCombat);
|
||||
|
||||
expect(element.textContent).toContain('62 / 100');
|
||||
expect(element.textContent).not.toContain('40 / 100');
|
||||
});
|
||||
|
||||
it('shows the world snapshot HP outside of combat', async () => {
|
||||
const element = await setup('/location', null);
|
||||
|
||||
expect(element.textContent).toContain('40 / 100');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { Router, RouterOutlet, isActive } from '@angular/router';
|
||||
import type { CharacterResponse } from '../../core/api/game-api.models';
|
||||
import { CombatStore } from '../../features/combat/combat.store';
|
||||
import { WorldStore } from '../../features/world/world.store';
|
||||
import { ContextPanelComponent } from '../context-panel/context-panel.component';
|
||||
import { GameFooterComponent } from '../game-footer/game-footer.component';
|
||||
@@ -22,11 +24,30 @@ export class AppShellComponent {
|
||||
private readonly router = inject(Router);
|
||||
|
||||
protected readonly worldStore = inject(WorldStore);
|
||||
private readonly combatStore = inject(CombatStore);
|
||||
|
||||
// The fight has its own log rail and wants the width, and the area info
|
||||
// belongs to the world view anyway, so the rail is dropped during combat.
|
||||
private readonly inCombat = isActive('/combat', this.router);
|
||||
|
||||
// Outside a fight the HUD shows WorldStore's regen-ticked HP. Mid-fight,
|
||||
// regen is paused server-side and the only HP that moves is the combat's --
|
||||
// the world snapshot is stale, so it must not drive the bar.
|
||||
protected readonly topBarCharacter = computed<CharacterResponse | null>(() => {
|
||||
const character = this.worldStore.displayedCharacter();
|
||||
const combat = this.inCombat() ? this.combatStore.combat() : null;
|
||||
if (!character || !combat) {
|
||||
return character;
|
||||
}
|
||||
|
||||
return {
|
||||
...character,
|
||||
currentHp: combat.player.currentHp,
|
||||
maxHp: combat.player.maxHp,
|
||||
hpRegenSince: null,
|
||||
};
|
||||
});
|
||||
|
||||
// The location view brings its own, far richer context sidebar. Showing the
|
||||
// shell's generic area panel next to it would say the same thing twice and
|
||||
// squeeze the artwork the screen is built around.
|
||||
|
||||
Reference in New Issue
Block a user