This commit is contained in:
Bastian Wagner
2026-08-23 19:25:57 +02:00
parent bbd5dbbd1f
commit d0eecc45d6
8 changed files with 119 additions and 10 deletions

View File

@@ -235,7 +235,7 @@ export const DIALOGUE_NODES: SeedDialogueNode[] = [
{ {
npcId: SOUTH_GATE_WARDEN_NPC_ID, npcId: SOUTH_GATE_WARDEN_NPC_ID,
key: 'warden-quest-offer', key: 'warden-quest-offer',
text: 'The road has gone bad. Start small. Bring me five Ashen Pelts from the rats beyond the gate. I want to know what the ash is doing to them.', text: 'The road has gone bad. Nobody reached us for days. The animals are going crazy. I want to know what the ash is doing to them. Travel to the Burned Road and hunt for these creatures.',
priority: 900, priority: 900,
conditions: [ conditions: [
{ {
@@ -272,7 +272,7 @@ export const DIALOGUE_NODES: SeedDialogueNode[] = [
{ {
npcId: SOUTH_GATE_WARDEN_NPC_ID, npcId: SOUTH_GATE_WARDEN_NPC_ID,
key: 'warden-quest-active', key: 'warden-quest-active',
text: 'Still out there, then. Five pelts, and no fewer. I am not paying for a guess.', text: 'Still out there, then. I need to know what happens on our roads. I am not paying for a guess.',
priority: 500, priority: 500,
conditions: [ conditions: [
{ {

View File

@@ -93,7 +93,7 @@ export const QUEST_OBJECTIVES: SeedQuestObjective[] = [
type: QuestObjectiveType.COLLECT_ITEM, type: QuestObjectiveType.COLLECT_ITEM,
targetKey: 'ash-pelt', targetKey: 'ash-pelt',
requiredQuantity: 1, requiredQuantity: 1,
description: 'Collect Ashen Pelts', description: 'Collect Ashen Pelts from Ash Rats',
npcLine: null, npcLine: null,
hintText: hintText:
'You defeatet a burned creature. Return to the South Gate Warden and report your finding.', 'You defeatet a burned creature. Return to the South Gate Warden and report your finding.',
@@ -112,9 +112,9 @@ export const QUEST_OBJECTIVES: SeedQuestObjective[] = [
type: QuestObjectiveType.TALK_TO_NPC, type: QuestObjectiveType.TALK_TO_NPC,
targetKey: SOUTH_GATE_WARDEN_KEY, targetKey: SOUTH_GATE_WARDEN_KEY,
requiredQuantity: 1, requiredQuantity: 1,
description: 'Return to the South Gate Warden', description: 'Show the pelt to the Warden.',
npcLine: npcLine:
"We need to investigate this further, collect more pelts and bring it to Borin. Wait, you're not equipped for hauling spoils yet. Go see Borin in Graufurt. Tell him I sent you. He'll complain, but he'll give you something useful.", "We need to investigate this further, collect more pelts and bring them to Borin. Wait, you're not equipped for hauling spoils yet. Go see Borin in Graufurt. Tell him I sent you. He'll complain, but he'll give you something useful.",
hintText: null, hintText: null,
advanceWhenBlocked: false, advanceWhenBlocked: false,
consumeOnComplete: false, consumeOnComplete: false,
@@ -136,7 +136,7 @@ export const QUEST_OBJECTIVES: SeedQuestObjective[] = [
requiredQuantity: 1, requiredQuantity: 1,
description: 'Speak with Borin in Graufurt', description: 'Speak with Borin in Graufurt',
npcLine: npcLine:
'But the South Gate Warden sent you. Fine. Take this. Bring it back full and make it worth my trouble.', 'What do you want? I have nothing to spare... But the South Gate Warden sent you? Fine. Take this. Bring it back full and make it worth my trouble.',
hintText: 'Borin gave you a bag for animal spoils. It fits 5 pelts or similar.', hintText: 'Borin gave you a bag for animal spoils. It fits 5 pelts or similar.',
advanceWhenBlocked: false, advanceWhenBlocked: false,
consumeOnComplete: false, consumeOnComplete: false,
@@ -158,7 +158,7 @@ export const QUEST_OBJECTIVES: SeedQuestObjective[] = [
type: QuestObjectiveType.COLLECT_ITEM, type: QuestObjectiveType.COLLECT_ITEM,
targetKey: 'ash-pelt', targetKey: 'ash-pelt',
requiredQuantity: 5, requiredQuantity: 5,
description: 'Collect Ashen Pelts', description: 'Hunt more Ash Rats and collect their pelt',
npcLine: null, npcLine: null,
// The bag holds five of *any* hide, not five pelts: a player carrying // The bag holds five of *any* hide, not five pelts: a player carrying
// Tough Hides alongside them can fill it before the fifth pelt. This step // Tough Hides alongside them can fill it before the fifth pelt. This step
@@ -182,7 +182,7 @@ export const QUEST_OBJECTIVES: SeedQuestObjective[] = [
type: QuestObjectiveType.TALK_TO_NPC, type: QuestObjectiveType.TALK_TO_NPC,
targetKey: SOUTH_GATE_WARDEN_KEY, targetKey: SOUTH_GATE_WARDEN_KEY,
requiredQuantity: 1, requiredQuantity: 1,
description: 'Bring the pelts to the South Gate Warden', description: 'Hunt Ashen Rats on the Burned Road and collect their pelt. Bring the pelts to the South Gate Warden',
npcLine: npcLine:
"Good. That's enough for me. From now on, take hides and trophies to Borin. He'll pay for useful spoils, and word gets around when you keep the roads clear.", "Good. That's enough for me. From now on, take hides and trophies to Borin. He'll pay for useful spoils, and word gets around when you keep the roads clear.",
hintText: null, hintText: null,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@@ -1,5 +1,5 @@
<div class="app-shell"> <div class="app-shell">
<app-top-bar [character]="worldStore.displayedCharacter()" /> <app-top-bar [character]="topBarCharacter()" />
<div class="app-shell__content" [class.app-shell__content--no-context]="!showContextPanel()"> <div class="app-shell__content" [class.app-shell__content--no-context]="!showContextPanel()">
<app-side-navigation /> <app-side-navigation />

View File

@@ -0,0 +1,88 @@
import { Component, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Router, provideRouter } from '@angular/router';
import type { CharacterResponse, Combat } from '../../core/api/game-api.models';
import { CombatStore } from '../../features/combat/combat.store';
import { WorldStore } from '../../features/world/world.store';
import { AppShellComponent } from './app-shell.component';
function characterFixture(overrides: Partial<CharacterResponse> = {}): CharacterResponse {
return {
id: 'character-1',
name: 'Aric Duskwalker',
renown: 3,
silver: 120,
currentHp: 40,
maxHp: 100,
attack: 10,
hpRegenPerSecond: 1,
// Regenerating, as if the fight started mid-heal -- the world snapshot's
// own HP would keep climbing toward maxHp if the HUD trusted it.
hpRegenSince: new Date().toISOString(),
currentLocation: { id: 'location-1', key: 'aschenfelder', name: 'Ashen Fields' },
...overrides,
};
}
const activeCombat: Combat = {
id: 'combat-1',
status: 'ACTIVE',
round: 2,
player: {
name: 'Aric Duskwalker',
maxHp: 100,
currentHp: 62,
potionsRemaining: 2,
potionsMax: 2,
statusEffects: [],
},
monster: {
key: 'ash-rat',
name: 'Ash Rat',
level: 1,
maxHp: 45,
currentHp: 31,
artworkPath: '/images/monsters/ash-rat.png',
pendingIntent: null,
guardRemainingRounds: null,
enraged: false,
},
events: [],
rewards: null,
};
@Component({ template: '' })
class BlankComponent {}
describe('AppShellComponent', () => {
async function setup(url: string, combat: Combat | null): Promise<HTMLElement> {
await TestBed.configureTestingModule({
imports: [AppShellComponent],
providers: [
provideRouter([{ path: '**', component: BlankComponent }]),
{ provide: WorldStore, useValue: { displayedCharacter: signal(characterFixture()) } },
{ provide: CombatStore, useValue: { combat: signal(combat) } },
],
}).compileComponents();
const router = TestBed.inject(Router);
await router.navigateByUrl(url);
const fixture = TestBed.createComponent(AppShellComponent);
fixture.detectChanges();
return fixture.nativeElement as HTMLElement;
}
it('shows the live combat HP, not the regen-ticked world snapshot, while in a fight', async () => {
const element = await setup('/combat/combat-1', activeCombat);
expect(element.textContent).toContain('62 / 100');
expect(element.textContent).not.toContain('40 / 100');
});
it('shows the world snapshot HP outside of combat', async () => {
const element = await setup('/location', null);
expect(element.textContent).toContain('40 / 100');
});
});

View File

@@ -1,5 +1,7 @@
import { Component, inject } from '@angular/core'; import { Component, computed, inject } from '@angular/core';
import { Router, RouterOutlet, isActive } from '@angular/router'; import { Router, RouterOutlet, isActive } from '@angular/router';
import type { CharacterResponse } from '../../core/api/game-api.models';
import { CombatStore } from '../../features/combat/combat.store';
import { WorldStore } from '../../features/world/world.store'; import { WorldStore } from '../../features/world/world.store';
import { ContextPanelComponent } from '../context-panel/context-panel.component'; import { ContextPanelComponent } from '../context-panel/context-panel.component';
import { GameFooterComponent } from '../game-footer/game-footer.component'; import { GameFooterComponent } from '../game-footer/game-footer.component';
@@ -22,11 +24,30 @@ export class AppShellComponent {
private readonly router = inject(Router); private readonly router = inject(Router);
protected readonly worldStore = inject(WorldStore); protected readonly worldStore = inject(WorldStore);
private readonly combatStore = inject(CombatStore);
// The fight has its own log rail and wants the width, and the area info // The fight has its own log rail and wants the width, and the area info
// belongs to the world view anyway, so the rail is dropped during combat. // belongs to the world view anyway, so the rail is dropped during combat.
private readonly inCombat = isActive('/combat', this.router); private readonly inCombat = isActive('/combat', this.router);
// Outside a fight the HUD shows WorldStore's regen-ticked HP. Mid-fight,
// regen is paused server-side and the only HP that moves is the combat's --
// the world snapshot is stale, so it must not drive the bar.
protected readonly topBarCharacter = computed<CharacterResponse | null>(() => {
const character = this.worldStore.displayedCharacter();
const combat = this.inCombat() ? this.combatStore.combat() : null;
if (!character || !combat) {
return character;
}
return {
...character,
currentHp: combat.player.currentHp,
maxHp: combat.player.maxHp,
hpRegenSince: null,
};
});
// The location view brings its own, far richer context sidebar. Showing the // The location view brings its own, far richer context sidebar. Showing the
// shell's generic area panel next to it would say the same thing twice and // shell's generic area panel next to it would say the same thing twice and
// squeeze the artwork the screen is built around. // squeeze the artwork the screen is built around.