From 40d69693bacd44067e5a747b626076ae3cb99714 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Sun, 23 Aug 2026 16:56:48 +0200 Subject: [PATCH] feat(web): announce a newly discovered route --- apps/web/src/app/core/api/game-api.models.ts | 7 ++++ .../world/local-location.store.spec.ts | 32 ++++++++++++++++ .../features/world/local-location.store.ts | 11 ++++-- .../location-interaction-panel.component.html | 5 +++ .../location-interaction-panel.component.scss | 8 ++++ ...cation-interaction-panel.component.spec.ts | 37 ++++++++++++++++++- 6 files changed, 96 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/core/api/game-api.models.ts b/apps/web/src/app/core/api/game-api.models.ts index 3fd4052..23e3661 100644 --- a/apps/web/src/app/core/api/game-api.models.ts +++ b/apps/web/src/app/core/api/game-api.models.ts @@ -89,6 +89,13 @@ export interface LocationInteractionResult { title: string; text: string; img?: string; + /** + * Set only the first time an interaction reveals a route. Optional on the + * client although the API always sends it: every existing test fixture + * builds this object literally, and a required field would break them all + * for no gain. + */ + discoveredLocation?: { key: string; name: string } | null; } export interface CurrentLocationResponse { diff --git a/apps/web/src/app/features/world/local-location.store.spec.ts b/apps/web/src/app/features/world/local-location.store.spec.ts index 5f8680c..5ecd837 100644 --- a/apps/web/src/app/features/world/local-location.store.spec.ts +++ b/apps/web/src/app/features/world/local-location.store.spec.ts @@ -122,6 +122,38 @@ describe('LocalLocationStore', () => { expect(store.interactionResult()).toBeNull(); }); + it('reloads the location when the interaction reveals a route', async () => { + const load = vi.fn().mockResolvedValue(undefined); + const store = setup( + { + runLocationInteraction: vi.fn().mockReturnValue( + of({ ...trackResult, discoveredLocation: { key: 'ash-pit', name: 'Ash Pit' } }), + ), + }, + { load }, + ); + + await store.runInteraction('inspect-tracks'); + + expect(load).toHaveBeenCalledTimes(1); + }); + + it('does not reload the location when nothing new was revealed', async () => { + const load = vi.fn().mockResolvedValue(undefined); + const store = setup( + { + runLocationInteraction: vi.fn().mockReturnValue( + of({ ...trackResult, discoveredLocation: null }), + ), + }, + { load }, + ); + + await store.runInteraction('inspect-tracks'); + + expect(load).not.toHaveBeenCalled(); + }); + it('resolves the hotspot an action mirrors', () => { const store = setup(); diff --git a/apps/web/src/app/features/world/local-location.store.ts b/apps/web/src/app/features/world/local-location.store.ts index 7aea055..03a9231 100644 --- a/apps/web/src/app/features/world/local-location.store.ts +++ b/apps/web/src/app/features/world/local-location.store.ts @@ -74,9 +74,14 @@ export class LocalLocationStore { this.interactionErrorState.set(null); try { - this.interactionResultState.set( - await firstValueFrom(this.api.runLocationInteraction(interactionKey)), - ); + const result = await firstValueFrom(this.api.runLocationInteraction(interactionKey)); + this.interactionResultState.set(result); + + if (result.discoveredLocation) { + // The connection list is server-filtered, so a fresh reveal only + // shows up after the location is re-read. + await this.load(); + } } catch (error) { this.interactionErrorState.set(this.toErrorMessage(error)); } finally { diff --git a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.html b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.html index 660653d..d7387f5 100644 --- a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.html +++ b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.html @@ -20,6 +20,11 @@

{{ result.title }}

{{ result.text }}

+ @if (result.discoveredLocation; as discovered) { +

+ New route discovered: {{ discovered.name }}. +

+ }
} @else { diff --git a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.scss b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.scss index 150808f..b4ee03c 100644 --- a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.scss +++ b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.scss @@ -63,6 +63,14 @@ color: var(--ar-danger); } +.interaction-panel__discovery { + margin: 0; + color: var(--ar-success); + font-size: 0.95rem; + font-weight: 700; + line-height: 1.55; +} + .interaction-panel__close { justify-self: end; padding: var(--ar-space-2) var(--ar-space-5); diff --git a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts index f874d31..735b7ac 100644 --- a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts +++ b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts @@ -2,7 +2,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LocationInteractionPanelComponent } from './location-interaction-panel.component'; async function setup(inputs: { - result?: { interactionKey: string; title: string; text: string; img?: string } | null; + result?: + | { + interactionKey: string; + title: string; + text: string; + img?: string; + discoveredLocation?: { key: string; name: string } | null; + } + | null; error?: string | null; }): Promise<{ fixture: ComponentFixture; @@ -110,4 +118,31 @@ describe('LocationInteractionPanelComponent', () => { expect(element.querySelector('[data-interaction-panel]')).toBeNull(); }); + + it('says so when the interaction revealed a route', async () => { + const { element } = await setup({ + result: { + interactionKey: 'inspect-watchpost', + title: 'The Watchpost', + text: 'Fresh tracks lead east.', + discoveredLocation: { key: 'ash-pit', name: 'Ash Pit' }, + }, + }); + + const banner = element.querySelector('[data-discovered-location]'); + expect(banner?.textContent).toContain('Ash Pit'); + }); + + it('stays quiet when nothing new was revealed', async () => { + const { element } = await setup({ + result: { + interactionKey: 'search-guard-quarters', + title: 'Guard Quarters', + text: 'Nothing but ash.', + discoveredLocation: null, + }, + }); + + expect(element.querySelector('[data-discovered-location]')).toBeNull(); + }); });