class_name CreatureVisual extends Node3D signal position_changed(creature_id: StringName, active_position: Vector3) signal arrived_at_sim_position(creature_id: StringName) const ARRIVAL_DISTANCE := 0.5 const PATH_RETARGET_DISTANCE := 0.8 const PATH_RETRY_BASE_SECONDS := 0.5 const PATH_RETRY_MAX_SECONDS := 4.0 @export_range(0.5, 12.0, 0.1) var move_speed := 3.5 @export_range(0.05, 1.0, 0.05) var waypoint_reached_distance := 0.3 @export_range(1.0, 20.0, 0.5) var rotation_speed := 8.0 var creature_id: StringName var sim_position := Vector3.INF var is_dead_visual := false var current_path := PackedVector3Array() var path_index := 0 var path_target := Vector3.INF var path_request_id := 0 var path_pending := false var path_query_count := 0 var path_retry_remaining := 0.0 var path_retry_delay := PATH_RETRY_BASE_SECONDS func _physics_process(delta: float) -> void: if is_dead_visual: return if not sim_position.is_finite(): return if _horizontal_distance_to(sim_position) <= ARRIVAL_DISTANCE: _stop_moving() return if path_retry_remaining > 0.0: path_retry_remaining = maxf(path_retry_remaining - delta, 0.0) if path_retry_remaining > 0.0: return _ensure_path() if path_pending: return if current_path.is_empty() or path_index >= current_path.size(): _request_path() return var next_position := current_path[path_index] var offset := next_position - global_position if offset.length() <= waypoint_reached_distance: path_index += 1 return var direction := offset.normalized() global_position = global_position.move_toward(next_position, move_speed * delta) position_changed.emit(creature_id, global_position) var target_angle := atan2(direction.x, direction.z) rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta) func set_sim_position(position: Vector3) -> void: if not position.is_finite(): return var target_changed := ( not sim_position.is_finite() or _horizontal_distance_to_position(sim_position, position) > PATH_RETARGET_DISTANCE ) sim_position = position if target_changed and not is_dead_visual: path_retry_remaining = 0.0 path_retry_delay = PATH_RETRY_BASE_SECONDS if _horizontal_distance_to(sim_position) <= ARRIVAL_DISTANCE: _stop_moving() return _request_path() func play_death() -> void: is_dead_visual = true _stop_moving() var tween := create_tween() tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) tween.tween_property(self, "scale", Vector3(0.001, 0.001, 0.001), 0.45) tween.tween_callback(queue_free) func build_visual() -> void: pass func _ensure_path() -> void: if ( path_pending or ( path_target.is_finite() and ( _horizontal_distance_to_position(path_target, sim_position) <= PATH_RETARGET_DISTANCE ) ) ): return _request_path() func _request_path() -> void: path_request_id += 1 var request_id := path_request_id path_target = sim_position current_path = PackedVector3Array() path_index = 0 path_pending = true await get_tree().physics_frame if request_id != path_request_id or is_dead_visual: return if _horizontal_distance_to(sim_position) <= ARRIVAL_DISTANCE: path_pending = false return current_path = NavigationServer3D.map_get_path( get_world_3d().navigation_map, global_position, sim_position, true ) path_query_count += 1 path_pending = false if current_path.is_empty(): _schedule_path_retry() return path_retry_remaining = 0.0 path_retry_delay = PATH_RETRY_BASE_SECONDS func _schedule_path_retry() -> void: current_path = PackedVector3Array() path_index = 0 path_pending = false path_target = sim_position path_retry_remaining = path_retry_delay path_retry_delay = minf(path_retry_delay * 2.0, PATH_RETRY_MAX_SECONDS) func _stop_moving() -> void: if ( current_path.is_empty() and not path_pending and path_retry_remaining <= 0.0 and path_target.is_finite() and sim_position.is_finite() and _horizontal_distance_to_position(path_target, sim_position) <= PATH_RETARGET_DISTANCE ): return path_request_id += 1 current_path = PackedVector3Array() path_index = 0 path_pending = false path_target = sim_position path_retry_remaining = 0.0 path_retry_delay = PATH_RETRY_BASE_SECONDS func _horizontal_distance_to(target_position: Vector3) -> float: return _horizontal_distance_to_position(global_position, target_position) static func _horizontal_distance_to_position(from_position: Vector3, to_position: Vector3) -> float: return Vector2(from_position.x, from_position.z).distance_to( Vector2(to_position.x, to_position.z) )