# DM Regiepult Basics — 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:** DM-Szene ausbauen: Top-Down-Karte (EG/OG umschaltbar) mit Spieler-Positionsmarkern, Live-Kamera-Feed pro Spieler (SubViewport) und Overlay-Toggle pro Spieler.

**Architecture:** `GameState` Autoload speichert Overlay-Zustände und Spieler-Kamera-Transforms client-lokal. Spieler broadcasten ihre Kamera-Position + Rotation periodisch per `rpc_id` an den DM-Peer. Der DM hat (a) einen SubViewport für die Top-Down-Karte, (b) einen SubViewport pro Spieler für den Live-Kamera-Feed — jeder Feed lädt `tavern.tscn` lokal und positioniert eine Camera3D auf dem zuletzt empfangenen Transform. Der DM sendet `request_set_overlay` an den Server, der per `rpc_id` an den Zielspieler weiterleitet.

**Tech Stack:** Godot 4.x, GDScript, ENet Multiplayer, SubViewport, godot-mcp MCP Tools

**Voraussetzung:** Schritt 5 abgeschlossen — `tavern.tscn`, `tavern.gd`, `tavern_lobby.tscn`, `dm_view.tscn`, `dm_view.gd` existieren; SceneManager routet DM nach `game_started` zu `dm_view.tscn`, Spieler zu `tavern.tscn`.

---

## File Map

| Aktion | Pfad | Verantwortung |
|--------|------|---------------|
| Erstellen | `scripts/game_state.gd` | Overlay-Zustände + Spieler-Positionen (client-lokal) |
| Ändern | `project.godot` | GameState als Autoload registrieren |
| Ändern | `scripts/network_manager.gd` | `request_set_overlay` + `set_overlay` + `sync_player_position` RPCs |
| Ändern | `scripts/tavern.gd` | DM-Rolle-Check in `_ready()`; Timer für Kamera-Transform-Broadcast |
| Ändern | `scenes/dm_view.tscn` | SubViewport (Top-Down) + HBoxContainer für Player-Cam-Feeds, Etagen-Buttons, Overlay-Panel |
| Ändern | `scripts/dm_view.gd` | Etagen-Switch, Marker-Verwaltung, Player-Cam-SubViewports, Overlay-Toggle-Buttons |

Alle Pfade relativ zu `ruf-der-pilze/`.

---

## Task 1: DM-Rolle-Check in `tavern.gd`

**Files:**
- Modify: `scripts/tavern.gd`

`tavern.tscn` wird von `dm_view.gd` in einem SubViewport instanziert. Ohne Rolle-Check würde `tavern.gd` einen PlayerController für den DM spawnen — das darf nicht passieren.

- [ ] **Step 1: Rolle-Check in `_ready()` ergänzen**

Ersetze die `_ready()` Funktion in `scripts/tavern.gd`:

```gdscript
func _ready() -> void:
	var args := OS.get_cmdline_args() + OS.get_cmdline_user_args()
	if OS.has_feature("dedicated_server") or "--server" in args:
		return
	var role: String = NetworkManager.players.get(NetworkManager.my_id, {}).get("role", "player")
	if role == "dm":
		return  # DM lädt tavern.tscn nur für Top-Down-Ansicht, kein PlayerController
	var room_index := SceneManager.pending_room_index
	_spawn_player(room_index)
```

- [ ] **Step 2: Verifizieren — Script-Syntax prüfen**

Öffne Godot. Erwartung: kein GDScript-Fehler in `tavern.gd`.

- [ ] **Step 3: Committen**

```bash
git add scripts/tavern.gd
git commit -m "fix: tavern.gd — skip player spawn when role is dm"
```

---

## Task 2: `GameState` Autoload erstellen

**Files:**
- Create: `scripts/game_state.gd`
- Modify: `project.godot`

`GameState` ist ein leichtgewichtiger Autoload ohne RPCs — er speichert nur lokale Zustände, die von anderen Autoloads (NetworkManager) befüllt werden.

- [ ] **Step 1: `scripts/game_state.gd` erstellen**

