feat(web): add local location contracts, shared fixtures and content glyphs
Mirrors the extended current-location payload and the interaction endpoint in the web API client. Replaces the per-spec CurrentLocationResponse literals with one shared fixture factory, so a contract change is answered in a single place. Adds the drawn glyph set used by hotspots, actions and reward previews. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,69 @@ export interface CurrentLocationConnection {
|
|||||||
danger: 'LOW' | 'HIGH';
|
danger: 'LOW' | 'HIGH';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LocationInteractionType =
|
||||||
|
| 'HUNT'
|
||||||
|
| 'INVESTIGATE'
|
||||||
|
| 'SEARCH'
|
||||||
|
| 'NPC'
|
||||||
|
| 'MAP'
|
||||||
|
| 'TRAVEL'
|
||||||
|
| 'SHOP'
|
||||||
|
| 'QUEST'
|
||||||
|
| 'BOSS'
|
||||||
|
| 'DUNGEON';
|
||||||
|
|
||||||
|
export type LocationType =
|
||||||
|
| 'SAFE_HUB'
|
||||||
|
| 'TRANSITION'
|
||||||
|
| 'HUNTING_GROUND'
|
||||||
|
| 'QUEST_LOCATION'
|
||||||
|
| 'OUTPOST'
|
||||||
|
| 'ELITE_ZONE'
|
||||||
|
| 'BOSS_LOCATION'
|
||||||
|
| 'DUNGEON_ENTRANCE';
|
||||||
|
|
||||||
|
export interface LocationPointOfInterest {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
actionLabel?: string;
|
||||||
|
type: LocationInteractionType;
|
||||||
|
iconKey: string;
|
||||||
|
xPercent: number;
|
||||||
|
yPercent: number;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationPrimaryAction {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
type: LocationInteractionType;
|
||||||
|
iconKey: string;
|
||||||
|
enabled: boolean;
|
||||||
|
/** Set when the action reveals the same result as a hotspot on the artwork. */
|
||||||
|
poiKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EncounterPreview {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
iconPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RewardPreview {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
iconKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationInteractionResult {
|
||||||
|
interactionKey: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CurrentLocationResponse {
|
export interface CurrentLocationResponse {
|
||||||
id: string;
|
id: string;
|
||||||
key: string;
|
key: string;
|
||||||
@@ -33,6 +96,18 @@ export interface CurrentLocationResponse {
|
|||||||
isSafe: boolean;
|
isSafe: boolean;
|
||||||
huntingEnabled: boolean;
|
huntingEnabled: boolean;
|
||||||
artworkPath: string;
|
artworkPath: string;
|
||||||
|
regionName: string;
|
||||||
|
regionTierLabel: string;
|
||||||
|
locationType: LocationType;
|
||||||
|
localDescription: string;
|
||||||
|
localArtworkPath: string;
|
||||||
|
/** `null` where nothing hostile can be met — the view reads that as safe. */
|
||||||
|
dangerRating: DangerRating | null;
|
||||||
|
recommendationLabel: string;
|
||||||
|
pointsOfInterest: LocationPointOfInterest[];
|
||||||
|
primaryActions: LocationPrimaryAction[];
|
||||||
|
encounterPreview: EncounterPreview[];
|
||||||
|
rewardPreview: RewardPreview[];
|
||||||
connections: CurrentLocationConnection[];
|
connections: CurrentLocationConnection[];
|
||||||
possibleMonsters: string[];
|
possibleMonsters: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,24 @@ describe('GameApiService', () => {
|
|||||||
request.flush({ status: 'IDLE' });
|
request.flush({ status: 'IDLE' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('posts a local interaction by key alone, never naming a location', () => {
|
||||||
|
service.runLocationInteraction('inspect-tracks').subscribe();
|
||||||
|
|
||||||
|
const request = http.expectOne('/api/world/current-location/interactions/inspect-tracks');
|
||||||
|
expect(request.request.method).toBe('POST');
|
||||||
|
expect(request.request.body).toEqual({});
|
||||||
|
request.flush({ interactionKey: 'inspect-tracks', title: 'Spuren', text: '…' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('escapes an interaction key so it cannot break out of its path segment', () => {
|
||||||
|
service.runLocationInteraction('a/../b').subscribe();
|
||||||
|
|
||||||
|
const request = http.expectOne(
|
||||||
|
'/api/world/current-location/interactions/a%2F..%2Fb',
|
||||||
|
);
|
||||||
|
request.flush({ interactionKey: 'a/../b', title: '', text: '' });
|
||||||
|
});
|
||||||
|
|
||||||
it('posts to the encounter-scoped attack endpoint with an empty body to start a combat', () => {
|
it('posts to the encounter-scoped attack endpoint with an empty body to start a combat', () => {
|
||||||
service.startCombat('encounter-uuid').subscribe();
|
service.startCombat('encounter-uuid').subscribe();
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CurrentLocationResponse,
|
CurrentLocationResponse,
|
||||||
CurrentTravel,
|
CurrentTravel,
|
||||||
HuntResult,
|
HuntResult,
|
||||||
|
LocationInteractionResult,
|
||||||
} from './game-api.models';
|
} from './game-api.models';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -22,6 +23,17 @@ export class GameApiService {
|
|||||||
return this.http.get<CurrentLocationResponse>('/api/world/current-location');
|
return this.http.get<CurrentLocationResponse>('/api/world/current-location');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a local hotspot interaction. Only the key travels: the server pairs
|
||||||
|
* it with the character's actual location.
|
||||||
|
*/
|
||||||
|
runLocationInteraction(interactionKey: string): Observable<LocationInteractionResult> {
|
||||||
|
return this.http.post<LocationInteractionResult>(
|
||||||
|
`/api/world/current-location/interactions/${encodeURIComponent(interactionKey)}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
startTravel(targetLocationId: string): Observable<CurrentTravel> {
|
startTravel(targetLocationId: string): Observable<CurrentTravel> {
|
||||||
return this.http.post<CurrentTravel>('/api/travel', { targetLocationId });
|
return this.http.post<CurrentTravel>('/api/travel', { targetLocationId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,38 +5,17 @@ import { Router, provideRouter } from '@angular/router';
|
|||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import type { Combat, CurrentLocationResponse, HuntResult } from '../../../core/api/game-api.models';
|
import type { Combat, CurrentLocationResponse, HuntResult } from '../../../core/api/game-api.models';
|
||||||
import { CombatStore } from '../../combat/combat.store';
|
import { CombatStore } from '../../combat/combat.store';
|
||||||
|
import {
|
||||||
|
burnedRoadFixture,
|
||||||
|
southGateFixture,
|
||||||
|
} from '../../world/current-location.fixture';
|
||||||
import { WorldStore } from '../../world/world.store';
|
import { WorldStore } from '../../world/world.store';
|
||||||
import { HuntingStore } from '../hunting.store';
|
import { HuntingStore } from '../hunting.store';
|
||||||
import { HuntPageComponent } from './hunt-page.component';
|
import { HuntPageComponent } from './hunt-page.component';
|
||||||
|
|
||||||
const southGate: CurrentLocationResponse = {
|
const southGate = southGateFixture({ connections: [] });
|
||||||
id: 'south-gate-id',
|
|
||||||
key: 'south-gate',
|
|
||||||
name: 'Südtor von Graufurt',
|
|
||||||
description: 'Der letzte sichere Schritt vor den Aschenfeldern.',
|
|
||||||
regionKey: 'ashen-fields',
|
|
||||||
minRecommendedLevel: 1,
|
|
||||||
maxRecommendedLevel: 1,
|
|
||||||
dangerLevel: 0,
|
|
||||||
isSafe: true,
|
|
||||||
huntingEnabled: false,
|
|
||||||
artworkPath: '/images/backgrounds/Suedtor.png',
|
|
||||||
connections: [],
|
|
||||||
possibleMonsters: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const burnedRoad: CurrentLocationResponse = {
|
const burnedRoad = burnedRoadFixture({ connections: [] });
|
||||||
...southGate,
|
|
||||||
id: 'burned-road-id',
|
|
||||||
key: 'burned-road',
|
|
||||||
name: 'Verbrannte Straße',
|
|
||||||
description: 'Die erste Jagdzone zwischen Asche und zerbrochenen Wagen.',
|
|
||||||
isSafe: false,
|
|
||||||
huntingEnabled: true,
|
|
||||||
artworkPath: '/images/backgrounds/Aschestrasse.png',
|
|
||||||
possibleMonsters: ['Aschenratte', 'Straßenräuber'],
|
|
||||||
connections: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const threeEncounterHunt: HuntResult = {
|
const threeEncounterHunt: HuntResult = {
|
||||||
id: 'hunt-id',
|
id: 'hunt-id',
|
||||||
|
|||||||
190
apps/web/src/app/features/world/current-location.fixture.ts
Normal file
190
apps/web/src/app/features/world/current-location.fixture.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import type {
|
||||||
|
CurrentLocationResponse,
|
||||||
|
LocationPointOfInterest,
|
||||||
|
LocationPrimaryAction,
|
||||||
|
} from '../../core/api/game-api.models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test fixtures for `GET /api/world/current-location`.
|
||||||
|
*
|
||||||
|
* Shared rather than re-declared per spec: the payload backs the map, the
|
||||||
|
* hunt screen and the local location view, so a field added to the contract
|
||||||
|
* needs to be answered in exactly one place.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const BURNED_ROAD_POIS: LocationPointOfInterest[] = [
|
||||||
|
{
|
||||||
|
key: 'hunt-area',
|
||||||
|
title: 'Jagdgebiet',
|
||||||
|
actionLabel: 'Jagd beginnen',
|
||||||
|
type: 'HUNT',
|
||||||
|
iconKey: 'hunt',
|
||||||
|
xPercent: 52,
|
||||||
|
yPercent: 44,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'wounded-scout',
|
||||||
|
title: 'Verwundeter Kundschafter',
|
||||||
|
actionLabel: 'Sprechen',
|
||||||
|
type: 'NPC',
|
||||||
|
iconKey: 'speak',
|
||||||
|
xPercent: 20,
|
||||||
|
yPercent: 60,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'inspect-tracks',
|
||||||
|
title: 'Verdächtige Spuren',
|
||||||
|
actionLabel: 'Untersuchen',
|
||||||
|
type: 'INVESTIGATE',
|
||||||
|
iconKey: 'investigate',
|
||||||
|
xPercent: 32,
|
||||||
|
yPercent: 78,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'search-abandoned-wagon',
|
||||||
|
title: 'Verlassener Wagen',
|
||||||
|
actionLabel: 'Durchsuchen',
|
||||||
|
type: 'SEARCH',
|
||||||
|
iconKey: 'search',
|
||||||
|
xPercent: 80,
|
||||||
|
yPercent: 68,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BURNED_ROAD_ACTIONS: LocationPrimaryAction[] = [
|
||||||
|
{
|
||||||
|
key: 'start-hunt',
|
||||||
|
label: 'Jagd beginnen',
|
||||||
|
description: 'Im Gebiet jagen',
|
||||||
|
type: 'HUNT',
|
||||||
|
iconKey: 'hunt',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'investigate-tracks',
|
||||||
|
label: 'Spuren untersuchen',
|
||||||
|
description: 'Hinweise finden',
|
||||||
|
type: 'INVESTIGATE',
|
||||||
|
iconKey: 'investigate',
|
||||||
|
enabled: true,
|
||||||
|
poiKey: 'inspect-tracks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'search-surroundings',
|
||||||
|
label: 'Umgebung durchsuchen',
|
||||||
|
description: 'Beute finden',
|
||||||
|
type: 'SEARCH',
|
||||||
|
iconKey: 'search',
|
||||||
|
enabled: true,
|
||||||
|
poiKey: 'search-abandoned-wagon',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'open-map',
|
||||||
|
label: 'Zur Karte',
|
||||||
|
description: 'Gebiet wechseln',
|
||||||
|
type: 'MAP',
|
||||||
|
iconKey: 'map',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function southGateFixture(
|
||||||
|
overrides: Partial<CurrentLocationResponse> = {},
|
||||||
|
): CurrentLocationResponse {
|
||||||
|
return {
|
||||||
|
id: 'south-gate-id',
|
||||||
|
key: 'south-gate',
|
||||||
|
name: 'Südtor von Graufurt',
|
||||||
|
description: 'Der letzte sichere Schritt vor den Aschenfeldern.',
|
||||||
|
regionKey: 'ashen-fields',
|
||||||
|
minRecommendedLevel: 1,
|
||||||
|
maxRecommendedLevel: 1,
|
||||||
|
dangerLevel: 0,
|
||||||
|
isSafe: true,
|
||||||
|
huntingEnabled: false,
|
||||||
|
artworkPath: '/images/backgrounds/Suedtor.png',
|
||||||
|
regionName: 'Aschenfelder',
|
||||||
|
regionTierLabel: 'Gebiet 1',
|
||||||
|
locationType: 'TRANSITION',
|
||||||
|
localDescription: 'Hinter den Wachtfeuern beginnen die Aschenfelder.',
|
||||||
|
localArtworkPath: '/images/backgrounds/Suedtor.png',
|
||||||
|
dangerRating: null,
|
||||||
|
recommendationLabel: '1',
|
||||||
|
pointsOfInterest: [],
|
||||||
|
primaryActions: [],
|
||||||
|
encounterPreview: [],
|
||||||
|
rewardPreview: [],
|
||||||
|
connections: [
|
||||||
|
{
|
||||||
|
targetLocation: {
|
||||||
|
id: 'burned-road-id',
|
||||||
|
key: 'burned-road',
|
||||||
|
name: 'Verbrannte Straße',
|
||||||
|
},
|
||||||
|
travelDurationSeconds: 10,
|
||||||
|
danger: 'LOW',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
possibleMonsters: [],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function burnedRoadFixture(
|
||||||
|
overrides: Partial<CurrentLocationResponse> = {},
|
||||||
|
): CurrentLocationResponse {
|
||||||
|
return southGateFixture({
|
||||||
|
id: 'burned-road-id',
|
||||||
|
key: 'burned-road',
|
||||||
|
name: 'Verbrannte Straße',
|
||||||
|
description: 'Die erste Jagdzone zwischen Asche und zerbrochenen Wagen.',
|
||||||
|
maxRecommendedLevel: 2,
|
||||||
|
dangerLevel: 1,
|
||||||
|
isSafe: false,
|
||||||
|
huntingEnabled: true,
|
||||||
|
artworkPath: '/images/backgrounds/Aschestrasse.png',
|
||||||
|
locationType: 'HUNTING_GROUND',
|
||||||
|
localDescription:
|
||||||
|
'Ein alter Handelsweg, der durch Feuer und Krieg in Asche gelegt wurde.',
|
||||||
|
localArtworkPath: '/images/backgrounds/Aschestrasse.png',
|
||||||
|
dangerRating: 'MATCH',
|
||||||
|
recommendationLabel: '1–2',
|
||||||
|
pointsOfInterest: BURNED_ROAD_POIS,
|
||||||
|
primaryActions: BURNED_ROAD_ACTIONS,
|
||||||
|
encounterPreview: [
|
||||||
|
{
|
||||||
|
key: 'ash-rat',
|
||||||
|
name: 'Aschenratte',
|
||||||
|
level: 1,
|
||||||
|
iconPath: '/images/monsters/icons/ash-rat-128.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'road-bandit',
|
||||||
|
name: 'Straßenräuber',
|
||||||
|
level: 2,
|
||||||
|
iconPath: '/images/monsters/icons/road-bandit-128.png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rewardPreview: [
|
||||||
|
{ key: 'silver', label: 'Silber', iconKey: 'silver' },
|
||||||
|
{ key: 'experience', label: 'Erfahrung', iconKey: 'experience' },
|
||||||
|
],
|
||||||
|
connections: [
|
||||||
|
{
|
||||||
|
targetLocation: {
|
||||||
|
id: 'south-gate-id',
|
||||||
|
key: 'south-gate',
|
||||||
|
name: 'Südtor von Graufurt',
|
||||||
|
},
|
||||||
|
travelDurationSeconds: 10,
|
||||||
|
danger: 'LOW',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
possibleMonsters: ['Aschenratte', 'Straßenräuber'],
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Line glyphs for local location content, addressed by the `iconKey` the
|
||||||
|
* server stores alongside a hotspot, action or reward.
|
||||||
|
*
|
||||||
|
* Drawn rather than loaded: these sit on top of artwork at sizes from 16px to
|
||||||
|
* 40px, where a downscaled bitmap turns to mush, and a new location can use an
|
||||||
|
* existing key without anyone exporting an asset. Swapping a key for a painted
|
||||||
|
* medallion later is a one-line change here.
|
||||||
|
*/
|
||||||
|
const GLYPHS: Readonly<Record<string, string>> = {
|
||||||
|
// Crossed hunting arrows.
|
||||||
|
hunt: 'M4 20 19 5M15 5h4v4M20 20 5 5M9 5H5v4',
|
||||||
|
// Magnifying glass over a trail.
|
||||||
|
investigate: 'M11 4a6 6 0 1 0 0 12 6 6 0 0 0 0-12M15.5 15.5 21 21',
|
||||||
|
// Lidded chest.
|
||||||
|
search: 'M3 9h18v11H3zM3 9l2-4h14l2 4M12 9v11M10 12h4',
|
||||||
|
// Speech bubble.
|
||||||
|
speak: 'M4 5h16v11h-9l-5 4v-4H4z',
|
||||||
|
// Compass rose.
|
||||||
|
map: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M15.5 8.5l-2 5-5 2 2-5z',
|
||||||
|
// Struck coin.
|
||||||
|
silver: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M12 7l1.7 3.3L17 12l-3.3 1.7L12 17l-1.7-3.3L7 12l3.3-1.7z',
|
||||||
|
// Rising rune.
|
||||||
|
experience: 'M12 3 4 12l8 9 8-9zM12 8v8M9 11l3-3 3 3',
|
||||||
|
// Travel marker, for locations whose hotspots lead onward.
|
||||||
|
travel: 'M12 3a6 6 0 0 1 6 6c0 4.5-6 12-6 12S6 13.5 6 9a6 6 0 0 1 6-6M12 7a2 2 0 1 0 0 4 2 2 0 0 0 0-4',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FALLBACK_GLYPH = 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M12 8v5M12 16h.01';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-location-icon',
|
||||||
|
template: `
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path [attr.d]="glyph" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
inline-size: 1.5rem;
|
||||||
|
block-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentcolor;
|
||||||
|
stroke-width: 1.4;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class LocationIconComponent {
|
||||||
|
@Input({ required: true }) iconKey!: string;
|
||||||
|
|
||||||
|
protected get glyph(): string {
|
||||||
|
return GLYPHS[this.iconKey] ?? FALLBACK_GLYPH;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
CurrentLocationResponse,
|
CurrentLocationResponse,
|
||||||
CurrentTravel,
|
CurrentTravel,
|
||||||
} from '../../core/api/game-api.models';
|
} from '../../core/api/game-api.models';
|
||||||
|
import { burnedRoadFixture, southGateFixture } from './current-location.fixture';
|
||||||
import { WorldStore } from './world.store';
|
import { WorldStore } from './world.store';
|
||||||
import { WorldPageComponent } from './world-page.component';
|
import { WorldPageComponent } from './world-page.component';
|
||||||
|
|
||||||
@@ -19,40 +20,9 @@ const burnedRoadConnection: CurrentLocationConnection = {
|
|||||||
danger: 'LOW',
|
danger: 'LOW',
|
||||||
};
|
};
|
||||||
|
|
||||||
const southGate: CurrentLocationResponse = {
|
const southGate = southGateFixture({ connections: [burnedRoadConnection] });
|
||||||
id: 'south-gate-id',
|
|
||||||
key: 'south-gate',
|
|
||||||
name: 'Südtor von Graufurt',
|
|
||||||
description: 'Der letzte sichere Schritt vor den Aschenfeldern.',
|
|
||||||
regionKey: 'ashen-fields',
|
|
||||||
minRecommendedLevel: 1,
|
|
||||||
maxRecommendedLevel: 1,
|
|
||||||
dangerLevel: 0,
|
|
||||||
isSafe: true,
|
|
||||||
huntingEnabled: false,
|
|
||||||
artworkPath: '/images/backgrounds/Suedtor.png',
|
|
||||||
connections: [burnedRoadConnection],
|
|
||||||
possibleMonsters: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const burnedRoad: CurrentLocationResponse = {
|
const burnedRoad = burnedRoadFixture();
|
||||||
...southGate,
|
|
||||||
id: 'burned-road-id',
|
|
||||||
key: 'burned-road',
|
|
||||||
name: 'Verbrannte Straße',
|
|
||||||
description: 'Die erste Jagdzone zwischen Asche und zerbrochenen Wagen.',
|
|
||||||
isSafe: false,
|
|
||||||
huntingEnabled: true,
|
|
||||||
artworkPath: '/images/backgrounds/Aschestrasse.png',
|
|
||||||
possibleMonsters: ['goblin', 'skeleton'],
|
|
||||||
connections: [
|
|
||||||
{
|
|
||||||
targetLocation: { id: 'south-gate-id', key: 'south-gate', name: 'Südtor von Graufurt' },
|
|
||||||
travelDurationSeconds: 10,
|
|
||||||
danger: 'LOW',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('WorldPageComponent', () => {
|
describe('WorldPageComponent', () => {
|
||||||
let selectedConnection: ReturnType<typeof signal<CurrentLocationConnection | null>>;
|
let selectedConnection: ReturnType<typeof signal<CurrentLocationConnection | null>>;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
CurrentTravel,
|
CurrentTravel,
|
||||||
} from '../../core/api/game-api.models';
|
} from '../../core/api/game-api.models';
|
||||||
import { GameApiService } from '../../core/api/game-api.service';
|
import { GameApiService } from '../../core/api/game-api.service';
|
||||||
|
import { southGateFixture } from './current-location.fixture';
|
||||||
import { WorldStore } from './world.store';
|
import { WorldStore } from './world.store';
|
||||||
|
|
||||||
const character: CharacterResponse = {
|
const character: CharacterResponse = {
|
||||||
@@ -21,19 +22,11 @@ const character: CharacterResponse = {
|
|||||||
currentLocation: { id: 'origin-id', key: 'south-gate', name: 'Südtor' },
|
currentLocation: { id: 'origin-id', key: 'south-gate', name: 'Südtor' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentLocation: CurrentLocationResponse = {
|
const currentLocation = southGateFixture({
|
||||||
id: 'origin-id',
|
id: 'origin-id',
|
||||||
key: 'south-gate',
|
|
||||||
name: 'Südtor',
|
name: 'Südtor',
|
||||||
description: 'Der Ausgang zur Wildnis.',
|
description: 'Der Ausgang zur Wildnis.',
|
||||||
regionKey: 'ashen-fields',
|
|
||||||
minRecommendedLevel: 1,
|
|
||||||
maxRecommendedLevel: 1,
|
|
||||||
dangerLevel: 1,
|
dangerLevel: 1,
|
||||||
isSafe: true,
|
|
||||||
huntingEnabled: false,
|
|
||||||
artworkPath: '/images/backgrounds/Suedtor.png',
|
|
||||||
possibleMonsters: [],
|
|
||||||
connections: [
|
connections: [
|
||||||
{
|
{
|
||||||
targetLocation: {
|
targetLocation: {
|
||||||
@@ -45,7 +38,7 @@ const currentLocation: CurrentLocationResponse = {
|
|||||||
danger: 'LOW',
|
danger: 'LOW',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
});
|
||||||
|
|
||||||
const travelling: CurrentTravel = {
|
const travelling: CurrentTravel = {
|
||||||
status: 'TRAVELLING',
|
status: 'TRAVELLING',
|
||||||
|
|||||||
Reference in New Issue
Block a user