feat(api): reveal the ash pit route from the watchpost hotspot
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
} from '../database/seeds/vertical-slice.constants';
|
} from '../database/seeds/vertical-slice.constants';
|
||||||
import { LocationMonster } from '../monsters/entities/location-monster.entity';
|
import { LocationMonster } from '../monsters/entities/location-monster.entity';
|
||||||
import { TravelService } from '../travel/travel.service';
|
import { TravelService } from '../travel/travel.service';
|
||||||
|
import { WorldDiscoveryService } from './discovery/world-discovery.service';
|
||||||
import { LocationConnection } from './entities/location-connection.entity';
|
import { LocationConnection } from './entities/location-connection.entity';
|
||||||
import { LocationDefinition } from './entities/location-definition.entity';
|
import { LocationDefinition } from './entities/location-definition.entity';
|
||||||
import type { LocationPointOfInterestContent } from './local-location.types';
|
import type { LocationPointOfInterestContent } from './local-location.types';
|
||||||
@@ -92,9 +93,51 @@ function createService(
|
|||||||
} as unknown as Repository<Character>,
|
} as unknown as Repository<Character>,
|
||||||
{ find: jest.fn() } as unknown as Repository<LocationConnection>,
|
{ find: jest.fn() } as unknown as Repository<LocationConnection>,
|
||||||
{ find: jest.fn() } as unknown as Repository<LocationMonster>,
|
{ find: jest.fn() } as unknown as Repository<LocationMonster>,
|
||||||
|
{
|
||||||
|
isTravelAllowed: () => Promise.resolve(true),
|
||||||
|
discover: jest.fn(),
|
||||||
|
} as unknown as WorldDiscoveryService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a `WorldService` sitting at the Burned Road with the given points of
|
||||||
|
* interest, and a stub `WorldDiscoveryService` whose `discover` mirrors the
|
||||||
|
* real one: it returns the ash pit the first time and `null` once
|
||||||
|
* `alreadyDiscovered` says the character already knows it.
|
||||||
|
*/
|
||||||
|
function buildService(options: {
|
||||||
|
pointsOfInterest: LocationPointOfInterestContent[];
|
||||||
|
alreadyDiscovered?: boolean;
|
||||||
|
}) {
|
||||||
|
const currentLocation = location(BURNED_ROAD_ID, options.pointsOfInterest);
|
||||||
|
const discover = jest.fn().mockResolvedValue(
|
||||||
|
options.alreadyDiscovered ? null : { key: 'ash-pit', name: 'Ash Pit' },
|
||||||
|
);
|
||||||
|
const worldDiscovery = {
|
||||||
|
discover,
|
||||||
|
isTravelAllowed: () => Promise.resolve(true),
|
||||||
|
} as unknown as WorldDiscoveryService;
|
||||||
|
|
||||||
|
const service = new WorldService(
|
||||||
|
{
|
||||||
|
completeTravelIfDue: jest.fn().mockResolvedValue({ status: 'IDLE' }),
|
||||||
|
} as unknown as TravelService,
|
||||||
|
{
|
||||||
|
findOne: jest.fn().mockResolvedValue({
|
||||||
|
id: CHARACTER_ID,
|
||||||
|
currentLocationId: currentLocation.id,
|
||||||
|
currentLocation,
|
||||||
|
}),
|
||||||
|
} as unknown as Repository<Character>,
|
||||||
|
{ find: jest.fn() } as unknown as Repository<LocationConnection>,
|
||||||
|
{ find: jest.fn() } as unknown as Repository<LocationMonster>,
|
||||||
|
worldDiscovery,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { service, discover };
|
||||||
|
}
|
||||||
|
|
||||||
async function expectRejected(promise: Promise<unknown>): Promise<void> {
|
async function expectRejected(promise: Promise<unknown>): Promise<void> {
|
||||||
await expect(promise).rejects.toBeInstanceOf(WorldDomainError);
|
await expect(promise).rejects.toBeInstanceOf(WorldDomainError);
|
||||||
await expect(promise).rejects.toMatchObject({
|
await expect(promise).rejects.toMatchObject({
|
||||||
@@ -112,6 +155,7 @@ describe('WorldService.runLocalInteraction', () => {
|
|||||||
interactionKey: 'inspect-tracks',
|
interactionKey: 'inspect-tracks',
|
||||||
title: 'Suspicious Tracks',
|
title: 'Suspicious Tracks',
|
||||||
text: 'Between the ash and broken stones you make out several fresh bootprints.',
|
text: 'Between the ash and broken stones you make out several fresh bootprints.',
|
||||||
|
discoveredLocation: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,6 +169,7 @@ describe('WorldService.runLocalInteraction', () => {
|
|||||||
title: 'Gate Watch',
|
title: 'Gate Watch',
|
||||||
text: 'Only heard at the South Gate.',
|
text: 'Only heard at the South Gate.',
|
||||||
img: '/images/npcs/graufurt-gate-watch.png',
|
img: '/images/npcs/graufurt-gate-watch.png',
|
||||||
|
discoveredLocation: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,4 +216,87 @@ describe('WorldService.runLocalInteraction', () => {
|
|||||||
service.runLocalInteraction(CHARACTER_ID, 'hunt-area'),
|
service.runLocalInteraction(CHARACTER_ID, 'hunt-area'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('discovers the route the hotspot points at', async () => {
|
||||||
|
const { service, discover } = buildService({
|
||||||
|
pointsOfInterest: [
|
||||||
|
{
|
||||||
|
key: 'inspect-watchpost',
|
||||||
|
title: 'The Watchpost',
|
||||||
|
actionLabel: 'Inspect',
|
||||||
|
type: 'INVESTIGATE',
|
||||||
|
iconKey: 'investigate',
|
||||||
|
xPercent: 50,
|
||||||
|
yPercent: 50,
|
||||||
|
enabled: true,
|
||||||
|
resultTitle: 'The Watchpost',
|
||||||
|
resultText: 'Fresh tracks lead east.',
|
||||||
|
discoversLocationKey: 'ash-pit',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.runLocalInteraction(
|
||||||
|
CHARACTER_ID,
|
||||||
|
'inspect-watchpost',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(discover).toHaveBeenCalledWith(CHARACTER_ID, 'ash-pit');
|
||||||
|
expect(result.discoveredLocation).toEqual({
|
||||||
|
key: 'ash-pit',
|
||||||
|
name: 'Ash Pit',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports no discovery the second time the hotspot is used', async () => {
|
||||||
|
const { service } = buildService({
|
||||||
|
alreadyDiscovered: true,
|
||||||
|
pointsOfInterest: [
|
||||||
|
{
|
||||||
|
key: 'inspect-watchpost',
|
||||||
|
title: 'The Watchpost',
|
||||||
|
type: 'INVESTIGATE',
|
||||||
|
iconKey: 'investigate',
|
||||||
|
xPercent: 50,
|
||||||
|
yPercent: 50,
|
||||||
|
enabled: true,
|
||||||
|
resultText: 'Fresh tracks lead east.',
|
||||||
|
discoversLocationKey: 'ash-pit',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.runLocalInteraction(
|
||||||
|
CHARACTER_ID,
|
||||||
|
'inspect-watchpost',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.discoveredLocation).toBeNull();
|
||||||
|
expect(result.text).toBe('Fresh tracks lead east.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports no discovery for a hotspot that reveals nothing', async () => {
|
||||||
|
const { service, discover } = buildService({
|
||||||
|
pointsOfInterest: [
|
||||||
|
{
|
||||||
|
key: 'search-quarters',
|
||||||
|
title: 'Guard Quarters',
|
||||||
|
type: 'SEARCH',
|
||||||
|
iconKey: 'search',
|
||||||
|
xPercent: 20,
|
||||||
|
yPercent: 60,
|
||||||
|
enabled: true,
|
||||||
|
resultText: 'Nothing but ash.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.runLocalInteraction(
|
||||||
|
CHARACTER_ID,
|
||||||
|
'search-quarters',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(discover).not.toHaveBeenCalled();
|
||||||
|
expect(result.discoveredLocation).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ export interface LocationPointOfInterestContent {
|
|||||||
* scout on the Burned Road stays a piece of scenery, Borin does not.
|
* scout on the Burned Road stays a piece of scenery, Borin does not.
|
||||||
*/
|
*/
|
||||||
npcKey?: string;
|
npcKey?: string;
|
||||||
|
/**
|
||||||
|
* Names a location this hotspot reveals (Playable Slice 0.10 §9). Setting it
|
||||||
|
* turns a read-only reveal into a piece of world progress, which is why the
|
||||||
|
* interaction endpoint writes as well as reads.
|
||||||
|
*/
|
||||||
|
discoversLocationKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,6 +126,11 @@ export interface LocationInteractionResultDto {
|
|||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
img?: string;
|
img?: string;
|
||||||
|
/**
|
||||||
|
* Set only on the interaction that reveals a route for the first time, so
|
||||||
|
* the UI can say so once instead of on every repeat.
|
||||||
|
*/
|
||||||
|
discoveredLocation: { key: string; name: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Strips server-only result text before a POI is sent to the client. */
|
/** Strips server-only result text before a POI is sent to the client. */
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Character } from '../characters/entities/character.entity';
|
|||||||
import { LocationMonster } from '../monsters/entities/location-monster.entity';
|
import { LocationMonster } from '../monsters/entities/location-monster.entity';
|
||||||
import { MonsterDefinition } from '../monsters/entities/monster-definition.entity';
|
import { MonsterDefinition } from '../monsters/entities/monster-definition.entity';
|
||||||
import { TravelModule } from '../travel/travel.module';
|
import { TravelModule } from '../travel/travel.module';
|
||||||
|
import { WorldDiscoveryModule } from './discovery/world-discovery.module';
|
||||||
import { LocationConnection } from './entities/location-connection.entity';
|
import { LocationConnection } from './entities/location-connection.entity';
|
||||||
import { WorldController } from './world.controller';
|
import { WorldController } from './world.controller';
|
||||||
import { WorldService } from './world.service';
|
import { WorldService } from './world.service';
|
||||||
@@ -17,6 +18,7 @@ import { WorldService } from './world.service';
|
|||||||
MonsterDefinition,
|
MonsterDefinition,
|
||||||
]),
|
]),
|
||||||
TravelModule,
|
TravelModule,
|
||||||
|
WorldDiscoveryModule,
|
||||||
],
|
],
|
||||||
controllers: [WorldController],
|
controllers: [WorldController],
|
||||||
providers: [WorldService],
|
providers: [WorldService],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '../database/seeds/vertical-slice.constants';
|
} from '../database/seeds/vertical-slice.constants';
|
||||||
import { LocationMonster } from '../monsters/entities/location-monster.entity';
|
import { LocationMonster } from '../monsters/entities/location-monster.entity';
|
||||||
import { TravelService } from '../travel/travel.service';
|
import { TravelService } from '../travel/travel.service';
|
||||||
|
import { WorldDiscoveryService } from './discovery/world-discovery.service';
|
||||||
import { LocationConnection } from './entities/location-connection.entity';
|
import { LocationConnection } from './entities/location-connection.entity';
|
||||||
import { LocationDefinition } from './entities/location-definition.entity';
|
import { LocationDefinition } from './entities/location-definition.entity';
|
||||||
import type {
|
import type {
|
||||||
@@ -16,6 +17,7 @@ import type {
|
|||||||
import { WorldService } from './world.service';
|
import { WorldService } from './world.service';
|
||||||
|
|
||||||
const CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
|
const CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
|
||||||
|
const ASH_PIT_ID = '20000000-0000-4000-8000-000000000004';
|
||||||
|
|
||||||
const SOUTH_GATE_POIS: LocationPointOfInterestContent[] = [
|
const SOUTH_GATE_POIS: LocationPointOfInterestContent[] = [
|
||||||
{
|
{
|
||||||
@@ -183,6 +185,80 @@ function burnedRoad(): LocationDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ashPitLocation: LocationDefinition = {
|
||||||
|
id: ASH_PIT_ID,
|
||||||
|
key: 'ash-pit',
|
||||||
|
name: 'Ash Pit',
|
||||||
|
description: 'A smoldering pit at the edge of the Ashen Fields.',
|
||||||
|
regionKey: 'ashen-fields',
|
||||||
|
minRecommendedLevel: 2,
|
||||||
|
maxRecommendedLevel: 3,
|
||||||
|
dangerLevel: 2,
|
||||||
|
isSafe: false,
|
||||||
|
huntingEnabled: true,
|
||||||
|
artworkPath: '/assets/locations/ash-pit.webp',
|
||||||
|
regionName: 'Ashen Fields',
|
||||||
|
regionTierLabel: 'Tier 1',
|
||||||
|
locationType: 'HUNTING_GROUND',
|
||||||
|
localDescription: 'The pit still smolders, day and night.',
|
||||||
|
localArtworkPath: '/images/backgrounds/ash-pit.png',
|
||||||
|
localPointsOfInterest: [],
|
||||||
|
localPrimaryActions: [],
|
||||||
|
localRewardPreview: [],
|
||||||
|
createdAt: new Date('2026-08-18T09:00:00.000Z'),
|
||||||
|
updatedAt: new Date('2026-08-18T09:00:00.000Z'),
|
||||||
|
characters: [],
|
||||||
|
outgoingConnections: [],
|
||||||
|
incomingConnections: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a `WorldService` sitting at the Burned Road, with a stub
|
||||||
|
* `WorldDiscoveryService` that mirrors the real gate: a connection with
|
||||||
|
* `requiresDiscovery` is only allowed once its target is in `discovered`.
|
||||||
|
*/
|
||||||
|
function buildService(
|
||||||
|
options: {
|
||||||
|
connections?: LocationConnection[];
|
||||||
|
discovered?: string[];
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const location = burnedRoad();
|
||||||
|
const travelService = {
|
||||||
|
completeTravelIfDue: jest.fn().mockResolvedValue({ status: 'IDLE' }),
|
||||||
|
} as unknown as TravelService;
|
||||||
|
const characters = {
|
||||||
|
findOne: jest.fn().mockResolvedValue(character(BURNED_ROAD_ID, location)),
|
||||||
|
} as unknown as Repository<Character>;
|
||||||
|
const connections = {
|
||||||
|
find: jest.fn().mockResolvedValue(options.connections ?? []),
|
||||||
|
} as unknown as Repository<LocationConnection>;
|
||||||
|
const locationMonsters = {
|
||||||
|
find: jest.fn().mockResolvedValue([]),
|
||||||
|
} as unknown as Repository<LocationMonster>;
|
||||||
|
const worldDiscovery = {
|
||||||
|
discover: jest.fn(),
|
||||||
|
isTravelAllowed: (
|
||||||
|
_characterId: string,
|
||||||
|
connection: { toLocationId: string; requiresDiscovery: boolean },
|
||||||
|
) =>
|
||||||
|
Promise.resolve(
|
||||||
|
!connection.requiresDiscovery ||
|
||||||
|
(options.discovered ?? []).includes(connection.toLocationId),
|
||||||
|
),
|
||||||
|
} as unknown as WorldDiscoveryService;
|
||||||
|
|
||||||
|
const service = new WorldService(
|
||||||
|
travelService,
|
||||||
|
characters,
|
||||||
|
connections,
|
||||||
|
locationMonsters,
|
||||||
|
worldDiscovery,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { service };
|
||||||
|
}
|
||||||
|
|
||||||
describe('WorldService', () => {
|
describe('WorldService', () => {
|
||||||
it('returns the authoritative current location and only enabled public connections', async () => {
|
it('returns the authoritative current location and only enabled public connections', async () => {
|
||||||
const callOrder: string[] = [];
|
const callOrder: string[] = [];
|
||||||
@@ -230,11 +306,16 @@ describe('WorldService', () => {
|
|||||||
const locationMonsters = {
|
const locationMonsters = {
|
||||||
find: findLocationMonsters,
|
find: findLocationMonsters,
|
||||||
} as unknown as Repository<LocationMonster>;
|
} as unknown as Repository<LocationMonster>;
|
||||||
|
const worldDiscovery = {
|
||||||
|
isTravelAllowed: () => Promise.resolve(true),
|
||||||
|
discover: jest.fn(),
|
||||||
|
} as unknown as WorldDiscoveryService;
|
||||||
const service = new WorldService(
|
const service = new WorldService(
|
||||||
travelService,
|
travelService,
|
||||||
characters,
|
characters,
|
||||||
connections,
|
connections,
|
||||||
locationMonsters,
|
locationMonsters,
|
||||||
|
worldDiscovery,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await service.getCurrentLocation(CHARACTER_ID);
|
const result = await service.getCurrentLocation(CHARACTER_ID);
|
||||||
@@ -313,11 +394,16 @@ describe('WorldService', () => {
|
|||||||
const locationMonsters = {
|
const locationMonsters = {
|
||||||
find: findLocationMonsters,
|
find: findLocationMonsters,
|
||||||
} as unknown as Repository<LocationMonster>;
|
} as unknown as Repository<LocationMonster>;
|
||||||
|
const worldDiscovery = {
|
||||||
|
isTravelAllowed: () => Promise.resolve(true),
|
||||||
|
discover: jest.fn(),
|
||||||
|
} as unknown as WorldDiscoveryService;
|
||||||
const service = new WorldService(
|
const service = new WorldService(
|
||||||
travelService,
|
travelService,
|
||||||
characters,
|
characters,
|
||||||
connections,
|
connections,
|
||||||
locationMonsters,
|
locationMonsters,
|
||||||
|
worldDiscovery,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await service.getCurrentLocation(CHARACTER_ID);
|
const result = await service.getCurrentLocation(CHARACTER_ID);
|
||||||
@@ -349,11 +435,16 @@ describe('WorldService', () => {
|
|||||||
const locationMonsters = {
|
const locationMonsters = {
|
||||||
find: findLocationMonsters,
|
find: findLocationMonsters,
|
||||||
} as unknown as Repository<LocationMonster>;
|
} as unknown as Repository<LocationMonster>;
|
||||||
|
const worldDiscovery = {
|
||||||
|
isTravelAllowed: () => Promise.resolve(true),
|
||||||
|
discover: jest.fn(),
|
||||||
|
} as unknown as WorldDiscoveryService;
|
||||||
const service = new WorldService(
|
const service = new WorldService(
|
||||||
travelService,
|
travelService,
|
||||||
characters,
|
characters,
|
||||||
connections,
|
connections,
|
||||||
locationMonsters,
|
locationMonsters,
|
||||||
|
worldDiscovery,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -458,6 +549,48 @@ describe('WorldService', () => {
|
|||||||
expect(result.dangerRating).toBeNull();
|
expect(result.dangerRating).toBeNull();
|
||||||
expect(result.encounterPreview).toEqual([]);
|
expect(result.encounterPreview).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides a gated connection until the character has discovered it', async () => {
|
||||||
|
const { service } = buildService({
|
||||||
|
connections: [
|
||||||
|
{
|
||||||
|
fromLocationId: BURNED_ROAD_ID,
|
||||||
|
toLocationId: ASH_PIT_ID,
|
||||||
|
travelDurationSeconds: 20,
|
||||||
|
ambushChance: '0.1500',
|
||||||
|
enabled: true,
|
||||||
|
requiresDiscovery: true,
|
||||||
|
toLocation: ashPitLocation,
|
||||||
|
} as unknown as LocationConnection,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const location = await service.getCurrentLocation(CHARACTER_ID);
|
||||||
|
|
||||||
|
expect(location.connections).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a gated connection once it has been discovered', async () => {
|
||||||
|
const { service } = buildService({
|
||||||
|
discovered: [ASH_PIT_ID],
|
||||||
|
connections: [
|
||||||
|
{
|
||||||
|
fromLocationId: BURNED_ROAD_ID,
|
||||||
|
toLocationId: ASH_PIT_ID,
|
||||||
|
travelDurationSeconds: 20,
|
||||||
|
ambushChance: '0.1500',
|
||||||
|
enabled: true,
|
||||||
|
requiresDiscovery: true,
|
||||||
|
toLocation: ashPitLocation,
|
||||||
|
} as unknown as LocationConnection,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const location = await service.getCurrentLocation(CHARACTER_ID);
|
||||||
|
|
||||||
|
expect(location.connections).toHaveLength(1);
|
||||||
|
expect(location.connections[0].targetLocation.key).toBe('ash-pit');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadBurnedRoad() {
|
async function loadBurnedRoad() {
|
||||||
@@ -486,6 +619,10 @@ async function loadLocation(
|
|||||||
{
|
{
|
||||||
find: jest.fn().mockResolvedValue(pool),
|
find: jest.fn().mockResolvedValue(pool),
|
||||||
} as unknown as Repository<LocationMonster>,
|
} as unknown as Repository<LocationMonster>,
|
||||||
|
{
|
||||||
|
isTravelAllowed: () => Promise.resolve(true),
|
||||||
|
discover: jest.fn(),
|
||||||
|
} as unknown as WorldDiscoveryService,
|
||||||
);
|
);
|
||||||
|
|
||||||
return service.getCurrentLocation(CHARACTER_ID);
|
return service.getCurrentLocation(CHARACTER_ID);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Character } from '../characters/entities/character.entity';
|
|||||||
import { calculateDangerRating, DangerRating } from '../hunting/danger-rating';
|
import { calculateDangerRating, DangerRating } from '../hunting/danger-rating';
|
||||||
import { LocationMonster } from '../monsters/entities/location-monster.entity';
|
import { LocationMonster } from '../monsters/entities/location-monster.entity';
|
||||||
import { TravelService } from '../travel/travel.service';
|
import { TravelService } from '../travel/travel.service';
|
||||||
|
import { WorldDiscoveryService } from './discovery/world-discovery.service';
|
||||||
import { LocationConnection } from './entities/location-connection.entity';
|
import { LocationConnection } from './entities/location-connection.entity';
|
||||||
import { LocationDefinition } from './entities/location-definition.entity';
|
import { LocationDefinition } from './entities/location-definition.entity';
|
||||||
import {
|
import {
|
||||||
@@ -68,6 +69,7 @@ export class WorldService {
|
|||||||
private readonly connections: Repository<LocationConnection>,
|
private readonly connections: Repository<LocationConnection>,
|
||||||
@InjectRepository(LocationMonster)
|
@InjectRepository(LocationMonster)
|
||||||
private readonly locationMonsters: Repository<LocationMonster>,
|
private readonly locationMonsters: Repository<LocationMonster>,
|
||||||
|
private readonly worldDiscovery: WorldDiscoveryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getCurrentLocation(
|
async getCurrentLocation(
|
||||||
@@ -85,6 +87,16 @@ export class WorldService {
|
|||||||
? await this.getEncounterPool(location.id)
|
? await this.getEncounterPool(location.id)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const visibleConnections: LocationConnection[] = [];
|
||||||
|
for (const connection of connections) {
|
||||||
|
if (
|
||||||
|
connection.enabled &&
|
||||||
|
(await this.worldDiscovery.isTravelAllowed(characterId, connection))
|
||||||
|
) {
|
||||||
|
visibleConnections.push(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: location.id,
|
id: location.id,
|
||||||
key: location.key,
|
key: location.key,
|
||||||
@@ -114,9 +126,7 @@ export class WorldService {
|
|||||||
iconPath: entry.monster.iconPath,
|
iconPath: entry.monster.iconPath,
|
||||||
})),
|
})),
|
||||||
rewardPreview: location.localRewardPreview,
|
rewardPreview: location.localRewardPreview,
|
||||||
connections: connections
|
connections: visibleConnections.map((connection) => ({
|
||||||
.filter((connection) => connection.enabled)
|
|
||||||
.map((connection) => ({
|
|
||||||
targetLocation: {
|
targetLocation: {
|
||||||
id: connection.toLocation.id,
|
id: connection.toLocation.id,
|
||||||
key: connection.toLocation.key,
|
key: connection.toLocation.key,
|
||||||
@@ -152,11 +162,21 @@ export class WorldService {
|
|||||||
throw locationInteractionUnavailable();
|
throw locationInteractionUnavailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A hotspot that reveals a route writes before it speaks. Idempotent by
|
||||||
|
// the unique pair, so a second click simply reports nothing new.
|
||||||
|
const discoveredLocation = poi.discoversLocationKey
|
||||||
|
? await this.worldDiscovery.discover(
|
||||||
|
characterId,
|
||||||
|
poi.discoversLocationKey,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
interactionKey: poi.key,
|
interactionKey: poi.key,
|
||||||
title: poi.resultTitle ?? poi.title,
|
title: poi.resultTitle ?? poi.title,
|
||||||
text: poi.resultText,
|
text: poi.resultText,
|
||||||
...(poi.resultImg === undefined ? {} : { img: poi.resultImg }),
|
...(poi.resultImg === undefined ? {} : { img: poi.resultImg }),
|
||||||
|
discoveredLocation,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user