synced stuff
This commit is contained in:
@@ -31,10 +31,6 @@
|
|||||||
[ext_resource type="AudioStream" uid="uid://w6yon88kjfml" path="res://assets/audio/sfx/nickes/lift_sfx.ogg" id="28_pf23h"]
|
[ext_resource type="AudioStream" uid="uid://w6yon88kjfml" path="res://assets/audio/sfx/nickes/lift_sfx.ogg" id="28_pf23h"]
|
||||||
[ext_resource type="AudioStream" uid="uid://d4kjyb1olr74s" path="res://assets/audio/sfx/weapons/bow/bow_draw-02.mp3" id="30_gl8cc"]
|
[ext_resource type="AudioStream" uid="uid://d4kjyb1olr74s" path="res://assets/audio/sfx/weapons/bow/bow_draw-02.mp3" id="30_gl8cc"]
|
||||||
[ext_resource type="AudioStream" uid="uid://mbtpqlb5n3gd" path="res://assets/audio/sfx/weapons/bow/bow_no_arrow.mp3" id="31_487ah"]
|
[ext_resource type="AudioStream" uid="uid://mbtpqlb5n3gd" path="res://assets/audio/sfx/weapons/bow/bow_no_arrow.mp3" id="31_487ah"]
|
||||||
[ext_resource type="AudioStream" uid="uid://fm6hrpckfknc" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_03.wav" id="32_bj30b"]
|
|
||||||
[ext_resource type="AudioStream" uid="uid://be3uspidyqm3x" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_04.wav" id="33_jc3p3"]
|
|
||||||
[ext_resource type="AudioStream" uid="uid://dvttykynr671m" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_05.wav" id="34_hax0n"]
|
|
||||||
[ext_resource type="AudioStream" uid="uid://sejnuklu653m" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_06.wav" id="35_t4otl"]
|
|
||||||
|
|
||||||
[sub_resource type="Gradient" id="Gradient_wqfne"]
|
[sub_resource type="Gradient" id="Gradient_wqfne"]
|
||||||
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
|
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
|
||||||
@@ -284,14 +280,6 @@ random_pitch = 1.0630184
|
|||||||
streams_count = 1
|
streams_count = 1
|
||||||
stream_0/stream = ExtResource("31_487ah")
|
stream_0/stream = ExtResource("31_487ah")
|
||||||
|
|
||||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_j2b1d"]
|
|
||||||
random_pitch = 1.1036249
|
|
||||||
streams_count = 4
|
|
||||||
stream_0/stream = ExtResource("32_bj30b")
|
|
||||||
stream_1/stream = ExtResource("33_jc3p3")
|
|
||||||
stream_2/stream = ExtResource("34_hax0n")
|
|
||||||
stream_3/stream = ExtResource("35_t4otl")
|
|
||||||
|
|
||||||
[node name="Player" type="CharacterBody2D" unique_id=937429705]
|
[node name="Player" type="CharacterBody2D" unique_id=937429705]
|
||||||
collision_mask = 67
|
collision_mask = 67
|
||||||
motion_mode = 1
|
motion_mode = 1
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
|
|
||||||
play_impact()
|
play_impact()
|
||||||
|
|
||||||
|
# CRITICAL: Stick to target on ALL clients FIRST (before damage check)
|
||||||
|
# This ensures the arrow stops on all clients, not just the authority
|
||||||
|
_stick_to_target(body)
|
||||||
|
|
||||||
# CRITICAL: Only the projectile owner (authority) should deal damage to players
|
# CRITICAL: Only the projectile owner (authority) should deal damage to players
|
||||||
if player_owner and player_owner.is_multiplayer_authority():
|
if player_owner and player_owner.is_multiplayer_authority():
|
||||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||||
@@ -176,8 +180,6 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
else:
|
else:
|
||||||
body.rpc_take_damage.rpc(20.0, attacker_pos)
|
body.rpc_take_damage.rpc(20.0, attacker_pos)
|
||||||
|
|
||||||
# Stick to target on ALL clients (both authority and non-authority)
|
|
||||||
_stick_to_target(body)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Deal damage to enemies
|
# Deal damage to enemies
|
||||||
@@ -206,8 +208,9 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
# Add to hit_targets so we don't check this enemy again
|
# Add to hit_targets so we don't check this enemy again
|
||||||
hit_targets[body] = true
|
hit_targets[body] = true
|
||||||
# Sync miss to all clients - arrow continues flying
|
# Sync miss to all clients - arrow continues flying
|
||||||
if is_inside_tree():
|
# CRITICAL: Validate body is still valid and use name instead of path
|
||||||
_sync_arrow_miss.rpc(body.get_path())
|
if is_inside_tree() and is_instance_valid(body) and body.is_inside_tree():
|
||||||
|
_sync_arrow_miss.rpc(body.name)
|
||||||
# Don't stick to target - let arrow continue flying
|
# Don't stick to target - let arrow continue flying
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -225,8 +228,9 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
# Add to hit_targets so we don't check this enemy again
|
# Add to hit_targets so we don't check this enemy again
|
||||||
hit_targets[body] = true
|
hit_targets[body] = true
|
||||||
# Sync dodge to all clients - arrow continues flying
|
# Sync dodge to all clients - arrow continues flying
|
||||||
if is_inside_tree():
|
# CRITICAL: Validate body is still valid and use name instead of path
|
||||||
_sync_arrow_dodge.rpc(body.get_path())
|
if is_inside_tree() and is_instance_valid(body) and body.is_inside_tree():
|
||||||
|
_sync_arrow_dodge.rpc(body.name)
|
||||||
# Don't stick to target - let arrow continue flying
|
# Don't stick to target - let arrow continue flying
|
||||||
print(body.name, " DODGED arrow! Arrow continues flying...")
|
print(body.name, " DODGED arrow! Arrow continues flying...")
|
||||||
return
|
return
|
||||||
@@ -246,8 +250,9 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
body.rpc_take_damage.rpc(damage, attacker_pos, false)
|
body.rpc_take_damage.rpc(damage, attacker_pos, false)
|
||||||
|
|
||||||
# Sync hit to all clients - arrow sticks
|
# Sync hit to all clients - arrow sticks
|
||||||
if is_inside_tree():
|
# CRITICAL: Validate body is still valid and use name instead of path
|
||||||
_sync_arrow_hit.rpc(body.get_path())
|
if is_inside_tree() and is_instance_valid(body) and body.is_inside_tree():
|
||||||
|
_sync_arrow_hit.rpc(body.name)
|
||||||
|
|
||||||
_stick_to_target(body)
|
_stick_to_target(body)
|
||||||
return
|
return
|
||||||
@@ -293,11 +298,22 @@ func _sync_arrow_collected():
|
|||||||
call_deferred("queue_free")
|
call_deferred("queue_free")
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func _sync_arrow_hit(target_path: NodePath):
|
func _sync_arrow_hit(target_name: String):
|
||||||
# Authority determined arrow HIT enemy - stick to it on all clients
|
# Authority determined arrow HIT enemy - stick to it on all clients
|
||||||
var target = get_node_or_null(target_path)
|
# CRITICAL: Validate arrow is still valid before processing
|
||||||
|
if not is_instance_valid(self) or not is_inside_tree():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find target by name in Entities node
|
||||||
|
var target = null
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world:
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
target = entities_node.get_node_or_null(target_name)
|
||||||
|
|
||||||
if not target:
|
if not target:
|
||||||
print("WARNING: Arrow hit target not found at path: ", target_path)
|
print("WARNING: Arrow hit target not found: ", target_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
if target not in hit_targets:
|
if target not in hit_targets:
|
||||||
@@ -307,17 +323,39 @@ func _sync_arrow_hit(target_path: NodePath):
|
|||||||
print("Arrow synced as HIT to: ", target.name)
|
print("Arrow synced as HIT to: ", target.name)
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func _sync_arrow_miss(target_path: NodePath):
|
func _sync_arrow_miss(target_name: String):
|
||||||
# Authority determined arrow MISSED enemy - continues flying on all clients
|
# Authority determined arrow MISSED enemy - continues flying on all clients
|
||||||
var target = get_node_or_null(target_path)
|
# CRITICAL: Validate arrow is still valid before processing
|
||||||
|
if not is_instance_valid(self) or not is_inside_tree():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find target by name in Entities node
|
||||||
|
var target = null
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world:
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
target = entities_node.get_node_or_null(target_name)
|
||||||
|
|
||||||
if target and target not in hit_targets:
|
if target and target not in hit_targets:
|
||||||
hit_targets[target] = true
|
hit_targets[target] = true
|
||||||
print("Arrow synced as MISS - continuing through: ", target.name if target else "unknown")
|
print("Arrow synced as MISS - continuing through: ", target.name if target else "unknown")
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func _sync_arrow_dodge(target_path: NodePath):
|
func _sync_arrow_dodge(target_name: String):
|
||||||
# Authority determined enemy DODGED arrow - continues flying on all clients
|
# Authority determined enemy DODGED arrow - continues flying on all clients
|
||||||
var target = get_node_or_null(target_path)
|
# CRITICAL: Validate arrow is still valid before processing
|
||||||
|
if not is_instance_valid(self) or not is_inside_tree():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find target by name in Entities node
|
||||||
|
var target = null
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world:
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
target = entities_node.get_node_or_null(target_name)
|
||||||
|
|
||||||
if target and target not in hit_targets:
|
if target and target not in hit_targets:
|
||||||
hit_targets[target] = true
|
hit_targets[target] = true
|
||||||
print("Arrow synced as DODGE - continuing through: ", target.name if target else "unknown")
|
print("Arrow synced as DODGE - continuing through: ", target.name if target else "unknown")
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||||
func _process(delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
# Update camera to follow local players
|
# Update camera to follow local players
|
||||||
_update_camera()
|
_update_camera()
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2784,8 +2784,8 @@ func _place_traps_in_dungeon(all_rooms: Array, start_room_index: int, exit_room_
|
|||||||
|
|
||||||
if not too_close_to_door:
|
if not too_close_to_door:
|
||||||
# Valid position - place trap
|
# Valid position - place trap
|
||||||
var trap_world_x = world_x * tile_size + tile_size / 2
|
var trap_world_x = world_x * tile_size + tile_size / 2.0
|
||||||
var trap_world_y = world_y * tile_size + tile_size / 2
|
var trap_world_y = world_y * tile_size + tile_size / 2.0
|
||||||
|
|
||||||
traps.append({
|
traps.append({
|
||||||
"position": Vector2(trap_world_x, trap_world_y),
|
"position": Vector2(trap_world_x, trap_world_y),
|
||||||
|
|||||||
@@ -423,6 +423,10 @@ func _send_initial_client_sync_with_retry(peer_id: int, local_count: int, retry_
|
|||||||
# Wait a bit after dungeon sync to ensure objects are spawned first
|
# Wait a bit after dungeon sync to ensure objects are spawned first
|
||||||
call_deferred("_sync_existing_chest_states_to_client", peer_id)
|
call_deferred("_sync_existing_chest_states_to_client", peer_id)
|
||||||
|
|
||||||
|
# Sync existing trap states (detected/disarmed) to the new client
|
||||||
|
# Wait a bit after dungeon sync to ensure traps are spawned first
|
||||||
|
call_deferred("_sync_existing_trap_states_to_client", peer_id)
|
||||||
|
|
||||||
# Note: Dungeon-spawned enemies are already synced via _sync_dungeon RPC
|
# Note: Dungeon-spawned enemies are already synced via _sync_dungeon RPC
|
||||||
# which includes dungeon_data.enemies and calls _spawn_enemies() on the client.
|
# which includes dungeon_data.enemies and calls _spawn_enemies() on the client.
|
||||||
# So we don't need to sync them again with individual RPCs.
|
# So we don't need to sync them again with individual RPCs.
|
||||||
@@ -430,15 +434,8 @@ func _send_initial_client_sync_with_retry(peer_id: int, local_count: int, retry_
|
|||||||
# Note: Interactable objects are also synced via _sync_dungeon RPC
|
# Note: Interactable objects are also synced via _sync_dungeon RPC
|
||||||
# which includes dungeon_data.interactable_objects and calls _spawn_interactable_objects() on the client.
|
# which includes dungeon_data.interactable_objects and calls _spawn_interactable_objects() on the client.
|
||||||
# However, chest open states and broken objects need to be synced separately since they change during gameplay.
|
# However, chest open states and broken objects need to be synced separately since they change during gameplay.
|
||||||
|
# NOTE: Torches are already in dungeon_data and are spawned from the blob, so we don't need to sync them separately.
|
||||||
|
|
||||||
# Sync existing torches to the new client
|
|
||||||
# Wait until AFTER dungeon chunks are sent (8 chunks * 0.15s = 1.2s, plus rooms/entities = ~1.6s)
|
|
||||||
# Add extra buffer to ensure chunks are complete
|
|
||||||
get_tree().create_timer(2.0).timeout.connect(func():
|
|
||||||
if is_inside_tree():
|
|
||||||
_sync_existing_torches_to_client(peer_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sync door states to the new client (wait until after dungeon chunks)
|
# Sync door states to the new client (wait until after dungeon chunks)
|
||||||
get_tree().create_timer(2.0).timeout.connect(func():
|
get_tree().create_timer(2.0).timeout.connect(func():
|
||||||
if is_inside_tree():
|
if is_inside_tree():
|
||||||
@@ -3465,6 +3462,11 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
|
|||||||
_spawn_blocking_doors()
|
_spawn_blocking_doors()
|
||||||
print("GameWorld: Client - Blocking doors spawned")
|
print("GameWorld: Client - Blocking doors spawned")
|
||||||
|
|
||||||
|
# Spawn traps on client
|
||||||
|
print("GameWorld: Client - Spawning traps...")
|
||||||
|
_spawn_traps()
|
||||||
|
print("GameWorld: Client - Traps spawned")
|
||||||
|
|
||||||
# Spawn room triggers on client
|
# Spawn room triggers on client
|
||||||
print("GameWorld: Client - Spawning room triggers...")
|
print("GameWorld: Client - Spawning room triggers...")
|
||||||
_spawn_room_triggers()
|
_spawn_room_triggers()
|
||||||
@@ -3885,6 +3887,11 @@ func _reassemble_dungeon_blob():
|
|||||||
_spawn_blocking_doors()
|
_spawn_blocking_doors()
|
||||||
print("GameWorld: Client - Blocking doors spawned")
|
print("GameWorld: Client - Blocking doors spawned")
|
||||||
|
|
||||||
|
# Spawn traps on client
|
||||||
|
print("GameWorld: Client - Spawning traps from blob...")
|
||||||
|
_spawn_traps()
|
||||||
|
print("GameWorld: Client - Traps spawned")
|
||||||
|
|
||||||
print("GameWorld: Client - Spawning room triggers from blob...")
|
print("GameWorld: Client - Spawning room triggers from blob...")
|
||||||
_spawn_room_triggers()
|
_spawn_room_triggers()
|
||||||
print("GameWorld: Client - Room triggers spawned")
|
print("GameWorld: Client - Room triggers spawned")
|
||||||
@@ -4785,6 +4792,73 @@ func _sync_existing_chest_states_to_client(client_peer_id: int, retry_count: int
|
|||||||
|
|
||||||
print("GameWorld: Synced ", opened_chest_count, " opened chests to client ", client_peer_id)
|
print("GameWorld: Synced ", opened_chest_count, " opened chests to client ", client_peer_id)
|
||||||
|
|
||||||
|
func _sync_existing_trap_states_to_client(client_peer_id: int, retry_count: int = 0):
|
||||||
|
# Sync trap states (detected/disarmed) to new client with retry logic
|
||||||
|
if not is_inside_tree():
|
||||||
|
return
|
||||||
|
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if peer is recognized before sending RPC
|
||||||
|
if not _check_peer_recognized(client_peer_id):
|
||||||
|
if retry_count < 15: # Reduced from 30
|
||||||
|
get_tree().create_timer(0.2).timeout.connect(func():
|
||||||
|
if is_inside_tree() and multiplayer.is_server():
|
||||||
|
_sync_existing_trap_states_to_client(client_peer_id, retry_count + 1)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
return
|
||||||
|
|
||||||
|
var synced_trap_count = 0
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.is_in_group("trap"):
|
||||||
|
# Found a trap - sync its state (detected/disarmed) to the client
|
||||||
|
var trap_name = child.name
|
||||||
|
var is_detected = child.is_detected if "is_detected" in child else false
|
||||||
|
var is_disarmed = child.is_disarmed if "is_disarmed" in child else false
|
||||||
|
_sync_trap_state_by_name.rpc_id(client_peer_id, trap_name, is_detected, is_disarmed)
|
||||||
|
synced_trap_count += 1
|
||||||
|
print("GameWorld: Syncing trap ", trap_name, " state (detected: ", is_detected, ", disarmed: ", is_disarmed, ") to client ", client_peer_id)
|
||||||
|
|
||||||
|
print("GameWorld: Synced ", synced_trap_count, " trap states to client ", client_peer_id)
|
||||||
|
|
||||||
|
@rpc("authority", "reliable")
|
||||||
|
func _sync_trap_state_by_name(trap_name: String, is_detected: bool, is_disarmed: bool):
|
||||||
|
# Client receives trap state sync by name (avoids node path RPC errors)
|
||||||
|
if not is_inside_tree():
|
||||||
|
return
|
||||||
|
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
return
|
||||||
|
|
||||||
|
var trap = entities_node.get_node_or_null(trap_name)
|
||||||
|
if trap and trap.is_in_group("trap"):
|
||||||
|
# Update trap state
|
||||||
|
if "is_detected" in trap:
|
||||||
|
trap.is_detected = is_detected
|
||||||
|
if "is_disarmed" in trap:
|
||||||
|
trap.is_disarmed = is_disarmed
|
||||||
|
|
||||||
|
# Update visuals
|
||||||
|
if is_detected and "sprite" in trap and trap.sprite:
|
||||||
|
trap.sprite.modulate.a = 1.0
|
||||||
|
|
||||||
|
if is_disarmed:
|
||||||
|
if "sprite" in trap and trap.sprite:
|
||||||
|
trap.sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
||||||
|
if "activation_area" in trap and trap.activation_area:
|
||||||
|
trap.activation_area.monitoring = false
|
||||||
|
|
||||||
|
print("GameWorld: Client received trap state sync for ", trap_name, " - detected: ", is_detected, ", disarmed: ", is_disarmed)
|
||||||
|
else:
|
||||||
|
print("GameWorld: WARNING - Trap ", trap_name, " not found when syncing state")
|
||||||
|
|
||||||
func _sync_broken_objects_to_client(client_peer_id: int, retry_count: int = 0):
|
func _sync_broken_objects_to_client(client_peer_id: int, retry_count: int = 0):
|
||||||
# Sync broken interactable objects to new client with retry logic
|
# Sync broken interactable objects to new client with retry logic
|
||||||
# Check if node is still valid and in tree
|
# Check if node is still valid and in tree
|
||||||
@@ -4907,18 +4981,28 @@ func _sync_existing_torches_to_client(client_peer_id: int, retry_count: int = 0)
|
|||||||
func _sync_torch_spawn(torch_position: Vector2, torch_rotation: float):
|
func _sync_torch_spawn(torch_position: Vector2, torch_rotation: float):
|
||||||
# Clients spawn torch when server tells them to
|
# Clients spawn torch when server tells them to
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
|
# CRITICAL: Check if torch already exists at this position to avoid duplicates
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if a torch already exists at this position (within 1 pixel tolerance)
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.is_in_group("torch"):
|
||||||
|
if child.global_position.distance_to(torch_position) < 1.0:
|
||||||
|
# Torch already exists at this position - skip spawning duplicate
|
||||||
|
return
|
||||||
|
|
||||||
var torch_scene = preload("res://scenes/torch_wall.tscn")
|
var torch_scene = preload("res://scenes/torch_wall.tscn")
|
||||||
if not torch_scene:
|
if not torch_scene:
|
||||||
return
|
return
|
||||||
|
|
||||||
var entities_node = get_node_or_null("Entities")
|
var torch = torch_scene.instantiate()
|
||||||
if entities_node:
|
torch.name = "Torch_%d" % entities_node.get_child_count()
|
||||||
var torch = torch_scene.instantiate()
|
entities_node.add_child(torch)
|
||||||
torch.name = "Torch_%d" % entities_node.get_child_count()
|
torch.global_position = torch_position
|
||||||
entities_node.add_child(torch)
|
torch.rotation_degrees = torch_rotation
|
||||||
torch.global_position = torch_position
|
print("Client spawned torch at ", torch_position, " with rotation ", torch_rotation)
|
||||||
torch.rotation_degrees = torch_rotation
|
|
||||||
print("Client spawned torch at ", torch_position, " with rotation ", torch_rotation)
|
|
||||||
|
|
||||||
func _sync_existing_door_states_to_client(client_peer_id: int, retry_count: int = 0):
|
func _sync_existing_door_states_to_client(client_peer_id: int, retry_count: int = 0):
|
||||||
# Sync door states to new client with retry logic
|
# Sync door states to new client with retry logic
|
||||||
|
|||||||
@@ -402,6 +402,12 @@ func _ready():
|
|||||||
|
|
||||||
can_send_rpcs = true
|
can_send_rpcs = true
|
||||||
print("Player ", name, " is now ready to send RPCs (is_server: ", multiplayer.is_server(), ")")
|
print("Player ", name, " is now ready to send RPCs (is_server: ", multiplayer.is_server(), ")")
|
||||||
|
|
||||||
|
# When we become ready to send RPCs, re-sync appearance to ensure new clients get it
|
||||||
|
# This handles the case where appearance was set up before new clients connected
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and character_stats and character_stats.race != "":
|
||||||
|
# Emit character_changed to trigger appearance sync for any newly connected clients
|
||||||
|
character_stats.character_changed.emit(character_stats)
|
||||||
|
|
||||||
func _duplicate_sprite_materials():
|
func _duplicate_sprite_materials():
|
||||||
# Duplicate shader materials for sprites that use tint parameters
|
# Duplicate shader materials for sprites that use tint parameters
|
||||||
@@ -469,6 +475,12 @@ func _initialize_character_stats():
|
|||||||
func _reinitialize_appearance_with_seed(_seed_value: int):
|
func _reinitialize_appearance_with_seed(_seed_value: int):
|
||||||
# Re-initialize appearance with the correct dungeon_seed
|
# Re-initialize appearance with the correct dungeon_seed
|
||||||
# This is called when a joiner receives dungeon_seed after players were already spawned
|
# This is called when a joiner receives dungeon_seed after players were already spawned
|
||||||
|
# CRITICAL: Only the authority should re-initialize appearance!
|
||||||
|
# Non-authority players will receive appearance via race/equipment sync
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
remove_meta("needs_appearance_reset") # Clear flag even if we skip
|
||||||
|
return # Non-authority will receive appearance via sync
|
||||||
|
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if not game_world or game_world.dungeon_seed == 0:
|
if not game_world or game_world.dungeon_seed == 0:
|
||||||
return # Still no seed, skip
|
return # Still no seed, skip
|
||||||
@@ -483,9 +495,8 @@ func _reinitialize_appearance_with_seed(_seed_value: int):
|
|||||||
LogManager.log_error("Player " + str(name) + " _reinitialize_appearance_with_seed: character_stats is null!", LogManager.CATEGORY_GAMEPLAY)
|
LogManager.log_error("Player " + str(name) + " _reinitialize_appearance_with_seed: character_stats is null!", LogManager.CATEGORY_GAMEPLAY)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Save current state (race, stats, equipment) before re-initializing
|
# Save current state (stats, equipment) before re-initializing
|
||||||
# We need to preserve these because they might have been set correctly already
|
# Race and appearance will be re-randomized with correct seed (deterministic)
|
||||||
var saved_race = character_stats.race
|
|
||||||
var saved_stats = {
|
var saved_stats = {
|
||||||
"str": character_stats.baseStats.str,
|
"str": character_stats.baseStats.str,
|
||||||
"dex": character_stats.baseStats.dex,
|
"dex": character_stats.baseStats.dex,
|
||||||
@@ -519,11 +530,12 @@ func _reinitialize_appearance_with_seed(_seed_value: int):
|
|||||||
appearance_rng.seed = new_seed_value
|
appearance_rng.seed = new_seed_value
|
||||||
|
|
||||||
# Re-run appearance setup with the correct seed
|
# Re-run appearance setup with the correct seed
|
||||||
# This will re-randomize visual appearance (skin, hair, facial hair, eyes, etc.)
|
# This will re-randomize EVERYTHING including race, appearance, stats
|
||||||
|
# Since the seed is deterministic, this will match what was generated on other clients
|
||||||
_setup_player_appearance()
|
_setup_player_appearance()
|
||||||
|
|
||||||
# Restore saved race, stats, and equipment (preserve them from before re-initialization)
|
# Restore saved stats (but keep the newly randomized race/appearance from correct seed)
|
||||||
character_stats.setRace(saved_race) # Restore original race
|
# The race/appearance from _setup_player_appearance() is now correct (deterministic seed)
|
||||||
character_stats.baseStats.str = saved_stats.str
|
character_stats.baseStats.str = saved_stats.str
|
||||||
character_stats.baseStats.dex = saved_stats.dex
|
character_stats.baseStats.dex = saved_stats.dex
|
||||||
character_stats.baseStats.int = saved_stats.int
|
character_stats.baseStats.int = saved_stats.int
|
||||||
@@ -540,7 +552,7 @@ func _reinitialize_appearance_with_seed(_seed_value: int):
|
|||||||
character_stats.xp = saved_stats.exp # CharacterStats uses 'xp' not 'exp'
|
character_stats.xp = saved_stats.exp # CharacterStats uses 'xp' not 'exp'
|
||||||
character_stats.level = saved_stats.level
|
character_stats.level = saved_stats.level
|
||||||
|
|
||||||
# Restore equipment
|
# Restore equipment (but Elf starting equipment will be re-added by _setup_player_appearance)
|
||||||
for slot_name in saved_equipment.keys():
|
for slot_name in saved_equipment.keys():
|
||||||
var item_data = saved_equipment[slot_name]
|
var item_data = saved_equipment[slot_name]
|
||||||
character_stats.equipment[slot_name] = Item.new(item_data) if item_data else null
|
character_stats.equipment[slot_name] = Item.new(item_data) if item_data else null
|
||||||
@@ -762,6 +774,118 @@ func _setup_player_appearance():
|
|||||||
|
|
||||||
print("Player ", name, " appearance set up: race=", character_stats.race)
|
print("Player ", name, " appearance set up: race=", character_stats.race)
|
||||||
|
|
||||||
|
func _setup_player_appearance_preserve_race():
|
||||||
|
# Same as _setup_player_appearance() but preserves existing race instead of randomizing it
|
||||||
|
# Ensure character_stats exists before setting appearance
|
||||||
|
if not character_stats:
|
||||||
|
LogManager.log_error("Player " + str(name) + " _setup_player_appearance_preserve_race: character_stats is null!", LogManager.CATEGORY_GAMEPLAY)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use existing race (don't randomize)
|
||||||
|
var selected_race = character_stats.race
|
||||||
|
if selected_race == "":
|
||||||
|
# Fallback: if no race set, randomize
|
||||||
|
var races = ["Dwarf", "Elf", "Human"]
|
||||||
|
selected_race = races[appearance_rng.randi() % races.size()]
|
||||||
|
character_stats.setRace(selected_race)
|
||||||
|
|
||||||
|
# Don't randomize stats - they should be synced separately
|
||||||
|
# Don't give starting equipment - that should be synced separately
|
||||||
|
|
||||||
|
# Randomize skin (human only for players)
|
||||||
|
var weights = [7, 6, 5, 4, 3, 2, 1]
|
||||||
|
var total_weight = 28
|
||||||
|
var random_value = appearance_rng.randi() % total_weight
|
||||||
|
var skin_index = 0
|
||||||
|
var cumulative = 0
|
||||||
|
for i in range(weights.size()):
|
||||||
|
cumulative += weights[i]
|
||||||
|
if random_value < cumulative:
|
||||||
|
skin_index = i
|
||||||
|
break
|
||||||
|
character_stats.setSkin(skin_index)
|
||||||
|
|
||||||
|
# Randomize hairstyle (0 = none, 1-12 = various styles)
|
||||||
|
var hair_style = appearance_rng.randi_range(0, 12)
|
||||||
|
character_stats.setHair(hair_style)
|
||||||
|
|
||||||
|
# Randomize hair color
|
||||||
|
var hair_colors = [
|
||||||
|
Color.WHITE, Color.BLACK, Color(0.4, 0.2, 0.1),
|
||||||
|
Color(0.8, 0.6, 0.4), Color(0.6, 0.3, 0.1), Color(0.9, 0.7, 0.5),
|
||||||
|
Color(0.2, 0.2, 0.2), Color(0.5, 0.5, 0.5), Color(0.5, 0.8, 0.2),
|
||||||
|
Color(0.9, 0.5, 0.1), Color(0.8, 0.3, 0.9), Color(1.0, 0.9, 0.2),
|
||||||
|
Color(1.0, 0.5, 0.8), Color(0.9, 0.2, 0.2), Color(0.2, 0.9, 0.9),
|
||||||
|
Color(0.6, 0.2, 0.9), Color(0.9, 0.7, 0.2), Color(0.3, 0.9, 0.3),
|
||||||
|
Color(0.2, 0.2, 0.9), Color(0.9, 0.4, 0.6), Color(0.5, 0.2, 0.8),
|
||||||
|
Color(0.9, 0.6, 0.1)
|
||||||
|
]
|
||||||
|
character_stats.setHairColor(hair_colors[appearance_rng.randi() % hair_colors.size()])
|
||||||
|
|
||||||
|
# Randomize facial hair based on race constraints
|
||||||
|
var facial_hair_style = 0
|
||||||
|
match selected_race:
|
||||||
|
"Dwarf":
|
||||||
|
facial_hair_style = appearance_rng.randi_range(1, 3)
|
||||||
|
"Elf":
|
||||||
|
facial_hair_style = 0
|
||||||
|
"Human":
|
||||||
|
facial_hair_style = 3 if appearance_rng.randf() < 0.5 else 0
|
||||||
|
character_stats.setFacialHair(facial_hair_style)
|
||||||
|
|
||||||
|
# Randomize facial hair color
|
||||||
|
if facial_hair_style > 0:
|
||||||
|
if appearance_rng.randf() < 0.3:
|
||||||
|
character_stats.setFacialHairColor(hair_colors[appearance_rng.randi() % hair_colors.size()])
|
||||||
|
else:
|
||||||
|
character_stats.setFacialHairColor(character_stats.hair_color)
|
||||||
|
|
||||||
|
# Randomize eyes
|
||||||
|
var eye_style = appearance_rng.randi_range(1, 14)
|
||||||
|
character_stats.setEyes(eye_style)
|
||||||
|
|
||||||
|
# Randomize eye color
|
||||||
|
var white_color = Color(0.9, 0.9, 0.9)
|
||||||
|
var other_eye_colors = [
|
||||||
|
Color(0.1, 0.1, 0.1), Color(0.2, 0.3, 0.8), Color(0.3, 0.7, 0.9),
|
||||||
|
Color(0.5, 0.8, 0.2), Color(0.9, 0.5, 0.1), Color(0.8, 0.3, 0.9),
|
||||||
|
Color(1.0, 0.9, 0.2), Color(1.0, 0.5, 0.8), Color(0.9, 0.2, 0.2),
|
||||||
|
Color(0.2, 0.9, 0.9), Color(0.6, 0.2, 0.9), Color(0.9, 0.7, 0.2),
|
||||||
|
Color(0.3, 0.9, 0.3), Color(0.2, 0.2, 0.9), Color(0.9, 0.4, 0.6),
|
||||||
|
Color(0.5, 0.2, 0.8), Color(0.9, 0.6, 0.1)
|
||||||
|
]
|
||||||
|
if appearance_rng.randf() < 0.75:
|
||||||
|
character_stats.setEyeColor(white_color)
|
||||||
|
else:
|
||||||
|
character_stats.setEyeColor(other_eye_colors[appearance_rng.randi() % other_eye_colors.size()])
|
||||||
|
|
||||||
|
# Randomize eyelashes
|
||||||
|
var eyelash_style = appearance_rng.randi_range(0, 8)
|
||||||
|
character_stats.setEyeLashes(eyelash_style)
|
||||||
|
|
||||||
|
# Randomize eyelash color
|
||||||
|
var eyelash_colors = [
|
||||||
|
Color(0.1, 0.1, 0.1), Color(0.2, 0.2, 0.2), Color(0.3, 0.2, 0.15),
|
||||||
|
Color(0.4, 0.3, 0.2), Color(0.5, 0.8, 0.2), Color(0.9, 0.5, 0.1),
|
||||||
|
Color(0.8, 0.3, 0.9), Color(1.0, 0.9, 0.2), Color(1.0, 0.5, 0.8),
|
||||||
|
Color(0.9, 0.2, 0.2), Color(0.9, 0.9, 0.9), Color(0.6, 0.2, 0.9)
|
||||||
|
]
|
||||||
|
if eyelash_style > 0:
|
||||||
|
character_stats.setEyelashColor(eyelash_colors[appearance_rng.randi() % eyelash_colors.size()])
|
||||||
|
|
||||||
|
# Randomize ears/addons based on race
|
||||||
|
match selected_race:
|
||||||
|
"Elf":
|
||||||
|
var elf_ear_style = skin_index + 1
|
||||||
|
character_stats.setEars(elf_ear_style)
|
||||||
|
_:
|
||||||
|
character_stats.setEars(0)
|
||||||
|
|
||||||
|
# Apply appearance to sprite layers
|
||||||
|
_apply_appearance_to_sprites()
|
||||||
|
|
||||||
|
print("Player ", name, " appearance re-initialized (preserved race=", selected_race, ")")
|
||||||
|
|
||||||
func _apply_appearance_to_sprites():
|
func _apply_appearance_to_sprites():
|
||||||
# Apply character_stats appearance to sprite layers
|
# Apply character_stats appearance to sprite layers
|
||||||
if not character_stats:
|
if not character_stats:
|
||||||
@@ -1036,34 +1160,33 @@ func _on_character_changed(_char: CharacterStats):
|
|||||||
equipment_data[slot_name] = null
|
equipment_data[slot_name] = null
|
||||||
_rpc_to_ready_peers("_sync_equipment", [equipment_data])
|
_rpc_to_ready_peers("_sync_equipment", [equipment_data])
|
||||||
|
|
||||||
# Sync race and base stats to all clients (for proper display)
|
# ALWAYS sync race and base stats to all clients (for proper display)
|
||||||
|
# This ensures new clients get appearance data even if they connect after initial setup
|
||||||
_rpc_to_ready_peers("_sync_race_and_stats", [character_stats.race, character_stats.baseStats.duplicate()])
|
_rpc_to_ready_peers("_sync_race_and_stats", [character_stats.race, character_stats.baseStats.duplicate()])
|
||||||
|
|
||||||
# Sync equipment and inventory to client (when server adds/removes items for a client player)
|
# Sync full appearance data (skin, hair, eyes, colors, etc.) to remote players
|
||||||
# This ensures joiners see items they pick up and equipment changes
|
# This ensures remote players have the exact same appearance as authority
|
||||||
# This must be checked separately from the authority-based sync because on the server,
|
var appearance_data = {
|
||||||
# a joiner's player has authority set to their peer_id, not the server's unique_id
|
"skin": character_stats.skin,
|
||||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and can_send_rpcs and is_inside_tree():
|
"hairstyle": character_stats.hairstyle,
|
||||||
var the_peer_id = get_multiplayer_authority()
|
"hair_color": character_stats.hair_color.to_html(true),
|
||||||
# Only sync if this is a client player (not server's own player)
|
"facial_hair": character_stats.facial_hair,
|
||||||
if the_peer_id != 0 and the_peer_id != multiplayer.get_unique_id():
|
"facial_hair_color": character_stats.facial_hair_color.to_html(true),
|
||||||
# Sync equipment
|
"eyes": character_stats.eyes,
|
||||||
var equipment_data = {}
|
"eye_color": character_stats.eye_color.to_html(true),
|
||||||
for slot_name in character_stats.equipment.keys():
|
"eye_lashes": character_stats.eye_lashes,
|
||||||
var item = character_stats.equipment[slot_name]
|
"eyelash_color": character_stats.eyelash_color.to_html(true),
|
||||||
if item:
|
"add_on": character_stats.add_on
|
||||||
equipment_data[slot_name] = item.save()
|
}
|
||||||
else:
|
_rpc_to_ready_peers("_sync_appearance", [appearance_data])
|
||||||
equipment_data[slot_name] = null
|
|
||||||
_sync_equipment.rpc_id(the_peer_id, equipment_data)
|
# Sync inventory changes to all clients
|
||||||
|
var inventory_data = []
|
||||||
# Sync inventory
|
for item in character_stats.inventory:
|
||||||
var inventory_data = []
|
if item:
|
||||||
for item in character_stats.inventory:
|
inventory_data.append(item.save())
|
||||||
if item:
|
_rpc_to_ready_peers("_sync_inventory", [inventory_data])
|
||||||
inventory_data.append(item.save())
|
print(name, " synced equipment, race, and inventory to all clients. Inventory size: ", inventory_data.size())
|
||||||
_sync_inventory.rpc_id(the_peer_id, inventory_data)
|
|
||||||
print(name, " syncing equipment and inventory to client peer_id=", the_peer_id, " inventory_size=", inventory_data.size())
|
|
||||||
|
|
||||||
func _get_player_color() -> Color:
|
func _get_player_color() -> Color:
|
||||||
# Legacy function - now returns white (no color tint)
|
# Legacy function - now returns white (no color tint)
|
||||||
@@ -1081,6 +1204,11 @@ func _is_player(obj) -> bool:
|
|||||||
|
|
||||||
# Helper function to get consistent object name for network sync
|
# Helper function to get consistent object name for network sync
|
||||||
func _get_object_name_for_sync(obj) -> String:
|
func _get_object_name_for_sync(obj) -> String:
|
||||||
|
# Check if object is still valid
|
||||||
|
if not is_instance_valid(obj) or not obj.is_inside_tree():
|
||||||
|
# Object is invalid or not in tree - return empty string to skip sync
|
||||||
|
return ""
|
||||||
|
|
||||||
# For interactable objects, use the consistent name (InteractableObject_X)
|
# For interactable objects, use the consistent name (InteractableObject_X)
|
||||||
if obj.name.begins_with("InteractableObject_"):
|
if obj.name.begins_with("InteractableObject_"):
|
||||||
return obj.name
|
return obj.name
|
||||||
@@ -1091,6 +1219,11 @@ func _get_object_name_for_sync(obj) -> String:
|
|||||||
if _is_player(obj):
|
if _is_player(obj):
|
||||||
return obj.name
|
return obj.name
|
||||||
# Last resort: use the node name (might be auto-generated like @CharacterBody2D@82)
|
# Last resort: use the node name (might be auto-generated like @CharacterBody2D@82)
|
||||||
|
# But only if it's a valid name (not auto-generated if possible)
|
||||||
|
if obj.name.begins_with("@") and obj.has_meta("object_index"):
|
||||||
|
# Try to use object_index instead of auto-generated name
|
||||||
|
var obj_index = obj.get_meta("object_index")
|
||||||
|
return "InteractableObject_%d" % obj_index
|
||||||
return obj.name
|
return obj.name
|
||||||
|
|
||||||
func _get_log_prefix() -> String:
|
func _get_log_prefix() -> String:
|
||||||
@@ -1625,6 +1758,11 @@ func _physics_process(delta):
|
|||||||
print("Player ", name, " (server) - all clients now ready! (waited ", time_since_last_ready, "s after last client)")
|
print("Player ", name, " (server) - all clients now ready! (waited ", time_since_last_ready, "s after last client)")
|
||||||
else:
|
else:
|
||||||
print("Player ", name, " (server) - all clients now ready! (no ready times tracked)")
|
print("Player ", name, " (server) - all clients now ready! (no ready times tracked)")
|
||||||
|
|
||||||
|
# Re-sync appearance to all clients now that they're all ready
|
||||||
|
# This ensures new clients get the correct appearance even if they connected after initial sync
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and character_stats and character_stats.race != "":
|
||||||
|
character_stats.character_changed.emit(character_stats)
|
||||||
|
|
||||||
# Sync position to all ready peers (clients and server)
|
# Sync position to all ready peers (clients and server)
|
||||||
# Only send if node is still valid and in tree
|
# Only send if node is still valid and in tree
|
||||||
@@ -2057,6 +2195,11 @@ func _handle_interactions():
|
|||||||
# Minimum charge time: 0.2 seconds, otherwise cancel
|
# Minimum charge time: 0.2 seconds, otherwise cancel
|
||||||
if charge_time < 0.2:
|
if charge_time < 0.2:
|
||||||
is_charging_bow = false
|
is_charging_bow = false
|
||||||
|
|
||||||
|
# CRITICAL: Sync bow charge end to other clients when cancelling
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_bow_charge_end.rpc()
|
||||||
|
|
||||||
print(name, " cancelled arrow (released too quickly, need at least 0.2s)")
|
print(name, " cancelled arrow (released too quickly, need at least 0.2s)")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -2189,9 +2332,11 @@ func _try_grab():
|
|||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||||
# Use consistent object name or index instead of path
|
# Use consistent object name or index instead of path
|
||||||
var obj_name = _get_object_name_for_sync(held_object)
|
var obj_name = _get_object_name_for_sync(held_object)
|
||||||
_rpc_to_ready_peers("_sync_initial_grab", [obj_name, grab_offset])
|
# CRITICAL: Validate object is still valid right before sending RPC
|
||||||
# Sync the grab state
|
if obj_name != "" and is_instance_valid(held_object) and held_object.is_inside_tree():
|
||||||
_rpc_to_ready_peers("_sync_grab", [obj_name, is_lifting, push_axis])
|
_rpc_to_ready_peers("_sync_initial_grab", [obj_name, grab_offset])
|
||||||
|
# Sync the grab state
|
||||||
|
_rpc_to_ready_peers("_sync_grab", [obj_name, is_lifting, push_axis])
|
||||||
|
|
||||||
print("Grabbed: ", closest_body.name)
|
print("Grabbed: ", closest_body.name)
|
||||||
|
|
||||||
@@ -2240,7 +2385,9 @@ func _lift_object():
|
|||||||
# Sync to network (non-blocking)
|
# Sync to network (non-blocking)
|
||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||||
var obj_name = _get_object_name_for_sync(held_object)
|
var obj_name = _get_object_name_for_sync(held_object)
|
||||||
_rpc_to_ready_peers("_sync_grab", [obj_name, true, push_axis])
|
# CRITICAL: Validate object is still valid right before sending RPC
|
||||||
|
if obj_name != "" and is_instance_valid(held_object) and held_object.is_inside_tree():
|
||||||
|
_rpc_to_ready_peers("_sync_grab", [obj_name, true, push_axis])
|
||||||
|
|
||||||
print("Lifted: ", held_object.name)
|
print("Lifted: ", held_object.name)
|
||||||
$SfxLift.play()
|
$SfxLift.play()
|
||||||
@@ -2282,7 +2429,9 @@ func _start_pushing():
|
|||||||
# Sync push state to network
|
# Sync push state to network
|
||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||||
var obj_name = _get_object_name_for_sync(held_object)
|
var obj_name = _get_object_name_for_sync(held_object)
|
||||||
_rpc_to_ready_peers("_sync_grab", [obj_name, false, push_axis]) # false = pushing, not lifting
|
# CRITICAL: Validate object is still valid right before sending RPC
|
||||||
|
if obj_name != "" and is_instance_valid(held_object) and held_object.is_inside_tree():
|
||||||
|
_rpc_to_ready_peers("_sync_grab", [obj_name, false, push_axis]) # false = pushing, not lifting
|
||||||
|
|
||||||
print("Pushing: ", held_object.name, " along axis: ", push_axis, " facing dir: ", push_direction_locked)
|
print("Pushing: ", held_object.name, " along axis: ", push_axis, " facing dir: ", push_direction_locked)
|
||||||
|
|
||||||
@@ -2816,7 +2965,9 @@ func _update_lifted_object():
|
|||||||
# Sync held object position over network
|
# Sync held object position over network
|
||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||||
var obj_name = _get_object_name_for_sync(held_object)
|
var obj_name = _get_object_name_for_sync(held_object)
|
||||||
_rpc_to_ready_peers("_sync_held_object_pos", [obj_name, held_object.global_position])
|
# CRITICAL: Validate object is still valid right before sending RPC
|
||||||
|
if obj_name != "" and is_instance_valid(held_object) and held_object.is_inside_tree():
|
||||||
|
_rpc_to_ready_peers("_sync_held_object_pos", [obj_name, held_object.global_position])
|
||||||
|
|
||||||
func _update_pushed_object():
|
func _update_pushed_object():
|
||||||
if held_object and is_instance_valid(held_object):
|
if held_object and is_instance_valid(held_object):
|
||||||
@@ -2898,7 +3049,9 @@ func _update_pushed_object():
|
|||||||
# Sync position over network
|
# Sync position over network
|
||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||||
var obj_name = _get_object_name_for_sync(held_object)
|
var obj_name = _get_object_name_for_sync(held_object)
|
||||||
_rpc_to_ready_peers("_sync_held_object_pos", [obj_name, held_object.global_position])
|
# CRITICAL: Validate object is still valid right before sending RPC
|
||||||
|
if obj_name != "" and is_instance_valid(held_object) and held_object.is_inside_tree():
|
||||||
|
_rpc_to_ready_peers("_sync_held_object_pos", [obj_name, held_object.global_position])
|
||||||
|
|
||||||
# Send RPCs only to peers who are ready to receive them
|
# Send RPCs only to peers who are ready to receive them
|
||||||
func _rpc_to_ready_peers(method: String, args: Array = []):
|
func _rpc_to_ready_peers(method: String, args: Array = []):
|
||||||
@@ -4071,31 +4224,126 @@ func _sync_stats_update(kills_count: int, coins_count: int):
|
|||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
||||||
# Client receives race and base stats from authority player
|
# Client receives race and base stats from authority player
|
||||||
if not is_multiplayer_authority():
|
# Accept initial sync (when race is empty), but reject changes if we're authority
|
||||||
|
print(name, " _sync_race_and_stats received: race=", race, " is_authority=", is_multiplayer_authority(), " current_race=", character_stats.race if character_stats else "null")
|
||||||
|
|
||||||
|
# CRITICAL: If we're the authority for this player, we should NOT process race syncs
|
||||||
|
# The authority player manages its own appearance and only syncs to others
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
# Only allow initial sync if race is empty (first time setup)
|
||||||
|
if character_stats and character_stats.race != "":
|
||||||
|
# We're authority and already have a race set - ignore updates
|
||||||
|
print(name, " _sync_race_and_stats REJECTED (we're authority and already have race)")
|
||||||
|
return
|
||||||
|
|
||||||
|
if character_stats:
|
||||||
character_stats.race = race
|
character_stats.race = race
|
||||||
character_stats.baseStats = base_stats
|
character_stats.baseStats = base_stats
|
||||||
|
|
||||||
|
# For remote players, we don't re-initialize appearance here
|
||||||
|
# Instead, we wait for _sync_appearance RPC which contains the full appearance data
|
||||||
|
# This ensures remote players have the exact same appearance as authority
|
||||||
|
|
||||||
|
# Update race-specific appearance parts (ears)
|
||||||
|
var skin_index = 0
|
||||||
|
if character_stats.skin != "":
|
||||||
|
var regex = RegEx.new()
|
||||||
|
regex.compile("Human(\\d+)\\.png")
|
||||||
|
var result = regex.search(character_stats.skin)
|
||||||
|
if result:
|
||||||
|
skin_index = int(result.get_string(1)) - 1
|
||||||
|
|
||||||
|
match race:
|
||||||
|
"Elf":
|
||||||
|
var elf_ear_style = skin_index + 1
|
||||||
|
character_stats.setEars(elf_ear_style)
|
||||||
|
|
||||||
|
# Give Elf starting bow and arrows to remote players
|
||||||
|
# (Authority players get this in _setup_player_appearance)
|
||||||
|
# Check if equipment is missing - give it regardless of whether race changed
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
var needs_equipment = false
|
||||||
|
if character_stats.equipment["mainhand"] == null or character_stats.equipment["offhand"] == null:
|
||||||
|
needs_equipment = true
|
||||||
|
else:
|
||||||
|
# Check if mainhand is not a bow or offhand is not arrows
|
||||||
|
var mainhand = character_stats.equipment["mainhand"]
|
||||||
|
var offhand = character_stats.equipment["offhand"]
|
||||||
|
if not mainhand or mainhand.item_name != "short_bow":
|
||||||
|
needs_equipment = true
|
||||||
|
elif not offhand or offhand.item_name != "arrow" or offhand.quantity < 3:
|
||||||
|
needs_equipment = true
|
||||||
|
|
||||||
|
if needs_equipment:
|
||||||
|
var starting_bow = ItemDatabase.create_item("short_bow")
|
||||||
|
var starting_arrows = ItemDatabase.create_item("arrow")
|
||||||
|
if starting_bow and starting_arrows:
|
||||||
|
starting_arrows.quantity = 3
|
||||||
|
character_stats.equipment["mainhand"] = starting_bow
|
||||||
|
character_stats.equipment["offhand"] = starting_arrows
|
||||||
|
_apply_appearance_to_sprites()
|
||||||
|
print("Elf player ", name, " (remote) received short bow and 3 arrows via race sync")
|
||||||
|
_:
|
||||||
|
character_stats.setEars(0)
|
||||||
|
|
||||||
print(name, " race and stats synced: race=", race, " STR=", base_stats.str, " PER=", base_stats.per)
|
print(name, " race and stats synced: race=", race, " STR=", base_stats.str, " PER=", base_stats.per)
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_appearance(appearance_data: Dictionary):
|
||||||
|
# Client receives full appearance data from authority player
|
||||||
|
# Apply it directly without any randomization
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
# We're authority - ignore appearance syncs for ourselves
|
||||||
|
return
|
||||||
|
|
||||||
|
if not character_stats:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Apply the synced appearance data directly
|
||||||
|
if appearance_data.has("skin"):
|
||||||
|
character_stats.skin = appearance_data["skin"]
|
||||||
|
if appearance_data.has("hairstyle"):
|
||||||
|
character_stats.hairstyle = appearance_data["hairstyle"]
|
||||||
|
if appearance_data.has("hair_color"):
|
||||||
|
character_stats.hair_color = Color(appearance_data["hair_color"])
|
||||||
|
if appearance_data.has("facial_hair"):
|
||||||
|
character_stats.facial_hair = appearance_data["facial_hair"]
|
||||||
|
if appearance_data.has("facial_hair_color"):
|
||||||
|
character_stats.facial_hair_color = Color(appearance_data["facial_hair_color"])
|
||||||
|
if appearance_data.has("eyes"):
|
||||||
|
character_stats.eyes = appearance_data["eyes"]
|
||||||
|
if appearance_data.has("eye_color"):
|
||||||
|
character_stats.eye_color = Color(appearance_data["eye_color"])
|
||||||
|
if appearance_data.has("eye_lashes"):
|
||||||
|
character_stats.eye_lashes = appearance_data["eye_lashes"]
|
||||||
|
if appearance_data.has("eyelash_color"):
|
||||||
|
character_stats.eyelash_color = Color(appearance_data["eyelash_color"])
|
||||||
|
if appearance_data.has("add_on"):
|
||||||
|
character_stats.add_on = appearance_data["add_on"]
|
||||||
|
|
||||||
|
# Apply appearance to sprites
|
||||||
|
_apply_appearance_to_sprites()
|
||||||
|
|
||||||
|
print(name, " appearance synced from authority")
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_equipment(equipment_data: Dictionary):
|
func _sync_equipment(equipment_data: Dictionary):
|
||||||
# Client receives equipment update from server or other clients
|
# Client receives equipment update from server or other clients
|
||||||
# Update equipment to match server/other players
|
# Update equipment to match server/other players
|
||||||
# Server also needs to accept equipment sync from clients (joiners) to update server's copy of joiner's player
|
|
||||||
if not character_stats:
|
if not character_stats:
|
||||||
return
|
return
|
||||||
|
|
||||||
# CRITICAL: Don't accept equipment syncs for our own player
|
# CRITICAL: Don't accept equipment syncs for our own player AFTER initial setup
|
||||||
# Each client manages their own equipment locally
|
# Accept initial sync (when all equipment is null), but reject changes if we're authority
|
||||||
if is_multiplayer_authority():
|
var has_any_equipment = false
|
||||||
print(name, " ignoring equipment sync (I'm the authority)")
|
for slot in character_stats.equipment.values():
|
||||||
return
|
if slot != null:
|
||||||
|
has_any_equipment = true
|
||||||
|
break
|
||||||
|
|
||||||
# On server, only accept if this is a client player (not server's own player)
|
if is_multiplayer_authority() and has_any_equipment:
|
||||||
if multiplayer.is_server():
|
# We're authority and already have equipment - ignore updates
|
||||||
var the_peer_id = get_multiplayer_authority()
|
return
|
||||||
# If this is the server's own player, ignore (server's own changes are handled differently)
|
|
||||||
if the_peer_id == 0 or the_peer_id == multiplayer.get_unique_id():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update equipment from data
|
# Update equipment from data
|
||||||
for slot_name in equipment_data.keys():
|
for slot_name in equipment_data.keys():
|
||||||
@@ -4105,6 +4353,13 @@ func _sync_equipment(equipment_data: Dictionary):
|
|||||||
else:
|
else:
|
||||||
character_stats.equipment[slot_name] = null
|
character_stats.equipment[slot_name] = null
|
||||||
|
|
||||||
|
# If we received equipment but don't have race yet, request race sync
|
||||||
|
# This handles the case where equipment sync arrives before race sync
|
||||||
|
if character_stats.race == "" and not is_multiplayer_authority():
|
||||||
|
# Request race sync from authority (they should send it, but if not, this ensures it)
|
||||||
|
# Actually, race should come via _sync_race_and_stats, so just wait for it
|
||||||
|
pass
|
||||||
|
|
||||||
# Update appearance
|
# Update appearance
|
||||||
_apply_appearance_to_sprites()
|
_apply_appearance_to_sprites()
|
||||||
print(name, " equipment synced: ", equipment_data.size(), " slots")
|
print(name, " equipment synced: ", equipment_data.size(), " slots")
|
||||||
@@ -4113,19 +4368,15 @@ func _sync_equipment(equipment_data: Dictionary):
|
|||||||
func _sync_inventory(inventory_data: Array):
|
func _sync_inventory(inventory_data: Array):
|
||||||
# Client receives inventory update from server
|
# Client receives inventory update from server
|
||||||
# Update inventory to match server's inventory
|
# Update inventory to match server's inventory
|
||||||
# CRITICAL: Don't accept inventory syncs for our own player
|
# Accept initial sync (when inventory is empty), but reject changes if we're authority
|
||||||
# Each client manages their own inventory locally (same as equipment)
|
if is_multiplayer_authority() and character_stats and character_stats.inventory.size() > 0:
|
||||||
if is_multiplayer_authority():
|
# We're authority and already have items - ignore updates
|
||||||
print(name, " ignoring inventory sync (I'm the authority)")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if multiplayer.is_server():
|
|
||||||
return # Server ignores this (it's the sender)
|
|
||||||
|
|
||||||
if not character_stats:
|
if not character_stats:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Clear and rebuild inventory from server data (only for OTHER players we're viewing)
|
# Clear and rebuild inventory from server data (only for OTHER players or initial sync)
|
||||||
character_stats.inventory.clear()
|
character_stats.inventory.clear()
|
||||||
for item_data in inventory_data:
|
for item_data in inventory_data:
|
||||||
if item_data != null:
|
if item_data != null:
|
||||||
@@ -4133,7 +4384,7 @@ func _sync_inventory(inventory_data: Array):
|
|||||||
|
|
||||||
# Emit character_changed to update UI
|
# Emit character_changed to update UI
|
||||||
character_stats.character_changed.emit(character_stats)
|
character_stats.character_changed.emit(character_stats)
|
||||||
print(name, " inventory synced from server: ", character_stats.inventory.size(), " items")
|
print(name, " inventory synced: ", character_stats.inventory.size(), " items")
|
||||||
|
|
||||||
func heal(amount: float):
|
func heal(amount: float):
|
||||||
if is_dead:
|
if is_dead:
|
||||||
|
|||||||
@@ -140,17 +140,26 @@ func _detect_trap(detecting_player: Node) -> void:
|
|||||||
sprite.modulate.a = 1.0
|
sprite.modulate.a = 1.0
|
||||||
|
|
||||||
# Sync detection to all clients (including server with call_local)
|
# Sync detection to all clients (including server with call_local)
|
||||||
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
# CRITICAL: Validate trap is still valid before sending RPC
|
||||||
|
# Use GameWorld RPC to avoid node path issues
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_inside_tree() and is_instance_valid(self):
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
_sync_trap_detected.rpc()
|
# Use GameWorld RPC with trap name instead of path
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_sync_trap_state_by_name"):
|
||||||
|
game_world._sync_trap_state_by_name.rpc(name, true, false) # detected=true, disarmed=false
|
||||||
|
|
||||||
print(detecting_player.name, " detected trap at ", global_position)
|
print(detecting_player.name, " detected trap at ", global_position)
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func _sync_trap_detected() -> void:
|
func _sync_trap_detected() -> void:
|
||||||
# Client receives trap detection notification
|
# Client receives trap detection notification
|
||||||
|
# CRITICAL: Validate trap is still valid before processing
|
||||||
|
if not is_instance_valid(self) or not is_inside_tree():
|
||||||
|
return
|
||||||
is_detected = true
|
is_detected = true
|
||||||
sprite.modulate.a = 1.0
|
if sprite:
|
||||||
|
sprite.modulate.a = 1.0
|
||||||
|
|
||||||
func _on_disarm_area_body_entered(body: Node) -> void:
|
func _on_disarm_area_body_entered(body: Node) -> void:
|
||||||
# Show "DISARM" text if player is Dwarf and trap is detected
|
# Show "DISARM" text if player is Dwarf and trap is detected
|
||||||
@@ -160,9 +169,23 @@ func _on_disarm_area_body_entered(body: Node) -> void:
|
|||||||
if not is_detected or is_disarmed:
|
if not is_detected or is_disarmed:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if player is Dwarf
|
# CRITICAL: Only show disarm text for LOCAL players who are Dwarves
|
||||||
if body.character_stats and body.character_stats.race == "Dwarf":
|
# Check if this player is the local player (has authority matching local peer ID)
|
||||||
_show_disarm_text(body)
|
var is_local = false
|
||||||
|
if body.has_method("is_multiplayer_authority") and body.is_multiplayer_authority():
|
||||||
|
# This player is controlled by the local client
|
||||||
|
is_local = true
|
||||||
|
elif multiplayer.has_multiplayer_peer():
|
||||||
|
# Check if this player's authority matches our local peer ID
|
||||||
|
var player_authority = body.get_multiplayer_authority()
|
||||||
|
var local_peer_id = multiplayer.get_unique_id()
|
||||||
|
if player_authority == local_peer_id:
|
||||||
|
is_local = true
|
||||||
|
|
||||||
|
if is_local:
|
||||||
|
# Check if player is Dwarf
|
||||||
|
if body.character_stats and body.character_stats.race == "Dwarf":
|
||||||
|
_show_disarm_text(body)
|
||||||
|
|
||||||
func _on_disarm_area_body_exited(body: Node) -> void:
|
func _on_disarm_area_body_exited(body: Node) -> void:
|
||||||
# Hide disarm text when player leaves area
|
# Hide disarm text when player leaves area
|
||||||
@@ -234,17 +257,26 @@ func _complete_disarm() -> void:
|
|||||||
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
||||||
|
|
||||||
# Sync disarm to all clients
|
# Sync disarm to all clients
|
||||||
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
# CRITICAL: Validate trap is still valid before sending RPC
|
||||||
|
# Use GameWorld RPC to avoid node path issues
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_inside_tree() and is_instance_valid(self):
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
_sync_trap_disarmed.rpc()
|
# Use GameWorld RPC with trap name instead of path
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_sync_trap_state_by_name"):
|
||||||
|
game_world._sync_trap_state_by_name.rpc(name, true, true) # detected=true, disarmed=true
|
||||||
|
|
||||||
print("Trap disarmed!")
|
print("Trap disarmed!")
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func _sync_trap_disarmed() -> void:
|
func _sync_trap_disarmed() -> void:
|
||||||
# Client receives trap disarm notification
|
# Client receives trap disarm notification
|
||||||
|
# CRITICAL: Validate trap is still valid before processing
|
||||||
|
if not is_instance_valid(self) or not is_inside_tree():
|
||||||
|
return
|
||||||
is_disarmed = true
|
is_disarmed = true
|
||||||
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
if sprite:
|
||||||
|
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
||||||
if activation_area:
|
if activation_area:
|
||||||
activation_area.monitoring = false
|
activation_area.monitoring = false
|
||||||
|
|
||||||
@@ -278,9 +310,14 @@ func _on_activation_area_body_shape_entered(_body_rid: RID, body: Node2D, _body_
|
|||||||
if not is_detected:
|
if not is_detected:
|
||||||
is_detected = true
|
is_detected = true
|
||||||
sprite.modulate.a = 1.0
|
sprite.modulate.a = 1.0
|
||||||
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
# CRITICAL: Validate trap is still valid before sending RPC
|
||||||
|
# Use GameWorld RPC to avoid node path issues
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_inside_tree() and is_instance_valid(self):
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
_sync_trap_detected.rpc()
|
# Use GameWorld RPC with trap name instead of path
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_sync_trap_state_by_name"):
|
||||||
|
game_world._sync_trap_state_by_name.rpc(name, true, false) # detected=true, disarmed=false
|
||||||
|
|
||||||
# Deal damage to player (with luck-based avoidance)
|
# Deal damage to player (with luck-based avoidance)
|
||||||
_deal_trap_damage(body)
|
_deal_trap_damage(body)
|
||||||
|
|||||||
Reference in New Issue
Block a user