Files
ashen-realms/docs/superpowers/specs/2026-08-18-first-visible-vertical-slice-design.md
2026-08-18 17:12:23 +02:00

11 KiB
Raw Blame History

Ashen Realms First Visible Vertical Slice Design

Date: 2026-08-18
Status: Approved in chat; awaiting written-spec review
Scope: First visible, server-authoritative world and travel slice

Purpose

This slice proves that Ashen Realms can present a premium dark-fantasy browser-RPG world while keeping character, location, connection, travel timing, and travel completion authoritative in the NestJS/PostgreSQL backend.

The completed slice lets a player open /world, see Aric Duskwalker at the Südtor von Graufurt, select the Verbrannte Straße, start a ten-second journey, watch a countdown derived from the server response, and arrive only after the backend completes the travel.

Scope boundaries

Included:

  • PostgreSQL and TypeORM foundation with explicit migration
  • Reproducible seed for one demo character, two locations, and two directed connections
  • Health, demo-character, current-location, travel-start, and current-travel REST endpoints
  • Angular application shell and /world route
  • Signal-based frontend state for the visible slice
  • Original location artwork plus code-native fantasy UI
  • Backend and frontend tests requested by the implementation prompt
  • Local development documentation

Explicitly excluded:

  • Authentication, users, JWTs, and account flows
  • Hunting, combat, loot, inventory, equipment, quests, merchants, and currencies
  • Realtime, WebSockets, chat, guilds, CMS, object storage, and microservices
  • Ambush resolution; ambushChance is stored but not rolled
  • Production containerization and NestJS static hosting, which are outside this slice's Definition of Done

Technical approach

The existing npm-workspace structure remains unchanged:

