Bastian Wagner 833fa52d8d perf: serve optimized JPEG derivatives for monster artwork
encounter-card.component rendered the raw 2.39MB ash-rat.png and
2.00MB road-bandit.png directly, up to 3 cards per hunt (~7MB/load,
re-rendered on "Neu suchen"). Add 560px-wide JPEG runtime derivatives
(generated via PowerShell System.Drawing, HighQualityBicubic, quality
82) and switch to the <picture>/<source srcset> pattern already used
by context-panel for location backgrounds, keeping the original PNGs
as the <img> fallback. Also add loading="lazy" decoding="async".

ash-rat.png: 2,506,677 B -> ash-rat-560.jpg: 35,896 B
road-bandit.png: 2,097,049 B -> road-bandit-560.jpg: 50,797 B
2026-08-19 14:33:32 +02:00
2026-08-19 11:14:17 +02:00

Ashen Realms

A dark-fantasy browser RPG built as an npm-workspace modular monolith:

  • apps/web — Angular 22 single-page application
  • apps/api — NestJS 11 + TypeORM + PostgreSQL API
  • packages/* — reserved shared boundaries (currently unused)

This README documents the first visible vertical slice: a server-authoritative world/travel loop between two locations (Südtor von Graufurt and Verbrannte Straße) for one demo character, with no login required.

Prerequisites

  • Node.js and npm (workspace-aware npm, matching the versions used by CI/your toolchain).

  • A reachable PostgreSQL server (PostgreSQL 13 or newer — the schema uses the built-in gen_random_uuid(), which needs no extensions) with a database named ashen_realms. Create it once, e.g.:

    psql -h localhost -U postgres -c "CREATE DATABASE ashen_realms;"
    psql -h localhost -U postgres -c "CREATE USER ashen WITH PASSWORD 'ashen';"
    psql -h localhost -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE ashen_realms TO ashen;"
    

    Adjust user/password/host to match your own local PostgreSQL instance; just keep DATABASE_URL (below) pointed at it.

Local setup

Run from the repository root:

npm install
Copy-Item .env.example .env
npm run db:migrate
npm run db:seed
npm run dev:api
npm run dev:web

Run dev:api and dev:web in separate terminals — both watch and keep running. Once both are up:

  • API: http://localhost:3000/api/... (see routes below)
  • Web: http://localhost:4200, which proxies /api/* requests to http://localhost:3000 in development (see apps/web/proxy.conf.json)

Open http://localhost:4200/world to see Aric Duskwalker at the Südtor von Graufurt and travel to the Verbrannte Straße and back.

Configuration (.env)

.env.example (repo root) documents every variable. Copy it to .env at the repo root — .env is git-ignored and must never be committed. The important one for local setup is:

DATABASE_URL=postgresql://ashen:ashen@localhost:5432/ashen_realms

This is the default local PostgreSQL connection string: database name ashen_realms, default port 5432. Edit it if your local PostgreSQL uses a different user, password, host, or port. All API workspace scripts (db:migrate, db:seed, dev:api, and apps/api's own test:e2e) resolve this .env from the repository root regardless of which workspace directory npm runs the underlying command in.

synchronize is always false; schema changes only happen through the checked-in migration at apps/api/src/database/migrations/1787072400000-CreateVisibleVerticalSlice.ts. The seed (apps/api/src/database/seeds/vertical-slice.seed.ts) is idempotent: running npm run db:seed multiple times upserts the same two locations, two connections, and one demo character without creating duplicates or resetting the character's current (live) location.

Ports

Service Port Notes
API (NestJS) 3000 All routes are under /api (e.g. /api/health).
Web (Angular dev server) 4200 Proxies /api/* to the API in development.

Available routes (first slice)

GET  /api/health
GET  /api/characters/me
GET  /api/world/current-location
GET  /api/travel/current
POST /api/travel                 { "targetLocationId": "<uuid>" }

POST /api/travel accepts exactly targetLocationId; the global validation pipe rejects any other property (e.g. a client-supplied arrivesAt) with 400 Bad Request, since arrivesAt is always server-derived.

Assets

apps/web/public/images holds only the derivative image files the Angular build actually serves (resized runtime/*-128.png HUD icons, runtime/*-960.jpg and runtime/*-1440.jpg background variants, and the small set of PNG fallbacks referenced directly by source/styles). Everything under apps/web/public is copied verbatim into the browser build, so keep that folder limited to files a component, template, or stylesheet actually references.

Original/unresized source art (enemy, NPC, and combat-status artwork, HUD icon originals, difficulty badges, etc.) that isn't loaded by the app lives in art/ at the repository root instead, mirroring the same subfolder layout (e.g. art/enemies, art/npc, art/hud). It is not part of any build output.

Scripts

Run these from the repository root unless noted otherwise.

Script Description
npm run dev:web Start the Angular dev server (port 4200).
npm run dev:api Start the NestJS API in watch mode (port 3000).
npm run build:web Production build of the Angular app.
npm run build:api Production build of the NestJS app.
npm run build Both builds.
npm test Unit tests for every workspace.
npm run test:e2e API end-to-end/smoke tests (see below).
npm run db:migrate Run pending TypeORM migrations against DATABASE_URL.
npm run db:revert Revert the last migration.
npm run db:seed Run the idempotent demo-content seed.

Testing

Unit tests never require a database:

npm test --workspace=@ashen-realms/api -- --runInBand

The API end-to-end smoke test (apps/api/test/visible-slice.e2e-spec.ts) always asserts GET /api/health. When DATABASE_URL is set and reachable, it additionally boots the real AppModule (real database, entities, and controllers) and asserts the seeded character/world responses and the arrivesAt validation rejection. Those database-backed assertions are skipped — not failed — when no database is configured, so npm run test:e2e is safe to run without any local PostgreSQL setup too:

npm run test:e2e --workspace=@ashen-realms/api -- --runInBand

Both builds:

npm run build:api
npm run build:web

Known limitations of this first vertical slice

This slice deliberately excludes (per docs/superpowers/specs/2026-08-18-first-visible-vertical-slice-design.md):

  • Authentication, user accounts, JWTs, or any login/registration flow — there is exactly one hardcoded demo character (Aric Duskwalker).
  • Hunting, combat, loot, inventory, equipment, quests, merchants, and currencies.
  • Realtime features: WebSockets, chat, guilds, CMS, object storage, or a multi-service/microservice architecture.
  • Ambush resolution: ambushChance is stored on each connection and surfaced only as a coarse LOW/HIGH danger rating; it is never rolled.
  • Production containerization or NestJS static hosting.
  • Client-side clock skew can shift how the travel countdown displays, but arrival is always confirmed by the server (GET /api/travel/current / GET /api/world/current-location), never inferred locally.

Only two locations and one directed pair of connections exist (south-gateburned-road), each with a fixed 10-second travel duration.

Description
No description provided
Readme 157 MiB
Languages
TypeScript 86.5%
SCSS 8.6%
HTML 4.2%
PowerShell 0.5%
JavaScript 0.2%