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 { GameApiService } from '../../core/api/game-api.service'; const GENERIC_ERROR_MESSAGE = 'Der Kampf konnte nicht geladen werden.'; // Mirrors the combat error codes returned by the combat endpoints. // Unknown/missing codes fall back to `GENERIC_ERROR_MESSAGE`. const COMBAT_ERROR_MESSAGES: Readonly> = { HUNT_ENCOUNTER_NOT_FOUND: 'Diese Begegnung wurde nicht gefunden.', HUNT_ENCOUNTER_ALREADY_CONSUMED: 'Diese Begegnung wurde bereits genutzt.', INVALID_HUNT_ENCOUNTER: 'Diese Begegnung ist nicht mehr gültig.', CHARACTER_TRAVELLING: 'Du kannst nicht kämpfen, während du unterwegs bist.', 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.', }; @Injectable({ providedIn: 'root' }) export class CombatStore { private readonly combatState = signal(null); private readonly loadingState = signal(false); private readonly actionPendingState = signal(false); private readonly errorState = signal(null); private readonly errorCodeState = signal(null); readonly combat = this.combatState.asReadonly(); readonly loading = this.loadingState.asReadonly(); readonly actionPending = this.actionPendingState.asReadonly(); readonly error = this.errorState.asReadonly(); readonly errorCode = this.errorCodeState.asReadonly(); constructor(private readonly api: GameApiService) {} async startCombat(encounterId: string): Promise { this.loadingState.set(true); this.clearError(); try { const combat = await firstValueFrom(this.api.startCombat(encounterId)); this.combatState.set(combat); } catch (error) { this.combatState.set(null); this.setError(error); } finally { this.loadingState.set(false); } } // Resolves the combat the character is already in, so an attack rejected with // COMBAT_ALREADY_ACTIVE can rejoin that fight instead of dead-ending. async loadActiveCombat(): Promise { this.loadingState.set(true); try { const combat = await firstValueFrom(this.api.getActiveCombat()); if (combat) { this.combatState.set(combat); this.clearError(); } return combat; } catch (error) { this.setError(error); return null; } finally { this.loadingState.set(false); } } async loadCombat(combatId: string): Promise { this.loadingState.set(true); this.clearError(); try { const combat = await firstValueFrom(this.api.getCombat(combatId)); this.combatState.set(combat); } catch (error) { this.setError(error); } finally { this.loadingState.set(false); } } async attack(): Promise { const combat = this.combatState(); if (!combat || this.actionPendingState()) { return; } this.actionPendingState.set(true); this.clearError(); try { const updated = await firstValueFrom(this.api.performCombatAction(combat.id, 'ATTACK')); this.combatState.set(updated); } catch (error) { this.setError(error); } finally { this.actionPendingState.set(false); } } clearError(): void { this.errorState.set(null); this.errorCodeState.set(null); } private setError(error: unknown): void { this.errorCodeState.set(this.toErrorCode(error)); this.errorState.set(this.toErrorMessage(error)); } private toErrorCode(error: unknown): string | null { if (error instanceof HttpErrorResponse) { return (error.error as { code?: string } | null)?.code ?? null; } return null; } private toErrorMessage(error: unknown): string { if (error instanceof HttpErrorResponse) { const code = (error.error as { code?: string } | null)?.code; return (code && COMBAT_ERROR_MESSAGES[code]) || GENERIC_ERROR_MESSAGE; } return error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE; } }