# Tavern Lobby — Design Spec

**Date:** 2026-04-14
**Status:** Approved
**Scope:** Replace flat 2D join UI (lobby.tscn) with an atmospheric 3D tavern placeholder scene

---

## Goal

The current lobby is a plain 2D UI (name input, role dropdown, player list). This spec replaces it with a minimal 3D tavern scene that serves as the atmospheric waiting room while players connect. The join UI becomes an overlay on top of the 3D scene.

This is a placeholder — character models, round table seating, and animations come in a future feature.

---

## Scene Structure

```
main.tscn  (Root — unchanged except CurrentScene node added)
└── CurrentScene (Node)
    └── tavern.tscn
        ├── Node3D (Root)
        │   ├── Camera3D           — fixed atmospheric angle
        │   ├── WorldEnvironment   — fog + ambient light
        │   ├── OmniLight3D        — warm candlelight (orange/yellow)
        │   └── MeshInstance3D ×5  — floor, ceiling, 3 walls (BoxMesh)
        └── CanvasLayer
            ├── JoinPanel          — name input + role dropdown (unchanged logic)
            └── WaitPanel          — player list + start button (unchanged logic)
```

**New Autoload:** `SceneManager` (`scripts/scene_manager.gd`)

---

## SceneManager

Single responsibility: load and swap the scene inside `main/CurrentScene`.

```gdscript
# Public API
SceneManager.transition_to("tavern")       # initial load
SceneManager.transition_to("chamber")      # next feature (adventure start)
SceneManager.transition_to("entrance_hall")
SceneManager.transition_to("refectory")
# etc.
```

**Startup:** `SceneManager._ready()` loads `tavern.tscn` into `CurrentScene` — but only on client instances. The dedicated server must not load any 3D scene:

```gdscript
func _ready() -> void:
    # Check both arg lists: user args (after --) and engine args (before --)
    # so --server works with or without the -- separator.
    var args := OS.get_cmdline_args() + OS.get_cmdline_user_args()
    var is_server := OS.has_feature("dedicated_server") or "--server" in args
    if not is_server:
        transition_to("tavern")
```

**`game_started` ownership:** `SceneManager` is the sole listener for `NetworkManager.game_started`. `tavern.gd` does NOT connect to this signal — that connection is removed compared to `lobby.gd`. On `game_started`, `SceneManager` logs a placeholder; the actual scene swap to `chamber` comes in the next feature.

**Role-based routing (stub, not active):**
```gdscript
func _load_for_role(scene_name: String) -> void:
    # my_id is only valid after NetworkManager._on_connected_to_server fires.
    # Never call this before game_started, as my_id will be 0.
    var role = NetworkManager.players.get(NetworkManager.my_id, {}).get("role", "player")
    # "dm" → dm variant — activated in next feature
```

**`transition_to` null guard:** The scene registry includes future entries whose `.tscn` files do not yet exist. `transition_to` must guard against missing files:
```gdscript
func transition_to(scene_name: String) -> void:
    var path = SCENES.get(scene_name, "")
    var packed = load(path) if path else null
    if packed == null:
        push_error("[SceneManager] Scene not found: %s" % scene_name)
        return
    # swap CurrentScene child
```

**Scene registry** (internal Dictionary):
```gdscript
const SCENES := {
    "tavern": "res://scenes/tavern.tscn",
    "chamber": "res://scenes/chamber.tscn",       # future
    "entrance_hall": "res://scenes/entrance_hall.tscn",  # future
    "refectory": "res://scenes/refectory.tscn",   # future
    "library": "res://scenes/library.tscn",       # future
    "chapel": "res://scenes/chapel.tscn",          # future
    "cloister": "res://scenes/cloister.tscn",     # future
    "sanctum": "res://scenes/sanctum.tscn",       # future
}
```

---

## Tavern 3D Atmosphere

Minimal placeholder — geometry only, no assets yet.

| Element | Value |
|---|---|
| Room size | ~12 × 8 × 4 m (BoxMesh per wall) |
| Camera | Fixed position, slight downward angle, FOV 70 |
| Ambient light | Warm orange, energy 0.4 |
| OmniLight color | `#e8a44a` (candlelight), energy 1.2, range 10 |
| Fog | `WorldEnvironment` → fog enabled, color `#1a0f00`, depth begin 8, end 20 |
| Wall color | Dark brown (`#2b1a0d`) unshaded material |

---

## UI Overlay (unchanged logic)

`CanvasLayer` sits on top of the 3D scene. `JoinPanel` and `WaitPanel` logic is ported 1:1 from `lobby.gd` into `tavern.gd`. No functional changes.

After joining (`_on_connected`): JoinPanel hides, WaitPanel shows — 3D scene visible in background throughout.

---

## Files Changed / Created

| Action | Path |
|---|---|
| Create | `ruf-der-pilze/scenes/tavern.tscn` |
| Create | `ruf-der-pilze/scripts/tavern.gd` |
| Create | `ruf-der-pilze/scripts/scene_manager.gd` |
| Modify | `ruf-der-pilze/scenes/main.tscn` — add `CurrentScene` node; delete `[node name="Lobby"]` entry and the `ext_resource` referencing `lobby.tscn` |
| Modify | `ruf-der-pilze/project.godot` — add SceneManager autoload, change main scene |
| Delete | `ruf-der-pilze/scenes/lobby.tscn` |
| Delete | `ruf-der-pilze/scripts/lobby.gd` |

---

## Out of Scope

- Character models at the table
- Player position sync in tavern
- Chamber / room scenes (adventure start)
- DM-specific scene variant
- Any animation or audio
