The screen a player stands on between activities: name, region, scene artwork with hotspots pinned by percentage, a four-button action bar and a context sidebar covering identity, danger, encounters, interactions and rewards. It owns no knowledge of any particular place. Hotspots and actions are routed by interaction type: HUNT and MAP hand off to the existing hunt and map screens, and everything that reveals text goes through the server-authoritative interaction endpoint. A second location therefore renders by supplying different content, which the Südtor case in the page spec exercises. The shell drops its generic area rail on /location, where the screen's own sidebar says the same thing better, and Ort joins the navigation as its first entry. Root and unknown routes now land on the location rather than the map: arriving somewhere should mean arriving at a place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
import { Component, OnInit, inject } from '@angular/core';
|
|
import { Router } from '@angular/router';
|
|
import {
|
|
LocationInteractionType,
|
|
LocationPointOfInterest,
|
|
LocationPrimaryAction,
|
|
} from '../../../core/api/game-api.models';
|
|
import { LocalLocationStore } from '../local-location.store';
|
|
import { LocationInteractionPanelComponent } from '../location-interaction-panel/location-interaction-panel.component';
|
|
import { LocationIconComponent } from '../location-icon/location-icon.component';
|
|
import { LocationPoiComponent } from '../location-poi/location-poi.component';
|
|
import { LocationSidebarComponent } from '../location-sidebar/location-sidebar.component';
|
|
|
|
const RUNTIME_ARTWORK: Readonly<Record<string, string>> = {
|
|
'/images/backgrounds/Suedtor.png': '/images/backgrounds/runtime/Suedtor-960.jpg',
|
|
'/images/backgrounds/Aschestrasse.png': '/images/backgrounds/runtime/Aschestrasse-960.jpg',
|
|
};
|
|
|
|
/**
|
|
* The screen the player stands on between activities.
|
|
*
|
|
* It renders whatever the current location's content describes and owns no
|
|
* knowledge of any particular place. Hunting and travel are not reimplemented
|
|
* here: HUNT and MAP hotspots hand off to the existing screens, and everything
|
|
* that reveals text goes through the server-authoritative interaction endpoint.
|
|
*/
|
|
@Component({
|
|
selector: 'app-location-page',
|
|
imports: [
|
|
LocationIconComponent,
|
|
LocationInteractionPanelComponent,
|
|
LocationPoiComponent,
|
|
LocationSidebarComponent,
|
|
],
|
|
templateUrl: './location-page.component.html',
|
|
styleUrl: './location-page.component.scss',
|
|
})
|
|
export class LocationPageComponent implements OnInit {
|
|
protected readonly store = inject(LocalLocationStore);
|
|
private readonly router = inject(Router);
|
|
|
|
ngOnInit(): void {
|
|
if (this.store.location() === null) {
|
|
void this.store.load();
|
|
}
|
|
}
|
|
|
|
protected retry(): void {
|
|
void this.store.load();
|
|
}
|
|
|
|
protected activatePoi(poi: LocationPointOfInterest): void {
|
|
this.dispatch(poi.type, poi.key);
|
|
}
|
|
|
|
protected activateAction(action: LocationPrimaryAction): void {
|
|
this.dispatch(action.type, action.poiKey ?? action.key);
|
|
}
|
|
|
|
protected runtimeArtwork(artworkPath: string): string | undefined {
|
|
return RUNTIME_ARTWORK[artworkPath];
|
|
}
|
|
|
|
/** True while this control's own interaction is in flight. */
|
|
protected isPending(key: string | undefined): boolean {
|
|
return key !== undefined && this.store.interactionPending() === key;
|
|
}
|
|
|
|
protected get busy(): boolean {
|
|
return this.store.interactionPending() !== null;
|
|
}
|
|
|
|
/**
|
|
* Routes a hotspot or action by its interaction type. Navigation types leave
|
|
* for the screen that owns them; every other type asks the server what
|
|
* happened. Unimplemented types are ignored rather than faked.
|
|
*/
|
|
private dispatch(type: LocationInteractionType, interactionKey: string): void {
|
|
switch (type) {
|
|
case 'HUNT':
|
|
void this.router.navigate(['/hunt']);
|
|
return;
|
|
case 'MAP':
|
|
case 'TRAVEL':
|
|
void this.router.navigate(['/world']);
|
|
return;
|
|
case 'INVESTIGATE':
|
|
case 'SEARCH':
|
|
case 'NPC':
|
|
void this.store.runInteraction(interactionKey);
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
}
|