| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | class_name Character extends RigidBody3D | 
					
						
							|  |  |  | ## "Lifelike" physics body suitable for player characters, NPCs, and enemies. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #region Exported Properties | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @export_group("Helper Nodes") | 
					
						
							|  |  |  | ## Animation player used to play appropriate animation for current state. | 
					
						
							|  |  |  | @export var _anim_player: AnimationPlayer | 
					
						
							|  |  |  | ## Body collider. Should be a capsule. Script may change height and radius. | 
					
						
							|  |  |  | @export var _collider: CollisionShape3D | 
					
						
							|  |  |  | ## Player for character-associated sounds such as footsteps and vocalizations. | 
					
						
							|  |  |  | @export var _audio_player: AudioStreamPlayer3D | 
					
						
							|  |  |  | ## Needed for saving and restoring properties between state transitions. | 
					
						
							|  |  |  | @export var _property_save_restore_stack: PropertySaveRestoreStack | 
					
						
							|  |  |  | ## Needed for keeping track of current state and state transitions. | 
					
						
							|  |  |  | @export var _state_machine: StateMachine | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @export_group("States") | 
					
						
							|  |  |  | ## How being in each possible state should influence the character. | 
					
						
							|  |  |  | @export var _state_properties: Dictionary[StringName, CharacterStateProperties] | 
					
						
							|  |  |  | ## State the character should be in when the scene starts. | 
					
						
							|  |  |  | @export var _initial_state: StringName = &'idle' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @export_group("Physics") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @export_subgroup("On Ground") | 
					
						
							|  |  |  | ## Refuse forward input if moving faster than this while grounded. | 
					
						
							|  |  |  | @export var _top_ground_speed: float = 20.0 | 
					
						
							|  |  |  | ## Magnitude of manual forward acceleration while grounded. | 
					
						
							|  |  |  | @export var _ground_acceleration: float = 15.0 | 
					
						
							|  |  |  | ## Magnitude of automatic deceleration while grounded. | 
					
						
							|  |  |  | @export var _ground_auto_deceleration: float = 10.0 | 
					
						
							|  |  |  | ## Magnitude of manual deceleration while grounded. | 
					
						
							|  |  |  | @export var _ground_manual_deceleration: float = 30.0 | 
					
						
							|  |  |  | ## Magnitude of centrifugal acceleration at top ground speed. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Character will be subject to a centrifugal force | 
					
						
							|  |  |  | ## proportional to current ground speed. | 
					
						
							|  |  |  | ## It is at its maximum at top ground speed, | 
					
						
							|  |  |  | ## and at zero when the character is at rest. | 
					
						
							|  |  |  | ## While this is by no means physically realistic | 
					
						
							|  |  |  | ## (in real life, there is no such thing as centrifugal force | 
					
						
							|  |  |  | ## in an inertial frame of reference), | 
					
						
							|  |  |  | ## it permits genre-staple parkour techniques | 
					
						
							|  |  |  | ## such as being able to run up walls and on ceilings | 
					
						
							|  |  |  | ## if moving fast enough. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## The centrifugal acceleration applied may actually exceed this amount, | 
					
						
							|  |  |  | ## but only whenever the character's ground speed exceeds its top ground speed. | 
					
						
							|  |  |  | @export var _max_centrifugal_acceleration: float = 20.0 | 
					
						
							|  |  |  | ## Jump height if the jump button is tapped and immediately released. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## This assumes the character is jumping from rest position on level ground | 
					
						
							|  |  |  | ## under standard gravity. When this assumption does not hold, | 
					
						
							|  |  |  | ## actual jump height may vary, but will vary in a physically sensible way. | 
					
						
							|  |  |  | ## Put another way, this property is used to compute the magnitudes | 
					
						
							|  |  |  | ## of both the initial jump impact and the vertical deceleration to apply | 
					
						
							|  |  |  | ## to truncate the jump if the button is released, and, once computed, | 
					
						
							|  |  |  | ## it is these magnitudes, not the jump height itself, | 
					
						
							|  |  |  | ## which remain fixed across all situations. | 
					
						
							|  |  |  | @export var _min_jump_height: float = 1.0 | 
					
						
							|  |  |  | ## Jump height if the jump button is held until the peak of the jump. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## This assumes the character is jumping from rest position on level ground | 
					
						
							|  |  |  | ## under standard gravity. When this assumption does not hold, | 
					
						
							|  |  |  | ## actual jump height may vary, but will vary in a physically sensible way. | 
					
						
							|  |  |  | ## Put another way, this property is used to compute the magnitudes | 
					
						
							|  |  |  | ## of both the initial jump impact and the vertical deceleration to apply | 
					
						
							|  |  |  | ## to truncate the jump if the button is released, and, once computed, | 
					
						
							|  |  |  | ## it is these magnitudes, not the jump height itself, | 
					
						
							|  |  |  | ## which remain fixed across all situations. | 
					
						
							|  |  |  | @export var _max_jump_height: float = 4.0 | 
					
						
							|  |  |  | ## Maximum angular distance in radians to normal of next ground. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## That is to say, if the angle between the current ground normal | 
					
						
							|  |  |  | ## and the normal of some contact surface exceeds this many radians, | 
					
						
							|  |  |  | ## then that contact surface is considered a wall or ceiling; | 
					
						
							|  |  |  | ## otherwise, it is ground, and can be walked upon. | 
					
						
							|  |  |  | @export var _max_slope_angle: float = 1.0 | 
					
						
							|  |  |  | ## Friction when standing still. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Friction at or above top speed is always 0.0. | 
					
						
							|  |  |  | ## Friction at any speed between 0.0 and top speed is decided | 
					
						
							|  |  |  | ## by interpolating between the value given here and 0.0, respectively. | 
					
						
							|  |  |  | @export var _max_friction: float = 0.25 | 
					
						
							|  |  |  | ## Audio stream to play when landing from a fall or jump. | 
					
						
							|  |  |  | @export var _landing_sound: AudioStream = null | 
					
						
							|  |  |  | ## Volume to play landing sound when landing from a fall or jump. | 
					
						
							|  |  |  | @export var _landing_sound_volume_db: float = 0.0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @export_subgroup("In Air") | 
					
						
							|  |  |  | ## Refuse forward input if moving faster than this while not grounded. | 
					
						
							|  |  |  | @export var _top_air_speed: float = 30.0 | 
					
						
							|  |  |  | ## Magnitude of manual forward acceleration while not grounded. | 
					
						
							|  |  |  | @export var _air_acceleration: float = 5.0 | 
					
						
							|  |  |  | ## Magnitude of automatic deceleration while not grounded. | 
					
						
							|  |  |  | @export var _air_auto_deceleration: float = 5.0 | 
					
						
							|  |  |  | ## Magnitude of manual deceleration while not grounded. | 
					
						
							|  |  |  | @export var _air_manual_deceleration: float = 10.0 | 
					
						
							|  |  |  | ## Lerp speed for gradually turning right-side-up while not grounded. | 
					
						
							|  |  |  | @export var _air_righting_lerp_speed: float = 0.5 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @export_group("Combat") | 
					
						
							|  |  |  | ## Amount of health character possesses when undamaged. | 
					
						
							|  |  |  | @export var max_health: float = 100.0 | 
					
						
							|  |  |  | ## Damage dealt is multiplied by this. | 
					
						
							|  |  |  | @export var attack_power: float = 1.0 | 
					
						
							|  |  |  | ## Damage taken is divided by this. | 
					
						
							|  |  |  | @export var defense_power: float = 1.0 | 
					
						
							|  |  |  | ## Knockback dealt is multiplied by this. | 
					
						
							|  |  |  | @export var knockback: float = 1.0 | 
					
						
							|  |  |  | ## Knockback taken is divided by this. | 
					
						
							|  |  |  | @export var knockback_resistance: float = 1.0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #endregion | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #region Private Properties | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var _state_handler_descriptors: Dictionary[StringName, Variant] | 
					
						
							|  |  |  | var _current_state_properties: CharacterStateProperties | 
					
						
							|  |  |  | var _state_coyote_time: float = 0.0 | 
					
						
							|  |  |  | var _state_time_elapsed: float = 0.0 | 
					
						
							|  |  |  | var _air_time_elapsed: float = 0.0 | 
					
						
							|  |  |  | var _grounded := false | 
					
						
							|  |  |  | var _touching_wall := false | 
					
						
							|  |  |  | var _body_state: PhysicsDirectBodyState3D | 
					
						
							|  |  |  | var _teleport_requested := false | 
					
						
							|  |  |  | var _teleport_target := Vector3.ZERO | 
					
						
							|  |  |  | var _freeze_lerp_requested := false | 
					
						
							|  |  |  | var _freeze_lerp_target := Vector3.ZERO | 
					
						
							|  |  |  | var _carrying: Node3D = null | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | @onready var _shapecast_object := PhysicsShapeQueryParameters3D.new() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | #endregion | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #region Public Properties | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Direction facing away from what we currently think is the ground. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## While the character is actually standing on a surface, | 
					
						
							|  |  |  | ## this is the contact normal of that surface. | 
					
						
							|  |  |  | ## Otherwise, this starts off at its last known value, | 
					
						
							|  |  |  | ## and is gradually interpolated toward the direction that opposes gravity. | 
					
						
							|  |  |  | var ground_normal := Vector3.UP | 
					
						
							|  |  |  | ## Direction facing away from what we currently think is the wall. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Unlike ground_normal, this property has no meaning | 
					
						
							|  |  |  | ## if there is not currently an actual wall. | 
					
						
							|  |  |  | var wall_normal := Vector3.ZERO | 
					
						
							|  |  |  | ## Current state of the state machine. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Attempting to set this property to the name of an unimplemented state | 
					
						
							|  |  |  | ## will fail with an error message. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Attempting to set this property while coyote time is active | 
					
						
							|  |  |  | ## will fail silently. Though it will not work, attempting to do it | 
					
						
							|  |  |  | ## is not to be considered an error; rather, the caller is expected | 
					
						
							|  |  |  | ## to repeat the attempt for as long as the state transition condition | 
					
						
							|  |  |  | ## remains satisfied, and the assignment will succeed once coyote time expires. | 
					
						
							|  |  |  | var state: StringName: | 
					
						
							|  |  |  | 	get(): return _state_machine.state | 
					
						
							|  |  |  | 	set(value): change_state(value) | 
					
						
							|  |  |  | ## Local-space direction the character "wants" / is "trying" to move in. | 
					
						
							|  |  |  | var impetus := Vector3.ZERO: | 
					
						
							|  |  |  | 	set(value): | 
					
						
							|  |  |  | 		if value.is_zero_approx(): | 
					
						
							|  |  |  | 			impetus = Vector3.ZERO | 
					
						
							|  |  |  | 		else: | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 			impetus = value.limit_length() | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | ## World-space direction the character "wants" / is "trying" to move in. | 
					
						
							|  |  |  | var global_impetus: Vector3: | 
					
						
							|  |  |  | 	get(): | 
					
						
							|  |  |  | 		var gi := global_transform*impetus - global_position | 
					
						
							|  |  |  | 		if gi.is_zero_approx(): | 
					
						
							|  |  |  | 			return Vector3.ZERO | 
					
						
							|  |  |  | 		else: | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 			return gi.limit_length() | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 	set(value): | 
					
						
							|  |  |  | 		impetus = (global_position + value)*global_transform | 
					
						
							|  |  |  | ## Whether the character is "trying" to jump. | 
					
						
							|  |  |  | var jump_impetus := false | 
					
						
							|  |  |  | ## Whether the character is "trying" to perform their primary action. | 
					
						
							|  |  |  | var action1_impetus := false | 
					
						
							|  |  |  | ## Whether the character is "trying" to perform their secondary action. | 
					
						
							|  |  |  | var action2_impetus := false | 
					
						
							| 
									
										
										
										
											2025-03-29 02:49:41 -07:00
										 |  |  | ## Whether the character is "trying" to perform a tertiary interaction. | 
					
						
							|  |  |  | var interact_impetus := false | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | ## Collision shape height. | 
					
						
							|  |  |  | var height: float: | 
					
						
							|  |  |  | 	get(): | 
					
						
							|  |  |  | 		if _current_state_properties.collider_horizontal: | 
					
						
							|  |  |  | 			return 2.0*_collider.shape.radius | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			return _collider.shape.height | 
					
						
							|  |  |  | 	set(value): | 
					
						
							|  |  |  | 		if _current_state_properties.collider_horizontal: | 
					
						
							|  |  |  | 			_collider.shape.radius = value/2.0 | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			_collider.shape.height = value | 
					
						
							|  |  |  | 		_collider.position.y = value/2.0 | 
					
						
							|  |  |  | 		_audio_player.position.y = _collider.position.y | 
					
						
							|  |  |  | ## Collision shape diameter. | 
					
						
							|  |  |  | var width: float: | 
					
						
							|  |  |  | 	get(): | 
					
						
							|  |  |  | 		if _current_state_properties.collider_horizontal: | 
					
						
							|  |  |  | 			return _collider.shape.height | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			return 2.0*_collider.shape.radius | 
					
						
							|  |  |  | 	set(value): | 
					
						
							|  |  |  | 		if _current_state_properties.collider_horizontal: | 
					
						
							|  |  |  | 			_collider.shape.height = value | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			_collider.shape.radius = value/2.0 | 
					
						
							|  |  |  | ## Character's current health. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Setting this variable to a positive value lower than its previous value | 
					
						
							|  |  |  | ## force-changes state to hit. | 
					
						
							|  |  |  | ## Setting it at or below 0.0 force-changes state to defeat. | 
					
						
							|  |  |  | ## Trying to set it to a value higher than max_health | 
					
						
							|  |  |  | ## will instead set it to max_health. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Normally you want to deal damage using the take_damage function, | 
					
						
							|  |  |  | ## as it also handles knockback, which directly invoking this setter does not. | 
					
						
							|  |  |  | ## However, the setter is suitable for custom knockback handling, | 
					
						
							|  |  |  | ## damage without knockback, and healing. | 
					
						
							|  |  |  | var health: float: | 
					
						
							|  |  |  | 	set(value): | 
					
						
							|  |  |  | 		if value >= max_health: | 
					
						
							|  |  |  | 			value = max_health | 
					
						
							|  |  |  | 		elif value <= 0.0: | 
					
						
							|  |  |  | 			force_change_state(&'defeat') | 
					
						
							|  |  |  | 		elif value < health && !state_uninterruptible(): | 
					
						
							|  |  |  | 			force_change_state(&'hit') | 
					
						
							|  |  |  | 		health = value | 
					
						
							|  |  |  | ## Object that the character is carrying if any, else null. | 
					
						
							|  |  |  | var carrying: Node3D: | 
					
						
							|  |  |  | 	get(): | 
					
						
							|  |  |  | 		return _carrying | 
					
						
							|  |  |  | 	set(value): | 
					
						
							|  |  |  | 		_pick_up_without_animation(value) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #endregion | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #region Overrides | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _ready() -> void: | 
					
						
							|  |  |  | 	_enforce_certain_properties() | 
					
						
							|  |  |  | 	_connect_to_state_machine() | 
					
						
							|  |  |  | 	force_change_state(_initial_state) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _exit_tree() -> void: | 
					
						
							|  |  |  | 	_disconnect_from_state_machine() | 
					
						
							|  |  |  | 	request_ready() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _integrate_forces(body_state: PhysicsDirectBodyState3D) -> void: | 
					
						
							|  |  |  | 	_body_state = body_state | 
					
						
							|  |  |  | 	_update_timers() | 
					
						
							|  |  |  | 	_update_anim_speed() | 
					
						
							|  |  |  | 	_check_contacts() | 
					
						
							|  |  |  | 	_reorient() | 
					
						
							|  |  |  | 	_handle_teleport_request() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _process(delta: float) -> void: | 
					
						
							|  |  |  | 	_handle_freeze_lerp_request(delta) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #endregion | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #region State-Agnostic Private Methods | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _update_timers() -> void: | 
					
						
							|  |  |  | 	if _state_coyote_time > 0.0: | 
					
						
							|  |  |  | 		_state_coyote_time -= _body_state.step | 
					
						
							|  |  |  | 	_state_time_elapsed += _body_state.step | 
					
						
							|  |  |  | 	if is_really_grounded(): | 
					
						
							|  |  |  | 		_air_time_elapsed = 0.0 | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		_air_time_elapsed += _body_state.step | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _update_anim_speed() -> void: | 
					
						
							|  |  |  | 	_anim_player.speed_scale = ( | 
					
						
							|  |  |  | 		_current_state_properties.animation_base_speed + ( | 
					
						
							|  |  |  | 			_current_state_properties.animation_speedup_with_velocity * | 
					
						
							|  |  |  | 			linear_velocity.length() | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _handle_teleport_request() -> void: | 
					
						
							|  |  |  | 	if _teleport_requested: | 
					
						
							|  |  |  | 		global_position = _teleport_target | 
					
						
							|  |  |  | 		_teleport_requested = false | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _handle_freeze_lerp_request(delta: float) -> void: | 
					
						
							|  |  |  | 	if freeze && _freeze_lerp_requested: | 
					
						
							|  |  |  | 		global_position = lerp( | 
					
						
							|  |  |  | 			global_position, | 
					
						
							|  |  |  | 			_freeze_lerp_target, | 
					
						
							|  |  |  | 			1.0 - 0.01**delta | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		if (global_position - _freeze_lerp_target).length() < height*0.03125: | 
					
						
							|  |  |  | 			global_position = _freeze_lerp_target | 
					
						
							|  |  |  | 			_freeze_lerp_requested = false | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _connect_to_state_machine() -> void: | 
					
						
							|  |  |  | 	_state_handler_descriptors = _make_state_handler_descriptors() | 
					
						
							|  |  |  | 	_state_machine.connect_handlers(_state_handler_descriptors) | 
					
						
							|  |  |  | 	_state_machine.state_changed.connect(_on_state_changed) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _disconnect_from_state_machine() -> void: | 
					
						
							|  |  |  | 	_state_machine.disconnect_handlers(_state_handler_descriptors) | 
					
						
							|  |  |  | 	_state_machine.state_changed.disconnect(_on_state_changed) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _enforce_certain_properties() -> void: | 
					
						
							|  |  |  | 	if _collider.shape is CapsuleShape3D: | 
					
						
							|  |  |  | 		_collider.shape = _collider.shape.duplicate() | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		_collider.shape = CapsuleShape3D.new() | 
					
						
							|  |  |  | 	_audio_player.bus = &'Sound Effects' | 
					
						
							|  |  |  | 	_property_save_restore_stack.target = self | 
					
						
							|  |  |  | 	if physics_material_override: | 
					
						
							|  |  |  | 		physics_material_override = physics_material_override.duplicate() | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		physics_material_override = PhysicsMaterial.new() | 
					
						
							|  |  |  | 	physics_material_override.friction = 0.0 | 
					
						
							|  |  |  | 	can_sleep = false | 
					
						
							|  |  |  | 	lock_rotation = true | 
					
						
							|  |  |  | 	freeze_mode = FREEZE_MODE_STATIC | 
					
						
							|  |  |  | 	custom_integrator = false | 
					
						
							|  |  |  | 	continuous_cd = true | 
					
						
							|  |  |  | 	contact_monitor = true | 
					
						
							|  |  |  | 	if max_contacts_reported < 6: | 
					
						
							|  |  |  | 		max_contacts_reported = 6 | 
					
						
							|  |  |  | 	collision_priority = 2.0 | 
					
						
							|  |  |  | 	health = max_health | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_state_changed(state_name: StringName) -> void: | 
					
						
							|  |  |  | 	# Empty out the property save/restore stack, | 
					
						
							|  |  |  | 	# restoring all saved properties along the way. | 
					
						
							|  |  |  | 	while !_property_save_restore_stack.is_empty(): | 
					
						
							|  |  |  | 		_property_save_restore_stack.pop() | 
					
						
							|  |  |  | 	# If there is a defined properties profile for this state: | 
					
						
							|  |  |  | 	if state_name in _state_properties: | 
					
						
							|  |  |  | 		_current_state_properties = _state_properties[state_name] | 
					
						
							|  |  |  | 		# Set coyote timer. | 
					
						
							|  |  |  | 		if _current_state_properties.use_coyote_time: | 
					
						
							|  |  |  | 			_state_coyote_time = _current_state_properties.coyote_time | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			_state_coyote_time = 0.0 | 
					
						
							|  |  |  | 		_state_time_elapsed = 0.0 | 
					
						
							|  |  |  | 		# Play animation. | 
					
						
							|  |  |  | 		var anim_name := _current_state_properties.animation_name | 
					
						
							|  |  |  | 		if _current_state_properties.animation_alt_name && randf() < 0.5: | 
					
						
							|  |  |  | 			anim_name = _current_state_properties.animation_alt_name | 
					
						
							|  |  |  | 		_anim_player.play( | 
					
						
							|  |  |  | 			anim_name, | 
					
						
							|  |  |  | 			_current_state_properties.animation_blend_time | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		# Play audio. | 
					
						
							|  |  |  | 		if _current_state_properties.audio: | 
					
						
							|  |  |  | 			play_sound( | 
					
						
							|  |  |  | 				_current_state_properties.audio, | 
					
						
							|  |  |  | 				_current_state_properties.audio_volume_db | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 		# Freeze/unfreeze. | 
					
						
							|  |  |  | 		freeze = ( | 
					
						
							|  |  |  | 			_current_state_properties.physics_mode == | 
					
						
							|  |  |  | 			CharacterStateProperties.PhysicsMode.FREEZE | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		# If entering non-carrying state, drop carried object. | 
					
						
							|  |  |  | 		if !_current_state_properties.is_carrying_state: | 
					
						
							|  |  |  | 			_put_down_without_animation() | 
					
						
							|  |  |  | 		# Save collider and etc properties to the stack | 
					
						
							|  |  |  | 		# and overwrite them with those provided by the state profile. | 
					
						
							|  |  |  | 		_property_save_restore_stack.push({ | 
					
						
							| 
									
										
										
										
											2025-03-29 11:45:10 -07:00
										 |  |  | 			^"_collider:rotation": ( | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 				Vector3.RIGHT*PI/2.0 | 
					
						
							|  |  |  | 				if _current_state_properties.collider_horizontal | 
					
						
							|  |  |  | 				else Vector3.ZERO | 
					
						
							|  |  |  | 			), | 
					
						
							| 
									
										
										
										
											2025-03-29 11:45:10 -07:00
										 |  |  | 			^"height": ( | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 				2.0*_current_state_properties.collider_radius | 
					
						
							|  |  |  | 				if _current_state_properties.collider_horizontal | 
					
						
							|  |  |  | 				else _current_state_properties.collider_length | 
					
						
							|  |  |  | 			), | 
					
						
							| 
									
										
										
										
											2025-03-29 11:45:10 -07:00
										 |  |  | 			^"width": ( | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 				_current_state_properties.collider_length | 
					
						
							|  |  |  | 				if _current_state_properties.collider_horizontal | 
					
						
							|  |  |  | 				else 2.0*_current_state_properties.collider_radius | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 		_property_save_restore_stack.push(_current_state_properties.etc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _handle_collision_with_other_character(other: Character) -> void: | 
					
						
							|  |  |  | 	if ( | 
					
						
							|  |  |  | 		other.is_attacking() && | 
					
						
							|  |  |  | 		!self.is_invulnerable() | 
					
						
							|  |  |  | 	): | 
					
						
							|  |  |  | 		take_damage( | 
					
						
							|  |  |  | 			other.get_attack_damage(), | 
					
						
							|  |  |  | 			other.get_attack_knockback(), | 
					
						
							|  |  |  | 			other.global_position, | 
					
						
							|  |  |  | 			other.linear_velocity | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _check_contacts() -> void: | 
					
						
							|  |  |  | 	# Sort static contacts into ground or wall and sum them up. | 
					
						
							|  |  |  | 	var new_ground := Vector3.ZERO | 
					
						
							|  |  |  | 	var new_wall := Vector3.ZERO | 
					
						
							|  |  |  | 	for i in _body_state.get_contact_count(): | 
					
						
							|  |  |  | 		var other := _body_state.get_contact_collider_object(i) | 
					
						
							|  |  |  | 		if other is StaticBody3D: | 
					
						
							|  |  |  | 			var contact_normal := _body_state.get_contact_local_normal(i) | 
					
						
							|  |  |  | 			if acos(contact_normal.dot(ground_normal)) <= _max_slope_angle: | 
					
						
							|  |  |  | 				new_ground += contact_normal | 
					
						
							|  |  |  | 			else: | 
					
						
							|  |  |  | 				new_wall += contact_normal | 
					
						
							|  |  |  | 		# Along the way, handle collisions with other kinds of objects. | 
					
						
							|  |  |  | 		elif other is Character: | 
					
						
							|  |  |  | 			_handle_collision_with_other_character(other) | 
					
						
							|  |  |  | 	# Update stored normals only if any direction "wins." | 
					
						
							|  |  |  | 	_grounded = !new_ground.is_zero_approx() | 
					
						
							|  |  |  | 	_touching_wall = !new_wall.is_zero_approx() | 
					
						
							|  |  |  | 	if _grounded: | 
					
						
							|  |  |  | 		ground_normal = new_ground.normalized() | 
					
						
							|  |  |  | 	if _touching_wall: | 
					
						
							|  |  |  | 		wall_normal = new_wall.normalized() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _reorient() -> void: | 
					
						
							|  |  |  | 	var gi := global_impetus | 
					
						
							|  |  |  | 	# Unless grounded, turn right-side-up. | 
					
						
							|  |  |  | 	if !_current_state_properties.counts_as_grounded: | 
					
						
							|  |  |  | 		var target_ground_normal: Vector3 = ( | 
					
						
							|  |  |  | 			Vector3.UP if gravity_scale >= 0.0 else Vector3.DOWN | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		ground_normal = lerp( | 
					
						
							|  |  |  | 			ground_normal, target_ground_normal, | 
					
						
							|  |  |  | 			1.0 - (1.0 - _air_righting_lerp_speed)**_body_state.step | 
					
						
							|  |  |  | 		) as Vector3 | 
					
						
							|  |  |  | 		if ground_normal.is_zero_approx(): | 
					
						
							|  |  |  | 			ground_normal = target_ground_normal | 
					
						
							|  |  |  | 		ground_normal = ground_normal.normalized() | 
					
						
							|  |  |  | 	# Determine target yaw. | 
					
						
							|  |  |  | 	var target_forward := Vector3.ZERO | 
					
						
							|  |  |  | 	match _current_state_properties.yaw_orientation: | 
					
						
							|  |  |  | 		CharacterStateProperties.OrientationMode.MANUAL: | 
					
						
							|  |  |  | 			# If manual, do not modify yaw. | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 			target_forward = -global_basis.z | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		CharacterStateProperties.OrientationMode.FOLLOW_VELOCITY: | 
					
						
							|  |  |  | 			# If follow velocity, face velocity direction. | 
					
						
							|  |  |  | 			if !linear_velocity.is_zero_approx(): | 
					
						
							|  |  |  | 				target_forward = linear_velocity.normalized() | 
					
						
							|  |  |  | 		CharacterStateProperties.OrientationMode.FOLLOW_ACCELERATION: | 
					
						
							|  |  |  | 			# If follow acceleration, face impetus direction. | 
					
						
							|  |  |  | 			if !gi.is_zero_approx(): | 
					
						
							|  |  |  | 				target_forward = gi.normalized() | 
					
						
							|  |  |  | 		CharacterStateProperties.OrientationMode.FOLLOW_NORMAL: | 
					
						
							|  |  |  | 			# If follow normal, face away from the wall if there is one. | 
					
						
							|  |  |  | 			# Else, mimic manual. | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 			target_forward = wall_normal if _touching_wall else -global_basis.z | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 	# Determine target pitch. | 
					
						
							|  |  |  | 	var target_upward := Vector3.ZERO | 
					
						
							|  |  |  | 	var old_target_forward := target_forward | 
					
						
							|  |  |  | 	match _current_state_properties.pitch_orientation: | 
					
						
							|  |  |  | 		CharacterStateProperties.OrientationMode.MANUAL: | 
					
						
							|  |  |  | 			# If manual, do not modify pitch. | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 			target_upward = global_basis.y | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		CharacterStateProperties.OrientationMode.FOLLOW_VELOCITY: | 
					
						
							|  |  |  | 			# If follow velocity, add influence from velocity to pitch. | 
					
						
							|  |  |  | 			target_upward = ground_normal | 
					
						
							|  |  |  | 			if !linear_velocity.is_zero_approx(): | 
					
						
							|  |  |  | 				var vnorm := linear_velocity.normalized() | 
					
						
							|  |  |  | 				target_forward += vnorm.project(target_upward) | 
					
						
							|  |  |  | 		CharacterStateProperties.OrientationMode.FOLLOW_ACCELERATION: | 
					
						
							|  |  |  | 			# If follow acceleration, add influence from impetus to pitch. | 
					
						
							|  |  |  | 			target_upward = ground_normal | 
					
						
							|  |  |  | 			if !gi.is_zero_approx(): | 
					
						
							|  |  |  | 				var inorm := gi.normalized() | 
					
						
							|  |  |  | 				target_forward += inorm.project(target_upward) | 
					
						
							|  |  |  | 		CharacterStateProperties.OrientationMode.FOLLOW_NORMAL: | 
					
						
							|  |  |  | 			# If follow normal, keep pitch level to ground. | 
					
						
							|  |  |  | 			target_upward = ground_normal | 
					
						
							|  |  |  | 	# In all cases, remove any pitch that was already implied by target_forward | 
					
						
							|  |  |  | 	# before other modifications involved in computing pitch. | 
					
						
							|  |  |  | 	# If pitch follows velocity or acceleration, | 
					
						
							|  |  |  | 	# then this amounts to *replacing* the pitch with the one implied | 
					
						
							|  |  |  | 	# by whichever vector the pitch is supposed to follow; | 
					
						
							|  |  |  | 	# otherwise, it amounts to keeping the pitch level to target_upward plane. | 
					
						
							|  |  |  | 	target_forward -= old_target_forward.project(target_upward) | 
					
						
							|  |  |  | 	# If target yaw and pitch are defined and not gimbal-locked, | 
					
						
							|  |  |  | 	# set angular velocity to try to rotate toward the described basis. | 
					
						
							|  |  |  | 	if ( | 
					
						
							|  |  |  | 		!target_forward.is_zero_approx() && | 
					
						
							|  |  |  | 		!target_forward.cross(target_upward).is_zero_approx() | 
					
						
							|  |  |  | 	): | 
					
						
							|  |  |  | 		target_forward = target_forward.normalized() | 
					
						
							|  |  |  | 		var target_basis := Basis.looking_at(target_forward, target_upward) | 
					
						
							|  |  |  | 		angular_velocity = ( | 
					
						
							|  |  |  | 			global_basis.x.cross(target_basis.x) + | 
					
						
							|  |  |  | 			global_basis.y.cross(target_basis.y) + | 
					
						
							|  |  |  | 			global_basis.z.cross(target_basis.z) | 
					
						
							|  |  |  | 		)*_current_state_properties.orientation_speed*_body_state.step | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		# If target yaw or pitch is undefined, | 
					
						
							|  |  |  | 		# or they exist but are gimbal-locked, | 
					
						
							|  |  |  | 		# cease all rotation. | 
					
						
							|  |  |  | 		angular_velocity = Vector3.ZERO | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _get_effective_impetus() -> Vector3: | 
					
						
							|  |  |  | 	var result := Vector3.ZERO | 
					
						
							|  |  |  | 	match _current_state_properties.physics_mode: | 
					
						
							|  |  |  | 		CharacterStateProperties.PhysicsMode.NORMAL: | 
					
						
							|  |  |  | 			# In normal mode, impetus is bound to ground plane. | 
					
						
							|  |  |  | 			result = global_impetus | 
					
						
							|  |  |  | 			result -= result.project(ground_normal) | 
					
						
							|  |  |  | 		CharacterStateProperties.PhysicsMode.FREE: | 
					
						
							|  |  |  | 			# In free mode, impetus may leave ground plane. | 
					
						
							|  |  |  | 			result = global_impetus | 
					
						
							|  |  |  | 	if result.is_zero_approx(): | 
					
						
							|  |  |  | 		return Vector3.ZERO | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		return result.normalized() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _do_standard_motion(delta: float) -> void: | 
					
						
							|  |  |  | 	# In freeze mode, this function does nothing. | 
					
						
							|  |  |  | 	if ( | 
					
						
							|  |  |  | 		_current_state_properties.physics_mode != | 
					
						
							|  |  |  | 		CharacterStateProperties.PhysicsMode.FREEZE | 
					
						
							|  |  |  | 	): | 
					
						
							|  |  |  | 		# Check where we are trying to go. | 
					
						
							|  |  |  | 		var effective_impetus := _get_effective_impetus() | 
					
						
							|  |  |  | 		# Check whether in free mode. | 
					
						
							|  |  |  | 		# Use air physics iff either in free mode or not grounded (by state). | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 		var use_air_physics := air_physics_active() | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		# Calculate "volitional" velocity | 
					
						
							|  |  |  | 		# (component of velocity projected into the space | 
					
						
							|  |  |  | 		# that our impetus is allowed to affect). | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 		var volitional_velocity := get_volitional_velocity() | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		# Check if we are exceeding top speed. | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 		var too_fast := going_too_fast() | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		# Regardless, also check how close we are to top speed, 1.0 max. | 
					
						
							|  |  |  | 		var top_speed_proximity: float = clamp( | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 			get_proportional_speed(), 0.0, 1.0 | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		) | 
					
						
							|  |  |  | 		# Use no friction with air physics. | 
					
						
							|  |  |  | 		# Otherwise, calculate friction according to speed | 
					
						
							|  |  |  | 		# (more speed = less friction [yes I know that's not realistic]). | 
					
						
							|  |  |  | 		physics_material_override.friction = ( | 
					
						
							|  |  |  | 			0.0 if use_air_physics | 
					
						
							|  |  |  | 			else lerp(_max_friction, 0.0, top_speed_proximity) | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		# Check which manual deceleration value to use. | 
					
						
							|  |  |  | 		var manual_deceleration: float = ( | 
					
						
							|  |  |  | 			_air_manual_deceleration if use_air_physics | 
					
						
							|  |  |  | 			else _ground_manual_deceleration | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		# Check which automatic deceleration value to use. | 
					
						
							|  |  |  | 		var auto_deceleration: float = ( | 
					
						
							|  |  |  | 			_air_auto_deceleration if use_air_physics | 
					
						
							|  |  |  | 			else _ground_auto_deceleration | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		# If we are using ground physics and not jumping, | 
					
						
							|  |  |  | 		# apply centrifugal force to allow running upside down. | 
					
						
							|  |  |  | 		if !use_air_physics && !jump_impetus: | 
					
						
							|  |  |  | 			apply_central_impulse( | 
					
						
							|  |  |  | 				-ground_normal*mass*delta*lerp( | 
					
						
							|  |  |  | 					0.0, _max_centrifugal_acceleration, | 
					
						
							|  |  |  | 					top_speed_proximity | 
					
						
							|  |  |  | 				) | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 		# If we are going too fast, adjust impetus | 
					
						
							|  |  |  | 		# to forbid manual acceleration in the direction we're already going. | 
					
						
							|  |  |  | 		if too_fast: | 
					
						
							|  |  |  | 			effective_impetus -= effective_impetus.project(volitional_velocity) | 
					
						
							|  |  |  | 			if !effective_impetus.is_zero_approx(): | 
					
						
							|  |  |  | 				effective_impetus = effective_impetus.normalized() | 
					
						
							|  |  |  | 		# If impetus, once adjusted, is zero, and volitional velocity is not, | 
					
						
							|  |  |  | 		# and we are not on a steep slope, then engage automatic deceleration. | 
					
						
							|  |  |  | 		if effective_impetus.is_zero_approx(): | 
					
						
							|  |  |  | 			if ( | 
					
						
							|  |  |  | 				!volitional_velocity.is_zero_approx() && | 
					
						
							|  |  |  | 				acos(ground_normal.dot( | 
					
						
							|  |  |  | 					Vector3.UP if gravity_scale >= 0.0 else Vector3.DOWN | 
					
						
							|  |  |  | 				)) <= _max_slope_angle | 
					
						
							|  |  |  | 			): | 
					
						
							|  |  |  | 				apply_central_impulse( | 
					
						
							|  |  |  | 					-volitional_velocity.normalized() * | 
					
						
							|  |  |  | 					mass*auto_deceleration*delta | 
					
						
							|  |  |  | 				) | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			# If impetus once adjusted is *not* zero, then proceed as follows: | 
					
						
							|  |  |  | 			# First, calculate "how decelerative" our impetus is, | 
					
						
							|  |  |  | 			# on a scale of 0.0 to 1.0. | 
					
						
							|  |  |  | 			var degree_decelerating: float | 
					
						
							|  |  |  | 			if volitional_velocity.is_zero_approx(): | 
					
						
							|  |  |  | 				degree_decelerating = 0.0 | 
					
						
							|  |  |  | 			else: | 
					
						
							|  |  |  | 				degree_decelerating = clamp( | 
					
						
							|  |  |  | 					(1.0 - ( | 
					
						
							|  |  |  | 						volitional_velocity.normalized().dot(effective_impetus)  | 
					
						
							|  |  |  | 					))/2.0, | 
					
						
							|  |  |  | 					0.0, 1.0 | 
					
						
							|  |  |  | 				) | 
					
						
							|  |  |  | 			# Then, check which forward acceleration value to use. | 
					
						
							|  |  |  | 			var max_accel: float = ( | 
					
						
							|  |  |  | 				_air_acceleration if use_air_physics else _ground_acceleration | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 			# Then, calculate true forward acceleration as follows: | 
					
						
							|  |  |  | 			# from standing at rest, | 
					
						
							|  |  |  | 			# it would be the forward acceleration value we just checked, | 
					
						
							|  |  |  | 			# but the closer we are to top speed, | 
					
						
							|  |  |  | 			# the closer that value is dragged down toward 0.0. | 
					
						
							|  |  |  | 			var forward_accel: float = lerp( | 
					
						
							|  |  |  | 				max_accel, 0.0, top_speed_proximity | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 			# Then, calculate actual acceleration as follows: | 
					
						
							|  |  |  | 			# simply interpolate between true forward acceleration | 
					
						
							|  |  |  | 			# and manual deceleration | 
					
						
							|  |  |  | 			# depending on "how decelerative" our impetus is. | 
					
						
							|  |  |  | 			var accel: float = lerp( | 
					
						
							|  |  |  | 				forward_accel, | 
					
						
							|  |  |  | 				manual_deceleration, | 
					
						
							|  |  |  | 				degree_decelerating | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 			# Finally, apply the resultant propulsion. | 
					
						
							|  |  |  | 			apply_central_impulse(effective_impetus*mass*accel*delta) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _shapecast(from: Vector3, to: Vector3) -> Dictionary: | 
					
						
							|  |  |  | 	# prepare from/to sweep | 
					
						
							|  |  |  | 	var space_state := get_world_3d().direct_space_state | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 	var query := _shapecast_object | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 	query.collide_with_areas = false | 
					
						
							|  |  |  | 	query.collide_with_bodies = true | 
					
						
							|  |  |  | 	query.collision_mask = 0xffffffff | 
					
						
							|  |  |  | 	query.exclude = [self] | 
					
						
							|  |  |  | 	query.margin = 0.0 | 
					
						
							|  |  |  | 	query.motion = to - from | 
					
						
							|  |  |  | 	query.shape = _collider.shape | 
					
						
							|  |  |  | 	query.transform = Transform3D(_collider.global_transform) | 
					
						
							|  |  |  | 	query.transform.origin = from | 
					
						
							|  |  |  | 	# do sweep | 
					
						
							|  |  |  | 	var dists := space_state.cast_motion(query) | 
					
						
							|  |  |  | 	# if no collisions: fail | 
					
						
							|  |  |  | 	if is_equal_approx(dists[0], 1.0) && is_equal_approx(dists[1], 1.0): | 
					
						
							|  |  |  | 		return {} | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		# if collisions: check if any intersections at safe point | 
					
						
							|  |  |  | 		# (i.e. if safe is actually unsafe) | 
					
						
							|  |  |  | 		query.motion = Vector3.ZERO | 
					
						
							|  |  |  | 		query.transform.origin = lerp(from, to, dists[0]) | 
					
						
							|  |  |  | 		if space_state.get_rest_info(query).is_empty(): | 
					
						
							|  |  |  | 			# if no intersections: check if any intersections at unsafe point | 
					
						
							|  |  |  | 			query.transform.origin = lerp(from, to, dists[1]) | 
					
						
							|  |  |  | 			var result := space_state.get_rest_info(query) | 
					
						
							|  |  |  | 			if result.is_empty(): | 
					
						
							|  |  |  | 				# if no intersections at unsafe point (should not happen): fail | 
					
						
							|  |  |  | 				return {} | 
					
						
							|  |  |  | 			else: | 
					
						
							|  |  |  | 				# if there is an intersection at unsafe point: | 
					
						
							|  |  |  | 				# add info about where the safe position is and return | 
					
						
							|  |  |  | 				result.own_position = ( | 
					
						
							|  |  |  | 					lerp(from, to, dists[0]) - global_basis.y*height/2.0 | 
					
						
							|  |  |  | 				) | 
					
						
							|  |  |  | 				return result | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			# if intersections: safe is actually unsafe, fail | 
					
						
							|  |  |  | 			return {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _get_ledge() -> Vector3: | 
					
						
							|  |  |  | 	var shapecast := _shapecast( | 
					
						
							|  |  |  | 		global_position + 2.0*height*ground_normal, | 
					
						
							|  |  |  | 		global_position - height*wall_normal/2.0 | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 	if ( | 
					
						
							|  |  |  | 		!shapecast.is_empty() && | 
					
						
							|  |  |  | 		acos(shapecast.normal.dot(ground_normal)) <= _max_slope_angle | 
					
						
							|  |  |  | 	): | 
					
						
							|  |  |  | 		return shapecast.own_position - global_position | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		return Vector3.ZERO | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _put_down_without_animation() -> void: | 
					
						
							|  |  |  | 	if _carrying: | 
					
						
							|  |  |  | 		_carrying.reparent(get_parent()) | 
					
						
							|  |  |  | 		_carrying.process_mode = PROCESS_MODE_INHERIT | 
					
						
							|  |  |  | 		_carrying = null | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _pick_up_without_animation(what: Node3D) -> void: | 
					
						
							|  |  |  | 	if _carrying: | 
					
						
							|  |  |  | 		_put_down_without_animation() | 
					
						
							|  |  |  | 	what.process_mode = PROCESS_MODE_DISABLED | 
					
						
							|  |  |  | 	what.reparent(self) | 
					
						
							|  |  |  | 	_carrying = what | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _state_animation_done() -> bool: | 
					
						
							|  |  |  | 	return !_anim_player.is_playing() || ( | 
					
						
							|  |  |  | 		_anim_player.current_animation != | 
					
						
							|  |  |  | 		_current_state_properties.animation_name && ( | 
					
						
							|  |  |  | 			!_current_state_properties.animation_alt_name || | 
					
						
							|  |  |  | 			_anim_player.current_animation != | 
					
						
							|  |  |  | 			_current_state_properties.animation_alt_name | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _get_jump_initial_velocity() -> float: | 
					
						
							|  |  |  | 	# h = vt - gt^2/2 | 
					
						
							|  |  |  | 	# dh/dt = v - gt = 0 | 
					
						
							|  |  |  | 	# v = gt | 
					
						
							|  |  |  | 	# t = v/g | 
					
						
							|  |  |  | 	# h = v(v/g) - g(v/g)^2/2 | 
					
						
							|  |  |  | 	# h = v^2/g - v^2/(2g) | 
					
						
							|  |  |  | 	# h = v^2/(2g) | 
					
						
							|  |  |  | 	# 2gh = v^2 | 
					
						
							|  |  |  | 	# v = sqrt(2gh) | 
					
						
							|  |  |  | 	var standard_gravity: float = PhysicsServer3D.area_get_param( | 
					
						
							|  |  |  | 		get_world_3d().space, | 
					
						
							|  |  |  | 		PhysicsServer3D.AreaParameter.AREA_PARAM_GRAVITY | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 	return sqrt(2.0*standard_gravity*_max_jump_height) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _get_jump_deceleration() -> float: | 
					
						
							|  |  |  | 	# v = sqrt(2gh) (from previous algebra) | 
					
						
							|  |  |  | 	# H = vt - Gt^2/2 | 
					
						
							|  |  |  | 	# vt - H = Gt^2/2 | 
					
						
							|  |  |  | 	# G = 2(vt - H)/t^2 = 2(sqrt(2gh)t - H)/t^2 | 
					
						
							|  |  |  | 	if _state_time_elapsed > 0.0: | 
					
						
							|  |  |  | 		var standard_gravity: float = PhysicsServer3D.area_get_param( | 
					
						
							|  |  |  | 			get_world_3d().space, | 
					
						
							|  |  |  | 			PhysicsServer3D.AreaParameter.AREA_PARAM_GRAVITY | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		var chonkbert: float = ( | 
					
						
							|  |  |  | 			2.0*( | 
					
						
							|  |  |  | 				sqrt( | 
					
						
							|  |  |  | 					2.0*standard_gravity*_max_jump_height | 
					
						
							|  |  |  | 				)*_state_time_elapsed - | 
					
						
							|  |  |  | 				_min_jump_height | 
					
						
							|  |  |  | 			)/(_state_time_elapsed**2.0) - | 
					
						
							|  |  |  | 			standard_gravity | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		if chonkbert > 0.0: | 
					
						
							|  |  |  | 			return chonkbert | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			return 0.0 | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		return 0.0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _apply_jump_impulse() -> void: | 
					
						
							|  |  |  | 	apply_central_impulse(mass*( | 
					
						
							|  |  |  | 		_get_jump_initial_velocity()*ground_normal - | 
					
						
							|  |  |  | 		linear_velocity.project(ground_normal) | 
					
						
							|  |  |  | 	)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _apply_jump_deceleration(delta: float) -> void: | 
					
						
							|  |  |  | 	apply_central_impulse( | 
					
						
							|  |  |  | 		mass*sign(gravity_scale)*Vector3.DOWN * | 
					
						
							|  |  |  | 		_get_jump_deceleration()*delta | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-29 11:13:11 -07:00
										 |  |  | func _pick_up_deferred_callback(what: Node3D) -> void: | 
					
						
							|  |  |  | 	if _carrying == what: | 
					
						
							|  |  |  | 		if what is RigidBody3D: | 
					
						
							|  |  |  | 			what.linear_velocity = Vector3.ZERO | 
					
						
							|  |  |  | 			what.angular_velocity = Vector3.ZERO | 
					
						
							|  |  |  | 		what.process_mode = PROCESS_MODE_DISABLED | 
					
						
							|  |  |  | 		what.reparent(self) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _put_down_deferred_callback(what: Node3D) -> void: | 
					
						
							|  |  |  | 	what.reparent(get_parent()) | 
					
						
							|  |  |  | 	what.process_mode = PROCESS_MODE_INHERIT | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _throw_deferred_callback(what: Node3D) -> void: | 
					
						
							|  |  |  | 	_put_down_deferred_callback(what) | 
					
						
							|  |  |  | 	if what is RigidBody3D: | 
					
						
							|  |  |  | 		what.linear_velocity = 1.5*linear_velocity.length()*knockback*( | 
					
						
							|  |  |  | 			(ground_normal/2.0 - global_basis.z).normalized() | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | #endregion | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #region Public Methods | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Whether the current state counts as grounded. | 
					
						
							|  |  |  | func is_grounded() -> bool: | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | 	return ( | 
					
						
							|  |  |  | 		_current_state_properties && | 
					
						
							|  |  |  | 		_current_state_properties.counts_as_grounded | 
					
						
							|  |  |  | 	) | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | ## Whether there is a current contact that counts as ground. | 
					
						
							|  |  |  | func is_really_grounded() -> bool: | 
					
						
							|  |  |  | 	return _grounded | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Whether there is a current contact that counts as a wall. | 
					
						
							|  |  |  | func is_touching_wall() -> bool: | 
					
						
							|  |  |  | 	return _touching_wall | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Whether coyote time prevents transition out of the current state. | 
					
						
							|  |  |  | func state_coyote_time_active() -> bool: | 
					
						
							|  |  |  | 	return _current_state_properties.use_coyote_time && ( | 
					
						
							|  |  |  | 		_state_coyote_time > 0.0 | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Whether there has been a ground contact since one coyote timespan ago. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Certain states use this function to decide whether to transition to falling, | 
					
						
							|  |  |  | ## independently of whether coyote time is still active for transitions | 
					
						
							|  |  |  | ## out of the state more generally. | 
					
						
							|  |  |  | func air_coyote_time_active() -> bool: | 
					
						
							|  |  |  | 	return _current_state_properties.use_coyote_time && ( | 
					
						
							|  |  |  | 		_air_time_elapsed <= _current_state_properties.coyote_time | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Whether the current state counts as an attack. | 
					
						
							|  |  |  | func is_attacking() -> bool: | 
					
						
							|  |  |  | 	return _current_state_properties.is_attack | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Attack damage before defense in current state if it is an attack. | 
					
						
							|  |  |  | func get_attack_damage() -> float: | 
					
						
							|  |  |  | 	if is_attacking(): | 
					
						
							|  |  |  | 		return attack_power*_current_state_properties.attack_base_damage | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		return 0.0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Attack knockback before resistance in current state if it is an attack. | 
					
						
							|  |  |  | func get_attack_knockback() -> float: | 
					
						
							|  |  |  | 	if is_attacking(): | 
					
						
							|  |  |  | 		return knockback*_current_state_properties.attack_base_knockback | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		return 0.0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Whether the character will ignore attacks received. | 
					
						
							|  |  |  | func is_invulnerable() -> bool: | 
					
						
							|  |  |  | 	return _current_state_properties.invulnerable | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Attempts state transition. May fail under certain conditions. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Failure conditions:[br] | 
					
						
							|  |  |  | ## * state does not exist (this is also an error condition);[br] | 
					
						
							|  |  |  | ## * coyote time is active;[br] | 
					
						
							|  |  |  | ## * state is uninterruptible | 
					
						
							|  |  |  | ##   (expected to leave it with force_change_state);[br] | 
					
						
							|  |  |  | ## * state is a carrying state and we are not carrying anything. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## In addition, if we *are* carrying something, | 
					
						
							|  |  |  | ## and the target state is *not* a carrying state, | 
					
						
							|  |  |  | ## but has an equivalent_carrying_state, | 
					
						
							|  |  |  | ## we will cancel this attempt and immediately attempt | 
					
						
							|  |  |  | ## to transition to the equivalent_carrying_state state instead. | 
					
						
							|  |  |  | ## In that case, the success or failure status | 
					
						
							|  |  |  | ## of that alternative transition attempt is returned. | 
					
						
							|  |  |  | func change_state(state_name: StringName) -> bool: | 
					
						
							|  |  |  | 	var prospective_state_properties := ( | 
					
						
							|  |  |  | 		_state_properties[state_name] | 
					
						
							|  |  |  | 		if state_name in _state_properties | 
					
						
							|  |  |  | 		else null | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 	if !state_exists(state_name): | 
					
						
							|  |  |  | 		# Disallow switching to nonexistent state. | 
					
						
							|  |  |  | 		# In addition, this is an error. | 
					
						
							|  |  |  | 		push_error("%s does not have state %s" % [self, state_name]) | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	elif _state_coyote_time > 0.0: | 
					
						
							|  |  |  | 		# Disallow switching away from state while coyote time is active. | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	elif _current_state_properties.uninterruptible: | 
					
						
							|  |  |  | 		# Disallow switching away from uninterruptible state. | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	elif !prospective_state_properties: | 
					
						
							|  |  |  | 		# Else, allow switching to state if it has no properties profile. | 
					
						
							|  |  |  | 		# (Remaining restrictions are based on the properties profile.) | 
					
						
							|  |  |  | 		_state_machine.state = state_name | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	elif prospective_state_properties.is_carrying_state && !_carrying: | 
					
						
							|  |  |  | 		# Disallow switching to carrying state while not carrying anything. | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	elif ( | 
					
						
							|  |  |  | 		_carrying && | 
					
						
							|  |  |  | 		!prospective_state_properties.is_carrying_state && | 
					
						
							|  |  |  | 		prospective_state_properties.equivalent_carrying_state | 
					
						
							|  |  |  | 	): | 
					
						
							|  |  |  | 		# If we are carrying something | 
					
						
							|  |  |  | 		# and there is an equivalent carrying state, | 
					
						
							|  |  |  | 		# switch to that one instead. | 
					
						
							|  |  |  | 		return change_state( | 
					
						
							|  |  |  | 			prospective_state_properties.equivalent_carrying_state | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		# If all checks pass, allow state transition. | 
					
						
							|  |  |  | 		_state_machine.state = state_name | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Directly invokes state transition without the sanity checks of change_state. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Suggested use is to ensure availability of actions | 
					
						
							|  |  |  | ## that can safely be exempted from other states' coyote time restrictions | 
					
						
							|  |  |  | ## and need to be highly responsive, such as jumping. | 
					
						
							|  |  |  | func force_change_state(state_name: StringName) -> void: | 
					
						
							|  |  |  | 	_state_machine.state = state_name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Teleports the character during the next physics tick. | 
					
						
							|  |  |  | func teleport(where: Vector3) -> void: | 
					
						
							|  |  |  | 	_teleport_requested = true | 
					
						
							|  |  |  | 	_teleport_target = where | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Lerps the character's position to the given target. Character must be frozen. | 
					
						
							|  |  |  | func set_freeze_lerp(where: Vector3) -> void: | 
					
						
							|  |  |  | 	if freeze: | 
					
						
							|  |  |  | 		_freeze_lerp_requested = true | 
					
						
							|  |  |  | 		_freeze_lerp_target = where | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		push_error("can't freeze-lerp %s: not frozen" % self) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Whether any behavior is implemented for some named state. | 
					
						
							|  |  |  | func state_exists(state_name: StringName) -> bool: | 
					
						
							|  |  |  | 	return ( | 
					
						
							|  |  |  | 		state_name in _state_handler_descriptors || | 
					
						
							|  |  |  | 		state_name in _state_properties | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Plays a sound on the character's dedicated audio player. | 
					
						
							|  |  |  | func play_sound(sound: AudioStream, volume_db: float = 0.0) -> void: | 
					
						
							|  |  |  | 	_audio_player.stop() | 
					
						
							|  |  |  | 	_audio_player.stream = sound | 
					
						
							|  |  |  | 	_audio_player.volume_db = volume_db | 
					
						
							|  |  |  | 	_audio_player.play() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## If vulnerable, processes damage and knockback and changes state. | 
					
						
							|  |  |  | func take_damage( | 
					
						
							|  |  |  | 	damage_in: float, | 
					
						
							|  |  |  | 	knockback_in: float, | 
					
						
							|  |  |  | 	source: Vector3 = global_position, | 
					
						
							|  |  |  | 	source_velocity: Vector3 = linear_velocity | 
					
						
							|  |  |  | ) -> void: | 
					
						
							|  |  |  | 	if !is_invulnerable(): | 
					
						
							|  |  |  | 		health -= damage_in/defense_power | 
					
						
							|  |  |  | 		var displacement := global_position - source | 
					
						
							|  |  |  | 		var relative_velocity := source_velocity - linear_velocity | 
					
						
							|  |  |  | 		var base_impulse: Vector3 | 
					
						
							|  |  |  | 		if displacement.is_zero_approx(): | 
					
						
							|  |  |  | 			base_impulse = relative_velocity | 
					
						
							|  |  |  | 		elif relative_velocity.length() < 1.0: | 
					
						
							|  |  |  | 			base_impulse = displacement.normalized() | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			base_impulse = ( | 
					
						
							|  |  |  | 				relative_velocity + | 
					
						
							|  |  |  | 				displacement.normalized()*relative_velocity.length() | 
					
						
							|  |  |  | 			).normalized()*relative_velocity.length() | 
					
						
							|  |  |  | 		if base_impulse.is_zero_approx(): | 
					
						
							|  |  |  | 			base_impulse = Vector3.UP | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			base_impulse = ( | 
					
						
							|  |  |  | 				(base_impulse.normalized() + Vector3.UP).normalized() * | 
					
						
							|  |  |  | 				base_impulse.length() | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 		apply_central_impulse( | 
					
						
							|  |  |  | 			mass*knockback_in*base_impulse/knockback_resistance | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func state_uninterruptible() -> bool: | 
					
						
							|  |  |  | 	return _current_state_properties.uninterruptible | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Puts down carried object. | 
					
						
							|  |  |  | func put_down(force: bool = false) -> void: | 
					
						
							|  |  |  | 	if _carrying: | 
					
						
							|  |  |  | 		if state_uninterruptible(): | 
					
						
							|  |  |  | 			if force: | 
					
						
							|  |  |  | 				_put_down_without_animation() | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			force_change_state(&'put-down') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Character picks up object and begins carrying it. | 
					
						
							|  |  |  | func pick_up(what: Node3D, force: bool = false) -> void: | 
					
						
							|  |  |  | 	if _carrying: | 
					
						
							|  |  |  | 		if state_uninterruptible(): | 
					
						
							|  |  |  | 			if force: | 
					
						
							|  |  |  | 				_put_down_without_animation() | 
					
						
							|  |  |  | 			else: | 
					
						
							|  |  |  | 				return | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			force_change_state(&'put-down') | 
					
						
							|  |  |  | 			await _state_machine.state_changed | 
					
						
							|  |  |  | 	if state_uninterruptible(): | 
					
						
							|  |  |  | 		if force: | 
					
						
							|  |  |  | 			_pick_up_without_animation(what) | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		_carrying = what | 
					
						
							|  |  |  | 		force_change_state(&'pick-up') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-29 02:12:43 -07:00
										 |  |  | ## Whether our current physics mode is free mode. | 
					
						
							|  |  |  | func is_free_moving() -> bool: | 
					
						
							|  |  |  | 	return _current_state_properties && ( | 
					
						
							|  |  |  | 		_current_state_properties.physics_mode == | 
					
						
							|  |  |  | 		CharacterStateProperties.PhysicsMode.FREE | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Whether we currently move according to air physics. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## If false, then we currently move according to ground physics. | 
					
						
							|  |  |  | func air_physics_active() -> bool: | 
					
						
							|  |  |  | 	return is_free_moving() || !is_grounded() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Component of linear velocity that can be affected by impetus. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## If we are free-moving, then this is simply our linear velocity. | 
					
						
							|  |  |  | ## Otherwise, it equals linear velocity projected onto ground plane. | 
					
						
							|  |  |  | func get_volitional_velocity() -> Vector3: | 
					
						
							|  |  |  | 	if is_free_moving(): | 
					
						
							|  |  |  | 		return linear_velocity | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		return linear_velocity - linear_velocity.project(ground_normal) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Top air speed if air physics active, else top ground speed. | 
					
						
							|  |  |  | func get_top_speed() -> float: | 
					
						
							|  |  |  | 	return _top_air_speed if air_physics_active() else _top_ground_speed | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Whether volitional velocity exceeds top speed. | 
					
						
							|  |  |  | func going_too_fast() -> bool: | 
					
						
							|  |  |  | 	return get_volitional_velocity().length() > get_top_speed() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Current volitional speed as compared to (divided by) top speed. | 
					
						
							|  |  |  | func get_proportional_speed() -> float: | 
					
						
							|  |  |  | 	return get_volitional_velocity().length()/get_top_speed() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | #endregion | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #region Protected Methods | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Creates handler descriptors to subscribe to the state machine. | 
					
						
							|  |  |  | ## [br][br] | 
					
						
							|  |  |  | ## Subclasses should override this | 
					
						
							|  |  |  | ## to extend the resulting dictionary with additional descriptors | 
					
						
							|  |  |  | ## for whatever novel states the subclass implements. | 
					
						
							|  |  |  | func _make_state_handler_descriptors() -> Dictionary[StringName, Variant]: | 
					
						
							|  |  |  | 	return { | 
					
						
							|  |  |  | 		&'idle': _on_idle_state_tick, | 
					
						
							|  |  |  | 		&'walk': _on_walk_state_tick, | 
					
						
							|  |  |  | 		&'run': _on_run_state_tick, | 
					
						
							|  |  |  | 		&'sprint': _on_sprint_state_tick, | 
					
						
							|  |  |  | 		&'jump': { | 
					
						
							|  |  |  | 			&'started': _on_jump_state_start, | 
					
						
							|  |  |  | 			&'ticked': _on_jump_state_tick | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		&'fall': _on_fall_state_tick, | 
					
						
							|  |  |  | 		&'skid': _on_skid_state_tick, | 
					
						
							|  |  |  | 		&'wall-slide': _on_wall_slide_state_tick, | 
					
						
							|  |  |  | 		&'swim': _on_swim_state_tick, | 
					
						
							|  |  |  | 		&'hang': _on_hang_state_tick, | 
					
						
							|  |  |  | 		&'pull-up': _on_pull_up_state_tick, | 
					
						
							|  |  |  | 		&'hit': _on_hit_state_tick, | 
					
						
							|  |  |  | 		&'victory1': _on_victory1_state_tick, | 
					
						
							|  |  |  | 		&'pick-up': { | 
					
						
							|  |  |  | 			&'started': _on_pick_up_state_start, | 
					
						
							|  |  |  | 			&'ticked': _on_pick_up_state_tick | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		&'put-down': { | 
					
						
							|  |  |  | 			&'ticked': _on_put_down_state_tick, | 
					
						
							|  |  |  | 			&'stopped': _on_put_down_state_stop | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2025-03-29 11:13:11 -07:00
										 |  |  | 		&'throw': _on_throw_state_tick, | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		&'idle-while-holding': _on_idle_while_holding_state_tick, | 
					
						
							|  |  |  | 		&'walk-while-holding': _on_walk_while_holding_state_tick, | 
					
						
							|  |  |  | 		&'run-while-holding': _on_run_while_holding_state_tick, | 
					
						
							|  |  |  | 		&'jump-while-holding': { | 
					
						
							|  |  |  | 			&'started': _on_jump_while_holding_state_start, | 
					
						
							|  |  |  | 			&'ticked': _on_jump_while_holding_state_tick | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		&'fall-while-holding': _on_fall_while_holding_state_tick, | 
					
						
							|  |  |  | 		&'swim-while-holding': _on_swim_while_holding_state_tick | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #endregion | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #region State Handlers | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_idle_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	if !is_really_grounded(): | 
					
						
							|  |  |  | 		state = &'fall' | 
					
						
							|  |  |  | 	elif jump_impetus: | 
					
						
							|  |  |  | 		force_change_state(&'jump') | 
					
						
							|  |  |  | 	elif !impetus.is_zero_approx(): | 
					
						
							|  |  |  | 		state = &'walk' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_walk_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	var top_speed_proximity: float = ( | 
					
						
							|  |  |  | 		linear_velocity - linear_velocity.project(ground_normal) | 
					
						
							|  |  |  | 	).length()/_top_ground_speed | 
					
						
							|  |  |  | 	if !is_really_grounded(): | 
					
						
							|  |  |  | 		state = &'fall' | 
					
						
							|  |  |  | 	elif jump_impetus: | 
					
						
							|  |  |  | 		force_change_state(&'jump') | 
					
						
							|  |  |  | 	elif top_speed_proximity >= 0.125: | 
					
						
							|  |  |  | 		state = &'run' | 
					
						
							|  |  |  | 	elif impetus.is_zero_approx() && top_speed_proximity < 0.03125: | 
					
						
							|  |  |  | 		state = &'idle' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_run_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	var top_speed_proximity: float = ( | 
					
						
							|  |  |  | 		linear_velocity - linear_velocity.project(ground_normal) | 
					
						
							|  |  |  | 	).length()/_top_ground_speed | 
					
						
							|  |  |  | 	if !is_really_grounded() && !air_coyote_time_active(): | 
					
						
							|  |  |  | 		state = &'fall' | 
					
						
							|  |  |  | 	elif jump_impetus: | 
					
						
							|  |  |  | 		force_change_state(&'jump') | 
					
						
							|  |  |  | 	elif top_speed_proximity >= 0.375: | 
					
						
							|  |  |  | 		state = &'sprint' | 
					
						
							|  |  |  | 	elif top_speed_proximity < 0.125: | 
					
						
							|  |  |  | 		state = &'walk' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_sprint_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	var top_speed_proximity: float = ( | 
					
						
							|  |  |  | 		linear_velocity - linear_velocity.project(ground_normal) | 
					
						
							|  |  |  | 	).length()/_top_ground_speed | 
					
						
							|  |  |  | 	if !is_really_grounded() && !air_coyote_time_active(): | 
					
						
							|  |  |  | 		state = &'fall' | 
					
						
							|  |  |  | 	elif jump_impetus: | 
					
						
							|  |  |  | 		force_change_state(&'jump') | 
					
						
							|  |  |  | 	elif top_speed_proximity < 0.375: | 
					
						
							|  |  |  | 		state = &'run' | 
					
						
							|  |  |  | 	elif _get_effective_impetus().dot(linear_velocity) <= -0.75: | 
					
						
							|  |  |  | 		state = &'skid' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_jump_state_start() -> void: | 
					
						
							|  |  |  | 	_apply_jump_impulse() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_jump_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	if !jump_impetus: | 
					
						
							|  |  |  | 		_apply_jump_deceleration(delta) | 
					
						
							| 
									
										
										
										
											2025-03-28 22:40:54 -07:00
										 |  |  | 	if is_really_grounded() && !state_coyote_time_active(): | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		play_sound(_landing_sound, _landing_sound_volume_db) | 
					
						
							|  |  |  | 		state = &'run' | 
					
						
							|  |  |  | 	elif linear_velocity.dot(ground_normal) <= -0.25: | 
					
						
							|  |  |  | 		state = &'fall' | 
					
						
							|  |  |  | 	elif _touching_wall: | 
					
						
							|  |  |  | 		var ledge_distance := _get_ledge() | 
					
						
							|  |  |  | 		if ledge_distance.is_zero_approx(): | 
					
						
							|  |  |  | 			state = &'wall-slide' | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			state = &'hang' | 
					
						
							|  |  |  | 			set_freeze_lerp(global_position + ledge_distance) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_fall_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							| 
									
										
										
										
											2025-03-28 22:40:54 -07:00
										 |  |  | 	if is_really_grounded() && !state_coyote_time_active(): | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		play_sound(_landing_sound, _landing_sound_volume_db) | 
					
						
							|  |  |  | 		state = &'run' | 
					
						
							|  |  |  | 	elif is_touching_wall(): | 
					
						
							|  |  |  | 		var ledge_distance := _get_ledge() | 
					
						
							|  |  |  | 		if ledge_distance.is_zero_approx(): | 
					
						
							|  |  |  | 			state = &'wall-slide' | 
					
						
							|  |  |  | 		else: | 
					
						
							|  |  |  | 			state = &'hang' | 
					
						
							|  |  |  | 			set_freeze_lerp(global_position + ledge_distance) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_hit_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	if is_really_grounded(): | 
					
						
							|  |  |  | 		state = &'idle' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_hang_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	global_basis = global_basis.slerp( | 
					
						
							|  |  |  | 		Basis.looking_at(-wall_normal, ground_normal), | 
					
						
							|  |  |  | 		1.0 - 0.01**delta | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 	if jump_impetus && !_freeze_lerp_requested: | 
					
						
							|  |  |  | 		force_change_state(&'pull-up') | 
					
						
							|  |  |  | 	elif action1_impetus || action2_impetus: | 
					
						
							|  |  |  | 		_freeze_lerp_requested = false | 
					
						
							|  |  |  | 		force_change_state(&'hit') | 
					
						
							|  |  |  | 		apply_central_impulse(4.0*mass*global_basis.z) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_pull_up_state_tick(_delta: float) -> void: | 
					
						
							|  |  |  | 	if _state_animation_done(): | 
					
						
							|  |  |  | 		force_change_state(&'idle') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_skid_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	if !is_really_grounded(): | 
					
						
							|  |  |  | 		state = &'fall' | 
					
						
							|  |  |  | 	elif _get_effective_impetus().dot(linear_velocity) >= 0.0: | 
					
						
							|  |  |  | 		state = &'walk' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_wall_slide_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	if jump_impetus && _state_coyote_time <= 0.0: | 
					
						
							|  |  |  | 		force_change_state(&'jump') | 
					
						
							| 
									
										
										
										
											2025-03-28 22:40:54 -07:00
										 |  |  | 		# in addition, not instead: | 
					
						
							|  |  |  | 		apply_central_impulse(mass*_get_jump_initial_velocity()*wall_normal) | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 	elif is_really_grounded(): | 
					
						
							|  |  |  | 		state = &'run' | 
					
						
							|  |  |  | 	elif !is_touching_wall(): | 
					
						
							|  |  |  | 		state = &'fall' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_victory1_state_tick(_delta: float) -> void: | 
					
						
							|  |  |  | 	if _state_animation_done(): | 
					
						
							|  |  |  | 		force_change_state(&'victory2') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_put_down_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	var target_displacement := ( | 
					
						
							|  |  |  | 		0.25*height*Vector3.UP + | 
					
						
							| 
									
										
										
										
											2025-03-29 02:44:10 -07:00
										 |  |  | 		1.5*width*Vector3.FORWARD | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 	) | 
					
						
							|  |  |  | 	if !_carrying: | 
					
						
							|  |  |  | 		force_change_state(&'idle') | 
					
						
							|  |  |  | 	elif _state_animation_done(): | 
					
						
							|  |  |  | 		_carrying.position = target_displacement | 
					
						
							|  |  |  | 		force_change_state(&'idle') | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		_carrying.position = lerp( | 
					
						
							|  |  |  | 			_carrying.position, | 
					
						
							|  |  |  | 			target_displacement, | 
					
						
							|  |  |  | 			_anim_player.current_animation_position / | 
					
						
							|  |  |  | 			_anim_player.current_animation_length | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_put_down_state_stop() -> void: | 
					
						
							|  |  |  | 	if _carrying: | 
					
						
							| 
									
										
										
										
											2025-03-29 11:13:11 -07:00
										 |  |  | 		call_deferred(&'_put_down_deferred_callback', _carrying) | 
					
						
							|  |  |  | 		_carrying = null | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | func _on_pick_up_state_start() -> void: | 
					
						
							|  |  |  | 	if _carrying: | 
					
						
							| 
									
										
										
										
											2025-03-29 11:13:11 -07:00
										 |  |  | 		call_deferred(&'_pick_up_deferred_callback', _carrying) | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 	else: | 
					
						
							|  |  |  | 		force_change_state(&'idle') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_pick_up_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	var target_displacement := ( | 
					
						
							|  |  |  | 		0.5*height*Vector3.UP + | 
					
						
							| 
									
										
										
										
											2025-03-29 11:45:10 -07:00
										 |  |  | 		0.75*width*Vector3.FORWARD | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 	) | 
					
						
							|  |  |  | 	if !_carrying: | 
					
						
							|  |  |  | 		force_change_state(&'idle') | 
					
						
							|  |  |  | 	elif _state_animation_done(): | 
					
						
							|  |  |  | 		_carrying.position = target_displacement | 
					
						
							|  |  |  | 		force_change_state(&'idle-while-holding') | 
					
						
							|  |  |  | 	else: | 
					
						
							|  |  |  | 		_carrying.position = lerp( | 
					
						
							|  |  |  | 			_carrying.position, | 
					
						
							|  |  |  | 			target_displacement, | 
					
						
							|  |  |  | 			_anim_player.current_animation_position / | 
					
						
							|  |  |  | 			_anim_player.current_animation_length | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_idle_while_holding_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	if !is_really_grounded(): | 
					
						
							|  |  |  | 		state = &'fall-while-holding' | 
					
						
							|  |  |  | 	elif jump_impetus: | 
					
						
							|  |  |  | 		force_change_state(&'jump-while-holding') | 
					
						
							|  |  |  | 	elif !impetus.is_zero_approx(): | 
					
						
							|  |  |  | 		state = &'walk-while-holding' | 
					
						
							| 
									
										
										
										
											2025-03-29 02:49:41 -07:00
										 |  |  | 	elif action1_impetus || action2_impetus || interact_impetus: | 
					
						
							| 
									
										
										
										
											2025-03-29 02:44:10 -07:00
										 |  |  | 		state = &'put-down' | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | func _on_walk_while_holding_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	var top_speed_proximity: float = ( | 
					
						
							|  |  |  | 		linear_velocity - linear_velocity.project(ground_normal) | 
					
						
							|  |  |  | 	).length()/_top_ground_speed | 
					
						
							|  |  |  | 	if !is_really_grounded(): | 
					
						
							|  |  |  | 		state = &'fall-while-holding' | 
					
						
							|  |  |  | 	elif jump_impetus: | 
					
						
							|  |  |  | 		force_change_state(&'jump-while-holding') | 
					
						
							|  |  |  | 	elif top_speed_proximity >= 0.125: | 
					
						
							|  |  |  | 		state = &'run-while-holding' | 
					
						
							|  |  |  | 	elif impetus.is_zero_approx() && top_speed_proximity < 0.03125: | 
					
						
							|  |  |  | 		state = &'idle-while-holding' | 
					
						
							| 
									
										
										
										
											2025-03-29 02:49:41 -07:00
										 |  |  | 	elif action1_impetus || action2_impetus || interact_impetus: | 
					
						
							|  |  |  | 		state = &'put-down' | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | func _on_run_while_holding_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	var top_speed_proximity: float = ( | 
					
						
							|  |  |  | 		linear_velocity - linear_velocity.project(ground_normal) | 
					
						
							|  |  |  | 	).length()/_top_ground_speed | 
					
						
							|  |  |  | 	if !is_really_grounded() && !air_coyote_time_active(): | 
					
						
							|  |  |  | 		state = &'fall-while-holding' | 
					
						
							|  |  |  | 	elif jump_impetus: | 
					
						
							|  |  |  | 		force_change_state(&'jump-while-holding') | 
					
						
							|  |  |  | 	elif top_speed_proximity < 0.125: | 
					
						
							|  |  |  | 		state = &'walk-while-holding' | 
					
						
							| 
									
										
										
										
											2025-03-29 11:13:11 -07:00
										 |  |  | 	elif action1_impetus || action2_impetus || interact_impetus: | 
					
						
							|  |  |  | 		state = &'throw' | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | func _on_jump_while_holding_state_start() -> void: | 
					
						
							|  |  |  | 	_apply_jump_impulse() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_jump_while_holding_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 	if !jump_impetus: | 
					
						
							|  |  |  | 		_apply_jump_deceleration(delta) | 
					
						
							| 
									
										
										
										
											2025-03-28 22:40:54 -07:00
										 |  |  | 	if is_really_grounded() && !state_coyote_time_active(): | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		play_sound(_landing_sound, _landing_sound_volume_db) | 
					
						
							|  |  |  | 		state = &'run-while-holding' | 
					
						
							|  |  |  | 	elif linear_velocity.dot(ground_normal) <= -0.25: | 
					
						
							|  |  |  | 		state = &'fall-while-holding' | 
					
						
							| 
									
										
										
										
											2025-03-29 11:13:11 -07:00
										 |  |  | 	elif action1_impetus || action2_impetus || interact_impetus: | 
					
						
							|  |  |  | 		state = &'throw' | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | func _on_fall_while_holding_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							| 
									
										
										
										
											2025-03-28 22:40:54 -07:00
										 |  |  | 	if is_really_grounded() && !state_coyote_time_active(): | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 		play_sound(_landing_sound, _landing_sound_volume_db) | 
					
						
							|  |  |  | 		state = &'run-while-holding' | 
					
						
							| 
									
										
										
										
											2025-03-29 11:13:11 -07:00
										 |  |  | 	elif action1_impetus || action2_impetus || interact_impetus: | 
					
						
							|  |  |  | 		state = &'throw' | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | func _on_swim_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func _on_swim_while_holding_state_tick(delta: float) -> void: | 
					
						
							|  |  |  | 	_do_standard_motion(delta) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-29 11:13:11 -07:00
										 |  |  | func _on_throw_state_tick(_delta: float) -> void: | 
					
						
							|  |  |  | 	if _state_animation_done(): | 
					
						
							|  |  |  | 		if _carrying: | 
					
						
							|  |  |  | 			_put_down_without_animation() | 
					
						
							|  |  |  | 		force_change_state(&'run') | 
					
						
							|  |  |  | 	elif _carrying && ( | 
					
						
							|  |  |  | 		_anim_player.current_animation_position / | 
					
						
							|  |  |  | 		_anim_player.current_animation_length | 
					
						
							|  |  |  | 	) >= 0.5: | 
					
						
							|  |  |  | 		call_deferred(&'_throw_deferred_callback', _carrying) | 
					
						
							|  |  |  | 		_carrying = null | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-28 19:02:51 -07:00
										 |  |  | #endregion |