feat(combat): rejoin the running combat instead of dead-ending
Attacking while a combat is already active returned COMBAT_ALREADY_ACTIVE and left the hunt page showing an error the player could not act on, with no way back into the fight they were already in. Add GET /api/combats/active so the client can resolve that combat, and have the hunt page navigate into it when an attack is rejected for this reason. CombatStore now also exposes the error code so callers can tell this case apart from a genuinely failed attack.
This commit is contained in:
@@ -24,24 +24,46 @@ export class CombatStore {
|
||||
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.errorState.set(null);
|
||||
this.clearError();
|
||||
|
||||
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));
|
||||
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);
|
||||
}
|
||||
@@ -49,13 +71,13 @@ export class CombatStore {
|
||||
|
||||
async loadCombat(combatId: string): Promise<void> {
|
||||
this.loadingState.set(true);
|
||||
this.errorState.set(null);
|
||||
this.clearError();
|
||||
|
||||
try {
|
||||
const combat = await firstValueFrom(this.api.getCombat(combatId));
|
||||
this.combatState.set(combat);
|
||||
} catch (error) {
|
||||
this.errorState.set(this.toErrorMessage(error));
|
||||
this.setError(error);
|
||||
} finally {
|
||||
this.loadingState.set(false);
|
||||
}
|
||||
@@ -68,13 +90,13 @@ export class CombatStore {
|
||||
}
|
||||
|
||||
this.actionPendingState.set(true);
|
||||
this.errorState.set(null);
|
||||
this.clearError();
|
||||
|
||||
try {
|
||||
const updated = await firstValueFrom(this.api.performCombatAction(combat.id, 'ATTACK'));
|
||||
this.combatState.set(updated);
|
||||
} catch (error) {
|
||||
this.errorState.set(this.toErrorMessage(error));
|
||||
this.setError(error);
|
||||
} finally {
|
||||
this.actionPendingState.set(false);
|
||||
}
|
||||
@@ -82,6 +104,20 @@ export class CombatStore {
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user