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 1fdd763..c1cc082 100644 --- a/apps/web/src/app/core/api/game-api.models.ts +++ b/apps/web/src/app/core/api/game-api.models.ts @@ -149,9 +149,10 @@ export interface HuntResult { } export type CombatStatus = 'ACTIVE' | 'WON' | 'LOST'; -export type CombatEventType = 'DAMAGE' | 'COMBAT_WON' | 'COMBAT_LOST'; +export type CombatEventType = 'DAMAGE' | 'HEAL' | 'DEFEND' | 'TELEGRAPH' | 'INTERRUPT' | 'COMBAT_WON' | 'COMBAT_LOST'; export type CombatSide = 'PLAYER' | 'MONSTER'; -export type CombatAction = 'ATTACK'; +export type CombatAction = 'ATTACK' | 'HEAVY_STRIKE' | 'SHIELD_BASH' | 'DEFEND' | 'POTION'; +export type CombatMonsterIntent = 'HEAVY_ATTACK'; export interface CombatEvent { round: number; @@ -166,6 +167,8 @@ export interface CombatPlayer { name: string; maxHp: number; currentHp: number; + potionsRemaining: number; + potionsMax: number; } export interface CombatMonster { @@ -175,6 +178,7 @@ export interface CombatMonster { maxHp: number; currentHp: number; artworkPath: string; + pendingIntent: CombatMonsterIntent | null; } export interface Combat { diff --git a/apps/web/src/app/core/resume-combat.spec.ts b/apps/web/src/app/core/resume-combat.spec.ts index 2c7f64f..0f4303a 100644 --- a/apps/web/src/app/core/resume-combat.spec.ts +++ b/apps/web/src/app/core/resume-combat.spec.ts @@ -9,7 +9,7 @@ const runningCombat: Combat = { id: 'combat-running', status: 'ACTIVE', round: 4, - player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 62 }, + player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 62, potionsRemaining: 2, potionsMax: 2 }, monster: { key: 'road-bandit', name: 'Straßenräuber', @@ -17,6 +17,7 @@ const runningCombat: Combat = { maxHp: 75, currentHp: 30, artworkPath: '/images/enemies/RoadBandit.png', + pendingIntent: null, }, events: [], rewards: null, diff --git a/apps/web/src/app/features/combat/combat.store.spec.ts b/apps/web/src/app/features/combat/combat.store.spec.ts index 4a2a1ee..5c0acda 100644 --- a/apps/web/src/app/features/combat/combat.store.spec.ts +++ b/apps/web/src/app/features/combat/combat.store.spec.ts @@ -10,7 +10,7 @@ const startedCombat: Combat = { id: 'combat-1', status: 'ACTIVE', round: 1, - player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100 }, + player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100, potionsRemaining: 2, potionsMax: 2 }, monster: { key: 'ash-rat', name: 'Aschenratte', @@ -18,6 +18,7 @@ const startedCombat: Combat = { maxHp: 45, currentHp: 45, artworkPath: '/images/monsters/ash-rat.png', + pendingIntent: null, }, events: [], rewards: null, @@ -139,14 +140,22 @@ describe('CombatStore', () => { it('sends only the ATTACK action and replaces combat with the server response', async () => { await store.startCombat('encounter-1'); - await store.attack(); + await store.performAction('ATTACK'); expect(api.performCombatAction).toHaveBeenCalledWith('combat-1', 'ATTACK'); expect(store.combat()).toEqual(afterAttack); }); + it('sends whichever action is requested', async () => { + await store.startCombat('encounter-1'); + + await store.performAction('HEAVY_STRIKE'); + + expect(api.performCombatAction).toHaveBeenCalledWith('combat-1', 'HEAVY_STRIKE'); + }); + it('does nothing when attacking without a loaded combat', async () => { - await store.attack(); + await store.performAction('ATTACK'); expect(api.performCombatAction).not.toHaveBeenCalled(); }); @@ -162,9 +171,9 @@ describe('CombatStore', () => { ), ); - const first = store.attack(); + const first = store.performAction('ATTACK'); expect(store.actionPending()).toBe(true); - const second = store.attack(); + const second = store.performAction('ATTACK'); resolveAttack(afterAttack); await Promise.all([first, second]); @@ -176,7 +185,7 @@ describe('CombatStore', () => { await store.startCombat('encounter-1'); api.performCombatAction.mockReturnValue(throwError(() => new Error('Netzwerkfehler'))); - await store.attack(); + await store.performAction('ATTACK'); expect(store.actionPending()).toBe(false); expect(store.combat()).toEqual(startedCombat); diff --git a/apps/web/src/app/features/combat/combat.store.ts b/apps/web/src/app/features/combat/combat.store.ts index 3ca086a..db18682 100644 --- a/apps/web/src/app/features/combat/combat.store.ts +++ b/apps/web/src/app/features/combat/combat.store.ts @@ -1,7 +1,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Injectable, signal } from '@angular/core'; import { firstValueFrom } from 'rxjs'; -import { Combat } from '../../core/api/game-api.models'; +import { Combat, CombatAction } from '../../core/api/game-api.models'; import { GameApiService } from '../../core/api/game-api.service'; const GENERIC_ERROR_MESSAGE = 'Der Kampf konnte nicht geladen werden.'; @@ -16,6 +16,7 @@ const COMBAT_ERROR_MESSAGES: Readonly> = { COMBAT_ALREADY_ACTIVE: 'Du befindest dich bereits in einem Kampf.', COMBAT_NOT_FOUND: 'Dieser Kampf wurde nicht gefunden.', COMBAT_ALREADY_FINISHED: 'Dieser Kampf ist bereits beendet.', + COMBAT_NO_POTIONS_REMAINING: 'Du hast keine Tränke mehr.', }; @Injectable({ providedIn: 'root' }) @@ -83,7 +84,7 @@ export class CombatStore { } } - async attack(): Promise { + async performAction(action: CombatAction): Promise { const combat = this.combatState(); if (!combat || this.actionPendingState()) { return; @@ -93,7 +94,7 @@ export class CombatStore { this.clearError(); try { - const updated = await firstValueFrom(this.api.performCombatAction(combat.id, 'ATTACK')); + const updated = await firstValueFrom(this.api.performCombatAction(combat.id, action)); this.combatState.set(updated); } catch (error) { this.setError(error); 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 0ff4a32..d3c76c8 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 @@ -51,7 +51,7 @@ const startedCombat: Combat = { id: 'combat-2', status: 'ACTIVE', round: 1, - player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100 }, + player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100, potionsRemaining: 2, potionsMax: 2 }, monster: { key: 'road-bandit', name: 'Straßenräuber', @@ -59,6 +59,7 @@ const startedCombat: Combat = { maxHp: 75, currentHp: 75, artworkPath: '/images/enemies/RoadBandit.png', + pendingIntent: null, }, events: [], rewards: null,