import { HttpErrorResponse } from '@angular/common/http'; import { Injectable, computed, inject, signal } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { LocationInteractionResult, LocationPointOfInterest, LocationPrimaryAction, } from '../../core/api/game-api.models'; import { GameApiService } from '../../core/api/game-api.service'; import { WorldStore } from './world.store'; const GENERIC_INTERACTION_ERROR = "This action isn't possible right now."; // Mirrors the codes in `apps/api/src/world/world.errors.ts`. const INTERACTION_ERROR_MESSAGES: Readonly> = { LOCATION_INTERACTION_UNAVAILABLE: "There's nothing to discover here.", CHARACTER_NOT_FOUND: 'Your character could not be found.', }; /** * State for the local location view. * * Location data comes from `WorldStore`, not from a second fetch path: that * store already settles due travel before answering, so arriving at a place * and looking around cannot disagree about where the character is. This store * adds only what is local to the screen — the open interaction result. */ @Injectable({ providedIn: 'root' }) export class LocalLocationStore { private readonly api = inject(GameApiService); private readonly worldStore = inject(WorldStore); private readonly interactionResultState = signal(null); private readonly interactionErrorState = signal(null); private readonly interactionPendingState = signal(null); readonly location = this.worldStore.currentLocation; readonly loading = this.worldStore.loading; readonly error = this.worldStore.error; readonly interactionResult = this.interactionResultState.asReadonly(); readonly interactionError = this.interactionErrorState.asReadonly(); /** Key of the interaction currently in flight, so only that control busies. */ readonly interactionPending = this.interactionPendingState.asReadonly(); readonly interactionOpen = computed( () => this.interactionResultState() !== null || this.interactionErrorState() !== null, ); load(): Promise { return this.worldStore.load(); } /** * Runs a hotspot or action that reveals text. Navigation types (HUNT, MAP) * never reach here — the page routes those itself. */ async runInteraction(interactionKey: string): Promise { if (this.interactionPendingState() !== null) { return; } this.interactionPendingState.set(interactionKey); this.interactionResultState.set(null); this.interactionErrorState.set(null); try { this.interactionResultState.set( await firstValueFrom(this.api.runLocationInteraction(interactionKey)), ); } catch (error) { this.interactionErrorState.set(this.toErrorMessage(error)); } finally { this.interactionPendingState.set(null); } } closeInteraction(): void { this.interactionResultState.set(null); this.interactionErrorState.set(null); } /** The hotspot an action mirrors, so an action bar entry can highlight it. */ interactionKeyOf( action: LocationPrimaryAction | LocationPointOfInterest, ): string | undefined { return 'poiKey' in action ? action.poiKey : action.key; } private toErrorMessage(error: unknown): string { if (error instanceof HttpErrorResponse) { const code = (error.error as { code?: string } | null)?.code; return (code && INTERACTION_ERROR_MESSAGES[code]) || GENERIC_INTERACTION_ERROR; } return GENERIC_INTERACTION_ERROR; } }