class_name GrunkBeast extends CharacterBody3D ## Grunk beast controller #region Constants const STALKING_SOUND_LIMIT := 25.0 #endregion #region Exported Properties @export var play_spawn_animation := true @export var base_speed := 80.0 @export var pursuit_speed := 360.0 @export var debug_destroy: bool: set(value): queue_free() #endregion #region Member Variables var gravity: Vector3 = ( ProjectSettings.get_setting("physics/3d/default_gravity") * ProjectSettings.get_setting("physics/3d/default_gravity_vector") ) var pathfinding := true var traversing_link := false @onready var model: BeastModel = %Shambler @onready var nav_agent: NavigationAgent3D = %NavAgent @onready var nav_probe: NavigationAgent3D = %NavProbe @onready var stalking_timer: Timer = %StalkingTimer @onready var blackboard: Blackboard = %Blackboard @onready var behavior: BeehaveTree = %GrunkBeastBehavior #endregion #region Character Controller func _ready() -> void: if play_spawn_animation: model.play_spawn_animation() behavior.disable() model.spawn_animation_finished.connect(behavior.enable) func is_pursuing() -> bool: return blackboard.has_value("pursuit_target") func is_stalking() -> bool: return false # TODO func get_speed() -> float: if is_pursuing(): return pursuit_speed return base_speed func path_shorter_than(target: Vector3, limit: float) -> bool: var length := 0.0 var last_pos := global_position nav_probe.target_position = target # Fail early if the target is unreachable. # NOTE: this call also forces a navigation path refresh! Do not remove! if not nav_probe.is_target_reachable(): return false var path := nav_probe.get_current_navigation_path().slice( nav_probe.get_current_navigation_path_index() ) if not path: # Shouldn't be possible, but if it is it would cause problems if we didn't fail here print_debug("Target is reachable but has no path (tell the developer!)") return false # Integrate along path for waypoint: Vector3 in path: length += last_pos.distance_to(waypoint) if length > limit: return false last_pos = waypoint return true ## Clear this beast's pursuit and stalking targets func clear_aggro() -> void: blackboard.erase_value("pursuit_target") blackboard.erase_value("stalking_target") func _physics_process(delta: float) -> void: var motion := Vector3.ZERO if pathfinding and not nav_agent.is_navigation_finished(): var path_pos := nav_agent.get_next_path_position() var relative_pos := path_pos - global_position motion = relative_pos.normalized() * get_speed() * delta velocity.x = motion.x velocity.z = motion.z if not is_on_floor(): velocity += gravity * delta if motion: model.set_target_rotation(atan2(motion.x, motion.z)) model.set_move_speed(velocity.length()) move_and_slide() func on_sound_detected(source: Vector3) -> void: # Check that the source isn't too far away, e.g. a sound from another room if behavior.enabled and path_shorter_than(source, STALKING_SOUND_LIMIT): blackboard.set_value("stalking_target", source) stalking_timer.start() #endregion func _on_link_reached(_details: Dictionary) -> void: traversing_link = true func _on_waypoint_reached(_details: Dictionary) -> void: traversing_link = false