37 KiB
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.mddocs/vertical-slice-world-content-design.mddocs/balancing-items-loot-design.mddocs/ui-visual-design-specification.md
Global Constraints
- Kein Nx, Turborepo oder vergleichbares Monorepo-Framework.
- Ein npm-Workspace-Root verwaltet
apps/*undpackages/*. - 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: trueist 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:
/worldscreen 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
/worldRoute
{
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/travelGET /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/attackPOST /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/inventoryGET /api/equipmentPOST /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 8–12
-
Step 1: World Screen lädt aktuellen Ort
-
Step 2: Reisebutton startet Travel
-
Step 3: Countdown nutzt ausschließlich
arrivesAtvom 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
-
/worldliefert 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 installam Root funktioniert.- Angular und NestJS aus einem npm-Workspace gebaut werden.
- PostgreSQL über TypeORM erreichbar ist.
- Migrationen funktionieren.
synchronizedeaktiviert ist./api/healtherreichbar 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 |