1738 lines
27 KiB
Markdown
1738 lines
27 KiB
Markdown
# Ashen Realms – Playable Slice 0.13
|
||
|
||
## Realtime Game Events Foundation
|
||
|
||
**Status:** Implementation Specification
|
||
**Slice:** 0.13
|
||
**Scope:** Zentraler WebSocket-Kanal für serverinitiierte Game Events
|
||
**Primary Use Case:** Combat Events
|
||
**Architecture:** REST Commands + WebSocket Events + server authoritative state
|
||
|
||
---
|
||
|
||
# 1. Ziel
|
||
|
||
Slice 0.13 führt eine zentrale Realtime-Kommunikationsschicht für Ashen Realms ein.
|
||
|
||
Bis einschließlich Slice 0.12 funktioniert das Spiel primär request-basiert:
|
||
|
||
```text
|
||
Client
|
||
→ REST Request
|
||
→ Server verarbeitet
|
||
→ Response
|
||
```
|
||
|
||
Das reicht für:
|
||
|
||
- Reisen
|
||
- Jagd
|
||
- Einzelspieler-Kämpfe
|
||
- Inventar
|
||
- Equipment
|
||
- Quests
|
||
- Händler
|
||
- Charakterverwaltung
|
||
|
||
Langfristig entstehen jedoch Situationen, bei denen sich der Spielzustand verändern kann, **ohne dass der aktuelle Client selbst einen Request ausgelöst hat**.
|
||
|
||
Beispiele:
|
||
|
||
- ein anderer Spieler führt im gemeinsamen Kampf eine Aktion aus
|
||
- ein NPC führt verzögert eine Kampfaktion aus
|
||
- ein anderer Spieler ist nun am Zug
|
||
- ein Gruppenmitglied tritt einem Kampf bei
|
||
- ein Begleiter oder Pet führt eine Aktion aus
|
||
- der Kampf endet durch die Aktion eines anderen Beteiligten
|
||
- später verändert ein externer Effekt Charakterwerte
|
||
- Gruppen-, Quest- oder Weltzustände ändern sich
|
||
|
||
Dafür wird ein zentraler WebSocket-Kanal eingeführt.
|
||
|
||
Grundsatz:
|
||
|
||
> **REST beschreibt, was der Spieler tun möchte. WebSocket Events beschreiben, was im Spiel passiert ist.**
|
||
|
||
---
|
||
|
||
# 2. Nicht-Ziel dieses Slices
|
||
|
||
Slice 0.13 führt **kein vollständiges Multiplayer-Kampfsystem** ein.
|
||
|
||
Noch nicht Teil dieses Slices:
|
||
|
||
- mehrere menschliche Spieler im selben Kampf
|
||
- mehrere Gegner in einem Kampf
|
||
- Pets
|
||
- NPC-Begleiter
|
||
- Party-System
|
||
- PvP
|
||
- Chat
|
||
- Location Presence
|
||
- Welt-Events
|
||
- globale Tick-Simulation
|
||
- WebSocket-basierte HP-Regeneration
|
||
- vollständige Umstellung bestehender REST-Endpunkte auf WebSockets
|
||
|
||
Der bestehende 1-vs-1-Combat bleibt funktional bestehen.
|
||
|
||
0.13 schafft ausschließlich die technische Grundlage, auf der spätere Mehrteilnehmer-Systeme aufbauen können.
|
||
|
||
---
|
||
|
||
# 3. Architekturprinzip
|
||
|
||
Ashen Realms verwendet ab Slice 0.13 drei unterschiedliche Mechanismen für unterschiedliche Arten von Zustand.
|
||
|
||
## 3.1 REST – Commands und Reads
|
||
|
||
REST bleibt verantwortlich für explizite Spieleraktionen.
|
||
|
||
Beispiele:
|
||
|
||
```text
|
||
POST /api/travel
|
||
POST /api/hunts
|
||
POST /api/combats/:id/actions
|
||
POST /api/equipment
|
||
|
||
GET /api/characters/me
|
||
GET /api/inventory
|
||
GET /api/combats/:id
|
||
```
|
||
|
||
Der Client sendet weiterhin keine berechneten Spielwerte an den Server.
|
||
|
||
Beispiel:
|
||
|
||
```json
|
||
{
|
||
"action": "ATTACK"
|
||
}
|
||
```
|
||
|
||
Nicht:
|
||
|
||
```json
|
||
{
|
||
"action": "ATTACK",
|
||
"damage": 27
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3.2 WebSocket – diskrete serverseitige Ereignisse
|
||
|
||
WebSocket wird verwendet, wenn der Server einen Client über ein Ereignis informieren muss.
|
||
|
||
Beispiele:
|
||
|
||
```text
|
||
combat.action.resolved
|
||
combat.turn.changed
|
||
combat.finished
|
||
|
||
später:
|
||
character.vitals.changed
|
||
party.member.joined
|
||
quest.updated
|
||
location.player.joined
|
||
world.event.started
|
||
```
|
||
|
||
---
|
||
|
||
## 3.3 Zeitstempel – kontinuierlich ableitbarer Zustand
|
||
|
||
Zeitabhängige Werte werden weiterhin nicht permanent übertragen.
|
||
|
||
Beispiele:
|
||
|
||
- HP-Regeneration
|
||
- Reisezeit
|
||
- Cooldowns
|
||
- Buff-Dauer
|
||
- Respawn-Zeit
|
||
|
||
Diese Systeme verwenden weiterhin:
|
||
|
||
```text
|
||
Basiszustand
|
||
+
|
||
Zeitstempel
|
||
+
|
||
Regel
|
||
```
|
||
|
||
Der Client darf daraus die Darstellung interpolieren.
|
||
|
||
Der Server bleibt für den tatsächlichen Zustand autoritativ.
|
||
|
||
---
|
||
|
||
# 4. Zentrale Regel
|
||
|
||
Es gibt **eine WebSocket-Verbindung pro eingeloggtem Client**.
|
||
|
||
Nicht:
|
||
|
||
```text
|
||
/combat websocket
|
||
/character websocket
|
||
/party websocket
|
||
```
|
||
|
||
Sondern:
|
||
|
||
```text
|
||
/events
|
||
```
|
||
|
||
Darüber werden verschiedene fachliche Event-Typen transportiert.
|
||
|
||
---
|
||
|
||
# 5. Zielarchitektur
|
||
|
||
```text
|
||
Angular Client
|
||
|
|
||
| REST Commands
|
||
v
|
||
NestJS Controllers
|
||
|
|
||
v
|
||
Domain Services
|
||
|
|
||
+--------------------------+
|
||
| |
|
||
v v
|
||
PostgreSQL GameEventPublisher
|
||
|
|
||
v
|
||
EventsGateway
|
||
|
|
||
| WebSocket
|
||
v
|
||
Angular Client
|
||
```
|
||
|
||
Beispiel Combat:
|
||
|
||
```text
|
||
Spieler klickt Angriff
|
||
|
|
||
v
|
||
POST /api/combats/:id/actions
|
||
|
|
||
v
|
||
CombatService
|
||
|
|
||
├── CombatEngine
|
||
├── State speichern
|
||
├── CombatEvents speichern
|
||
└── Transaction Commit
|
||
|
|
||
v
|
||
CombatEventPublisher
|
||
|
|
||
v
|
||
EventsGateway
|
||
|
|
||
v
|
||
combat.action.resolved
|
||
```
|
||
|
||
---
|
||
|
||
# 6. Wichtigste Architekturregel
|
||
|
||
Ein WebSocket-Event darf erst veröffentlicht werden, **nachdem der zugehörige persistente Zustand erfolgreich gespeichert wurde**.
|
||
|
||
Nicht:
|
||
|
||
```text
|
||
Event senden
|
||
→ danach DB speichern
|
||
```
|
||
|
||
Sondern:
|
||
|
||
```text
|
||
DB Transaction
|
||
→ Commit
|
||
→ Event veröffentlichen
|
||
```
|
||
|
||
Dadurch gilt:
|
||
|
||
> Wenn ein Client ein Event erhält, existiert der gemeldete Zustand bereits autoritativ auf dem Server.
|
||
|
||
---
|
||
|
||
# 7. WebSocket-Verbindung
|
||
|
||
Endpoint:
|
||
|
||
```text
|
||
/events
|
||
```
|
||
|
||
Die konkrete technische Umsetzung darf beispielsweise mit NestJS WebSocket Gateway und Socket.IO oder nativen WebSockets erfolgen.
|
||
|
||
Für V1 sollte die einfachere, robuste NestJS-Integration bevorzugt werden.
|
||
|
||
---
|
||
|
||
# 8. Authentifizierung
|
||
|
||
Die WebSocket-Verbindung muss authentifiziert sein.
|
||
|
||
Der Server muss aus der Verbindung mindestens bestimmen können:
|
||
|
||
```text
|
||
userId
|
||
characterId
|
||
```
|
||
|
||
Ein Client darf niemals selbst angeben:
|
||
|
||
```text
|
||
Ich bin Character X.
|
||
```
|
||
|
||
ohne dass der Server diese Zuordnung aus der Authentifizierung validiert.
|
||
|
||
---
|
||
|
||
# 9. Connection Lifecycle
|
||
|
||
Der Client baut nach erfolgreicher Authentifizierung eine Verbindung auf.
|
||
|
||
```text
|
||
Login
|
||
→ Character laden
|
||
→ WebSocket /events verbinden
|
||
```
|
||
|
||
Bei Logout:
|
||
|
||
```text
|
||
WebSocket trennen
|
||
```
|
||
|
||
Bei Verbindungsverlust:
|
||
|
||
```text
|
||
Reconnect versuchen
|
||
```
|
||
|
||
Der WebSocket darf niemals Voraussetzung dafür sein, dass der persistente Spielzustand korrekt bleibt.
|
||
|
||
---
|
||
|
||
# 10. Reconnect-Prinzip
|
||
|
||
Ein WebSocket ist ein Benachrichtigungskanal, nicht die Source of Truth.
|
||
|
||
Wenn der Client die Verbindung verliert:
|
||
|
||
```text
|
||
WebSocket disconnected
|
||
```
|
||
|
||
läuft das Spiel serverseitig weiter.
|
||
|
||
Nach Wiederherstellung:
|
||
|
||
```text
|
||
WebSocket reconnect
|
||
→ aktuellen Zustand per REST synchronisieren
|
||
```
|
||
|
||
Beispielsweise:
|
||
|
||
```text
|
||
GET /api/characters/me
|
||
GET /api/combats/:id
|
||
```
|
||
|
||
Der Client darf niemals annehmen, dass er während des Disconnects keine Events verpasst hat.
|
||
|
||
---
|
||
|
||
# 11. Event Envelope
|
||
|
||
Alle Events verwenden eine gemeinsame Grundstruktur.
|
||
|
||
Beispiel:
|
||
|
||
```ts
|
||
export interface GameEvent<TPayload = unknown> {
|
||
id: string;
|
||
sequence?: number;
|
||
type: GameEventType;
|
||
occurredAt: string;
|
||
payload: TPayload;
|
||
}
|
||
```
|
||
|
||
Beispiel:
|
||
|
||
```json
|
||
{
|
||
"id": "event-uuid",
|
||
"sequence": 42,
|
||
"type": "combat.action.resolved",
|
||
"occurredAt": "2026-08-21T15:00:00.000Z",
|
||
"payload": {
|
||
"combatId": "combat-uuid"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
# 12. Event Naming
|
||
|
||
Event-Namen verwenden:
|
||
|
||
```text
|
||
domain.entity-or-action.event
|
||
```
|
||
|
||
Für Slice 0.13:
|
||
|
||
```text
|
||
combat.action.resolved
|
||
combat.turn.changed
|
||
combat.finished
|
||
combat.state.changed
|
||
```
|
||
|
||
Später möglich:
|
||
|
||
```text
|
||
character.vitals.changed
|
||
character.stats.changed
|
||
|
||
party.member.joined
|
||
party.member.left
|
||
|
||
quest.updated
|
||
|
||
location.player.joined
|
||
location.player.left
|
||
```
|
||
|
||
---
|
||
|
||
# 13. Keine technischen Event-Namen
|
||
|
||
Nicht:
|
||
|
||
```text
|
||
UPDATE_COMBAT
|
||
REFRESH_UI
|
||
DB_CHANGED
|
||
SYNC_PLAYER
|
||
```
|
||
|
||
Events beschreiben fachlich, was passiert ist.
|
||
|
||
Gut:
|
||
|
||
```text
|
||
combat.turn.changed
|
||
```
|
||
|
||
Schlecht:
|
||
|
||
```text
|
||
refreshCombatScreen
|
||
```
|
||
|
||
---
|
||
|
||
# 14. Rooms / Channels
|
||
|
||
Die zentrale WebSocket-Verbindung verwendet serverseitige Rooms.
|
||
|
||
Vorgesehene Room-Typen:
|
||
|
||
```text
|
||
user:<userId>
|
||
character:<characterId>
|
||
combat:<combatId>
|
||
```
|
||
|
||
Später:
|
||
|
||
```text
|
||
party:<partyId>
|
||
location:<locationId>
|
||
guild:<guildId>
|
||
```
|
||
|
||
---
|
||
|
||
# 15. User Room
|
||
|
||
Jeder eingeloggte Client wird automatisch Mitglied von:
|
||
|
||
```text
|
||
user:<userId>
|
||
```
|
||
|
||
Dieser Room ist geeignet für:
|
||
|
||
- globale persönliche Benachrichtigungen
|
||
- Account-bezogene Ereignisse
|
||
- spätere systemweite Hinweise
|
||
|
||
---
|
||
|
||
# 16. Character Room
|
||
|
||
Der Client tritt zusätzlich bei:
|
||
|
||
```text
|
||
character:<characterId>
|
||
```
|
||
|
||
Geeignet für:
|
||
|
||
- Charakteränderungen
|
||
- Statusänderungen
|
||
- später Buffs
|
||
- später Ruf
|
||
- später externe Heilung
|
||
|
||
---
|
||
|
||
# 17. Combat Room
|
||
|
||
Wenn ein Charakter einen aktiven Kampf besitzt:
|
||
|
||
```text
|
||
combat:<combatId>
|
||
```
|
||
|
||
Der Server fügt den Client diesem Room hinzu.
|
||
|
||
Beim Verlassen oder Ende des Kampfes wird die Subscription entfernt.
|
||
|
||
Später können mehrere Spieler gleichzeitig Mitglied desselben Combat Rooms sein.
|
||
|
||
---
|
||
|
||
# 18. Kein frei wählbares Room Joining
|
||
|
||
Der Client darf nicht einfach senden:
|
||
|
||
```text
|
||
join combat:xyz
|
||
```
|
||
|
||
und dadurch beliebige Kämpfe beobachten.
|
||
|
||
Der Server validiert immer:
|
||
|
||
```text
|
||
Ist dieser Charakter Teilnehmer dieses Kampfes?
|
||
```
|
||
|
||
Erst danach darf der Socket dem Room beitreten.
|
||
|
||
---
|
||
|
||
# 19. Game Events Backend-Struktur
|
||
|
||
Vorgesehene Struktur:
|
||
|
||
```text
|
||
apps/api/src/events/
|
||
├── events.module.ts
|
||
├── events.gateway.ts
|
||
├── game-event.types.ts
|
||
├── game-event-publisher.service.ts
|
||
├── event-room.service.ts
|
||
└── events.gateway.spec.ts
|
||
```
|
||
|
||
Optional fachlich getrennt:
|
||
|
||
```text
|
||
apps/api/src/combat/
|
||
└── combat-event.publisher.ts
|
||
```
|
||
|
||
---
|
||
|
||
# 20. `EventsGateway`
|
||
|
||
Verantwortlich für:
|
||
|
||
- WebSocket-Verbindungen
|
||
- Authentifizierung
|
||
- Disconnect
|
||
- Reconnect-Unterstützung
|
||
- Room-Mitgliedschaften
|
||
- Senden von Events
|
||
|
||
Nicht verantwortlich für:
|
||
|
||
- Combat-Regeln
|
||
- Schadensberechnung
|
||
- Charakterwerte
|
||
- Loot
|
||
- Questlogik
|
||
|
||
---
|
||
|
||
# 21. `GameEventPublisher`
|
||
|
||
Der Domain-Code soll nicht direkt mit dem WebSocket-Gateway kommunizieren.
|
||
|
||
Nicht:
|
||
|
||
```ts
|
||
this.eventsGateway.server
|
||
.to(room)
|
||
.emit(...);
|
||
```
|
||
|
||
innerhalb des CombatService.
|
||
|
||
Stattdessen:
|
||
|
||
```ts
|
||
this.gameEventPublisher.publishToCombat(
|
||
combatId,
|
||
event,
|
||
);
|
||
```
|
||
|
||
Dadurch bleibt der Transport austauschbar und die Domain-Logik kennt keine Socket.IO-Details.
|
||
|
||
---
|
||
|
||
# 22. Beispiel Publisher Interface
|
||
|
||
Konzeptionell:
|
||
|
||
```ts
|
||
export interface GameEventPublisher {
|
||
publishToUser(
|
||
userId: string,
|
||
event: GameEvent,
|
||
): void;
|
||
|
||
publishToCharacter(
|
||
characterId: string,
|
||
event: GameEvent,
|
||
): void;
|
||
|
||
publishToCombat(
|
||
combatId: string,
|
||
event: GameEvent,
|
||
): void;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
# 23. Combat als erster Use Case
|
||
|
||
Slice 0.13 verwendet Combat als ersten realen Event-Produzenten.
|
||
|
||
Der bestehende Combat-Flow bleibt:
|
||
|
||
```text
|
||
Client
|
||
→ POST Combat Action
|
||
→ Server berechnet Resultat
|
||
→ Server persistiert Resultat
|
||
→ Response
|
||
```
|
||
|
||
Zusätzlich:
|
||
|
||
```text
|
||
→ Server veröffentlicht Combat Events
|
||
→ WebSocket liefert sie an Combat Room
|
||
```
|
||
|
||
---
|
||
|
||
# 24. `combat.action.resolved`
|
||
|
||
Wird veröffentlicht, wenn eine Combat Action serverseitig vollständig verarbeitet wurde.
|
||
|
||
Beispiel:
|
||
|
||
```json
|
||
{
|
||
"type": "combat.action.resolved",
|
||
"payload": {
|
||
"combatId": "abc",
|
||
"round": 4,
|
||
"events": [
|
||
{
|
||
"sequence": 21,
|
||
"type": "PLAYER_ATTACK",
|
||
"actorId": "player",
|
||
"targetId": "monster",
|
||
"amount": 20
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
Der Client verwendet dieses Event für:
|
||
|
||
- Animationen
|
||
- Damage Numbers
|
||
- Combat Log
|
||
- UI-State
|
||
|
||
---
|
||
|
||
# 25. `combat.turn.changed`
|
||
|
||
Wird veröffentlicht, sobald sich der aktive Actor ändert.
|
||
|
||
Payload:
|
||
|
||
```ts
|
||
interface CombatTurnChangedPayload {
|
||
combatId: string;
|
||
actorId: string;
|
||
startedAt: string;
|
||
expiresAt?: string;
|
||
}
|
||
```
|
||
|
||
Für den aktuellen 1-vs-1-Combat kann dieses Event zunächst nur begrenzt genutzt werden.
|
||
|
||
Die Struktur wird jedoch bereits für spätere Mehrteilnehmer-Kämpfe vorbereitet.
|
||
|
||
---
|
||
|
||
# 26. `combat.finished`
|
||
|
||
Wird bei Sieg, Niederlage oder später Flucht veröffentlicht.
|
||
|
||
Beispiel:
|
||
|
||
```json
|
||
{
|
||
"type": "combat.finished",
|
||
"payload": {
|
||
"combatId": "abc",
|
||
"result": "WON"
|
||
}
|
||
}
|
||
```
|
||
|
||
Später können weitere Informationen ergänzt werden:
|
||
|
||
```text
|
||
loot
|
||
reputation
|
||
questProgress
|
||
```
|
||
|
||
Diese Daten müssen jedoch weiterhin serverseitig autoritativ sein.
|
||
|
||
---
|
||
|
||
# 27. `combat.state.changed`
|
||
|
||
Optionales allgemeines Sync-Event.
|
||
|
||
Es darf verwendet werden, wenn der Client wissen soll:
|
||
|
||
> Der Combat-State hat sich geändert; lade bei Bedarf den aktuellen Zustand neu.
|
||
|
||
Beispiel:
|
||
|
||
```json
|
||
{
|
||
"type": "combat.state.changed",
|
||
"payload": {
|
||
"combatId": "abc",
|
||
"revision": 17
|
||
}
|
||
}
|
||
```
|
||
|
||
Es soll nicht jede spezialisierte Combat-Nachricht ersetzen.
|
||
|
||
---
|
||
|
||
# 28. HTTP Response und WebSocket Event
|
||
|
||
Für die Aktion des lokalen Spielers darf die HTTP Response weiterhin das Resultat enthalten.
|
||
|
||
Das erzeugt bewusst mögliche doppelte Information:
|
||
|
||
```text
|
||
HTTP Response
|
||
+
|
||
WebSocket Broadcast
|
||
```
|
||
|
||
Der Client muss damit umgehen können.
|
||
|
||
Deshalb brauchen Events stabile IDs oder Sequenzen.
|
||
|
||
---
|
||
|
||
# 29. Event-Deduplizierung
|
||
|
||
Der Client darf dasselbe Combat Event nicht zweimal animieren.
|
||
|
||
CombatEvents besitzen bereits beziehungsweise erhalten eine stabile Reihenfolge.
|
||
|
||
Beispiel:
|
||
|
||
```text
|
||
combatId
|
||
sequence
|
||
```
|
||
|
||
Client speichert:
|
||
|
||
```text
|
||
lastProcessedSequence
|
||
```
|
||
|
||
Wenn:
|
||
|
||
```text
|
||
sequence <= lastProcessedSequence
|
||
```
|
||
|
||
wird das Event nicht erneut abgespielt.
|
||
|
||
---
|
||
|
||
# 30. Event-Reihenfolge
|
||
|
||
Innerhalb eines Combat Rooms muss die Reihenfolge serverseitig eindeutig sein.
|
||
|
||
Beispiel:
|
||
|
||
```text
|
||
41 PLAYER_ATTACK
|
||
42 DAMAGE_APPLIED
|
||
43 MONSTER_ATTACK
|
||
44 DAMAGE_APPLIED
|
||
45 TURN_CHANGED
|
||
```
|
||
|
||
Der Client spielt diese Reihenfolge ab.
|
||
|
||
Nicht die Empfangszeit entscheidet über die fachliche Reihenfolge.
|
||
|
||
---
|
||
|
||
# 31. Persistierte CombatEvents bleiben Source of Truth
|
||
|
||
WebSocket Events ersetzen nicht die persistierten `CombatEvent`-Datensätze.
|
||
|
||
Die Datenbank bleibt maßgeblich.
|
||
|
||
Dadurch werden später möglich:
|
||
|
||
- Reconnect
|
||
- Combat Log
|
||
- Debugging
|
||
- Replay
|
||
- nachträgliches Nachladen verpasster Events
|
||
|
||
---
|
||
|
||
# 32. Resync nach Event-Lücke
|
||
|
||
Wenn ein Client erkennt:
|
||
|
||
```text
|
||
letzte bekannte Sequence = 40
|
||
neues Event = 44
|
||
```
|
||
|
||
existiert eine Lücke.
|
||
|
||
Dann darf der Client nicht raten.
|
||
|
||
Er synchronisiert:
|
||
|
||
```text
|
||
GET /api/combats/:id
|
||
```
|
||
|
||
oder später:
|
||
|
||
```text
|
||
GET /api/combats/:id/events?after=40
|
||
```
|
||
|
||
Der konkrete Events-After-Endpunkt muss in 0.13 noch nicht zwingend implementiert werden.
|
||
|
||
---
|
||
|
||
# 33. Frontend-Struktur
|
||
|
||
Vorgeschlagene Struktur:
|
||
|
||
```text
|
||
apps/web/src/app/core/events/
|
||
├── game-events.service.ts
|
||
├── game-events.models.ts
|
||
├── game-events.store.ts
|
||
└── game-events.service.spec.ts
|
||
```
|
||
|
||
---
|
||
|
||
# 34. `GameEventsService`
|
||
|
||
Verantwortlich für:
|
||
|
||
- Verbindung herstellen
|
||
- Reconnect
|
||
- eingehende Events empfangen
|
||
- Event-Typen verteilen
|
||
- Lifecycle verwalten
|
||
|
||
Nicht verantwortlich für:
|
||
|
||
- Combat-State
|
||
- Character-State
|
||
- Quest-State
|
||
|
||
---
|
||
|
||
# 35. Event-Verteilung im Frontend
|
||
|
||
Der zentrale Event-Service verteilt Events fachlich.
|
||
|
||
Konzeptionell:
|
||
|
||
```ts
|
||
combatEvents$
|
||
characterEvents$
|
||
partyEvents$
|
||
```
|
||
|
||
oder mit Angular Signals:
|
||
|
||
```ts
|
||
combatEvent
|
||
characterEvent
|
||
```
|
||
|
||
Feature Stores abonnieren nur relevante Eventtypen.
|
||
|
||
---
|
||
|
||
# 36. CombatStore Integration
|
||
|
||
Der CombatStore reagiert auf:
|
||
|
||
```text
|
||
combat.action.resolved
|
||
combat.turn.changed
|
||
combat.finished
|
||
```
|
||
|
||
Der Store darf daraus:
|
||
|
||
- Events in Animationsqueue einreihen
|
||
- Aktionen aktivieren/deaktivieren
|
||
- Combat-State aktualisieren
|
||
- bei Unsicherheit REST-Resync auslösen
|
||
|
||
---
|
||
|
||
# 37. WebSocket Event ≠ Animation
|
||
|
||
Das Backend definiert fachliche Events.
|
||
|
||
Nicht:
|
||
|
||
```text
|
||
PLAY_SWORD_ANIMATION
|
||
WAIT_800_MS
|
||
SHOW_RED_NUMBER
|
||
```
|
||
|
||
Sondern:
|
||
|
||
```text
|
||
ATTACK
|
||
DAMAGE
|
||
HEAL
|
||
STATUS_APPLIED
|
||
TURN_CHANGED
|
||
```
|
||
|
||
Der Client entscheidet, wie diese visuell dargestellt werden.
|
||
|
||
Damit bleibt die Trennung erhalten:
|
||
|
||
> Server entscheidet, was passiert. Client entscheidet, wie es aussieht.
|
||
|
||
---
|
||
|
||
# 38. Keine WebSocket-HP-Ticks
|
||
|
||
Das bestehende HP-Regenerationsmodell bleibt unverändert.
|
||
|
||
Weiterhin:
|
||
|
||
```text
|
||
currentHp
|
||
hpRegenSince
|
||
hpRegenPerSecond
|
||
```
|
||
|
||
Der Client berechnet die sichtbare Regeneration lokal.
|
||
|
||
Nicht implementieren:
|
||
|
||
```text
|
||
character.hp.changed 71
|
||
character.hp.changed 72
|
||
character.hp.changed 73
|
||
```
|
||
|
||
pro Sekunde.
|
||
|
||
---
|
||
|
||
# 39. Spätere Character Events
|
||
|
||
Die Architektur soll jedoch ermöglichen:
|
||
|
||
```text
|
||
character.vitals.changed
|
||
```
|
||
|
||
wenn eine **diskrete externe Veränderung** eintritt.
|
||
|
||
Beispiele:
|
||
|
||
- ein anderer Spieler heilt
|
||
- ein Gruppenbuff verändert Max HP
|
||
- ein Debuff verursacht Schaden
|
||
- eine externe Spielaktion verändert HP
|
||
|
||
Beispiel:
|
||
|
||
```json
|
||
{
|
||
"type": "character.vitals.changed",
|
||
"payload": {
|
||
"characterId": "abc",
|
||
"currentHp": 84,
|
||
"hpRegenSince": "2026-08-21T15:10:20.000Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
Danach läuft die bestehende lokale Regeneration weiter.
|
||
|
||
Dieses Event muss in Slice 0.13 noch nicht produktiv verwendet werden.
|
||
|
||
---
|
||
|
||
# 40. Keine globale Server-Tick-Schleife
|
||
|
||
Slice 0.13 führt ausdrücklich keinen zentralen:
|
||
|
||
```text
|
||
10 Hz
|
||
20 Hz
|
||
60 Hz
|
||
```
|
||
|
||
Game Loop ein.
|
||
|
||
Ashen Realms bleibt:
|
||
|
||
```text
|
||
event-driven
|
||
+
|
||
turn-based
|
||
+
|
||
timestamp-based
|
||
```
|
||
|
||
Server-Ticks werden erst eingeführt, wenn ein konkretes zukünftiges Feature sie tatsächlich benötigt.
|
||
|
||
---
|
||
|
||
# 41. Vorbereitung für verzögerte NPC-Aktionen
|
||
|
||
Die Event-Infrastruktur muss ermöglichen, dass später folgendes passiert:
|
||
|
||
```text
|
||
Player Action
|
||
→ Commit
|
||
→ Combat Event
|
||
→ NPC Turn geplant
|
||
→ HTTP Request ist längst beendet
|
||
→ NPC Action wird ausgeführt
|
||
→ Commit
|
||
→ WebSocket Combat Event
|
||
```
|
||
|
||
Der Client muss dadurch nicht mehr selbst warten und anschließend eine NPC-Aktion simulieren.
|
||
|
||
Die tatsächliche serverseitige NPC-Turn-Scheduling-Logik ist nicht Teil von 0.13.
|
||
|
||
---
|
||
|
||
# 42. Keine `sleep()`-Requests
|
||
|
||
Nicht implementieren:
|
||
|
||
```ts
|
||
await sleep(1000);
|
||
performNpcAttack();
|
||
return response;
|
||
```
|
||
|
||
HTTP Requests dürfen nicht künstlich offen gehalten werden, um Kampftiming zu simulieren.
|
||
|
||
Spätere AI-Turns müssen unabhängig vom ursprünglichen Request verarbeitet werden können.
|
||
|
||
---
|
||
|
||
# 43. Vorbereitung auf Combat Participants
|
||
|
||
0.13 muss noch keine vollständige `CombatParticipant`-Migration durchführen, darf die Event Contracts aber nicht hart auf:
|
||
|
||
```text
|
||
PLAYER
|
||
MONSTER
|
||
```
|
||
|
||
begrenzen.
|
||
|
||
Events sollten allgemein mit:
|
||
|
||
```text
|
||
actorId
|
||
targetId
|
||
```
|
||
|
||
arbeiten.
|
||
|
||
Nicht:
|
||
|
||
```text
|
||
playerDamage
|
||
monsterDamage
|
||
```
|
||
|
||
Dadurch können spätere Teilnehmer sein:
|
||
|
||
```text
|
||
PLAYER
|
||
NPC
|
||
PET
|
||
COMPANION
|
||
BOSS
|
||
```
|
||
|
||
---
|
||
|
||
# 44. Beispiel zukünftiger Kampf
|
||
|
||
Die 0.13-Architektur soll später ohne neuen Transportmechanismus ermöglichen:
|
||
|
||
```text
|
||
TEAM A
|
||
- Player A
|
||
- Player B
|
||
- Pet
|
||
|
||
TEAM B
|
||
- Bandit
|
||
- Wolf
|
||
- Bandit
|
||
```
|
||
|
||
Ablauf:
|
||
|
||
```text
|
||
Player A handelt
|
||
→ Event
|
||
|
||
Wolf handelt
|
||
→ Event
|
||
|
||
Player B ist dran
|
||
→ Event
|
||
|
||
Player B Client erhält:
|
||
combat.turn.changed
|
||
```
|
||
|
||
---
|
||
|
||
# 45. Fehlerbehandlung
|
||
|
||
WebSocket-Fehler dürfen keinen falschen Spielzustand erzeugen.
|
||
|
||
Bei:
|
||
|
||
```text
|
||
Disconnect
|
||
invalid event
|
||
sequence gap
|
||
parse error
|
||
```
|
||
|
||
gilt:
|
||
|
||
```text
|
||
UI als unsynchronisiert markieren
|
||
→ REST Resync
|
||
```
|
||
|
||
Nicht:
|
||
|
||
```text
|
||
lokal weitersimulieren
|
||
```
|
||
|
||
---
|
||
|
||
# 46. Client-Verbindungsstatus
|
||
|
||
Optional sichtbar oder intern:
|
||
|
||
```ts
|
||
type EventConnectionState =
|
||
| 'DISCONNECTED'
|
||
| 'CONNECTING'
|
||
| 'CONNECTED'
|
||
| 'RECONNECTING';
|
||
```
|
||
|
||
Bei kurzfristigem Disconnect soll nicht sofort eine störende Fehlermeldung erscheinen.
|
||
|
||
Erst wenn Realtime tatsächlich für eine aktive Funktion nötig ist, muss der Zustand prominent dargestellt werden.
|
||
|
||
---
|
||
|
||
# 47. Security
|
||
|
||
Der Server muss prüfen:
|
||
|
||
- gültige Authentifizierung
|
||
- Socket gehört zum User
|
||
- Character gehört zum User
|
||
- Combat gehört zum Character
|
||
- Room-Mitgliedschaft ist erlaubt
|
||
|
||
Der Client darf:
|
||
|
||
- keine fremden Combat Rooms abonnieren
|
||
- keine fremden Character Rooms abonnieren
|
||
- keine Server Events fälschen
|
||
|
||
---
|
||
|
||
# 48. Rate Limits
|
||
|
||
Für 0.13 ist kein komplexes WebSocket Rate Limiting notwendig.
|
||
|
||
Dennoch gilt:
|
||
|
||
Client-Nachrichten über den Socket sollten möglichst minimal bleiben.
|
||
|
||
Da Commands weiterhin über REST laufen, ist der WebSocket in 0.13 fast ausschließlich:
|
||
|
||
```text
|
||
Server → Client
|
||
```
|
||
|
||
---
|
||
|
||
# 49. Warum trotzdem WebSocket statt SSE
|
||
|
||
Obwohl Slice 0.13 hauptsächlich Server-Push benötigt, wird direkt WebSocket verwendet.
|
||
|
||
Grund:
|
||
|
||
Spätere Systeme benötigen voraussichtlich bidirektionale Echtzeitkommunikation:
|
||
|
||
- Multiplayer Combat
|
||
- Parties
|
||
- Chat
|
||
- Presence
|
||
- Social Systems
|
||
|
||
Dadurch wird vermieden:
|
||
|
||
```text
|
||
REST
|
||
+ SSE
|
||
+ später zusätzlich WebSocket
|
||
```
|
||
|
||
Ziel bleibt:
|
||
|
||
```text
|
||
REST
|
||
+
|
||
WebSocket
|
||
```
|
||
|
||
---
|
||
|
||
# 50. Shared Contracts
|
||
|
||
Cross-Boundary Contracts gehören nach:
|
||
|
||
```text
|
||
packages/shared
|
||
```
|
||
|
||
Geeignet:
|
||
|
||
```ts
|
||
GameEvent
|
||
GameEventType
|
||
CombatActionResolvedPayload
|
||
CombatTurnChangedPayload
|
||
CombatFinishedPayload
|
||
```
|
||
|
||
Nicht dort ablegen:
|
||
|
||
- NestJS Gateway
|
||
- Socket.IO Server
|
||
- Angular Services
|
||
- TypeORM Entities
|
||
|
||
---
|
||
|
||
# 51. Vorgeschlagene Dateien Backend
|
||
|
||
```text
|
||
packages/shared/src/events/
|
||
├── game-event.ts
|
||
├── game-event-type.ts
|
||
└── combat-events.ts
|
||
|
||
apps/api/src/events/
|
||
├── events.module.ts
|
||
├── events.gateway.ts
|
||
├── event-room.service.ts
|
||
├── game-event-publisher.service.ts
|
||
├── events.gateway.spec.ts
|
||
└── game-event-publisher.service.spec.ts
|
||
```
|
||
|
||
Zusätzlich Änderungen in:
|
||
|
||
```text
|
||
apps/api/src/combat/
|
||
```
|
||
|
||
---
|
||
|
||
# 52. Vorgeschlagene Dateien Frontend
|
||
|
||
```text
|
||
apps/web/src/app/core/events/
|
||
├── game-events.service.ts
|
||
├── game-events.service.spec.ts
|
||
└── game-events.models.ts
|
||
```
|
||
|
||
Änderungen:
|
||
|
||
```text
|
||
CombatStore
|
||
App initialization
|
||
Authentication lifecycle
|
||
```
|
||
|
||
---
|
||
|
||
# 53. Implementierungsreihenfolge
|
||
|
||
## Task 1 – Shared Event Contracts
|
||
|
||
Implementieren:
|
||
|
||
```text
|
||
GameEvent
|
||
GameEventType
|
||
Combat Event Payloads
|
||
```
|
||
|
||
Tests:
|
||
|
||
- TypeScript compile
|
||
- Contract consistency
|
||
|
||
---
|
||
|
||
## Task 2 – WebSocket Gateway
|
||
|
||
Implementieren:
|
||
|
||
```text
|
||
/events
|
||
Authentication
|
||
Connect
|
||
Disconnect
|
||
```
|
||
|
||
Tests:
|
||
|
||
- unauthenticated rejected
|
||
- authenticated accepted
|
||
|
||
---
|
||
|
||
## Task 3 – Room Infrastructure
|
||
|
||
Implementieren:
|
||
|
||
```text
|
||
user room
|
||
character room
|
||
combat room
|
||
```
|
||
|
||
Tests:
|
||
|
||
- nur berechtigte Membership
|
||
- kein fremder Combat Room
|
||
|
||
---
|
||
|
||
## Task 4 – GameEventPublisher
|
||
|
||
Domain-unabhängige Publish API implementieren.
|
||
|
||
Tests:
|
||
|
||
```text
|
||
publishToUser
|
||
publishToCharacter
|
||
publishToCombat
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5 – Combat Integration
|
||
|
||
Nach erfolgreichem Combat Commit:
|
||
|
||
```text
|
||
combat.action.resolved
|
||
combat.turn.changed
|
||
combat.finished
|
||
```
|
||
|
||
veröffentlichen.
|
||
|
||
---
|
||
|
||
## Task 6 – Frontend GameEventsService
|
||
|
||
Implementieren:
|
||
|
||
```text
|
||
connect
|
||
disconnect
|
||
reconnect
|
||
event parsing
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7 – CombatStore Integration
|
||
|
||
CombatEvents empfangen und in bestehende Combat-Darstellung integrieren.
|
||
|
||
Bestehende REST-Funktionalität muss weiterhin funktionieren.
|
||
|
||
---
|
||
|
||
## Task 8 – Reconnect & Resync
|
||
|
||
Bei Reconnect:
|
||
|
||
```text
|
||
Character neu laden
|
||
aktiven Combat prüfen
|
||
Combat State neu laden
|
||
Rooms erneut herstellen
|
||
```
|
||
|
||
---
|
||
|
||
# 54. Testing Backend
|
||
|
||
Mindestens folgende Tests:
|
||
|
||
### Gateway
|
||
|
||
- nicht authentifizierte Verbindung wird abgelehnt
|
||
- authentifizierte Verbindung wird akzeptiert
|
||
- User Room wird automatisch verbunden
|
||
- Character Room wird korrekt verbunden
|
||
- fremder Combat Room wird abgelehnt
|
||
|
||
### Publisher
|
||
|
||
- Combat Event geht nur an Combat Room
|
||
- Character Event geht nur an Character Room
|
||
|
||
### Combat
|
||
|
||
- Action wird persistiert
|
||
- erst danach Event veröffentlicht
|
||
- Kampfende veröffentlicht `combat.finished`
|
||
- Event-Sequenz entspricht persistierter Reihenfolge
|
||
|
||
---
|
||
|
||
# 55. Testing Frontend
|
||
|
||
Mindestens:
|
||
|
||
- Verbindung wird nach Login aufgebaut
|
||
- Verbindung wird bei Logout geschlossen
|
||
- Reconnect wird versucht
|
||
- unbekannte Event-Typen crashen den Client nicht
|
||
- Combat Event erreicht CombatStore
|
||
- doppelte Sequence wird ignoriert
|
||
- Sequence Gap löst Resync aus
|
||
- Disconnect zerstört lokalen Spielzustand nicht
|
||
|
||
---
|
||
|
||
# 56. End-to-End-Test
|
||
|
||
Minimaler E2E-Flow:
|
||
|
||
```text
|
||
Client verbindet /events
|
||
|
||
→ Hunt starten
|
||
→ Combat starten
|
||
→ Combat Room betreten
|
||
→ ATTACK per REST senden
|
||
|
||
Server:
|
||
→ Action validieren
|
||
→ Combat berechnen
|
||
→ Combat speichern
|
||
→ Transaction committen
|
||
→ combat.action.resolved senden
|
||
|
||
Client:
|
||
→ Event empfangen
|
||
→ Combat UI aktualisieren
|
||
→ Animation ausführen
|
||
```
|
||
|
||
Bei letztem Treffer:
|
||
|
||
```text
|
||
→ Combat WON
|
||
→ combat.finished
|
||
→ Combat Room verlassen
|
||
```
|
||
|
||
---
|
||
|
||
# 57. Migration Requirement
|
||
|
||
Slice 0.13 sollte möglichst **keine neue Datenbankmigration nur für WebSockets** benötigen.
|
||
|
||
Persistierte CombatEvents bleiben bestehen.
|
||
|
||
Falls für Event-Reihenfolge bereits ein geeignetes Feld existiert, wird dieses weiterverwendet.
|
||
|
||
Nur wenn eine stabile Sequenz heute technisch nicht vorhanden ist, darf eine kleine CombatEvent-Sequenzmigration ergänzt werden.
|
||
|
||
---
|
||
|
||
# 58. Performance-Grundsatz
|
||
|
||
Keine globalen Broadcasts.
|
||
|
||
Nicht:
|
||
|
||
```text
|
||
send to all connected clients
|
||
```
|
||
|
||
Sondern:
|
||
|
||
```text
|
||
send to combat room
|
||
send to character room
|
||
send to user room
|
||
```
|
||
|
||
Damit wächst das System später mit der Anzahl aktiver Spieler besser mit.
|
||
|
||
---
|
||
|
||
# 59. Observability
|
||
|
||
Für Development Logging vorsehen:
|
||
|
||
```text
|
||
socket connected
|
||
socket disconnected
|
||
room joined
|
||
room left
|
||
event published
|
||
```
|
||
|
||
Keine vollständigen sensiblen Payloads ungefiltert loggen.
|
||
|
||
Besonders hilfreich:
|
||
|
||
```text
|
||
event type
|
||
combatId
|
||
room
|
||
sequence
|
||
```
|
||
|
||
---
|
||
|
||
# 60. Definition of Done
|
||
|
||
Slice 0.13 ist abgeschlossen, wenn:
|
||
|
||
- ein authentifizierter zentraler `/events` WebSocket existiert
|
||
- der Client genau eine zentrale Verbindung verwendet
|
||
- Reconnect funktioniert
|
||
- User-, Character- und Combat-Room-Konzept existiert
|
||
- Clients keine fremden Rooms abonnieren können
|
||
- ein gemeinsames `GameEvent`-Format existiert
|
||
- Combat Event Contracts in `packages/shared` liegen
|
||
- Combat-Aktionen weiterhin per REST ausgelöst werden
|
||
- Combat-Änderungen zusätzlich per WebSocket veröffentlicht werden
|
||
- Events erst nach erfolgreichem DB Commit gesendet werden
|
||
- Combat Events geordnet und deduplizierbar sind
|
||
- der Frontend CombatStore Events empfangen kann
|
||
- Disconnect und Reconnect keinen Spielzustand beschädigen
|
||
- HP-Regeneration unverändert timestamp-basiert funktioniert
|
||
- keine globale Server-Tick-Schleife eingeführt wurde
|
||
- bestehende Slice-0.12-Funktionalität weiterhin funktioniert
|
||
- Backend- und Frontendtests grün sind
|
||
|
||
---
|
||
|
||
# 61. Architektur nach Slice 0.13
|
||
|
||
Nach erfolgreicher Implementierung gilt:
|
||
|
||
```text
|
||
Ashen Realms Client
|
||
/ \
|
||
/ \
|
||
REST Commands WebSocket Events
|
||
| ^
|
||
v |
|
||
NestJS API EventsGateway
|
||
| ^
|
||
v |
|
||
Domain Services ---- EventPublisher
|
||
|
|
||
v
|
||
PostgreSQL
|
||
```
|
||
|
||
Grundregel:
|
||
|
||
> **REST: Ich möchte etwas tun.**
|
||
|
||
> **WebSocket: Etwas ist passiert.**
|
||
|
||
> **Zeitstempel: Etwas verändert sich vorhersehbar mit der Zeit.**
|
||
|
||
---
|
||
|
||
# 62. Vorbereitung für Slice 0.14+
|
||
|
||
Die Architektur soll insbesondere folgende nächsten Schritte ermöglichen:
|
||
|
||
### Slice 0.14
|
||
Combat Participants / mehrere Kampfteilnehmer
|
||
|
||
### Slice 0.15
|
||
Erster Mehrgegnerkampf
|
||
|
||
Beispiel:
|
||
|
||
```text
|
||
1 Spieler
|
||
vs.
|
||
3 NPCs
|
||
```
|
||
|
||
### Später
|
||
|
||
```text
|
||
2 Spieler
|
||
vs.
|
||
3 NPCs
|
||
```
|
||
|
||
sowie:
|
||
|
||
```text
|
||
Pets
|
||
NPC-Begleiter
|
||
Party Combat
|
||
PvP
|
||
```
|
||
|
||
Diese Systeme dürfen denselben:
|
||
|
||
```text
|
||
/events WebSocket
|
||
```
|
||
|
||
weiterverwenden.
|
||
|
||
Es wird kein separates Realtime-System für jeden neuen Featurebereich aufgebaut.
|
||
|
||
---
|
||
|
||
# 63. Leitentscheidung
|
||
|
||
Die wichtigste Entscheidung von Slice 0.13 lautet:
|
||
|
||
> **Ashen Realms erhält keine klassische permanente Realtime-Simulation, sondern eine serverautoritative, ereignisgetriebene Realtime-Schicht.**
|
||
|
||
Damit bleibt das Spiel technisch passend zu seiner Identität:
|
||
|
||
- browserbasiert
|
||
- rundenbasiert
|
||
- persistent
|
||
- serverautoritativ
|
||
- später multiplayerfähig
|
||
- ohne unnötigen permanenten Game-Server-Tick |