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://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://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"]
|
||||
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
|
||||
@@ -284,14 +280,6 @@ random_pitch = 1.0630184
|
||||
streams_count = 1
|
||||
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]
|
||||
collision_mask = 67
|
||||
motion_mode = 1
|
||||
|
||||
@@ -164,6 +164,10 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
||||
|
||||
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
|
||||
if player_owner and player_owner.is_multiplayer_authority():
|
||||
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:
|
||||
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
|
||||
|
||||
# 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
|
||||
hit_targets[body] = true
|
||||
# Sync miss to all clients - arrow continues flying
|
||||
if is_inside_tree():
|
||||
_sync_arrow_miss.rpc(body.get_path())
|
||||
# CRITICAL: Validate body is still valid and use name instead of 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
|
||||
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
|
||||
hit_targets[body] = true
|
||||
# Sync dodge to all clients - arrow continues flying
|
||||
if is_inside_tree():
|
||||
_sync_arrow_dodge.rpc(body.get_path())
|
||||
# CRITICAL: Validate body is still valid and use name instead of 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
|
||||
print(body.name, " DODGED arrow! Arrow continues flying...")
|
||||
return
|
||||
@@ -246,8 +250,9 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
||||
body.rpc_take_damage.rpc(damage, attacker_pos, false)
|
||||
|
||||
# Sync hit to all clients - arrow sticks
|
||||
if is_inside_tree():
|
||||
_sync_arrow_hit.rpc(body.get_path())
|
||||
# CRITICAL: Validate body is still valid and use name instead of 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)
|
||||
return
|
||||
@@ -293,11 +298,22 @@ func _sync_arrow_collected():
|
||||
call_deferred("queue_free")
|
||||
|
||||
@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
|
||||
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:
|
||||
print("WARNING: Arrow hit target not found at path: ", target_path)
|
||||
print("WARNING: Arrow hit target not found: ", target_name)
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
@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
|
||||
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:
|
||||
hit_targets[target] = true
|
||||
print("Arrow synced as MISS - continuing through: ", target.name if target else "unknown")
|
||||
|
||||
@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
|
||||
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:
|
||||
hit_targets[target] = true
|
||||
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.
|
||||
func _process(delta: float) -> void:
|
||||
func _process(_delta: float) -> void:
|
||||
# Update camera to follow local players
|
||||
_update_camera()
|
||||
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:
|
||||
# Valid position - place trap
|
||||
var trap_world_x = world_x * tile_size + tile_size / 2
|
||||
var trap_world_y = world_y * 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.0
|
||||
|
||||
traps.append({
|
||||
"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
|
||||
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
|
||||
# which includes dungeon_data.enemies and calls _spawn_enemies() on the client.
|
||||
# So we don't need to sync them again with individual RPCs.
|
||||
@@ -430,14 +434,7 @@ func _send_initial_client_sync_with_retry(peer_id: int, local_count: int, retry_
|
||||
# Note: Interactable objects are also synced via _sync_dungeon RPC
|
||||
# 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.
|
||||
|
||||
# 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)
|
||||
)
|
||||
# NOTE: Torches are already in dungeon_data and are spawned from the blob, so we don't need to sync them separately.
|
||||
|
||||
# Sync door states to the new client (wait until after dungeon chunks)
|
||||
get_tree().create_timer(2.0).timeout.connect(func():
|
||||
@@ -3465,6 +3462,11 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
|
||||
_spawn_blocking_doors()
|
||||
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
|
||||
print("GameWorld: Client - Spawning room triggers...")
|
||||
_spawn_room_triggers()
|
||||
@@ -3885,6 +3887,11 @@ func _reassemble_dungeon_blob():
|
||||
_spawn_blocking_doors()
|
||||
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...")
|
||||
_spawn_room_triggers()
|
||||
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)
|
||||
|
||||
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):
|
||||
# Sync broken interactable objects to new client with retry logic
|
||||
# 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):
|
||||
# Clients spawn torch when server tells them to
|
||||
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")
|
||||
if not torch_scene:
|
||||
return
|
||||
|
||||
var entities_node = get_node_or_null("Entities")
|
||||
if entities_node:
|
||||
var torch = torch_scene.instantiate()
|
||||
torch.name = "Torch_%d" % entities_node.get_child_count()
|
||||
entities_node.add_child(torch)
|
||||
torch.global_position = torch_position
|
||||
torch.rotation_degrees = torch_rotation
|
||||
print("Client spawned torch at ", torch_position, " with rotation ", torch_rotation)
|
||||
var torch = torch_scene.instantiate()
|
||||
torch.name = "Torch_%d" % entities_node.get_child_count()
|
||||
entities_node.add_child(torch)
|
||||
torch.global_position = torch_position
|
||||
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):
|
||||
# Sync door states to new client with retry logic
|
||||
|
||||
@@ -403,6 +403,12 @@ func _ready():
|
||||
can_send_rpcs = true
|
||||
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():
|
||||
# Duplicate shader materials for sprites that use tint parameters
|
||||
# This prevents shared material state between players
|
||||
@@ -469,6 +475,12 @@ func _initialize_character_stats():
|
||||
func _reinitialize_appearance_with_seed(_seed_value: int):
|
||||
# Re-initialize appearance with the correct dungeon_seed
|
||||
# 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")
|
||||
if not game_world or game_world.dungeon_seed == 0:
|
||||
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)
|
||||
return
|
||||
|
||||
# Save current state (race, stats, equipment) before re-initializing
|
||||
# We need to preserve these because they might have been set correctly already
|
||||
var saved_race = character_stats.race
|
||||
# Save current state (stats, equipment) before re-initializing
|
||||
# Race and appearance will be re-randomized with correct seed (deterministic)
|
||||
var saved_stats = {
|
||||
"str": character_stats.baseStats.str,
|
||||
"dex": character_stats.baseStats.dex,
|
||||
@@ -519,11 +530,12 @@ func _reinitialize_appearance_with_seed(_seed_value: int):
|
||||
appearance_rng.seed = new_seed_value
|
||||
|
||||
# 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()
|
||||
|
||||
# Restore saved race, stats, and equipment (preserve them from before re-initialization)
|
||||
character_stats.setRace(saved_race) # Restore original race
|
||||
# Restore saved stats (but keep the newly randomized race/appearance from correct seed)
|
||||
# The race/appearance from _setup_player_appearance() is now correct (deterministic seed)
|
||||
character_stats.baseStats.str = saved_stats.str
|
||||
character_stats.baseStats.dex = saved_stats.dex
|
||||
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.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():
|
||||
var item_data = saved_equipment[slot_name]
|
||||
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)
|
||||
|
||||
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():
|
||||
# Apply character_stats appearance to sprite layers
|
||||
if not character_stats:
|
||||
@@ -1036,34 +1160,33 @@ func _on_character_changed(_char: CharacterStats):
|
||||
equipment_data[slot_name] = null
|
||||
_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()])
|
||||
|
||||
# Sync equipment and inventory to client (when server adds/removes items for a client player)
|
||||
# This ensures joiners see items they pick up and equipment changes
|
||||
# This must be checked separately from the authority-based sync because on the server,
|
||||
# a joiner's player has authority set to their peer_id, not the server's unique_id
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and can_send_rpcs and is_inside_tree():
|
||||
var the_peer_id = get_multiplayer_authority()
|
||||
# Only sync if this is a client player (not server's own player)
|
||||
if the_peer_id != 0 and the_peer_id != multiplayer.get_unique_id():
|
||||
# Sync equipment
|
||||
var equipment_data = {}
|
||||
for slot_name in character_stats.equipment.keys():
|
||||
var item = character_stats.equipment[slot_name]
|
||||
if item:
|
||||
equipment_data[slot_name] = item.save()
|
||||
else:
|
||||
equipment_data[slot_name] = null
|
||||
_sync_equipment.rpc_id(the_peer_id, equipment_data)
|
||||
# Sync full appearance data (skin, hair, eyes, colors, etc.) to remote players
|
||||
# This ensures remote players have the exact same appearance as authority
|
||||
var appearance_data = {
|
||||
"skin": character_stats.skin,
|
||||
"hairstyle": character_stats.hairstyle,
|
||||
"hair_color": character_stats.hair_color.to_html(true),
|
||||
"facial_hair": character_stats.facial_hair,
|
||||
"facial_hair_color": character_stats.facial_hair_color.to_html(true),
|
||||
"eyes": character_stats.eyes,
|
||||
"eye_color": character_stats.eye_color.to_html(true),
|
||||
"eye_lashes": character_stats.eye_lashes,
|
||||
"eyelash_color": character_stats.eyelash_color.to_html(true),
|
||||
"add_on": character_stats.add_on
|
||||
}
|
||||
_rpc_to_ready_peers("_sync_appearance", [appearance_data])
|
||||
|
||||
# Sync inventory
|
||||
var inventory_data = []
|
||||
for item in character_stats.inventory:
|
||||
if item:
|
||||
inventory_data.append(item.save())
|
||||
_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())
|
||||
# Sync inventory changes to all clients
|
||||
var inventory_data = []
|
||||
for item in character_stats.inventory:
|
||||
if item:
|
||||
inventory_data.append(item.save())
|
||||
_rpc_to_ready_peers("_sync_inventory", [inventory_data])
|
||||
print(name, " synced equipment, race, and inventory to all clients. Inventory size: ", inventory_data.size())
|
||||
|
||||
func _get_player_color() -> Color:
|
||||
# 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
|
||||
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)
|
||||
if obj.name.begins_with("InteractableObject_"):
|
||||
return obj.name
|
||||
@@ -1091,6 +1219,11 @@ func _get_object_name_for_sync(obj) -> String:
|
||||
if _is_player(obj):
|
||||
return obj.name
|
||||
# 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
|
||||
|
||||
func _get_log_prefix() -> String:
|
||||
@@ -1626,6 +1759,11 @@ func _physics_process(delta):
|
||||
else:
|
||||
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)
|
||||
# Only send if node is still valid and in tree
|
||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree() and is_instance_valid(self):
|
||||
@@ -2057,6 +2195,11 @@ func _handle_interactions():
|
||||
# Minimum charge time: 0.2 seconds, otherwise cancel
|
||||
if charge_time < 0.2:
|
||||
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)")
|
||||
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():
|
||||
# Use consistent object name or index instead of path
|
||||
var obj_name = _get_object_name_for_sync(held_object)
|
||||
_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])
|
||||
# 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_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)
|
||||
|
||||
@@ -2240,7 +2385,9 @@ func _lift_object():
|
||||
# Sync to network (non-blocking)
|
||||
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)
|
||||
_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)
|
||||
$SfxLift.play()
|
||||
@@ -2282,7 +2429,9 @@ func _start_pushing():
|
||||
# Sync push state to network
|
||||
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)
|
||||
_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)
|
||||
|
||||
@@ -2816,7 +2965,9 @@ func _update_lifted_object():
|
||||
# Sync held object position over network
|
||||
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)
|
||||
_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():
|
||||
if held_object and is_instance_valid(held_object):
|
||||
@@ -2898,7 +3049,9 @@ func _update_pushed_object():
|
||||
# Sync position over network
|
||||
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)
|
||||
_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
|
||||
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")
|
||||
func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
||||
# 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.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)
|
||||
|
||||
@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")
|
||||
func _sync_equipment(equipment_data: Dictionary):
|
||||
# Client receives equipment update from server or other clients
|
||||
# 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:
|
||||
return
|
||||
|
||||
# CRITICAL: Don't accept equipment syncs for our own player
|
||||
# Each client manages their own equipment locally
|
||||
if is_multiplayer_authority():
|
||||
print(name, " ignoring equipment sync (I'm the authority)")
|
||||
return
|
||||
# CRITICAL: Don't accept equipment syncs for our own player AFTER initial setup
|
||||
# Accept initial sync (when all equipment is null), but reject changes if we're authority
|
||||
var has_any_equipment = false
|
||||
for slot in character_stats.equipment.values():
|
||||
if slot != null:
|
||||
has_any_equipment = true
|
||||
break
|
||||
|
||||
# On server, only accept if this is a client player (not server's own player)
|
||||
if multiplayer.is_server():
|
||||
var the_peer_id = get_multiplayer_authority()
|
||||
# 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
|
||||
if is_multiplayer_authority() and has_any_equipment:
|
||||
# We're authority and already have equipment - ignore updates
|
||||
return
|
||||
|
||||
# Update equipment from data
|
||||
for slot_name in equipment_data.keys():
|
||||
@@ -4105,6 +4353,13 @@ func _sync_equipment(equipment_data: Dictionary):
|
||||
else:
|
||||
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
|
||||
_apply_appearance_to_sprites()
|
||||
print(name, " equipment synced: ", equipment_data.size(), " slots")
|
||||
@@ -4113,19 +4368,15 @@ func _sync_equipment(equipment_data: Dictionary):
|
||||
func _sync_inventory(inventory_data: Array):
|
||||
# Client receives inventory update from server
|
||||
# Update inventory to match server's inventory
|
||||
# CRITICAL: Don't accept inventory syncs for our own player
|
||||
# Each client manages their own inventory locally (same as equipment)
|
||||
if is_multiplayer_authority():
|
||||
print(name, " ignoring inventory sync (I'm the authority)")
|
||||
# Accept initial sync (when inventory is empty), but reject changes if we're authority
|
||||
if is_multiplayer_authority() and character_stats and character_stats.inventory.size() > 0:
|
||||
# We're authority and already have items - ignore updates
|
||||
return
|
||||
|
||||
if multiplayer.is_server():
|
||||
return # Server ignores this (it's the sender)
|
||||
|
||||
if not character_stats:
|
||||
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()
|
||||
for item_data in inventory_data:
|
||||
if item_data != null:
|
||||
@@ -4133,7 +4384,7 @@ func _sync_inventory(inventory_data: Array):
|
||||
|
||||
# Emit character_changed to update UI
|
||||
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):
|
||||
if is_dead:
|
||||
|
||||
@@ -140,17 +140,26 @@ func _detect_trap(detecting_player: Node) -> void:
|
||||
sprite.modulate.a = 1.0
|
||||
|
||||
# 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():
|
||||
_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)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func _sync_trap_detected() -> void:
|
||||
# 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
|
||||
sprite.modulate.a = 1.0
|
||||
if sprite:
|
||||
sprite.modulate.a = 1.0
|
||||
|
||||
func _on_disarm_area_body_entered(body: Node) -> void:
|
||||
# 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:
|
||||
return
|
||||
|
||||
# Check if player is Dwarf
|
||||
if body.character_stats and body.character_stats.race == "Dwarf":
|
||||
_show_disarm_text(body)
|
||||
# CRITICAL: Only show disarm text for LOCAL players who are Dwarves
|
||||
# Check if this player is the local player (has authority matching local peer ID)
|
||||
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:
|
||||
# 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)
|
||||
|
||||
# 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():
|
||||
_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!")
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func _sync_trap_disarmed() -> void:
|
||||
# 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
|
||||
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:
|
||||
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:
|
||||
is_detected = true
|
||||
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():
|
||||
_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_trap_damage(body)
|
||||
|
||||
Reference in New Issue
Block a user