sprites
This commit is contained in:
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 |
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">
|
<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 />
|
||||||
|
|||||||
@@ -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 { 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user