Files
ashen-realms/docs/playable-slices/Ashen Realms – Playable Slice 0.13_ Realtime Game Events Foundation.md
Bastian Wagner 221819880c arts und agents
2026-08-21 17:32:05 +02:00

27 KiB
Raw Blame History

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:

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:

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:

{
  "action": "ATTACK"
}

Nicht:

{
  "action": "ATTACK",
  "damage": 27
}

3.2 WebSocket diskrete serverseitige Ereignisse

WebSocket wird verwendet, wenn der Server einen Client über ein Ereignis informieren muss.

Beispiele:

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:

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:

/combat websocket
/character websocket
/party websocket

Sondern:

/events

Darüber werden verschiedene fachliche Event-Typen transportiert.


5. Zielarchitektur

Angular Client
    |
    | REST Commands
    v
NestJS Controllers
    |
    v
Domain Services
    |
    +--------------------------+
    |                          |
    v                          v
PostgreSQL               GameEventPublisher
                               |
                               v
                         EventsGateway
                               |
                               | WebSocket
                               v
                         Angular Client

Beispiel Combat:

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:

Event senden
→ danach DB speichern

Sondern:

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:

/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:

userId
characterId

Ein Client darf niemals selbst angeben:

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.

Login
→ Character laden
→ WebSocket /events verbinden

Bei Logout:

WebSocket trennen

Bei Verbindungsverlust:

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:

WebSocket disconnected

läuft das Spiel serverseitig weiter.

Nach Wiederherstellung:

WebSocket reconnect
→ aktuellen Zustand per REST synchronisieren

Beispielsweise:

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:

export interface GameEvent<TPayload = unknown> {
  id: string;
  sequence?: number;
  type: GameEventType;
  occurredAt: string;
  payload: TPayload;
}

Beispiel:

