Files
ashen-realms/apps/web/src/app/features/combat/combat.store.ts
Bastian Wagner 5586e6c672 feat(web): translate error messages surfaced from API error codes to English
Translates the five client-side error-message maps (world, hunting, combat,
inventory, local-location stores) that re-translate already-English API
error codes back to display text, per design doc §4. Also fixes four
component specs that hard-coded the same German strings when asserting
rendered error text (combat-page, hunt-page, location-interaction-panel,
location-page).
2026-08-21 21:29:10 +02:00

134 lines
4.3 KiB
TypeScript

import { HttpErrorResponse } from '@angular/common/http';
import { Injectable, signal } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { Combat, CombatAction } from '../../core/api/game-api.models';
import { GameApiService } from '../../core/api/game-api.service';
const GENERIC_ERROR_MESSAGE = 'Could not load the combat.';
// 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: 'This encounter could not be found.',
HUNT_ENCOUNTER_ALREADY_CONSUMED: 'This encounter has already been used.',
INVALID_HUNT_ENCOUNTER: 'This encounter is no longer valid.',
CHARACTER_TRAVELLING: "You can't fight while travelling.",
CHARACTER_TOO_WOUNDED: "You're too badly wounded to fight. Wait until you've recovered.",
COMBAT_ALREADY_ACTIVE: "You're already in a combat.",
COMBAT_NOT_FOUND: 'This combat could not be found.',
COMBAT_ALREADY_FINISHED: 'This combat has already ended.',
COMBAT_NO_POTIONS_REMAINING: 'You have no potions left.',
};
@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);
private readonly errorCodeState = signal<string | null>(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<void> {
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<Combat | null> {
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<void> {
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 performAction(action: CombatAction): Promise<void> {
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, action));
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;
}
}