From a6450817de626ae69f8c8fb5a838db9a2c552e2a Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Wed, 19 Aug 2026 16:33:37 +0200 Subject: [PATCH] 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 --- apps/web/src/app/core/api/game-api.models.ts | 38 +++++++++++++++++++ .../src/app/core/api/game-api.service.spec.ts | 26 +++++++++++++ apps/web/src/app/core/api/game-api.service.ts | 21 +++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/core/api/game-api.models.ts b/apps/web/src/app/core/api/game-api.models.ts index cd185f2..e7646fc 100644 --- a/apps/web/src/app/core/api/game-api.models.ts +++ b/apps/web/src/app/core/api/game-api.models.ts @@ -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[]; +} diff --git a/apps/web/src/app/core/api/game-api.service.spec.ts b/apps/web/src/app/core/api/game-api.service.spec.ts index d05bf54..932aea3 100644 --- a/apps/web/src/app/core/api/game-api.service.spec.ts +++ b/apps/web/src/app/core/api/game-api.service.spec.ts @@ -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({}); + }); }); diff --git a/apps/web/src/app/core/api/game-api.service.ts b/apps/web/src/app/core/api/game-api.service.ts index 8ae3195..f3167f1 100644 --- a/apps/web/src/app/core/api/game-api.service.ts +++ b/apps/web/src/app/core/api/game-api.service.ts @@ -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 { return this.http.post('/api/hunts', {}); } + + startCombat(encounterId: string): Observable { + return this.http.post(`/api/hunt-encounters/${encounterId}/attack`, {}); + } + + getCombat(combatId: string): Observable { + return this.http.get(`/api/combats/${combatId}`); + } + + performCombatAction(combatId: string, action: CombatAction): Observable { + return this.http.post(`/api/combats/${combatId}/actions`, { action }); + } }