class_name RootMovementHandler extends Node @export_category("Nodes") @export var character : Character3D @export var body : CharacterBody3D @export var player_camera : PlayerCamera3D @export var slice_ring_spawner : SliceRingSpawner @export_category("Input Settings") @export var _walk_reaction_speed : float = 1.0 @export var _walk_turn_speed : float = 10.0 @export_category("Input Resources") @export var _player_movement_standard : InputResource @export var _player_movement_slice_time : InputResource @export_category("Physics Constants") @export_range(0.0, 10.0, 0.01) var _gravity_multiplier : float = 1.0 @export_range(0.0, 100.0, 0.1) var _terminal_velocity : float = 35.0 @export_range(0.0, 10.0, 0.05) var _air_acceleration : float = 6.0 @export_range(0.0, 20.0, 0.05) var _max_air_velocity : float = 6.0 @export_range(0.0, 10.0, 0.05) var _drag : float = 1.0 @export_range(0.0, 50.0, 0.05) var _slice_time_aim_blend_speed = 30.0 @onready var _state_chart : StateChart = $StandardMovementChart var _target_movement_vector : Vector2 = Vector2.ZERO var _current_movement_speed : float = 0.0 var _physics_motion_accumulator : Vector3 = Vector3.ZERO var _gravity : float = 0.0 var _slice_time_aim_blend : float = 0.0 var _slice_time_tilt : float = 0.5 var _slice_time_angle : float = 0.0 signal state_changed(StringName) func _ready() -> void: InputHandler.add_input_resource(self, _player_movement_standard) character.character_skeleton.animation_tree.advance_expression_base_node = self.get_path() func _on_movement_2d(_delta:float, _vector:Vector2) -> void: _target_movement_vector = _vector func _on_slice_time_rotate_2d(_delta:float, _vector:Vector2) -> void: _target_movement_vector = _vector func _on_jump_pressed() -> void: pass func _on_slice_time_pressed() -> void: _state_chart.send_event("slice_time") func _on_slice_time_released() -> void: _state_chart.send_event("no_more_slice_time") func _on_walking_state_entered() -> void: _physics_motion_accumulator = Vector3.ZERO _gravity = 0.0 emit_signal("state_changed", "walk") func _on_coyote_time_state_entered() -> void: _on_walking_state_entered() func _on_walking_state_processing(_delta:float) -> void: _camera_relative_heading_process(_delta) func _on_coyote_time_state_processing(_delta: float) -> void: _on_walking_state_processing(_delta) func _on_walking_state_physics_processing(_delta:float) -> void: _root_motion_walk_process(_delta) _lateral_movement_physics_process(_delta) #_gravity_physics_process(_delta) _move_character() if !body.is_on_floor(): _state_chart.send_event("fall") _gravity = 0.0 func _on_coyote_time_state_physics_processing(_delta: float) -> void: _on_walking_state_physics_processing(_delta) func _on_falling_state_entered() -> void: _gravity = 0.0 _physics_motion_accumulator = body.velocity func _on_falling_state_processing(_delta:float) -> void: _camera_relative_heading_process(_delta) _physics_movement_accumulation_process(_delta) func _on_falling_state_physics_processing(_delta:float) -> void: _physics_movement_physics_process(_delta) _gravity_physics_process(_delta) _move_character() if body.is_on_floor(): _state_chart.send_event("land") func _on_landed_state_entered() -> void: emit_signal("state_changed", "landed") func _on_landed_state_physics_processing(_delta:float) -> void: pass func _on_slice_time_state_entered() -> void: InputHandler.remove_input_resource(self, _player_movement_standard) InputHandler.add_input_resource(self, _player_movement_slice_time) _reset_all_movement() character.character_skeleton.animation_tree["parameters/playback"].travel("slice_time") _slice_time_tilt = 0.5 player_camera.state_chart.send_event("slice_time") slice_ring_spawner.spawn(self, true); func _on_slice_time_state_exited() -> void: _reset_all_movement() character.character_skeleton.animation_tree["parameters/playback"].travel("walk") slice_ring_spawner.despawn() InputHandler.remove_input_resource(self, _player_movement_slice_time) InputHandler.add_input_resource(self, _player_movement_standard) player_camera.state_chart.send_event("no_more_slice_time") func _on_slice_time_state_processing(_delta:float) -> void: character.character_skeleton.animation_tree["parameters/slice_time/slice_time_idle/slice_time_pitch/seek_request"] = _slice_time_tilt #character.character_skeleton.animation_tree["parameters/slice_time_active/aim_angle/seek_request"] = 0.0 func _on_slice_time_state_physics_processing(_delta:float) -> void: #_lateral_movement_physics_process(_delta) #_move_character() pass func _on_slice_time_aim_state_entered() -> void: var _slice_time_input : InputAction = _player_movement_standard.find_input_action("slice_time") if not _slice_time_input or not _slice_time_input.is_active(): _state_chart.send_event("no_more_slice_time") func _on_slice_time_aim_state_processing(_delta:float) -> void: _slice_time_tilt = clampf(_slice_time_tilt + (_target_movement_vector.y * _delta), 0.0, 1.0) var old_blend_time : float = character.character_skeleton.animation_tree["parameters/slice_time/slice_time_idle/slice_time_angle_blend/blend_amount"] as float character.character_skeleton.animation_tree["parameters/slice_time/slice_time_idle/slice_time_angle_blend/blend_amount"] = lerpf(old_blend_time, _slice_time_aim_blend, _slice_time_aim_blend_speed * _delta) character.character_skeleton.animation_tree["parameters/slice_time/slice_time_idle/slice_time_angle/seek_request"] = _slice_time_angle var old_slice_tilt : float = character.character_skeleton.animation_tree["parameters/slice_time/slice_time_idle/slice_time_pitch/seek_request"] character.character_skeleton.animation_tree["parameters/slice_time/slice_time_idle/slice_time_pitch/seek_request"] = lerpf(old_slice_tilt, _slice_time_tilt, _slice_time_aim_blend_speed * _delta) func _on_slice_time_aim_state_physics_processing(_delta:float) -> void: _slice_time_rotation_physics_process(_delta) func _on_slice_time_slice_state_entered() -> void: pass func _on_slice_time_slice_state_processing(_delta:float) -> void: _on_slice_time_aim_state_processing(_delta) func _on_slice_time_slice_state_physics_processing(_delta:float) -> void: _on_slice_time_aim_state_physics_processing(_delta) func _move_character(): body.move_and_slide() func _root_motion_walk_process(_delta:float) -> void: _current_movement_speed = lerpf(_current_movement_speed, _target_movement_vector.length_squared(), clampf(_delta * _walk_reaction_speed, 0.0, 1.0)) character.character_skeleton.animation_tree["parameters/walk/blend_position"] = _current_movement_speed func _camera_relative_heading_process(_delta:float): var _target_movement_length_squared : float = _target_movement_vector.length_squared() if _target_movement_length_squared >= 0.0001: var _camera_relative_rotation : Quaternion = \ Quaternion(Vector3(_target_movement_vector.x, 0.0, _target_movement_vector.y).normalized(), \ Vector3(-player_camera.camera.global_basis.z.x, 0.0, -player_camera.camera.global_basis.z.z).normalized()) character.quaternion = character.quaternion.slerp(_camera_relative_rotation, clampf(_delta * _walk_turn_speed, 0.0, 1.0)) func _lateral_movement_physics_process(_delta:float) -> void: var root_motion_speed : Vector3 = character.quaternion * character.character_skeleton.animation_tree.get_root_motion_position() if root_motion_speed.length_squared() >= 0.00000000: body.velocity = root_motion_speed / _delta func _physics_movement_accumulation_process(_delta:float) -> void: _physics_motion_accumulator = _physics_motion_accumulator.lerp(character.basis.z * _max_air_velocity, _target_movement_vector.length_squared() * _delta * _air_acceleration) func _physics_movement_physics_process(_delta:float) -> void: _physics_motion_accumulator = _physics_motion_accumulator.lerp(Vector3.ZERO, _drag * _delta * (1.0 - _target_movement_vector.length_squared())) body.velocity = _physics_motion_accumulator func _slice_time_rotation_physics_process(_delta:float) -> void: character.rotate_y(-_target_movement_vector.x * _delta * 1.5) func _on_slice_aim_update(_is_aiming:bool, _angle:float) -> void: _slice_time_angle = 1.0 - (absf(_angle - PI) / TAU) _slice_time_aim_blend = 1.0 if _is_aiming else 0.0 func _on_slice_executed() -> void: _state_chart.send_event("slice") func _gravity_physics_process(_delta:float) -> void: _gravity = clamp(_gravity + (ProjectSettings.get_setting("physics/3d/default_gravity") * _gravity_multiplier * _delta), 0.0, _terminal_velocity) body.velocity -= character.global_basis.y * _gravity func _reset_all_movement() -> void: _target_movement_vector = Vector2.ZERO _current_movement_speed = 0.0 _physics_motion_accumulator = Vector3.ZERO _gravity = 0.0 character.character_skeleton.animation_tree["parameters/walk/blend_position"] = _current_movement_speed