Files
ashen-realms/docs/Ashen_Realms_Technical_Foundation_Implementation_Plan.md
2026-08-18 17:01:21 +02:00

37 KiB
Raw Blame History

Ashen Realms Technical Foundation & Monorepo Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Eine schlanke, produktionsfähige technische Basis für Ashen Realms erstellen, in der Angular und NestJS in einem npm-Workspace-Monorepo entwickelt und als ein einziges Docker-Image ausgeliefert werden, während PostgreSQL als separater persistenter Datendienst läuft.

Architecture: Ashen Realms wird als modularer Monolith umgesetzt. Angular ist die browserseitige SPA, NestJS ist der einzige Runtime-Prozess im Produktionscontainer und stellt sowohl /api/* als auch die gebauten Angular-Dateien bereit. PostgreSQL wird über TypeORM angesprochen; Spiellogik ist vollständig serverautoritativ.

Tech Stack: Node.js 24 LTS, npm Workspaces, Angular 22.x, NestJS 11.x, TypeScript, TypeORM 1.x, PostgreSQL, SCSS, Docker Multi-Stage Build, Jest/Vitest je nach generiertem Angular-Setup, Supertest für API-Tests.

Spec: Diese Planung implementiert die bestehenden Projektunterlagen:

  • docs/design-manifest.md
  • docs/vertical-slice-world-content-design.md
  • docs/balancing-items-loot-design.md
  • docs/ui-visual-design-specification.md

Global Constraints

  • Kein Nx, Turborepo oder vergleichbares Monorepo-Framework.
  • Ein npm-Workspace-Root verwaltet apps/* und packages/*.
  • Produktionsdeployment besteht aus genau einem Ashen-Realms-Anwendungscontainer.
  • PostgreSQL läuft außerhalb des Anwendungscontainers.
  • NestJS ist der einzige Runtime-Prozess im Anwendungscontainer.
  • Angular wird im Produktionsbuild statisch durch NestJS ausgeliefert.
  • Alle API-Routen verwenden den Prefix /api.
  • Spiellogik ist serverautoritativ.
  • TypeORM verwendet Migrationen; synchronize: true ist in Produktion verboten.
  • Content-Definitionen und Player-State bleiben fachlich getrennt.
  • Keine Microservices, kein Redis, Kafka, RabbitMQ, GraphQL, CQRS oder Event Sourcing für V1.
  • Assets liegen für den ersten Vertical Slice lokal im Web-Bundle.
  • Desktop ist die primäre Zielplattform für den ersten UI-Prototyp.
  • Der erste technische End-to-End-Loop lautet: Charakter → Welt → Reise → Jagd → Kampf → Loot → Inventar → Equipment.

1. Zielarchitektur

Browser
   |
   | HTTPS
   v
+--------------------------------------------------+
| Ashen Realms Application Container              |
|                                                  |
|  NestJS                                         |
|  +-- /api/*            REST API                 |
|  +-- /*                Angular SPA              |
|  +-- Auth                                      |
|  +-- Characters                                |
|  +-- World / Travel                            |
|  +-- Hunting                                   |
|  +-- Combat                                    |
|  +-- Inventory / Equipment                     |
|  +-- Loot                                      |
|                                                  |
+-------------------------+------------------------+
                          |
                          | PostgreSQL protocol
                          v
                 +------------------+
                 | PostgreSQL       |
                 | persistent data  |
                 +------------------+

Entwicklungsmodus

localhost:4200  Angular Dev Server
       |
       | /api proxy
       v
localhost:3000  NestJS
       |
       v
localhost:5432  PostgreSQL

Produktiv existiert kein separater Angular-Server.


2. Repository-Struktur

ashen-realms/
├── apps/
│   ├── web/
│   │   ├── public/
│   │   │   └── assets/
│   │   │       ├── characters/
│   │   │       ├── items/
│   │   │       ├── locations/
│   │   │       ├── monsters/
│   │   │       └── ui/
│   │   └── src/
│   │       └── app/
│   │           ├── core/
│   │           ├── layout/
│   │           ├── features/
│   │           │   ├── world/
│   │           │   ├── hunting/
│   │           │   ├── combat/
│   │           │   ├── inventory/
│   │           │   └── character/
│   │           └── shared/
│   │
│   └── api/
│       ├── src/
│       │   ├── auth/
│       │   ├── users/
│       │   ├── characters/
│       │   ├── world/
│       │   ├── travel/
│       │   ├── hunting/
│       │   ├── combat/
│       │   ├── monsters/
│       │   ├── items/
│       │   ├── inventory/
│       │   ├── equipment/
│       │   ├── loot/
│       │   ├── database/
│       │   ├── config/
│       │   ├── app.module.ts
│       │   └── main.ts
│       └── test/
│
├── packages/
│   ├── shared/
│   │   └── src/
│   │       ├── enums/
│   │       ├── contracts/
│   │       └── index.ts
│   │
│   └── game-content/
│       └── src/
│           ├── schemas/
│           ├── enums/
│           └── index.ts
│
├── docs/
│   ├── design-manifest.md
│   ├── vertical-slice-world-content-design.md
│   ├── balancing-items-loot-design.md
│   ├── ui-visual-design-specification.md
│   └── superpowers/
│       └── plans/
│
├── .dockerignore
├── .env.example
├── .gitignore
├── Dockerfile
├── package.json
├── package-lock.json
├── tsconfig.base.json
└── README.md

3. Verantwortlichkeiten der Workspaces

apps/web

Verantwortlich für:

  • Darstellung des Spielzustands
  • Eingaben des Spielers
  • Navigation
  • UI-State
  • Animationen
  • Aufruf der REST-API
  • Countdown-Darstellung für Reisen
  • Darstellung serverseitig berechneter Kampfereignisse

Nicht verantwortlich für:

  • Schadensberechnung
  • Loot-Rolls
  • Reiseabschluss
  • seltene Begegnungswürfe
  • XP-Vergabe
  • Itemwerte

apps/api

Verantwortlich für:

  • Authentifizierung
  • Persistenz
  • Charakterzustand
  • Reisen
  • Jagdergebnisse
  • Kampfregeln
  • Loot
  • Inventar
  • Equipment
  • XP
  • serverautoritative Validierung

packages/shared

Nur für echte Cross-Boundary-Verträge.

Geeignet:

export enum DangerRating {
  WEAK = 'WEAK',
  MATCH = 'MATCH',
  STRONG = 'STRONG',
  VERY_DANGEROUS = 'VERY_DANGEROUS',
  DEADLY = 'DEADLY',
}
export interface CombatEventDto {
  type: string;
  source: 'PLAYER' | 'MONSTER';
  target: 'PLAYER' | 'MONSTER';
  amount?: number;
}

Nicht geeignet:

  • TypeORM Entities
  • NestJS Services
  • Angular Components
  • Domain-Implementierungsdetails

packages/game-content

V1 enthält hauptsächlich gemeinsame Content-Schemas und Enums.

Die tatsächlichen persistierten Content-Daten liegen in PostgreSQL und werden über Seeds/Migrationen angelegt.

Beispiel:

export enum ItemRarity {
  COMMON = 'COMMON',
  RARE = 'RARE',
  EPIC = 'EPIC',
}

Später kann dieses Package Validierungsschemas für ein CMS enthalten.


4. API-Konventionen

Alle Endpunkte beginnen mit:

/api

Beispiele:

POST /api/auth/register
POST /api/auth/login

GET  /api/characters/me

GET  /api/world/current-location
POST /api/travel
GET  /api/travel/current

POST /api/hunts
POST /api/hunt-encounters/:id/attack

POST /api/combats/:id/actions

GET  /api/inventory
GET  /api/equipment
POST /api/equipment
DELETE /api/equipment/:slot

Fehler verwenden ein konsistentes Format:

{
  "statusCode": 400,
  "code": "INVALID_TRAVEL_TARGET",
  "message": "The selected location is not connected to the current location."
}

5. TypeORM-Konzept

Data Source

Datei:

apps/api/src/database/data-source.ts

Konzeptionell:

import 'reflect-metadata';
import { DataSource } from 'typeorm';

export const AppDataSource = new DataSource({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  entities: ['dist/**/*.entity.js'],
  migrations: ['dist/database/migrations/*.js'],
  synchronize: false,
});

