feat(world): serve local location view content from the API
Adds the server side of the local location view: location_definitions carries region naming, a location type, a scene-level description and artwork, plus JSONB points of interest, primary actions and a reward preview. Locations become content, so a second location renders through the same components with different data. GET /api/world/current-location gains those fields, a recommendation label and a danger rating derived from the weighted average of the location's own monster pool — a rare elite no longer makes a beginner road read as lethal. The encounter preview is derived from that same pool rather than duplicating it. POST /api/world/current-location/interactions/:key reveals a hotspot's authored result. The location is resolved from the character, never from the request, and result text never ships with the location payload, so a caller cannot read or trigger a hotspot it has not travelled to. Seeds the Verbrannte Straße with its four hotspots and the Südtor with its own transition content. Adds Verwilderter Straßenhund and Verkohlter Plünderer to the road's pool, including combat sprites, so the preview shows encounters the hunt can actually roll. Medallion icons move to images/monsters/icons, where both combat and the location view read them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BIN
apps/web/public/images/combat/sprites/charred-looter-620.png
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
apps/web/public/images/combat/sprites/wild-road-dog-760.png
Normal file
|
After Width: | Height: | Size: 323 KiB |
BIN
apps/web/public/images/monsters/charred-looter.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
BIN
apps/web/public/images/monsters/icons/charred-looter-128.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
BIN
apps/web/public/images/monsters/icons/wild-road-dog-128.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
apps/web/public/images/monsters/runtime/charred-looter-560.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
apps/web/public/images/monsters/runtime/wild-road-dog-560.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/web/public/images/monsters/wild-road-dog.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
@@ -61,7 +61,7 @@ describe('EncounterCardComponent', () => {
|
||||
const element = render(ashRatEncounter);
|
||||
const crest = element.querySelector('.encounter-card__crest-icon');
|
||||
|
||||
expect(crest?.getAttribute('src')).toBe('/images/combat/icons/ash-rat-128.png');
|
||||
expect(crest?.getAttribute('src')).toBe('/images/monsters/icons/ash-rat-128.png');
|
||||
});
|
||||
|
||||
it('leaves the crest empty for a monster without an icon', () => {
|
||||
|
||||
@@ -16,6 +16,12 @@ describe('monsterCutoutPath', () => {
|
||||
it('returns the background-free cut-out for a known monster key', () => {
|
||||
expect(monsterCutoutPath('ash-rat')).toBe('/images/combat/sprites/ash-rat-760.png');
|
||||
expect(monsterCutoutPath('road-bandit')).toBe('/images/combat/sprites/road-bandit-620.png');
|
||||
expect(monsterCutoutPath('wild-road-dog')).toBe(
|
||||
'/images/combat/sprites/wild-road-dog-760.png',
|
||||
);
|
||||
expect(monsterCutoutPath('charred-looter')).toBe(
|
||||
'/images/combat/sprites/charred-looter-620.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined for a monster without a cut-out', () => {
|
||||
@@ -25,8 +31,11 @@ describe('monsterCutoutPath', () => {
|
||||
|
||||
describe('monsterIconPath', () => {
|
||||
it('returns the medallion icon for a known monster key', () => {
|
||||
expect(monsterIconPath('ash-rat')).toBe('/images/combat/icons/ash-rat-128.png');
|
||||
expect(monsterIconPath('road-bandit')).toBe('/images/combat/icons/road-bandit-128.png');
|
||||
expect(monsterIconPath('ash-rat')).toBe('/images/monsters/icons/ash-rat-128.png');
|
||||
expect(monsterIconPath('road-bandit')).toBe('/images/monsters/icons/road-bandit-128.png');
|
||||
expect(monsterIconPath('charred-looter')).toBe(
|
||||
'/images/monsters/icons/charred-looter-128.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined for a monster without an icon', () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const RUNTIME_MONSTER_ARTWORK: Readonly<Record<string, string>> = {
|
||||
'/images/monsters/ash-rat.png': '/images/monsters/runtime/ash-rat-560.jpg',
|
||||
'/images/monsters/road-bandit.png': '/images/monsters/runtime/road-bandit-560.jpg',
|
||||
'/images/monsters/wild-road-dog.png': '/images/monsters/runtime/wild-road-dog-560.jpg',
|
||||
'/images/monsters/charred-looter.png': '/images/monsters/runtime/charred-looter-560.jpg',
|
||||
};
|
||||
|
||||
export function runtimeMonsterArtworkPath(artworkPath: string): string | undefined {
|
||||
@@ -12,11 +14,18 @@ export function runtimeMonsterArtworkPath(artworkPath: string): string | undefin
|
||||
const MONSTER_CUTOUT: Readonly<Record<string, string>> = {
|
||||
'ash-rat': '/images/combat/sprites/ash-rat-760.png',
|
||||
'road-bandit': '/images/combat/sprites/road-bandit-620.png',
|
||||
'wild-road-dog': '/images/combat/sprites/wild-road-dog-760.png',
|
||||
'charred-looter': '/images/combat/sprites/charred-looter-620.png',
|
||||
};
|
||||
|
||||
// Medallions live under `monsters/`, not `combat/`: the local location view
|
||||
// shows the same icons in its encounter preview, where they are served
|
||||
// straight from `MonsterDefinition.iconPath`.
|
||||
const MONSTER_ICON: Readonly<Record<string, string>> = {
|
||||
'ash-rat': '/images/combat/icons/ash-rat-128.png',
|
||||
'road-bandit': '/images/combat/icons/road-bandit-128.png',
|
||||
'ash-rat': '/images/monsters/icons/ash-rat-128.png',
|
||||
'road-bandit': '/images/monsters/icons/road-bandit-128.png',
|
||||
'wild-road-dog': '/images/monsters/icons/wild-road-dog-128.png',
|
||||
'charred-looter': '/images/monsters/icons/charred-looter-128.png',
|
||||
};
|
||||
|
||||
// Share of the battlefield height each monster sprite occupies, so a hulking
|
||||
@@ -24,6 +33,8 @@ const MONSTER_ICON: Readonly<Record<string, string>> = {
|
||||
const COMBAT_MONSTER_SCALE: Readonly<Record<string, number>> = {
|
||||
'ash-rat': 0.46,
|
||||
'road-bandit': 0.82,
|
||||
'wild-road-dog': 0.58,
|
||||
'charred-looter': 0.86,
|
||||
};
|
||||
|
||||
const DEFAULT_MONSTER_SCALE = 0.6;
|
||||
|
||||