```gdscript
extends Node

# Overlay-Zustand pro Spieler (nur auf dem DM-Client befüllt via RPC)
# "default" = kein Overlay aktiv
var overlay_states: Dictionary = {}  # peer_id (int) → String

# Spieler-Positionen (nur auf dem DM-Client befüllt via sync_player_position)
var player_positions: Dictionary = {}  # peer_id (int) → {position: Vector3, y_rotation: float}


func get_overlay(peer_id: int) -> String:
	return overlay_states.get(peer_id, "default")


func set_overlay_local(peer_id: int, overlay_name: String) -> void:
	overlay_states[peer_id] = overlay_name
	print("[GameState] Overlay für %d → %s" % [peer_id, overlay_name])


func update_player_transform(peer_id: int, position: Vector3, rotation: Vector3) -> void:
	player_positions[peer_id] = {position = position, rotation = rotation}
```

- [ ] **Step 2: GameState in `project.godot` als Autoload eintragen**

Füge in `project.godot` unter `[autoload]` hinzu:

```ini
GameState="*res://scripts/game_state.gd"
```

Ergebnis:
```ini
[autoload]

MCPGameBridge="res://addons/godot_mcp/game_bridge/mcp_game_bridge.gd"
NetworkManager="*res://scripts/network_manager.gd"
SceneManager="*res://scripts/scene_manager.gd"
GameState="*res://scripts/game_state.gd"
```

- [ ] **Step 3: Verifizieren — Godot öffnen**

Öffne Godot. Erwartung: kein Fehler beim Projektladen, GameState erscheint in den Autoload-Einstellungen (Project → Project Settings → Autoload).

- [ ] **Step 4: Committen**

```bash
git add scripts/game_state.gd project.godot
git commit -m "feat: add GameState autoload — overlay states and player positions"
```

---

## Task 3: NetworkManager — Overlay + Positions RPCs

**Files:**
- Modify: `scripts/network_manager.gd`

Drei neue Funktionen:
1. `request_set_overlay` — DM → Server (Anfrage)
2. `set_overlay` — Server → Zielspieler (Ausführung)
3. `sync_player_position` — Spieler → DM-Peer (Positions-Update)

- [ ] **Step 1: `request_set_overlay` hinzufügen**

Füge am Ende von `network_manager.gd` hinzu:

```gdscript
@rpc("any_peer", "call_remote", "reliable")
func request_set_overlay(target_peer_id: int, overlay_name: String) -> void:
	if not multiplayer.is_server():
		return
	var requester_id := multiplayer.get_remote_sender_id()
	if players.get(requester_id, {}).get("role", "") != "dm":
		return
	set_overlay.rpc_id(target_peer_id, overlay_name)
```

- [ ] **Step 2: `set_overlay` hinzufügen**

```gdscript
@rpc("authority", "call_remote", "reliable")
func set_overlay(overlay_name: String) -> void:
	GameState.set_overlay_local(my_id, overlay_name)
```

- [ ] **Step 3: `sync_player_position` hinzufügen**

```gdscript
@rpc("any_peer", "call_remote", "unreliable")
func sync_player_position(player_id: int, position: Vector3, rotation: Vector3) -> void:
	GameState.update_player_transform(player_id, position, rotation)
```

- [ ] **Step 4: Verifizieren — Script-Syntax prüfen**

Öffne Godot. Erwartung: kein GDScript-Fehler in `network_manager.gd`.

- [ ] **Step 5: Committen**

```bash
git add scripts/network_manager.gd
git commit -m "feat: network_manager — overlay RPC + player position sync"
```

---

## Task 4: `tavern.gd` — Kamera-Transform-Broadcast

**Files:**
- Modify: `scripts/tavern.gd`

Spieler senden ihre Kamera-Position + vollständige Rotation alle 0.1 Sekunden an den DM-Peer. Die vollständige Rotation (nicht nur Y) ist nötig damit der DM-Feed den exakten Blickwinkel des Spielers rendert.

- [ ] **Step 1: `_get_dm_peer_id()` Hilfsfunktion hinzufügen**

Füge in `tavern.gd` hinzu:

```gdscript
func _get_dm_peer_id() -> int:
	for peer_id in NetworkManager.players.keys():
		if NetworkManager.players[peer_id].get("role", "") == "dm":
			return peer_id
	return 0
```

- [ ] **Step 2: Timer für Kamera-Broadcast einrichten**

Ergänze `_ready()` in `tavern.gd` (nach `_spawn_player`, nur für Spieler-Clients):

