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) { + + } 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(); } }