sprites
This commit is contained in:
@@ -235,7 +235,7 @@ export const DIALOGUE_NODES: SeedDialogueNode[] = [
|
||||
{
|
||||
npcId: SOUTH_GATE_WARDEN_NPC_ID,
|
||||
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,
|
||||
conditions: [
|
||||
{
|
||||
@@ -272,7 +272,7 @@ export const DIALOGUE_NODES: SeedDialogueNode[] = [
|
||||
{
|
||||
npcId: SOUTH_GATE_WARDEN_NPC_ID,
|
||||
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,
|
||||
conditions: [
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@ export const QUEST_OBJECTIVES: SeedQuestObjective[] = [
|
||||
type: QuestObjectiveType.COLLECT_ITEM,
|
||||
targetKey: 'ash-pelt',
|
||||
requiredQuantity: 1,
|
||||
description: 'Collect Ashen Pelts',
|
||||
description: 'Collect Ashen Pelts from Ash Rats',
|
||||
npcLine: null,
|
||||
hintText:
|
||||
'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,
|
||||
targetKey: SOUTH_GATE_WARDEN_KEY,
|
||||
requiredQuantity: 1,
|
||||
description: 'Return to the South Gate Warden',
|
||||
description: 'Show the pelt to the Warden.',
|
||||
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,
|
||||
advanceWhenBlocked: false,
|
||||
consumeOnComplete: false,
|
||||
@@ -136,7 +136,7 @@ export const QUEST_OBJECTIVES: SeedQuestObjective[] = [
|
||||
requiredQuantity: 1,
|
||||
description: 'Speak with Borin in Graufurt',
|
||||
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.',
|
||||
advanceWhenBlocked: false,
|
||||
consumeOnComplete: false,
|
||||
@@ -158,7 +158,7 @@ export const QUEST_OBJECTIVES: SeedQuestObjective[] = [
|
||||
type: QuestObjectiveType.COLLECT_ITEM,
|
||||
targetKey: 'ash-pelt',
|
||||
requiredQuantity: 5,
|
||||
description: 'Collect Ashen Pelts',
|
||||
description: 'Hunt more Ash Rats and collect their pelt',
|
||||
npcLine: null,
|
||||
// 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
|
||||
@@ -182,7 +182,7 @@ export const QUEST_OBJECTIVES: SeedQuestObjective[] = [
|
||||
type: QuestObjectiveType.TALK_TO_NPC,
|
||||
targetKey: SOUTH_GATE_WARDEN_KEY,
|
||||
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:
|
||||
"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,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 2.3 MiB |
BIN
apps/web/public/images/items/raider-warband-mark.png
Normal file
BIN
apps/web/public/images/items/raider-warband-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
BIN
apps/web/public/images/items/scorched-hide.png
Normal file
BIN
apps/web/public/images/items/scorched-hide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -1,5 +1,5 @@
|
||||
<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()">
|
||||
<app-side-navigation />
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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 type { CharacterResponse } from '../../core/api/game-api.models';
|
||||
import { CombatStore } from '../../features/combat/combat.store';
|
||||
import { WorldStore } from '../../features/world/world.store';
|
||||
import { ContextPanelComponent } from '../context-panel/context-panel.component';
|
||||
import { GameFooterComponent } from '../game-footer/game-footer.component';
|
||||
@@ -22,11 +24,30 @@ export class AppShellComponent {
|
||||
private readonly router = inject(Router);
|
||||
|
||||
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
|
||||
// belongs to the world view anyway, so the rail is dropped during combat.
|
||||
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
|
||||
// shell's generic area panel next to it would say the same thing twice and
|
||||
// squeeze the artwork the screen is built around.
|
||||
|
||||
Reference in New Issue
Block a user