feat: add hunt page, combat placeholder, and Jagd navigation

Wires up Slice 0.2 end-to-end: HuntPageComponent renders the
hunting-unavailable/ready/loading/encounters-found states off
HuntingStore and WorldStore, Angreifen hands the HuntEncounter id to a
new inert CombatPlaceholderPageComponent via /combat/new, the Jagd nav
entry is enabled with router-driven active state (matching Karte's),
and the context panel now lists possible encounters for hunting-enabled
locations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-19 14:06:57 +02:00
parent 784bd3ce3a
commit 4e684dc45e
11 changed files with 664 additions and 24 deletions

View File

@@ -14,6 +14,20 @@ export const routes: Routes = [
(module) => module.WorldPageComponent, (module) => module.WorldPageComponent,
), ),
}, },
{
path: 'hunt',
loadComponent: () =>
import('./features/hunting/hunt-page/hunt-page.component').then(
(module) => module.HuntPageComponent,
),
},
{
path: 'combat/new',
loadComponent: () =>
import('./features/combat/combat-placeholder-page.component').then(
(module) => module.CombatPlaceholderPageComponent,
),
},
], ],
}, },
{ path: '**', redirectTo: 'world' }, { path: '**', redirectTo: 'world' },

View File

@@ -1,6 +1,6 @@
import { signal, WritableSignal } from '@angular/core'; import { signal, WritableSignal } from '@angular/core';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router'; import { Router, provideRouter } from '@angular/router';
import { CharacterResponse } from './core/api/game-api.models'; import { CharacterResponse } from './core/api/game-api.models';
import { WorldStore } from './features/world/world.store'; import { WorldStore } from './features/world/world.store';
import { AppShellComponent } from './layout/app-shell/app-shell.component'; import { AppShellComponent } from './layout/app-shell/app-shell.component';
@@ -19,7 +19,10 @@ describe('App', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [AppShellComponent], imports: [AppShellComponent],
providers: [ providers: [
provideRouter([]), provideRouter([
{ path: 'world', children: [] },
{ path: 'hunt', children: [] },
]),
{ {
provide: WorldStore, provide: WorldStore,
useValue: { character, currentLocation, selectedConnection }, useValue: { character, currentLocation, selectedConnection },
@@ -28,9 +31,12 @@ describe('App', () => {
}).compileComponents(); }).compileComponents();
}); });
it('renders the reusable game shell with only Karte available', () => { it('renders the reusable game shell with Karte and Jagd available', async () => {
const fixture = TestBed.createComponent(AppShellComponent); const fixture = TestBed.createComponent(AppShellComponent);
const router = TestBed.inject(Router);
await router.navigateByUrl('/world');
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
expect(element.querySelector('app-top-bar')).not.toBeNull(); expect(element.querySelector('app-top-bar')).not.toBeNull();
@@ -44,7 +50,13 @@ describe('App', () => {
expect(mapButton?.getAttribute('aria-current')).toBe('page'); expect(mapButton?.getAttribute('aria-current')).toBe('page');
expect(mapButton?.getAttribute('aria-label')).toBe('Karte'); expect(mapButton?.getAttribute('aria-label')).toBe('Karte');
for (const destination of ['hunt', 'quests', 'inventory', 'character']) { const huntButton = element.querySelector<HTMLButtonElement>('[data-navigation="hunt"]');
expect(huntButton).not.toBeNull();
expect(huntButton?.disabled).toBe(false);
expect(huntButton?.getAttribute('aria-current')).toBeNull();
expect(huntButton?.getAttribute('aria-label')).toBe('Jagd');
for (const destination of ['quests', 'inventory', 'character']) {
expect( expect(
element.querySelector<HTMLButtonElement>(`[data-navigation="${destination}"]`)?.disabled, element.querySelector<HTMLButtonElement>(`[data-navigation="${destination}"]`)?.disabled,
).toBe(true); ).toBe(true);
@@ -53,6 +65,21 @@ describe('App', () => {
expect(element.textContent).not.toContain('Shop'); expect(element.textContent).not.toContain('Shop');
}); });
it('marks Jagd as the active navigation entry while on /hunt', async () => {
const fixture = TestBed.createComponent(AppShellComponent);
const router = TestBed.inject(Router);
await router.navigateByUrl('/hunt');
fixture.detectChanges();
await fixture.whenStable();
const element = fixture.nativeElement as HTMLElement;
const mapButton = element.querySelector<HTMLButtonElement>('[data-navigation="world"]');
const huntButton = element.querySelector<HTMLButtonElement>('[data-navigation="hunt"]');
expect(huntButton?.getAttribute('aria-current')).toBe('page');
expect(mapButton?.getAttribute('aria-current')).toBeNull();
});
it('renders loaded character values supplied by the WorldStore', () => { it('renders loaded character values supplied by the WorldStore', () => {
character.set({ character.set({
id: 'character-id', id: 'character-id',

View File

@@ -0,0 +1,72 @@
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-combat-placeholder-page',
template: `
<section class="combat-placeholder" aria-label="Kampfvorbereitung">
<span class="combat-placeholder__eyebrow">KAMPF</span>
<h2>Vorbereitung auf den Kampf</h2>
<p>
Die Klinge ist gezogen, der Gegner steht bereit doch das eigentliche Gefecht liegt noch
vor dir. Diese Ansicht ist ein Zwischenhalt auf dem Weg in den Kampf, der in einem
späteren Schritt folgt.
</p>
@if (encounterId) {
<p class="combat-placeholder__id" data-encounter-id>
Vorbereitung auf den Kampf gegen Begegnung {{ encounterId }}…
</p>
}
</section>
`,
styles: [
`
:host {
display: block;
}
.combat-placeholder {
display: grid;
gap: var(--ar-space-3);
max-inline-size: 40rem;
margin: var(--ar-space-6) auto;
padding: var(--ar-space-5);
border: 1px solid var(--ar-border);
background: var(--ar-panel);
box-shadow: var(--ar-shadow-raised);
text-align: center;
}
.combat-placeholder__eyebrow {
color: var(--ar-gold);
font-size: var(--ar-font-sm);
letter-spacing: 0.14em;
text-transform: uppercase;
}
.combat-placeholder h2 {
margin: 0;
font-family: Georgia, 'Times New Roman', serif;
font-size: 1.6rem;
font-weight: 400;
}
.combat-placeholder p {
margin: 0;
color: var(--ar-text-muted);
line-height: 1.55;
}
.combat-placeholder__id {
color: var(--ar-text-muted);
font-size: var(--ar-font-sm);
font-style: italic;
}
`,
],
})
export class CombatPlaceholderPageComponent {
private readonly route = inject(ActivatedRoute);
protected readonly encounterId = this.route.snapshot.queryParamMap.get('encounterId');
}

View File

@@ -0,0 +1,67 @@
<section class="hunt-page" aria-label="Jagd">
@if (worldStore.currentLocation(); as location) {
@if (!location.huntingEnabled) {
<section class="hunt-page__unavailable">
<h2>Keine Jagd verfügbar</h2>
<p>
Am Südtor von Graufurt gibt es keine regulären Jagdgebiete. Reise in ein gefährlicheres
Gebiet, um nach Gegnern zu suchen.
</p>
<button type="button" data-hunt-to-world (click)="goToWorld()">Zur Karte</button>
</section>
} @else if (huntingStore.currentHunt(); as hunt) {
<section class="hunt-page__results" [attr.aria-label]="'Begegnungen bei ' + location.name">
<p class="hunt-page__results-heading">{{ location.name }} — Begegnungen</p>
<div class="hunt-page__encounters">
@for (encounter of huntingStore.encounters(); track encounter.id) {
<app-encounter-card [encounter]="encounter" (attack)="onAttack($event)" />
}
</div>
<div class="hunt-page__actions">
<button
type="button"
data-hunt-refresh
[disabled]="huntingStore.loading()"
(click)="refreshHunt()"
>
Neu suchen
</button>
<button type="button" data-hunt-to-world (click)="goToWorld()">Zur Karte</button>
</div>
</section>
} @else {
<section class="hunt-page__ready">
<h2>{{ location.name }}</h2>
<p>{{ location.description }}</p>
<p class="hunt-page__hint">
Durchsuche die Umgebung nach Spuren und Gegnern, bevor du dich in den Kampf wagst.
</p>
<button
type="button"
data-hunt-start
[disabled]="huntingStore.loading()"
(click)="startHunt()"
>
Jagd beginnen
</button>
</section>
}
@if (huntingStore.loading()) {
<p class="hunt-page__loading" role="status">Du suchst nach Spuren...</p>
}
} @else {
<section class="hunt-page__empty" aria-live="polite">
<p>Jagdgebiet wird vorbereitet.</p>
</section>
}
@if (huntingStore.error(); as error) {
<section class="hunt-page__error" role="alert">
<p>{{ error }}</p>
<button type="button" data-hunt-retry (click)="retry()">Erneut versuchen</button>
</section>
}
</section>

View File

@@ -0,0 +1,127 @@
:host {
display: block;
min-block-size: 100%;
}
.hunt-page {
position: relative;
min-block-size: 100%;
}
.hunt-page__unavailable,
.hunt-page__ready,
.hunt-page__results {
padding: var(--ar-space-5);
border: 1px solid var(--ar-border);
background: var(--ar-panel);
box-shadow: var(--ar-shadow-raised);
}
.hunt-page__unavailable h2,
.hunt-page__ready h2 {
margin: 0 0 var(--ar-space-3);
font-family: Georgia, 'Times New Roman', serif;
font-size: 1.6rem;
font-weight: 400;
}
.hunt-page__unavailable p,
.hunt-page__ready p {
margin: 0 0 var(--ar-space-4);
color: var(--ar-text-muted);
line-height: 1.55;
}
.hunt-page__hint {
font-style: italic;
}
.hunt-page__results-heading {
margin: 0 0 var(--ar-space-4);
color: var(--ar-gold);
font-family: Georgia, 'Times New Roman', serif;
font-size: 1.3rem;
}
.hunt-page__encounters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
gap: var(--ar-space-4);
margin-block-end: var(--ar-space-5);
}
.hunt-page__actions {
display: flex;
flex-wrap: wrap;
gap: var(--ar-space-3);
}
.hunt-page__unavailable button,
.hunt-page__ready button,
.hunt-page__actions button {
padding: var(--ar-space-2) var(--ar-space-4);
border: 1px solid var(--ar-border-highlight);
border-radius: var(--ar-radius-sm);
color: var(--ar-text);
background: linear-gradient(180deg, #263b4b, #17232d);
cursor: pointer;
font-family: Georgia, 'Times New Roman', serif;
font-size: 1rem;
}
.hunt-page__unavailable button:hover,
.hunt-page__ready button:not(:disabled):hover,
.hunt-page__actions button:not(:disabled):hover {
border-color: #d6b26b;
background: linear-gradient(180deg, #315067, #1a2c3a);
}
.hunt-page__ready button:disabled,
.hunt-page__actions button:disabled {
cursor: not-allowed;
opacity: 0.52;
}
.hunt-page__loading,
.hunt-page__error,
.hunt-page__empty {
margin: var(--ar-space-4) 0 0;
border: 1px solid var(--ar-border);
background: var(--ar-panel);
box-shadow: var(--ar-shadow-raised);
}
.hunt-page__loading,
.hunt-page__empty {
padding: var(--ar-space-4);
color: var(--ar-text-muted);
}
.hunt-page__error {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--ar-space-4);
padding: var(--ar-space-3) var(--ar-space-4);
border-color: var(--ar-danger);
}
.hunt-page__error p {
margin: 0;
}
.hunt-page__error button {
flex: 0 0 auto;
padding: var(--ar-space-2) var(--ar-space-3);
border: 1px solid var(--ar-border-highlight);
border-radius: var(--ar-radius-sm);
color: var(--ar-text);
background: #1a2023;
cursor: pointer;
}
@media (width < 720px) {
.hunt-page__encounters {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,222 @@
import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Router, provideRouter } from '@angular/router';
import { vi } from 'vitest';
import type { CurrentLocationResponse, HuntResult } from '../../../core/api/game-api.models';
import { WorldStore } from '../../world/world.store';
import { HuntingStore } from '../hunting.store';
import { HuntPageComponent } from './hunt-page.component';
const southGate: CurrentLocationResponse = {
id: 'south-gate-id',
key: 'south-gate',
name: 'Südtor von Graufurt',
description: 'Der letzte sichere Schritt vor den Aschenfeldern.',
regionKey: 'ashen-fields',
minRecommendedLevel: 1,
maxRecommendedLevel: 1,
dangerLevel: 0,
isSafe: true,
huntingEnabled: false,
artworkPath: '/images/backgrounds/Suedtor.png',
connections: [],
possibleMonsters: [],
};
const burnedRoad: CurrentLocationResponse = {
...southGate,
id: 'burned-road-id',
key: 'burned-road',
name: 'Verbrannte Straße',
description: 'Die erste Jagdzone zwischen Asche und zerbrochenen Wagen.',
isSafe: false,
huntingEnabled: true,
artworkPath: '/images/backgrounds/Aschestrasse.png',
possibleMonsters: ['Aschenratte', 'Straßenräuber'],
connections: [],
};
const threeEncounterHunt: HuntResult = {
id: 'hunt-id',
location: { id: 'burned-road-id', key: 'burned-road', name: 'Verbrannte Straße' },
encounters: [
{
id: 'encounter-1',
monster: {
key: 'ash-rat',
name: 'Aschenratte',
level: 1,
artworkPath: '/images/enemies/AshRat.png',
},
dangerRating: 'WEAK',
},
{
id: 'encounter-2',
monster: {
key: 'road-bandit',
name: 'Straßenräuber',
level: 3,
artworkPath: '/images/enemies/RoadBandit.png',
},
dangerRating: 'MATCH',
},
{
id: 'encounter-3',
monster: {
key: 'ash-rat',
name: 'Aschenratte',
level: 1,
artworkPath: '/images/enemies/AshRat.png',
},
dangerRating: 'WEAK',
},
],
};
describe('HuntPageComponent', () => {
let worldStore: {
currentLocation: ReturnType<typeof signal<CurrentLocationResponse | null>>;
};
let huntingStore: {
currentHunt: ReturnType<typeof signal<HuntResult | null>>;
loading: ReturnType<typeof signal<boolean>>;
error: ReturnType<typeof signal<string | null>>;
encounters: () => HuntResult['encounters'];
startHunt: ReturnType<typeof vi.fn>;
refreshHunt: ReturnType<typeof vi.fn>;
selectEncounter: ReturnType<typeof vi.fn>;
};
let router: Router;
async function setup(location: CurrentLocationResponse | null, hunt: HuntResult | null = null) {
worldStore = { currentLocation: signal(location) };
const currentHunt = signal(hunt);
huntingStore = {
currentHunt,
loading: signal(false),
error: signal<string | null>(null),
encounters: () => currentHunt()?.encounters ?? [],
startHunt: vi.fn(() => Promise.resolve()),
refreshHunt: vi.fn(() => Promise.resolve()),
selectEncounter: vi.fn(),
};
await TestBed.configureTestingModule({
imports: [HuntPageComponent],
providers: [
provideRouter([]),
{ provide: WorldStore, useValue: worldStore },
{ provide: HuntingStore, useValue: huntingStore },
],
}).compileComponents();
router = TestBed.inject(Router);
vi.spyOn(router, 'navigate').mockResolvedValue(true);
const fixture = TestBed.createComponent(HuntPageComponent);
fixture.detectChanges();
return fixture;
}
it('shows the hunting-unavailable state at the Südtor, with no Jagd beginnen button, and a working Zur Karte action', async () => {
const fixture = await setup(southGate);
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Keine Jagd verfügbar');
expect(element.textContent).toContain(
'Am Südtor von Graufurt gibt es keine regulären Jagdgebiete.',
);
expect(
Array.from(element.querySelectorAll('button')).some(
(button) => button.textContent?.trim() === 'Jagd beginnen',
),
).toBe(false);
const toWorldButton = element.querySelector<HTMLButtonElement>('[data-hunt-to-world]');
expect(toWorldButton?.textContent?.trim()).toBe('Zur Karte');
toWorldButton?.click();
expect(router.navigate).toHaveBeenCalledWith(['/world']);
});
it('calls startHunt when Jagd beginnen is clicked at a hunting-enabled location', async () => {
const fixture = await setup(burnedRoad);
const element = fixture.nativeElement as HTMLElement;
element.querySelector<HTMLButtonElement>('[data-hunt-start]')?.click();
expect(huntingStore.startHunt).toHaveBeenCalledOnce();
});
it('renders 3 encounter cards, duplicates included, with the correct data', async () => {
const fixture = await setup(burnedRoad, threeEncounterHunt);
const element = fixture.nativeElement as HTMLElement;
const cards = element.querySelectorAll('app-encounter-card');
expect(cards.length).toBe(3);
expect(element.textContent).toMatch(/Aschenratte[\s\S]*Straßenräuber[\s\S]*Aschenratte/);
expect(element.querySelectorAll('img[src="/images/enemies/AshRat.png"]').length).toBe(2);
expect(element.querySelectorAll('img[src="/images/enemies/RoadBandit.png"]').length).toBe(1);
expect(element.textContent).toContain('Stufe 1');
expect(element.textContent).toContain('Stufe 3');
});
it('calls refreshHunt when Neu suchen is clicked', async () => {
const fixture = await setup(burnedRoad, threeEncounterHunt);
const element = fixture.nativeElement as HTMLElement;
element.querySelector<HTMLButtonElement>('[data-hunt-refresh]')?.click();
expect(huntingStore.refreshHunt).toHaveBeenCalledOnce();
});
it('navigates to /combat/new with the encounter id (not the monster key) when Angreifen is clicked', async () => {
const fixture = await setup(burnedRoad, threeEncounterHunt);
const element = fixture.nativeElement as HTMLElement;
const attackButtons = Array.from(element.querySelectorAll('button')).filter(
(button) => button.textContent?.trim() === 'Angreifen',
);
expect(attackButtons.length).toBe(3);
attackButtons[1].click();
expect(huntingStore.selectEncounter).toHaveBeenCalledWith('encounter-2');
expect(router.navigate).toHaveBeenCalledWith(['/combat/new'], {
queryParams: { encounterId: 'encounter-2' },
});
expect(router.navigate).not.toHaveBeenCalledWith(['/combat/new'], {
queryParams: { encounterId: 'road-bandit' },
});
});
it('does not trigger a hunt automatically on page entry', async () => {
await setup(burnedRoad);
expect(huntingStore.startHunt).not.toHaveBeenCalled();
});
it('shows a loading state and disables the triggering action', async () => {
const fixture = await setup(burnedRoad);
huntingStore.loading.set(true);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Du suchst nach Spuren...');
expect(element.querySelector<HTMLButtonElement>('[data-hunt-start]')?.disabled).toBe(true);
});
it('displays a hunting error and retries via startHunt when there is no current hunt', async () => {
const fixture = await setup(burnedRoad);
huntingStore.error.set('An diesem Ort gibt es keine Jagdgebiete.');
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.querySelector('[role="alert"]')?.textContent).toContain(
'An diesem Ort gibt es keine Jagdgebiete.',
);
element.querySelector<HTMLButtonElement>('[data-hunt-retry]')?.click();
expect(huntingStore.startHunt).toHaveBeenCalledOnce();
});
});

View File

@@ -0,0 +1,42 @@
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { EncounterCardComponent } from '../encounter-card/encounter-card.component';
import { HuntingStore } from '../hunting.store';
import { WorldStore } from '../../world/world.store';
@Component({
selector: 'app-hunt-page',
imports: [EncounterCardComponent],
templateUrl: './hunt-page.component.html',
styleUrl: './hunt-page.component.scss',
})
export class HuntPageComponent {
protected readonly worldStore = inject(WorldStore);
protected readonly huntingStore = inject(HuntingStore);
private readonly router = inject(Router);
protected startHunt(): void {
void this.huntingStore.startHunt();
}
protected refreshHunt(): void {
void this.huntingStore.refreshHunt();
}
protected retry(): void {
if (this.huntingStore.currentHunt() === null) {
void this.huntingStore.startHunt();
} else {
void this.huntingStore.refreshHunt();
}
}
protected goToWorld(): void {
void this.router.navigate(['/world']);
}
protected onAttack(encounterId: string): void {
this.huntingStore.selectEncounter(encounterId);
void this.router.navigate(['/combat/new'], { queryParams: { encounterId } });
}
}

View File

@@ -30,6 +30,12 @@
<dt>Jagd</dt> <dt>Jagd</dt>
<dd>{{ location.huntingEnabled ? 'Jagd möglich' : 'Keine Jagd' }}</dd> <dd>{{ location.huntingEnabled ? 'Jagd möglich' : 'Keine Jagd' }}</dd>
</div> </div>
@if (location.huntingEnabled && location.possibleMonsters.length) {
<div>
<dt>Mögliche Begegnungen</dt>
<dd>{{ location.possibleMonsters.join(', ') }}</dd>
</div>
}
</dl> </dl>
} @else { } @else {
<span class="context-panel__eyebrow">GEBIETSINFO</span> <span class="context-panel__eyebrow">GEBIETSINFO</span>

View File

@@ -3,15 +3,7 @@ import { TestBed } from '@angular/core/testing';
import { WorldStore } from '../../features/world/world.store'; import { WorldStore } from '../../features/world/world.store';
import { ContextPanelComponent } from './context-panel.component'; import { ContextPanelComponent } from './context-panel.component';
describe('ContextPanelComponent', () => { const southGate = {
it('uses a runtime location derivative while preserving the API artwork path as image fallback', async () => {
await TestBed.configureTestingModule({
imports: [ContextPanelComponent],
providers: [
{
provide: WorldStore,
useValue: {
currentLocation: signal({
id: 'south-gate-id', id: 'south-gate-id',
key: 'south-gate', key: 'south-gate',
name: 'Südtor von Graufurt', name: 'Südtor von Graufurt',
@@ -24,7 +16,29 @@ describe('ContextPanelComponent', () => {
huntingEnabled: false, huntingEnabled: false,
artworkPath: '/images/backgrounds/Suedtor.png', artworkPath: '/images/backgrounds/Suedtor.png',
connections: [], connections: [],
}), possibleMonsters: [],
};
const burnedRoad = {
...southGate,
id: 'burned-road-id',
key: 'burned-road',
name: 'Verbrannte Straße',
isSafe: false,
huntingEnabled: true,
artworkPath: '/images/backgrounds/Aschestrasse.png',
possibleMonsters: ['Aschenratte', 'Straßenräuber'],
};
describe('ContextPanelComponent', () => {
it('uses a runtime location derivative while preserving the API artwork path as image fallback', async () => {
await TestBed.configureTestingModule({
imports: [ContextPanelComponent],
providers: [
{
provide: WorldStore,
useValue: {
currentLocation: signal(southGate),
selectedConnection: signal(null), selectedConnection: signal(null),
}, },
}, },
@@ -42,4 +56,48 @@ describe('ContextPanelComponent', () => {
'/images/backgrounds/Suedtor.png', '/images/backgrounds/Suedtor.png',
); );
}); });
it('hides "Mögliche Begegnungen" when the location has no hunting', async () => {
await TestBed.configureTestingModule({
imports: [ContextPanelComponent],
providers: [
{
provide: WorldStore,
useValue: {
currentLocation: signal(southGate),
selectedConnection: signal(null),
},
},
],
}).compileComponents();
const fixture = TestBed.createComponent(ContextPanelComponent);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).not.toContain('Mögliche Begegnungen');
});
it('lists possible monsters as plain text when hunting is enabled', async () => {
await TestBed.configureTestingModule({
imports: [ContextPanelComponent],
providers: [
{
provide: WorldStore,
useValue: {
currentLocation: signal(burnedRoad),
selectedConnection: signal(null),
},
},
],
}).compileComponents();
const fixture = TestBed.createComponent(ContextPanelComponent);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Mögliche Begegnungen');
expect(element.textContent).toContain('Aschenratte, Straßenräuber');
expect(element.textContent).not.toMatch(/\d+\s?%/);
});
}); });

View File

@@ -1,10 +1,12 @@
<nav class="side-navigation" aria-label="Spielnavigation"> <nav class="side-navigation" aria-label="Spielnavigation">
<button <button
class="side-navigation__item side-navigation__item--active" class="side-navigation__item"
type="button" type="button"
routerLink="/world" routerLink="/world"
routerLinkActive="side-navigation__item--active"
[routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page"
data-navigation="world" data-navigation="world"
aria-current="page"
aria-label="Karte" aria-label="Karte"
> >
<img src="/images/hud/runtime/MapsIcon-128.png" alt="" /> <img src="/images/hud/runtime/MapsIcon-128.png" alt="" />
@@ -14,9 +16,12 @@
<button <button
class="side-navigation__item" class="side-navigation__item"
type="button" type="button"
routerLink="/hunt"
routerLinkActive="side-navigation__item--active"
[routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page"
data-navigation="hunt" data-navigation="hunt"
disabled aria-label="Jagd"
aria-label="Jagd ist noch nicht verfügbar"
> >
<img src="/images/hud/runtime/HuntIcon-128.png" alt="" /> <img src="/images/hud/runtime/HuntIcon-128.png" alt="" />
<span>Jagd</span> <span>Jagd</span>

View File

@@ -1,9 +1,9 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterLink } from '@angular/router'; import { RouterLink, RouterLinkActive } from '@angular/router';
@Component({ @Component({
selector: 'app-side-navigation', selector: 'app-side-navigation',
imports: [RouterLink], imports: [RouterLink, RouterLinkActive],
templateUrl: './side-navigation.component.html', templateUrl: './side-navigation.component.html',
styleUrl: './side-navigation.component.scss', styleUrl: './side-navigation.component.scss',
}) })