fix alot of shit for webrtc to work

This commit is contained in:
2026-01-17 10:19:51 +01:00
parent f71b510cfc
commit eb718fa990
68 changed files with 6616 additions and 917 deletions

View File

@@ -263,8 +263,6 @@ func _ready():
_setup_player_appearance()
# Authority is set by player_manager after adding to scene
# Just log it here
print("Player ", name, " ready. Authority: ", get_multiplayer_authority(), " Is local: ", is_local_player)
# Hide interaction indicator by default
if interaction_indicator:
@@ -385,7 +383,6 @@ func _initialize_character_stats():
appearance_rng = RandomNumberGenerator.new()
var seed_value = hash(str(peer_id) + "_" + str(local_player_index))
appearance_rng.seed = seed_value
print(name, " appearance/stats seed: ", seed_value, " (peer_id: ", peer_id, ", local_index: ", local_player_index, ")")
# Create character stats
character_stats = CharacterStats.new()
@@ -415,13 +412,7 @@ func _randomize_stats():
character_stats.baseStats.cha = appearance_rng.randi_range(8, 12)
character_stats.baseStats.lck = appearance_rng.randi_range(8, 12)
print(name, " randomized stats: STR=", character_stats.baseStats.str,
" DEX=", character_stats.baseStats.dex,
" INT=", character_stats.baseStats.int,
" END=", character_stats.baseStats.end,
" WIS=", character_stats.baseStats.wis,
" CHA=", character_stats.baseStats.cha,
" LCK=", character_stats.baseStats.lck)
# Stats randomized (verbose logging removed)
func _setup_player_appearance():
# Randomize appearance - players spawn "bare" (naked, no equipment)
@@ -600,10 +591,7 @@ func _apply_appearance_to_sprites():
if sprite_weapon:
sprite_weapon.texture = null # Weapons don't use character sprite layers
print(name, " appearance applied: skin=", character_stats.skin,
" hair=", character_stats.hairstyle,
" facial_hair=", character_stats.facial_hair,
" eyes=", character_stats.eyes)
# Appearance applied (verbose logging removed)
func _on_character_changed(_char: CharacterStats):
# Update appearance when character stats change (e.g., equipment)
@@ -619,7 +607,7 @@ func _on_character_changed(_char: CharacterStats):
equipment_data[slot_name] = item.save() # Serialize item data
else:
equipment_data[slot_name] = null
_sync_equipment.rpc(equipment_data)
_rpc_to_ready_peers("_sync_equipment", [equipment_data])
# 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
@@ -661,6 +649,88 @@ func _is_player(obj) -> bool:
# Check if it's a player by looking for player-specific properties
return obj.is_in_group("player") or ("is_local_player" in obj and "peer_id" in obj)
# Helper function to get consistent object name for network sync
func _get_object_name_for_sync(obj) -> String:
# For interactable objects, use the consistent name (InteractableObject_X)
if obj.name.begins_with("InteractableObject_"):
return obj.name
if obj.has_meta("object_index"):
var obj_index = obj.get_meta("object_index")
return "InteractableObject_%d" % obj_index
# For players, use their unique name
if _is_player(obj):
return obj.name
# Last resort: use the node name (might be auto-generated like @CharacterBody2D@82)
return obj.name
func _get_log_prefix() -> String:
if multiplayer.has_multiplayer_peer():
return "[H] " if multiplayer.is_server() else "[J] "
return ""
func _can_place_down_at(place_pos: Vector2, placed_obj: Node) -> bool:
var space_state = get_world_2d().direct_space_state
var placed_shape = _get_collision_shape_for(placed_obj)
if not placed_shape:
# Fallback to 16x16
placed_shape = RectangleShape2D.new()
placed_shape.size = Vector2(16, 16)
var params = PhysicsShapeQueryParameters2D.new()
params.shape = placed_shape
params.transform = Transform2D(0.0, place_pos)
params.collision_mask = 1 | 2 | 64 # Players, objects, walls
params.exclude = [self, placed_obj]
var hits = space_state.intersect_shape(params, 8)
return hits.is_empty()
func _find_closest_place_pos(direction: Vector2, placed_obj: Node) -> Vector2:
var dir = direction.normalized()
if dir.length() < 0.1:
dir = last_movement_direction.normalized()
if dir.length() < 0.1:
dir = Vector2.RIGHT
var player_extent = _get_collision_extent(self)
var obj_extent = _get_collision_extent(placed_obj)
# Start just outside player + object bounds
var start_dist = max(8.0, player_extent + obj_extent + 1.0)
var max_dist = start_dist + 32.0
var step = 2.0
var best_pos = global_position + dir * max_dist
for d in range(int(start_dist), int(max_dist) + 1, int(step)):
var test_pos = global_position + dir * float(d)
if _can_place_down_at(test_pos, placed_obj):
return test_pos
return best_pos
func _get_collision_shape_for(node: Node) -> Shape2D:
if not node:
return null
var shape_node = node.get_node_or_null("CollisionShape2D")
if not shape_node:
shape_node = node.find_child("CollisionShape2D", true, false)
if shape_node and "shape" in shape_node:
return shape_node.shape
return null
func _get_collision_extent(node: Node) -> float:
var shape = _get_collision_shape_for(node)
if shape is RectangleShape2D:
return max(shape.size.x, shape.size.y) * 0.5
if shape is CapsuleShape2D:
return shape.radius + shape.height * 0.5
if shape is CircleShape2D:
return shape.radius
if shape is ConvexPolygonShape2D:
var rect = shape.get_rect()
return max(rect.size.x, rect.size.y) * 0.5
# Fallback
return 8.0
func _update_animation(delta):
# Update animation frame timing
time_since_last_frame += delta
@@ -1031,15 +1101,10 @@ func _physics_process(delta):
else:
print("Player ", name, " (server) - all clients now ready! (no ready times tracked)")
# On server, also wait a bit after setting all_clients_ready to ensure nodes are registered
if not multiplayer.is_server():
_sync_position.rpc(position, velocity, position_z, is_airborne, current_direction, current_animation)
elif all_clients_ready:
# Wait an additional 0.2 seconds after setting all_clients_ready before sending RPCs
var current_time = Time.get_ticks_msec() / 1000.0
var time_since_ready = current_time - all_clients_ready_time
if time_since_ready >= 0.2:
_sync_position.rpc(position, velocity, position_z, is_airborne, current_direction, current_animation)
# 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):
_rpc_to_ready_peers("_sync_position", [position, velocity, position_z, is_airborne, current_direction, current_animation])
# Always move and slide to maintain horizontal velocity
# When airborne, velocity is set by throw and decreases with friction
@@ -1396,8 +1461,12 @@ func _handle_interactions():
# Gamepad (X button)
attack_just_pressed = Input.is_joy_button_pressed(input_device, JOY_BUTTON_X)
if attack_just_pressed and can_attack and not is_lifting and not is_pushing:
_perform_attack()
if attack_just_pressed and can_attack:
if is_lifting:
# Attack while lifting -> throw immediately (no movement required)
_force_throw_held_object(last_movement_direction)
elif not is_pushing:
_perform_attack()
# Note: just_grabbed_this_frame is reset when we block a release, or stays true if we grabbed this frame
# This ensures it persists to the next frame to block immediate release
@@ -1475,9 +1544,11 @@ func _try_grab():
# Sync initial grab to network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_initial_grab.rpc(held_object.get_path(), grab_offset)
# 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
_sync_grab.rpc(held_object.get_path(), is_lifting, push_axis)
_rpc_to_ready_peers("_sync_grab", [obj_name, is_lifting, push_axis])
print("Grabbed: ", closest_body.name)
@@ -1525,7 +1596,8 @@ 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():
_sync_grab.rpc(held_object.get_path(), true, push_axis)
var obj_name = _get_object_name_for_sync(held_object)
_rpc_to_ready_peers("_sync_grab", [obj_name, true, push_axis])
print("Lifted: ", held_object.name)
$SfxLift.play()
@@ -1565,7 +1637,8 @@ 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():
_sync_grab.rpc(held_object.get_path(), false, push_axis) # false = pushing, not lifting
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
print("Pushing: ", held_object.name, " along axis: ", push_axis, " facing dir: ", push_direction_locked)
@@ -1598,7 +1671,8 @@ func _stop_pushing():
# Sync release to network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_release.rpc(released_obj.get_path())
var obj_name = _get_object_name_for_sync(released_obj)
_rpc_to_ready_peers("_sync_release", [obj_name])
# Release the object and re-enable collision completely
if _is_box(released_obj):
@@ -1660,6 +1734,99 @@ func _throw_object():
is_lifting = false
is_pushing = false
# Re-enable collision completely
if _is_box(thrown_obj):
# Box: set position and physics first
thrown_obj.global_position = throw_start_pos
# Set throw velocity for box (same force as player throw)
if "throw_velocity" in thrown_obj:
thrown_obj.throw_velocity = throw_direction * throw_force / thrown_obj.weight
if "is_frozen" in thrown_obj:
thrown_obj.is_frozen = false
# Make box airborne with same arc as players
if "is_airborne" in thrown_obj:
thrown_obj.is_airborne = true
thrown_obj.position_z = 2.5
thrown_obj.velocity_z = 100.0 # Scaled down for 1x scale
# Call on_thrown if available
if thrown_obj.has_method("on_thrown"):
thrown_obj.on_thrown(self, throw_direction * throw_force)
# ⚡ Delay collision re-enable to prevent self-collision
await get_tree().create_timer(0.1).timeout
if thrown_obj and is_instance_valid(thrown_obj):
thrown_obj.set_collision_layer_value(2, true)
thrown_obj.set_collision_mask_value(1, true)
thrown_obj.set_collision_mask_value(2, true)
thrown_obj.set_collision_mask_value(7, true)
elif _is_player(thrown_obj):
# Player: set position and physics first
thrown_obj.global_position = throw_start_pos
# Set horizontal velocity for the arc
thrown_obj.velocity = throw_direction * throw_force * 0.8 # Slightly reduced for arc
# Make player airborne with Z velocity
if "is_airborne" in thrown_obj:
thrown_obj.is_airborne = true
thrown_obj.position_z = 2.5 # Start slightly off ground
thrown_obj.velocity_z = 100.0 # Scaled down for 1x scale
if thrown_obj.has_method("set_being_held"):
thrown_obj.set_being_held(false)
# ⚡ Delay collision re-enable to prevent self-collision
await get_tree().create_timer(0.1).timeout
if thrown_obj and is_instance_valid(thrown_obj):
thrown_obj.set_collision_layer_value(1, true)
thrown_obj.set_collision_mask_value(1, true)
thrown_obj.set_collision_mask_value(7, true)
if thrown_obj.has_method("on_thrown"):
thrown_obj.on_thrown(self, throw_direction * throw_force)
# Play throw animation
_set_animation("THROW")
$SfxThrow.play()
# Sync throw 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(thrown_obj)
_rpc_to_ready_peers("_sync_throw", [obj_name, throw_start_pos, throw_direction * throw_force, name])
print("Threw ", thrown_obj.name, " from ", throw_start_pos, " with force: ", throw_direction * throw_force)
func _force_throw_held_object(direction: Vector2):
if not held_object or not is_lifting:
return
# Check if object can be thrown
if held_object.has_method("can_be_thrown") and not held_object.can_be_thrown():
# Can't throw this object, place it down instead
_place_down_object()
return
var throw_direction = direction.normalized()
if throw_direction.length() < 0.1:
throw_direction = last_movement_direction.normalized()
if throw_direction.length() < 0.1:
throw_direction = Vector2.RIGHT
# Position object at player's position before throwing
var throw_start_pos = global_position + throw_direction * 10 # Start slightly in front
# Store reference before clearing
var thrown_obj = held_object
# Clear state first (important!)
held_object = null
grab_offset = Vector2.ZERO
is_lifting = false
is_pushing = false
# Re-enable collision completely
if _is_box(thrown_obj):
# Box: set position and physics first
@@ -1718,7 +1885,8 @@ func _throw_object():
# Sync throw over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_throw.rpc(thrown_obj.get_path(), throw_start_pos, throw_direction * throw_force, get_path())
var obj_name = _get_object_name_for_sync(thrown_obj)
_rpc_to_ready_peers("_sync_throw", [obj_name, throw_start_pos, throw_direction * throw_force, name])
print("Threw ", thrown_obj.name, " from ", throw_start_pos, " with force: ", throw_direction * throw_force)
@@ -1727,10 +1895,13 @@ func _place_down_object():
return
# Place object in front of player based on last movement direction
var place_offset = last_movement_direction * 15 # Scaled down for 1x scale
var place_pos = global_position + place_offset
var place_pos = _find_closest_place_pos(last_movement_direction, held_object)
var placed_obj = held_object
if not _can_place_down_at(place_pos, placed_obj):
print("DEBUG: Place down blocked - space not free")
return
# Clear state
held_object = null
grab_offset = Vector2.ZERO
@@ -1775,7 +1946,8 @@ func _place_down_object():
# Sync place down over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_place_down.rpc(placed_obj.get_path(), place_pos)
var obj_name = _get_object_name_for_sync(placed_obj)
_rpc_to_ready_peers("_sync_place_down", [obj_name, place_pos])
print("Placed down ", placed_obj.name, " at ", place_pos)
@@ -1893,7 +2065,7 @@ func _perform_attack():
# Sync attack over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_attack.rpc(current_direction, attack_direction)
_rpc_to_ready_peers("_sync_attack", [current_direction, attack_direction])
# Reset attack cooldown (instant if cooldown is 0)
if attack_cooldown > 0:
@@ -1920,7 +2092,8 @@ 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():
_sync_held_object_pos.rpc(held_object.get_path(), held_object.global_position)
var obj_name = _get_object_name_for_sync(held_object)
_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):
@@ -1963,10 +2136,10 @@ func _update_pushed_object():
# Account for collision shape offset
var shape_offset = held_collision_shape.position if held_collision_shape else Vector2.ZERO
query.transform = Transform2D(0, target_pos + shape_offset)
query.collision_mask = 64 # Layer 7 = walls (bit 6 = 64)
query.collision_mask = 1 | 2 | 64 # Players, objects, walls
query.collide_with_areas = false
query.collide_with_bodies = true
query.exclude = [held_object.get_rid()] # Exclude the object itself
query.exclude = [held_object.get_rid(), get_rid()] # Exclude the object and the holder
var results = space_state.intersect_shape(query)
was_blocked = results.size() > 0
@@ -1976,11 +2149,11 @@ func _update_pushed_object():
# Fallback: use point query
var query = PhysicsPointQueryParameters2D.new()
query.position = target_pos
query.collision_mask = 64 # Layer 7 = walls (bit 6 = 64)
query.collision_mask = 1 | 2 | 64 # Players, objects, walls
query.collide_with_areas = false
query.collide_with_bodies = true
if held_object is CharacterBody2D:
query.exclude = [held_object.get_rid()]
query.exclude = [held_object.get_rid(), get_rid()]
var results = space_state.intersect_point(query)
was_blocked = results.size() > 0
@@ -2001,11 +2174,40 @@ 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():
_sync_held_object_pos.rpc(held_object.get_path(), held_object.global_position)
var obj_name = _get_object_name_for_sync(held_object)
_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 = []):
if not multiplayer.has_multiplayer_peer():
return
var game_world = get_tree().get_first_node_in_group("game_world")
# Server can use the ready-peer helper for safe fanout
if multiplayer.is_server() and game_world and game_world.has_method("_rpc_node_to_ready_peers"):
game_world._rpc_node_to_ready_peers(self, method, args)
return
# Clients: only send to peers marked ready by server
if game_world and "clients_ready" in game_world:
for target_peer_id in multiplayer.get_peers():
# Always allow sending to server (peer 1)
if target_peer_id == 1:
callv("rpc_id", [target_peer_id, method] + args)
continue
if game_world.clients_ready.has(target_peer_id) and game_world.clients_ready[target_peer_id]:
callv("rpc_id", [target_peer_id, method] + args)
else:
# Fallback: send to all peers
callv("rpc", [method] + args)
# Network sync
@rpc("any_peer", "unreliable")
func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, airborne: bool = false, dir: int = 0, anim: String = "IDLE"):
# Check if node still exists and is valid before processing
if not is_inside_tree() or not is_instance_valid(self):
return
# Only update if we're not the authority (remote player)
if not is_multiplayer_authority():
position = pos
@@ -2029,6 +2231,10 @@ func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, airborne: bo
@rpc("any_peer", "reliable")
func _sync_attack(direction: int, attack_dir: Vector2):
# Sync attack to other clients
# Check if node still exists and is valid before processing
if not is_inside_tree() or not is_instance_valid(self):
return
if not is_multiplayer_authority():
current_direction = direction as Direction
_set_animation("SWORD")
@@ -2036,6 +2242,10 @@ func _sync_attack(direction: int, attack_dir: Vector2):
# Delay before spawning sword slash
await get_tree().create_timer(0.15).timeout
# Check again after delay - node might have been destroyed
if not is_inside_tree() or not is_instance_valid(self):
return
# Spawn sword projectile on client
if sword_projectile_scene:
var projectile = sword_projectile_scene.instantiate()
@@ -2048,11 +2258,36 @@ func _sync_attack(direction: int, attack_dir: Vector2):
print(name, " performed synced attack!")
@rpc("any_peer", "reliable")
func _sync_throw(obj_path: NodePath, throw_pos: Vector2, force: Vector2, thrower_path: NodePath):
func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_name: String):
# Sync throw to all clients (RPC sender already threw on their side)
var obj = get_node_or_null(obj_path)
var thrower = get_node_or_null(thrower_path)
print("_sync_throw received: ", obj_path, " found obj: ", obj != null, " thrower: ", thrower != null, " is_authority: ", is_multiplayer_authority())
# Check if node is still valid and in tree
if not is_inside_tree():
return
# Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_") and entities_node:
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
var thrower = null
if entities_node:
thrower = entities_node.get_node_or_null(thrower_name)
print("_sync_throw received: ", obj_name, " found obj: ", obj != null, " thrower: ", thrower != null, " is_authority: ", is_multiplayer_authority())
if obj:
obj.global_position = throw_pos
@@ -2089,6 +2324,7 @@ func _sync_throw(obj_path: NodePath, throw_pos: Vector2, force: Vector2, thrower
obj.set_collision_layer_value(2, true)
obj.set_collision_mask_value(1, true)
obj.set_collision_mask_value(2, true)
obj.set_collision_mask_value(7, true)
elif is_player:
print("Syncing player throw on client! pos: ", throw_pos, " force: ", force)
# Player: set physics first
@@ -2109,12 +2345,36 @@ func _sync_throw(obj_path: NodePath, throw_pos: Vector2, force: Vector2, thrower
if obj and is_instance_valid(obj):
obj.set_collision_layer_value(1, true)
obj.set_collision_mask_value(1, true)
obj.set_collision_mask_value(7, true)
@rpc("any_peer", "reliable")
func _sync_initial_grab(obj_path: NodePath, _offset: Vector2):
func _sync_initial_grab(obj_name: String, _offset: Vector2):
# Sync initial grab to other clients
# Check if node is still valid and in tree
if not is_inside_tree():
return
if not is_multiplayer_authority():
var obj = get_node_or_null(obj_path)
# Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_"):
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
if obj:
# Disable collision for grabbed object
if _is_box(obj):
@@ -2125,13 +2385,36 @@ func _sync_initial_grab(obj_path: NodePath, _offset: Vector2):
obj.set_collision_layer_value(1, false)
obj.set_collision_mask_value(1, false)
print("Synced initial grab on client: ", obj_path)
print("Synced initial grab on client: ", obj_name)
@rpc("any_peer", "reliable")
func _sync_grab(obj_path: NodePath, is_lift: bool, axis: Vector2 = Vector2.ZERO):
func _sync_grab(obj_name: String, is_lift: bool, axis: Vector2 = Vector2.ZERO):
# Sync lift/push state to other clients
# Check if node is still valid and in tree
if not is_inside_tree():
return
if not is_multiplayer_authority():
var obj = get_node_or_null(obj_path)
# Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_"):
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
if obj:
if is_lift:
# Lifting - completely disable collision
@@ -2174,10 +2457,33 @@ func _sync_grab(obj_path: NodePath, is_lift: bool, axis: Vector2 = Vector2.ZERO)
print("Synced grab on client: lift=", is_lift, " axis=", axis)
@rpc("any_peer", "reliable")
func _sync_release(obj_path: NodePath):
func _sync_release(obj_name: String):
# Sync release to other clients
# Check if node is still valid and in tree
if not is_inside_tree():
return
if not is_multiplayer_authority():
var obj = get_node_or_null(obj_path)
# Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_"):
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
if obj:
# Re-enable collision completely
if _is_box(obj):
@@ -2198,10 +2504,33 @@ func _sync_release(obj_path: NodePath):
obj.set_being_held(false)
@rpc("any_peer", "reliable")
func _sync_place_down(obj_path: NodePath, place_pos: Vector2):
func _sync_place_down(obj_name: String, place_pos: Vector2):
# Sync placing down to other clients
# Check if node is still valid and in tree
if not is_inside_tree():
return
if not is_multiplayer_authority():
var obj = get_node_or_null(obj_path)
# Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_"):
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
if obj:
obj.global_position = place_pos
@@ -2247,10 +2576,33 @@ func _sync_teleport_position(new_pos: Vector2):
print(name, " teleported to ", new_pos, " (synced from server, is_authority: ", is_multiplayer_authority(), ")")
@rpc("any_peer", "unreliable")
func _sync_held_object_pos(obj_path: NodePath, pos: Vector2):
func _sync_held_object_pos(obj_name: String, pos: Vector2):
# Sync held object position to other clients
# Check if node is still valid and in tree
if not is_inside_tree():
return
if not is_multiplayer_authority():
var obj = get_node_or_null(obj_path)
# Find object by name (consistent name like InteractableObject_X)
var obj = null
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.is_inside_tree():
return # GameWorld not ready yet
var entities_node = game_world.get_node_or_null("Entities")
if entities_node:
obj = entities_node.get_node_or_null(obj_name)
# Fallback: if name lookup fails and name looks like InteractableObject_X, try object_index lookup
if not obj and obj_name.begins_with("InteractableObject_"):
var index_str = obj_name.substr(20) # Skip "InteractableObject_"
if index_str.is_valid_int():
var obj_index = index_str.to_int()
for child in entities_node.get_children():
if child.has_meta("object_index") and child.get_meta("object_index") == obj_index:
obj = child
break
if obj:
# Don't update position if object is airborne (being thrown)
if "is_airborne" in obj and obj.is_airborne:
@@ -2313,28 +2665,34 @@ func _break_free_from_holder():
# Sync break free over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_break_free.rpc(being_held_by.get_path(), struggle_direction)
_rpc_to_ready_peers("_sync_break_free", [being_held_by.name, struggle_direction])
struggle_time = 0.0
struggle_direction = Vector2.ZERO
being_held_by = null
@rpc("any_peer", "reliable")
func _sync_break_free(holder_path: NodePath, direction: Vector2):
var holder = get_node_or_null(holder_path)
func _sync_break_free(holder_name: String, direction: Vector2):
var holder = 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:
holder = entities_node.get_node_or_null(holder_name)
if holder and holder.has_method("_force_place_down"):
holder._force_place_down(direction)
func _force_place_down(direction: Vector2):
# Forced to place down held object in specified direction
if held_object and is_lifting:
var place_offset = direction.normalized() * 20
if place_offset.length() < 0.1:
place_offset = last_movement_direction * 20
var place_pos = position + place_offset
var place_pos = _find_closest_place_pos(direction, held_object)
var placed_obj = held_object
if not _can_place_down_at(place_pos, placed_obj):
print("DEBUG: Forced place down blocked - space not free")
return
# Clear state
held_object = null
grab_offset = Vector2.ZERO
@@ -2414,9 +2772,17 @@ func take_damage(amount: float, attacker_position: Vector2):
_show_damage_number(0.0, attacker_position, false, false, true) # is_dodged = true
# Sync dodge visual to other clients
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_sync_damage.rpc(0.0, attacker_position, false, false, true) # is_dodged = true
_rpc_to_ready_peers("_sync_damage", [0.0, attacker_position, false, false, true]) # is_dodged = true
return # No damage taken, exit early
# If taking damage while holding something, drop/throw immediately
if held_object:
if is_lifting:
var throw_dir = (global_position - attacker_position).normalized()
_force_throw_held_object(throw_dir)
else:
_stop_pushing()
# If not dodged, apply damage with DEF reduction
var actual_damage = amount
if character_stats:
@@ -2466,7 +2832,7 @@ func take_damage(amount: float, attacker_position: Vector2):
# Sync damage visual effects to other clients (including damage numbers)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_sync_damage.rpc(actual_damage, attacker_position)
_rpc_to_ready_peers("_sync_damage", [actual_damage, attacker_position])
# Check if dead - but wait for damage animation to play first
var health = character_stats.hp if character_stats else current_health
@@ -2517,7 +2883,8 @@ func _die():
# Sync release to network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_release.rpc(released_obj.get_path())
var obj_name = _get_object_name_for_sync(released_obj)
_rpc_to_ready_peers("_sync_release", [obj_name])
print(name, " released ", released_obj.name, " on death")
else:
@@ -2548,7 +2915,7 @@ func _die():
# Sync death over network (only authority sends)
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_death.rpc()
_rpc_to_ready_peers("_sync_death", [])
# Wait for DIE animation to complete (200+200+200+800 = 1400ms = 1.4s)
await get_tree().create_timer(1.4).timeout
@@ -2586,7 +2953,7 @@ func _die():
# THEN sync to other clients
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_force_holder_to_drop.rpc(other_player.get_path())
_rpc_to_ready_peers("_force_holder_to_drop", [other_player.name])
found_holder = true
break
@@ -2709,17 +3076,22 @@ func _respawn():
# Sync respawn over network (only authority sends)
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_sync_respawn.rpc(new_respawn_pos)
_rpc_to_ready_peers("_sync_respawn", [new_respawn_pos])
@rpc("any_peer", "reliable")
func _force_holder_to_drop(holder_path: NodePath):
func _force_holder_to_drop(holder_name: String):
# Force a specific player to drop what they're holding
_force_holder_to_drop_local(holder_path)
_force_holder_to_drop_local(holder_name)
func _force_holder_to_drop_local(holder_path: NodePath):
func _force_holder_to_drop_local(holder_name: String):
# Local function to clear holder's held object
print("_force_holder_to_drop_local called for holder path: ", holder_path)
var holder = get_node_or_null(holder_path)
print("_force_holder_to_drop_local called for holder: ", holder_name)
var holder = 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:
holder = entities_node.get_node_or_null(holder_name)
if holder and is_instance_valid(holder):
print(" Found holder: ", holder.name, ", their held_object: ", holder.held_object)
if holder.held_object == self:
@@ -2863,6 +3235,12 @@ func heal(amount: float):
func add_key(amount: int = 1):
keys += amount
print(name, " picked up ", amount, " key(s)! Total keys: ", keys)
# Sync key count to owning client (server authoritative)
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
var owner_peer_id = get_multiplayer_authority()
if owner_peer_id != 0 and owner_peer_id != multiplayer.get_unique_id():
_sync_keys.rpc_id(owner_peer_id, keys)
func has_key() -> bool:
return keys > 0
@@ -2870,10 +3248,17 @@ func has_key() -> bool:
func use_key():
if keys > 0:
keys -= 1
print(name, " used a key! Remaining keys: ", keys)
print(_get_log_prefix(), name, " used a key! Remaining keys: ", keys)
return true
return false
@rpc("any_peer", "reliable")
func _sync_keys(new_key_count: int):
# Sync key count to client
if not is_inside_tree():
return
keys = new_key_count
@rpc("authority", "reliable")
func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false):
# Show damage number (red, using dmg_numbers.png font) above player