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 6388fcc..fe48588 100644 --- a/apps/api/src/database/seeds/local-location.content.ts +++ b/apps/api/src/database/seeds/local-location.content.ts @@ -158,6 +158,7 @@ export const SOUTH_GATE_LOCAL_CONTENT: LocalLocationContent = { resultTitle: 'Notice Board', resultText: 'Weathered notices flutter in the wind. A fresh one warns of raiders on the Burned Road and promises silver for every bandit killed.', + resultImg: '/images/environment/notice-board.png', }, // Was authored scenery with a line of result text until Slice 0.9 made the // warden a real NPC with a quest to give. A hotspot carries result text or diff --git a/apps/api/src/database/seeds/npc-content.ts b/apps/api/src/database/seeds/npc-content.ts index 6f17405..12a4cf5 100644 --- a/apps/api/src/database/seeds/npc-content.ts +++ b/apps/api/src/database/seeds/npc-content.ts @@ -144,7 +144,9 @@ export const NPC_DEFINITIONS: SeedNpcDefinition[] = [ 'They have watched the road long enough to stop expecting good news from it. Whatever comes back through the gate, they want it counted.', locationKey: 'south-gate', factionKey: 'border-guard', - portraitPath: '/images/npcs/south-gate-warden.png', + // The gate watch's own portrait, painted before this slice made them a + // person. Reused rather than duplicated: it is the same figure. + portraitPath: '/images/npcs/graufurt-gate-watch.png', artworkPath: null, capabilities: [ NpcCapability.DIALOGUE, 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/favicon.png b/apps/web/public/favicon.png new file mode 100644 index 0000000..69b9265 Binary files /dev/null and b/apps/web/public/favicon.png differ diff --git a/apps/web/public/images/environment/notice-board.png b/apps/web/public/images/environment/notice-board.png new file mode 100644 index 0000000..1a2a82c Binary files /dev/null and b/apps/web/public/images/environment/notice-board.png differ diff --git a/apps/web/public/images/environment/notice-board_comic.png b/apps/web/public/images/environment/notice-board_comic.png new file mode 100644 index 0000000..78e3716 Binary files /dev/null and b/apps/web/public/images/environment/notice-board_comic.png differ diff --git a/apps/web/public/images/items/basic-hide-bag.png b/apps/web/public/images/items/basic-hide-bag.png new file mode 100644 index 0000000..56ca9d9 Binary files /dev/null and b/apps/web/public/images/items/basic-hide-bag.png differ diff --git a/apps/web/public/images/items/basic-trophy-pouch.png b/apps/web/public/images/items/basic-trophy-pouch.png new file mode 100644 index 0000000..113d8b4 Binary files /dev/null and b/apps/web/public/images/items/basic-trophy-pouch.png differ diff --git a/apps/web/public/images/npcs/borin.png b/apps/web/public/images/npcs/borin.png index 443d80d..46f1888 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 new file mode 100644 index 0000000..f958ba0 Binary files /dev/null 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/public/images/npcs/south-gate-warden.png b/apps/web/public/images/npcs/south-gate-warden.png deleted file mode 100644 index 46792e4..0000000 Binary files a/apps/web/public/images/npcs/south-gate-warden.png and /dev/null differ diff --git a/apps/web/src/app/core/api/game-api.models.ts b/apps/web/src/app/core/api/game-api.models.ts index a0a470a..c951c3a 100644 --- a/apps/web/src/app/core/api/game-api.models.ts +++ b/apps/web/src/app/core/api/game-api.models.ts @@ -88,6 +88,7 @@ export interface LocationInteractionResult { interactionKey: string; title: string; text: string; + img?: string; } export interface CurrentLocationResponse { diff --git a/apps/web/src/app/features/npc/merchant-page.component.scss b/apps/web/src/app/features/npc/merchant-page.component.scss index 9613549..f67e4ca 100644 --- a/apps/web/src/app/features/npc/merchant-page.component.scss +++ b/apps/web/src/app/features/npc/merchant-page.component.scss @@ -39,7 +39,7 @@ .merchant__portrait { inline-size: 7rem; block-size: 7rem; - object-fit: cover; + object-fit: contain; border: 1px solid var(--ar-border-highlight); border-radius: var(--ar-radius-md); background: var(--ar-panel-muted); 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..660653d 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,8 +7,21 @@ data-interaction-panel > @if (result) { -
{{ result.text }}
+{{ result.text }}
+
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..150808f 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,27 @@
pointer-events: auto;
}
+.interaction-panel__body {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--ar-space-4);
+}
+
+.interaction-panel__portrait {
+ flex: 0 0 auto;
+ 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__copy {
+ display: grid;
+ gap: var(--ar-space-3);
+}
+
.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