# 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 { 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: character: combat: ``` Später: ```text party: location: guild: ``` --- # 15. User Room Jeder eingeloggte Client wird automatisch Mitglied von: ```text user: ``` 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: ``` 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: ``` 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