feat: Implement Nakama serialization, Realtime API, and a comprehensive lobby system with UI and player management.
This commit is contained in:
@@ -644,9 +644,7 @@ class UserPresence extends NakamaAsyncResult:
|
|||||||
return NakamaSerializer.serialize(self)
|
return NakamaSerializer.serialize(self)
|
||||||
|
|
||||||
func _to_string():
|
func _to_string():
|
||||||
if is_exception(): return get_exception()._to_string()
|
return "UserPresence"
|
||||||
return "UserPresence<persistence=%s, session_id=%s, status=%s, username=%s, user_id=%s>" % [
|
|
||||||
persistence, session_id, status, username, user_id]
|
|
||||||
|
|
||||||
static func create(p_ns : GDScript, p_dict : Dictionary) -> UserPresence:
|
static func create(p_ns : GDScript, p_dict : Dictionary) -> UserPresence:
|
||||||
return _safe_ret(NakamaSerializer.deserialize(p_ns, "UserPresence", p_dict), UserPresence) as UserPresence
|
return _safe_ret(NakamaSerializer.deserialize(p_ns, "UserPresence", p_dict), UserPresence) as UserPresence
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
class_name NakamaSerializer
|
class_name NakamaSerializer
|
||||||
|
|
||||||
static func serialize(p_obj : Object) -> Dictionary:
|
static var _global_depth := 0
|
||||||
|
|
||||||
|
static func serialize(p_obj : Object, p_depth : int = 0) -> Dictionary:
|
||||||
|
_global_depth += 1
|
||||||
|
if _global_depth > 64:
|
||||||
|
_global_depth -= 1
|
||||||
|
printerr("NakamaSerializer: Global recursion limit reached!")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var result = _serialize_impl(p_obj, p_depth)
|
||||||
|
_global_depth -= 1
|
||||||
|
return result
|
||||||
|
|
||||||
|
static func _serialize_impl(p_obj : Object, p_depth : int) -> Dictionary:
|
||||||
var out = {}
|
var out = {}
|
||||||
var schema = p_obj.get("_SCHEMA")
|
var schema = p_obj.get("_SCHEMA")
|
||||||
if schema == null:
|
if schema == null:
|
||||||
@@ -18,13 +31,13 @@ static func serialize(p_obj : Object) -> Dictionary:
|
|||||||
var val_type = typeof(val)
|
var val_type = typeof(val)
|
||||||
match val_type:
|
match val_type:
|
||||||
TYPE_OBJECT: # Simple objects
|
TYPE_OBJECT: # Simple objects
|
||||||
out[k] = serialize(val)
|
out[k] = serialize(val, p_depth + 1)
|
||||||
TYPE_ARRAY: # Array of objects
|
TYPE_ARRAY: # Array of objects
|
||||||
var arr = []
|
var arr = []
|
||||||
for e in val:
|
for e in val:
|
||||||
if typeof(e) != TYPE_OBJECT:
|
if typeof(e) != TYPE_OBJECT:
|
||||||
continue
|
continue
|
||||||
arr.append(serialize(e))
|
arr.append(serialize(e, p_depth + 1))
|
||||||
out[k] = arr
|
out[k] = arr
|
||||||
TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_STRING_ARRAY: # Array of ints, bools, or strings
|
TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_STRING_ARRAY: # Array of ints, bools, or strings
|
||||||
var arr = []
|
var arr = []
|
||||||
@@ -41,7 +54,7 @@ static func serialize(p_obj : Object) -> Dictionary:
|
|||||||
for l in val:
|
for l in val:
|
||||||
if typeof(val[l]) != TYPE_OBJECT:
|
if typeof(val[l]) != TYPE_OBJECT:
|
||||||
continue
|
continue
|
||||||
dict[l] = serialize(val[l])
|
dict[l] = serialize(val[l], p_depth + 1)
|
||||||
else: # Map of simple types
|
else: # Map of simple types
|
||||||
for l in val:
|
for l in val:
|
||||||
var e = val[l]
|
var e = val[l]
|
||||||
|
|||||||
@@ -166,6 +166,9 @@ func _ready():
|
|||||||
NakamaManager.connected_to_nakama.connect(_on_connected_to_nakama)
|
NakamaManager.connected_to_nakama.connect(_on_connected_to_nakama)
|
||||||
NakamaManager.connection_failed.connect(_on_connection_failed)
|
NakamaManager.connection_failed.connect(_on_connection_failed)
|
||||||
|
|
||||||
|
# Connect UserProfileManager signals
|
||||||
|
UserProfileManager.profile_updated.connect(_on_profile_updated)
|
||||||
|
|
||||||
# Show main menu initially
|
# Show main menu initially
|
||||||
_show_panel("main_menu")
|
_show_panel("main_menu")
|
||||||
|
|
||||||
@@ -487,6 +490,17 @@ func _on_connection_failed(error_message: String) -> void:
|
|||||||
connection_status.text = "Connection failed: %s" % error_message
|
connection_status.text = "Connection failed: %s" % error_message
|
||||||
_show_panel("main_menu")
|
_show_panel("main_menu")
|
||||||
|
|
||||||
|
func _on_profile_updated() -> void:
|
||||||
|
"""Handle profile updates (name/avatar change)."""
|
||||||
|
var new_name = UserProfileManager.get_display_name()
|
||||||
|
|
||||||
|
# Update input if visible
|
||||||
|
if player_name_input:
|
||||||
|
player_name_input.text = new_name
|
||||||
|
|
||||||
|
# Sync to LobbyManager if we are in a room or just locally
|
||||||
|
LobbyManager.set_player_name(new_name)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Player Slot Updates
|
# Player Slot Updates
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
+1
-1
@@ -394,8 +394,8 @@ custom_minimum_size = Vector2(90, 28)
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_fonts/font = ExtResource("5_pc087")
|
theme_override_fonts/font = ExtResource("5_pc087")
|
||||||
theme_override_font_sizes/font_size = 11
|
theme_override_font_sizes/font_size = 11
|
||||||
item_count = 3
|
|
||||||
selected = 0
|
selected = 0
|
||||||
|
item_count = 3
|
||||||
popup/item_0/text = "Normal"
|
popup/item_0/text = "Normal"
|
||||||
popup/item_0/id = 0
|
popup/item_0/id = 0
|
||||||
popup/item_1/text = "Scarce"
|
popup/item_1/text = "Scarce"
|
||||||
|
|||||||
+107
-31
@@ -370,6 +370,9 @@ func _setup_host_game():
|
|||||||
if touch_controls:
|
if touch_controls:
|
||||||
touch_controls.set_player(player_character)
|
touch_controls.set_player(player_character)
|
||||||
|
|
||||||
|
# Set host name
|
||||||
|
player_character.display_name = LobbyManager.local_player_name
|
||||||
|
|
||||||
# Spawn client players that joined via lobby (need to add them first)
|
# Spawn client players that joined via lobby (need to add them first)
|
||||||
var lobby_players = LobbyManager.get_players()
|
var lobby_players = LobbyManager.get_players()
|
||||||
for lobby_player in lobby_players:
|
for lobby_player in lobby_players:
|
||||||
@@ -429,6 +432,13 @@ func _spawn_lobby_client_sync(peer_id: int):
|
|||||||
player_character.add_to_group("Players", true)
|
player_character.add_to_group("Players", true)
|
||||||
GameStateManager.add_player(peer_id)
|
GameStateManager.add_player(peer_id)
|
||||||
|
|
||||||
|
# Set name from LobbyManager data if available
|
||||||
|
var lobby_players = LobbyManager.get_players()
|
||||||
|
for p_data in lobby_players:
|
||||||
|
if p_data.get("id") == peer_id:
|
||||||
|
player_character.display_name = p_data.get("name", "Player")
|
||||||
|
break
|
||||||
|
|
||||||
# Tell all clients to create this player
|
# Tell all clients to create this player
|
||||||
rpc("add_newly_connected_player_character", peer_id)
|
rpc("add_newly_connected_player_character", peer_id)
|
||||||
|
|
||||||
@@ -529,43 +539,93 @@ func _start_game():
|
|||||||
ui_manager.initialize_leaderboard_with_players(all_players)
|
ui_manager.initialize_leaderboard_with_players(all_players)
|
||||||
|
|
||||||
func _assign_random_spawn_positions():
|
func _assign_random_spawn_positions():
|
||||||
"""Assign random unique spawn positions to all players."""
|
"""Assign spawn positions distributed to 4 corners (2 per corner for 8 players)."""
|
||||||
# Fetch all valid walkable positions from the generated map
|
|
||||||
var valid_spawns = []
|
|
||||||
var enhanced_gridmap = $EnhancedGridMap
|
var enhanced_gridmap = $EnhancedGridMap
|
||||||
|
if not enhanced_gridmap:
|
||||||
if enhanced_gridmap:
|
return
|
||||||
for x in range(enhanced_gridmap.columns):
|
|
||||||
for z in range(enhanced_gridmap.rows):
|
|
||||||
var ground = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
|
|
||||||
if ground == 0: # Walkable
|
|
||||||
valid_spawns.append(Vector2i(x, z))
|
|
||||||
|
|
||||||
# Fallback if map generation failed or is empty
|
|
||||||
if valid_spawns.size() < 12:
|
|
||||||
print("Warning: Low spawn count! Adding defaults.")
|
|
||||||
for i in range(12):
|
|
||||||
if not Vector2i(0, i) in valid_spawns:
|
|
||||||
valid_spawns.append(Vector2i(0, i))
|
|
||||||
|
|
||||||
# Shuffle spawn locations
|
# Lists for each quadrant
|
||||||
valid_spawns.shuffle()
|
var spawns_TL = [] # Top-Left
|
||||||
|
var spawns_TR = [] # Top-Right
|
||||||
|
var spawns_BL = [] # Bottom-Left
|
||||||
|
var spawns_BR = [] # Bottom-Right
|
||||||
|
var all_spawns = [] # Fallback
|
||||||
|
|
||||||
|
var mid_x = enhanced_gridmap.columns / 2
|
||||||
|
var mid_z = enhanced_gridmap.rows / 2
|
||||||
|
|
||||||
|
for x in range(enhanced_gridmap.columns):
|
||||||
|
for z in range(enhanced_gridmap.rows):
|
||||||
|
var ground = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||||
|
if ground == 0: # Walkable
|
||||||
|
var pos = Vector2i(x, z)
|
||||||
|
all_spawns.append(pos)
|
||||||
|
|
||||||
|
if x < mid_x and z < mid_z:
|
||||||
|
spawns_TL.append(pos)
|
||||||
|
elif x >= mid_x and z < mid_z:
|
||||||
|
spawns_TR.append(pos)
|
||||||
|
elif x < mid_x and z >= mid_z:
|
||||||
|
spawns_BL.append(pos)
|
||||||
|
else:
|
||||||
|
spawns_BR.append(pos)
|
||||||
|
|
||||||
|
# Sort lists by distance to corners (closest to corner should be last, to be popped first)
|
||||||
|
# TL: Close to (0,0) -> Sort descending distance (so closest is at end)
|
||||||
|
spawns_TL.sort_custom(func(a, b): return a.length_squared() > b.length_squared())
|
||||||
|
|
||||||
|
# TR: Close to (13, 0)
|
||||||
|
var tr_corner = Vector2i(enhanced_gridmap.columns - 1, 0)
|
||||||
|
spawns_TR.sort_custom(func(a, b): return a.distance_squared_to(tr_corner) > b.distance_squared_to(tr_corner))
|
||||||
|
|
||||||
|
# BL: Close to (0, 13)
|
||||||
|
var bl_corner = Vector2i(0, enhanced_gridmap.rows - 1)
|
||||||
|
spawns_BL.sort_custom(func(a, b): return a.distance_squared_to(bl_corner) > b.distance_squared_to(bl_corner))
|
||||||
|
|
||||||
|
# BR: Close to (13, 13)
|
||||||
|
var br_corner = Vector2i(enhanced_gridmap.columns - 1, enhanced_gridmap.rows - 1)
|
||||||
|
spawns_BR.sort_custom(func(a, b): return a.distance_squared_to(br_corner) > b.distance_squared_to(br_corner))
|
||||||
|
|
||||||
|
# Fallback shuffle
|
||||||
|
all_spawns.shuffle()
|
||||||
|
|
||||||
# Get all players
|
# Get all players
|
||||||
var all_players = get_tree().get_nodes_in_group("Players")
|
var all_players = get_tree().get_nodes_in_group("Players")
|
||||||
|
|
||||||
# Assign positions
|
|
||||||
var spawn_index = 0
|
var spawn_index = 0
|
||||||
|
|
||||||
|
# Round-robin assignment to corners: TL, TR, BR, BL, TL, TR, BR, BL...
|
||||||
|
# Order: TL -> TR -> BR -> BL (Clockwise-ish)
|
||||||
|
var quadrants = [spawns_TL, spawns_TR, spawns_BR, spawns_BL]
|
||||||
|
|
||||||
for player in all_players:
|
for player in all_players:
|
||||||
if spawn_index >= valid_spawns.size():
|
var assigned_pos = Vector2i(-1, -1)
|
||||||
print("Critical: Not enough spawn points for players!")
|
|
||||||
break
|
# Try to get from the current quadrant
|
||||||
var spawn_pos = valid_spawns[spawn_index]
|
var quadrant_idx = spawn_index % 4
|
||||||
# Set position and sync to all clients
|
var quadrant = quadrants[quadrant_idx]
|
||||||
player.current_position = spawn_pos
|
|
||||||
player.position = player.grid_to_world(spawn_pos)
|
if quadrant.size() > 0:
|
||||||
player.spawn_point_selected = true
|
assigned_pos = quadrant.pop_back()
|
||||||
player.rpc("set_spawn_position", spawn_pos)
|
else:
|
||||||
|
# Fallback: Try other quadrants if preferred one is empty
|
||||||
|
for q in quadrants:
|
||||||
|
if q.size() > 0:
|
||||||
|
assigned_pos = q.pop_back()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Ultimate fallback: Random from anywhere
|
||||||
|
if assigned_pos == Vector2i(-1, -1) and all_spawns.size() > 0:
|
||||||
|
assigned_pos = all_spawns.pop_back()
|
||||||
|
|
||||||
|
if assigned_pos != Vector2i(-1, -1):
|
||||||
|
# Set position and sync to all clients
|
||||||
|
player.current_position = assigned_pos
|
||||||
|
player.position = player.grid_to_world(assigned_pos)
|
||||||
|
player.spawn_point_selected = true
|
||||||
|
player.rpc("set_spawn_position", assigned_pos)
|
||||||
|
else:
|
||||||
|
print("Critical: No spawn point found for player ", player.name)
|
||||||
|
|
||||||
spawn_index += 1
|
spawn_index += 1
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -910,9 +970,11 @@ func request_full_player_sync(requesting_peer_id: int):
|
|||||||
var player_data = {
|
var player_data = {
|
||||||
"peer_id": peer_id,
|
"peer_id": peer_id,
|
||||||
"position": player.current_position,
|
"position": player.current_position,
|
||||||
|
"name": player.display_name,
|
||||||
"goals": player.goals,
|
"goals": player.goals,
|
||||||
"playerboard": player.playerboard,
|
"playerboard": player.playerboard,
|
||||||
"is_bot": player.is_bot || player.is_in_group("Bots")
|
"is_bot": player.is_bot || player.is_in_group("Bots"),
|
||||||
|
"spawn_point_selected": player.spawn_point_selected
|
||||||
}
|
}
|
||||||
rpc_id(requesting_peer_id, "create_specific_player", player_data)
|
rpc_id(requesting_peer_id, "create_specific_player", player_data)
|
||||||
await get_tree().create_timer(0.1).timeout
|
await get_tree().create_timer(0.1).timeout
|
||||||
@@ -933,6 +995,20 @@ func create_specific_player(data: Dictionary):
|
|||||||
add_child(player_character)
|
add_child(player_character)
|
||||||
player_character.add_to_group("Players", true)
|
player_character.add_to_group("Players", true)
|
||||||
|
|
||||||
|
# Set spawn flag directly so it doesn't hide itself
|
||||||
|
if data.has("spawn_point_selected") and data["spawn_point_selected"]:
|
||||||
|
player_character.spawn_point_selected = true
|
||||||
|
player_character.visible = true
|
||||||
|
|
||||||
|
# Ensure visual position matches logical
|
||||||
|
var new_pos = player_character.grid_to_world(data["position"])
|
||||||
|
player_character.global_position = new_pos
|
||||||
|
player_character.target_visual_position = new_pos
|
||||||
|
|
||||||
|
# Set display name
|
||||||
|
if data.has("name"):
|
||||||
|
player_character.display_name = data["name"]
|
||||||
|
|
||||||
if data["is_bot"]:
|
if data["is_bot"]:
|
||||||
player_character.add_to_group("Bots", true)
|
player_character.add_to_group("Bots", true)
|
||||||
player_character.is_bot = true
|
player_character.is_bot = true
|
||||||
|
|||||||
+50
-6
@@ -13,7 +13,20 @@ var powerup_manager
|
|||||||
var score: int = 0
|
var score: int = 0
|
||||||
|
|
||||||
# Display name (synced across network)
|
# Display name (synced across network)
|
||||||
var display_name: String = ""
|
var _display_name: String = ""
|
||||||
|
var display_name: String:
|
||||||
|
set(value):
|
||||||
|
_display_name = value
|
||||||
|
# Update label if it exists
|
||||||
|
var name_label = get_node_or_null("Name")
|
||||||
|
if name_label:
|
||||||
|
name_label.text = _display_name
|
||||||
|
|
||||||
|
# Sync to other peers if we are authority
|
||||||
|
if is_multiplayer_authority() and is_inside_tree():
|
||||||
|
rpc("sync_display_name", _display_name)
|
||||||
|
get:
|
||||||
|
return _display_name
|
||||||
|
|
||||||
# Special effect states
|
# Special effect states
|
||||||
var is_frozen: bool = false
|
var is_frozen: bool = false
|
||||||
@@ -262,6 +275,11 @@ func _ready():
|
|||||||
rpc("sync_position", current_position)
|
rpc("sync_position", current_position)
|
||||||
else:
|
else:
|
||||||
target_visual_position = global_position
|
target_visual_position = global_position
|
||||||
|
# If random spawn is enabled, do NOT broadcast (0,0). Wait for set_spawn_position.
|
||||||
|
|
||||||
|
# Prevent visual "jump" by hiding until spawn position is confirmed
|
||||||
|
if LobbyManager.get_randomize_spawn() and not spawn_point_selected:
|
||||||
|
visible = false
|
||||||
|
|
||||||
func _init_managers():
|
func _init_managers():
|
||||||
movement_manager = load("res://scripts/managers/player_movement_manager.gd").new()
|
movement_manager = load("res://scripts/managers/player_movement_manager.gd").new()
|
||||||
@@ -588,8 +606,9 @@ func sync_bot_status(is_bot_status: bool):
|
|||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func sync_display_name(new_name: String) -> void:
|
func sync_display_name(new_name: String) -> void:
|
||||||
"""Sync display name across network."""
|
"""Sync display name across network."""
|
||||||
display_name = new_name
|
_display_name = new_name
|
||||||
$Name.text = display_name
|
if has_node("Name"):
|
||||||
|
$Name.text = _display_name
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func sync_modulate(color: Color) -> void:
|
func sync_modulate(color: Color) -> void:
|
||||||
@@ -893,6 +912,11 @@ func activate_powerup(effect_id: int):
|
|||||||
|
|
||||||
|
|
||||||
func _process(delta):
|
func _process(delta):
|
||||||
|
# Failsafe: Ensure player is visible if spawn point is selected and random spawn is active
|
||||||
|
# This handles race conditions where set_spawn_position might be called before _ready finishes hiding
|
||||||
|
if LobbyManager.get_randomize_spawn() and spawn_point_selected and not visible:
|
||||||
|
visible = true
|
||||||
|
|
||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
# Visual debugging - show display name with connection status
|
# Visual debugging - show display name with connection status
|
||||||
$Name.text = display_name if not display_name.is_empty() else str(name)
|
$Name.text = display_name if not display_name.is_empty() else str(name)
|
||||||
@@ -906,8 +930,11 @@ func _process(delta):
|
|||||||
# Client-side visual smoothing
|
# Client-side visual smoothing
|
||||||
# Only interpolate if NOT running a movement tween, OR if the drift is large (teleport/snap)
|
# Only interpolate if NOT running a movement tween, OR if the drift is large (teleport/snap)
|
||||||
if not is_player_moving:
|
if not is_player_moving:
|
||||||
# Snap to target if very close (prevents micro-jitter)
|
var dist_sq = global_position.distance_squared_to(target_visual_position)
|
||||||
if global_position.distance_squared_to(target_visual_position) < 0.001:
|
|
||||||
|
if dist_sq > 4.0: # If distance > 2.0 units (teleport/spawn), snap immediately
|
||||||
|
global_position = target_visual_position
|
||||||
|
elif dist_sq < 0.001: # Prevent micro-jitter
|
||||||
global_position = target_visual_position
|
global_position = target_visual_position
|
||||||
else:
|
else:
|
||||||
# Interpolate towards the target position received from authority
|
# Interpolate towards the target position received from authority
|
||||||
@@ -1157,6 +1184,12 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
|||||||
is_player_moving = true
|
is_player_moving = true
|
||||||
if path.size() > 0:
|
if path.size() > 0:
|
||||||
target_position = Vector2i(path[-1].x, path[-1].y)
|
target_position = Vector2i(path[-1].x, path[-1].y)
|
||||||
|
|
||||||
|
# FORCE SNAP START: Ensure we start animating from our actual grid position
|
||||||
|
# This prevents "jumps" if the visual node drifted or was set via global_position vs position mismatch
|
||||||
|
var start_world_pos = grid_to_world(current_position)
|
||||||
|
if global_position.distance_squared_to(start_world_pos) > 0.001:
|
||||||
|
global_position = start_world_pos
|
||||||
|
|
||||||
var tween = create_tween()
|
var tween = create_tween()
|
||||||
tween.set_trans(Tween.TRANS_LINEAR)
|
tween.set_trans(Tween.TRANS_LINEAR)
|
||||||
@@ -1167,7 +1200,8 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
|||||||
step_duration = step_duration / movement_manager.speed_multiplier
|
step_duration = step_duration / movement_manager.speed_multiplier
|
||||||
|
|
||||||
for point in path:
|
for point in path:
|
||||||
tween.tween_property(self, "position", grid_to_world(Vector2i(point.x, point.y)), step_duration)
|
# Use global_position for consistency
|
||||||
|
tween.tween_property(self, "global_position", grid_to_world(Vector2i(point.x, point.y)), step_duration)
|
||||||
|
|
||||||
tween.tween_callback(func():
|
tween.tween_callback(func():
|
||||||
current_position = Vector2i(path[-1].x, path[-1].y)
|
current_position = Vector2i(path[-1].x, path[-1].y)
|
||||||
@@ -1739,6 +1773,13 @@ func update_visual_position():
|
|||||||
@rpc("any_peer", "call_local")
|
@rpc("any_peer", "call_local")
|
||||||
func sync_position(pos: Vector2i):
|
func sync_position(pos: Vector2i):
|
||||||
current_position = pos
|
current_position = pos
|
||||||
|
|
||||||
|
# If random spawn is active, receiving a position sync essentially confirms a valid spawn
|
||||||
|
if LobbyManager.get_randomize_spawn():
|
||||||
|
spawn_point_selected = true
|
||||||
|
if not visible:
|
||||||
|
visible = true
|
||||||
|
|
||||||
# Always update the visual position after position sync
|
# Always update the visual position after position sync
|
||||||
var new_pos = Vector3(
|
var new_pos = Vector3(
|
||||||
current_position.x * cell_size.x + cell_size.x * 0.5,
|
current_position.x * cell_size.x + cell_size.x * 0.5,
|
||||||
@@ -1767,6 +1808,9 @@ func set_spawn_position(pos: Vector2i):
|
|||||||
|
|
||||||
global_position = new_pos
|
global_position = new_pos
|
||||||
target_visual_position = new_pos
|
target_visual_position = new_pos
|
||||||
|
|
||||||
|
# Reveal character now that it's in the correct position
|
||||||
|
visible = true
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
|||||||
@@ -277,6 +277,37 @@ func sync_character(player_id: int, character_name: String) -> void:
|
|||||||
emit_signal("character_changed", player_id, character_name)
|
emit_signal("character_changed", player_id, character_name)
|
||||||
emit_signal("player_list_changed")
|
emit_signal("player_list_changed")
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Player Name Management
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
func set_player_name(new_name: String) -> void:
|
||||||
|
"""Set local player's name. Syncs to all peers."""
|
||||||
|
local_player_name = new_name
|
||||||
|
var my_id = multiplayer.get_unique_id()
|
||||||
|
|
||||||
|
# Update local player data
|
||||||
|
for player in players_in_room:
|
||||||
|
if player["id"] == my_id:
|
||||||
|
player["name"] = new_name
|
||||||
|
break
|
||||||
|
|
||||||
|
# Sync to all peers if connected
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
rpc("sync_player_name", my_id, new_name)
|
||||||
|
|
||||||
|
emit_signal("player_list_changed")
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func sync_player_name(player_id: int, new_name: String) -> void:
|
||||||
|
"""Sync player name across all clients."""
|
||||||
|
for player in players_in_room:
|
||||||
|
if player["id"] == player_id:
|
||||||
|
player["name"] = new_name
|
||||||
|
break
|
||||||
|
|
||||||
|
emit_signal("player_list_changed")
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Area Selection (Host Only)
|
# Area Selection (Host Only)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user