From 4915a1d945926e1739db28cee60f2542d454b76d Mon Sep 17 00:00:00 2001 From: Elrinth Date: Fri, 26 Dec 2025 07:59:27 +0100 Subject: [PATCH] now both players can take damage --- src/scripts/Autoloads/multiplayer_manager.gd | 84 +++++++++--------- src/scripts/entities/player/player.gd | 89 +++++++++++++++++++- src/scripts/entities/world/pot.gd | 21 ++++- 3 files changed, 149 insertions(+), 45 deletions(-) diff --git a/src/scripts/Autoloads/multiplayer_manager.gd b/src/scripts/Autoloads/multiplayer_manager.gd index ed8cf4f..d32bcf7 100644 --- a/src/scripts/Autoloads/multiplayer_manager.gd +++ b/src/scripts/Autoloads/multiplayer_manager.gd @@ -233,42 +233,40 @@ func broadcast_chat_message(message: String): @rpc("any_peer", "reliable") func request_lift_pot(pot_path: NodePath, peer_id: int): if multiplayer.is_server(): - var pot = get_node_or_null(pot_path) + var entity = get_node_or_null(pot_path) var player = get_tree().get_current_scene().get_node("SpawnRoot").get_node_or_null(str(peer_id)) - if pot and "lift" in pot and pot.liftable and player: - # Clear grabbed entity if it's not the pot we're lifting - if player.grabbed_entity != null and player.grabbed_entity != pot and "release" in player.grabbed_entity: - player.grabbed_entity.release() - if player.grabbed_entity != pot: - player.grabbed_entity = null - # Don't clear grabbed_entity_path if we're lifting the same pot - # This prevents the pot from being released when we're trying to lift it - player.is_grabbing = false - player.is_lifting = true - # Set held_entity_path directly on server, use RPC for clients - player.held_entity_path = str(pot.get_path()) - # Send RPC to all other players (excluding server) - var all_players = get_tree().get_current_scene().get_node("SpawnRoot").get_children() - for p in all_players: - if p != player and p.has_method("set_held_entity_path_rpc"): - p.set_held_entity_path_rpc.rpc(str(pot.get_path())) - pot.lift(player) - # Set animation on the player who is lifting (the joiner) - player.current_animation = "LIFT" - # Sync animation to all clients - player.sync_animation.rpc("LIFT") - # Pot state is auto-synced via @export variables, no need for manual sync + if entity and player: + # Check if entity is a pot or a player + var is_pot = "lift" in entity and "liftable" in entity and entity.liftable + var is_player_entity = "is_player" in entity and entity.is_player and not entity.is_being_lifted - # Sync animation and entity to all clients - #print("MultiplayerManager syncing LIFT animation and held_entity to all clients") - # Send RPC to all players except the server (since server already has correct state) - #var all_players = get_tree().get_current_scene().get_node("SpawnRoot").get_children() - #for p in all_players: - #if p.has_method("sync_animation") and p.get_multiplayer_authority() != 1: - #print("MultiplayerManager syncing to player: ", p.name) - #p.sync_animation.rpc("LIFT") - #p.sync_held_entity.rpc(str(pot.get_path())) - #p.sync_grabbed_entity.rpc("") + if (is_pot or is_player_entity): + # Clear grabbed entity if it's not the entity we're lifting + if player.grabbed_entity != null and player.grabbed_entity != entity and "release" in player.grabbed_entity: + player.grabbed_entity.release() + if player.grabbed_entity != entity: + player.grabbed_entity = null + player.is_grabbing = false + # DON'T clear is_lifting - it should stay true while holding the pot + # is_lifting will be cleared when player releases button or throws + # Set held_entity_path directly on server, use RPC for clients + player.held_entity_path = str(entity.get_path()) + # Send RPC to the requesting player (joiner) to sync held_entity_path + player.set_held_entity_path_rpc.rpc(str(entity.get_path())) + # Send RPC to all other players (excluding server and requesting player) + var all_players = get_tree().get_current_scene().get_node("SpawnRoot").get_children() + for p in all_players: + if p != player and p.has_method("set_held_entity_path_rpc"): + p.set_held_entity_path_rpc.rpc(str(entity.get_path())) + + # Call lift on the entity (works for both pot and player) + if "lift" in entity: + entity.lift(player) + + # Set animation on the player who is lifting (the joiner) + player.current_animation = "LIFT" + # Sync animation to all clients + player.sync_animation.rpc("LIFT") @rpc("any_peer", "reliable") func request_put_down_pot(pot_path: NodePath, peer_id: int): @@ -287,16 +285,22 @@ func request_put_down_pot(pot_path: NodePath, peer_id: int): @rpc("any_peer", "reliable") func request_throw_pot(pot_path: NodePath, peer_id: int, direction: Vector2): if multiplayer.is_server(): - var pot = get_node_or_null(pot_path) + var entity = get_node_or_null(pot_path) var player = get_tree().get_current_scene().get_node("SpawnRoot").get_node_or_null(str(peer_id)) - if pot and player: - # Check if the pot is being held by this player - if pot.holder_peer_id == peer_id or (pot.holder != null and pot.holder.get_multiplayer_authority() == peer_id): - pot.throw(direction) + if entity and player: + # Check if the entity (pot or player) is being held by this player + var is_held = false + if "holder_peer_id" in entity: + is_held = entity.holder_peer_id == peer_id or (entity.holder != null and entity.holder.get_multiplayer_authority() == peer_id) + elif "is_being_lifted" in entity and entity.is_being_lifted: + is_held = entity.holder_peer_id == peer_id or (entity.holder != null and entity.holder.get_multiplayer_authority() == peer_id) + + if is_held and "throw" in entity: + entity.throw(direction) # Use RPC to clear held_entity_path on all players (including server) player.set_held_entity_path_rpc.rpc("") player.current_animation = "THROW" - # Pot state is auto-synced via @export variables, no need for manual sync + # Entity state is auto-synced via @export variables, no need for manual sync # Sync animation to all clients (entity sync is handled automatically by PlayerSynchronizer) var all_players = get_tree().get_current_scene().get_node("SpawnRoot").get_children() diff --git a/src/scripts/entities/player/player.gd b/src/scripts/entities/player/player.gd index 7326118..044aa18 100644 --- a/src/scripts/entities/player/player.gd +++ b/src/scripts/entities/player/player.gd @@ -285,7 +285,17 @@ func _physics_process(delta: float) -> void: if stats.hp > 0: #print("we are below", knockback_timer, ", delta was:", delta) stats.is_invulnerable = false + # Sync invulnerability cleared to clients + if multiplayer.is_server(): + sync_invulnerability.rpc(false) move_and_collide(velocity * delta) + else: + # Client also needs to decrement knockback_timer for visual effects + if knockback_timer > 0: + velocity = velocity.move_toward(Vector2.ZERO, 300 * delta) + knockback_timer -= delta + if knockback_timer <= 0.0: + knockback_timer = 0 if is_moving: # this only plays on server.... must play on client also somehow... if !$SfxWalk.playing and $SfxWalk/TimerWalk.is_stopped(): @@ -769,19 +779,40 @@ func die_sound(): pass func take_damage(iBody: Node2D, _iByWhoOrWhat: Node2D) -> void: + # Direct call version - used when called locally + var damager_pos = iBody.global_position if iBody != null else global_position + var damager_path = _iByWhoOrWhat.get_path() if _iByWhoOrWhat != null else "" + _take_damage_internal(damager_pos, damager_path, _iByWhoOrWhat) + +@rpc("any_peer", "reliable") +func take_damage_rpc(damager_position: Vector2, damager_path: String, damager_peer_id: int): + # RPC version - used when server calls on joiner's player node + # Get the actual damager node if we can + var _iByWhoOrWhat = null + if damager_path != "": + _iByWhoOrWhat = get_node_or_null(damager_path) + _take_damage_internal(damager_position, damager_path, _iByWhoOrWhat) + +func _take_damage_internal(damager_position: Vector2, damager_path: String, _iByWhoOrWhat: Node2D): + # Internal function that handles the actual damage logic + # This allows both direct calls and RPC calls to work if !stats.is_invulnerable: - take_dmg_sound.rpc() + # Apply damage (works on both server and client since each player has authority over themselves) stats.take_damage(13.0) stats.is_invulnerable = true knockback_timer = knockback_duration + + # Sync invulnerability state to all clients (so other players know this player is invulnerable) + sync_invulnerability.rpc(true) + if current_animation != "DAMAGE": time_since_last_frame = 0 current_frame = 0 current_animation = "DAMAGE" - # Calculate knockback direction from the damaging body - knockback_direction = (global_position - iBody.global_position).normalized() + # Calculate knockback direction from the damaging body position + knockback_direction = (global_position - damager_position).normalized() velocity = knockback_direction * knockback_strength _updateHp() if held_entity != null: @@ -819,7 +850,7 @@ func take_damage(iBody: Node2D, _iByWhoOrWhat: Node2D) -> void: pass else: _iByWhoOrWhat.stats.forceUpdate() - ' + ' # give score to other player... if current_animation != "DIE": @@ -831,6 +862,46 @@ func take_damage(iBody: Node2D, _iByWhoOrWhat: Node2D) -> void: #_updateScore.rpc() call_deferred("_on_died") return + +@rpc("any_peer", "reliable") +func take_damage_effects(damager_position: Vector2, damager_path: String): + # Client receives damage effects - play visual/audio but don't modify stats + # Note: is_invulnerable is already set to true by sync_invulnerability RPC + # We just need to set knockback_timer for visual effects + take_dmg_sound.rpc() + knockback_timer = knockback_duration + + if current_animation != "DAMAGE": + time_since_last_frame = 0 + current_frame = 0 + + current_animation = "DAMAGE" + + # Calculate knockback direction from the damaging body position + knockback_direction = (global_position - damager_position).normalized() + velocity = knockback_direction * knockback_strength + + if held_entity != null: + if multiplayer.is_server(): + held_entity.throw(knockback_direction) + else: + MultiplayerManager.request_throw_pot.rpc_id(1, held_entity.get_path(), multiplayer.get_unique_id(), knockback_direction) + held_entity = null + held_entity_path = "" + is_lifting = false + + if grabbed_entity != null and "release" in grabbed_entity: + grabbed_entity.release() + grabbed_entity = null + is_grabbing = false + + # Create damage number + if damage_number_scene: + var damage_number = damage_number_scene.instantiate() + get_tree().current_scene.call_deferred("add_child", damage_number) + damage_number.global_position = global_position + Vector2(0, -16) + damage_number.direction = Vector2(0, -1) + damage_number.label = "1" @rpc("call_local", "reliable") func sync_player_kills(iKills: int): @@ -854,6 +925,16 @@ func sync_animation(animation_name: String): current_animation = animation_name pass +@rpc("any_peer", "reliable") +func sync_invulnerability(is_invul: bool): + # Sync invulnerability state from server to clients + # This ensures both server and client have the same invulnerability state + stats.is_invulnerable = is_invul + # If invulnerability is cleared, also clear knockback_timer on client + if not is_invul: + knockback_timer = 0 + pass + # RPC functions removed - entity synchronization now handled by setter functions @rpc("reliable") diff --git a/src/scripts/entities/world/pot.gd b/src/scripts/entities/world/pot.gd index 41ed4b2..ea87a79 100644 --- a/src/scripts/entities/world/pot.gd +++ b/src/scripts/entities/world/pot.gd @@ -817,7 +817,26 @@ func _on_area_2d_collision_body_entered(body: Node2D) -> void: if abs(velocity.x) > 0.05 or abs(velocity.y) > 0.05: if "take_damage" in body or body is TileMapLayer or collider is TileMapLayer: if "take_damage" in body: - body.take_damage(self, thrown_by) + # Check if body is a player - if so, check if it's a joiner (needs RPC) or server (direct call) + # Otherwise, call directly (for enemies, etc.) + if "is_player" in body and body.is_player: + # Player - check if it's a joiner (needs RPC) or server (direct call) + # If the player's authority matches the server's unique ID, it's the server's own player + var player_authority = body.get_multiplayer_authority() + var is_server_player = (player_authority == 1) # Server's peer ID is 1 + + if is_server_player: + # Server's own player - call directly (server has authority) + body.take_damage(self, thrown_by) + else: + # Joiner player - use RPC with serializable parameters (joiner has authority) + var damager_pos = self.global_position + var damager_path = thrown_by.get_path() if thrown_by != null else "" + var damager_peer_id = thrown_by.get_multiplayer_authority() if thrown_by != null else 0 + body.take_damage_rpc.rpc(damager_pos, damager_path, damager_peer_id) + else: + # Non-player (enemy, etc.) - call directly + body.take_damage(self, thrown_by) elif collider != self and "breakPot" in collider: collider.take_damage(self, thrown_by) # create particles from pot: