class_name Player extends CharacterBody3D const SPEED = 5.0 const JUMP_VELOCITY = 4.5 @export var _projectile_holder: Node @export var _projectile_point: Marker3D @export var _camera_pivot: Node3D @export var _camera: Camera3D @export var _line_of_sight: ShapeCast3D @export var _shot_sound: AudioStreamPlayer3D @export var id: int @export var input_dir := Vector2() @export var input_jumped := false var _max_speed := 16 var _mouse_sensitivity := 0.008 # radians/pixel var _projectile_speed := 12.0 ## Could be only one thing at a time to consider. var _interaction_selection: Node3D var controls_disabled := false @export var held_thing := { "item_id": &"empty_hand", "count": 0 } # What the others see. func _init_bystander() -> void: $Nickname.text = GameState.fetch().player_data[id].username $Nickname.show() func _ready() -> void: if id != multiplayer.get_unique_id(): _init_bystander() return $Model.hide() _camera.make_current() # TODO: reliable rng var spawn_point: Marker3D = get_tree().get_nodes_in_group("spawn_points").pick_random() global_position = spawn_point.global_position reset_physics_interpolation() func _process(_delta: float) -> void: if id != multiplayer.get_unique_id(): return _process_input() func _physics_process(delta: float) -> void: ## Process interactivity selection. if id == multiplayer.get_unique_id(): var collider: Object = null if _line_of_sight.is_colliding(): var possible_collider = _line_of_sight.get_collider(0) if empty_handed(): collider = possible_collider elif possible_collider and \ GameState.are_bundles_stackable(held_thing, possible_collider.owner.item_bundle): collider = possible_collider if collider != _interaction_selection: if _interaction_selection != null: _interaction_selection.get_parent().mark_non_interactive() if collider != null: collider.owner.mark_interactive() _interaction_selection = collider # Add the gravity. if not is_on_floor(): velocity += get_gravity() * delta # Handle jump. if input_jumped and is_on_floor(): velocity.y = JUMP_VELOCITY # Get the input direction and handle the movement/deceleration. var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() if direction: velocity.x = direction.x * SPEED velocity.z = direction.z * SPEED else: velocity.x = move_toward(velocity.x, 0, SPEED) velocity.z = move_toward(velocity.z, 0, SPEED) if velocity.length() > _max_speed: velocity = velocity.clampf(0, _max_speed) $StairStepper.stair_step_up(direction) if move_and_slide() and is_on_floor(): for idx in range(get_slide_collision_count()): var collision := get_slide_collision(idx) if collision.get_collider().is_in_group("voids"): # TODO: reliable rng var spawn_point: Marker3D = get_tree().get_nodes_in_group("spawn_points").pick_random() global_position = spawn_point.global_position reset_physics_interpolation() $StairStepper.stair_step_down() @rpc("any_peer", "call_local", "reliable") func hold_thing(p_bundle: Dictionary) -> void: var item := GameState.fetch().INVENTORY_ITEM_DB[p_bundle["item_id"]] as InventoryItem held_thing = p_bundle var base_node := _camera_pivot.get_node("HeldViewmodel") for child in base_node.get_children(): child.queue_free() if item.model != null: var model = item.model.instantiate() base_node.add_child(model) else: # Create a icon sprite based one instead. var model := preload("res://src/ingame/quad_viewmodel.tscn").instantiate() model.reflect_bundle(p_bundle) base_node.add_child(model) if item.model != null and id == multiplayer.get_unique_id(): # Disable depth test and increase render priority. # TODO: in more complex scenarios model scene might want to have its own callback for this. for model in base_node.get_children(): for submodel in model.get_children(): var mesh := submodel.mesh.duplicate() as Mesh var material := mesh.surface_get_material(0).duplicate() material.render_priority += 1 material.no_depth_test = true mesh.surface_set_material(0, material) submodel.mesh = mesh @rpc("any_peer", "call_local", "reliable") func throw_thing() -> void: held_thing = { "item_id": &"empty_hand", "count": 0 } for child in _camera_pivot.get_node("HeldViewmodel").get_children(): child.queue_free() func empty_handed() -> bool: return held_thing["item_id"] == &"empty_hand" func _unhandled_input(event: InputEvent) -> void: if controls_disabled or id != multiplayer.get_unique_id(): return if event is InputEventMouseMotion: rotate_y(-event.relative.x * _mouse_sensitivity) _camera_pivot.rotate_x(-event.relative.y * _mouse_sensitivity) _camera_pivot.rotation.x = clamp(_camera_pivot.rotation.x, -1.2, 1.2) return if event.is_action_pressed("pick"): if _interaction_selection != null: if empty_handed(): _interaction_selection.owner.get_picked_up.rpc() hold_thing.rpc(_interaction_selection.owner.item_bundle) elif GameState.are_bundles_stackable(held_thing, _interaction_selection.owner.item_bundle): _interaction_selection.owner.get_picked_up.rpc() var bundle := GameState.combine_bundles(held_thing, _interaction_selection.owner.item_bundle) hold_thing.rpc(bundle) if event.is_action_pressed("fire") and not empty_handed(): var item := GameState.fetch().INVENTORY_ITEM_DB[held_thing["item_id"]] as InventoryItem var new_projectile: Node3D = item.bomb.instantiate() _projectile_holder.add_child(new_projectile, true) _set_projectile_authority.rpc(new_projectile.get_path(), id) new_projectile.set_item_bundle.rpc(held_thing) new_projectile.set_global_pos.rpc(_projectile_point.global_position) new_projectile.rotation = rotation new_projectile.sender_body = self new_projectile.sender_id = id new_projectile.body.process_mode = Node.PROCESS_MODE_INHERIT new_projectile.body.linear_velocity = -_camera.global_basis.z * _projectile_speed _shot_sound.play() throw_thing.rpc() @rpc("authority", "call_local", "reliable") func _set_projectile_authority(path: NodePath, authority_id: int) -> void: if not _projectile_holder.has_node(path): return _projectile_holder.get_node(path).set_multiplayer_authority(authority_id) func _process_input() -> void: if controls_disabled: input_dir = Vector2.ZERO input_jumped = false return input_dir = Input.get_vector("strafe_left", "strafe_right", "move_forward", "move_backward") input_jumped = Input.is_action_just_pressed("jump")