Files
2026-08-12 10:02:45 +02:00

166 lines
4.5 KiB
GDScript

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)
)