1993 lines
37 KiB
Markdown
1993 lines
37 KiB
Markdown
# 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 8–12
|
||
|
||
- [ ] **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 |
|