extends CharacterBody2D var tileParticleScene = preload("res://scripts/components/TileParticle.tscn") var liftable = true var thrown_by = null var entity_id = "" var object_name = "bomb" @export var is_being_thrown = false @export var is_being_lifted = false: set(value): is_being_lifted = value @export var is_being_put_down = false: set(value): is_being_put_down = value @export var is_being_grabbed = false @export var is_moving = false @export var is_spawning = false @export var is_fused = false var throw_speed = 200 var throw_height = 30 var current_height = 0 var gravity = 800 var holder = null var flipFromWall = false # Add Z-axis variables similar to loot.gd @export var positionZ = 0.0 var velocityZ = 0.0 var accelerationZ = -330.0 # Gravity var bounceRestitution = 0.3 var minBounceVelocity = 60.0 var destroy_initiated = false @export var is_destroyed = false # Smooth lifting variables var target_position = Vector2.ZERO var lift_height = 12.0 # Height above player's head var lift_speed = 10.0 # Speed of smooth movement var put_down_start_pos = Vector2.ZERO var put_down_target_pos = Vector2.ZERO @export var lift_progress = 0.0 @export var re_enable_collision_after_time = 0.0 var re_enable_time = 0.08 var previousFrameVel = Vector2.ZERO var hasShownSmokePuffs = false var grab_follow_speed = 15.0 # Speed at which pot follows holder when grabbed var previous_holder_position = Vector2.ZERO # Track previous holder position to calculate movement delta var previous_client_position = Vector2.ZERO # Track previous position on client for movement detection var push_lead_factor = 1.1 # When pushing, pot moves slightly ahead (110% of player movement) var min_push_distance = 12.0 # Minimum distance pot should maintain from player when pushing to avoid blocking var _throw_collision_initialized = false # Track if collision state has been initialized for throw on client @export var holder_peer_id: int = 0: set(value): holder_peer_id = value # Clear state when holder is cleared if value == 0: holder = null # Always clear grabbed and lifted states when holder is cleared is_being_grabbed = false is_being_lifted = false elif value != 0: # Find the holder by peer ID var spawn_root = get_tree().get_current_scene().get_node("SpawnRoot") if spawn_root: holder = spawn_root.get_node_or_null(str(value)) func _ready() -> void: if is_spawning: liftable = false $Area2DCollision.set_deferred("monitoring", false) self.set_collision_layer_value(8, false) self.set_collision_mask_value(9, false) self.set_collision_mask_value(10, false) self.set_collision_mask_value(8, false) update_sprite_scale() pass indicate(false) pass func _physics_process(delta: float) -> void: # Update holder based on holder_peer_id for network sync # CRITICAL: holder_peer_id is the source of truth - if it's 0, there's no holder if holder_peer_id != 0: var player = get_tree().get_current_scene().get_node("SpawnRoot").get_node_or_null(str(holder_peer_id)) if player and holder != player: holder = player elif holder_peer_id == 0: # No holder - clear everything # BUT: Don't clear velocity if pot is being thrown (it needs velocity to fly) if holder != null: holder = null if is_being_grabbed and not is_being_thrown: is_being_grabbed = false if is_being_lifted and not is_being_thrown: is_being_lifted = false if velocity != Vector2.ZERO and not is_being_thrown: velocity = Vector2.ZERO # Handle lifted pot position on ALL clients for smooth following if is_being_lifted and holder: $GPUParticles2D.emitting = false if lift_progress < 1.0: lift_progress += delta * lift_speed lift_progress = min(lift_progress, 1.0) # Smoothly interpolate from current position to above holder during lifting # Use the same calculation on both server and client to ensure consistent Y position var target_pos = holder.global_position + Vector2(0, 0) if lift_progress < 1.0: global_position = global_position.lerp(target_pos, lift_progress) positionZ = lift_height * lift_progress else: # When fully lifted, maintain exact position above holder global_position = target_pos positionZ = lift_height update_sprite_scale() # Ensure sprite scale/offset is updated consistently else: # Debug: Check why pot is not following if is_being_lifted and !holder: # Fix inconsistent state if holder_peer_id == 0: is_being_lifted = false elif !is_being_lifted and holder: # Pot has holder but is_being_lifted is false - this is normal during transitions pass update_sprite_scale() if multiplayer.is_server(): # CRITICAL: If holder_peer_id is 0, ALWAYS clear grab state immediately # This must happen before ANY logic runs to prevent movement after release # BUT: Don't clear velocity if pot is being thrown (it needs velocity to fly) if holder_peer_id == 0 and not is_being_thrown: is_being_grabbed = false if holder != null: holder = null if velocity != Vector2.ZERO: velocity = Vector2.ZERO if is_moving: is_moving = false if $SfxDrag2.playing: $SfxDrag2.stop() # Skip all grab logic if holder_peer_id is 0 pass if is_being_thrown: re_enable_collision_after_time -= delta if re_enable_collision_after_time <= 0.0: # enable collisions with players again after the delay self.set_collision_layer_value(8, true) # Collision layer is already enabled, just re-enable collision mask with players self.set_collision_mask_value(9, true) # Re-enable collision with players self.set_collision_mask_value(10, true) # Re-enable collision with players (if using both) re_enable_collision_after_time = 0 # Apply gravity to vertical movement velocityZ += accelerationZ * delta positionZ += velocityZ * delta if positionZ <= 0: # Pot has hit the ground positionZ = 0 $SfxLand.play() $GPUParticles2D.emitting = true $GPUParticles2D/TimerSmokeParticles.start() if abs(velocityZ) > minBounceVelocity: velocityZ = - velocityZ * bounceRestitution else: velocityZ = 0 is_being_thrown = false velocity = velocity.lerp(Vector2.ZERO, 0.5) if velocity.x == 0 and velocity.y == 0: thrown_by = null # Move horizontally if self.get_collision_layer_value(8) == false: move_and_slide() else: var collision = move_and_collide(velocity * delta) if collision: if positionZ == 0: is_being_thrown = false update_sprite_scale() elif is_being_put_down: lift_progress -= delta * lift_speed if lift_progress <= 0.0: $SfxLand.play() $GPUParticles2D.emitting = true $GPUParticles2D/TimerSmokeParticles.start() lift_progress = 0 is_being_put_down = false is_being_lifted = false holder = null positionZ = 0 # enable collisions again self.set_collision_layer_value(8, true) self.set_collision_mask_value(9, true) self.set_collision_mask_value(10, true) self.set_collision_mask_value(7, true) self.set_collision_mask_value(8, true) $Area2DCollision.set_deferred("monitoring", true) else: global_position = put_down_start_pos.lerp(put_down_target_pos, 1.0 - lift_progress) positionZ = lift_height * lift_progress update_sprite_scale() # CRITICAL: Only follow if holder_peer_id is NOT 0 (this is the source of truth) # Check holder_peer_id FIRST before is_being_grabbed to ensure we never follow when released # ONLY run this on server (pot has authority 1 = server) # DOUBLE CHECK: holder_peer_id must be non-zero AND holder must exist AND match # BUT: Don't run grab logic if pot is being thrown (throw logic handles movement) if holder_peer_id != 0 and is_being_grabbed and holder != null and not is_being_thrown: # Only follow if holder's authority matches holder_peer_id if holder.get_multiplayer_authority() == holder_peer_id: # Calculate how much the player has moved since last frame var player_movement = holder.global_position - previous_holder_position # Only move pot if player has moved if player_movement.length() > 0.01: # Get the locked grab direction to determine push/pull axis var locked_dir = Vector2.ZERO if "locked_grab_direction" in holder and holder.locked_grab_direction != Vector2.ZERO: locked_dir = holder.locked_grab_direction.normalized() if locked_dir != Vector2.ZERO: # Project player's movement onto the push/pull direction var movement_along_axis = player_movement.project(locked_dir) # Determine if pushing (moving in locked_dir direction) or pulling (moving opposite) var is_pushing = movement_along_axis.dot(locked_dir) > 0 if is_pushing: # PUSHING: Pot moves first, ahead of player # Move pot by slightly more than player movement so it leads (moves first) var push_movement = movement_along_axis * push_lead_factor global_position += push_movement velocity = push_movement / delta if delta > 0 else Vector2.ZERO else: # PULLING: Player moves first, pot follows behind # Move pot by the exact same amount as player (pot follows immediately) global_position += movement_along_axis velocity = movement_along_axis / delta if delta > 0 else Vector2.ZERO else: # No locked direction, just move pot by player's movement global_position += player_movement velocity = player_movement / delta if delta > 0 else Vector2.ZERO # Update previous position for next frame previous_holder_position = holder.global_position if velocity.length() > 1.0: is_moving = true $GPUParticles2D.emitting = true $GPUParticles2D/TimerSmokeParticles.start() if !$SfxDrag2.playing: $SfxDrag2.play() else: is_moving = false if $SfxDrag2.playing: $SfxDrag2.stop() previousFrameVel = velocity else: # Player hasn't moved - keep pot at current position velocity = Vector2.ZERO is_moving = false if $SfxDrag2.playing: $SfxDrag2.stop() # Still update previous position even if not moving previous_holder_position = holder.global_position else: # No valid holder, clear grab state and stop movement immediately is_being_grabbed = false is_moving = false if $SfxDrag2.playing: $SfxDrag2.stop() velocity = Vector2.ZERO holder = null holder_peer_id = 0 # Force clear to ensure no following pass # CRITICAL FINAL CHECK: If holder_peer_id is 0, STOP ALL MOVEMENT immediately # This must run AFTER all grab logic to catch any cases where holder_peer_id was set to 0 # but the pot is still trying to move # BUT: Don't clear velocity if pot is being thrown (it needs velocity to fly) if holder_peer_id == 0 and not is_being_thrown: if is_being_grabbed: is_being_grabbed = false if holder != null: holder = null if velocity != Vector2.ZERO: velocity = Vector2.ZERO if is_moving: is_moving = false if $SfxDrag2.playing: $SfxDrag2.stop() # Only handle free-falling if we're not being held (holder_peer_id == 0 is the source of truth) # BUT: Don't run free-falling logic if pot is being thrown (throw logic handles movement) if holder_peer_id == 0 and !is_being_lifted and !is_being_thrown: # it just spawned or is free-falling: # Apply gravity to vertical movement velocityZ += accelerationZ * delta positionZ += velocityZ * delta if positionZ <= 0: if is_spawning: is_spawning = false liftable = true $Area2DCollision.set_deferred("monitoring", true) self.set_collision_layer_value(8, true) self.set_collision_mask_value(9, true) self.set_collision_mask_value(10, true) self.set_collision_mask_value(8, true) # Pot has hit the ground positionZ = 0 if abs(velocityZ) > 30: $SfxLand.play() $GPUParticles2D.emitting = true $GPUParticles2D/TimerSmokeParticles.start() if abs(velocityZ) > minBounceVelocity: velocityZ = - velocityZ * bounceRestitution else: velocityZ = 0 velocity = velocity.lerp(Vector2.ZERO, 0.5) move_and_collide(velocity * delta) update_sprite_scale() pass else: # CRITICAL: If holder_peer_id is 0, ALWAYS clear grab state immediately (client side) # This must happen before any grab logic runs if holder_peer_id == 0: is_being_grabbed = false if holder != null: holder = null if is_fused and $AnimationPlayer.current_animation == "idle": if $SfxBombFuse.playing: $SfxBombFuse.play() $AnimationPlayer.play("fused") # for client:' if is_being_thrown: if $SfxDrag2.playing: $SfxDrag2.stop() # Timer is synced from server via sync_throw_timer RPC # If not initialized yet, wait for RPC (fallback initialization) if not _throw_collision_initialized: # Fallback: initialize if RPC hasn't arrived yet self.set_collision_layer_value(8, false) self.set_collision_mask_value(7, true) self.set_collision_mask_value(9, false) self.set_collision_mask_value(10, false) # CRITICAL: Set timer to a positive value to prevent immediate expiration re_enable_collision_after_time = re_enable_time _throw_collision_initialized = true # Apply the same throw movement logic on client for smooth movement # Handle collision re-enable timer (same as server) # CRITICAL: Only decrement and check timer if it's been initialized AND is positive if _throw_collision_initialized and re_enable_collision_after_time > 0.0: re_enable_collision_after_time -= delta if re_enable_collision_after_time <= 0.0: # enable collisions with players again after the delay self.set_collision_layer_value(8, true) # Collision layer is already enabled, just re-enable collision mask with players self.set_collision_mask_value(9, true) # Re-enable collision with players self.set_collision_mask_value(10, true) # Re-enable collision with players (if using both) re_enable_collision_after_time = 0 # Apply gravity to vertical movement velocityZ += accelerationZ * delta positionZ += velocityZ * delta if positionZ <= 0: # Pot has hit the ground positionZ = 0 if !$SfxLand.playing: $SfxLand.play() $GPUParticles2D.emitting = true $GPUParticles2D/TimerSmokeParticles.start() if abs(velocityZ) > minBounceVelocity: velocityZ = - velocityZ * bounceRestitution else: velocityZ = 0 is_being_thrown = false velocity = velocity.lerp(Vector2.ZERO, 0.5) if velocity.x == 0 and velocity.y == 0: thrown_by = null # Move horizontally using the same logic as server if self.get_collision_layer_value(8) == false: move_and_slide() else: var collision = move_and_collide(velocity * delta) if collision: if positionZ == 0: is_being_thrown = false update_sprite_scale() else: # Pot is no longer being thrown - reset initialization flag if _throw_collision_initialized: _throw_collision_initialized = false if is_being_put_down: if $SfxDrag2.playing: $SfxDrag2.stop() if !$SfxLand.playing: $SfxLand.play() $GPUParticles2D.emitting = true $GPUParticles2D/TimerSmokeParticles.start() elif is_being_lifted: if $SfxDrag2.playing: $SfxDrag2.stop() $GPUParticles2D.emitting = false # Update position on client to follow holder if holder: target_position = holder.global_position + Vector2(0, 0) if lift_progress < 1.0: lift_progress += delta * lift_speed lift_progress = min(lift_progress, 1.0) global_position = global_position.lerp(target_position, lift_progress) positionZ = lift_height * lift_progress else: # When fully lifted, maintain exact position above holder global_position = target_position positionZ = lift_height update_sprite_scale() # CRITICAL: On client, do NOT move the pot - only the server should move it # The pot's position is synced via replication from the server # We only handle visual/audio effects here elif holder_peer_id == 0: # No holder - ensure we're not in grabbed state if is_being_grabbed: is_being_grabbed = false is_moving = false if $SfxDrag2.playing: $SfxDrag2.stop() elif is_being_grabbed and holder_peer_id != 0: # Only handle visual/audio effects on client, don't move the pot # The server handles the actual movement and syncs position via replication if holder != null and holder.get_multiplayer_authority() == holder_peer_id: # Check if pot appears to be moving (based on position changes from server) var pot_movement = global_position - previous_client_position # If pot has moved, it's being pushed/pulled if pot_movement.length() > 0.5: is_moving = true $GPUParticles2D.emitting = true $GPUParticles2D/TimerSmokeParticles.start() if !$SfxDrag2.playing: $SfxDrag2.play() else: is_moving = false if $SfxDrag2.playing: $SfxDrag2.stop() # Update previous position for next frame previous_client_position = global_position else: # No valid holder, clear state is_being_grabbed = false is_moving = false if $SfxDrag2.playing: $SfxDrag2.stop() elif !is_being_grabbed: if is_spawning: if positionZ <= 0: $SfxLand.play() $GPUParticles2D.emitting = true $GPUParticles2D/TimerSmokeParticles.start() else: if $SfxLand.playing: $SfxLand.stop() if $SfxDrag2.playing: $SfxDrag2.stop() pass # Fix stuck put_down state if is_being_put_down and lift_progress <= 0: is_being_put_down = false update_sprite_scale() if is_destroyed and !destroy_initiated: destroy_initiated = true show_destroy_effect() # Update debug label if has_node("LabelPotStateNDirectionNSpeed"): var collision_layer_8 = self.get_collision_layer_value(8) var collision_str = "Col 8: %s" % ("TRUE" if collision_layer_8 else "FALSE") collision_str += "\nthrown? %s" % ("TRUE" if is_being_thrown else "FALSE") $LabelPotStateNDirectionNSpeed.text = collision_str pass pass func update_sprite_scale() -> void: # Calculate scale based on height # Maximum height will have scale 1.3, ground will have scale 1.0 var height_factor = positionZ / 50.0 # Assuming 50 is max height var posY = positionZ # Direct mapping of Z to Y offset var sc = 1.0 + (0.1 * height_factor) # Slightly less scale change than loot (0.3 instead of 0.8) $Sprite2D.scale = Vector2(sc, sc) $Sprite2D.offset.y = - posY # Also update shadow position and scale if is_being_lifted: $Sprite2D.z_as_relative = false $Sprite2D.z_index = 12 $Sprite2DShadow.offset.y = 0 # Base shadow position else: $Sprite2D.z_as_relative = true $Sprite2D.z_index = 0 $Sprite2DShadow.offset.y = 0 #$Sprite2DShadow.scale = Vector2(1.125 * sc, 0.5 * sc) # Scale shadow with height $Sprite2DShadow.scale = Vector2(1, 1) #$Sprite2DShadow.modulate. func throw(direction: Vector2, initial_velocity: float = 200): # When thrown, enable collision layer so pot can use move_and_collide # But disable collision mask with players temporarily (re-enabled after re_enable_collision_after_time) # Enable collision with walls so pot can bounce off walls self.set_collision_layer_value(8, false) # Enable pot's collision layer (needed for move_and_collide to work) self.set_collision_mask_value(7, true) # Enable collision with walls self.set_collision_mask_value(9, false) # Disable collision with players initially self.set_collision_mask_value(10, false) # Disable collision with players initially (if using both) $Area2DCollision.set_deferred("monitoring", true) $SfxThrow.play() is_being_lifted = false is_being_thrown = true is_being_put_down = false is_being_grabbed = false # Clear grab state thrown_by = holder holder = null holder_peer_id = 0 # Clear the network holder reference velocity = direction * initial_velocity velocityZ = throw_height positionZ = lift_height current_height = 0 re_enable_collision_after_time = re_enable_time # Sync timer to clients so they know when to re-enable collisions sync_throw_timer.rpc(re_enable_time) @rpc("any_peer", "reliable") func sync_throw_timer(timer_value: float): # Client receives timer value from server if not multiplayer.is_server(): # CRITICAL: Always set collision layer to false FIRST when receiving timer sync # This ensures it's false even if something else tried to set it to true self.set_collision_layer_value(8, false) self.set_collision_mask_value(7, true) # Enable collision with walls self.set_collision_mask_value(9, false) # Disable collision with players initially self.set_collision_mask_value(10, false) # Disable collision with players initially # Set timer value from server (ensures it's positive) re_enable_collision_after_time = timer_value _throw_collision_initialized = true @rpc("any_peer", "reliable") func throw_rpc(direction: Vector2, initial_velocity: float = 200): # Only execute on server to avoid conflicts if multiplayer.is_server(): throw(direction, initial_velocity) func grab(new_holder: CharacterBody2D) -> bool: if positionZ <= 0 and holder == null: # only allow grab if no previous owner and position is 0 $GPUParticles2D/TimerSmokeParticles.stop() # reset... holder = new_holder holder_peer_id = new_holder.get_multiplayer_authority() is_being_grabbed = true indicate(false) # Initialize previous position to current position so we can track movement previous_holder_position = new_holder.global_position previous_client_position = global_position # Initialize client position tracking # Disable pot's collision layer so grabber can't collide with it (prevents blocking when pushing) # Note: This also means other players can't collide with pot while it's grabbed # The pot uses direct position updates (not move_and_collide) so collisions aren't needed for movement self.set_collision_layer_value(8, false) # Disable pot's collision layer # Don't change pot's position - it should stay where it is and only move when player pushes/pulls return true return false func release(): # Clear all grab-related state # CRITICAL: Don't re-enable collision layer if pot is being thrown (throw logic handles it) if is_being_thrown: # Pot is being thrown - don't change collision layer, throw logic will handle it holder = null holder_peer_id = 0 is_being_grabbed = false hasShownSmokePuffs = false #velocity = Vector2.ZERO indicate(true) if $SfxDrag2.playing: $SfxDrag2.stop() return # CRITICAL: If we're not the server, we need to notify the server to release # The pot has authority 1 (server), so the server must be the one to clear holder_peer_id if not multiplayer.is_server(): # Client requests server to release - use holder_peer_id directly if holder_peer_id != 0: request_release_pot.rpc_id(1, get_path(), holder_peer_id) # Also clear locally for immediate visual feedback holder = null holder_peer_id = 0 is_being_grabbed = false hasShownSmokePuffs = false #velocity = Vector2.ZERO # Re-enable pot's collision layer when released #self.set_collision_layer_value(8, true) # Re-enable pot's collision layer indicate(true) if $SfxDrag2.playing: $SfxDrag2.stop() else: # Server can release directly holder = null holder_peer_id = 0 is_being_grabbed = false hasShownSmokePuffs = false #velocity = Vector2.ZERO # Re-enable pot's collision layer when released #self.set_collision_layer_value(8, true) # Re-enable pot's collision layer indicate(true) if $SfxDrag2.playing: $SfxDrag2.stop() pass @rpc("any_peer", "reliable") func request_release_pot(pot_path: NodePath, peer_id: int): if multiplayer.is_server(): var pot = 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.holder = null pot.holder_peer_id = 0 pot.is_being_grabbed = false pot.velocity = Vector2.ZERO # Re-enable pot's collision layer when released pot.set_collision_layer_value(8, true) # Re-enable pot's collision layer pot.indicate(true) if $SfxDrag2.playing: $SfxDrag2.stop() pass func lift(new_holder: CharacterBody2D): if (new_holder != holder and holder != null) and "lose_held_entity" in holder: # steal from holder holder.lose_held_entity(self) indicate(false) $Area2DCollision.set_deferred("monitoring", false) thrown_by = null holder = new_holder holder_peer_id = new_holder.get_multiplayer_authority() # disable collisions with walls and players when lifted self.set_collision_layer_value(8, false) # Disable pot's collision layer (so players can't collide with it) self.set_collision_mask_value(7, false) # Disable collision with walls self.set_collision_mask_value(8, false) # Disable collision with other pots self.set_collision_mask_value(9, false) # Disable collision with players self.set_collision_mask_value(10, false) # Disable collision with players (if using both) is_being_lifted = true is_being_grabbed = false is_being_thrown = false is_being_put_down = false lift_progress = 0.0 velocityZ = 0 # Store initial position for smooth lifting - don't change current position yet # The pot will smoothly glide from its current position to above the holder $SfxLand.play() @rpc("any_peer", "reliable") func lift_rpc(holder_path: NodePath): # Only execute on server to avoid conflicts if multiplayer.is_server(): # Find the holder by path var holder_node = get_node_or_null(holder_path) if holder_node and holder_node is CharacterBody2D: lift(holder_node) func put_down() -> bool: if not is_being_lifted or is_being_put_down: return false var dropDir = holder.last_direction dropDir.x *= 12 if dropDir.y > 0: dropDir.y *= 10 else: dropDir.y *= 10 put_down_target_pos = holder.global_position + dropDir # First check: Direct space state query for walls var space_state = get_world_2d().direct_space_state var params = PhysicsPointQueryParameters2D.new() params.position = put_down_target_pos params.collision_mask = 64 # Layer for walls (usually layer 7) params.collide_with_areas = true params.collide_with_bodies = true var results = space_state.intersect_point(params) if results.size() > 0: # Found overlapping walls at target position return false # Second check: Line of sight between player and target position var query = PhysicsRayQueryParameters2D.create( holder.global_position, put_down_target_pos, 64 # Wall collision mask ) query.collide_with_areas = true query.collide_with_bodies = true var result = space_state.intersect_ray(query) if result: # Hit something between player and target position return false # Third check: Make sure we're not placing on top of another pot or object params.collision_mask = 128 # Layer for pots/objects results = space_state.intersect_point(params) if results.size() > 0: # Found overlapping objects at target position return false $Area2DCollision.set_deferred("monitoring", false) # Position is valid, proceed with putting down self.set_collision_mask_value(7, true) # instantly reenenable collision with wall is_being_put_down = true is_being_lifted = false put_down_start_pos = global_position thrown_by = null holder = null holder_peer_id = 0 indicate(true) return true func remove(): var fade_tween = create_tween() fade_tween.set_trans(Tween.TRANS_CUBIC) fade_tween.set_ease(Tween.EASE_OUT) fade_tween.tween_property($Sprite2D, "modulate:a", 0.0, 0.6) #await fade_tween.finished await $SfxShatter.finished if $SfxLand.playing: $SfxLand.stop() if $SfxDrag2.playing: $SfxDrag2.stop() $GPUParticles2D.emitting = false #$SfxShatter.stop() if multiplayer.is_server(): call_deferred("queue_free") pass func create4TileParticles(): var sprite_texture = $Sprite2D.texture var frame_width = sprite_texture.get_width() / $Sprite2D.hframes var frame_height = sprite_texture.get_height() / $Sprite2D.vframes var frame_x = ($Sprite2D.frame % $Sprite2D.hframes) * frame_width var frame_y = ($Sprite2D.frame / $Sprite2D.hframes) * frame_height # Create 4 particles with different directions and different parts of the texture var directions = [ Vector2(-1, -1).normalized(), # Top-left Vector2(1, -1).normalized(), # Top-right Vector2(-1, 1).normalized(), # Bottom-left Vector2(1, 1).normalized() # Bottom-right ] var regions = [ Rect2(frame_x, frame_y, frame_width / 2, frame_height / 2), # Top-left Rect2(frame_x + frame_width / 2, frame_y, frame_width / 2, frame_height / 2), # Top-right Rect2(frame_x, frame_y + frame_height / 2, frame_width / 2, frame_height / 2), # Bottom-left Rect2(frame_x + frame_width / 2, frame_y + frame_height / 2, frame_width / 2, frame_height / 2) # Bottom-right ] for i in range(4): var tp = tileParticleScene.instantiate() as CharacterBody2D var spr2D = tp.get_node("Sprite2D") as Sprite2D tp.global_position = global_position # Set up the sprite's texture and region spr2D.texture = sprite_texture spr2D.region_enabled = true spr2D.region_rect = regions[i] # Add some randomness to the velocity var speed = randf_range(170, 200) var dir = directions[i] + Vector2(randf_range(-0.2, 0.2), randf_range(-0.2, 0.2)) tp.velocity = dir * speed # Add some rotation tp.angular_velocity = randf_range(-7, 7) get_parent().call_deferred("add_child", tp) func indicate(iIndicate: bool): $Indicator.visible = iIndicate if !liftable or is_being_lifted: $Indicator.visible = false pass func _on_area_2d_collision_body_entered(body: Node2D) -> void: if is_being_thrown == false or body == self or body == thrown_by: return if multiplayer.is_server(): var collision_shape = $Area2DCollision.get_overlapping_bodies() if collision_shape.size() > 0: var collider = collision_shape[0] var normal = (global_position - collider.global_position).normalized() 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) elif collider != self and "breakPot" in collider: collider.take_damage(self, thrown_by) # create particles from pot: take_damage.rpc(null, null) pass normal = velocity.normalized() velocity = velocity.bounce(normal) * 0.4 # slow down pass # Replace with function body. func show_destroy_effect(): $GPUParticles2D.emitting = true $Sprite2D.frame = 13 + 19 + 19 $Sprite2DShadow.visible = false liftable = false indicate(false) create4TileParticles() is_being_thrown = false $Sprite2DShadow.visible = false # Play shatter sound $SfxShatter.play() self.call_deferred("remove") pass @rpc("any_peer", "reliable") func request_grab_pot(pot_path: NodePath, peer_id: int): if multiplayer.is_server(): var pot = 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 "grab" in pot and player: if pot.grab(player): # grab() function already disables collisions, but ensure it's done player.grabbed_entity = pot player.grabbed_entity_path = str(pot.get_path()) player.current_animation = "IDLE_PUSH" # Lock direction to current last_direction when grabbing player.locked_grab_direction = player.last_direction # Sync to all clients (including the joiner who requested it) player.set_grabbed_entity_path_rpc.rpc(str(pot.get_path())) player.sync_animation.rpc("IDLE_PUSH") @rpc("any_peer", "reliable") func request_lift_pot(_pot_path: NodePath, _peer_id: int): # This function is now handled by MultiplayerManager # Keeping it for backward compatibility but it should not be called pass @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 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 (either by holder_peer_id or by checking the holder directly) if pot.holder_peer_id == peer_id or (pot.holder != null and pot.holder.get_multiplayer_authority() == peer_id): pot.throw(direction) player.held_entity = null player.held_entity_path = "" player.current_animation = "THROW" # Sync pot state to all clients first pot.sync_pot_state.rpc(false, 0) # Not lifted, no holder # Sync animation and clear held entity to all clients var all_players = get_tree().get_current_scene().get_node("SpawnRoot").get_children() for p in all_players: if p.has_method("sync_animation"): p.sync_animation.rpc("THROW") p.sync_held_entity.rpc("") # Clear held entity @rpc("call_local") func take_damage(_iBody: Node2D, _iByWhoOrWhat: Node2D) -> void: is_destroyed = true # will trigger show_destroy_effect for clients... show_destroy_effect() # remove all kind of collision since it's broken now... self.set_deferred("monitoring", false) self.set_collision_layer_value(8, false) self.set_collision_mask_value(7, false) self.set_collision_mask_value(8, false) self.set_collision_mask_value(9, false) self.set_collision_mask_value(10, false) pass @rpc("call_local", "reliable") func sync_pot_state(lifted: bool, holder_id: int): is_being_lifted = lifted holder_peer_id = holder_id if holder_peer_id != 0: var player = get_tree().get_current_scene().get_node("SpawnRoot").get_node_or_null(str(holder_peer_id)) if player: holder = player else: holder = null else: holder = null pass func _on_area_2d_collision_body_exited(_body: Node2D) -> void: pass # Replace with function body. func _on_area_2d_pickup_body_entered(_body: Node2D) -> void: indicate(true) pass # Replace with function body. func _on_area_2d_pickup_body_exited(_body: Node2D) -> void: indicate(false) pass # Replace with function body. func _on_timer_smoke_particles_timeout() -> void: $GPUParticles2D.emitting = false pass # Replace with function body.