```gdscript
func _ready() -> void:
	var args := OS.get_cmdline_args() + OS.get_cmdline_user_args()
	if OS.has_feature("dedicated_server") or "--server" in args:
		return
	var role: String = NetworkManager.players.get(NetworkManager.my_id, {}).get("role", "player")
	if role == "dm":
		return
	var room_index := SceneManager.pending_room_index
	_spawn_player(room_index)
	# Kamera-Transform-Broadcast alle 0.1s
	var timer := Timer.new()
	timer.wait_time = 0.1
	timer.autostart = true
	timer.timeout.connect(_broadcast_camera_transform)
	add_child(timer)


func _broadcast_camera_transform() -> void:
	var dm_id := _get_dm_peer_id()
	if dm_id == 0:
		return
	var controller := get_node_or_null("PlayerController")
	if controller == null:
		return
	var cam := controller.get_node_or_null("Camera3D") as Camera3D
	var pos := cam.global_position if cam != null else controller.global_position
	var rot := cam.global_rotation if cam != null else controller.global_rotation
	NetworkManager.sync_player_position.rpc_id(dm_id, NetworkManager.my_id, pos, rot)
```

- [ ] **Step 3: Verifizieren — Script-Syntax prüfen**

Öffne Godot. Erwartung: kein GDScript-Fehler in `tavern.gd`.

- [ ] **Step 4: Committen**

```bash
git add scripts/tavern.gd
git commit -m "feat: tavern.gd — broadcast camera transform (pos + rotation) to DM every 0.1s"
```

---

## Task 5: `dm_view.tscn` — Top-Down SubViewport + Etagen-Switch

**Files:**
- Modify: `scenes/dm_view.tscn`
- Modify: `scripts/dm_view.gd`

Die bestehende Stub-Szene wird durch eine voll funktionsfähige DM-Ansicht ersetzt. Die Szene wird per MCP-Tool oder im Godot-Editor aufgebaut.

- [ ] **Step 1: `dm_view.tscn` Struktur aufbauen**

Ersetze den Inhalt von `scenes/dm_view.tscn` mit folgender Node-Struktur:

```
Node "DmView"                          ← script = dm_view.gd
  VBoxContainer "RootLayout"           ← füllt gesamten Viewport
    HSplitContainer "TopSection"
      size_flags_vertical = SIZE_EXPAND_FILL
      SubViewportContainer "MapContainer"
        custom_minimum_size = Vector2(700, 500)
        SubViewport "MapViewport"
          size = Vector2i(700, 500)
          Node3D "MapRoot"             ← tavern.tscn wird hier per Script hinzugefügt
            Node3D "PlayerMarkers"     ← programmatisch befüllte Marker
            Camera3D "TopDownCam"
              transform.origin = Vector3(2, 25, 0)
              rotation_degrees = Vector3(-90, 0, 0)
              current = true
      VBoxContainer "SidePanel"
        custom_minimum_size = Vector2(220, 0)
        Label "LblFloor"
          text = "Etage"
        HBoxContainer "FloorButtons"
          Button "BtnEG"
            text = "Erdgeschoss"
            toggle_mode = true
            button_pressed = true
          Button "BtnOG"
            text = "Obergeschoss"
            toggle_mode = true
        HSeparator
        Label "LblOverlay"
          text = "Overlays"
        ScrollContainer "OverlayScroll"
          size_flags_vertical = SIZE_EXPAND_FILL
          VBoxContainer "PlayerList"   ← programmatisch befüllt
    HSeparator
    HBoxContainer "PlayerCamsRow"      ← Player-Cam-Feeds; per Script befüllt
      custom_minimum_size = Vector2(0, 200)
```

Hinweis: `VBoxContainer "RootLayout"` füllt den gesamten Viewport — kein CanvasLayer nötig, da alles im 2D-Control-Tree liegt.

- [ ] **Step 2: `dm_view.gd` — Top-Down-Szene laden + Etagen-Switch**

Ersetze den Inhalt von `scripts/dm_view.gd`:

