docs: add DM Regiepult Basics plan, update STATUS and CLAUDE.md

Plan covers: GameState autoload, overlay RPCs, camera transform broadcast,
top-down SubViewport (EG/OG), player position markers, live player cam feeds
per SubViewport, overlay-toggle panel.
This commit is contained in:
2026-04-14 02:59:56 +02:00
parent 0ff31813c8
commit f5e3e36747
3 changed files with 770 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
# Ruf der Pilze — Projektstatus
Zuletzt aktualisiert: 2026-04-14
Zuletzt aktualisiert: 2026-04-14 (Plan Schritt 6 fertig)
---
@@ -21,6 +21,8 @@ Zuletzt aktualisiert: 2026-04-14
- **Taverne + Szenen-Wechsel nach Spielstart** — 2-stöckige In-Game-Taverne (Blockout), taproom als Sub-Scene, game_started → Spieler in Zimmer, DM in Stub-Szene
Spec: `docs/superpowers/specs/2026-04-14-tavern-scene-design.md`
Plan: `docs/plans/2026-04-14-tavern-scene-plan.md`
- **DM Regiepult Basics** — Top-Down-Karte (EG/OG), Spieler-Positionsmarker, Live-Cam-Feeds pro Spieler, Overlay-Toggle
Plan: `docs/plans/2026-04-14-dm-regiepult-basics.md`
---
@@ -32,6 +34,7 @@ Zuletzt aktualisiert: 2026-04-14
4. ✅ Tavern Lobby — 3D-Taverne als Warteraum, SceneManager Autoload
5. ⏳ Szenen-Wechsel nach Spielstart — Chamber/DM-Szene nach game_started
6. ⏳ DM Regiepult Basics — Overlay-Toggle, Top-Down pro Etage, Player-Cams
Plan: `docs/plans/2026-04-14-dm-regiepult-basics.md`
7. ⏳ Refectorium — asymmetrische Wahrnehmung (erster Raum)
8. ⏳ Alle Räume aufbauen
9. ⏳ Polish — Audio, Nebel, Licht, Würfel-UI

View File

@@ -0,0 +1,765 @@
# 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"
```

View File

@@ -238,6 +238,7 @@ Verzeichnis: `../Anna_Model/` (außerhalb von `ruf-der-pilze/`, im Repo-Root)
Kein separates "chamber" — Zimmer sind Teil von tavern.tscn
Plan: ../docs/plans/2026-04-14-tavern-scene-plan.md
6. ⏳ DM Regiepult Basics — Overlay-Toggle, Top-Down pro Etage, Player-Cams
Plan: `../docs/plans/2026-04-14-dm-regiepult-basics.md`
7. ⏳ Erster Raum — Refectory mit asymmetrischer Wahrnehmung
8. ⏳ Alle Räume aufbauen
9. ⏳ Polish — Audio, Nebel, Licht, Würfel-UI