diff --git a/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.html b/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.html
index 77b1d87..5ec2587 100644
--- a/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.html
+++ b/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.html
@@ -64,4 +64,11 @@
}
+
+ @if (combatStore.error(); as combatError) {
+
+ {{ combatError }}
+
+
+ }
diff --git a/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.spec.ts b/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.spec.ts
index 5452bc1..430ff18 100644
--- a/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.spec.ts
+++ b/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.spec.ts
@@ -1,8 +1,10 @@
+// apps/web/src/app/features/hunting/hunt-page/hunt-page.component.spec.ts
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 type { Combat, CurrentLocationResponse, HuntResult } from '../../../core/api/game-api.models';
+import { CombatStore } from '../../combat/combat.store';
import { WorldStore } from '../../world/world.store';
import { HuntingStore } from '../hunting.store';
import { HuntPageComponent } from './hunt-page.component';
@@ -42,12 +44,7 @@ const threeEncounterHunt: HuntResult = {
encounters: [
{
id: 'encounter-1',
- monster: {
- key: 'ash-rat',
- name: 'Aschenratte',
- level: 1,
- artworkPath: '/images/enemies/AshRat.png',
- },
+ monster: { key: 'ash-rat', name: 'Aschenratte', level: 1, artworkPath: '/images/enemies/AshRat.png' },
dangerRating: 'WEAK',
},
{
@@ -62,17 +59,28 @@ const threeEncounterHunt: HuntResult = {
},
{
id: 'encounter-3',
- monster: {
- key: 'ash-rat',
- name: 'Aschenratte',
- level: 1,
- artworkPath: '/images/enemies/AshRat.png',
- },
+ monster: { key: 'ash-rat', name: 'Aschenratte', level: 1, artworkPath: '/images/enemies/AshRat.png' },
dangerRating: 'WEAK',
},
],
};
+const startedCombat: Combat = {
+ id: 'combat-2',
+ status: 'ACTIVE',
+ round: 1,
+ player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100 },
+ monster: {
+ key: 'road-bandit',
+ name: 'Straßenräuber',
+ level: 3,
+ maxHp: 75,
+ currentHp: 75,
+ artworkPath: '/images/enemies/RoadBandit.png',
+ },
+ events: [],
+};
+
describe('HuntPageComponent', () => {
let worldStore: {
currentLocation: ReturnType>;
@@ -87,6 +95,12 @@ describe('HuntPageComponent', () => {
refreshHunt: ReturnType;
selectEncounter: ReturnType;
};
+ let combatStore: {
+ combat: ReturnType>;
+ error: ReturnType>;
+ startCombat: ReturnType;
+ clearError: ReturnType;
+ };
let router: Router;
async function setup(location: CurrentLocationResponse | null, hunt: HuntResult | null = null) {
@@ -101,6 +115,12 @@ describe('HuntPageComponent', () => {
refreshHunt: vi.fn(() => Promise.resolve()),
selectEncounter: vi.fn(),
};
+ combatStore = {
+ combat: signal(null),
+ error: signal(null),
+ startCombat: vi.fn(() => Promise.resolve()),
+ clearError: vi.fn(),
+ };
await TestBed.configureTestingModule({
imports: [HuntPageComponent],
@@ -108,6 +128,7 @@ describe('HuntPageComponent', () => {
provideRouter([]),
{ provide: WorldStore, useValue: worldStore },
{ provide: HuntingStore, useValue: huntingStore },
+ { provide: CombatStore, useValue: combatStore },
],
}).compileComponents();
@@ -124,9 +145,7 @@ describe('HuntPageComponent', () => {
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(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',
@@ -171,8 +190,11 @@ describe('HuntPageComponent', () => {
expect(huntingStore.refreshHunt).toHaveBeenCalledOnce();
});
- it('navigates to /combat/new with the encounter id (not the monster key) when Angreifen is clicked', async () => {
+ it('starts a real combat from the encounter id (not the monster key) and navigates to /combat/:combatId', async () => {
const fixture = await setup(burnedRoad, threeEncounterHunt);
+ combatStore.startCombat.mockImplementation(async () => {
+ combatStore.combat.set(startedCombat);
+ });
const element = fixture.nativeElement as HTMLElement;
const attackButtons = Array.from(element.querySelectorAll('button')).filter(
@@ -181,14 +203,43 @@ describe('HuntPageComponent', () => {
expect(attackButtons.length).toBe(3);
attackButtons[1].click();
+ await Promise.resolve();
+ await Promise.resolve();
- 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' },
- });
+ expect(combatStore.startCombat).toHaveBeenCalledWith('encounter-2');
+ expect(combatStore.startCombat).not.toHaveBeenCalledWith('road-bandit');
+ expect(router.navigate).toHaveBeenCalledWith(['/combat', 'combat-2']);
+ });
+
+ it('does not navigate when starting the combat fails', 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',
+ );
+ attackButtons[0].click();
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(combatStore.startCombat).toHaveBeenCalledWith('encounter-1');
+ expect(router.navigate).not.toHaveBeenCalledWith(['/combat', expect.anything()]);
+ });
+
+ it('shows a combat-start error and dismisses it', async () => {
+ const fixture = await setup(burnedRoad, threeEncounterHunt);
+ combatStore.error.set('Du befindest dich bereits in einem Kampf.');
+ fixture.detectChanges();
+
+ const element = fixture.nativeElement as HTMLElement;
+ const alerts = Array.from(element.querySelectorAll('[role="alert"]'));
+ expect(alerts.some((alert) => alert.textContent?.includes('Du befindest dich bereits in einem Kampf.'))).toBe(
+ true,
+ );
+
+ element.querySelector('[data-hunt-combat-dismiss]')?.click();
+
+ expect(combatStore.clearError).toHaveBeenCalledOnce();
});
it('does not trigger a hunt automatically on page entry', async () => {
diff --git a/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.ts b/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.ts
index 0f60dfd..476f7cc 100644
--- a/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.ts
+++ b/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.ts
@@ -1,8 +1,9 @@
import { Component, OnInit, inject } from '@angular/core';
import { Router } from '@angular/router';
+import { CombatStore } from '../../combat/combat.store';
+import { WorldStore } from '../../world/world.store';
import { EncounterCardComponent } from '../encounter-card/encounter-card.component';
import { HuntingStore } from '../hunting.store';
-import { WorldStore } from '../../world/world.store';
@Component({
selector: 'app-hunt-page',
@@ -13,6 +14,7 @@ import { WorldStore } from '../../world/world.store';
export class HuntPageComponent implements OnInit {
protected readonly worldStore = inject(WorldStore);
protected readonly huntingStore = inject(HuntingStore);
+ protected readonly combatStore = inject(CombatStore);
private readonly router = inject(Router);
ngOnInit(): void {
@@ -41,8 +43,15 @@ export class HuntPageComponent implements OnInit {
void this.router.navigate(['/world']);
}
- protected onAttack(encounterId: string): void {
- this.huntingStore.selectEncounter(encounterId);
- void this.router.navigate(['/combat/new'], { queryParams: { encounterId } });
+ protected async onAttack(encounterId: string): Promise {
+ await this.combatStore.startCombat(encounterId);
+ const combat = this.combatStore.combat();
+ if (combat) {
+ void this.router.navigate(['/combat', combat.id]);
+ }
+ }
+
+ protected dismissCombatError(): void {
+ this.combatStore.clearError();
}
}