1333 lines
44 KiB
GDScript
1333 lines
44 KiB
GDScript
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
|
|
|
|
@onready var _shapecast_object := PhysicsShapeQueryParameters3D.new()
|
|
|
|
#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:
|
|
impetus = value.limit_length()
|
|
## 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:
|
|
return gi.limit_length()
|
|
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
|
|
## Whether the character is "trying" to perform a tertiary interaction.
|
|
var interact_impetus := false
|
|
## 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({
|
|
^".:_collider:rotation": (
|
|
Vector3.RIGHT*PI/2.0
|
|
if _current_state_properties.collider_horizontal
|
|
else Vector3.ZERO
|
|
),
|
|
^".:height": (
|
|
2.0*_current_state_properties.collider_radius
|
|
if _current_state_properties.collider_horizontal
|
|
else _current_state_properties.collider_length
|
|
),
|
|
^".:width": (
|
|
_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.
|
|
target_forward = -global_basis.z
|
|
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.
|
|
target_forward = wall_normal if _touching_wall else -global_basis.z
|
|
# 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.
|
|
target_upward = global_basis.y
|
|
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).
|
|
var use_air_physics := air_physics_active()
|
|
# Calculate "volitional" velocity
|
|
# (component of velocity projected into the space
|
|
# that our impetus is allowed to affect).
|
|
var volitional_velocity := get_volitional_velocity()
|
|
# Check if we are exceeding top speed.
|
|
var too_fast := going_too_fast()
|
|
# Regardless, also check how close we are to top speed, 1.0 max.
|
|
var top_speed_proximity: float = clamp(
|
|
get_proportional_speed(), 0.0, 1.0
|
|
)
|
|
# 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
|
|
var query := _shapecast_object
|
|
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
|
|
)
|
|
|
|
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()
|
|
)
|
|
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
|
|
## Whether the current state counts as grounded.
|
|
func is_grounded() -> bool:
|
|
return (
|
|
_current_state_properties &&
|
|
_current_state_properties.counts_as_grounded
|
|
)
|
|
|
|
## 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')
|
|
|
|
## 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()
|
|
|
|
#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
|
|
},
|
|
&'throw': _on_throw_state_tick,
|
|
&'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)
|
|
if is_really_grounded() && !state_coyote_time_active():
|
|
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)
|
|
if is_really_grounded() && !state_coyote_time_active():
|
|
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')
|
|
# in addition, not instead:
|
|
apply_central_impulse(mass*_get_jump_initial_velocity()*wall_normal)
|
|
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 +
|
|
1.5*width*Vector3.FORWARD
|
|
)
|
|
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:
|
|
call_deferred(&'_put_down_deferred_callback', _carrying)
|
|
_carrying = null
|
|
|
|
func _on_pick_up_state_start() -> void:
|
|
if _carrying:
|
|
call_deferred(&'_pick_up_deferred_callback', _carrying)
|
|
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 +
|
|
width*Vector3.FORWARD
|
|
)
|
|
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'
|
|
elif action1_impetus || action2_impetus || interact_impetus:
|
|
state = &'put-down'
|
|
|
|
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'
|
|
elif action1_impetus || action2_impetus || interact_impetus:
|
|
state = &'put-down'
|
|
|
|
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'
|
|
elif action1_impetus || action2_impetus || interact_impetus:
|
|
state = &'throw'
|
|
|
|
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)
|
|
if is_really_grounded() && !state_coyote_time_active():
|
|
play_sound(_landing_sound, _landing_sound_volume_db)
|
|
state = &'run-while-holding'
|
|
elif linear_velocity.dot(ground_normal) <= -0.25:
|
|
state = &'fall-while-holding'
|
|
elif action1_impetus || action2_impetus || interact_impetus:
|
|
state = &'throw'
|
|
|
|
func _on_fall_while_holding_state_tick(delta: float) -> void:
|
|
_do_standard_motion(delta)
|
|
if is_really_grounded() && !state_coyote_time_active():
|
|
play_sound(_landing_sound, _landing_sound_volume_db)
|
|
state = &'run-while-holding'
|
|
elif action1_impetus || action2_impetus || interact_impetus:
|
|
state = &'throw'
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
#endregion
|