```gdscript
extends Node

const TAVERN_SCENE := "res://scenes/tavern.tscn"
const CAM_EG_Y := 25.0  # Top-Down Höhe Erdgeschoss (alles bis y < 4m)
const CAM_OG_Y := 30.0  # Top-Down Höhe Obergeschoss (ab y > 4m)

var _top_down_cam: Camera3D
var _player_markers: Node3D
var _markers_by_id: Dictionary = {}  # peer_id → MeshInstance3D


func _ready() -> void:
	_load_tavern_into_viewport()
	_setup_floor_buttons()
	_setup_overlay_panel()
	GameState.player_positions  # keine direkte Verbindung nötig — Polling in _process


func _load_tavern_into_viewport() -> void:
	var packed := ResourceLoader.load(TAVERN_SCENE, "PackedScene") as PackedScene
	if packed == null:
		push_error("[DmView] tavern.tscn nicht gefunden")
		return
	var map_root := get_node("Layout/MapContainer/MapViewport/MapRoot") as Node3D
	_top_down_cam = map_root.get_node("TopDownCam") as Camera3D
	_player_markers = map_root.get_node("PlayerMarkers") as Node3D
	var tavern := packed.instantiate()
	tavern.name = "Tavern"
	map_root.add_child(tavern)
	print("[DmView] Tavern in SubViewport geladen")


func _setup_floor_buttons() -> void:
	var btn_eg := get_node("Layout/SidePanel/FloorButtons/BtnEG") as Button
	var btn_og := get_node("Layout/SidePanel/FloorButtons/BtnOG") as Button
	btn_eg.pressed.connect(func() -> void: _switch_floor(false))
	btn_og.pressed.connect(func() -> void: _switch_floor(true))


func _switch_floor(upper: bool) -> void:
	if _top_down_cam == null:
		return
	_top_down_cam.global_position.y = CAM_OG_Y if upper else CAM_EG_Y
	print("[DmView] Etage gewechselt → %s" % ("OG" if upper else "EG"))


func _setup_overlay_panel() -> void:
	# Wird in Task 7 befüllt — jetzt leer lassen
	pass


func _process(_delta: float) -> void:
	_update_player_markers()


func _update_player_markers() -> void:
	for peer_id in GameState.player_positions.keys():
		var data: Dictionary = GameState.player_positions[peer_id]
		var pos: Vector3 = data.get("position", Vector3.ZERO)
		var rot: Vector3 = data.get("rotation", Vector3.ZERO)
		var marker := _get_or_create_marker(peer_id)
		marker.global_position = pos + Vector3(0, 0.2, 0)  # leicht über dem Boden
		marker.rotation.y = rot.y  # Blickrichtung auf der Karte


func _get_or_create_marker(peer_id: int) -> MeshInstance3D:
	if peer_id in _markers_by_id:
		return _markers_by_id[peer_id]
	var mesh := MeshInstance3D.new()
	var sphere := SphereMesh.new()
	sphere.radius = 0.25
	sphere.height = 0.5
	mesh.mesh = sphere
	var mat := StandardMaterial3D.new()
	mat.albedo_color = _peer_color(peer_id)
	mat.emission_enabled = true
	mat.emission = mat.albedo_color
	mat.emission_energy_multiplier = 0.5
	mesh.material_override = mat
	_player_markers.add_child(mesh)
	_markers_by_id[peer_id] = mesh
	print("[DmView] Marker erstellt für Peer %d" % peer_id)
	return mesh


func _peer_color(peer_id: int) -> Color:
	var colors := [Color.RED, Color.CYAN, Color.YELLOW, Color.MAGENTA, Color.GREEN]
	return colors[peer_id % colors.size()]
```

- [ ] **Step 3: Verifizieren — DM-Szene im Editor prüfen**

Öffne `dm_view.tscn` im Godot-Editor.
Erwartung: HSplitContainer sichtbar, SubViewportContainer links, SidePanel rechts, Etagen-Buttons vorhanden. Keine Fehler in der Scene-Hierarchie.

- [ ] **Step 4: Verifizieren — DM-Client startet korrekt**

Starte ein Multiplayer-Spiel (Server + 1 Spieler + 1 DM). DM klickt Start.
Erwartung:
- DM-Client: `dm_view.tscn` lädt, Taverne im SubViewport sichtbar (Top-Down)
- Console: `[DmView] Tavern in SubViewport geladen`
- Etagen-Buttons klickbar, Kamera wechselt die Höhe

- [ ] **Step 5: Committen**

```bash
git add scenes/dm_view.tscn scripts/dm_view.gd
git commit -m "feat: dm_view — top-down SubViewport + floor switch (EG/OG)"
```

---

## Task 6: `dm_view.gd` — Spieler-Positionsmarker (Etagen-Filter)

**Files:**
- Modify: `scripts/dm_view.gd`

Marker nur für die aktuelle Etage einblenden: Spieler auf EG (y < 4) verschwinden beim OG-Wechsel und umgekehrt.

