docs: fix plan issues — RPC relay, lambda order, class_name, INTERACT_DISTANCE step
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
**Goal:** Add a d20 roller to the player HUD; results are broadcast to all connected peers; DM sees a DC input field in the Regiepult.
|
||||
|
||||
**Architecture:** Two new RPCs in `network_manager.gd` handle roll broadcast. Player-side UI lives in a CanvasLayer added dynamically by `tavern.gd`. DM-side DC display is added to `dm_view.tscn`/`dm_view.gd`. No new Autoload. Roll log is local per client (no sync needed beyond the broadcast).
|
||||
**Architecture:** Two new RPCs in `network_manager.gd` handle roll broadcast using the same server-relay pattern as overlay: player sends to server via `rpc_id(1)`, server re-broadcasts to all via `_relay_roll.rpc()`. Player-side UI lives in a CanvasLayer added dynamically by `tavern.gd`. DM-side DC display is added to `dm_view.tscn`/`dm_view.gd`. No new Autoload. Roll log is local per client (no sync needed beyond the broadcast).
|
||||
|
||||
**Tech Stack:** Godot 4.x GDScript, MultiplayerAPI RPC, CanvasLayer UI
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
- `NetworkManager.my_id` is the local peer ID
|
||||
- `tavern.gd._ready()` already skips PlayerController for DM — dice UI must also skip for DM
|
||||
- `dm_view.tscn` SidePanel has `OverlayScroll` — DC section goes below it
|
||||
- Roll flow: Player rolls locally → calls `broadcast_roll.rpc()` → all peers receive → update local log
|
||||
- Roll flow: Player rolls locally → `broadcast_roll.rpc_id(1, ...)` → server receives → `_relay_roll.rpc()` → all peers receive + emit `roll_received` signal → update local log
|
||||
- In ENet star topology, clients cannot directly address all peers — all broadcasts must go through the server. Same pattern as `request_set_overlay` → `set_overlay`.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `scripts/network_manager.gd` | Add `broadcast_roll` RPC |
|
||||
| `scripts/network_manager.gd` | Add `broadcast_roll` RPC (client→server) and `_relay_roll` RPC (server→all) |
|
||||
| `scripts/tavern.gd` | Add dice CanvasLayer + roll logic |
|
||||
| `scenes/dm_view.tscn` | Add DC section to SidePanel |
|
||||
| `scripts/dm_view.gd` | Handle incoming rolls, update DC display |
|
||||
@@ -34,30 +35,31 @@
|
||||
**Files:**
|
||||
- Modify: `scripts/network_manager.gd` — append one RPC
|
||||
|
||||
The roll is trusted from the client (this is a cooperative game, not a competitive one). No server validation needed — simpler and fewer round trips.
|
||||
The roll is trusted from the client (cooperative game, not competitive). Uses the same server-relay pattern as overlays: client → server → server broadcasts to all.
|
||||
|
||||
- [ ] **Step 1: Add RPC at end of network_manager.gd**
|
||||
|
||||
```gdscript
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func broadcast_roll(roller_peer_id: int, d20_result: int, modifier: int) -> void:
|
||||
pass # Handled by whoever connects to this signal via _notification or direct call
|
||||
```
|
||||
|
||||
Wait — GDScript doesn't have signal-from-RPC natively. Instead, emit a signal:
|
||||
- [ ] **Step 1: Add signal and two RPCs at end of network_manager.gd**
|
||||
|
||||
Add the signal near the other signal declarations at the top of the file:
|
||||
```gdscript
|
||||
signal roll_received(roller_peer_id: int, player_name: String, d20_result: int, modifier: int, total: int)
|
||||
```
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
Add two RPCs at the end of the file:
|
||||
```gdscript
|
||||
# Client calls this on server only (rpc_id(1, ...))
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func broadcast_roll(roller_peer_id: int, d20_result: int, modifier: int) -> void:
|
||||
if not multiplayer.is_server(): return
|
||||
_relay_roll.rpc(roller_peer_id, d20_result, modifier)
|
||||
|
||||
# Server broadcasts to all (including itself via call_local)
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
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)
|
||||
```
|
||||
|
||||
Add `signal roll_received(...)` near the other signal declarations at the top of the file.
|
||||
|
||||
- [ ] **Step 2: Open Godot, check for parse errors**
|
||||
|
||||
Project → Reload. No errors in NetworkManager.
|
||||
@@ -119,15 +121,17 @@ The dice panel is a CanvasLayer added programmatically in `tavern.gd`. DM client
|
||||
mod_label.text = " Mod:"
|
||||
hbox.add_child(mod_label)
|
||||
|
||||
# Declare mod_display BEFORE the lambdas that capture it
|
||||
var mod_display := Label.new()
|
||||
mod_display.text = "0"
|
||||
mod_display.custom_minimum_size = Vector2(30, 0)
|
||||
mod_display.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
|
||||
var mod_minus := Button.new()
|
||||
mod_minus.text = "-"
|
||||
mod_minus.pressed.connect(func() -> void: _modifier -= 1; _update_mod_display(mod_display))
|
||||
hbox.add_child(mod_minus)
|
||||
|
||||
var mod_display := Label.new()
|
||||
mod_display.text = "0"
|
||||
mod_display.custom_minimum_size = Vector2(30, 0)
|
||||
mod_display.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
hbox.add_child(mod_display)
|
||||
|
||||
var mod_plus := Button.new()
|
||||
@@ -142,7 +146,7 @@ The dice panel is a CanvasLayer added programmatically in `tavern.gd`. DM client
|
||||
|
||||
func _on_roll_pressed() -> void:
|
||||
var d20 := randi() % 20 + 1
|
||||
NetworkManager.broadcast_roll.rpc(NetworkManager.my_id, d20, _modifier)
|
||||
NetworkManager.broadcast_roll.rpc_id(1, NetworkManager.my_id, d20, _modifier)
|
||||
|
||||
func _on_roll_received(roller_peer_id: int, player_name: String, d20_result: int, modifier: int, total: int) -> void:
|
||||
var mod_str := " %+d" % modifier if modifier != 0 else ""
|
||||
@@ -156,12 +160,14 @@ The dice panel is a CanvasLayer added programmatically in `tavern.gd`. DM client
|
||||
|
||||
- [ ] **Step 2: Call _setup_dice_ui() in _ready() for players only**
|
||||
|
||||
In the existing `_ready()`, after `_spawn_player(room_index)` and before the timer:
|
||||
In the existing `_ready()`, add at the very end — after the timer block. The DM already returns early at the top of `_ready()` (before `_spawn_player` is reached), so this call is DM-safe without any extra guard:
|
||||
|
||||
```gdscript
|
||||
_setup_dice_ui()
|
||||
```
|
||||
|
||||
**Important:** do not add this inside the `if role == "dm": return` block and do not add it before the timer. Place it as the last statement in `_ready()`.
|
||||
|
||||
- [ ] **Step 3: Open Godot, check for parse errors**
|
||||
|
||||
- [ ] **Step 4: Run as player client, click "d20 Würfeln"**
|
||||
|
||||
@@ -99,6 +99,7 @@ Any object in the world that players can interact with (keys, switches, doors) g
|
||||
# Base class for all interactable objects in the world.
|
||||
# Attach to a StaticBody3D or Area3D that has a CollisionShape3D.
|
||||
# Override interact() in subclasses.
|
||||
class_name Interactable
|
||||
extends Node3D
|
||||
|
||||
## Called when a player interacts with this object.
|
||||
@@ -107,6 +108,8 @@ Any object in the world that players can interact with (keys, switches, doors) g
|
||||
pass
|
||||
```
|
||||
|
||||
The `class_name Interactable` declaration is required so subclasses can use `extends Interactable` instead of the fragile string path form.
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
@@ -135,7 +138,6 @@ This script is attached to the `CharacterBody3D` that `tavern.gd` creates. It ha
|
||||
|
||||
var _camera: Camera3D
|
||||
var _raycast: RayCast3D
|
||||
var _interact_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -157,8 +159,6 @@ This script is attached to the `CharacterBody3D` that `tavern.gd` creates. It ha
|
||||
velocity.z = move_dir.z * SPEED
|
||||
move_and_slide()
|
||||
|
||||
_update_interact_hint()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
@@ -181,13 +181,6 @@ This script is attached to the `CharacterBody3D` that `tavern.gd` creates. It ha
|
||||
var target := _raycast.get_collider()
|
||||
if target != null and target.has_method("interact"):
|
||||
target.interact(NetworkManager.my_id)
|
||||
|
||||
|
||||
func _update_interact_hint() -> void:
|
||||
if _interact_label == null:
|
||||
return
|
||||
_interact_label.visible = _raycast.is_colliding() and _raycast.get_collider() != null \
|
||||
and _raycast.get_collider().has_method("interact")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Open Godot, check for parse errors**
|
||||
@@ -204,9 +197,18 @@ This script is attached to the `CharacterBody3D` that `tavern.gd` creates. It ha
|
||||
## Task 4: Update tavern.gd _spawn_player() to use player_controller.gd
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/tavern.gd` — `_spawn_player()` method
|
||||
- Modify: `scripts/tavern.gd` — `_spawn_player()` method only
|
||||
|
||||
- [ ] **Step 1: Replace _spawn_player() in tavern.gd**
|
||||
**Important:** Only replace `_spawn_player()`. Do NOT modify `_ready()`. If this plan is implemented after the Dice Roller plan, `_ready()` will already contain a `_setup_dice_ui()` call that must be preserved.
|
||||
|
||||
- [ ] **Step 1: Add INTERACT_DISTANCE constant to tavern.gd**
|
||||
|
||||
At the top of `tavern.gd`, alongside existing constants, add:
|
||||
```gdscript
|
||||
const INTERACT_DISTANCE := 2.5
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace _spawn_player() in tavern.gd**
|
||||
|
||||
Current implementation creates `Node3D.new()`. Replace with:
|
||||
|
||||
@@ -250,24 +252,22 @@ This script is attached to the `CharacterBody3D` that `tavern.gd` creates. It ha
|
||||
controller.global_rotation = spawn.global_rotation
|
||||
```
|
||||
|
||||
Note: add `const INTERACT_DISTANCE := 2.5` constant at the top of `tavern.gd`.
|
||||
|
||||
- [ ] **Step 2: Verify _broadcast_camera_transform() still works**
|
||||
- [ ] **Step 3: Verify _broadcast_camera_transform() still works**
|
||||
|
||||
The method calls `get_node_or_null("PlayerController") as Node3D`. `CharacterBody3D` extends `Node3D`, so the cast works. It also calls `controller.get_node_or_null("Camera3D")` — still correct since camera is still named `Camera3D`.
|
||||
|
||||
However, the camera is now at local position (0, 1.6, 0) inside the controller. The broadcast sends `cam.global_position` / `cam.global_rotation` — this is correct (global, not local).
|
||||
The camera is now at local position (0, 1.6, 0) inside the controller. The broadcast sends `cam.global_position` / `cam.global_rotation` — correct (global, not local).
|
||||
|
||||
- [ ] **Step 3: Open Godot, check for parse errors**
|
||||
- [ ] **Step 4: Open Godot, check for parse errors**
|
||||
|
||||
- [ ] **Step 4: Run as player client**
|
||||
- [ ] **Step 5: Run as player client**
|
||||
|
||||
- WASD moves player
|
||||
- Mouse look works
|
||||
- Player doesn't fall through floor (capsule collision)
|
||||
- Camera is at eye level (~1.6m)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add ruf-der-pilze/scripts/tavern.gd
|
||||
@@ -287,14 +287,14 @@ Add one interactable object in the tavern (e.g., the bar counter or a crate) to
|
||||
- [ ] **Step 1: Create scripts/test_interactable.gd**
|
||||
|
||||
```gdscript
|
||||
extends interactable
|
||||
extends Interactable
|
||||
|
||||
func interact(peer_id: int) -> void:
|
||||
var player_name: String = NetworkManager.players.get(peer_id, {}).get("name", "???")
|
||||
print("[Interactable] %s hat interagiert (Peer %d)" % [player_name, peer_id])
|
||||
```
|
||||
|
||||
Note: `extends interactable` — GDScript allows extending a script file directly. Or use `extends "res://scripts/interactable.gd"`.
|
||||
`extends Interactable` works because `interactable.gd` declares `class_name Interactable` (Task 2).
|
||||
|
||||
- [ ] **Step 2: Add test object to tavern.tscn via Godot editor**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user