Für Development/CLI muss ein TypeScript-kompatibler Data-Source-Pfad konfiguriert werden.


Migrationen

Ablage:

apps/api/src/database/migrations/

Workflow:

Entity ändern
→ Migration generieren
→ SQL prüfen
→ Migration ausführen
→ Tests

Produktionsstart führt Migrationen kontrolliert vor dem App-Start aus oder über einen expliziten Deployment-Schritt.

Keine automatische Schema-Synchronisation.


6. V1-Datenmodell

Content

LocationDefinition
LocationConnection
MonsterDefinition
LocationMonster
AbilityDefinition
ItemDefinition
LootTable
LootTableEntry

Player State

User
Character
CharacterItem
CharacterEquipment
Travel
Hunt
HuntEncounter
Combat
CombatEvent

7. Kern-Relationen

User
└── Character[]

Character
├── currentLocation -> LocationDefinition
├── CharacterItem[]
├── CharacterEquipment[]
├── Travel[]
├── Hunt[]
└── Combat[]

LocationDefinition
├── LocationConnection[]
└── LocationMonster[]

MonsterDefinition
├── AbilityDefinition[]
└── LootTable

ItemDefinition
└── CharacterItem[]

8. Zentrale Services

CharacterStatsService

Einzige Quelle für effektive Charakterwerte.

export interface EffectiveCharacterStats {
  maxHp: number;
  currentHp: number;
  attack: number;
  weaponDamage: number;
  armor: number;
  combatPower: number;
}

