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

1993 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```text
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
```text
localhost:4200 Angular Dev Server
|
| /api proxy
v
localhost:3000 NestJS
|
v
localhost:5432 PostgreSQL
```
Produktiv existiert kein separater Angular-Server.
---
# 2. Repository-Struktur
```text
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:
```ts
export enum DangerRating {
WEAK = 'WEAK',
MATCH = 'MATCH',
STRONG = 'STRONG',
VERY_DANGEROUS = 'VERY_DANGEROUS',
DEADLY = 'DEADLY',
}
```
```ts
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:
```ts
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:
```text
/api
```
Beispiele:
```text
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:
```json
{
"statusCode": 400,
"code": "INVALID_TRAVEL_TARGET",
"message": "The selected location is not connected to the current location."
}
```
---
# 5. TypeORM-Konzept
## Data Source
Datei:
```text
apps/api/src/database/data-source.ts
```
Konzeptionell:
```ts
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:
```text
apps/api/src/database/migrations/
```
Workflow:
```text
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
```text
LocationDefinition
LocationConnection
MonsterDefinition
LocationMonster
AbilityDefinition
ItemDefinition
LootTable
LootTableEntry
```
## Player State
```text
User
Character
CharacterItem
CharacterEquipment
Travel
Hunt
HuntEncounter
Combat
CombatEvent
```
---
# 7. Kern-Relationen
```text
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.
```ts
export interface EffectiveCharacterStats {
maxHp: number;
currentHp: number;
attack: number;
weaponDamage: number;
armor: number;
combatPower: number;
}
```
Berechnet aus:
```text
Character Base Stats
+ Equipment
+ ItemDefinition
+ später Buffs / Sets
```
---
## `TravelService`
Verantwortlich für:
```ts
startTravel(characterId: string, targetLocationId: string): Promise<Travel>
getCurrentTravel(characterId: string): Promise<Travel | null>
completeTravelIfDue(characterId: string): Promise<Travel | null>
```
---
## `HuntingService`
```ts
startHunt(characterId: string): Promise<HuntResultDto>
attackEncounter(characterId: string, encounterId: string): Promise<Combat>
```
---
## `CombatEngineService`
Framework-unabhängige Kampflogik.
```ts
resolveAction(
state: CombatEngineState,
action: CombatActionInput,
): CombatEngineResult
```
Wichtig:
Der Service soll möglichst ohne Datenbankzugriff testbar sein.
---
## `CombatService`
Orchestriert:
```text
DB-Zustand laden
→ CombatEngineService aufrufen
→ Events persistieren
→ Sieg/Niederlage prüfen
→ Loot auslösen
→ Transaktion committen
```
---
## `LootService`
```ts
rollLoot(monsterDefinitionId: string): Promise<LootResult>
grantLoot(characterId: string, result: LootResult): Promise<void>
```
---
# 9. Umgebungsvariablen
`.env.example`
```dotenv
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`:
```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:
```text
@ashen-realms/web
@ashen-realms/api
@ashen-realms/shared
@ashen-realms/game-content
```
---
# 11. Angular Development Proxy
`apps/web/proxy.conf.json`
```json
{
"/api": {
"target": "http://localhost:3000",
"secure": false,
"changeOrigin": true
}
}
```
Angular-Dev-Start verwendet diese Proxy-Konfiguration.
Frontend-Code ruft immer relative URLs auf:
```ts
this.http.get('/api/characters/me');
```
Nicht:
```ts
this.http.get('http://localhost:3000/api/characters/me');
```
Dadurch funktioniert derselbe Code in Development und Produktion.
---
# 12. Angular App Shell
V1-Komponenten:
```text
AppShellComponent
TopBarComponent
SideNavigationComponent
GameFooterComponent
ContextPanelComponent
```
Feature-Routen:
```text
/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:
```text
/app/
├── api-dist/
├── web-dist/
└── node_modules/
```
NestJS liefert:
```text
/api/* → Controller
/* → Angular Static Files
```
Nicht gefundene Nicht-API-Routen fallen auf:
```text
web-dist/index.html
```
Damit funktionieren direkte Aufrufe wie:
```text
/world
/inventory
/combat/123
```
---
# 14. Docker-Ziel
Ein Image, ein Prozess.
## Multi-Stage-Konzept
```dockerfile
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:
```text
GET /api/health
```
Response:
```json
{
"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:
```text
apps/api/src/database/seeds/
```
Erste Seeds:
```text
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:
```text
ashen-fields
burned-road
dusk-wolf
ashen-blade
```
Interne UUIDs dürfen variieren; fachliche Referenzen verwenden stabile Keys.
---
# 18. Teststrategie
## Unit Tests
Besonders wichtig:
```text
CharacterStatsService
CombatEngineService
LootService
TravelService
HuntingService
```
## API Integration Tests
Mindestens:
```text
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:
```text
1 Character
2 Locations
1 Connection
2 Monsters
3 Items
1 Hunt
1 Combat Flow
1 Loot Table
Inventory
Equipment
```
Beispiel:
```text
Graufurt Südtor
|
v
Verbrannte Straße
|
+-- Aschenratte
+-- Straßenräuber
```
Items:
```text
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**
```bash
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**
```bash
npx @angular/cli@22 new web \
--directory apps/web \
--routing \
--style scss \
--skip-git \
--standalone
```
- [ ] **Step 4: Erzeuge NestJS**
```bash
npx @nestjs/cli new apps/api \
--package-manager npm \
--skip-git
```
Danach den Paketnamen auf `@ashen-realms/api` setzen.
- [ ] **Step 5: Erzeuge Shared Packages**
```bash
mkdir -p packages/shared/src
mkdir -p packages/game-content/src
```
Minimaler `packages/shared/package.json`:
```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**
```bash
npm install
```
- [ ] **Step 7: Verifiziere Builds**
```bash
npm run build:web
npm run build:api
```
Expected: beide Builds erfolgreich.
- [ ] **Step 8: Commit**
```bash
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**
```bash
npm install --workspace=@ashen-realms/api typeorm @nestjs/typeorm pg reflect-metadata
```
- [ ] **Step 2: Schreibe den fehlschlagenden Konfigurationstest**
```ts
describe('database config', () => {
it('disables synchronize', () => {
const config = createDatabaseConfig(
'postgresql://test:test@localhost:5432/test',
);
expect(config.synchronize).toBe(false);
});
});
```
- [ ] **Step 3: Implementiere `createDatabaseConfig`**
```ts
export function createDatabaseConfig(url: string) {
return {
type: 'postgres' as const,
url,
autoLoadEntities: true,
synchronize: false,
};
}
```
- [ ] **Step 4: Registriere TypeORM**
```ts
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**
```bash
npm test --workspace=@ashen-realms/api
```
Expected: PASS.
- [ ] **Step 7: Commit**
```bash
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**
```ts
it('returns ok', () => {
expect(controller.getHealth()).toEqual({ status: 'ok' });
});
```
- [ ] **Step 2: Implementiere Controller**
```ts
@Controller('health')
export class HealthController {
@Get()
getHealth() {
return { status: 'ok' };
}
}
```
- [ ] **Step 3: Setze globalen Prefix**
```ts
app.setGlobalPrefix('api');
```
- [ ] **Step 4: Teste API**
```bash
npm run start:dev --workspace=@ashen-realms/api
curl http://localhost:3000/api/health
```
Expected:
```json
{"status":"ok"}
```
- [ ] **Step 5: Commit**
```bash
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:
```html
<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**
```ts
{
path: 'world',
loadComponent: () =>
import('./features/world/world-page/world-page.component')
.then(m => m.WorldPageComponent),
}
```
- [ ] **Step 4: Setze Default Redirect**
```ts
{ path: '', pathMatch: 'full', redirectTo: 'world' }
```
- [ ] **Step 5: Führe Tests aus**
```bash
npm test --workspace=@ashen-realms/web
```
- [ ] **Step 6: Commit**
```bash
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**
```ts
@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**
```bash
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:
```text
id
key
name
description
regionKey
minRecommendedLevel
maxRecommendedLevel
dangerLevel
isSafe
huntingEnabled
artworkPath
createdAt
updatedAt
```
`key` erhält einen Unique Index.
- [ ] **Step 2: Implementiere `LocationConnection`**
Pflichtfelder:
```text
id
fromLocation
toLocation
travelDurationSeconds
ambushChance
enabled
```
- [ ] **Step 3: Implementiere `MonsterDefinition`**
Pflichtfelder:
```text
id
key
name
level
maxHp
attack
armor
experienceReward
silverMin
silverMax
artworkPath
```
- [ ] **Step 4: Implementiere `LocationMonster`**
Pflichtfelder:
```text
id
location
monster
weight
encounterType
enabled
```
- [ ] **Step 5: Implementiere `ItemDefinition`**
Pflichtfelder:
```text
id
key
name
description
type
equipmentSlot
rarity
tier
requiredLevel
weaponDamage
bonusHp
bonusAttack
bonusArmor
sellPrice
iconPath
```
- [ ] **Step 6: Generiere Migration**
```bash
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**
```bash
npm run typeorm:migration:run --workspace=@ashen-realms/api
```
- [ ] **Step 9: Commit**
```bash
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:
```ts
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:
```ts
const combatPower =
maxHp / 10 +
attack * 2 +
weaponDamage * 2 +
armor * 1.5;
```
- [ ] **Step 4: Führe Unit Tests aus**
```bash
npm test --workspace=@ashen-realms/api -- character-stats
```
- [ ] **Step 5: Generiere und prüfe Migration**
- [ ] **Step 6: Commit**
```bash
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:
```json
{
"targetLocationId": "uuid"
}
```
- [ ] **Step 6: Führe Tests aus**
- [ ] **Step 7: Commit**
```bash
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**
```bash
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**
```ts
expect(
calculateDamage({
attack: 12,
weaponDamage: 15,
armor: 20,
multiplier: 1,
}),
).toBe(20);
```
- [ ] **Step 2: Implementiere Schadensformel**
```ts
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:
```text
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**
```bash
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:
```text
round
playerCurrentHp
monsterCurrentHp
playerState jsonb
monsterState jsonb
```
- [ ] **Step 3: Implementiere Action Endpoint**
Request:
```json
{
"action": "ATTACK"
}
```
Kein Damage-Feld zulassen.
- [ ] **Step 4: Persistiere CombatEvents**
Jede Runde erhält geordnete Events.
- [ ] **Step 5: Commit**
```bash
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:
```text
Combat WON
XP
Silver
CharacterItem
Combat status
```
- [ ] **Step 4: Implementiere Inventory GET**
- [ ] **Step 5: Implementiere Equip Endpoint**
Validierungen:
```text
Item gehört Character
Item ist ausrüstbar
Slot stimmt
requiredLevel erfüllt
```
- [ ] **Step 6: Führe End-to-End-Test aus**
Flow:
```text
Hunt
→ Encounter
→ Combat
→ Win
→ Loot
→ Inventory
→ Equip
→ Stats increased
```
- [ ] **Step 7: Commit**
```bash
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**
```text
south-gate
burned-road
```
- [ ] **Step 2: Seed Verbindung**
```text
south-gate -> burned-road
duration = 10
ambushChance = 0.05
```
- [ ] **Step 3: Seed Monster**
```text
ash-rat
road-bandit
```
- [ ] **Step 4: Seed Items**
```text
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**
```bash
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**
```bash
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**
```bash
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**
```bash
npm run build
```
- [ ] **Step 5: Starte nur NestJS**
```bash
node apps/api/dist/main.js
```
- [ ] **Step 6: Prüfe**
```bash
curl http://localhost:3000/api/health
curl -I http://localhost:3000/world
```
Expected:
- API liefert JSON
- `/world` liefert Angular HTML
- [ ] **Step 7: Commit**
```bash
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:
```text
node_modules
.git
dist
coverage
.env
```
- [ ] **Step 2: Implementiere Multi-Stage Dockerfile**
Basis ist Node 24 Alpine.
- [ ] **Step 3: Baue Image**
```bash
docker build -t ashen-realms:local .
```
- [ ] **Step 4: Starte Container**
```bash
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**
```bash
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**
```bash
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:
```text
Aschenfelder vollständig
→ Dämmerwald
→ Vergessene Ruinen
```
Anschließend:
```text
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 |