feat(combat): add CombatStore
Signal-based store wrapping GameApiService.startCombat/getCombat/ performCombatAction with loading/error/actionPending state, following the HuntingStore/WorldStore pattern. startCombat failures clear any previously-loaded combat; loadCombat/attack failures preserve the last-known-good combat. attack() guards against re-entrancy and no-loaded-combat calls. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
95
apps/web/src/app/features/combat/combat.store.ts
Normal file
95
apps/web/src/app/features/combat/combat.store.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
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<Record<string, string>> = {
|
||||
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<Combat | null>(null);
|
||||
private readonly loadingState = signal(false);
|
||||
private readonly actionPendingState = signal(false);
|
||||
private readonly errorState = signal<string | null>(null);
|
||||
|
||||
readonly combat = this.combatState.asReadonly();
|
||||
readonly loading = this.loadingState.asReadonly();
|
||||
readonly actionPending = this.actionPendingState.asReadonly();
|
||||
readonly error = this.errorState.asReadonly();
|
||||
|
||||
constructor(private readonly api: GameApiService) {}
|
||||
|
||||
async startCombat(encounterId: string): Promise<void> {
|
||||
this.loadingState.set(true);
|
||||
this.errorState.set(null);
|
||||
|
||||
try {
|
||||
const combat = await firstValueFrom(this.api.startCombat(encounterId));
|
||||
this.combatState.set(combat);
|
||||
} catch (error) {
|
||||
this.combatState.set(null);
|
||||
this.errorState.set(this.toErrorMessage(error));
|
||||
} finally {
|
||||
this.loadingState.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
async loadCombat(combatId: string): Promise<void> {
|
||||
this.loadingState.set(true);
|
||||
this.errorState.set(null);
|
||||
|
||||
try {
|
||||
const combat = await firstValueFrom(this.api.getCombat(combatId));
|
||||
this.combatState.set(combat);
|
||||
} catch (error) {
|
||||
this.errorState.set(this.toErrorMessage(error));
|
||||
} finally {
|
||||
this.loadingState.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
async attack(): Promise<void> {
|
||||
const combat = this.combatState();
|
||||
if (!combat || this.actionPendingState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPendingState.set(true);
|
||||
this.errorState.set(null);
|
||||
|
||||
try {
|
||||
const updated = await firstValueFrom(this.api.performCombatAction(combat.id, 'ATTACK'));
|
||||
this.combatState.set(updated);
|
||||
} catch (error) {
|
||||
this.errorState.set(this.toErrorMessage(error));
|
||||
} finally {
|
||||
this.actionPendingState.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
clearError(): void {
|
||||
this.errorState.set(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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user