27 KiB
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
/eventsWebSocket 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/sharedliegen - 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