- [ ] **Step 1: Etagen-Zustand und Marker-Sichtbarkeit verwalten**

Ergänze in `dm_view.gd` die Variable und den Etagen-Switch:

```gdscript
var _showing_upper_floor: bool = false


func _switch_floor(upper: bool) -> void:
	_showing_upper_floor = upper
	if _top_down_cam == null:
		return
	_top_down_cam.global_position.y = CAM_OG_Y if upper else CAM_EG_Y
	print("[DmView] Etage gewechselt → %s" % ("OG" if upper else "EG"))
```

Ergänze die Sichtbarkeits-Logik in `_update_player_markers()`:

```gdscript
func _update_player_markers() -> void:
	for peer_id in GameState.player_positions.keys():
		var data: Dictionary = GameState.player_positions[peer_id]
		var pos: Vector3 = data.get("position", Vector3.ZERO)
		var rot: Vector3 = data.get("rotation", Vector3.ZERO)
		var marker := _get_or_create_marker(peer_id)
		marker.global_position = pos + Vector3(0, 0.2, 0)
		marker.rotation.y = rot.y
		# Marker nur auf der aktuellen Etage zeigen (OG = y > 3.5)
		var is_upper := pos.y > 3.5
		marker.visible = (is_upper == _showing_upper_floor)
```

- [ ] **Step 2: Verifizieren — Marker-Sichtbarkeit im Test**

Starte Multiplayer-Test (1 Spieler in Room1 = OG). DM sieht:
- EG-Ansicht: kein Marker sichtbar (Spieler ist im OG)
- OG-Ansicht: farbiger Marker bei Room1-SpawnPoint sichtbar

Console (DM): `[DmView] Marker erstellt für Peer <id>`

- [ ] **Step 3: Committen**

```bash
git add scripts/dm_view.gd
git commit -m "feat: dm_view — player markers with floor filter"
```

---

## Task 7: `dm_view.gd` — Player-Cam-Feeds

**Files:**
- Modify: `scripts/dm_view.gd`

Pro Spieler wird ein SubViewport mit einer lokalen Kopie von `tavern.tscn` erstellt. Eine Camera3D in diesem SubViewport wird in `_process` auf den zuletzt empfangenen Kamera-Transform des Spielers gesetzt. So sieht der DM in Echtzeit was jeder Spieler sieht.

- [ ] **Step 1: Variablen und `_setup_player_cams()` Aufruf ergänzen**

Ergänze in `dm_view.gd` die Variablen am Anfang des Scripts:

```gdscript
var _player_cam_cams: Dictionary = {}   # peer_id → Camera3D (in SubViewport)
```

Ergänze in `_ready()` den Aufruf:

```gdscript
func _ready() -> void:
	_load_tavern_into_viewport()
	_setup_floor_buttons()
	_setup_player_cams()      # NEU
	_setup_overlay_panel()
```

- [ ] **Step 2: `_setup_player_cams()` implementieren**

```gdscript
func _setup_player_cams() -> void:
	NetworkManager.player_joined.connect(_on_player_joined_cam)
	NetworkManager.player_left.connect(_on_player_left_cam)
	for peer_id in NetworkManager.players.keys():
		var info: Dictionary = NetworkManager.players[peer_id]
		if info.get("role", "") == "player":
			_create_player_cam(peer_id, info.get("name", "???"))


func _on_player_joined_cam(peer_id: int, player_name: String, role: String) -> void:
	if role == "player":
		_create_player_cam(peer_id, player_name)


func _on_player_left_cam(peer_id: int) -> void:
	var row := get_node("RootLayout/PlayerCamsRow")
	var panel := row.get_node_or_null("CamPanel_%d" % peer_id)
	if panel != null:
		panel.queue_free()
	_player_cam_cams.erase(peer_id)
```

- [ ] **Step 3: `_create_player_cam()` implementieren**

