# 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)
	_broadcast_player_left.rpc(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()


# call_remote: Server hat lokal bereits gelöscht; nur Clients führen aus
@rpc("authority", "call_remote", "reliable")
func _broadcast_player_left(peer_id: int) -> void:
	players.erase(peer_id)
	player_left.emit(peer_id)
```

- [ ] **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
cd /home/mpuchstein/Dev/Godot/DnD_Anna_OneShot/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
cd /home/mpuchstein/Dev/Godot/DnD_Anna_OneShot/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
