diff --git a/docs/STATUS.md b/docs/STATUS.md index fa540a9..8b8685f 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,6 +1,6 @@ # Ruf der Pilze — Projektstatus -Zuletzt aktualisiert: 2026-04-16 (Schritt 11 abgeschlossen — Sporennebel Slider; alle Mechaniken fertig) +Zuletzt aktualisiert: 2026-04-16 (Schritt 12 abgeschlossen — Lite Charakterbogen) --- @@ -18,6 +18,8 @@ Zuletzt aktualisiert: 2026-04-16 (Schritt 11 abgeschlossen — Sporennebel Slide game_started → Spieler in zugewiesenem Zimmer (SpawnPoint), DM in dm_view.tscn Stub - **DM Regiepult Basics** — GameState Autoload, Overlay-RPC-Kette, EG+OG Top-Down nebeneinander, Spieler-Positionsmarker (etagen-gefiltert), Live-Cam-Feeds pro Spieler, Overlay-Toggle-Panel +- **Lite Charakterbogen** — Rasse/Klasse OptionButtons, 6 Ability-Modifier SpinBoxen, 6 Archetype-Quick-Fill-Buttons; + Auto-Sync via `request_update_character` RPC, `character_updated` Signal in NetworkManager ### 🔄 In Arbeit @@ -47,7 +49,7 @@ Zuletzt aktualisiert: 2026-04-16 (Schritt 11 abgeschlossen — Sporennebel Slide ### Charaktere + Assets -12. ⏳ Lite Charakterbogen in Lobby — CharName, Rasse, Klasse, 6 Skill-Modifier; Archetypes für Dev +12. ✅ Lite Charakterbogen in Lobby — Rasse/Klasse OptionButtons, 6 Modifier-SpinBoxen, 6 Archetype-Buttons; RPC-Sync 13. ⏳ Charakter-Platzhalter — farbige Capsules pro Archetyp, austauschbar 14. ⏳ Tavern Art Pass — echte Props (Tische, Stühle, Tresen, Betten, Fässer, Kerzen) via Polyhaven/Sketchfab/Meshy diff --git a/ruf-der-pilze/CLAUDE.md b/ruf-der-pilze/CLAUDE.md index 065fac3..d8096d3 100644 --- a/ruf-der-pilze/CLAUDE.md +++ b/ruf-der-pilze/CLAUDE.md @@ -251,7 +251,7 @@ Verzeichnis: `../Anna_Model/` (außerhalb von `ruf-der-pilze/`, im Repo-Root) 10. ✅ Spore Overlay — spore_overlay.gdshader, overlay_changed signal, CanvasLayer layer=10 11. ✅ Sporennebel Slider — SporeLevel Autoload, request_spore_level RPC, HSlider im DM-Panel --- Charaktere + Assets --- -12. ⏳ Lite Charakterbogen — CharName, Rasse, Klasse, 6 Skill-Modifier; Archetypes für Dev +12. ✅ Lite Charakterbogen — Rasse/Klasse OptionButtons, 6 Modifier-SpinBoxen, 6 Archetype-Buttons; RPC-Sync 13. ⏳ Charakter-Platzhalter — farbige Capsules pro Archetyp 14. ⏳ Tavern Art Pass — echte Props via Polyhaven/Sketchfab/Meshy AI --- Klosterwelt --- @@ -271,7 +271,6 @@ Verzeichnis: `../Anna_Model/` (außerhalb von `ruf-der-pilze/`, im Repo-Root) 26. ⏳ Aktions-Selektor — klassen-spezifische Zauber/Angriff-Effekte (nur visuell) 27. ⏳ Full Spore Effects — Nebel-Partikel, Bewegungsverzerrung 28. ⏳ Soundscape + Spieluhr-Melodie — direktionaler Audio, Annas Lied - --- ## Arbeitsweise diff --git a/ruf-der-pilze/scenes/tavern_lobby.tscn b/ruf-der-pilze/scenes/tavern_lobby.tscn index e4fd68c..be4675b 100644 --- a/ruf-der-pilze/scenes/tavern_lobby.tscn +++ b/ruf-der-pilze/scenes/tavern_lobby.tscn @@ -48,3 +48,103 @@ custom_minimum_size = Vector2(0, 150) [node name="StartButton" type="Button" parent="CanvasLayer/WaitPanel"] text = "Spiel starten" visible = false + +[node name="CharSheet" type="VBoxContainer" parent="CanvasLayer/WaitPanel"] +visible = false + +[node name="LblChar" type="Label" parent="CanvasLayer/WaitPanel/CharSheet"] +text = "Charakter" + +[node name="CharGrid" type="GridContainer" parent="CanvasLayer/WaitPanel/CharSheet"] +columns = 2 + +[node name="LblRace" type="Label" parent="CanvasLayer/WaitPanel/CharSheet/CharGrid"] +text = "Rasse" + +[node name="RaceOption" type="OptionButton" parent="CanvasLayer/WaitPanel/CharSheet/CharGrid"] + +[node name="LblClass" type="Label" parent="CanvasLayer/WaitPanel/CharSheet/CharGrid"] +text = "Klasse" + +[node name="ClassOption" type="OptionButton" parent="CanvasLayer/WaitPanel/CharSheet/CharGrid"] + +[node name="HSeparator" type="HSeparator" parent="CanvasLayer/WaitPanel/CharSheet"] + +[node name="LblModifiers" type="Label" parent="CanvasLayer/WaitPanel/CharSheet"] +text = "Werte" + +[node name="ModGrid" type="GridContainer" parent="CanvasLayer/WaitPanel/CharSheet"] +columns = 6 + +[node name="LblSTR" type="Label" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +text = "STR" + +[node name="LblDEX" type="Label" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +text = "DEX" + +[node name="LblCON" type="Label" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +text = "CON" + +[node name="LblINT" type="Label" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +text = "INT" + +[node name="LblWIS" type="Label" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +text = "WIS" + +[node name="LblCHA" type="Label" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +text = "CHA" + +[node name="SpinSTR" type="SpinBox" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +min_value = -5.0 +max_value = 10.0 +value = 0.0 + +[node name="SpinDEX" type="SpinBox" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +min_value = -5.0 +max_value = 10.0 +value = 0.0 + +[node name="SpinCON" type="SpinBox" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +min_value = -5.0 +max_value = 10.0 +value = 0.0 + +[node name="SpinINT" type="SpinBox" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +min_value = -5.0 +max_value = 10.0 +value = 0.0 + +[node name="SpinWIS" type="SpinBox" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +min_value = -5.0 +max_value = 10.0 +value = 0.0 + +[node name="SpinCHA" type="SpinBox" parent="CanvasLayer/WaitPanel/CharSheet/ModGrid"] +min_value = -5.0 +max_value = 10.0 +value = 0.0 + +[node name="HSeparator2" type="HSeparator" parent="CanvasLayer/WaitPanel/CharSheet"] + +[node name="LblArchetype" type="Label" parent="CanvasLayer/WaitPanel/CharSheet"] +text = "Schnell-Auswahl (Dev)" + +[node name="ArchetypeRow" type="HBoxContainer" parent="CanvasLayer/WaitPanel/CharSheet"] + +[node name="BtnKrieger" type="Button" parent="CanvasLayer/WaitPanel/CharSheet/ArchetypeRow"] +text = "Krieger" + +[node name="BtnMagier" type="Button" parent="CanvasLayer/WaitPanel/CharSheet/ArchetypeRow"] +text = "Magier" + +[node name="BtnKleriker" type="Button" parent="CanvasLayer/WaitPanel/CharSheet/ArchetypeRow"] +text = "Kleriker" + +[node name="BtnSchurke" type="Button" parent="CanvasLayer/WaitPanel/CharSheet/ArchetypeRow"] +text = "Schurke" + +[node name="BtnDruide" type="Button" parent="CanvasLayer/WaitPanel/CharSheet/ArchetypeRow"] +text = "Druide" + +[node name="BtnBarbar" type="Button" parent="CanvasLayer/WaitPanel/CharSheet/ArchetypeRow"] +text = "Barbar" diff --git a/ruf-der-pilze/scripts/network_manager.gd b/ruf-der-pilze/scripts/network_manager.gd index b45c638..392c60b 100644 --- a/ruf-der-pilze/scripts/network_manager.gd +++ b/ruf-der-pilze/scripts/network_manager.gd @@ -16,6 +16,7 @@ signal player_list_synced() signal game_started() signal roll_received(roller_peer_id: int, player_name: String, d20_result: int, modifier: int, total: int) signal overlay_changed(overlay_name: String) +signal character_updated(peer_id: int, data: Dictionary) func start_server(port: int, max_clients: int) -> void: @@ -197,3 +198,21 @@ func _relay_roll(roller_peer_id: int, d20_result: int, modifier: int) -> void: var player_name: String = players.get(roller_peer_id, {}).get("name", "???") var total := d20_result + modifier roll_received.emit(roller_peer_id, player_name, d20_result, modifier, total) + + +# Client → Server +@rpc("any_peer", "call_remote", "reliable") +func request_update_character(data: Dictionary) -> void: + if not multiplayer.is_server(): return + var peer_id := multiplayer.get_remote_sender_id() + if not players.has(peer_id): return + players[peer_id]["char"] = data + _relay_character_update.rpc(peer_id, data) + + +# Server → All +@rpc("authority", "call_local", "reliable") +func _relay_character_update(peer_id: int, data: Dictionary) -> void: + if players.has(peer_id): + players[peer_id]["char"] = data + character_updated.emit(peer_id, data) diff --git a/ruf-der-pilze/scripts/tavern_lobby.gd b/ruf-der-pilze/scripts/tavern_lobby.gd index 6c7c3c4..198afb6 100644 --- a/ruf-der-pilze/scripts/tavern_lobby.gd +++ b/ruf-der-pilze/scripts/tavern_lobby.gd @@ -3,6 +3,63 @@ extends Node3D var _local_role: String = "" var _pending_player_name: String = "" +const ARCHETYPES := { + "Krieger": {"race": "Mensch", "class": "Krieger", "STR": 3, "DEX": 1, "CON": 2, "INT": 0, "WIS": 0, "CHA": 0}, + "Magier": {"race": "Elf", "class": "Magier", "STR":-1, "DEX": 2, "CON": 0, "INT": 4, "WIS": 2, "CHA": 1}, + "Kleriker": {"race": "Zwerg", "class": "Kleriker", "STR": 1, "DEX": 0, "CON": 2, "INT": 1, "WIS": 3, "CHA": 0}, + "Schurke": {"race": "Halbelf", "class": "Schurke", "STR": 0, "DEX": 4, "CON": 1, "INT": 1, "WIS": 1, "CHA": 2}, + "Druide": {"race": "Mensch", "class": "Druide", "STR": 0, "DEX": 1, "CON": 1, "INT": 1, "WIS": 3, "CHA": 0}, + "Barbar": {"race": "Halbork", "class": "Barbar", "STR": 4, "DEX": 2, "CON": 3, "INT":-1, "WIS": 0, "CHA":-1}, +} + +func _collect_char_data() -> Dictionary: + var race_opt := $CanvasLayer/WaitPanel/CharSheet/CharGrid/RaceOption as OptionButton + var class_opt := $CanvasLayer/WaitPanel/CharSheet/CharGrid/ClassOption as OptionButton + return { + "race": race_opt.get_item_text(race_opt.selected), + "class": class_opt.get_item_text(class_opt.selected), + "STR": int($CanvasLayer/WaitPanel/CharSheet/ModGrid/SpinSTR.value), + "DEX": int($CanvasLayer/WaitPanel/CharSheet/ModGrid/SpinDEX.value), + "CON": int($CanvasLayer/WaitPanel/CharSheet/ModGrid/SpinCON.value), + "INT": int($CanvasLayer/WaitPanel/CharSheet/ModGrid/SpinINT.value), + "WIS": int($CanvasLayer/WaitPanel/CharSheet/ModGrid/SpinWIS.value), + "CHA": int($CanvasLayer/WaitPanel/CharSheet/ModGrid/SpinCHA.value), + } + +func _send_char_data() -> void: + NetworkManager.request_update_character.rpc_id(1, _collect_char_data()) + +func _setup_char_sheet() -> void: + var race_opt := $CanvasLayer/WaitPanel/CharSheet/CharGrid/RaceOption as OptionButton + var class_opt := $CanvasLayer/WaitPanel/CharSheet/CharGrid/ClassOption as OptionButton + for race in ["Mensch", "Elf", "Zwerg", "Halbork", "Halbelf", "Tiefling"]: + race_opt.add_item(race) + for cls in ["Krieger", "Magier", "Kleriker", "Schurke", "Druide", "Barbar"]: + class_opt.add_item(cls) + for archetype_name: String in ARCHETYPES: + var btn := $CanvasLayer/WaitPanel/CharSheet/ArchetypeRow.get_node( + NodePath("Btn" + archetype_name)) as Button + var preset: Dictionary = ARCHETYPES[archetype_name] + btn.pressed.connect(func(): + var r_opt := $CanvasLayer/WaitPanel/CharSheet/CharGrid/RaceOption as OptionButton + var c_opt := $CanvasLayer/WaitPanel/CharSheet/CharGrid/ClassOption as OptionButton + for i in r_opt.item_count: + if r_opt.get_item_text(i) == preset["race"]: + r_opt.selected = i + for i in c_opt.item_count: + if c_opt.get_item_text(i) == preset["class"]: + c_opt.selected = i + for stat: String in ["STR", "DEX", "CON", "INT", "WIS", "CHA"]: + ($CanvasLayer/WaitPanel/CharSheet/ModGrid.get_node( + NodePath("Spin" + stat)) as SpinBox).value = preset[stat] + _send_char_data() + ) + for spin_name: String in ["SpinSTR", "SpinDEX", "SpinCON", "SpinINT", "SpinWIS", "SpinCHA"]: + ($CanvasLayer/WaitPanel/CharSheet/ModGrid.get_node(NodePath(spin_name)) as SpinBox + ).value_changed.connect(func(_v: float): _send_char_data()) + (race_opt as OptionButton).item_selected.connect(func(_i: int): _send_char_data()) + (class_opt as OptionButton).item_selected.connect(func(_i: int): _send_char_data()) + func _ready() -> void: $CanvasLayer/JoinPanel/RoleOption.add_item("Spieler") @@ -16,6 +73,7 @@ func _ready() -> void: NetworkManager.player_joined.connect(_on_player_joined) NetworkManager.player_left.connect(_on_player_left) NetworkManager.player_list_synced.connect(_rebuild_player_list) + _setup_char_sheet() func _on_join_pressed() -> void: @@ -32,6 +90,7 @@ func _on_connected() -> void: $CanvasLayer/JoinPanel.visible = false $CanvasLayer/WaitPanel.visible = true $CanvasLayer/WaitPanel/StartButton.visible = (_local_role == "dm") + $CanvasLayer/WaitPanel/CharSheet.visible = true func _exit_tree() -> void: