diff --git a/README.md b/README.md
index a2da761..8a28ca6 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,10 @@ for one demo character, with no login required.
## Local setup
Run from the repository root:
+```
+docker run --name ashen-postgres -e POSTGRES_USER=ashen -e POSTGRES_PASSWORD=ashen -e POSTGRES_DB=ashen_realms -p 5432:5432 -d postgres:16
+
+```
```powershell
npm install
diff --git a/apps/api/src/database/seeds/item-content.ts b/apps/api/src/database/seeds/item-content.ts
index f5dc4da..4115761 100644
--- a/apps/api/src/database/seeds/item-content.ts
+++ b/apps/api/src/database/seeds/item-content.ts
@@ -278,13 +278,13 @@ function entry(
* rather than seeding an item that does nothing.
*/
export const LOOT_TABLE_ENTRIES: SeedLootTableEntry[] = [
- entry(ASH_RAT_LOOT_TABLE_ID, 'ash-pelt', 1, '1.0000'),
+ entry(ASH_RAT_LOOT_TABLE_ID, 'ash-pelt', 1, '0.6000'),
entry(ASH_RAT_LOOT_TABLE_ID, 'worn-short-sword', 2, '0.0800'),
entry(ROAD_BANDIT_LOOT_TABLE_ID, 'bandit-blade', 1, '0.1800'),
entry(ROAD_BANDIT_LOOT_TABLE_ID, 'bandit-hood', 2, '0.1200'),
entry(ROAD_BANDIT_LOOT_TABLE_ID, 'raider-gloves', 3, '0.0800'),
- entry(ROAD_BANDIT_LOOT_TABLE_ID, 'bandit-insignia', 4, '1.0000'),
- entry(WILD_ROAD_DOG_LOOT_TABLE_ID, 'tough-hide', 1, '1.0000'),
+ entry(ROAD_BANDIT_LOOT_TABLE_ID, 'bandit-insignia', 4, '0.6000'),
+ entry(WILD_ROAD_DOG_LOOT_TABLE_ID, 'tough-hide', 1, '0.6000'),
entry(WILD_ROAD_DOG_LOOT_TABLE_ID, 'ash-boots', 2, '0.0800'),
entry(CHARRED_LOOTER_LOOT_TABLE_ID, 'charred-raider-insignia', 1, '1.0000'),
entry(CHARRED_LOOTER_LOOT_TABLE_ID, 'reinforced-leather-jacket', 2, '0.1000'),
diff --git a/apps/api/src/database/seeds/local-location.content.ts b/apps/api/src/database/seeds/local-location.content.ts
index 3275c4e..d944291 100644
--- a/apps/api/src/database/seeds/local-location.content.ts
+++ b/apps/api/src/database/seeds/local-location.content.ts
@@ -171,6 +171,7 @@ export const SOUTH_GATE_LOCAL_CONTENT: LocalLocationContent = {
resultTitle: 'Gate Watch',
resultText:
'"Beyond the gate, Graufurt\'s protection ends. Whoever heads south does so at their own risk — and rarely comes back the way they left."',
+ resultImg: '/images/npcs/graufurt-gate-watch.png',
},
// Borin stands at the gate rather than deeper in a town that does not
// exist yet as a location. Carries `npcKey` instead of result text, so
diff --git a/apps/api/src/world/local-location-interaction.spec.ts b/apps/api/src/world/local-location-interaction.spec.ts
index ce4a097..0dca51c 100644
--- a/apps/api/src/world/local-location-interaction.spec.ts
+++ b/apps/api/src/world/local-location-interaction.spec.ts
@@ -63,6 +63,7 @@ const SOUTH_GATE_POIS: LocationPointOfInterestContent[] = [
enabled: true,
resultTitle: 'Gate Watch',
resultText: 'Only heard at the South Gate.',
+ resultImg: '/images/npcs/graufurt-gate-watch.png',
},
];
@@ -114,6 +115,19 @@ describe('WorldService.runLocalInteraction', () => {
});
});
+ it('includes the authored image when the interaction carries one', async () => {
+ const service = createService(location(SOUTH_GATE_ID, SOUTH_GATE_POIS));
+
+ await expect(
+ service.runLocalInteraction(CHARACTER_ID, 'gate-watch'),
+ ).resolves.toEqual({
+ interactionKey: 'gate-watch',
+ title: 'Gate Watch',
+ text: 'Only heard at the South Gate.',
+ img: '/images/npcs/graufurt-gate-watch.png',
+ });
+ });
+
it('settles travel before resolving which location the character stands at', async () => {
const completeTravelIfDue = jest.fn().mockResolvedValue({ status: 'IDLE' });
const service = createService(
diff --git a/apps/api/src/world/local-location.types.ts b/apps/api/src/world/local-location.types.ts
index 54dcf2e..f12e817 100644
--- a/apps/api/src/world/local-location.types.ts
+++ b/apps/api/src/world/local-location.types.ts
@@ -46,6 +46,8 @@ export interface LocationPointOfInterestContent {
enabled: boolean;
resultTitle?: string;
resultText?: string;
+ /** Authored portrait shown alongside the result text, e.g. an NPC's face. */
+ resultImg?: string;
/**
* Names a real NPC (NPC spec §24), which turns this hotspot into a doorway
* to that person's screen instead of an authored one-shot text reveal. A
@@ -117,6 +119,7 @@ export interface LocationInteractionResultDto {
interactionKey: string;
title: string;
text: string;
+ img?: string;
}
/** Strips server-only result text before a POI is sent to the client. */
diff --git a/apps/api/src/world/world.service.ts b/apps/api/src/world/world.service.ts
index 904201e..17a2d14 100644
--- a/apps/api/src/world/world.service.ts
+++ b/apps/api/src/world/world.service.ts
@@ -156,6 +156,7 @@ export class WorldService {
interactionKey: poi.key,
title: poi.resultTitle ?? poi.title,
text: poi.resultText,
+ ...(poi.resultImg === undefined ? {} : { img: poi.resultImg }),
};
}
diff --git a/apps/web/public/images/npcs/borin.png b/apps/web/public/images/npcs/borin.png
index b343a91..443d80d 100644
Binary files a/apps/web/public/images/npcs/borin.png and b/apps/web/public/images/npcs/borin.png differ
diff --git a/apps/web/public/images/npcs/borin_comic.png b/apps/web/public/images/npcs/borin_comic.png
new file mode 100644
index 0000000..b343a91
Binary files /dev/null and b/apps/web/public/images/npcs/borin_comic.png differ
diff --git a/apps/web/public/images/npcs/graufurt-gate-watch.png b/apps/web/public/images/npcs/graufurt-gate-watch.png
index 8e6282f..39c04e7 100644
Binary files a/apps/web/public/images/npcs/graufurt-gate-watch.png and b/apps/web/public/images/npcs/graufurt-gate-watch.png differ
diff --git a/apps/web/public/images/npcs/graufurt-gate-watch_comic.png b/apps/web/public/images/npcs/graufurt-gate-watch_comic.png
new file mode 100644
index 0000000..8e6282f
Binary files /dev/null and b/apps/web/public/images/npcs/graufurt-gate-watch_comic.png differ
diff --git a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.html b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.html
index 2781546..9e56835 100644
--- a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.html
+++ b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.html
@@ -7,6 +7,15 @@
data-interaction-panel
>
@if (result) {
+ @if (result.img) {
+
+ }
{{ result.text }}
} @else { diff --git a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.scss b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.scss index 7153d64..c11e3b7 100644 --- a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.scss +++ b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.scss @@ -22,6 +22,15 @@ pointer-events: auto; } +.interaction-panel__portrait { + inline-size: 5rem; + block-size: 5rem; + object-fit: contain; + border: 1px solid var(--ar-border-highlight); + border-radius: var(--ar-radius-md); + background: var(--ar-panel-muted); +} + .interaction-panel__title { margin: 0; color: var(--ar-gold); diff --git a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts index 30fedeb..f874d31 100644 --- a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts +++ b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LocationInteractionPanelComponent } from './location-interaction-panel.component'; async function setup(inputs: { - result?: { interactionKey: string; title: string; text: string } | null; + result?: { interactionKey: string; title: string; text: string; img?: string } | null; error?: string | null; }): Promise<{ fixture: ComponentFixture