```gdscript
func _create_player_cam(peer_id: int, player_name: String) -> void:
	var packed := ResourceLoader.load("res://scenes/tavern.tscn", "PackedScene") as PackedScene
	if packed == null:
		push_error("[DmView] tavern.tscn nicht gefunden für Player-Cam")
		return

	var viewport := SubViewport.new()
	viewport.size = Vector2i(320, 180)

	var scene_root := Node3D.new()
	var tavern := packed.instantiate()
	scene_root.add_child(tavern)

	var cam := Camera3D.new()
	cam.name = "PlayerCam"
	cam.current = true
	scene_root.add_child(cam)
	viewport.add_child(scene_root)

	var container := SubViewportContainer.new()
	container.name = "CamPanel_%d" % peer_id
	container.custom_minimum_size = Vector2(320, 180)
	container.stretch = true
	container.add_child(viewport)

	var wrapper := VBoxContainer.new()
	var label := Label.new()
	label.text = player_name
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	wrapper.add_child(label)
	wrapper.add_child(container)

	get_node("RootLayout/PlayerCamsRow").add_child(wrapper)
	_player_cam_cams[peer_id] = cam
	print("[DmView] Player-Cam erstellt für %s (Peer %d)" % [player_name, peer_id])
```

- [ ] **Step 4: Camera-Transforms in `_process` aktualisieren**

Ergänze `_process` in `dm_view.gd`:

```gdscript
func _process(_delta: float) -> void:
	_update_player_markers()
	_update_player_cams()    # NEU


func _update_player_cams() -> void:
	for peer_id in _player_cam_cams.keys():
		if not GameState.player_positions.has(peer_id):
			continue
		var data: Dictionary = GameState.player_positions[peer_id]
		var pos: Vector3 = data.get("position", Vector3.ZERO)
		var rot: Vector3 = data.get("rotation", Vector3.ZERO)
		var cam: Camera3D = _player_cam_cams[peer_id]
		cam.global_position = pos
		cam.global_rotation = rot
```

- [ ] **Step 5: Verifizieren — Player-Cam-Feeds erscheinen**

Starte Multiplayer-Test (1-2 Spieler + DM). DM startet Spiel.
Erwartungen:
- DM-Ansicht unten: ein Panel pro Spieler mit Spieler-Name und 3D-Feed
- Feeds zeigen den Blickwinkel der jeweiligen Spieler (Zimmer-Perspektive)
- Console (DM): `[DmView] Player-Cam erstellt für Spieler1 (Peer <id>)`

- [ ] **Step 6: Committen**

```bash
git add scripts/dm_view.gd
git commit -m "feat: dm_view — live player cam feeds (SubViewport per player)"
```

---

## Task 8: `dm_view.gd` — Overlay-Toggle Panel

**Files:**
- Modify: `scripts/dm_view.gd`

Dynamisch eine Zeile pro Spieler im SidePanel erstellen. Jede Zeile hat:
- Spieler-Name (Label)
- Toggle-Button (aktueller Overlay-Zustand)

Klick sendet `request_set_overlay` RPC an den Server.

- [ ] **Step 1: `_setup_overlay_panel()` implementieren**

Ersetze `_setup_overlay_panel()` in `dm_view.gd`:

```gdscript
func _setup_overlay_panel() -> void:
	NetworkManager.player_joined.connect(_on_player_joined)
	NetworkManager.player_left.connect(_on_player_left)
	# Bereits verbundene Spieler hinzufügen
	for peer_id in NetworkManager.players.keys():
		var info: Dictionary = NetworkManager.players[peer_id]
		if info.get("role", "") == "player":
			_add_overlay_row(peer_id, info.get("name", "???"))


func _on_player_joined(peer_id: int, player_name: String, role: String) -> void:
	if role == "player":
		_add_overlay_row(peer_id, player_name)


func _on_player_left(peer_id: int) -> void:
	var list := get_node("Layout/SidePanel/OverlayScroll/PlayerList") as VBoxContainer
	var row := list.get_node_or_null("Row_%d" % peer_id)
	if row != null:
		row.queue_free()
```

- [ ] **Step 2: `_add_overlay_row()` implementieren**

```gdscript
const OVERLAY_CYCLE := ["default", "spore_active"]


func _add_overlay_row(peer_id: int, player_name: String) -> void:
	var list := get_node("Layout/SidePanel/OverlayScroll/PlayerList") as VBoxContainer
	var row := HBoxContainer.new()
	row.name = "Row_%d" % peer_id
	var label := Label.new()
	label.text = player_name
	label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	var btn := Button.new()
	btn.text = "● default"
	btn.pressed.connect(func() -> void: _toggle_overlay(peer_id, btn))
	row.add_child(label)
	row.add_child(btn)
	list.add_child(row)


func _toggle_overlay(peer_id: int, btn: Button) -> void:
	var current := GameState.get_overlay(peer_id)
	var idx := OVERLAY_CYCLE.find(current)
	var next: String = OVERLAY_CYCLE[(idx + 1) % OVERLAY_CYCLE.size()]
	NetworkManager.request_set_overlay.rpc_id(1, peer_id, next)  # 1 = Server peer_id
	# Lokal vormerken damit Button-Text sofort aktualisiert wird
	GameState.set_overlay_local(peer_id, next)
	btn.text = "● %s" % next
	print("[DmView] Overlay-Toggle → Spieler %d: %s" % [peer_id, next])
```

