docs: add tavern lobby implementation plan
This commit is contained in:
456
docs/plans/2026-04-14-tavern-lobby-plan.md
Normal file
456
docs/plans/2026-04-14-tavern-lobby-plan.md
Normal file
@@ -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"
|
||||
```
|
||||
Reference in New Issue
Block a user