class_name Character extends CharacterBody3D enum SkinColor { BLUE, YELLOW, GREEN, RED } const NORMAL_SPEED = 6.0 const SPRINT_SPEED = 10.0 const JUMP_VELOCITY = 7.5 const FALL_GRAVITY_MULTIPLIER = 1.6 const BASE_NICKNAME_HEIGHT := 2.0 const SERVER_ANIMATION_REQUESTS_PER_SECOND := 10.0 const SERVER_ANIMATION_REQUEST_BURST := 6.0 const PICKUP_ANIMATION_DELAY_MSEC := 1000 const PICKUP_ANIMATION_WINDOW_MSEC := 2500 const PICKUP_REQUEST_COOLDOWN_MSEC := 1000 const ALLOWED_ANIMATION_STATES := { &"Idle": true, &"Run": true, &"Sprint": true, &"Jump": true, &"Jump2": true, &"Fall": true, &"Attack1": true, &"Emote2": true } const HAT_NODES_BY_ITEM := { "fedora": "Fedora", "headphones": "Headphones", "pirate_hat": "PirateHat", "sheriff_hat": "SheriffHat", "sombrero": "Sombrero", "wizard_hat": "WizardHat" } const WEAPON_NODES_BY_ITEM := {"sword": "Sword", "sword_big": "SwordBig", "axe": "Axe"} const BACKPACK_NODES_BY_ITEM := {"backpack": "Backpack"} const HEAD_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach/" const HAND_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/LeftHandAttach/" const BACK_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/BackAttach/" const FIRST_PERSON_HIDDEN_BONES: Array[StringName] = [&"Head", &"HeadTop"] @export var skin_color: SkinColor = SkinColor.BLUE @export_category("Nickname") @export_range(0.0, 1.0, 0.01) var nickname_clearance: float = 0.2 @export_category("Objects") @export var _body: Node3D = null @export var _spring_arm_offset: SpringArmCharacter = null @export_category("Skin Colors") @export var blue_texture: CompressedTexture2D @export var yellow_texture: CompressedTexture2D @export var green_texture: CompressedTexture2D @export var red_texture: CompressedTexture2D var player_inventory: PlayerInventory var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") var can_double_jump = true var has_double_jumped = false var is_attacking := false var is_collecting := false var _current_speed: float var _spawn_point = Vector3(0, 5, 0) var _animation_sequence := 0 var _last_applied_animation_sequence := 0 var _last_requested_animation: StringName = &"" var _appearance_sync_requesters: Dictionary = {} var _server_animation_request_tokens := SERVER_ANIMATION_REQUEST_BURST var _last_server_animation_token_update_msec := 0 var _server_pickup_animation_started_msec := -1 var _last_server_pickup_request_msec := -PICKUP_REQUEST_COOLDOWN_MSEC var _pickup_area_camera_yaw_offset := 0.0 var _equipped_hat_visual_id := "" @onready var nickname: Label3D = $PlayerNick/Nickname @onready var _bottom_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Bottom") @onready var _chest_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Chest") @onready var _face_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Face") @onready var _limbs_head_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/LimbsAndHead") @onready var _skeleton: Skeleton3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D") @onready var _pickup_area: Area3D = $GodotRobot3D/InfrontArea3D @onready var _first_person_hud: CanvasLayer = $FirstPersonHUD func _enter_tree(): set_multiplayer_authority(str(name).to_int()) $SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority() func _ready(): if multiplayer.is_server(): player_inventory = PlayerInventory.new() _add_starting_items() call_deferred("_sync_equipment_appearance") if not is_multiplayer_authority(): call_deferred("_sync_inventory_to_owner") set_player_skin(skin_color) var animation_player := get_node_or_null("GodotRobot3D/AnimationPlayer") as AnimationPlayer if animation_player: animation_player.animation_finished.connect(_on_animation_finished) _body.play_animation_state(&"Idle") nickname.visible = true _set_nickname_height(BASE_NICKNAME_HEIGHT) call_deferred("_update_nickname_height") if not multiplayer.is_server(): call_deferred("_request_equipment_appearance") if is_multiplayer_authority() and _spring_arm_offset: _pickup_area_camera_yaw_offset = wrapf( _pickup_area.global_rotation.y - _spring_arm_offset.global_rotation.y, -PI, PI ) _spring_arm_offset.perspective_changed.connect(_on_camera_perspective_changed) _on_camera_perspective_changed(_spring_arm_offset.is_first_person) func _on_camera_perspective_changed(first_person: bool) -> void: if not is_multiplayer_authority(): return _body.visible = true _bottom_mesh.visible = not first_person _chest_mesh.visible = not first_person _face_mesh.visible = not first_person _limbs_head_mesh.visible = true nickname.visible = not first_person _first_person_hud.visible = first_person for bone_name in FIRST_PERSON_HIDDEN_BONES: var bone_index := _skeleton.find_bone(bone_name) if bone_index >= 0: _skeleton.set_bone_pose_scale(bone_index, Vector3.ZERO if first_person else Vector3.ONE) if first_person: _align_pickup_area_with_camera() else: _pickup_area.rotation = Vector3.ZERO call_deferred("_refresh_nickname_height_after_perspective_change") func _refresh_nickname_height_after_perspective_change() -> void: if not is_multiplayer_authority() or _is_local_first_person(): return var height := _calculate_nickname_height(_equipped_hat_visual_id) _set_nickname_height(height) nickname.visible = true if multiplayer.is_server(): _broadcast_nickname_height(height) func _align_pickup_area_with_camera() -> void: var pickup_rotation := _pickup_area.global_rotation pickup_rotation.y = wrapf(_spring_arm_offset.global_rotation.y + _pickup_area_camera_yaw_offset, -PI, PI) _pickup_area.global_rotation = pickup_rotation func _physics_process(delta): if not multiplayer.has_multiplayer_peer() or not is_multiplayer_authority(): return var current_scene = get_tree().get_current_scene() var should_freeze = false if current_scene: if current_scene.has_method("is_gameplay_input_blocked") and current_scene.is_gameplay_input_blocked(): should_freeze = true elif current_scene.has_method("is_chat_visible") and current_scene.is_chat_visible(): should_freeze = true elif current_scene.has_method("is_inventory_visible") and current_scene.is_inventory_visible(): should_freeze = true if is_attacking or is_collecting: velocity.x = 0 velocity.z = 0 _apply_gravity(delta) move_and_slide() return if should_freeze: _freeze() _apply_gravity(delta) move_and_slide() _request_animation(_body.get_movement_animation(velocity)) return if Input.is_action_just_pressed("pickup") and is_on_floor() and _has_collectible_item_in_front(): is_collecting = true _request_animation(&"Emote2", true) return if Input.is_action_just_pressed("attack") and is_on_floor(): _start_attack() return if is_on_floor(): can_double_jump = true has_double_jumped = false if Input.is_action_just_pressed("jump"): velocity.y = JUMP_VELOCITY can_double_jump = true _request_animation(&"Jump", true) else: _apply_gravity(delta) if can_double_jump and not has_double_jumped and Input.is_action_just_pressed("jump"): velocity.y = JUMP_VELOCITY has_double_jumped = true can_double_jump = false _request_animation(&"Jump2", true) _move() var collided = move_and_slide() if collided: _push_collided_items() _request_animation(_body.get_movement_animation(velocity)) func _apply_gravity(delta: float) -> void: if is_on_floor(): return var gravity_multiplier = FALL_GRAVITY_MULTIPLIER if velocity.y < 0 else 1.0 velocity.y -= gravity * gravity_multiplier * delta func _start_attack() -> void: if is_attacking or is_collecting or not is_on_floor(): return is_attacking = true velocity.x = 0 velocity.z = 0 _request_animation(&"Attack1", true) func _on_animation_finished(animation_name: StringName) -> void: match animation_name: &"Attack1": is_attacking = false &"Emote2": is_collecting = false func _request_animation(state: StringName, restart: bool = false) -> void: if not ALLOWED_ANIMATION_STATES.has(state): return if not restart and _last_requested_animation == state: return _last_requested_animation = state _body.play_animation_state(state, restart) if multiplayer.is_server(): request_animation_state(state) else: request_animation_state.rpc_id(1, state) @rpc("any_peer", "call_local", "reliable") func request_animation_state(state: StringName) -> void: if not multiplayer.is_server() or not _is_owner_request(): return if not ALLOWED_ANIMATION_STATES.has(state): return if not _server_consume_animation_request_token(): return if state == &"Emote2": if not _is_grounded_on_server() or not _has_collectible_item_in_front(): return _server_pickup_animation_started_msec = Time.get_ticks_msec() else: _server_pickup_animation_started_msec = -1 _server_publish_animation(state) func _server_consume_animation_request_token() -> bool: var now := Time.get_ticks_msec() if _last_server_animation_token_update_msec == 0: _last_server_animation_token_update_msec = now else: var elapsed_seconds := (now - _last_server_animation_token_update_msec) / 1000.0 _server_animation_request_tokens = minf( SERVER_ANIMATION_REQUEST_BURST, _server_animation_request_tokens + elapsed_seconds * SERVER_ANIMATION_REQUESTS_PER_SECOND ) _last_server_animation_token_update_msec = now if _server_animation_request_tokens < 1.0: return false _server_animation_request_tokens -= 1.0 return true func _server_publish_animation(state: StringName) -> void: if not multiplayer.is_server(): return _animation_sequence += 1 sync_animation_state.rpc(state, _animation_sequence) sync_animation_state(state, _animation_sequence) @rpc("any_peer", "call_local", "reliable") func sync_animation_state(state: StringName, sequence: int) -> void: var sender_id := multiplayer.get_remote_sender_id() if sender_id != 1 and not (sender_id == 0 and multiplayer.is_server()): return if sequence <= _last_applied_animation_sequence: return _last_applied_animation_sequence = sequence if not ALLOWED_ANIMATION_STATES.has(state): return # The owner already played the input-driven state immediately. if is_multiplayer_authority(): return _body.play_animation_state(state, true) func _push_collided_items() -> void: for i in get_slide_collision_count(): var c = get_slide_collision(i) if c.get_collider() is RigidBody3D: apply_force_to_server_object.rpc_id(1, c.get_collider().name, -c.get_normal()) func _process(_delta): if not multiplayer.has_multiplayer_peer(): return if not is_multiplayer_authority(): return var first_person := _spring_arm_offset != null and _spring_arm_offset.is_first_person if first_person: _align_pickup_area_with_camera() var camera_input_blocked := false var current_scene := get_tree().get_current_scene() if current_scene and current_scene.has_method("is_camera_input_blocked"): camera_input_blocked = current_scene.is_camera_input_blocked() _first_person_hud.visible = ( first_person and not camera_input_blocked and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED ) _check_out_of_bounds() func _freeze(): velocity.x = 0 velocity.z = 0 _current_speed = 0 _request_animation(&"Idle") func _move() -> void: var input_direction: Vector2 = Vector2.ZERO if is_multiplayer_authority(): input_direction = Input.get_vector("move_left", "move_right", "move_forward", "move_backward") var direction: Vector3 = transform.basis * Vector3(input_direction.x, 0, input_direction.y).normalized() _is_running() direction = direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y) if direction: velocity.x = direction.x * _current_speed velocity.z = direction.z * _current_speed _body.apply_rotation(velocity) return velocity.x = move_toward(velocity.x, 0, _current_speed) velocity.z = move_toward(velocity.z, 0, _current_speed) func _is_running() -> bool: if Input.is_action_pressed("shift"): _current_speed = SPRINT_SPEED return true _current_speed = NORMAL_SPEED return false func _check_out_of_bounds(): if global_transform.origin.y < -15.0: _reset_position_after_fall() func _reset_position_after_fall(): global_transform.origin = _spawn_point velocity = Vector3.ZERO func _get_texture_from_name(color: SkinColor) -> CompressedTexture2D: match color: SkinColor.BLUE: return blue_texture SkinColor.GREEN: return green_texture SkinColor.RED: return red_texture SkinColor.YELLOW: return yellow_texture _: return blue_texture func set_player_skin(skin_name: SkinColor) -> void: var texture = _get_texture_from_name(skin_name) _set_mesh_texture(_bottom_mesh, texture) _set_mesh_texture(_chest_mesh, texture) _set_mesh_texture(_face_mesh, texture) _set_mesh_texture(_limbs_head_mesh, texture) func _set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2D) -> void: if mesh_instance: var new_material := StandardMaterial3D.new() new_material.albedo_texture = texture mesh_instance.set_surface_override_material(0, new_material) @rpc("any_peer", "call_local", "reliable") func sync_inventory_to_owner(inventory_data: Dictionary): var sender_id = multiplayer.get_remote_sender_id() if sender_id != 1 and not (sender_id == 0 and multiplayer.is_server()): return if not is_multiplayer_authority(): return if not player_inventory: player_inventory = PlayerInventory.new() player_inventory.from_dict(inventory_data) var level_scene = get_tree().get_current_scene() if ( level_scene and get_multiplayer_authority() == multiplayer.get_unique_id() and level_scene.has_method("update_local_inventory_display") ): level_scene.update_local_inventory_display() @rpc("any_peer", "call_local", "reliable") func request_move_item(from_slot: int, to_slot: int, quantity: int = -1): if not multiplayer.is_server(): return var requesting_client = multiplayer.get_remote_sender_id() if not _is_owner_request(): push_warning( ( "Client " + str(requesting_client) + " tried to modify inventory for player " + str(get_multiplayer_authority()) ) ) return if not player_inventory: return if not player_inventory.is_slot_active(from_slot) or not player_inventory.is_slot_active(to_slot): push_warning("Invalid slot indices: from=" + str(from_slot) + " to=" + str(to_slot)) return if quantity != -1 and quantity <= 0: push_warning("Invalid move quantity: " + str(quantity)) return var success = false if quantity == -1: success = player_inventory.move_item(from_slot, to_slot) if not success: success = player_inventory.swap_items(from_slot, to_slot) else: success = player_inventory.move_item(from_slot, to_slot, quantity) if success: _sync_inventory_to_owner() @rpc("any_peer", "call_local", "reliable") func request_add_item(item_id: String, quantity: int = 1): if not multiplayer.is_server(): return var requesting_client = multiplayer.get_remote_sender_id() var is_local_server_call = requesting_client == 0 and multiplayer.get_unique_id() == 1 if requesting_client != 1 and not is_local_server_call: push_warning( "Client " + str(requesting_client) + " tried to add items to player " + str(get_multiplayer_authority()) ) return if not player_inventory: return if quantity <= 0: push_warning("Invalid quantity: " + str(quantity)) return var item = ItemDatabase.get_item(item_id) if not item: push_warning("Item not found: " + item_id) return var remaining = player_inventory.add_item(item, quantity) var added = quantity - remaining if added > 0: _sync_inventory_to_owner() func request_add_single_item(item_id: String) -> bool: if not multiplayer.is_server(): return false if player_inventory == null: return false var item = ItemDatabase.get_item(item_id) if not item: push_warning("Item not found: " + item_id) return false var remaining = player_inventory.add_item(item, 1) if remaining == 0: _sync_inventory_to_owner() return true return false @rpc("any_peer", "call_local", "reliable") func request_remove_item(item_id: String, quantity: int = 1): if not multiplayer.is_server(): return var requesting_client = multiplayer.get_remote_sender_id() if not _is_owner_request(): push_warning( ( "Client " + str(requesting_client) + " tried to remove items from player " + str(get_multiplayer_authority()) ) ) return if not player_inventory: return if quantity <= 0: push_warning("Invalid quantity: " + str(quantity)) return var removed = player_inventory.remove_item(item_id, quantity) if removed > 0: _sync_inventory_to_owner() @rpc("authority", "call_local", "reliable") func add_world_item(scene_path: String, player_position: Vector3) -> void: var item_container = get_node_or_null("/root/Level/Environment/ItemContainer") if not item_container: push_warning("ItemContainer not found at /root/Level/Environment/ItemContainer") return if scene_path.is_empty() or not ResourceLoader.exists(scene_path): push_warning("Cannot add world item: invalid scene path '" + scene_path + "'") return var packed_scene = load(scene_path) as PackedScene if not packed_scene: push_warning("Cannot add world item: scene path is not a PackedScene '" + scene_path + "'") return var instance_item := packed_scene.instantiate() as Node3D if not instance_item: push_warning("Cannot add world item: scene root is not a Node3D '" + scene_path + "'") return item_container.add_child(instance_item, true) instance_item.global_position = player_position func get_inventory() -> PlayerInventory: return player_inventory func _sync_inventory_to_owner() -> void: if not multiplayer.is_server() or not player_inventory: return var owner_id = get_multiplayer_authority() if owner_id == 1: sync_inventory_to_owner(player_inventory.to_dict()) else: sync_inventory_to_owner.rpc_id(owner_id, player_inventory.to_dict()) @rpc("any_peer", "call_local", "reliable") func request_equip_item(from_slot: int, item_type: Item.ItemType) -> void: if not multiplayer.is_server() or not _is_owner_request(): return if not player_inventory or not player_inventory.is_slot_active(from_slot): return if item_type != Item.ItemType.WEAPON and item_type != Item.ItemType.HAT and item_type != Item.ItemType.BACKPACK: return if player_inventory.equip_from_slot(from_slot, item_type): _sync_inventory_to_owner() _sync_equipment_appearance() @rpc("any_peer", "call_local", "reliable") func request_unequip_item(item_type: Item.ItemType, destination_slot: int = -1) -> void: if not multiplayer.is_server() or not _is_owner_request(): return if not player_inventory: return if item_type != Item.ItemType.WEAPON and item_type != Item.ItemType.HAT and item_type != Item.ItemType.BACKPACK: return if destination_slot < -1 or destination_slot >= PlayerInventory.MAX_INVENTORY_SIZE: return if destination_slot >= 0 and not player_inventory.is_slot_active(destination_slot): return if player_inventory.unequip_to_slot(item_type, destination_slot): _sync_inventory_to_owner() _sync_equipment_appearance() func _is_owner_request() -> bool: var sender := multiplayer.get_remote_sender_id() return sender == get_multiplayer_authority() or (sender == 0 and multiplayer.is_server()) func _sync_equipment_appearance() -> void: if not multiplayer.is_server() or not player_inventory: return var weapon_id := player_inventory.equipped_weapon.item_id var hat_id := player_inventory.equipped_hat.item_id var backpack_id := player_inventory.equipped_backpack.item_id var nickname_height := _calculate_nickname_height(hat_id) _broadcast_nickname_height(nickname_height) sync_equipment_appearance.rpc(weapon_id, hat_id, backpack_id) sync_equipment_appearance(weapon_id, hat_id, backpack_id) func _request_equipment_appearance() -> void: if multiplayer.is_server() or not multiplayer.has_multiplayer_peer(): return request_equipment_appearance.rpc_id(1) @rpc("any_peer", "reliable") func request_equipment_appearance() -> void: if not multiplayer.is_server() or not player_inventory: return var requester_id := multiplayer.get_remote_sender_id() if requester_id <= 0: return if _appearance_sync_requesters.has(requester_id): return _appearance_sync_requesters[requester_id] = true _sync_equipment_appearance_to_peer(requester_id) func _sync_equipment_appearance_to_peer(peer_id: int) -> void: if not multiplayer.is_server() or not player_inventory or peer_id <= 0: return var weapon_id := player_inventory.equipped_weapon.item_id var hat_id := player_inventory.equipped_hat.item_id var backpack_id := player_inventory.equipped_backpack.item_id sync_equipment_appearance.rpc_id(peer_id, weapon_id, hat_id, backpack_id) @rpc("any_peer", "reliable") func sync_equipment_appearance(weapon_id: String, hat_id: String, backpack_id: String) -> void: var sender := multiplayer.get_remote_sender_id() if sender != 1 and not (sender == 0 and multiplayer.is_server()): return _set_equipment_visibility(weapon_id, hat_id, backpack_id) func _set_equipment_visibility(weapon_id: String, hat_id: String, backpack_id: String) -> void: _equipped_hat_visual_id = hat_id _set_equipment_nodes_visibility(HEAD_EQUIPMENT_PATH, HAT_NODES_BY_ITEM, hat_id) _set_equipment_nodes_visibility(HAND_EQUIPMENT_PATH, WEAPON_NODES_BY_ITEM, weapon_id) _set_equipment_nodes_visibility(BACK_EQUIPMENT_PATH, BACKPACK_NODES_BY_ITEM, backpack_id) func _broadcast_nickname_height(height: float) -> void: if not multiplayer.is_server(): return var level_scene := get_tree().get_current_scene() if level_scene and level_scene.has_method("register_player_nickname_height"): level_scene.register_player_nickname_height(get_multiplayer_authority(), height) func _set_equipment_nodes_visibility(parent_path: String, nodes_by_item: Dictionary, equipped_item_id: String) -> void: for item_id in nodes_by_item: var equipment := get_node_or_null(parent_path + str(nodes_by_item[item_id])) as Node3D if equipment: equipment.visible = item_id == equipped_item_id func _update_nickname_height(hat_id: String = "") -> void: if not nickname: return nickname.visible = not _is_local_first_person() _set_nickname_height(_calculate_nickname_height(hat_id)) func _calculate_nickname_height(hat_id: String) -> float: var target_height := BASE_NICKNAME_HEIGHT var equipped_hat := _get_hat_node(hat_id) if equipped_hat: var hat_top := _get_visual_top(equipped_hat) if hat_top > -INF and hat_top < INF: target_height = max(BASE_NICKNAME_HEIGHT, hat_top + nickname_clearance) return target_height func _set_nickname_height(height: float) -> void: var nickname_position := nickname.position nickname_position.y = height nickname.position = nickname_position func apply_synced_nickname_height(height: float) -> void: if not nickname: return nickname.visible = not _is_local_first_person() if height > -INF and height < INF: _set_nickname_height(maxf(BASE_NICKNAME_HEIGHT, height)) else: _set_nickname_height(BASE_NICKNAME_HEIGHT) func _is_local_first_person() -> bool: return is_multiplayer_authority() and _spring_arm_offset != null and _spring_arm_offset.is_first_person func get_current_nickname_height() -> float: return nickname.position.y if nickname else BASE_NICKNAME_HEIGHT func _get_hat_node(hat_id: String) -> Node3D: if HAT_NODES_BY_ITEM.has(hat_id): var requested_hat_path := HEAD_EQUIPMENT_PATH + str(HAT_NODES_BY_ITEM[hat_id]) var requested_hat := get_node_or_null(requested_hat_path) as Node3D if requested_hat: return requested_hat return null func _get_visual_top(root: Node3D) -> float: var visual_top := -INF if root is MeshInstance3D: visual_top = _get_mesh_top(root as MeshInstance3D) for child in root.find_children("*", "MeshInstance3D", true, false): var mesh_instance := child as MeshInstance3D if mesh_instance: visual_top = max(visual_top, _get_mesh_top(mesh_instance)) return visual_top func _get_mesh_top(mesh_instance: MeshInstance3D) -> float: var visual_top := -INF var mesh_bounds := mesh_instance.get_aabb() for endpoint_index in range(8): var endpoint_global := mesh_instance.to_global(mesh_bounds.get_endpoint(endpoint_index)) visual_top = max(visual_top, to_local(endpoint_global).y) return visual_top func _add_starting_items(): if not player_inventory: return var backpack := ItemDatabase.get_item("backpack") if backpack: player_inventory.add_item(backpack, 1) var starting_item_ids: Array[String] = [ "fedora", "headphones", "pirate_hat", "sheriff_hat", "sombrero", "wizard_hat", "sword", "sword_big", "axe", "chicken_leg", "bone", "chalice" ] for item_id in starting_item_ids: var item = ItemDatabase.get_item(item_id) if item: player_inventory.add_item(item, 1) func pickup() -> void: if multiplayer.is_server(): request_pickup() else: request_pickup.rpc_id(1) @rpc("any_peer", "call_local", "reliable") func request_pickup() -> void: if not multiplayer.is_server() or not _is_owner_request(): return var now := Time.get_ticks_msec() if now - _last_server_pickup_request_msec < PICKUP_REQUEST_COOLDOWN_MSEC: return _last_server_pickup_request_msec = now var animation_elapsed := now - _server_pickup_animation_started_msec if ( _server_pickup_animation_started_msec < 0 or animation_elapsed < PICKUP_ANIMATION_DELAY_MSEC or animation_elapsed > PICKUP_ANIMATION_WINDOW_MSEC ): return if not _is_grounded_on_server(): return if not _has_collectible_item_in_front(): return _server_pickup_animation_started_msec = -1 _server_pickup() func _server_pickup() -> void: for item in _get_collectible_items_in_front(): var result := request_add_single_item(item.item_id) if result and item.is_inside_tree(): item.queue_free() func _has_collectible_item_in_front() -> bool: return not _get_collectible_items_in_front().is_empty() func _get_collectible_items_in_front() -> Array[ItemRigidBody3D]: var collectible_items: Array[ItemRigidBody3D] = [] var pickup_area := get_node_or_null("GodotRobot3D/InfrontArea3D") as Area3D if not pickup_area: return collectible_items for body in pickup_area.get_overlapping_bodies(): var item := body as ItemRigidBody3D if item and not item.item_id.is_empty() and ItemDatabase.get_item(item.item_id): collectible_items.append(item) return collectible_items @rpc("any_peer", "call_local", "reliable") func apply_force_to_server_object(object_name: String, normal: Vector3) -> void: var object_node = get_node_or_null("/root/Level/Environment/ItemContainer") if object_node: for n in object_node.get_children(): if n.name == object_name and n is RigidBody3D: n.apply_force(normal * 100) func _is_grounded_on_server() -> bool: if not multiplayer.is_server() or not is_inside_tree(): return false var query := PhysicsRayQueryParameters3D.new() query.from = global_position + Vector3.UP * 0.15 query.to = global_position + Vector3.DOWN * 0.3 query.collision_mask = 2 query.exclude = [get_rid()] query.collide_with_areas = false return not get_world_3d().direct_space_state.intersect_ray(query).is_empty()