fix alot of shit for webrtc to work
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user