Files
ashen-realms/apps/api/src/world/local-location-interaction.spec.ts
Bastian Wagner 0dfb62bfc4 fix(api): clean up this slice's own lint violations
Fixed five prettier/prettier formatting errors in the three spec files
Slice 0.10 wrote or edited (world-discovery.service.spec.ts,
local-location-interaction.spec.ts, vertical-slice.seed.spec.ts).
Scoped by hand to just those locations so the pre-existing errors
elsewhere in the workspace, unrelated to this slice, are left alone.

Also corrects the implementation notes, which previously understated
the lint state as entirely pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 17:24:03 +02:00

305 lines
9.0 KiB
TypeScript

import { Repository } from 'typeorm';
import { Character } from '../characters/entities/character.entity';
import {
BURNED_ROAD_ID,
SOUTH_GATE_ID,
} from '../database/seeds/vertical-slice.constants';
import { LocationMonster } from '../monsters/entities/location-monster.entity';
import { TravelService } from '../travel/travel.service';
import { WorldDiscoveryService } from './discovery/world-discovery.service';
import { LocationConnection } from './entities/location-connection.entity';
import { LocationDefinition } from './entities/location-definition.entity';
import type { LocationPointOfInterestContent } from './local-location.types';
import { WorldDomainError } from './world.errors';
import { WorldService } from './world.service';
const CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
const BURNED_ROAD_POIS: LocationPointOfInterestContent[] = [
{
key: 'hunt-area',
title: 'Hunting Ground',
actionLabel: 'Begin Hunt',
type: 'HUNT',
iconKey: 'hunt',
xPercent: 52,
yPercent: 44,
enabled: true,
},
{
key: 'inspect-tracks',
title: 'Suspicious Tracks',
actionLabel: 'Investigate',
type: 'INVESTIGATE',
iconKey: 'investigate',
xPercent: 32,
yPercent: 78,
enabled: true,
resultTitle: 'Suspicious Tracks',
resultText:
'Between the ash and broken stones you make out several fresh bootprints.',
},
{
key: 'sealed-crypt',
title: 'Sealed Crypt',
type: 'DUNGEON',
iconKey: 'search',
xPercent: 90,
yPercent: 20,
enabled: false,
resultTitle: 'Sealed Crypt',
resultText: 'Still sealed.',
},
];
const SOUTH_GATE_POIS: LocationPointOfInterestContent[] = [
{
key: 'gate-watch',
title: 'Gate Watch',
actionLabel: 'Talk',
type: 'NPC',
iconKey: 'speak',
xPercent: 45,
yPercent: 52,
enabled: true,
resultTitle: 'Gate Watch',
resultText: 'Only heard at the South Gate.',
resultImg: '/images/npcs/graufurt-gate-watch.png',
},
];
function location(
id: string,
pointsOfInterest: LocationPointOfInterestContent[],
): LocationDefinition {
return {
id,
localPointsOfInterest: pointsOfInterest,
} as LocationDefinition;
}
function createService(
currentLocation: LocationDefinition,
completeTravelIfDue = jest.fn().mockResolvedValue({ status: 'IDLE' }),
) {
return new WorldService(
{ completeTravelIfDue } 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>,
{
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> {
await expect(promise).rejects.toBeInstanceOf(WorldDomainError);
await expect(promise).rejects.toMatchObject({
code: 'LOCATION_INTERACTION_UNAVAILABLE',
});
}
describe('WorldService.runLocalInteraction', () => {
it('returns the authored result of an enabled interaction at the current location', async () => {
const service = createService(location(BURNED_ROAD_ID, BURNED_ROAD_POIS));
await expect(
service.runLocalInteraction(CHARACTER_ID, 'inspect-tracks'),
).resolves.toEqual({
interactionKey: 'inspect-tracks',
title: 'Suspicious Tracks',
text: 'Between the ash and broken stones you make out several fresh bootprints.',
discoveredLocation: null,
});
});
it('includes the authored image when the interaction carries one', async () => {
const service = createService(location(SOUTH_GATE_ID, SOUTH_GATE_POIS));
await expect(
service.runLocalInteraction(CHARACTER_ID, 'gate-watch'),
).resolves.toEqual({
interactionKey: 'gate-watch',
title: 'Gate Watch',
text: 'Only heard at the South Gate.',
img: '/images/npcs/graufurt-gate-watch.png',
discoveredLocation: null,
});
});
it('settles travel before resolving which location the character stands at', async () => {
const completeTravelIfDue = jest.fn().mockResolvedValue({ status: 'IDLE' });
const service = createService(
location(BURNED_ROAD_ID, BURNED_ROAD_POIS),
completeTravelIfDue,
);
await service.runLocalInteraction(CHARACTER_ID, 'inspect-tracks');
expect(completeTravelIfDue).toHaveBeenCalledWith(CHARACTER_ID);
});
it('rejects an interaction that belongs to a different location', async () => {
const service = createService(location(SOUTH_GATE_ID, SOUTH_GATE_POIS));
await expectRejected(
service.runLocalInteraction(CHARACTER_ID, 'inspect-tracks'),
);
});
it('rejects an unknown interaction key', async () => {
const service = createService(location(BURNED_ROAD_ID, BURNED_ROAD_POIS));
await expectRejected(
service.runLocalInteraction(CHARACTER_ID, 'open-vault'),
);
});
it('rejects a disabled interaction', async () => {
const service = createService(location(BURNED_ROAD_ID, BURNED_ROAD_POIS));
await expectRejected(
service.runLocalInteraction(CHARACTER_ID, 'sealed-crypt'),
);
});
it('rejects a navigation hotspot that has no result to reveal', async () => {
const service = createService(location(BURNED_ROAD_ID, BURNED_ROAD_POIS));
await expectRejected(
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();
});
});