From f5e3e36747fdf6a58338ee22fc266da2cfe3ce37 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 14 Apr 2026 02:59:56 +0200 Subject: [PATCH] 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. --- docs/STATUS.md | 5 +- docs/plans/2026-04-14-dm-regiepult-basics.md | 765 +++++++++++++++++++ ruf-der-pilze/CLAUDE.md | 1 + 3 files changed, 770 insertions(+), 1 deletion(-) create mode 100644 docs/plans/2026-04-14-dm-regiepult-basics.md diff --git a/docs/STATUS.md b/docs/STATUS.md index 02aa6ab..c657124 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -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 diff --git a/docs/plans/2026-04-14-dm-regiepult-basics.md b/docs/plans/2026-04-14-dm-regiepult-basics.md new file mode 100644 index 0000000..1d41405 --- /dev/null +++ b/docs/plans/2026-04-14-dm-regiepult-basics.md @@ -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 ` + +- [ ] **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 )` + +- [ ] **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 : spore_active` +- Spieler-Console: `[GameState] Overlay für → 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 ` 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 → 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" +``` diff --git a/ruf-der-pilze/CLAUDE.md b/ruf-der-pilze/CLAUDE.md index 5999017..8c7744a 100644 --- a/ruf-der-pilze/CLAUDE.md +++ b/ruf-der-pilze/CLAUDE.md @@ -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