From 90fa17b8bb5cd72b244842ddbb603aba3760cb57 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 14 Apr 2026 01:10:58 +0200 Subject: [PATCH] docs: add tavern lobby implementation plan --- docs/plans/2026-04-14-tavern-lobby-plan.md | 456 +++++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 docs/plans/2026-04-14-tavern-lobby-plan.md diff --git a/docs/plans/2026-04-14-tavern-lobby-plan.md b/docs/plans/2026-04-14-tavern-lobby-plan.md new file mode 100644 index 0000000..8f96b05 --- /dev/null +++ b/docs/plans/2026-04-14-tavern-lobby-plan.md @@ -0,0 +1,456 @@ +# Tavern Lobby 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:** Replace the flat 2D join UI (lobby.tscn) with an atmospheric 3D tavern scene where the join UI floats as a CanvasLayer overlay, and introduce a SceneManager Autoload that will handle all future scene transitions. + +**Architecture:** A new `SceneManager` Autoload owns a `CurrentScene` node in `main.tscn` and swaps scenes into it via `transition_to(scene_name)`. The tavern scene is a `Node3D` with BoxMesh geometry, warm lighting, and fog, with the existing join UI ported to a `CanvasLayer` on top. The `game_started` signal is handled exclusively by `SceneManager` (not by `tavern.gd`). + +**Tech Stack:** Godot 4.6 (GDScript), ENet MultiplayerAPI, existing `NetworkManager` Autoload + +**Spec:** `docs/superpowers/specs/2026-04-14-tavern-lobby-design.md` + +--- + +## File Map + +| Action | File | Responsibility | +|---|---|---| +| Create | `ruf-der-pilze/scripts/scene_manager.gd` | Scene registry, transition logic, `game_started` handler | +| Create | `ruf-der-pilze/scenes/tavern.tscn` | 3D tavern geometry + CanvasLayer UI | +| Create | `ruf-der-pilze/scripts/tavern.gd` | Join/wait UI logic (ported from lobby.gd) | +| Modify | `ruf-der-pilze/project.godot` | Register SceneManager autoload | +| Modify | `ruf-der-pilze/scenes/main.tscn` | Add CurrentScene node, remove Lobby | +| Delete | `ruf-der-pilze/scenes/lobby.tscn` | Replaced by tavern.tscn | +| Delete | `ruf-der-pilze/scripts/lobby.gd` | Replaced by tavern.gd | + +--- + +## Task 1: SceneManager Autoload + +**Files:** +- Create: `ruf-der-pilze/scripts/scene_manager.gd` +- Modify: `ruf-der-pilze/project.godot` + +- [ ] **Step 1: Create `scene_manager.gd`** + +```gdscript +extends Node + +const SCENES := { + "tavern": "res://scenes/tavern.tscn", + "chamber": "res://scenes/chamber.tscn", + "entrance_hall": "res://scenes/entrance_hall.tscn", + "refectory": "res://scenes/refectory.tscn", + "library": "res://scenes/library.tscn", + "chapel": "res://scenes/chapel.tscn", + "cloister": "res://scenes/cloister.tscn", + "sanctum": "res://scenes/sanctum.tscn", +} + +var _current_scene_node: Node = null + + +func _ready() -> void: + var is_server := OS.has_feature("dedicated_server") \ + or "--server" in OS.get_cmdline_user_args() + if not is_server: + call_deferred("transition_to", "tavern") + NetworkManager.game_started.connect(_on_game_started) + + +func transition_to(scene_name: String) -> void: + var path: String = SCENES.get(scene_name, "") + var packed: PackedScene = load(path) if path else null + if packed == null: + push_error("[SceneManager] Scene not found: %s" % scene_name) + return + if _current_scene_node != null: + _current_scene_node.queue_free() + _current_scene_node = null + _current_scene_node = packed.instantiate() + get_node("/root/Main/CurrentScene").add_child(_current_scene_node) + + +func _on_game_started() -> void: + print("[SceneManager] Spiel gestartet — chamber transition kommt im nächsten Feature") + + +func _load_for_role(scene_name: String) -> void: + # my_id is only valid after NetworkManager._on_connected_to_server fires. + # Do not call before game_started — my_id will be 0 before that. + var role: String = NetworkManager.players.get(NetworkManager.my_id, {}).get("role", "player") + # TODO next feature: load dm variant when role == "dm" + print("[SceneManager] Role %s → %s (stub, not switching yet)" % [role, scene_name]) +``` + +- [ ] **Step 2: Register SceneManager in `project.godot`** + +In the `[autoload]` section, add after the `NetworkManager` line: +``` +SceneManager="*res://scripts/scene_manager.gd" +``` + +Result: +```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" +``` + +- [ ] **Step 3: Smoke-test server mode** + +Run: +```bash +cd ruf-der-pilze && godot --headless -- --server +``` + +Expected output (no errors, no scene loading): +``` +[Server] Gestartet auf Port 4242 +``` +SceneManager must NOT print anything about tavern on server. If it does, the `is_server` guard is broken. + +- [ ] **Step 4: Commit** + +```bash +git add ruf-der-pilze/scripts/scene_manager.gd ruf-der-pilze/project.godot +git commit -m "feat: add SceneManager autoload with scene registry" +``` + +--- + +## Task 2: Update `main.tscn` + +**Files:** +- Modify: `ruf-der-pilze/scenes/main.tscn` + +Current content has a `[node name="Lobby"]` child and an `ext_resource` referencing `lobby.tscn`. Both must be removed. A `CurrentScene` node is added instead. + +- [ ] **Step 1: Replace `main.tscn` content** + +``` +[gd_scene load_steps=1 format=3] + +[ext_resource type="Script" path="res://scripts/main.gd" id="1"] + +[node name="Main" type="Node"] +script = ExtResource("1") + +[node name="CurrentScene" type="Node" parent="."] +``` + +- [ ] **Step 2: Commit** + +```bash +git add ruf-der-pilze/scenes/main.tscn +git commit -m "refactor: replace Lobby child with CurrentScene in main.tscn" +``` + +> Note: Do NOT run the game yet — `tavern.tscn` doesn't exist, so SceneManager will push_error. That's expected. Continue to Task 3. + +--- + +## Task 3: Create `tavern.gd` + +**Files:** +- Create: `ruf-der-pilze/scripts/tavern.gd` + +Port from `lobby.gd` with two changes: +1. All node paths updated: `$JoinPanel` → `$CanvasLayer/JoinPanel` (etc.) +2. `NetworkManager.game_started` connection and `_on_game_started()` removed (SceneManager owns that signal) + +- [ ] **Step 1: Create `tavern.gd`** + +```gdscript +extends Node3D + +var _local_role: String = "" +var _pending_player_name: String = "" + + +func _ready() -> void: + $CanvasLayer/JoinPanel/RoleOption.add_item("Spieler") + $CanvasLayer/JoinPanel/RoleOption.add_item("DM") + + $CanvasLayer/JoinPanel/JoinButton.pressed.connect(_on_join_pressed) + $CanvasLayer/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) + + +func _on_join_pressed() -> void: + var player_name: String = ($CanvasLayer/JoinPanel/NameInput as LineEdit).text.strip_edges() + if player_name.is_empty(): + return + _local_role = "dm" if $CanvasLayer/JoinPanel/RoleOption.selected == 1 else "player" + _pending_player_name = player_name + NetworkManager.join_server("127.0.0.1", 4242) + + +func _on_connected() -> void: + NetworkManager.register.rpc_id(1, _pending_player_name, _local_role) + $CanvasLayer/JoinPanel.visible = false + $CanvasLayer/WaitPanel.visible = true + $CanvasLayer/WaitPanel/StartButton.visible = (_local_role == "dm") + + +func _on_connection_failed() -> void: + push_error("[Tavern] Verbindung fehlgeschlagen") + _local_role = "" + _pending_player_name = "" + + +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: + $CanvasLayer/WaitPanel/PlayerList.clear() + for p in NetworkManager.players.values(): + $CanvasLayer/WaitPanel/PlayerList.add_item("%s (%s)" % [p.name, p.role]) + + +func _on_start_pressed() -> void: + NetworkManager.request_start_game.rpc_id(1) +``` + +- [ ] **Step 2: Commit** + +```bash +git add ruf-der-pilze/scripts/tavern.gd +git commit -m "feat: add tavern.gd (port of lobby.gd with CanvasLayer paths)" +``` + +--- + +## Task 4: Create `tavern.tscn` + +**Files:** +- Create: `ruf-der-pilze/scenes/tavern.tscn` + +3D room (12 × 8 × 4 m) with warm candlelight and fog. UI ported from `lobby.tscn` into a `CanvasLayer`. + +Room geometry: +- Floor: BoxMesh 12 × 0.1 × 8, at y = −0.05 +- Ceiling: BoxMesh 12 × 0.1 × 8, at y = 4.05 +- Back wall: BoxMesh 12 × 4 × 0.1, at z = −4 +- Left wall: BoxMesh 0.1 × 4 × 8, at x = −6 +- Right wall: BoxMesh 0.1 × 4 × 8, at x = 6 + +Camera: position (0, 3, 6), rotated −18° on X axis (looking slightly downward into the room). + +- [ ] **Step 1: Create `tavern.tscn`** + +``` +[gd_scene load_steps=9 format=3] + +[ext_resource type="Script" path="res://scripts/tavern.gd" id="1"] + +[sub_resource type="Environment" id="Environment_1"] +background_mode = 1 +background_color = Color(0.102, 0.059, 0, 1) +ambient_light_color = Color(0.6, 0.35, 0.1, 1) +ambient_light_energy = 0.4 +fog_enabled = true +fog_light_color = Color(0.102, 0.059, 0, 1) +fog_density = 0.05 + +[sub_resource type="StandardMaterial3D" id="Material_1"] +albedo_color = Color(0.169, 0.102, 0.051, 1) + +[sub_resource type="BoxMesh" id="BoxMesh_floor"] +size = Vector3(12, 0.1, 8) + +[sub_resource type="BoxMesh" id="BoxMesh_ceiling"] +size = Vector3(12, 0.1, 8) + +[sub_resource type="BoxMesh" id="BoxMesh_back"] +size = Vector3(12, 4, 0.1) + +[sub_resource type="BoxMesh" id="BoxMesh_left"] +size = Vector3(0.1, 4, 8) + +[sub_resource type="BoxMesh" id="BoxMesh_right"] +size = Vector3(0.1, 4, 8) + +[node name="Tavern" type="Node3D"] +script = ExtResource("1") + +[node name="Camera3D" type="Camera3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 0.951, 0.309, 0, -0.309, 0.951, 0, 3, 6) +fov = 70.0 +current = true + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_1") + +[node name="OmniLight3D" type="OmniLight3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0) +light_color = Color(0.91, 0.643, 0.29, 1) +light_energy = 1.2 +omni_range = 10.0 + +[node name="Floor" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0) +mesh = SubResource("BoxMesh_floor") +surface_material_override/0 = SubResource("Material_1") + +[node name="Ceiling" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.05, 0) +mesh = SubResource("BoxMesh_ceiling") +surface_material_override/0 = SubResource("Material_1") + +[node name="WallBack" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, -4) +mesh = SubResource("BoxMesh_back") +surface_material_override/0 = SubResource("Material_1") + +[node name="WallLeft" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 2, 0) +mesh = SubResource("BoxMesh_left") +surface_material_override/0 = SubResource("Material_1") + +[node name="WallRight" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 2, 0) +mesh = SubResource("BoxMesh_right") +surface_material_override/0 = SubResource("Material_1") + +[node name="CanvasLayer" type="CanvasLayer" parent="."] + +[node name="JoinPanel" type="VBoxContainer" parent="CanvasLayer"] +offset_left = 300.0 +offset_top = 200.0 +offset_right = 700.0 +offset_bottom = 500.0 + +[node name="Title" type="Label" parent="CanvasLayer/JoinPanel"] +text = "Ruf der Pilze" + +[node name="NameInput" type="LineEdit" parent="CanvasLayer/JoinPanel"] +placeholder_text = "Dein Name" + +[node name="RoleOption" type="OptionButton" parent="CanvasLayer/JoinPanel"] + +[node name="JoinButton" type="Button" parent="CanvasLayer/JoinPanel"] +text = "Beitreten" + +[node name="WaitPanel" type="VBoxContainer" parent="CanvasLayer"] +visible = false +offset_left = 300.0 +offset_top = 200.0 +offset_right = 700.0 +offset_bottom = 500.0 + +[node name="WaitLabel" type="Label" parent="CanvasLayer/WaitPanel"] +text = "Warte auf Spieler..." + +[node name="PlayerList" type="ItemList" parent="CanvasLayer/WaitPanel"] +custom_minimum_size = Vector2(0, 150) + +[node name="StartButton" type="Button" parent="CanvasLayer/WaitPanel"] +text = "Spiel starten" +visible = false +``` + +> Note: If Godot logs a `load_steps` mismatch warning on first run, open the scene in the editor once — it will correct the value automatically on save. + +- [ ] **Step 2: Commit** + +```bash +git add ruf-der-pilze/scenes/tavern.tscn +git commit -m "feat: add tavern.tscn — 3D room with CanvasLayer UI overlay" +``` + +--- + +## Task 5: End-to-End Verification + +- [ ] **Step 1: Run server** + +```bash +cd ruf-der-pilze && godot --headless -- --server +``` + +Expected: `[Server] Gestartet auf Port 4242` — no 3D scene loading, no errors. + +- [ ] **Step 2: Run client** + +In a second terminal: +```bash +cd ruf-der-pilze && godot +``` + +Expected: +- Dark brown 3D room visible in background +- Warm orange lighting from center +- Fog visible toward the back wall +- JoinPanel (Name field, Role dropdown, "Beitreten" button) floating on top + +- [ ] **Step 3: Join as player** + +Enter a name, select "Spieler", click "Beitreten". + +Expected: +- JoinPanel disappears +- WaitPanel appears with player list +- "Spiel starten" button is NOT visible (player role) +- Server console: `[Server] Peer verbunden: ...` + +- [ ] **Step 4: Join as DM** + +Open a third terminal, run another client. Enter a name, select "DM", join. + +Expected: +- Both clients show updated player list +- DM client sees "Spiel starten" button + +- [ ] **Step 5: Start game** + +DM clicks "Spiel starten". + +Expected (all consoles): +``` +[SceneManager] Spiel gestartet — chamber transition kommt im nächsten Feature +``` +No crash. No scene change yet (that's the next feature). + +- [ ] **Step 6: Delete legacy files** + +```bash +git rm ruf-der-pilze/scenes/lobby.tscn ruf-der-pilze/scripts/lobby.gd +``` + +- [ ] **Step 7: Re-run full verification** (repeat Steps 1–5 to confirm deletion didn't break anything) + +- [ ] **Step 8: Update `docs/STATUS.md`** + +Mark "Tavern Lobby" as done, add "Szenen-Wechsel (Chamber)" as next: + +```markdown +### ✅ Abgeschlossen +- ... +- **Tavern Lobby** — 3D tavern replaces flat lobby UI; SceneManager Autoload introduced + +### ⏳ Als nächstes +- **Szenen-Wechsel nach Spielstart** — game_started → transition to chamber/DM scene + Plan: noch zu erstellen +``` + +- [ ] **Step 9: Archive plan, final commit** + +```bash +mv docs/plans/2026-04-14-tavern-lobby-plan.md docs/plans/archive/ +git add -A +git commit -m "feat: replace lobby with 3D tavern scene and SceneManager" +```