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).
134 lines
4.3 KiB
TypeScript
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;
|
|
}
|
|
}
|