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:
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: '1–2',
|
||||
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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('App', () => {
|
||||
attack: 1,
|
||||
hpRegenPerSecond: 1,
|
||||
hpRegenSince: null,
|
||||
currentLocation: { id: 'location-id', key: 'south-gate', name: 'Südtor von Graufurt' },
|
||||
currentLocation: { id: 'location-id', key: 'south-gate', name: 'Graufurt South Gate' },
|
||||
};
|
||||
const value: CharacterResponse = {
|
||||
id: 'character-id',
|
||||
@@ -173,7 +173,7 @@ describe('App', () => {
|
||||
attack: 12,
|
||||
hpRegenPerSecond: 1,
|
||||
hpRegenSince: null,
|
||||
currentLocation: { id: 'location-id', key: 'south-gate', name: 'Südtor von Graufurt' },
|
||||
currentLocation: { id: 'location-id', key: 'south-gate', name: 'Graufurt South Gate' },
|
||||
};
|
||||
character.set(decoy);
|
||||
displayedCharacter.set(value);
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('GameApiService', () => {
|
||||
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: '…' });
|
||||
request.flush({ interactionKey: 'inspect-tracks', title: 'Suspicious Tracks', text: '…' });
|
||||
});
|
||||
|
||||
it('escapes an interaction key so it cannot break out of its path segment', () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ const runningCombat: Combat = {
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 62, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'road-bandit',
|
||||
name: 'Straßenräuber',
|
||||
name: 'Road Bandit',
|
||||
level: 3,
|
||||
maxHp: 75,
|
||||
currentHp: 30,
|
||||
|
||||
@@ -14,7 +14,7 @@ const activeCombat: Combat = {
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 95, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Aschenratte',
|
||||
name: 'Ash Rat',
|
||||
level: 1,
|
||||
maxHp: 45,
|
||||
currentHp: 31,
|
||||
@@ -28,7 +28,7 @@ const activeCombat: Combat = {
|
||||
rewards: null,
|
||||
};
|
||||
|
||||
const monsterHitLine = 'Aschenratte hits Aric Duskwalker for 5 damage.';
|
||||
const monsterHitLine = 'Ash Rat hits Aric Duskwalker for 5 damage.';
|
||||
|
||||
function countOccurrences(haystack: string | null, needle: string): number {
|
||||
return haystack ? haystack.split(needle).length - 1 : 0;
|
||||
@@ -97,7 +97,7 @@ describe('CombatPageComponent', () => {
|
||||
|
||||
expect(element.textContent).toContain('Aric Duskwalker');
|
||||
expect(element.textContent).toContain('95 / 100');
|
||||
expect(element.textContent).toContain('Aschenratte');
|
||||
expect(element.textContent).toContain('Ash Rat');
|
||||
expect(element.textContent).toContain('31 / 45');
|
||||
expect(element.querySelector('[data-combat-round]')?.textContent).toContain('Round 2');
|
||||
expect(element.querySelector('[data-combat-attack]')).toBeTruthy();
|
||||
@@ -169,7 +169,7 @@ describe('CombatPageComponent', () => {
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.querySelector('[data-combat-telegraph]')?.textContent).toContain(
|
||||
'Aschenratte is winding up a Heavy Strike.',
|
||||
'Ash Rat is winding up a Heavy Strike.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -194,16 +194,16 @@ describe('CombatPageComponent', () => {
|
||||
|
||||
expect(element.textContent).toContain('Aric Duskwalker drinks a potion and heals 35 HP.');
|
||||
expect(element.textContent).toContain('Aric Duskwalker braces to defend.');
|
||||
expect(element.textContent).toContain('Aschenratte is winding up a Heavy Strike.');
|
||||
expect(element.textContent).toContain("Aric Duskwalker interrupts Aschenratte's attack.");
|
||||
expect(element.textContent).toContain('Ash Rat is winding up a Heavy Strike.');
|
||||
expect(element.textContent).toContain("Aric Duskwalker interrupts Ash Rat's attack.");
|
||||
});
|
||||
|
||||
it('renders the structured events as readable English combat-log entries', async () => {
|
||||
const fixture = await setup(activeCombat);
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.textContent).toContain('Aric Duskwalker hits Aschenratte for 14 damage.');
|
||||
expect(element.textContent).toContain('Aschenratte hits Aric Duskwalker for 5 damage.');
|
||||
expect(element.textContent).toContain('Aric Duskwalker hits Ash Rat for 14 damage.');
|
||||
expect(element.textContent).toContain('Ash Rat hits Aric Duskwalker for 5 damage.');
|
||||
});
|
||||
|
||||
it('calls combatStore.performAction("ATTACK") when Attack is clicked', async () => {
|
||||
@@ -334,7 +334,7 @@ describe('CombatPageComponent', () => {
|
||||
await vi.advanceTimersByTimeAsync(540);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.textContent).toContain("Aric Duskwalker interrupts Aschenratte's attack.");
|
||||
expect(element.textContent).toContain("Aric Duskwalker interrupts Ash Rat's attack.");
|
||||
expect(element.querySelector('[data-combat-telegraph]')).toBeNull();
|
||||
expect(monster?.classList.contains('sprite--lunge')).toBe(false);
|
||||
});
|
||||
@@ -557,7 +557,7 @@ describe('CombatPageComponent', () => {
|
||||
it('renders a dropped item with its icon, name, and rarity', async () => {
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
monster: { ...activeCombat.monster, key: 'road-bandit', name: 'Straßenräuber', currentHp: 0 },
|
||||
monster: { ...activeCombat.monster, key: 'road-bandit', name: 'Road Bandit', currentHp: 0 },
|
||||
rewards: {
|
||||
silver: 12,
|
||||
items: [
|
||||
@@ -565,7 +565,7 @@ describe('CombatPageComponent', () => {
|
||||
characterItemId: 'character-item-1',
|
||||
item: {
|
||||
key: 'bandit-blade',
|
||||
name: 'Räuberklinge',
|
||||
name: 'Bandit Blade',
|
||||
rarity: 'COMMON',
|
||||
iconPath: '/images/items/bandit-blade.png',
|
||||
},
|
||||
@@ -579,7 +579,7 @@ describe('CombatPageComponent', () => {
|
||||
expect(element.querySelector<HTMLImageElement>('[data-item-icon]')?.getAttribute('src')).toBe(
|
||||
'/images/items/bandit-blade.png',
|
||||
);
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain('Räuberklinge');
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain('Bandit Blade');
|
||||
expect(element.querySelector('[data-item-rarity]')?.textContent).toContain('Common');
|
||||
expect(element.querySelector('[data-reward-empty]')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ const startedCombat: Combat = {
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Aschenratte',
|
||||
name: 'Ash Rat',
|
||||
level: 1,
|
||||
maxHp: 45,
|
||||
currentHp: 45,
|
||||
|
||||
@@ -7,7 +7,7 @@ const dawnwolfEncounter: HuntEncounter = {
|
||||
id: 'encounter-id-9f2c',
|
||||
monster: {
|
||||
key: 'dawnwolf',
|
||||
name: 'Dämmerwolf',
|
||||
name: 'Dawnwolf',
|
||||
level: 3,
|
||||
artworkPath: '/images/enemies/Dawnwolf.png',
|
||||
},
|
||||
@@ -19,7 +19,7 @@ const ashRatEncounter: HuntEncounter = {
|
||||
id: 'encounter-id-4b71',
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Aschenratte',
|
||||
name: 'Ash Rat',
|
||||
level: 1,
|
||||
artworkPath: '/images/monsters/ash-rat.png',
|
||||
},
|
||||
@@ -45,11 +45,11 @@ describe('EncounterCardComponent', () => {
|
||||
const element = render(dawnwolfEncounter);
|
||||
const artwork = element.querySelector('.encounter-card__artwork');
|
||||
|
||||
expect(element.textContent).toContain('Dämmerwolf');
|
||||
expect(element.textContent).toContain('Dawnwolf');
|
||||
expect(element.textContent).toContain('Level 3');
|
||||
expect(element.textContent).toContain('Suitable');
|
||||
expect(artwork?.getAttribute('src')).toBe('/images/enemies/Dawnwolf.png');
|
||||
expect(artwork?.getAttribute('alt')).toBe('Dämmerwolf');
|
||||
expect(artwork?.getAttribute('alt')).toBe('Dawnwolf');
|
||||
});
|
||||
|
||||
it('shows the background-free cut-out instead of the framed artwork when one exists', () => {
|
||||
|
||||
@@ -19,11 +19,11 @@ const burnedRoad = burnedRoadFixture({ connections: [] });
|
||||
|
||||
const threeEncounterHunt: HuntResult = {
|
||||
id: 'hunt-id',
|
||||
location: { id: 'burned-road-id', key: 'burned-road', name: 'Verbrannte Straße' },
|
||||
location: { id: 'burned-road-id', key: 'burned-road', name: 'Burned Road' },
|
||||
encounters: [
|
||||
{
|
||||
id: 'encounter-1',
|
||||
monster: { key: 'ash-rat', name: 'Aschenratte', level: 1, artworkPath: '/images/enemies/AshRat.png' },
|
||||
monster: { key: 'ash-rat', name: 'Ash Rat', level: 1, artworkPath: '/images/enemies/AshRat.png' },
|
||||
dangerRating: 'WEAK',
|
||||
status: 'AVAILABLE',
|
||||
},
|
||||
@@ -31,7 +31,7 @@ const threeEncounterHunt: HuntResult = {
|
||||
id: 'encounter-2',
|
||||
monster: {
|
||||
key: 'road-bandit',
|
||||
name: 'Straßenräuber',
|
||||
name: 'Road Bandit',
|
||||
level: 3,
|
||||
artworkPath: '/images/enemies/RoadBandit.png',
|
||||
},
|
||||
@@ -40,7 +40,7 @@ const threeEncounterHunt: HuntResult = {
|
||||
},
|
||||
{
|
||||
id: 'encounter-3',
|
||||
monster: { key: 'ash-rat', name: 'Aschenratte', level: 1, artworkPath: '/images/enemies/AshRat.png' },
|
||||
monster: { key: 'ash-rat', name: 'Ash Rat', level: 1, artworkPath: '/images/enemies/AshRat.png' },
|
||||
dangerRating: 'WEAK',
|
||||
status: 'AVAILABLE',
|
||||
},
|
||||
@@ -54,7 +54,7 @@ const startedCombat: Combat = {
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'road-bandit',
|
||||
name: 'Straßenräuber',
|
||||
name: 'Road Bandit',
|
||||
level: 3,
|
||||
maxHp: 75,
|
||||
currentHp: 75,
|
||||
@@ -130,7 +130,7 @@ describe('HuntPageComponent', () => {
|
||||
return fixture;
|
||||
}
|
||||
|
||||
it('shows the hunting-unavailable state at the Südtor, with no Begin Hunt button, and a way back to the location', async () => {
|
||||
it('shows the hunting-unavailable state at the South Gate, with no Begin Hunt button, and a way back to the location', async () => {
|
||||
const fixture = await setup(southGate);
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
@@ -164,7 +164,7 @@ describe('HuntPageComponent', () => {
|
||||
|
||||
const cards = element.querySelectorAll('app-encounter-card');
|
||||
expect(cards.length).toBe(3);
|
||||
expect(element.textContent).toMatch(/Aschenratte[\s\S]*Straßenräuber[\s\S]*Aschenratte/);
|
||||
expect(element.textContent).toMatch(/Ash Rat[\s\S]*Road Bandit[\s\S]*Ash Rat/);
|
||||
expect(
|
||||
element.querySelectorAll(
|
||||
'.encounter-card__artwork[src="/images/combat/sprites/ash-rat-760.png"]',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { HuntingStore } from './hunting.store';
|
||||
|
||||
const huntResult: HuntResult = {
|
||||
id: 'hunt-id',
|
||||
location: { id: 'origin-id', key: 'south-gate', name: 'Südtor' },
|
||||
location: { id: 'origin-id', key: 'south-gate', name: 'South Gate' },
|
||||
encounters: [
|
||||
{
|
||||
id: 'encounter-1',
|
||||
@@ -18,7 +18,7 @@ const huntResult: HuntResult = {
|
||||
},
|
||||
{
|
||||
id: 'encounter-2',
|
||||
monster: { key: 'bear', name: 'Bär', level: 3, artworkPath: '/images/enemies/Bear.png' },
|
||||
monster: { key: 'bear', name: 'Bear', level: 3, artworkPath: '/images/enemies/Bear.png' },
|
||||
dangerRating: 'STRONG',
|
||||
status: 'DEFEATED',
|
||||
},
|
||||
@@ -27,11 +27,11 @@ const huntResult: HuntResult = {
|
||||
|
||||
const refreshedHuntResult: HuntResult = {
|
||||
id: 'hunt-id-2',
|
||||
location: { id: 'origin-id', key: 'south-gate', name: 'Südtor' },
|
||||
location: { id: 'origin-id', key: 'south-gate', name: 'South Gate' },
|
||||
encounters: [
|
||||
{
|
||||
id: 'encounter-3',
|
||||
monster: { key: 'rat', name: 'Ratte', level: 1, artworkPath: '/images/enemies/Rat.png' },
|
||||
monster: { key: 'rat', name: 'Rat', level: 1, artworkPath: '/images/enemies/Rat.png' },
|
||||
dangerRating: 'WEAK',
|
||||
status: 'AVAILABLE',
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ const wornSword: InventoryItem = {
|
||||
item: {
|
||||
key: 'worn-short-sword',
|
||||
name: 'Abgenutztes Kurzschwert',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 8,
|
||||
@@ -27,8 +27,8 @@ const banditBlade: InventoryItem = {
|
||||
equipped: false,
|
||||
item: {
|
||||
key: 'bandit-blade',
|
||||
name: 'Räuberklinge',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
name: 'Bandit Blade',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 11,
|
||||
@@ -46,7 +46,7 @@ const ashPelt: InventoryItem = {
|
||||
item: {
|
||||
key: 'ash-pelt',
|
||||
name: 'Aschenfell',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: null,
|
||||
weaponDamage: 0,
|
||||
|
||||
@@ -11,10 +11,10 @@ interface StatRow {
|
||||
|
||||
type StatKey = 'weaponDamage' | 'bonusAttack' | 'bonusHp' | 'bonusArmor';
|
||||
const STAT_LABELS: ReadonlyArray<{ label: string; key: StatKey }> = [
|
||||
{ label: 'Waffenschaden', key: 'weaponDamage' },
|
||||
{ label: 'Angriff', key: 'bonusAttack' },
|
||||
{ label: 'Leben', key: 'bonusHp' },
|
||||
{ label: 'Rüstung', key: 'bonusArmor' },
|
||||
{ label: 'Weapon Damage', key: 'weaponDamage' },
|
||||
{ label: 'Attack', key: 'bonusAttack' },
|
||||
{ label: 'Health', key: 'bonusHp' },
|
||||
{ label: 'Armor', key: 'bonusArmor' },
|
||||
];
|
||||
|
||||
/** Selected-item details and equip comparison (spec §32–37). */
|
||||
|
||||
@@ -17,7 +17,7 @@ const inventory: InventoryResponse = {
|
||||
item: {
|
||||
key: 'worn-short-sword',
|
||||
name: 'Abgenutztes Kurzschwert',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 8,
|
||||
@@ -33,8 +33,8 @@ const inventory: InventoryResponse = {
|
||||
equipped: false,
|
||||
item: {
|
||||
key: 'bandit-blade',
|
||||
name: 'Räuberklinge',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
name: 'Bandit Blade',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 11,
|
||||
@@ -70,7 +70,7 @@ const character: CharacterResponse = {
|
||||
attack: 6,
|
||||
hpRegenPerSecond: 1,
|
||||
hpRegenSince: null,
|
||||
currentLocation: { id: 'loc-1', key: 'south-gate', name: 'Südtor' },
|
||||
currentLocation: { id: 'loc-1', key: 'south-gate', name: 'South Gate' },
|
||||
};
|
||||
|
||||
interface SetupOptions {
|
||||
@@ -176,8 +176,8 @@ describe('InventoryPageComponent', () => {
|
||||
equipped: true,
|
||||
item: {
|
||||
key: 'iron-helm',
|
||||
name: 'Eiserner Helm',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
name: 'Iron Helm',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'HEAD',
|
||||
weaponDamage: 0,
|
||||
|
||||
@@ -16,7 +16,7 @@ const inventory: InventoryResponse = {
|
||||
item: {
|
||||
key: 'worn-short-sword',
|
||||
name: 'Abgenutztes Kurzschwert',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 8,
|
||||
@@ -32,8 +32,8 @@ const inventory: InventoryResponse = {
|
||||
equipped: false,
|
||||
item: {
|
||||
key: 'bandit-blade',
|
||||
name: 'Räuberklinge',
|
||||
description: 'Beschreibung des Gegenstands.',
|
||||
name: 'Bandit Blade',
|
||||
description: 'Item description.',
|
||||
rarity: 'COMMON',
|
||||
equipmentSlot: 'WEAPON',
|
||||
weaponDamage: 11,
|
||||
@@ -61,7 +61,7 @@ const equipment: EquipmentResponse = {
|
||||
|
||||
const equippedAfter: EquipmentResponse = {
|
||||
...equipment,
|
||||
slots: { ...equipment.slots, WEAPON: { characterItemId: 'item-blade', item: { key: 'bandit-blade', name: 'Räuberklinge', rarity: 'COMMON', iconPath: '/images/items/bandit-blade.png' } } },
|
||||
slots: { ...equipment.slots, WEAPON: { characterItemId: 'item-blade', item: { key: 'bandit-blade', name: 'Bandit Blade', rarity: 'COMMON', iconPath: '/images/items/bandit-blade.png' } } },
|
||||
stats: { maxHp: 100, attack: 7, weaponDamage: 11, armor: 0 },
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { WorldStore } from './world.store';
|
||||
|
||||
const trackResult = {
|
||||
interactionKey: 'inspect-tracks',
|
||||
title: 'Verdächtige Spuren',
|
||||
text: 'Frische Stiefelabdrücke führen nach Osten.',
|
||||
title: 'Suspicious Tracks',
|
||||
text: 'Among the ash and broken stones you make out several fresh bootprints. They lead east, toward the abandoned watchpost.',
|
||||
};
|
||||
|
||||
function setup(api: Partial<GameApiService> = {}, world: Partial<WorldStore> = {}) {
|
||||
@@ -128,7 +128,7 @@ describe('LocalLocationStore', () => {
|
||||
expect(
|
||||
store.interactionKeyOf({
|
||||
key: 'investigate-tracks',
|
||||
label: 'Spuren untersuchen',
|
||||
label: 'Investigate tracks',
|
||||
type: 'INVESTIGATE',
|
||||
iconKey: 'investigate',
|
||||
enabled: true,
|
||||
|
||||
@@ -37,25 +37,27 @@ describe('LocationInteractionPanelComponent', () => {
|
||||
const { element } = await setup({
|
||||
result: {
|
||||
interactionKey: 'inspect-tracks',
|
||||
title: 'Verdächtige Spuren',
|
||||
text: 'Frische Stiefelabdrücke führen nach Osten.',
|
||||
title: 'Suspicious Tracks',
|
||||
text: 'Among the ash and broken stones you make out several fresh bootprints. They lead east, toward the abandoned watchpost.',
|
||||
},
|
||||
});
|
||||
|
||||
expect(element.textContent).toContain('Verdächtige Spuren');
|
||||
expect(element.textContent).toContain('Frische Stiefelabdrücke führen nach Osten.');
|
||||
expect(element.textContent).toContain('Suspicious Tracks');
|
||||
expect(element.textContent).toContain(
|
||||
'Among the ash and broken stones you make out several fresh bootprints. They lead east, toward the abandoned watchpost.',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows an NPC line through the same panel, with no branching choices', async () => {
|
||||
const { element } = await setup({
|
||||
result: {
|
||||
interactionKey: 'wounded-scout',
|
||||
title: 'Verwundeter Kundschafter',
|
||||
text: '„Die Straße ist nicht mehr sicher."',
|
||||
title: 'Wounded Scout',
|
||||
text: '"The road isn\'t safe anymore. The raiders are coming from the direction of the old watchpost. If you keep going, keep your eyes open."',
|
||||
},
|
||||
});
|
||||
|
||||
expect(element.textContent).toContain('Verwundeter Kundschafter');
|
||||
expect(element.textContent).toContain('Wounded Scout');
|
||||
// Exactly one control: close. A dialogue tree is out of scope here.
|
||||
expect(element.querySelectorAll('button')).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -62,11 +62,11 @@ describe('LocationPageComponent', () => {
|
||||
it('names the place and its region straight away', async () => {
|
||||
const { element } = await setup();
|
||||
|
||||
expect(element.querySelector('h1')?.textContent).toContain('Verbrannte Straße');
|
||||
expect(element.textContent).toContain('Gebiet 1');
|
||||
expect(element.textContent).toContain('Aschenfelder');
|
||||
expect(element.querySelector('h1')?.textContent).toContain('Burned Road');
|
||||
expect(element.textContent).toContain('Tier 1');
|
||||
expect(element.textContent).toContain('Ashen Fields');
|
||||
expect(element.textContent).toContain(
|
||||
'Ein alter Handelsweg, der durch Feuer und Krieg in Asche gelegt wurde.',
|
||||
'An old trade road, burned to ash by fire and war. Charred carts, broken weapons, and silenced cries line the path into the Ashen Fields.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('LocationPageComponent', () => {
|
||||
const image = element.querySelector('img') as HTMLImageElement;
|
||||
|
||||
expect(image.getAttribute('src')).toBe('/images/backgrounds/Aschestrasse.png');
|
||||
expect(image.getAttribute('alt')).toBe('Location view: Verbrannte Straße');
|
||||
expect(image.getAttribute('alt')).toBe('Location view: Burned Road');
|
||||
});
|
||||
|
||||
it('places every hotspot on the artwork', async () => {
|
||||
@@ -96,10 +96,10 @@ describe('LocationPageComponent', () => {
|
||||
const actions = element.querySelectorAll('[data-action]');
|
||||
|
||||
expect([...actions].map((action) => action.querySelector('.location-action__label')?.textContent?.trim())).toEqual([
|
||||
'Jagd beginnen',
|
||||
'Spuren untersuchen',
|
||||
'Umgebung durchsuchen',
|
||||
'Zur Karte',
|
||||
'Begin Hunt',
|
||||
'Investigate tracks',
|
||||
'Search surroundings',
|
||||
'To Map',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -173,13 +173,13 @@ describe('LocationPageComponent', () => {
|
||||
|
||||
interactionResult.set({
|
||||
interactionKey: 'inspect-tracks',
|
||||
title: 'Verdächtige Spuren',
|
||||
text: 'Frische Stiefelabdrücke führen nach Osten.',
|
||||
title: 'Suspicious Tracks',
|
||||
text: 'Among the ash and broken stones you make out several fresh bootprints. They lead east, toward the abandoned watchpost.',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.querySelector('[data-interaction-panel]')?.textContent).toContain(
|
||||
'Frische Stiefelabdrücke führen nach Osten.',
|
||||
'Among the ash and broken stones you make out several fresh bootprints. They lead east, toward the abandoned watchpost.',
|
||||
);
|
||||
// The scene is not replaced by the panel.
|
||||
expect(element.querySelector('img')).not.toBeNull();
|
||||
@@ -234,7 +234,7 @@ describe('LocationPageComponent', () => {
|
||||
it('renders a second location from its own data alone', async () => {
|
||||
const { element } = await setup(southGateFixture(southGateContent()));
|
||||
|
||||
expect(element.querySelector('h1')?.textContent).toContain('Südtor von Graufurt');
|
||||
expect(element.querySelector('h1')?.textContent).toContain('Graufurt South Gate');
|
||||
expect(element.querySelectorAll('app-location-poi')).toHaveLength(1);
|
||||
expect(element.querySelectorAll('[data-action]')).toHaveLength(1);
|
||||
});
|
||||
@@ -245,8 +245,8 @@ function southGateContent(): Partial<CurrentLocationResponse> {
|
||||
pointsOfInterest: [
|
||||
{
|
||||
key: 'gate-watch',
|
||||
title: 'Torwache',
|
||||
actionLabel: 'Sprechen',
|
||||
title: 'Gate Watch',
|
||||
actionLabel: 'Talk',
|
||||
type: 'NPC',
|
||||
iconKey: 'speak',
|
||||
xPercent: 45,
|
||||
@@ -257,8 +257,8 @@ function southGateContent(): Partial<CurrentLocationResponse> {
|
||||
primaryActions: [
|
||||
{
|
||||
key: 'talk-to-watch',
|
||||
label: 'Wache ansprechen',
|
||||
description: 'Lage erfragen',
|
||||
label: 'Talk to the watch',
|
||||
description: 'Ask about the situation',
|
||||
type: 'NPC',
|
||||
iconKey: 'speak',
|
||||
enabled: true,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { LocationPoiComponent } from './location-poi.component';
|
||||
|
||||
const wagon: LocationPointOfInterest = {
|
||||
key: 'search-abandoned-wagon',
|
||||
title: 'Verlassener Wagen',
|
||||
actionLabel: 'Durchsuchen',
|
||||
title: 'Abandoned Wagon',
|
||||
actionLabel: 'Search',
|
||||
type: 'SEARCH',
|
||||
iconKey: 'search',
|
||||
xPercent: 80,
|
||||
@@ -53,8 +53,8 @@ describe('LocationPoiComponent', () => {
|
||||
const { fixture } = await setup();
|
||||
const text = (fixture.nativeElement as HTMLElement).textContent ?? '';
|
||||
|
||||
expect(text).toContain('Verlassener Wagen');
|
||||
expect(text).toContain('Durchsuchen');
|
||||
expect(text).toContain('Abandoned Wagon');
|
||||
expect(text).toContain('Search');
|
||||
});
|
||||
|
||||
it('emits the whole hotspot when clicked', async () => {
|
||||
@@ -102,7 +102,7 @@ describe('LocationPoiComponent', () => {
|
||||
it('describes itself with both title and action for screen readers', async () => {
|
||||
const { button } = await setup();
|
||||
|
||||
expect(button.getAttribute('aria-label')).toBe('Verlassener Wagen: Durchsuchen');
|
||||
expect(button.getAttribute('aria-label')).toBe('Abandoned Wagon: Search');
|
||||
});
|
||||
|
||||
it('falls back to the title alone when a hotspot has no action label', async () => {
|
||||
@@ -111,6 +111,6 @@ describe('LocationPoiComponent', () => {
|
||||
actionLabel: undefined,
|
||||
});
|
||||
|
||||
expect(button.getAttribute('aria-label')).toBe('Verlassener Wagen');
|
||||
expect(button.getAttribute('aria-label')).toBe('Abandoned Wagon');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,8 +20,8 @@ describe('LocationSidebarComponent', () => {
|
||||
const element = await render(burnedRoadFixture());
|
||||
const text = element.textContent ?? '';
|
||||
|
||||
expect(text).toContain('Aschenfelder');
|
||||
expect(text).toContain('Verbrannte Straße');
|
||||
expect(text).toContain('Ashen Fields');
|
||||
expect(text).toContain('Burned Road');
|
||||
expect(text).toContain('Hunting Ground');
|
||||
expect(text).toContain('1–2');
|
||||
});
|
||||
@@ -44,8 +44,8 @@ describe('LocationSidebarComponent', () => {
|
||||
const encounters = element.querySelectorAll('[data-sidebar="encounters"] li');
|
||||
|
||||
expect([...encounters].map((item) => item.textContent?.trim())).toEqual([
|
||||
'Aschenratte',
|
||||
'Straßenräuber',
|
||||
'Ash Rat',
|
||||
'Road Bandit',
|
||||
]);
|
||||
expect(element.textContent).toContain('the hunt rolls independently');
|
||||
});
|
||||
@@ -55,10 +55,10 @@ describe('LocationSidebarComponent', () => {
|
||||
const interactions = element.querySelectorAll('[data-sidebar="interactions"] li');
|
||||
|
||||
expect([...interactions].map((item) => item.textContent?.trim())).toEqual([
|
||||
'Jagd beginnen',
|
||||
'Untersuchen',
|
||||
'Durchsuchen',
|
||||
'Sprechen',
|
||||
'Begin Hunt',
|
||||
'Investigate',
|
||||
'Search',
|
||||
'Talk',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('LocationSidebarComponent', () => {
|
||||
});
|
||||
|
||||
expect(element.querySelector('[data-sidebar="interactions"]')?.textContent).not.toContain(
|
||||
'Sprechen',
|
||||
'Talk',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('LocationSidebarComponent', () => {
|
||||
const rewards = element.querySelectorAll('[data-sidebar="rewards"] li');
|
||||
|
||||
expect([...rewards].map((item) => item.textContent?.trim())).toEqual([
|
||||
'Ausrüstung',
|
||||
'Equipment',
|
||||
'Material',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ const burnedRoadConnection: CurrentLocationConnection = {
|
||||
targetLocation: {
|
||||
id: 'burned-road-id',
|
||||
key: 'burned-road',
|
||||
name: 'Verbrannte Straße',
|
||||
name: 'Burned Road',
|
||||
},
|
||||
travelDurationSeconds: 10,
|
||||
danger: 'LOW',
|
||||
@@ -84,7 +84,7 @@ describe('WorldPageComponent', () => {
|
||||
|
||||
expect(store.selectConnection).toHaveBeenCalledWith(burnedRoadConnection);
|
||||
expect(element.textContent).toContain('Destination');
|
||||
expect(element.textContent).toContain('Verbrannte Straße');
|
||||
expect(element.textContent).toContain('Burned Road');
|
||||
expect(element.textContent).toContain('00:00:10');
|
||||
expect(element.textContent).toContain('Low');
|
||||
});
|
||||
@@ -129,7 +129,7 @@ describe('WorldPageComponent', () => {
|
||||
it('renders the server-derived countdown while travel is active without enabling another start', () => {
|
||||
store.currentTravel.set({
|
||||
status: 'TRAVELLING',
|
||||
originLocation: { id: 'south-gate-id', key: 'south-gate', name: 'Südtor von Graufurt' },
|
||||
originLocation: { id: 'south-gate-id', key: 'south-gate', name: 'Graufurt South Gate' },
|
||||
targetLocation: burnedRoadConnection.targetLocation,
|
||||
startedAt: '2026-08-19T06:00:00.000Z',
|
||||
arrivesAt: '2026-08-19T06:00:06.000Z',
|
||||
@@ -140,7 +140,7 @@ describe('WorldPageComponent', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
expect(element.textContent).toContain('Destination: Verbrannte Straße');
|
||||
expect(element.textContent).toContain('Destination: Burned Road');
|
||||
expect(element.textContent).toContain('Arrival in');
|
||||
expect(element.textContent).toContain('00:00:06');
|
||||
expect(element.querySelector<HTMLButtonElement>('[data-travel-start]')?.disabled).toBe(true);
|
||||
@@ -194,7 +194,7 @@ describe('WorldPageComponent', () => {
|
||||
store.arrived.set({
|
||||
id: 'burned-road-id',
|
||||
key: 'burned-road',
|
||||
name: 'Verbrannte Straße',
|
||||
name: 'Burned Road',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -207,7 +207,7 @@ describe('WorldPageComponent', () => {
|
||||
it('stays on the map while a journey is still running', () => {
|
||||
store.currentTravel.set({
|
||||
status: 'TRAVELLING',
|
||||
originLocation: { id: 'south-gate-id', key: 'south-gate', name: 'Südtor von Graufurt' },
|
||||
originLocation: { id: 'south-gate-id', key: 'south-gate', name: 'Graufurt South Gate' },
|
||||
targetLocation: burnedRoadConnection.targetLocation,
|
||||
startedAt: '2026-08-20T10:00:00.000Z',
|
||||
arrivesAt: '2026-08-20T10:00:10.000Z',
|
||||
|
||||
@@ -21,12 +21,12 @@ const character: CharacterResponse = {
|
||||
attack: 6,
|
||||
hpRegenPerSecond: 1,
|
||||
hpRegenSince: null,
|
||||
currentLocation: { id: 'origin-id', key: 'south-gate', name: 'Südtor' },
|
||||
currentLocation: { id: 'origin-id', key: 'south-gate', name: 'South Gate' },
|
||||
};
|
||||
|
||||
const currentLocation = southGateFixture({
|
||||
id: 'origin-id',
|
||||
name: 'Südtor',
|
||||
name: 'South Gate',
|
||||
description: 'Der Ausgang zur Wildnis.',
|
||||
dangerLevel: 1,
|
||||
connections: [
|
||||
@@ -34,7 +34,7 @@ const currentLocation = southGateFixture({
|
||||
targetLocation: {
|
||||
id: 'target-id',
|
||||
key: 'burned-road',
|
||||
name: 'Verbrannte Straße',
|
||||
name: 'Burned Road',
|
||||
},
|
||||
travelDurationSeconds: 10,
|
||||
danger: 'LOW',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<div class="app-shell__content" [class.app-shell__content--no-context]="!showContextPanel()">
|
||||
<app-side-navigation />
|
||||
<main class="app-shell__main" aria-label="Spielinhalt">
|
||||
<main class="app-shell__main" aria-label="Game content">
|
||||
<router-outlet />
|
||||
</main>
|
||||
@if (showContextPanel()) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ContextPanelComponent } from './context-panel.component';
|
||||
const southGate = {
|
||||
id: 'south-gate-id',
|
||||
key: 'south-gate',
|
||||
name: 'Südtor von Graufurt',
|
||||
name: 'Graufurt South Gate',
|
||||
description: 'Der letzte sichere Schritt vor den Aschenfeldern.',
|
||||
regionKey: 'ashen-fields',
|
||||
minRecommendedLevel: 1,
|
||||
@@ -23,11 +23,11 @@ const burnedRoad = {
|
||||
...southGate,
|
||||
id: 'burned-road-id',
|
||||
key: 'burned-road',
|
||||
name: 'Verbrannte Straße',
|
||||
name: 'Burned Road',
|
||||
isSafe: false,
|
||||
huntingEnabled: true,
|
||||
artworkPath: '/images/backgrounds/Aschestrasse.png',
|
||||
possibleMonsters: ['Aschenratte', 'Straßenräuber'],
|
||||
possibleMonsters: ['Ash Rat', 'Road Bandit'],
|
||||
};
|
||||
|
||||
describe('ContextPanelComponent', () => {
|
||||
@@ -97,7 +97,7 @@ describe('ContextPanelComponent', () => {
|
||||
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
expect(element.textContent).toContain('Possible Encounters');
|
||||
expect(element.textContent).toContain('Aschenratte, Straßenräuber');
|
||||
expect(element.textContent).toContain('Ash Rat, Road Bandit');
|
||||
expect(element.textContent).not.toMatch(/\d+\s?%/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ItemCardComponent } from './item-card.component';
|
||||
|
||||
const banditBlade: RewardItemSummary = {
|
||||
key: 'bandit-blade',
|
||||
name: 'Räuberklinge',
|
||||
name: 'Bandit Blade',
|
||||
rarity: 'COMMON',
|
||||
iconPath: '/images/items/bandit-blade.png',
|
||||
};
|
||||
@@ -27,8 +27,8 @@ describe('ItemCardComponent', () => {
|
||||
|
||||
const icon = element.querySelector<HTMLImageElement>('[data-item-icon]');
|
||||
expect(icon?.getAttribute('src')).toBe('/images/items/bandit-blade.png');
|
||||
expect(icon?.getAttribute('alt')).toBe('Räuberklinge');
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain('Räuberklinge');
|
||||
expect(icon?.getAttribute('alt')).toBe('Bandit Blade');
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain('Bandit Blade');
|
||||
expect(element.querySelector('[data-item-rarity]')?.textContent).toContain('Common');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user