feat(combat): add frontend Combat models and API methods

Adds Combat domain models (Combat, CombatPlayer, CombatMonster, CombatEvent)
and three GameApiService methods to interact with backend combat endpoints:
- startCombat(encounterId): POST /api/hunt-encounters/:id/attack
- getCombat(combatId): GET /api/combats/:id
- performCombatAction(combatId, action): POST /api/combats/:id/actions

Includes test coverage for all three methods.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-19 16:33:37 +02:00
parent bb42195ef5
commit a6450817de
3 changed files with 84 additions and 1 deletions

View File

@@ -68,3 +68,41 @@ export interface HuntResult {
location: LocationSummary;
encounters: HuntEncounter[];
}
export type CombatStatus = 'ACTIVE' | 'WON' | 'LOST';
export type CombatEventType = 'DAMAGE' | 'COMBAT_WON' | 'COMBAT_LOST';
export type CombatSide = 'PLAYER' | 'MONSTER';
export type CombatAction = 'ATTACK';
export interface CombatEvent {
round: number;
sequence: number;
type: CombatEventType;
source: CombatSide;
target: CombatSide;
amount?: number;
}
export interface CombatPlayer {
name: string;
maxHp: number;
currentHp: number;
}
export interface CombatMonster {
key: string;
name: string;
level: number;
maxHp: number;
currentHp: number;
artworkPath: string;
}
export interface Combat {
id: string;
status: CombatStatus;
round: number;
player: CombatPlayer;
monster: CombatMonster;
events: CombatEvent[];
}

View File

@@ -47,4 +47,30 @@ describe('GameApiService', () => {
expect(request.request.body).toEqual({ targetLocationId: 'target-uuid' });
request.flush({ status: 'IDLE' });
});
it('posts to the encounter-scoped attack endpoint with an empty body to start a combat', () => {
service.startCombat('encounter-uuid').subscribe();
const request = http.expectOne('/api/hunt-encounters/encounter-uuid/attack');
expect(request.request.method).toBe('POST');
expect(request.request.body).toEqual({});
request.flush({});
});
it('gets a combat by id', () => {
service.getCombat('combat-uuid').subscribe();
const request = http.expectOne('/api/combats/combat-uuid');
expect(request.request.method).toBe('GET');
request.flush({});
});
it('posts only the action enum when performing a combat action', () => {
service.performCombatAction('combat-uuid', 'ATTACK').subscribe();
const request = http.expectOne('/api/combats/combat-uuid/actions');
expect(request.request.method).toBe('POST');
expect(request.request.body).toEqual({ action: 'ATTACK' });
request.flush({});
});
});

View File

@@ -1,7 +1,14 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { CharacterResponse, CurrentLocationResponse, CurrentTravel, HuntResult } from './game-api.models';
import {
CharacterResponse,
Combat,
CombatAction,
CurrentLocationResponse,
CurrentTravel,
HuntResult,
} from './game-api.models';
@Injectable({ providedIn: 'root' })
export class GameApiService {
@@ -26,4 +33,16 @@ export class GameApiService {
startHunt(): Observable<HuntResult> {
return this.http.post<HuntResult>('/api/hunts', {});
}
startCombat(encounterId: string): Observable<Combat> {
return this.http.post<Combat>(`/api/hunt-encounters/${encounterId}/attack`, {});
}
getCombat(combatId: string): Observable<Combat> {
return this.http.get<Combat>(`/api/combats/${combatId}`);
}
performCombatAction(combatId: string, action: CombatAction): Observable<Combat> {
return this.http.post<Combat>(`/api/combats/${combatId}/actions`, { action });
}
}