{
  "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:

domain.entity-or-action.event

Für Slice 0.13:

combat.action.resolved
combat.turn.changed
combat.finished
combat.state.changed

Später möglich:

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:

UPDATE_COMBAT
REFRESH_UI
DB_CHANGED
SYNC_PLAYER

Events beschreiben fachlich, was passiert ist.

Gut:

combat.turn.changed

Schlecht:

refreshCombatScreen

14. Rooms / Channels

Die zentrale WebSocket-Verbindung verwendet serverseitige Rooms.

Vorgesehene Room-Typen:

user:<userId>
character:<characterId>
combat:<combatId>

Später:

party:<partyId>
location:<locationId>
guild:<guildId>

15. User Room

Jeder eingeloggte Client wird automatisch Mitglied von:

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:

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:

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:

join combat:xyz

und dadurch beliebige Kämpfe beobachten.

Der Server validiert immer:

Ist dieser Charakter Teilnehmer dieses Kampfes?

Erst danach darf der Socket dem Room beitreten.


19. Game Events Backend-Struktur

Vorgesehene Struktur:

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:

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:

this.eventsGateway.server
  .to(room)
  .emit(...);

innerhalb des CombatService.

Stattdessen:

this.gameEventPublisher.publishToCombat(
  combatId,
  event,
);

Dadurch bleibt der Transport austauschbar und die Domain-Logik kennt keine Socket.IO-Details.


22. Beispiel Publisher Interface

Konzeptionell:

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:

Client
→ POST Combat Action
→ Server berechnet Resultat
→ Server persistiert Resultat
→ Response

Zusätzlich:

→ 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:

{
  "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:

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:

{
  "type": "combat.finished",
  "payload": {
    "combatId": "abc",
    "result": "WON"
  }
}

Später können weitere Informationen ergänzt werden:

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:

{
  "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:

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:

combatId
sequence

Client speichert:

lastProcessedSequence

Wenn:

sequence <= lastProcessedSequence

wird das Event nicht erneut abgespielt.


30. Event-Reihenfolge

Innerhalb eines Combat Rooms muss die Reihenfolge serverseitig eindeutig sein.

Beispiel:

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:

letzte bekannte Sequence = 40
neues Event = 44

existiert eine Lücke.

Dann darf der Client nicht raten.

Er synchronisiert:

GET /api/combats/:id

oder später:

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:

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:

combatEvents$
characterEvents$
partyEvents$

oder mit Angular Signals:

combatEvent
characterEvent

Feature Stores abonnieren nur relevante Eventtypen.


36. CombatStore Integration

Der CombatStore reagiert auf:

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:

PLAY_SWORD_ANIMATION
WAIT_800_MS
SHOW_RED_NUMBER

Sondern:

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:

currentHp
hpRegenSince
hpRegenPerSecond

Der Client berechnet die sichtbare Regeneration lokal.

Nicht implementieren:

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:

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:

{
  "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:

10 Hz
20 Hz
60 Hz

Game Loop ein.

Ashen Realms bleibt:

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:

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:

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:

PLAYER
MONSTER

begrenzen.

Events sollten allgemein mit:

actorId
targetId

arbeiten.

Nicht:

playerDamage
monsterDamage

Dadurch können spätere Teilnehmer sein:

PLAYER
NPC
PET
COMPANION
BOSS

44. Beispiel zukünftiger Kampf

Die 0.13-Architektur soll später ohne neuen Transportmechanismus ermöglichen:

TEAM A
- Player A
- Player B
- Pet

TEAM B
- Bandit
- Wolf
- Bandit

Ablauf:

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:

Disconnect
invalid event
sequence gap
parse error

gilt:

UI als unsynchronisiert markieren
→ REST Resync

Nicht:

lokal weitersimulieren

46. Client-Verbindungsstatus

Optional sichtbar oder intern:

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:

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:

REST
+ SSE
+ später zusätzlich WebSocket

Ziel bleibt:

REST
+
WebSocket

50. Shared Contracts

Cross-Boundary Contracts gehören nach:

packages/shared

Geeignet:

GameEvent
GameEventType
CombatActionResolvedPayload
CombatTurnChangedPayload
CombatFinishedPayload

Nicht dort ablegen:

  • NestJS Gateway
  • Socket.IO Server
  • Angular Services
  • TypeORM Entities

51. Vorgeschlagene Dateien Backend

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:

apps/api/src/combat/

52. Vorgeschlagene Dateien Frontend

apps/web/src/app/core/events/
├── game-events.service.ts
├── game-events.service.spec.ts
└── game-events.models.ts

Änderungen:

CombatStore
App initialization
Authentication lifecycle

53. Implementierungsreihenfolge

Task 1 Shared Event Contracts

Implementieren:

GameEvent
GameEventType
Combat Event Payloads

Tests:

  • TypeScript compile
  • Contract consistency

Task 2 WebSocket Gateway

Implementieren:

/events
Authentication
Connect
Disconnect

Tests:

  • unauthenticated rejected
  • authenticated accepted

Task 3 Room Infrastructure

Implementieren:

user room
character room
combat room

Tests:

  • nur berechtigte Membership
  • kein fremder Combat Room

Task 4 GameEventPublisher

Domain-unabhängige Publish API implementieren.

Tests:

publishToUser
publishToCharacter
publishToCombat

Task 5 Combat Integration

Nach erfolgreichem Combat Commit:

combat.action.resolved
combat.turn.changed
combat.finished

veröffentlichen.


Task 6 Frontend GameEventsService

Implementieren:

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:

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:

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:

→ 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:

send to all connected clients

Sondern:

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:

socket connected
socket disconnected
room joined
room left
event published

Keine vollständigen sensiblen Payloads ungefiltert loggen.

Besonders hilfreich:

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:

                    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:

1 Spieler
vs.
3 NPCs

Später

2 Spieler
vs.
3 NPCs

sowie:

Pets
NPC-Begleiter
Party Combat
PvP

Diese Systeme dürfen denselben:

/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