feat: expose current world location

This commit is contained in:
Bastian Wagner
2026-08-18 20:57:30 +02:00
parent d06f05047b
commit c641168c7d
7 changed files with 387 additions and 1 deletions

View File

@@ -0,0 +1,167 @@
import { NotFoundException } from '@nestjs/common';
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 { TravelService } from '../travel/travel.service';
import { LocationConnection } from './entities/location-connection.entity';
import { LocationDefinition } from './entities/location-definition.entity';
import { WorldService } from './world.service';
const CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
function currentLocation(): LocationDefinition {
return {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'S\u00fcdtor von Graufurt',
description:
'Das S\u00fcdtor ist der sichere Ausgangspunkt nach S\u00fcden.',
regionKey: 'ashen-fields',
minRecommendedLevel: 1,
maxRecommendedLevel: 1,
dangerLevel: 0,
isSafe: true,
huntingEnabled: false,
artworkPath: '/assets/locations/south-gate.webp',
createdAt: new Date('2026-08-18T09:00:00.000Z'),
updatedAt: new Date('2026-08-18T09:00:00.000Z'),
characters: [],
outgoingConnections: [],
incomingConnections: [],
};
}
function burnedRoad(): LocationDefinition {
return {
id: BURNED_ROAD_ID,
key: 'burned-road',
name: 'Verbrannte Stra\u00dfe',
description: 'Eine verbrannte Handelsroute durch die Aschenfelder.',
regionKey: 'ashen-fields',
minRecommendedLevel: 1,
maxRecommendedLevel: 2,
dangerLevel: 1,
isSafe: false,
huntingEnabled: true,
artworkPath: '/assets/locations/burned-road.webp',
createdAt: new Date('2026-08-18T09:00:00.000Z'),
updatedAt: new Date('2026-08-18T09:00:00.000Z'),
characters: [],
outgoingConnections: [],
incomingConnections: [],
};
}
describe('WorldService', () => {
it('returns the authoritative current location and only enabled public connections', async () => {
const callOrder: string[] = [];
const location = currentLocation();
const completeTravelIfDue = jest.fn().mockImplementation(() => {
callOrder.push('completeTravelIfDue');
return Promise.resolve({ status: 'IDLE' });
});
const travelService = {
completeTravelIfDue,
} as unknown as TravelService;
const findCharacter = jest.fn().mockImplementation(() => {
callOrder.push('findCharacter');
return Promise.resolve({
id: CHARACTER_ID,
currentLocationId: SOUTH_GATE_ID,
currentLocation: location,
});
});
const characters = {
findOne: findCharacter,
} as unknown as Repository<Character>;
const findConnections = jest.fn().mockResolvedValue([
{
id: '30000000-0000-4000-8000-000000000001',
fromLocationId: SOUTH_GATE_ID,
toLocationId: BURNED_ROAD_ID,
travelDurationSeconds: 10,
ambushChance: '0.0500',
enabled: true,
fromLocation: location,
toLocation: burnedRoad(),
},
{
id: '30000000-0000-4000-8000-000000000002',
fromLocationId: SOUTH_GATE_ID,
toLocationId: '20000000-0000-4000-8000-000000000003',
travelDurationSeconds: 30,
ambushChance: '0.9000',
enabled: false,
fromLocation: location,
toLocation: burnedRoad(),
},
]);
const connections = {
find: findConnections,
} as unknown as Repository<LocationConnection>;
const service = new WorldService(travelService, characters, connections);
const result = await service.getCurrentLocation(CHARACTER_ID);
expect(callOrder).toEqual(['completeTravelIfDue', 'findCharacter']);
expect(result).toEqual({
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'S\u00fcdtor von Graufurt',
description:
'Das S\u00fcdtor ist der sichere Ausgangspunkt nach S\u00fcden.',
regionKey: 'ashen-fields',
minRecommendedLevel: 1,
maxRecommendedLevel: 1,
dangerLevel: 0,
isSafe: true,
huntingEnabled: false,
artworkPath: '/assets/locations/south-gate.webp',
connections: [
{
targetLocation: {
id: BURNED_ROAD_ID,
key: 'burned-road',
name: 'Verbrannte Stra\u00dfe',
},
travelDurationSeconds: 10,
danger: 'LOW',
},
],
});
expect(findCharacter).toHaveBeenCalledWith({
where: { id: CHARACTER_ID },
relations: { currentLocation: true },
});
expect(findConnections).toHaveBeenCalledWith({
where: { fromLocationId: SOUTH_GATE_ID, enabled: true },
relations: { toLocation: true },
});
});
it('reports a missing character after travel completion', async () => {
const completeTravelIfDue = jest.fn().mockResolvedValue({
status: 'IDLE',
});
const travelService = {
completeTravelIfDue,
} as unknown as TravelService;
const findCharacter = jest.fn().mockResolvedValue(null);
const characters = {
findOne: findCharacter,
} as unknown as Repository<Character>;
const findConnections = jest.fn();
const connections = {
find: findConnections,
} as unknown as Repository<LocationConnection>;
const service = new WorldService(travelService, characters, connections);
await expect(
service.getCurrentLocation(CHARACTER_ID),
).rejects.toBeInstanceOf(NotFoundException);
expect(findConnections).not.toHaveBeenCalled();
});
});