Berechnet aus:

Character Base Stats
+ Equipment
+ ItemDefinition
+ später Buffs / Sets

TravelService

Verantwortlich für:

startTravel(characterId: string, targetLocationId: string): Promise<Travel>
getCurrentTravel(characterId: string): Promise<Travel | null>
completeTravelIfDue(characterId: string): Promise<Travel | null>

HuntingService

startHunt(characterId: string): Promise<HuntResultDto>
attackEncounter(characterId: string, encounterId: string): Promise<Combat>

CombatEngineService

Framework-unabhängige Kampflogik.

resolveAction(
  state: CombatEngineState,
  action: CombatActionInput,
): CombatEngineResult

Wichtig:

Der Service soll möglichst ohne Datenbankzugriff testbar sein.


CombatService

Orchestriert:

DB-Zustand laden
→ CombatEngineService aufrufen
→ Events persistieren
→ Sieg/Niederlage prüfen
→ Loot auslösen
→ Transaktion committen

LootService

rollLoot(monsterDefinitionId: string): Promise<LootResult>
grantLoot(characterId: string, result: LootResult): Promise<void>

9. Umgebungsvariablen

.env.example

NODE_ENV=development
PORT=3000

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

JWT_ACCESS_SECRET=change-me
JWT_REFRESH_SECRET=change-me-too
JWT_ACCESS_TTL=15m
JWT_REFRESH_TTL=30d

Keine Secrets im Repository.


10. Root npm Workspaces

Root package.json:

{
  "name": "ashen-realms",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "dev:web": "npm run start --workspace=@ashen-realms/web",
    "dev:api": "npm run start:dev --workspace=@ashen-realms/api",
    "build:web": "npm run build --workspace=@ashen-realms/web",
    "build:api": "npm run build --workspace=@ashen-realms/api",
    "build": "npm run build:web && npm run build:api",
    "test": "npm run test --workspaces --if-present",
    "lint": "npm run lint --workspaces --if-present"
  }
}

Workspace-Namen:

@ashen-realms/web
@ashen-realms/api
@ashen-realms/shared
@ashen-realms/game-content

11. Angular Development Proxy

apps/web/proxy.conf.json

{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false,
    "changeOrigin": true
  }
}

Angular-Dev-Start verwendet diese Proxy-Konfiguration.

Frontend-Code ruft immer relative URLs auf:

this.http.get('/api/characters/me');

Nicht:

this.http.get('http://localhost:3000/api/characters/me');

Dadurch funktioniert derselbe Code in Development und Produktion.


12. Angular App Shell

V1-Komponenten:

AppShellComponent
TopBarComponent
SideNavigationComponent
GameFooterComponent
ContextPanelComponent

Feature-Routen:

/world
/hunt
/combat/:combatId
/inventory
/character

Die visuelle Umsetzung folgt docs/ui-visual-design-specification.md.


13. Produktionsauslieferung von Angular

Angular wird zuerst gebaut.

Das erzeugte Browser-Bundle wird anschließend in einen Pfad kopiert, den NestJS statisch ausliefert.

Ziel im Runtime-Image:

/app/
├── api-dist/
├── web-dist/
└── node_modules/

NestJS liefert:

/api/*  → Controller
/*      → Angular Static Files

Nicht gefundene Nicht-API-Routen fallen auf:

web-dist/index.html

Damit funktionieren direkte Aufrufe wie:

/world
/inventory
/combat/123

14. Docker-Ziel

Ein Image, ein Prozess.

Multi-Stage-Konzept

FROM node:24-alpine AS build

WORKDIR /app

COPY package*.json ./
COPY apps ./apps
COPY packages ./packages
COPY tsconfig.base.json ./

RUN npm ci
RUN npm run build


FROM node:24-alpine AS runtime

ENV NODE_ENV=production
WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY --from=build /app/apps/api/dist ./api-dist
COPY --from=build /app/apps/web/dist ./web-build

EXPOSE 3000

CMD ["node", "api-dist/main.js"]

Der konkrete Angular-Ausgabeunterordner muss beim tatsächlichen Bootstrap einmal geprüft und im Dockerfile entsprechend gesetzt werden.


15. Container-Prinzipien

Der App-Container ist zustandslos.

Nicht im Container persistieren:

  • PostgreSQL-Daten
  • Uploads
  • Sessions als lokale Dateien
  • User-Content

Ein Container-Neustart darf keine Spieldaten verlieren.


16. Health Endpoint

V1:

GET /api/health

Response:

{
  "status": "ok"
}

Später kann DB-Konnektivität ergänzt werden.

Dieser Endpoint wird auch für Docker-/Deployment-Healthchecks verwendet.


17. Seed-Strategie

V1 benötigt reproduzierbare Content-Daten.

Ablage:

apps/api/src/database/seeds/

Erste Seeds:

locations.seed.ts
location-connections.seed.ts
items.seed.ts
monsters.seed.ts
location-monsters.seed.ts
loot-tables.seed.ts

Seeds verwenden stabile key-Werte:

ashen-fields
burned-road
dusk-wolf
ashen-blade

Interne UUIDs dürfen variieren; fachliche Referenzen verwenden stabile Keys.


18. Teststrategie

Unit Tests

Besonders wichtig:

CharacterStatsService
CombatEngineService
LootService
TravelService
HuntingService

API Integration Tests

Mindestens:

GET /api/health
Travel start
Hunt start
Combat action
Inventory read
Equip item

Frontend Tests

Für V1:

  • App Shell rendert
  • Navigation funktioniert
  • World Screen verarbeitet API-State
  • Combat Action sendet nur Action-ID und niemals Schadenswerte

19. Erster End-to-End-Slice

Der erste wirklich spielbare technische Slice enthält nur:

1 Character
2 Locations
1 Connection
2 Monsters
3 Items
1 Hunt
1 Combat Flow
1 Loot Table
Inventory
Equipment

Beispiel:

Graufurt Südtor
   |
   v
Verbrannte Straße
   |
   +-- Aschenratte
   +-- Straßenräuber

Items:

Abgenutztes Kurzschwert
Räuberklinge
Räuberhaube

Damit wird zuerst die Architektur bewiesen, bevor der komplette Content eingepflegt wird.


20. Nicht im ersten technischen Slice

Noch nicht implementieren:

  • Quests
  • Gebietswährungen
  • Händler
  • Sets
  • Bosskampf
  • Social Features
  • Realtime
  • Chat
  • Gilden
  • Auktionshaus
  • Crafting
  • Admin CMS
  • Object Storage
  • PvP

21. Implementierungsreihenfolge

Task 1: Workspace und Toolchain

Files:

  • Create: package.json
  • Create: tsconfig.base.json
  • Create: .gitignore
  • Create: .env.example
  • Create: README.md
  • Create: apps/web/*
  • Create: apps/api/*
  • Create: packages/shared/*
  • Create: packages/game-content/*

Interfaces:

  • Produces: npm Workspaces @ashen-realms/web, @ashen-realms/api, @ashen-realms/shared, @ashen-realms/game-content

  • Consumes: none

  • Step 1: Initialisiere den Root-Workspace

mkdir ashen-realms
cd ashen-realms
npm init -y
mkdir -p apps packages docs
  • Step 2: Konfiguriere npm Workspaces

Root package.json auf die Struktur aus Abschnitt 10 setzen.

  • Step 3: Erzeuge Angular
npx @angular/cli@22 new web \
  --directory apps/web \
  --routing \
  --style scss \
  --skip-git \
  --standalone
  • Step 4: Erzeuge NestJS
npx @nestjs/cli new apps/api \
  --package-manager npm \
  --skip-git

Danach den Paketnamen auf @ashen-realms/api setzen.

  • Step 5: Erzeuge Shared Packages
mkdir -p packages/shared/src
mkdir -p packages/game-content/src

Minimaler packages/shared/package.json:

{
  "name": "@ashen-realms/shared",
  "version": "0.0.1",
  "private": true,
  "main": "src/index.ts",
  "types": "src/index.ts"
}

Analog für @ashen-realms/game-content.

  • Step 6: Installiere Root-Abhängigkeiten
npm install
  • Step 7: Verifiziere Builds
npm run build:web
npm run build:api

Expected: beide Builds erfolgreich.

  • Step 8: Commit
git add .
git commit -m "chore: bootstrap angular nest monorepo"

Task 2: PostgreSQL und TypeORM Basis

Files:

  • Create: apps/api/src/config/database.config.ts
  • Create: apps/api/src/database/data-source.ts
  • Create: apps/api/src/database/database.module.ts
  • Modify: apps/api/src/app.module.ts
  • Modify: apps/api/package.json
  • Test: apps/api/src/database/database.module.spec.ts

Interfaces:

  • Produces: AppDataSource, TypeORM connection through Nest

  • Consumes: DATABASE_URL

  • Step 1: Installiere DB-Abhängigkeiten

npm install --workspace=@ashen-realms/api typeorm @nestjs/typeorm pg reflect-metadata
  • Step 2: Schreibe den fehlschlagenden Konfigurationstest
describe('database config', () => {
  it('disables synchronize', () => {
    const config = createDatabaseConfig(
      'postgresql://test:test@localhost:5432/test',
    );

    expect(config.synchronize).toBe(false);
  });
});
  • Step 3: Implementiere createDatabaseConfig
export function createDatabaseConfig(url: string) {
  return {
    type: 'postgres' as const,
    url,
    autoLoadEntities: true,
    synchronize: false,
  };
}
  • Step 4: Registriere TypeORM
TypeOrmModule.forRootAsync({
  useFactory: () => createDatabaseConfig(
    process.env.DATABASE_URL as string,
  ),
})
  • Step 5: Lege TypeORM CLI DataSource an

data-source.ts verwendet dieselbe DATABASE_URL, Entities und Migrationspfade.

  • Step 6: Führe Tests aus
npm test --workspace=@ashen-realms/api

Expected: PASS.

  • Step 7: Commit
git add apps/api
git commit -m "feat: add postgres typeorm foundation"

Task 3: Health API und globaler /api Prefix

Files:

  • Create: apps/api/src/health/health.controller.ts
  • Create: apps/api/src/health/health.module.ts
  • Create: apps/api/src/health/health.controller.spec.ts
  • Modify: apps/api/src/app.module.ts
  • Modify: apps/api/src/main.ts

Interfaces:

  • Produces: GET /api/health

  • Consumes: Nest application

  • Step 1: Schreibe Controller-Test

it('returns ok', () => {
  expect(controller.getHealth()).toEqual({ status: 'ok' });
});
  • Step 2: Implementiere Controller
@Controller('health')
export class HealthController {
  @Get()
  getHealth() {
    return { status: 'ok' };
  }
}
  • Step 3: Setze globalen Prefix
app.setGlobalPrefix('api');
  • Step 4: Teste API
npm run start:dev --workspace=@ashen-realms/api
curl http://localhost:3000/api/health

Expected:

{"status":"ok"}
  • Step 5: Commit
git add apps/api
git commit -m "feat: add health endpoint"

Task 4: Angular App Shell

Files:

  • Create: apps/web/src/app/layout/app-shell/*
  • Create: apps/web/src/app/layout/top-bar/*
  • Create: apps/web/src/app/layout/side-navigation/*
  • Create: apps/web/src/app/layout/game-footer/*
  • Create: apps/web/src/app/features/world/world-page/*
  • Modify: apps/web/src/app/app.routes.ts
  • Modify: apps/web/src/app/app.component.*
  • Test: component specs generated beside components

Interfaces:

  • Produces: /world screen shell

  • Consumes: UI Visual Design Specification

  • Step 1: Schreibe App-Shell-Test

Testet, dass Topbar, Navigation, Router Outlet und Footer vorhanden sind.

  • Step 2: Implementiere Shell

Struktur:

<div class="app-shell">
  <app-top-bar />
  <app-side-navigation />
  <main class="app-shell__content">
    <router-outlet />
  </main>
  <app-game-footer />
</div>
  • Step 3: Erzeuge /world Route
{
  path: 'world',
  loadComponent: () =>
    import('./features/world/world-page/world-page.component')
      .then(m => m.WorldPageComponent),
}
  • Step 4: Setze Default Redirect
{ path: '', pathMatch: 'full', redirectTo: 'world' }
  • Step 5: Führe Tests aus
npm test --workspace=@ashen-realms/web
  • Step 6: Commit
git add apps/web
git commit -m "feat: add game application shell"

Task 5: Angular API Proxy und typed API Client

Files:

  • Create: apps/web/proxy.conf.json
  • Create: apps/web/src/app/core/api/api-client.service.ts
  • Create: apps/web/src/app/core/api/api-client.service.spec.ts
  • Modify: apps/web/package.json
  • Modify: Angular dev-server config if required

Interfaces:

  • Produces: ApiClientService.get<T>(), post<T>()

  • Consumes: relative /api/* URLs

  • Step 1: Schreibe Proxy-Datei

Verwende Abschnitt 11.

  • Step 2: Schreibe API-Client-Test

Der Test verifiziert, dass /api/health relativ aufgerufen wird.

  • Step 3: Implementiere Client
@Injectable({ providedIn: 'root' })
export class ApiClientService {
  constructor(private readonly http: HttpClient) {}

  get<T>(path: string) {
    return this.http.get<T>(`/api${path}`);
  }

  post<T>(path: string, body: unknown) {
    return this.http.post<T>(`/api${path}`, body);
  }
}
  • Step 4: Passe Dev-Start an

Angular Dev Server startet mit Proxy.

  • Step 5: Commit
git add apps/web
git commit -m "feat: add frontend api client"

Task 6: Erste Content-Entities

Files:

  • Create: apps/api/src/world/entities/location-definition.entity.ts
  • Create: apps/api/src/world/entities/location-connection.entity.ts
  • Create: apps/api/src/monsters/entities/monster-definition.entity.ts
  • Create: apps/api/src/monsters/entities/location-monster.entity.ts
  • Create: apps/api/src/items/entities/item-definition.entity.ts
  • Create: corresponding modules
  • Test: metadata/entity tests

Interfaces:

  • Produces: persistierbare World/Monster/Item Content-Definitionen

  • Consumes: TypeORM foundation

  • Step 1: Implementiere LocationDefinition

Pflichtfelder:

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

key erhält einen Unique Index.

  • Step 2: Implementiere LocationConnection

Pflichtfelder:

id
fromLocation
toLocation
travelDurationSeconds
ambushChance
enabled
  • Step 3: Implementiere MonsterDefinition

Pflichtfelder:

id
key
name
level
maxHp
attack
armor
experienceReward
silverMin
silverMax
artworkPath
  • Step 4: Implementiere LocationMonster

Pflichtfelder:

id
location
monster
weight
encounterType
enabled
  • Step 5: Implementiere ItemDefinition

Pflichtfelder:

id
key
name
description
type
equipmentSlot
rarity
tier
requiredLevel
weaponDamage
bonusHp
bonusAttack
bonusArmor
sellPrice
iconPath
  • Step 6: Generiere Migration
npm run typeorm:migration:generate --workspace=@ashen-realms/api -- InitialContent
  • Step 7: Prüfe generiertes SQL

Keine Drop-/Recreate-Operationen an unerwarteten Tabellen akzeptieren.

  • Step 8: Führe Migration aus
npm run typeorm:migration:run --workspace=@ashen-realms/api
  • Step 9: Commit
git add apps/api
git commit -m "feat: add core content entities"

Task 7: Character, Inventory und Equipment State

Files:

  • Create: apps/api/src/users/entities/user.entity.ts
  • Create: apps/api/src/characters/entities/character.entity.ts
  • Create: apps/api/src/inventory/entities/character-item.entity.ts
  • Create: apps/api/src/equipment/entities/character-equipment.entity.ts
  • Create: apps/api/src/characters/character-stats.service.ts
  • Test: character-stats.service.spec.ts

Interfaces:

  • Produces: EffectiveCharacterStats

  • Consumes: ItemDefinition

  • Step 1: Schreibe failing stats test

Beispiel:

it('combines base stats with equipped items', () => {
  const result = service.calculate(character, equipment);

  expect(result.attack).toBe(12);
  expect(result.weaponDamage).toBe(15);
  expect(result.armor).toBe(25);
  expect(result.maxHp).toBe(150);
});
  • Step 2: Implementiere Entities

Nutze UUID Primary Keys und Foreign Keys.

  • Step 3: Implementiere CharacterStatsService

Combat Power:

const combatPower =
  maxHp / 10 +
  attack * 2 +
  weaponDamage * 2 +
  armor * 1.5;
  • Step 4: Führe Unit Tests aus
npm test --workspace=@ashen-realms/api -- character-stats
  • Step 5: Generiere und prüfe Migration

  • Step 6: Commit

git add apps/api
git commit -m "feat: add character inventory equipment state"

Task 8: Travel Vertical Slice

Files:

  • Create: apps/api/src/travel/entities/travel.entity.ts
  • Create: apps/api/src/travel/travel.service.ts
  • Create: apps/api/src/travel/travel.controller.ts
  • Create: service/controller tests

Interfaces:

  • Produces:

    • POST /api/travel
    • GET /api/travel/current
  • Consumes: Character current location, LocationConnection

  • Step 1: Schreibe Test für ungültiges Reiseziel

Nicht verbundene Locations müssen mit Domain-Fehler abgelehnt werden.

  • Step 2: Schreibe Test für gültige Reise

Testet startedAt, arrivesAt, Origin und Target.

  • Step 3: Implementiere startTravel

Nur der Server berechnet arrivesAt.

  • Step 4: Implementiere completeTravelIfDue

Vor Ablauf darf currentLocationId nicht wechseln.

  • Step 5: Implementiere Controller

Request enthält nur:

{
  "targetLocationId": "uuid"
}
  • Step 6: Führe Tests aus

  • Step 7: Commit

git add apps/api
git commit -m "feat: add server authoritative travel"

Task 9: Hunt und Encounter Slice

Files:

  • Create: apps/api/src/hunting/entities/hunt.entity.ts
  • Create: apps/api/src/hunting/entities/hunt-encounter.entity.ts
  • Create: apps/api/src/hunting/hunting.service.ts
  • Create: apps/api/src/hunting/hunting.controller.ts
  • Create: tests

Interfaces:

  • Produces:

    • POST /api/hunts
    • persisted HuntEncounter IDs
  • Consumes: LocationMonster weighted pool

  • Step 1: Schreibe Test für Jagd in nicht jagdbarem Ort

Expected: Domain-Fehler.

  • Step 2: Schreibe deterministischen Weight-Test

Random-Quelle im Test injizieren, damit Auswahl reproduzierbar ist.

  • Step 3: Implementiere startHunt

Server erzeugt Encounter-Datensätze.

  • Step 4: Verhindere frei wählbare Monster IDs

Combat darf später nur aus gültiger Encounter-ID starten.

  • Step 5: Führe Tests aus

  • Step 6: Commit

git add apps/api
git commit -m "feat: add hunting encounters"

Task 10: Combat Engine

Files:

  • Create: apps/api/src/combat/combat-engine.service.ts
  • Create: apps/api/src/combat/combat-engine.types.ts
  • Create: apps/api/src/combat/combat-engine.service.spec.ts

Interfaces:

  • Produces:

    • resolveAction(state, action): CombatEngineResult
  • Consumes:

    • fixed player stats
    • monster stats
    • action input
  • Step 1: Schreibe Schadensformel-Test

expect(
  calculateDamage({
    attack: 12,
    weaponDamage: 15,
    armor: 20,
    multiplier: 1,
  }),
).toBe(20);
  • Step 2: Implementiere Schadensformel
const raw = (attack + weaponDamage) * multiplier;
const mitigated = raw * 60 / (60 + armor);
return Math.max(1, Math.round(mitigated));
  • Step 3: Schreibe Tests für Aktionen

Abdecken:

ATTACK = 100 %
HEAVY_STRIKE = 160 %
SHIELD_BASH = 70 % + interrupt
DEFEND = 50 % incoming reduction
POTION = 35 % max HP
  • Step 4: Implementiere minimale Actions

Keine Persistenz in diesem Service.

  • Step 5: Führe Tests aus

  • Step 6: Commit

git add apps/api/src/combat
git commit -m "feat: add deterministic combat engine"

Task 11: Persistierter Combat Flow

Files:

  • Create: apps/api/src/combat/entities/combat.entity.ts
  • Create: apps/api/src/combat/entities/combat-event.entity.ts
  • Create: apps/api/src/combat/combat.service.ts
  • Create: apps/api/src/combat/combat.controller.ts
  • Create: integration tests

Interfaces:

  • Produces:

    • POST /api/hunt-encounters/:id/attack
    • POST /api/combats/:id/actions
  • Consumes: CombatEngineService, HuntEncounter

  • Step 1: Teste Combat-Erstellung nur aus gültigem Encounter

  • Step 2: Persistiere Startzustand

Speichere:

round
playerCurrentHp
monsterCurrentHp
playerState jsonb
monsterState jsonb
  • Step 3: Implementiere Action Endpoint

Request:

{
  "action": "ATTACK"
}

Kein Damage-Feld zulassen.

  • Step 4: Persistiere CombatEvents

Jede Runde erhält geordnete Events.

  • Step 5: Commit
git add apps/api/src/combat
git commit -m "feat: persist combat rounds and events"

Task 12: Loot, Inventory und Equipment End-to-End

Files:

  • Create: apps/api/src/loot/entities/loot-table.entity.ts
  • Create: apps/api/src/loot/entities/loot-table-entry.entity.ts
  • Create: apps/api/src/loot/loot.service.ts
  • Create: apps/api/src/inventory/inventory.controller.ts
  • Create: apps/api/src/equipment/equipment.controller.ts
  • Create: tests

Interfaces:

  • Produces:

    • loot grants
    • GET /api/inventory
    • GET /api/equipment
    • POST /api/equipment
  • Consumes: Combat WON state

  • Step 1: Schreibe deterministischen Loot-Roll-Test

Random-Quelle injizieren.

  • Step 2: Implementiere Loot-Tabellen

  • Step 3: Vergib Loot in einer DB-Transaktion

Atomar:

Combat WON
XP
Silver
CharacterItem
Combat status
  • Step 4: Implementiere Inventory GET

  • Step 5: Implementiere Equip Endpoint

Validierungen:

Item gehört Character
Item ist ausrüstbar
Slot stimmt
requiredLevel erfüllt
  • Step 6: Führe End-to-End-Test aus

Flow:

Hunt
→ Encounter
→ Combat
→ Win
→ Loot
→ Inventory
→ Equip
→ Stats increased
  • Step 7: Commit
git add apps/api
git commit -m "feat: complete loot inventory equipment loop"

Task 13: Seed des Mini-Slices

Files:

  • Create: apps/api/src/database/seeds/vertical-slice.seed.ts
  • Create: seed command
  • Test: seed integration test

Interfaces:

  • Produces:

    • Südtor
    • Verbrannte Straße
    • Aschenratte
    • Straßenräuber
    • drei Items
    • Loot tables
  • Consumes: content entities

  • Step 1: Seed Locations

south-gate
burned-road
  • Step 2: Seed Verbindung
south-gate -> burned-road
duration = 10
ambushChance = 0.05
  • Step 3: Seed Monster
ash-rat
road-bandit
  • Step 4: Seed Items
worn-short-sword
bandit-blade
bandit-hood
  • Step 5: Seed Loot

  • Step 6: Seed idempotent machen

Wiederholtes Ausführen darf keine Duplikate erzeugen.

  • Step 7: Commit
git add apps/api/src/database
git commit -m "feat: seed first playable content slice"

Task 14: Angular World → Hunt → Combat Flow

Files:

  • Create/Modify feature components under:
    • apps/web/src/app/features/world/
    • apps/web/src/app/features/hunting/
    • apps/web/src/app/features/combat/
    • apps/web/src/app/features/inventory/
  • Create API state services
  • Test components/services

Interfaces:

  • Produces: first playable browser flow

  • Consumes: REST endpoints from Tasks 812

  • Step 1: World Screen lädt aktuellen Ort

  • Step 2: Reisebutton startet Travel

  • Step 3: Countdown nutzt ausschließlich arrivesAt vom Server

  • Step 4: Jagdseite rendert HuntEncounter Cards

  • Step 5: Angreifen öffnet Combat Route

  • Step 6: Combat Buttons senden nur Action Enum

  • Step 7: CombatEvents treiben Animation und Log

  • Step 8: Nach Sieg öffnet Loot-Zusammenfassung

  • Step 9: Inventar erlaubt Ausrüsten

  • Step 10: Verifiziere gesamten Browserflow

  • Step 11: Commit

git add apps/web
git commit -m "feat: connect first playable frontend loop"

Task 15: NestJS Static Hosting für Angular

Files:

  • Modify: apps/api/src/app.module.ts
  • Modify: apps/api/src/main.ts
  • Modify: build scripts
  • Test: production static-serving smoke test

Interfaces:

  • Produces: ein NestJS-Prozess für API + Angular

  • Consumes: Angular production build

  • Step 1: Installiere Static Serving

npm install --workspace=@ashen-realms/api @nestjs/serve-static
  • Step 2: Konfiguriere Angular-Ausgabepfad

Build-Ausgabe in gemeinsamen Runtime-Pfad kopieren.

  • Step 3: Konfiguriere SPA-Fallback

/api/* bleibt API; andere unbekannte Pfade liefern index.html.

  • Step 4: Baue Production
npm run build
  • Step 5: Starte nur NestJS
node apps/api/dist/main.js
  • Step 6: Prüfe
curl http://localhost:3000/api/health
curl -I http://localhost:3000/world

Expected:

  • API liefert JSON

  • /world liefert Angular HTML

  • Step 7: Commit

git add apps package.json
git commit -m "feat: serve angular through nest"

Task 16: Docker Production Image

Files:

  • Create: Dockerfile
  • Create: .dockerignore
  • Modify: README.md
  • Test: container smoke test

Interfaces:

  • Produces: ein deploybares Ashen-Realms-Image

  • Consumes: root build

  • Step 1: Erstelle .dockerignore

Mindestens:

node_modules
.git
dist
coverage
.env
  • Step 2: Implementiere Multi-Stage Dockerfile

Basis ist Node 24 Alpine.

  • Step 3: Baue Image
docker build -t ashen-realms:local .
  • Step 4: Starte Container
docker run --rm \
  -p 3000:3000 \
  -e DATABASE_URL="$DATABASE_URL" \
  -e JWT_ACCESS_SECRET="local-access" \
  -e JWT_REFRESH_SECRET="local-refresh" \
  ashen-realms:local
  • Step 5: Smoke Test
curl http://localhost:3000/api/health
curl -I http://localhost:3000/world
  • Step 6: Verifiziere genau einen Runtime-Prozess

Container enthält keinen Angular Dev Server und keinen PostgreSQL-Prozess.

  • Step 7: Commit
git add Dockerfile .dockerignore README.md
git commit -m "chore: add single-container production image"

22. Definition of Done für das technische Fundament

Das Fundament ist fertig, wenn:

  • npm install am Root funktioniert.
  • Angular und NestJS aus einem npm-Workspace gebaut werden.
  • PostgreSQL über TypeORM erreichbar ist.
  • Migrationen funktionieren.
  • synchronize deaktiviert ist.
  • /api/health erreichbar ist.
  • Angular lokal über Proxy gegen NestJS arbeitet.
  • Production Build Angular über NestJS ausliefert.
  • ein Docker-Image beide Anwendungsteile enthält.
  • der Runtime-Container nur NestJS startet.
  • PostgreSQL außerhalb des App-Containers bleibt.
  • der Mini-Slice Reise → Jagd → Kampf → Loot → Equipment funktioniert.
  • Combat und Loot serverseitig bestimmt werden.
  • Unit- und Integrationstests grün sind.

23. Nach diesem Plan

Erst danach den Content schrittweise erweitern:

Aschenfelder vollständig
→ Dämmerwald
→ Vergessene Ruinen

Anschließend:

Quests
→ Gebietswährungen
→ Händler
→ Sets
→ Auth-Härtung
→ Social / Realtime
→ CMS

Die Architektur wird nicht vorab für diese späteren Systeme aufgebläht.


24. Architekturentscheidungen V1

Bereich Entscheidung
Frontend Angular 22
Backend NestJS 11
Sprache TypeScript
ORM TypeORM 1.x
Datenbank PostgreSQL
Monorepo npm Workspaces
Monorepo Framework keines
API REST
API Prefix /api
Realtime noch keines
Runtime Node.js 24 LTS
Deployment 1 App-Docker-Container
PostgreSQL separater Dienst
Assets V1 im Angular Bundle
Architektur modularer Monolith
Spiellogik serverautoritativ
DB Schema TypeORM Migrationen
synchronize immer false außerhalb Wegwerf-Tests