113 lines
3.6 KiB
TypeScript
113 lines
3.6 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, poi.npcKey);
|
|
}
|
|
|
|
protected activateAction(action: LocationPrimaryAction): void {
|
|
this.dispatch(action.type, action.poiKey ?? action.key, action.npcKey);
|
|
}
|
|
|
|
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,
|
|
npcKey?: 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':
|
|
// A hotspot that names an NPC opens that person's screen; one that
|
|
// does not is scenery with a line of authored text behind it, and
|
|
// still goes through the interaction endpoint.
|
|
if (npcKey) {
|
|
void this.router.navigate(['/npc', npcKey]);
|
|
return;
|
|
}
|
|
void this.store.runInteraction(interactionKey);
|
|
return;
|
|
case 'SHOP':
|
|
if (npcKey) {
|
|
void this.router.navigate(['/npc', npcKey]);
|
|
}
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
}
|