- [ ] **Step 3: Verifizieren — Overlay-Toggle im Test**

Starte Multiplayer-Test (1 Spieler + 1 DM). DM klickt Toggle-Button für Spieler.
Erwartungen:
- DM-Console: `[DmView] Overlay-Toggle → Spieler <id>: spore_active`
- Spieler-Console: `[GameState] Overlay für <id> → spore_active`
- Button-Text auf DM wechselt zu `● spore_active`
- Nochmal klicken: zurück auf `● default`

- [ ] **Step 4: Committen**

```bash
git add scripts/dm_view.gd
git commit -m "feat: dm_view — overlay toggle panel per player"
```

---

## Task 9: End-to-End Test

**Files:** keine Änderungen

Vollständiger Durchlauf: Server + 2 Spieler-Clients + 1 DM-Client.

- [ ] **Step 1: Server starten**

```bash
cd ruf-der-pilze
godot --headless -- --server
```

- [ ] **Step 2: 2 Spieler verbinden und Spiel starten (DM klickt Start)**

Clients verbinden, DM startet das Spiel.
Erwartung Spieler: `tavern.tscn` geladen, Kamera im zugewiesenen Zimmer.
Erwartung DM: `dm_view.tscn` geladen, Taverne im SubViewport sichtbar.

- [ ] **Step 3: Top-Down-Ansicht prüfen**

DM wechselt zwischen EG und OG.
Erwartung:
- EG: Gastraum von oben sichtbar, Tresen erkennbar
- OG: Korridor und Zimmer von oben sichtbar
- Kamera-Wechsel sofort, kein Flackern

- [ ] **Step 4: Spieler-Marker prüfen**

Spieler-Clients sind in Zimmer (OG). DM wechselt zu OG.
Erwartung: farbige Marker für beide Spieler im OG-Bereich sichtbar.
Marker bewegen sich (auch wenn Spieler keine Bewegung hat, Position wird gesynct).
Console (DM): `[DmView] Marker erstellt für Peer <id>` für beide Spieler.

- [ ] **Step 5: Player-Cam-Feeds prüfen**

Unten im DM-Fenster: zwei Panels (eines pro Spieler) mit dem 3D-Feed aus ihrer Perspektive.
Erwartung:
- Beide Panels zeigen den Raum aus Spieler-Sicht (First-Person-ähnlich)
- Feed aktualisiert sich wenn Spieler sich dreht (Kamera-Rotation sichtbar)
- Console (DM): `[DmView] Player-Cam erstellt für Spieler1` und `Spieler2`

- [ ] **Step 6: Overlay-Toggle prüfen**

DM klickt Toggle für Spieler 1: `spore_active`.
Erwartung:
- DM-Button: `● spore_active`
- Spieler 1-Console: `[GameState] Overlay für <id> → spore_active`
- Spieler 2: unverändert (`default`)

- [ ] **Step 7: MCP Screenshot**

Nutze `mcp__godot-mcp__scene` Screenshot-Tool für visuelle Dokumentation:
- DM-Ansicht gesamt: Top-Down-Karte + Player-Cam-Feeds unten
- DM-Ansicht OG mit Spieler-Markern

- [ ] **Step 8: docs/STATUS.md und CLAUDE.md updaten**

In `docs/STATUS.md`:
- Schritt 6 (DM Regiepult Basics) auf ✅ setzen
- "In Arbeit" leeren
- Schritt 7 (Refectorium) als "Als nächstes" eintragen

In `ruf-der-pilze/CLAUDE.md`:
- Entwicklungs-Reihenfolge: Schritt 6 auf ✅

- [ ] **Step 9: Finaler Commit**

```bash
git add docs/STATUS.md ruf-der-pilze/CLAUDE.md
git commit -m "docs: mark DM Regiepult Basics complete, update STATUS"
```
