fix: close out remaining German player-facing text across web and API tests

Repo-wide grep sweep (apps/web/src, apps/api/src) for leftover German
content strings missed by Tasks 1-9, mostly in spec fixtures/assertions
that mirror already-translated seed content (monster/item names, POI
titles and action labels, location names/descriptions) plus a few real
source-file gaps:

- inventory-detail-panel.component.ts: STAT_LABELS (Waffenschaden,
  Angriff, Leben, Rüstung) were never translated by Task 6; now match
  the identical English labels already used in inventory-page.component.html.
- app-shell.component.html: aria-label="Spielinhalt" -> "Game content"
  (this file was outside every prior task's file list).
- location-interaction-panel.component.spec.ts: dead NPC-quote fixture
  translated to match the real wounded-scout POI text.

Code comments referencing German source-spec section titles or
not-yet-seeded faction names, and inline calculation-documentation
comments, are left as-is per the source spec's scope (dev-facing
comments may stay German).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACkMEDYiwtcfchqKkiUJNX
This commit is contained in:
Bastian Wagner
2026-08-21 22:59:34 +02:00
parent ae163f000a
commit 1322285b0f
38 changed files with 243 additions and 241 deletions

View File

@@ -61,7 +61,7 @@ describe('CharacterStatsService', () => {
expect(stats.armor).toBe(0);
});
it("applies Räuberklinge's weapon damage and bonus attack", async () => {
it("applies the Bandit Blade's weapon damage and bonus attack", async () => {
const scope = fakeScope([
{
slot: EquipmentSlot.WEAPON,

View File

@@ -37,7 +37,7 @@ describe('CharactersService', () => {
currentLocation: {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'Südtor von Graufurt',
name: 'Graufurt South Gate',
},
}),
} as unknown as Repository<Character>;
@@ -57,7 +57,7 @@ describe('CharactersService', () => {
currentLocation: {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'Südtor von Graufurt',
name: 'Graufurt South Gate',
},
});
expect(characterStats.calculate).toHaveBeenCalledWith(
@@ -78,7 +78,7 @@ describe('CharactersService', () => {
currentLocation: {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'Südtor von Graufurt',
name: 'Graufurt South Gate',
},
}),
} as unknown as Repository<Character>;

View File

@@ -209,7 +209,7 @@ function monster(
return {
id: MONSTER_ID,
key: 'road-bandit',
name: 'Straßenräuber',
name: 'Road Bandit',
level: 2,
maxHp: 75,
attack: 9,
@@ -301,7 +301,7 @@ function createHarness() {
itemDefinition({
id: BANDIT_BLADE_DEFINITION_ID,
key: 'bandit-blade',
name: 'Räuberklinge',
name: 'Bandit Blade',
weaponDamage: 11,
bonusAttack: 1,
iconPath: '/images/items/bandit-blade.png',
@@ -360,7 +360,7 @@ function createHarness() {
return { state, equipmentService, combatService };
}
describe('equipping Räuberklinge increases combat damage (spec §45, §60)', () => {
describe('equipping the Bandit Blade increases combat damage (spec §45, §60)', () => {
it('deals more damage against the same monster after the upgrade than before it', async () => {
const { state, combatService, equipmentService } = createHarness();

View File

@@ -193,7 +193,7 @@ function monster(
return {
id: MONSTER_ID,
key: 'ash-rat',
name: 'Aschenratte',
name: 'Ash Rat',
level: 1,
maxHp: 45,
attack: 5,
@@ -334,7 +334,7 @@ describe('CombatService', () => {
});
expect(combat.monster).toEqual({
key: 'ash-rat',
name: 'Aschenratte',
name: 'Ash Rat',
level: 1,
maxHp: 45,
currentHp: 45,
@@ -1016,7 +1016,7 @@ describe('CombatService', () => {
characterItemId: 'character-item-1',
item: {
key: 'bandit-blade',
name: 'Räuberklinge',
name: 'Bandit Blade',
rarity: 'COMMON',
iconPath: '/images/items/bandit-blade.png',
},

View File

@@ -250,7 +250,7 @@ describe('seedVisibleVerticalSlice', () => {
expect(locationMonsterRepository.rows).toHaveLength(4);
});
it('seeds the local view content of the Verbrannte Straße with four points of interest', async () => {
it('seeds the local view content of the Burned Road with four points of interest', async () => {
const locationRepository = new InMemoryRepository();
const dataSource = createDataSource(
locationRepository,
@@ -306,7 +306,7 @@ describe('seedVisibleVerticalSlice', () => {
]);
});
it('gives the Südtor its own local content so a second location needs no new component', async () => {
it('gives the South Gate its own local content so a second location needs no new component', async () => {
const locationRepository = new InMemoryRepository();
const dataSource = createDataSource(
locationRepository,
@@ -347,7 +347,7 @@ describe('seedVisibleVerticalSlice', () => {
locationRepository.rows.push({
id: persistedSouthGateId,
key: 'south-gate',
name: 'Veraltetes Südtor',
name: 'Outdated South Gate',
});
const dataSource = createDataSource(
locationRepository,

View File

@@ -274,7 +274,7 @@ describe('EquipmentService', () => {
const banditBlade = itemDefinition({
id: 'def-bandit-blade',
key: 'bandit-blade',
name: 'Räuberklinge',
name: 'Bandit Blade',
weaponDamage: 11,
bonusAttack: 1,
iconPath: '/images/items/bandit-blade.png',

View File

@@ -6,7 +6,7 @@ const CHARACTER = { attack: 6, armor: 0, hp: 100 };
const CHARACTER_POWER = 44;
describe('calculateDangerRating', () => {
it('rates the seeded Aschenratte as MATCH', () => {
it('rates the seeded Ash Rat as MATCH', () => {
// Aschenratte: attack 5, armor 0, maxHp 45
// power(monster) = 5*4 + 0*2 + floor(45/5) = 20 + 0 + 9 = 29
// ratio = 29 / 44 = 0.6590909... -> not < 0.65, and < 1.0 -> MATCH
@@ -15,7 +15,7 @@ describe('calculateDangerRating', () => {
expect(calculateDangerRating(CHARACTER, monster)).toBe(DangerRating.MATCH);
});
it('rates the seeded Straßenräuber as STRONG', () => {
it('rates the seeded Road Bandit as STRONG', () => {
// Straßenräuber: attack 9, armor 5, maxHp 75
// power(monster) = 9*4 + 5*2 + floor(75/5) = 36 + 10 + 15 = 61
// ratio = 61 / 44 = 1.3863636... -> not < 1.0, and < 1.7 -> STRONG

View File

@@ -37,7 +37,7 @@ describe('HuntingController', () => {
it('delegates to huntingService.startHunt with the demo character id and returns its result', async () => {
const huntResult = {
id: 'hunt-1',
location: { id: 'loc-1', key: 'burned-road', name: 'Verbrannte Strasse' },
location: { id: 'loc-1', key: 'burned-road', name: 'Burned Road' },
encounters: [],
};
startHunt.mockResolvedValue(huntResult);
@@ -53,13 +53,13 @@ describe('HuntingController', () => {
it('serves the resumable hunt with its encounter statuses', async () => {
const huntResult = {
id: 'hunt-1',
location: { id: 'loc-1', key: 'burned-road', name: 'Verbrannte Strasse' },
location: { id: 'loc-1', key: 'burned-road', name: 'Burned Road' },
encounters: [
{
id: 'encounter-1',
monster: {
key: 'ash-rat',
name: 'Aschenratte',
name: 'Ash Rat',
level: 1,
artworkPath: '/images/monsters/ash-rat.png',
},

View File

@@ -18,8 +18,8 @@ import type { RandomSource } from '../shared/random-source';
const CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
const HUNTING_LOCATION_ID = '20000000-0000-4000-8000-000000000001';
const SAFE_LOCATION_ID = '20000000-0000-4000-8000-000000000002';
const MONSTER_A_ID = '30000000-0000-4000-8000-000000000001'; // Aschenratte
const MONSTER_B_ID = '30000000-0000-4000-8000-000000000002'; // Strassenraeuber
const MONSTER_A_ID = '30000000-0000-4000-8000-000000000001'; // Ash Rat
const MONSTER_B_ID = '30000000-0000-4000-8000-000000000002'; // Road Bandit
const LOCATION_MONSTER_A_ID = '40000000-0000-4000-8000-000000000001';
const LOCATION_MONSTER_B_ID = '40000000-0000-4000-8000-000000000002';
@@ -191,7 +191,7 @@ function huntingLocation(): LocationDefinition {
return {
id: HUNTING_LOCATION_ID,
key: 'burned-road',
name: 'Verbrannte Strasse',
name: 'Burned Road',
description: 'A burned road.',
regionKey: 'ashen-fields',
minRecommendedLevel: 1,
@@ -367,12 +367,12 @@ describe('HuntingService', () => {
const monsterA = monsterDefinition(
MONSTER_A_ID,
'aschenratte',
'Aschenratte',
'Ash Rat',
);
const monsterB = monsterDefinition(
MONSTER_B_ID,
'strassenraeuber',
'Straßenräuber',
'Road Bandit',
);
const state = createState();
state.characters[0].currentLocationId = HUNTING_LOCATION_ID;
@@ -394,7 +394,7 @@ describe('HuntingService', () => {
expect(result.location).toEqual({
id: HUNTING_LOCATION_ID,
key: 'burned-road',
name: 'Verbrannte Strasse',
name: 'Burned Road',
});
expect(dataSource.state.hunts).toHaveLength(1);
expect(dataSource.state.hunts[0]).toMatchObject({
@@ -447,12 +447,12 @@ describe('HuntingService', () => {
const monsterA = monsterDefinition(
MONSTER_A_ID,
'aschenratte',
'Aschenratte',
'Ash Rat',
);
const monsterB = monsterDefinition(
MONSTER_B_ID,
'strassenraeuber',
'Straßenräuber',
'Road Bandit',
);
const state = createState();
state.characters[0].currentLocationId = HUNTING_LOCATION_ID;
@@ -488,7 +488,7 @@ describe('HuntingService', () => {
const monsterA = monsterDefinition(
MONSTER_A_ID,
'aschenratte',
'Aschenratte',
'Ash Rat',
);
const state = createState();
state.characters[0].currentLocationId = HUNTING_LOCATION_ID;
@@ -524,12 +524,12 @@ describe('HuntingService', () => {
const monsterA = monsterDefinition(
MONSTER_A_ID,
'aschenratte',
'Aschenratte',
'Ash Rat',
);
const monsterB = monsterDefinition(
MONSTER_B_ID,
'strassenraeuber',
'Straßenräuber',
'Road Bandit',
);
const state = createState();
state.characters[0].currentLocationId = HUNTING_LOCATION_ID;
@@ -559,7 +559,7 @@ describe('HuntingService', () => {
const weakMonster = monsterDefinition(
MONSTER_A_ID,
'aschenratte',
'Aschenratte',
'Ash Rat',
{
attack: 1,
armor: 0,
@@ -595,7 +595,7 @@ describe('HuntingService', () => {
const monsterA = monsterDefinition(
MONSTER_A_ID,
'aschenratte',
'Aschenratte',
'Ash Rat',
);
const state = createState();
state.characters[0].currentLocationId = HUNTING_LOCATION_ID;
@@ -637,7 +637,7 @@ describe('HuntingService', () => {
const monsterA = monsterDefinition(
MONSTER_A_ID,
'aschenratte',
'Aschenratte',
'Ash Rat',
);
const state = createState();
state.characters[0].currentLocationId = HUNTING_LOCATION_ID;
@@ -696,7 +696,7 @@ describe('HuntingService', () => {
expect(result?.location).toEqual({
id: HUNTING_LOCATION_ID,
key: 'burned-road',
name: 'Verbrannte Strasse',
name: 'Burned Road',
});
expect(result?.encounters.map((encounter) => encounter.status)).toEqual([
HuntEncounterStatus.AVAILABLE,

View File

@@ -18,8 +18,8 @@ function characterItem(overrides: Partial<CharacterItem> = {}): CharacterItem {
updatedAt: new Date('2026-08-18T09:00:00.000Z'),
itemDefinition: {
key: 'worn-short-sword',
name: 'Abgenutztes Kurzschwert',
description: 'Die Klinge eines Rekruten, öfter geschliffen als geführt.',
name: 'Worn Shortsword',
description: "A recruit's blade, sharpened more often than it's been swung.",
rarity: ItemRarity.COMMON,
type: ItemType.EQUIPMENT,
equipmentSlot: EquipmentSlot.WEAPON,
@@ -66,8 +66,8 @@ describe('InventoryService', () => {
equipped: true,
item: {
key: 'worn-short-sword',
name: 'Abgenutztes Kurzschwert',
description: 'Die Klinge eines Rekruten, öfter geschliffen als geführt.',
name: 'Worn Shortsword',
description: "A recruit's blade, sharpened more often than it's been swung.",
rarity: 'COMMON',
equipmentSlot: 'WEAPON',
weaponDamage: 8,

View File

@@ -121,7 +121,7 @@ function milestone(
return {
id: MILESTONE_ID,
key: 'first-hunt',
name: 'Erste erfolgreiche Jagd',
name: 'First Successful Hunt',
description: '',
renownReward: 1,
repeatable: false,

View File

@@ -158,7 +158,7 @@ function createState(overrides: Partial<State> = {}): State {
{
id: BANDIT_BLADE,
key: 'bandit-blade',
name: 'Räuberklinge',
name: 'Bandit Blade',
type: ItemType.EQUIPMENT,
rarity: ItemRarity.COMMON,
iconPath: '/images/items/bandit-blade.png',
@@ -239,7 +239,7 @@ describe('CombatRewardService', () => {
});
});
describe('Aschenratte', () => {
describe('Ash Rat', () => {
it('grants a silver roll inside 4-7, persisted on the character', async () => {
const state = createState();
@@ -266,7 +266,7 @@ describe('CombatRewardService', () => {
});
});
describe('Straßenräuber', () => {
describe('Road Bandit', () => {
const banditCombat = combat({ monsterDefinitionId: ROAD_BANDIT_ID });
it('grants a silver roll inside 9-15', async () => {
@@ -281,7 +281,7 @@ describe('CombatRewardService', () => {
expect(reward.silver).toBe(9);
});
it('persists a dropped Räuberklinge as a CharacterItem and references it in the reward', async () => {
it('persists a dropped Bandit Blade as a CharacterItem and references it in the reward', async () => {
const state = createState();
const reward = await service(
@@ -301,7 +301,7 @@ describe('CombatRewardService', () => {
characterItemId: state.characterItems[0].id,
item: {
key: 'bandit-blade',
name: 'Räuberklinge',
name: 'Bandit Blade',
rarity: ItemRarity.COMMON,
iconPath: '/images/items/bandit-blade.png',
},
@@ -311,7 +311,7 @@ describe('CombatRewardService', () => {
expect(state.combatRewardItems).toHaveLength(1);
});
it('reports no items when the Räuberklinge does not drop', async () => {
it('reports no items when the Bandit Blade does not drop', async () => {
const state = createState();
const reward = await service(state, fakeLoot()).grantVictoryRewards(
@@ -450,7 +450,7 @@ describe('CombatRewardService', () => {
{
id: BANDIT_BLADE,
key: 'bandit-blade',
name: 'Räuberklinge',
name: 'Bandit Blade',
type: ItemType.EQUIPMENT,
rarity: ItemRarity.COMMON,
iconPath: '/images/items/bandit-blade.png',
@@ -458,7 +458,7 @@ describe('CombatRewardService', () => {
{
id: BANDIT_HOOD,
key: 'bandit-hood',
name: 'Räuberkapuze',
name: 'Bandit Hood',
type: ItemType.EQUIPMENT,
rarity: ItemRarity.COMMON,
iconPath: '/images/items/bandit-hood.png',

View File

@@ -167,12 +167,12 @@ function createState(): FakeState {
const southGate = location(
SOUTH_GATE_ID,
'south-gate',
'S\u00fcdtor von Graufurt',
'Graufurt South Gate',
);
const burnedRoad = location(
BURNED_ROAD_ID,
'burned-road',
'Verbrannte Stra\u00dfe',
'Burned Road',
);
const character: Character = {
id: CHARACTER_ID,
@@ -237,12 +237,12 @@ describe('TravelService', () => {
originLocation: {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'S\u00fcdtor von Graufurt',
name: 'Graufurt South Gate',
},
targetLocation: {
id: BURNED_ROAD_ID,
key: 'burned-road',
name: 'Verbrannte Stra\u00dfe',
name: 'Burned Road',
},
startedAt: new Date('2026-08-18T10:00:00.000Z'),
arrivesAt: new Date('2026-08-18T10:00:10.000Z'),
@@ -319,12 +319,12 @@ describe('TravelService', () => {
originLocation: {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'S\u00fcdtor von Graufurt',
name: 'Graufurt South Gate',
},
targetLocation: {
id: BURNED_ROAD_ID,
key: 'burned-road',
name: 'Verbrannte Stra\u00dfe',
name: 'Burned Road',
},
startedAt: new Date('2026-08-18T09:59:50.000Z'),
arrivesAt: new Date('2026-08-18T10:00:10.000Z'),
@@ -341,12 +341,12 @@ describe('TravelService', () => {
originLocation: {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'S\u00fcdtor von Graufurt',
name: 'Graufurt South Gate',
},
targetLocation: {
id: BURNED_ROAD_ID,
key: 'burned-road',
name: 'Verbrannte Stra\u00dfe',
name: 'Burned Road',
},
startedAt: new Date('2026-08-18T09:59:50.000Z'),
arrivesAt: new Date('2026-08-18T10:00:00.001Z'),
@@ -367,7 +367,7 @@ describe('TravelService', () => {
targetLocation: {
id: BURNED_ROAD_ID,
key: 'burned-road',
name: 'Verbrannte Stra\u00dfe',
name: 'Burned Road',
},
});
expect(dataSource.state.characters[0].currentLocationId).toBe(

View File

@@ -17,8 +17,8 @@ const CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
const BURNED_ROAD_POIS: LocationPointOfInterestContent[] = [
{
key: 'hunt-area',
title: 'Jagdgebiet',
actionLabel: 'Jagd beginnen',
title: 'Hunting Ground',
actionLabel: 'Begin Hunt',
type: 'HUNT',
iconKey: 'hunt',
xPercent: 52,
@@ -27,42 +27,42 @@ const BURNED_ROAD_POIS: LocationPointOfInterestContent[] = [
},
{
key: 'inspect-tracks',
title: 'Verdächtige Spuren',
actionLabel: 'Untersuchen',
title: 'Suspicious Tracks',
actionLabel: 'Investigate',
type: 'INVESTIGATE',
iconKey: 'investigate',
xPercent: 32,
yPercent: 78,
enabled: true,
resultTitle: 'Verdächtige Spuren',
resultTitle: 'Suspicious Tracks',
resultText:
'Zwischen Asche und zerbrochenen Steinen erkennst du mehrere frische Stiefelabdrücke.',
'Between the ash and broken stones you make out several fresh bootprints.',
},
{
key: 'sealed-crypt',
title: 'Versiegelte Krypta',
title: 'Sealed Crypt',
type: 'DUNGEON',
iconKey: 'search',
xPercent: 90,
yPercent: 20,
enabled: false,
resultTitle: 'Versiegelte Krypta',
resultText: 'Noch verschlossen.',
resultTitle: 'Sealed Crypt',
resultText: 'Still sealed.',
},
];
const SOUTH_GATE_POIS: LocationPointOfInterestContent[] = [
{
key: 'gate-watch',
title: 'Torwache',
actionLabel: 'Sprechen',
title: 'Gate Watch',
actionLabel: 'Talk',
type: 'NPC',
iconKey: 'speak',
xPercent: 45,
yPercent: 52,
enabled: true,
resultTitle: 'Torwache',
resultText: 'Nur am Südtor zu hören.',
resultTitle: 'Gate Watch',
resultText: 'Only heard at the South Gate.',
},
];
@@ -109,8 +109,8 @@ describe('WorldService.runLocalInteraction', () => {
service.runLocalInteraction(CHARACTER_ID, 'inspect-tracks'),
).resolves.toEqual({
interactionKey: 'inspect-tracks',
title: 'Verdächtige Spuren',
text: 'Zwischen Asche und zerbrochenen Steinen erkennst du mehrere frische Stiefelabdrücke.',
title: 'Suspicious Tracks',
text: 'Between the ash and broken stones you make out several fresh bootprints.',
});
});

View File

@@ -19,8 +19,8 @@ describe('WorldController', () => {
it('forwards only the interaction key, never a caller-supplied location', () => {
const runLocalInteraction = jest.fn().mockResolvedValue({
interactionKey: 'inspect-tracks',
title: 'Verdächtige Spuren',
text: 'Frische Stiefelabdrücke.',
title: 'Suspicious Tracks',
text: 'Fresh bootprints.',
});
const controller = new WorldController({
runLocalInteraction,

View File

@@ -20,23 +20,23 @@ const CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
const SOUTH_GATE_POIS: LocationPointOfInterestContent[] = [
{
key: 'gate-watch',
title: 'Torwache',
actionLabel: 'Sprechen',
title: 'Gate Watch',
actionLabel: 'Talk',
type: 'NPC',
iconKey: 'speak',
xPercent: 45,
yPercent: 52,
enabled: true,
resultTitle: 'Torwache',
resultText: 'Geheimer Servertext.',
resultTitle: 'Gate Watch',
resultText: 'Secret server text.',
},
];
const BURNED_ROAD_POIS: LocationPointOfInterestContent[] = [
{
key: 'hunt-area',
title: 'Jagdgebiet',
actionLabel: 'Jagd beginnen',
title: 'Hunting Ground',
actionLabel: 'Begin Hunt',
type: 'HUNT',
iconKey: 'hunt',
xPercent: 52,
@@ -45,42 +45,42 @@ const BURNED_ROAD_POIS: LocationPointOfInterestContent[] = [
},
{
key: 'inspect-tracks',
title: 'Verdächtige Spuren',
actionLabel: 'Untersuchen',
title: 'Suspicious Tracks',
actionLabel: 'Investigate',
type: 'INVESTIGATE',
iconKey: 'investigate',
xPercent: 32,
yPercent: 78,
enabled: true,
resultTitle: 'Verdächtige Spuren',
resultText: 'Frische Stiefelabdrücke führen nach Osten.',
resultTitle: 'Suspicious Tracks',
resultText: 'Fresh bootprints lead east.',
},
{
key: 'sealed-crypt',
title: 'Versiegelte Krypta',
actionLabel: 'Öffnen',
title: 'Sealed Crypt',
actionLabel: 'Open',
type: 'DUNGEON',
iconKey: 'search',
xPercent: 90,
yPercent: 20,
enabled: false,
resultTitle: 'Versiegelte Krypta',
resultText: 'Noch verschlossen.',
resultTitle: 'Sealed Crypt',
resultText: 'Still sealed.',
},
];
const BURNED_ROAD_ACTIONS: LocationPrimaryActionContent[] = [
{
key: 'start-hunt',
label: 'Jagd beginnen',
description: 'Im Gebiet jagen',
label: 'Begin Hunt',
description: 'Hunt in this area',
type: 'HUNT',
iconKey: 'hunt',
enabled: true,
},
{
key: 'investigate-tracks',
label: 'Spuren untersuchen',
label: 'Investigate tracks',
type: 'INVESTIGATE',
iconKey: 'investigate',
enabled: true,
@@ -121,17 +121,17 @@ function poolEntry(
// worst entry this pool reads STRONG; judged by what a traveller actually
// meets it reads MATCH.
const BURNED_ROAD_POOL = [
poolEntry('Aschenratte', 70, { level: 1, attack: 5, armor: 0, maxHp: 45 }),
poolEntry('Straßenräuber', 30, { level: 2, attack: 9, armor: 5, maxHp: 75 }),
poolEntry('Ash Rat', 70, { level: 1, attack: 5, armor: 0, maxHp: 45 }),
poolEntry('Road Bandit', 30, { level: 2, attack: 9, armor: 5, maxHp: 75 }),
];
function currentLocation(): LocationDefinition {
return {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'S\u00fcdtor von Graufurt',
name: 'Graufurt South Gate',
description:
'Das S\u00fcdtor ist der sichere Ausgangspunkt nach S\u00fcden.',
'The South Gate is the safe starting point heading south.',
regionKey: 'ashen-fields',
minRecommendedLevel: 1,
maxRecommendedLevel: 1,
@@ -139,10 +139,10 @@ function currentLocation(): LocationDefinition {
isSafe: true,
huntingEnabled: false,
artworkPath: '/assets/locations/south-gate.webp',
regionName: 'Aschenfelder',
regionTierLabel: 'Gebiet 1',
regionName: 'Ashen Fields',
regionTierLabel: 'Tier 1',
locationType: 'TRANSITION',
localDescription: 'Hinter den Wachtfeuern beginnen die Aschenfelder.',
localDescription: 'Beyond the watch-fires begin the Ashen Fields.',
localArtworkPath: '/images/backgrounds/Suedtor.png',
localPointsOfInterest: SOUTH_GATE_POIS,
localPrimaryActions: [],
@@ -159,8 +159,8 @@ function burnedRoad(): LocationDefinition {
return {
id: BURNED_ROAD_ID,
key: 'burned-road',
name: 'Verbrannte Stra\u00dfe',
description: 'Eine verbrannte Handelsroute durch die Aschenfelder.',
name: 'Burned Road',
description: 'A burned trade route through the Ashen Fields.',
regionKey: 'ashen-fields',
minRecommendedLevel: 1,
maxRecommendedLevel: 2,
@@ -168,14 +168,14 @@ function burnedRoad(): LocationDefinition {
isSafe: false,
huntingEnabled: true,
artworkPath: '/assets/locations/burned-road.webp',
regionName: 'Aschenfelder',
regionTierLabel: 'Gebiet 1',
regionName: 'Ashen Fields',
regionTierLabel: 'Tier 1',
locationType: 'HUNTING_GROUND',
localDescription: 'Ein alter Handelsweg, in Asche gelegt.',
localDescription: 'An old trade road, burned to ash.',
localArtworkPath: '/images/backgrounds/Aschestrasse.png',
localPointsOfInterest: BURNED_ROAD_POIS,
localPrimaryActions: BURNED_ROAD_ACTIONS,
localRewardPreview: [{ key: 'silver', label: 'Silber', iconKey: 'silver' }],
localRewardPreview: [{ key: 'silver', label: 'Silver', iconKey: 'silver' }],
createdAt: new Date('2026-08-18T09:00:00.000Z'),
updatedAt: new Date('2026-08-18T09:00:00.000Z'),
characters: [],
@@ -244,9 +244,9 @@ describe('WorldService', () => {
expect(result).toEqual({
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'S\u00fcdtor von Graufurt',
name: 'Graufurt South Gate',
description:
'Das S\u00fcdtor ist der sichere Ausgangspunkt nach S\u00fcden.',
'The South Gate is the safe starting point heading south.',
regionKey: 'ashen-fields',
minRecommendedLevel: 1,
maxRecommendedLevel: 1,
@@ -254,18 +254,18 @@ describe('WorldService', () => {
isSafe: true,
huntingEnabled: false,
artworkPath: '/assets/locations/south-gate.webp',
regionName: 'Aschenfelder',
regionTierLabel: 'Gebiet 1',
regionName: 'Ashen Fields',
regionTierLabel: 'Tier 1',
locationType: 'TRANSITION',
localDescription: 'Hinter den Wachtfeuern beginnen die Aschenfelder.',
localDescription: 'Beyond the watch-fires begin the Ashen Fields.',
localArtworkPath: '/images/backgrounds/Suedtor.png',
dangerRating: null,
recommendationLabel: '1',
pointsOfInterest: [
{
key: 'gate-watch',
title: 'Torwache',
actionLabel: 'Sprechen',
title: 'Gate Watch',
actionLabel: 'Talk',
type: 'NPC',
iconKey: 'speak',
xPercent: 45,
@@ -281,7 +281,7 @@ describe('WorldService', () => {
targetLocation: {
id: BURNED_ROAD_ID,
key: 'burned-road',
name: 'Verbrannte Stra\u00dfe',
name: 'Burned Road',
},
travelDurationSeconds: 10,
danger: 'LOW',
@@ -325,8 +325,8 @@ describe('WorldService', () => {
const result = await service.getCurrentLocation(CHARACTER_ID);
expect(result.possibleMonsters).toEqual([
'Aschenratte',
'Stra\u00dfenr\u00e4uber',
'Ash Rat',
'Road Bandit',
]);
expect(findLocationMonsters).toHaveBeenCalledWith({
where: { locationId: BURNED_ROAD_ID, enabled: true },
@@ -373,13 +373,13 @@ describe('WorldService', () => {
expect(result).toEqual(
expect.objectContaining({
regionName: 'Aschenfelder',
regionTierLabel: 'Gebiet 1',
regionName: 'Ashen Fields',
regionTierLabel: 'Tier 1',
locationType: 'HUNTING_GROUND',
localDescription: 'Ein alter Handelsweg, in Asche gelegt.',
localDescription: 'An old trade road, burned to ash.',
localArtworkPath: '/images/backgrounds/Aschestrasse.png',
recommendationLabel: '12',
rewardPreview: [{ key: 'silver', label: 'Silber', iconKey: 'silver' }],
rewardPreview: [{ key: 'silver', label: 'Silver', iconKey: 'silver' }],
}),
);
expect(result.primaryActions).toEqual(BURNED_ROAD_ACTIONS);
@@ -397,8 +397,8 @@ describe('WorldService', () => {
expect(result.pointsOfInterest).toEqual([
{
key: 'hunt-area',
title: 'Jagdgebiet',
actionLabel: 'Jagd beginnen',
title: 'Hunting Ground',
actionLabel: 'Begin Hunt',
type: 'HUNT',
iconKey: 'hunt',
xPercent: 52,
@@ -407,8 +407,8 @@ describe('WorldService', () => {
},
{
key: 'inspect-tracks',
title: 'Verdächtige Spuren',
actionLabel: 'Untersuchen',
title: 'Suspicious Tracks',
actionLabel: 'Investigate',
type: 'INVESTIGATE',
iconKey: 'investigate',
xPercent: 32,
@@ -417,8 +417,8 @@ describe('WorldService', () => {
},
{
key: 'sealed-crypt',
title: 'Versiegelte Krypta',
actionLabel: 'Öffnen',
title: 'Sealed Crypt',
actionLabel: 'Open',
type: 'DUNGEON',
iconKey: 'search',
xPercent: 90,
@@ -426,8 +426,8 @@ describe('WorldService', () => {
enabled: false,
},
]);
expect(JSON.stringify(result)).not.toContain('Frische Stiefelabdrücke');
expect(JSON.stringify(result)).not.toContain('Noch verschlossen');
expect(JSON.stringify(result)).not.toContain('Fresh bootprints');
expect(JSON.stringify(result)).not.toContain('Still sealed');
});
it('derives the encounter preview from the location monster pool', async () => {
@@ -435,16 +435,16 @@ describe('WorldService', () => {
expect(result.encounterPreview).toEqual([
{
key: 'aschenratte',
name: 'Aschenratte',
key: 'ash rat',
name: 'Ash Rat',
level: 1,
iconPath: '/images/monsters/icons/aschenratte-128.png',
iconPath: '/images/monsters/icons/ash rat-128.png',
},
{
key: 'straßenräuber',
name: 'Straßenräuber',
key: 'road bandit',
name: 'Road Bandit',
level: 2,
iconPath: '/images/monsters/icons/straßenräuber-128.png',
iconPath: '/images/monsters/icons/road bandit-128.png',
},
]);
});