From 3894a4550d7ff2cccda3f07443166020698b1627 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Mon, 13 Apr 2026 05:00:02 +0200 Subject: [PATCH] docs: add Lobby + Rollen implementation plan --- docs/plans/2026-04-13-lobby-rollen-plan.md | 468 +++++++++++++++++++++ 1 file changed, 468 insertions(+) create mode 100644 docs/plans/2026-04-13-lobby-rollen-plan.md diff --git a/docs/plans/2026-04-13-lobby-rollen-plan.md b/docs/plans/2026-04-13-lobby-rollen-plan.md new file mode 100644 index 0000000..64e3da9 --- /dev/null +++ b/docs/plans/2026-04-13-lobby-rollen-plan.md @@ -0,0 +1,468 @@ +# Lobby + Rollen — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Spieler verbinden sich über eine UI-Lobby, geben Namen und Rolle (Spieler/DM) ein, sehen alle anderen im Warteraum, und der DM startet das Spiel. + +**Architecture:** `NetworkManager` (Autoload) wird um `players`-Dict, Lobby-RPCs und -Signale erweitert. `lobby.tscn` enthält ein Join-Panel und ein Wait-Panel. `main.tscn` lädt `lobby.tscn` als Child. Alle Spielerdaten leben in `NetworkManager.players` für Zugriff durch spätere Szenen. + +**Tech Stack:** Godot 4.6.2, GDScript, ENetMultiplayerPeer, Godot MultiplayerAPI, Godot Control-Nodes + +**Spec:** `docs/plans/2026-04-13-lobby-rollen-design.md` + +**Arbeitsverzeichnis:** Alle Bash-Befehle aus `/home/mpuchstein/Dev/Godot/DnD_Anna_OneShot/` (Repo-Root), sofern nicht anders angegeben. + +--- + +## Dateiübersicht + +| Datei | Aktion | Verantwortlichkeit | +|---|---|---| +| `ruf-der-pilze/scripts/network_manager.gd` | Erweitern | `players`-Dict, neue Signale, neue RPCs | +| `ruf-der-pilze/scripts/lobby.gd` | Erstellen | Lobby-UI-Logik (Join-Flow, Warteraum) | +| `ruf-der-pilze/scenes/lobby.tscn` | Erstellen | Node-Struktur: JoinPanel + WaitPanel | +| `ruf-der-pilze/scenes/main.tscn` | Erweitern | `lobby.tscn` als Child-Szene einbinden | + +--- + +### Task 1: NetworkManager erweitern — Variablen + Signale + +**Files:** +- Modify: `ruf-der-pilze/scripts/network_manager.gd` + +- [ ] **Step 1: `players`-Dict und neue Signale hinzufügen** + +Direkt nach Zeile 9 (`var my_id: int = 0`) einfügen: + +```gdscript +var players: Dictionary = {} # peer_id (int) → {name: String, role: String} + +signal player_joined(peer_id: int, player_name: String, role: String) +signal player_left(peer_id: int) +signal player_list_synced() +signal game_started() +``` + +- [ ] **Step 2: `_on_peer_disconnected` anpassen** + +Aktuelle Zeilen 42–45 ersetzen durch: + +```gdscript +func _on_peer_disconnected(id: int) -> void: + peers.erase(id) + players.erase(id) + peer_disconnected.emit(id) + player_left.emit(id) + print("[Server] Peer getrennt: %d" % id) +``` + +- [ ] **Step 3: Datei prüfen** + +```bash +head -20 ruf-der-pilze/scripts/network_manager.gd +``` + +Erwartete Ausgabe: `players: Dictionary`, alle vier neuen Signale sichtbar. + +- [ ] **Step 4: Commit** + +```bash +git add ruf-der-pilze/scripts/network_manager.gd +git commit -m "net: add players dict and lobby signals to NetworkManager" +``` + +--- + +### Task 2: NetworkManager erweitern — Lobby-RPCs + +**Files:** +- Modify: `ruf-der-pilze/scripts/network_manager.gd` + +- [ ] **Step 1: Sechs neue RPC-Funktionen ans Ende der Datei anhängen** + +```gdscript +@rpc("any_peer", "call_remote", "reliable") +func register(player_name: String, role: String) -> void: + if not multiplayer.is_server(): + return + var sender_id := multiplayer.get_remote_sender_id() + if role == "dm": + for p in players.values(): + if p.role == "dm": + kick.rpc_id(sender_id, "Es ist bereits ein DM verbunden.") + return + # Erst bestehende Spielerliste an neuen Client senden (noch ohne den neuen Spieler) + sync_players.rpc_id(sender_id, players) + # Dann neuen Spieler an alle broadcasten (inkl. Server selbst durch call_local) + _broadcast_player_joined.rpc(sender_id, player_name, role) + + +@rpc("authority", "call_local", "reliable") +func _broadcast_player_joined(peer_id: int, player_name: String, role: String) -> void: + players[peer_id] = {name = player_name, role = role} + player_joined.emit(peer_id, player_name, role) + + +@rpc("authority", "call_remote", "reliable") +func sync_players(player_list: Dictionary) -> void: + # Nur Dict befüllen — kein Signal per Spieler emittieren. + # Lobby-UI rebuilt die Liste einmalig via player_list_synced. + players = player_list + player_list_synced.emit() + + +@rpc("authority", "call_remote", "reliable") +func kick(reason: String) -> void: + push_error("[Client] Verbindung abgelehnt: %s" % reason) + multiplayer.multiplayer_peer.close.call_deferred() + + +@rpc("any_peer", "call_remote", "reliable") +func request_start_game() -> void: + if not multiplayer.is_server(): + return + var requester_id := multiplayer.get_remote_sender_id() + if players.get(requester_id, {}).get("role", "") != "dm": + return + start_game.rpc() + + +@rpc("authority", "call_local", "reliable") +func start_game() -> void: + game_started.emit() +``` + +- [ ] **Step 2: Einrückung prüfen (Tabs, nicht Spaces)** + +```bash +cat -A ruf-der-pilze/scripts/network_manager.gd | grep -c "^\^I" +``` + +Alle eingerückten Zeilen müssen mit `^I` (Tab) beginnen. Keine Spaces. + +- [ ] **Step 3: Commit** + +```bash +git add ruf-der-pilze/scripts/network_manager.gd +git commit -m "net: add register/kick/sync/start_game RPCs to NetworkManager" +``` + +--- + +### Task 3: lobby.tscn erstellen + +**Files:** +- Create: `ruf-der-pilze/scenes/lobby.tscn` + +- [ ] **Step 1: `lobby.tscn` als Textdatei erstellen** + +Erstelle `ruf-der-pilze/scenes/lobby.tscn` mit folgendem Inhalt: + +``` +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://scripts/lobby.gd" id="1"] + +[node name="Lobby" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1") + +[node name="JoinPanel" type="VBoxContainer" parent="."] +offset_left = 300.0 +offset_top = 200.0 +offset_right = 700.0 +offset_bottom = 500.0 + +[node name="Title" type="Label" parent="JoinPanel"] +text = "Ruf der Pilze" + +[node name="NameInput" type="LineEdit" parent="JoinPanel"] +placeholder_text = "Dein Name" + +[node name="RoleOption" type="OptionButton" parent="JoinPanel"] + +[node name="JoinButton" type="Button" parent="JoinPanel"] +text = "Beitreten" + +[node name="WaitPanel" type="VBoxContainer" parent="."] +visible = false +offset_left = 300.0 +offset_top = 200.0 +offset_right = 700.0 +offset_bottom = 500.0 + +[node name="WaitLabel" type="Label" parent="WaitPanel"] +text = "Warte auf Spieler..." + +[node name="PlayerList" type="ItemList" parent="WaitPanel"] +custom_minimum_size = Vector2(0, 150) + +[node name="StartButton" type="Button" parent="WaitPanel"] +text = "Spiel starten" +visible = false +``` + +**Hinweis zu RoleOption:** Die Optionen "Spieler" und "DM" werden in `lobby.gd` per Code hinzugefügt (`add_item()`), nicht in der `.tscn`. + +- [ ] **Step 2: Datei prüfen** + +```bash +cat ruf-der-pilze/scenes/lobby.tscn +``` + +Alle Nodes vorhanden: `Lobby`, `JoinPanel`, `NameInput`, `RoleOption`, `JoinButton`, `WaitPanel`, `WaitLabel`, `PlayerList`, `StartButton`. + +- [ ] **Step 3: Commit** + +```bash +git add ruf-der-pilze/scenes/lobby.tscn +git commit -m "net: add lobby.tscn with join and wait panels" +``` + +--- + +### Task 4: lobby.gd erstellen + +**Files:** +- Create: `ruf-der-pilze/scripts/lobby.gd` + +- [ ] **Step 1: `lobby.gd` erstellen** + +Erstelle `ruf-der-pilze/scripts/lobby.gd` (Tabs für Einrückung): + +```gdscript +extends Control + +var _local_role: String = "" + + +func _ready() -> void: + $JoinPanel/RoleOption.add_item("Spieler") + $JoinPanel/RoleOption.add_item("DM") + + $JoinPanel/JoinButton.pressed.connect(_on_join_pressed) + $WaitPanel/StartButton.pressed.connect(_on_start_pressed) + + NetworkManager.connected_to_server.connect(_on_connected) + NetworkManager.connection_failed.connect(_on_connection_failed) + NetworkManager.player_joined.connect(_on_player_joined) + NetworkManager.player_left.connect(_on_player_left) + NetworkManager.player_list_synced.connect(_rebuild_player_list) + NetworkManager.game_started.connect(_on_game_started) + + +func _on_join_pressed() -> void: + var player_name := $JoinPanel/NameInput.text.strip_edges() + if player_name.is_empty(): + return + _local_role = "dm" if $JoinPanel/RoleOption.selected == 1 else "player" + NetworkManager.join_server("127.0.0.1", 4242) + + +func _on_connected() -> void: + var player_name := $JoinPanel/NameInput.text.strip_edges() + NetworkManager.register.rpc_id(1, player_name, _local_role) + $JoinPanel.visible = false + $WaitPanel.visible = true + $WaitPanel/StartButton.visible = (_local_role == "dm") + + +func _on_connection_failed() -> void: + push_error("[Lobby] Verbindung fehlgeschlagen") + + +func _on_player_joined(_peer_id: int, _player_name: String, _role: String) -> void: + _rebuild_player_list() + + +func _on_player_left(_peer_id: int) -> void: + _rebuild_player_list() + + +func _rebuild_player_list() -> void: + $WaitPanel/PlayerList.clear() + for p in NetworkManager.players.values(): + $WaitPanel/PlayerList.add_item("%s (%s)" % [p.name, p.role]) + + +func _on_start_pressed() -> void: + NetworkManager.request_start_game.rpc_id(1) + + +func _on_game_started() -> void: + # Platzhalter: Szenen-Wechsel kommt im nächsten Feature + print("[Lobby] Spiel gestartet!") +``` + +- [ ] **Step 2: Tab-Einrückung prüfen** + +```bash +cat -A ruf-der-pilze/scripts/lobby.gd | head -30 +``` + +Alle Einrückungen erscheinen als `^I`. + +- [ ] **Step 3: Commit** + +```bash +git add ruf-der-pilze/scripts/lobby.gd +git commit -m "net: add lobby.gd with join flow and wait room logic" +``` + +--- + +### Task 5: main.tscn erweitern — Lobby einbinden + +**Files:** +- Modify: `ruf-der-pilze/scenes/main.tscn` + +- [ ] **Step 1: `lobby.tscn` als instanziierte Child-Szene in `main.tscn` eintragen** + +Aktueller Inhalt von `main.tscn`: +``` +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://scripts/main.gd" id="1"] + +[node name="Main" type="Node"] +script = ExtResource("1") +``` + +Ersetzen durch: +``` +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://scripts/main.gd" id="1"] +[ext_resource type="PackedScene" path="res://scenes/lobby.tscn" id="2"] + +[node name="Main" type="Node"] +script = ExtResource("1") + +[node name="Lobby" parent="." instance=ExtResource("2")] +``` + +- [ ] **Step 2: Prüfen** + +```bash +cat ruf-der-pilze/scenes/main.tscn +``` + +`load_steps=3`, beide ext_resources sichtbar, `Lobby`-Node als Child. + +- [ ] **Step 3: Commit** + +```bash +git add ruf-der-pilze/scenes/main.tscn +git commit -m "net: add lobby scene instance to main.tscn" +``` + +--- + +### Task 6: Integration testen + +**Files:** keine Änderungen — nur Verifikation + +- [ ] **Step 1: Server headless starten** + +```bash +# aus ruf-der-pilze/ +godot --headless -- --server +``` + +Erwartete Ausgabe: +``` +[Server] Gestartet auf Port 4242 +``` + +- [ ] **Step 2: Client 1 verbinden (Godot Editor, F5)** + +Im Godot Editor F5 drücken. Die Lobby-UI erscheint: +- JoinPanel mit Namensfeld, Rollenauswahl ("Spieler" / "DM"), "Beitreten"-Button + +Name eingeben (z.B. "Klaus"), Rolle "Spieler", "Beitreten" klicken. + +Erwartetes Ergebnis: +- JoinPanel verschwindet, WaitPanel erscheint +- PlayerList zeigt: "Klaus (player)" +- "Spiel starten" Button ist **nicht** sichtbar + +Server-Terminal: +``` +[Server] Peer verbunden: XXXXXX +``` + +- [ ] **Step 3: Client 2 verbinden (zweiter Godot-Prozess)** + +Zweiten Godot-Prozess starten (z.B. über Terminal): +```bash +# aus ruf-der-pilze/ +godot +``` + +Name "Marie", Rolle "DM", "Beitreten". + +Erwartetes Ergebnis: +- Beide Clients zeigen in der PlayerList: "Klaus (player)" und "Marie (dm)" +- Nur Marie sieht "Spiel starten" + +- [ ] **Step 4: DM-Duplikat testen (Kick)** + +Dritten Client starten, Rolle "DM" wählen, "Beitreten". + +Erwartetes Ergebnis: +- `push_error` im Output: `[Client] Verbindung abgelehnt: Es ist bereits ein DM verbunden.` +- Verbindung wird geschlossen + +- [ ] **Step 5: Spiel starten** + +DM ("Marie") klickt "Spiel starten". + +Erwartetes Ergebnis (alle Clients): +``` +[Lobby] Spiel gestartet! +``` + +- [ ] **Step 6: Disconnect testen** + +Einen Client schließen → andere Clients' PlayerList aktualisiert sich. + +- [ ] **Step 7: STATUS.md updaten** + +`/home/mpuchstein/Dev/Godot/DnD_Anna_OneShot/docs/STATUS.md` aktualisieren: + +```markdown +### ✅ Abgeschlossen +- MCP-Addon eingerichtet +- Projektstruktur angelegt +- CLAUDE.md mit vollständiger Spielkonzept-Dokumentation +- **Multiplayer Grundgerüst** — ENet Server/Client, NetworkManager Autoload, welcome RPC +- **Lobby + Rollen** — Join-UI, Warteraum, Spieler/DM-Rollen, sync, kick, Spiel-Start-Signal + +### ⏳ Als nächstes +- **Szenen-Wechsel nach Spielstart** — lobby.tscn → Spielszene (Eingangshalle) + Plan: noch zu erstellen +``` + +- [ ] **Step 8: Pläne archivieren + Commit** + +```bash +mkdir -p docs/plans/archive +mv docs/plans/2026-04-13-lobby-rollen-design.md docs/plans/archive/ +mv docs/plans/2026-04-13-lobby-rollen-plan.md docs/plans/archive/ +git add docs/ +git commit -m "docs: mark Lobby + Rollen as complete, archive plans, update STATUS" +``` + +--- + +## Verifikations-Checkliste + +- [ ] Server startet: `[Server] Gestartet auf Port 4242` +- [ ] Client verbindet: JoinPanel → WaitPanel Übergang funktioniert +- [ ] PlayerList zeigt alle verbundenen Spieler (Name + Rolle) +- [ ] Zweiter DM wird gekickt mit Fehlermeldung +- [ ] Nur DM sieht "Spiel starten" Button +- [ ] Alle Clients loggen `[Lobby] Spiel gestartet!` wenn DM startet +- [ ] Disconnect entfernt Spieler aus der Liste anderer Clients +- [ ] Kein Parser-Fehler in Godot Output