feat(combat): start real combats from the hunt page and navigate to /combat/:combatId
This commit is contained in:
@@ -64,4 +64,11 @@
|
|||||||
<button type="button" data-hunt-retry (click)="retry()">Erneut versuchen</button>
|
<button type="button" data-hunt-retry (click)="retry()">Erneut versuchen</button>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (combatStore.error(); as combatError) {
|
||||||
|
<section class="hunt-page__error" role="alert">
|
||||||
|
<p>{{ combatError }}</p>
|
||||||
|
<button type="button" data-hunt-combat-dismiss (click)="dismissCombatError()">Schließen</button>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
// apps/web/src/app/features/hunting/hunt-page/hunt-page.component.spec.ts
|
||||||
import { signal } from '@angular/core';
|
import { signal } from '@angular/core';
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { Router, provideRouter } from '@angular/router';
|
import { Router, provideRouter } from '@angular/router';
|
||||||
import { vi } from 'vitest';
|
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 { WorldStore } from '../../world/world.store';
|
||||||
import { HuntingStore } from '../hunting.store';
|
import { HuntingStore } from '../hunting.store';
|
||||||
import { HuntPageComponent } from './hunt-page.component';
|
import { HuntPageComponent } from './hunt-page.component';
|
||||||
@@ -42,12 +44,7 @@ const threeEncounterHunt: HuntResult = {
|
|||||||
encounters: [
|
encounters: [
|
||||||
{
|
{
|
||||||
id: 'encounter-1',
|
id: 'encounter-1',
|
||||||
monster: {
|
monster: { key: 'ash-rat', name: 'Aschenratte', level: 1, artworkPath: '/images/enemies/AshRat.png' },
|
||||||
key: 'ash-rat',
|
|
||||||
name: 'Aschenratte',
|
|
||||||
level: 1,
|
|
||||||
artworkPath: '/images/enemies/AshRat.png',
|
|
||||||
},
|
|
||||||
dangerRating: 'WEAK',
|
dangerRating: 'WEAK',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -62,17 +59,28 @@ const threeEncounterHunt: HuntResult = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'encounter-3',
|
id: 'encounter-3',
|
||||||
monster: {
|
monster: { key: 'ash-rat', name: 'Aschenratte', level: 1, artworkPath: '/images/enemies/AshRat.png' },
|
||||||
key: 'ash-rat',
|
|
||||||
name: 'Aschenratte',
|
|
||||||
level: 1,
|
|
||||||
artworkPath: '/images/enemies/AshRat.png',
|
|
||||||
},
|
|
||||||
dangerRating: 'WEAK',
|
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', () => {
|
describe('HuntPageComponent', () => {
|
||||||
let worldStore: {
|
let worldStore: {
|
||||||
currentLocation: ReturnType<typeof signal<CurrentLocationResponse | null>>;
|
currentLocation: ReturnType<typeof signal<CurrentLocationResponse | null>>;
|
||||||
@@ -87,6 +95,12 @@ describe('HuntPageComponent', () => {
|
|||||||
refreshHunt: ReturnType<typeof vi.fn>;
|
refreshHunt: ReturnType<typeof vi.fn>;
|
||||||
selectEncounter: ReturnType<typeof vi.fn>;
|
selectEncounter: ReturnType<typeof vi.fn>;
|
||||||
};
|
};
|
||||||
|
let combatStore: {
|
||||||
|
combat: ReturnType<typeof signal<Combat | null>>;
|
||||||
|
error: ReturnType<typeof signal<string | null>>;
|
||||||
|
startCombat: ReturnType<typeof vi.fn>;
|
||||||
|
clearError: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
let router: Router;
|
let router: Router;
|
||||||
|
|
||||||
async function setup(location: CurrentLocationResponse | null, hunt: HuntResult | null = null) {
|
async function setup(location: CurrentLocationResponse | null, hunt: HuntResult | null = null) {
|
||||||
@@ -101,6 +115,12 @@ describe('HuntPageComponent', () => {
|
|||||||
refreshHunt: vi.fn(() => Promise.resolve()),
|
refreshHunt: vi.fn(() => Promise.resolve()),
|
||||||
selectEncounter: vi.fn(),
|
selectEncounter: vi.fn(),
|
||||||
};
|
};
|
||||||
|
combatStore = {
|
||||||
|
combat: signal<Combat | null>(null),
|
||||||
|
error: signal<string | null>(null),
|
||||||
|
startCombat: vi.fn(() => Promise.resolve()),
|
||||||
|
clearError: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [HuntPageComponent],
|
imports: [HuntPageComponent],
|
||||||
@@ -108,6 +128,7 @@ describe('HuntPageComponent', () => {
|
|||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: WorldStore, useValue: worldStore },
|
{ provide: WorldStore, useValue: worldStore },
|
||||||
{ provide: HuntingStore, useValue: huntingStore },
|
{ provide: HuntingStore, useValue: huntingStore },
|
||||||
|
{ provide: CombatStore, useValue: combatStore },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
@@ -124,9 +145,7 @@ describe('HuntPageComponent', () => {
|
|||||||
const element = fixture.nativeElement as HTMLElement;
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
|
||||||
expect(element.textContent).toContain('Keine Jagd verfügbar');
|
expect(element.textContent).toContain('Keine Jagd verfügbar');
|
||||||
expect(element.textContent).toContain(
|
expect(element.textContent).toContain('Am Südtor von Graufurt gibt es keine regulären Jagdgebiete.');
|
||||||
'Am Südtor von Graufurt gibt es keine regulären Jagdgebiete.',
|
|
||||||
);
|
|
||||||
expect(
|
expect(
|
||||||
Array.from(element.querySelectorAll('button')).some(
|
Array.from(element.querySelectorAll('button')).some(
|
||||||
(button) => button.textContent?.trim() === 'Jagd beginnen',
|
(button) => button.textContent?.trim() === 'Jagd beginnen',
|
||||||
@@ -171,8 +190,11 @@ describe('HuntPageComponent', () => {
|
|||||||
expect(huntingStore.refreshHunt).toHaveBeenCalledOnce();
|
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);
|
const fixture = await setup(burnedRoad, threeEncounterHunt);
|
||||||
|
combatStore.startCombat.mockImplementation(async () => {
|
||||||
|
combatStore.combat.set(startedCombat);
|
||||||
|
});
|
||||||
const element = fixture.nativeElement as HTMLElement;
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
|
||||||
const attackButtons = Array.from(element.querySelectorAll('button')).filter(
|
const attackButtons = Array.from(element.querySelectorAll('button')).filter(
|
||||||
@@ -181,14 +203,43 @@ describe('HuntPageComponent', () => {
|
|||||||
expect(attackButtons.length).toBe(3);
|
expect(attackButtons.length).toBe(3);
|
||||||
|
|
||||||
attackButtons[1].click();
|
attackButtons[1].click();
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
expect(huntingStore.selectEncounter).toHaveBeenCalledWith('encounter-2');
|
expect(combatStore.startCombat).toHaveBeenCalledWith('encounter-2');
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['/combat/new'], {
|
expect(combatStore.startCombat).not.toHaveBeenCalledWith('road-bandit');
|
||||||
queryParams: { encounterId: 'encounter-2' },
|
expect(router.navigate).toHaveBeenCalledWith(['/combat', 'combat-2']);
|
||||||
});
|
});
|
||||||
expect(router.navigate).not.toHaveBeenCalledWith(['/combat/new'], {
|
|
||||||
queryParams: { encounterId: 'road-bandit' },
|
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<HTMLButtonElement>('[data-hunt-combat-dismiss]')?.click();
|
||||||
|
|
||||||
|
expect(combatStore.clearError).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not trigger a hunt automatically on page entry', async () => {
|
it('does not trigger a hunt automatically on page entry', async () => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
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 { EncounterCardComponent } from '../encounter-card/encounter-card.component';
|
||||||
import { HuntingStore } from '../hunting.store';
|
import { HuntingStore } from '../hunting.store';
|
||||||
import { WorldStore } from '../../world/world.store';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hunt-page',
|
selector: 'app-hunt-page',
|
||||||
@@ -13,6 +14,7 @@ import { WorldStore } from '../../world/world.store';
|
|||||||
export class HuntPageComponent implements OnInit {
|
export class HuntPageComponent implements OnInit {
|
||||||
protected readonly worldStore = inject(WorldStore);
|
protected readonly worldStore = inject(WorldStore);
|
||||||
protected readonly huntingStore = inject(HuntingStore);
|
protected readonly huntingStore = inject(HuntingStore);
|
||||||
|
protected readonly combatStore = inject(CombatStore);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -41,8 +43,15 @@ export class HuntPageComponent implements OnInit {
|
|||||||
void this.router.navigate(['/world']);
|
void this.router.navigate(['/world']);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onAttack(encounterId: string): void {
|
protected async onAttack(encounterId: string): Promise<void> {
|
||||||
this.huntingStore.selectEncounter(encounterId);
|
await this.combatStore.startCombat(encounterId);
|
||||||
void this.router.navigate(['/combat/new'], { queryParams: { encounterId } });
|
const combat = this.combatStore.combat();
|
||||||
|
if (combat) {
|
||||||
|
void this.router.navigate(['/combat', combat.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected dismissCombatError(): void {
|
||||||
|
this.combatStore.clearError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user