# 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 getCurrentTravel(characterId: string): Promise completeTravelIfDue(characterId: string): Promise ``` --- ## `HuntingService` ```ts startHunt(characterId: string): Promise attackEncounter(characterId: string, encounterId: string): Promise ``` --- ## `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 grantLoot(characterId: string, result: LootResult): Promise ``` --- # 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
``` - [ ] **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()`, `post()` - 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(path: string) { return this.http.get(`/api${path}`); } post(path: string, body: unknown) { return this.http.post(`/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 |