apps/web       Angular SPA
apps/api       NestJS modular monolith
packages/*     reserved shared boundaries
docs/          product and implementation documentation

The existing Angular 21.2 scaffold is upgraded to Angular 22 to match the technical foundation document. NestJS remains version 11. No Nx, Turborepo, GraphQL, CQRS, event bus, or separate service is introduced.

The API uses a global /api prefix. Angular calls only relative /api/... URLs and the development server proxies them to http://localhost:3000.

Persistence model

All primary keys are UUIDs. Entity and table names are explicit and stable. Timestamps use PostgreSQL timestamptz. synchronize is always false.

Character

  • id
  • name
  • level
  • experience
  • baseHp
  • baseAttack
  • currentHp
  • currentLocationId
  • createdAt
  • updatedAt

baseHp is exposed as maxHp and baseAttack as attack until equipment is implemented.

LocationDefinition

  • id
  • unique key
  • name
  • description
  • regionKey
  • minRecommendedLevel
  • maxRecommendedLevel
  • dangerLevel
  • isSafe
  • huntingEnabled
  • artworkPath
  • createdAt
  • updatedAt

LocationConnection

  • id
  • fromLocationId
  • toLocationId
  • travelDurationSeconds
  • ambushChance
  • enabled

The pair (fromLocationId, toLocationId) is unique.

Travel

  • id
  • characterId
  • originLocationId
  • targetLocationId
  • startedAt
  • arrivesAt
  • status (TRAVELLING or COMPLETED)
  • createdAt

A PostgreSQL partial unique index permits only one TRAVELLING row per character. Service validation still returns a domain-friendly error before this constraint is reached.

Migration and seed

One reviewed TypeORM migration creates the four tables, foreign keys, enum, indexes, and uniqueness constraints. No runtime schema synchronization is used.

The seed is idempotent and uses stable UUID constants for this deliberately fixed demo slice. It upserts:

  • Aric Duskwalker, level 1, 100 HP, 6 base attack, at south-gate
  • south-gate, safe, level 1, hunting disabled
  • burned-road, unsafe, level 12, hunting enabled
  • south-gate -> burned-road, ten seconds, 0.05 ambush chance
  • burned-road -> south-gate, ten seconds, 0.05 ambush chance

Re-running the seed restores definition fields without duplicating rows. It does not reset the demo character's live location or delete travel history; a separate database reset remains an explicit developer action.

Backend modules and API behavior

Health

GET /api/health returns { "status": "ok" } without requiring a database query.

Characters

GET /api/characters/me resolves the stable demo-character ID in the backend and returns character data with the current location summary. No character ID is hardcoded in Angular.

World

GET /api/world/current-location first asks the travel domain to complete any due journey. It then returns the authoritative current location and enabled outgoing connections.

The public connection DTO contains only target-location summary, duration, and a textual danger rating. For this slice, ambushChance <= 0.05 maps to LOW. The raw probability remains internal.

If a journey is still active, the endpoint continues to report the origin as the character's current location; arrival is never inferred by the client.

Travel

POST /api/travel accepts exactly:

{
  "targetLocationId": "uuid"
}

It validates the target, current character location, enabled directed connection, and absence of an active journey. It derives startedAt from the server clock and arrivesAt from the connection duration.

GET /api/travel/current runs completion logic in a database transaction. A due travel changes both the travel status and character location atomically. Before arrivesAt, neither value changes. Concurrent completion requests are serialized using row locking and remain idempotent.

When active, the endpoint returns TRAVELLING, origin, target, startedAt, and arrivesAt. When it completes a journey, it returns COMPLETED and the target. With no travel history it returns an explicit idle response rather than pretending a journey completed.

Domain errors use NestJS HTTP exceptions with stable error codes such as INVALID_TRAVEL_TARGET and TRAVEL_ALREADY_ACTIVE.

Angular architecture

Shell

The root renders a reusable AppShellComponent composed from:

  • TopBarComponent
  • SideNavigationComponent
  • routed main content
  • ContextPanelComponent owned by the world feature
  • GameFooterComponent

/ redirects to /world. Karte is active. Jagd, Quests, Inventar, and Charakter are visible but disabled. Shop is not shown.

API and state

A typed API service owns all relative HTTP calls. A focused world store/facade owns these Angular signals:

  • character
  • currentLocation
  • selectedConnection
  • currentTravel
  • remainingSeconds
  • loading
  • error

Components render state and forward player decisions. They do not validate connections, calculate travel completion, or mutate the character location.

On initial load, the store requests character, current location, and current travel. Starting travel posts only targetLocationId. The countdown is recalculated from the backend's ISO arrivesAt against the browser clock for presentation only. At zero, the store polls GET /api/travel/current; only a server COMPLETED response triggers reloading character and world data.

Timers are disposed when the store or page is destroyed. Errors appear in-shell and keep retry actions available; browser alerts are not used.

Visual design

The visible surface follows the reference anatomy without copying the screenshots:

  • compact full-width topbar with portrait, name, level, and HP
  • narrow persistent left navigation
  • dominant illustrated Aschenfelder scene in the center
  • two large, readable location nodes connected by a visible travel path
  • dense right-side location context panel
  • bottom-centered travel panel inside the scene
  • restrained persistent footer

An original raster artwork depicts the Südtor opening toward a burned road and distant ash fields. It contains no embedded UI text. Location nodes, connection paths, labels, controls, icons, and panels remain HTML/CSS/SVG so they stay interactive and accessible.

The visual system uses dark stone/metal surfaces, thin bronze borders, warm ash highlights, cool blue selection light, a restrained serif display face with readable system fallbacks, limited radii, and no glassmorphism, neon, white cards, or dashboard grids.

Central SCSS tokens define background, panel, muted panel, borders, text, gold, blue, success, warning, danger, spacing, radius, shadow, and motion values. Component styles consume these tokens rather than duplicating palette values.

Desktop is optimized for 1920×1080, 1440×900, and 1366×768. Tablet retains the core hierarchy with a narrower navigation and context column. Mobile optimization is deliberately deferred.

Accessibility and interaction

  • Location nodes are real buttons with selected, current, hover, focus, and disabled states.
  • Color never communicates danger or status alone; text labels remain present.
  • Focus indicators fit the bronze/blue fantasy system.
  • Motion is subtle and disabled or reduced under prefers-reduced-motion.
  • Loading and error states remain readable against the artwork.

Testing strategy

Implementation follows test-driven development.

Backend tests cover:

  • health response
  • world DTO construction from location and connections
  • valid connection starts travel
  • invalid connection is rejected
  • arrivesAt is derived from the injected server clock
  • travel stays active before arrival
  • due travel atomically completes and updates character location

Frontend tests cover:

  • application shell composition
  • world state loads character/location/travel through the API service
  • travel start sends only targetLocationId
  • countdown is derived from arrivesAt
  • zero countdown polls the backend and does not locally mark arrival

Repository verification includes all workspace tests, API build, web build, migration compilation, seed compilation, and a browser walkthrough of selection, travel start, countdown, server completion, and return travel.

Visual verification compares the rendered implementation with docs/references/world-travel-screen.png for layout hierarchy, density, typography, palette, panel treatment, artwork prominence, node legibility, and interaction states. The reference image itself is never shipped as an application asset.

Local operation

The README documents:

  1. npm install
  2. create PostgreSQL database ashen_realms
  3. copy .env.example to .env and set DATABASE_URL
  4. run migrations
  5. run the seed
  6. start API and web in separate terminals

Root scripts provide stable commands for migration, seed, API development, web development, tests, and both builds.

Acceptance criteria

The slice is accepted when a clean database can be migrated and seeded, both applications start, /api/health returns OK, and the browser visibly supports the complete Südtor-to-Verbrannte-Straße travel flow with backend-owned arrival.

No excluded system is introduced, no frontend fake character becomes a data source, no absolute API URL is compiled into Angular, and no schema change depends on synchronize.