extends RigidBody3D class_name Unit @export_group("Defence") @export var max_hp: float = 100 @export_group("Movement") @export var movement_force: float = 100 @export var rotation_torque: float = 3 @export var max_speed: float = 100 @export_group("Navigation") @export var minimum_progress_rate: float = 1.0 @export var stuck_time: float = 1.0 @onready var hp: float = max_hp var target_velocity: Vector3 = Vector3() var avoidance_velocity: Vector3 = Vector3() var avoidance_timeout: float = 0.0 var last_distance_to_target: float = 0.0 var stuck_timer: float = 0.0 var moving: bool = false var move_target: Vector3 = Vector3(16, 1, 13): set(target): move_target = target nav_agent_3d.target_position = move_target var move_radius: float = 5.0 var held_item: Item = null: set(val): held_item = val if held_item_meshinstance != null: if held_item != null: held_item_meshinstance.mesh = held_item.model held_item_meshinstance.visible = (held_item != null) @onready var shapecast_3d: ShapeCast3D = $ShapeCast3D @onready var nav_agent_3d: NavigationAgent3D = $NavigationAgent3D @onready var label_3d: Label3D = $Label3D @onready var held_item_meshinstance: MeshInstance3D = null var action_timeout: float = 0.0 enum TaskStatus { INTERRUPTED, TIMED_OUT, IMPOSSIBLE, DONE, } signal task_updated(task_status: TaskStatus) func _ready() -> void: nav_agent_3d.connect("velocity_computed", avoidance_velocity_computed) pass func avoidance_velocity_computed(velocity: Vector3) -> void: if velocity != target_velocity: avoidance_velocity = velocity avoidance_timeout = 0.5 func _process(delta: float) -> void: label_3d.text = "HP: %d" % hp if action_timeout > 0: action_timeout -= delta if action_timeout <= 0: task_updated.emit(TaskStatus.TIMED_OUT) if moving: if nav_agent_3d.is_target_reached() \ or nav_agent_3d.target_position.is_zero_approx() \ or !nav_agent_3d.is_target_reachable(): moving = false task_updated.emit(TaskStatus.DONE) target_velocity = Vector3() #nav_agent_3d.target_position = move_target + Vector3(randfn(0, move_radius), 0, randfn(0, move_radius)) #nav_agent_3d.target_position = NavigationServer3D.map_get_random_point(NavigationServer3D.get_maps()[0], 1, true) last_distance_to_target = nav_agent_3d.distance_to_target() else: var next_point: Vector3 = nav_agent_3d.get_next_path_position() if shapecast_3d.is_colliding(): var distance_to_target: float = global_position.distance_to(next_point) var progress_rate: float = (last_distance_to_target - distance_to_target) / delta last_distance_to_target = distance_to_target if progress_rate < minimum_progress_rate: stuck_timer += delta if stuck_timer >= stuck_time: unstuck() else: label_3d.modulate = Color.WHITE stuck_timer = 0 if global_position.y <= -10: unstuck() DebugDraw3D.draw_sphere(nav_agent_3d.target_position, 0.5, Color.RED) #DebugDraw3D.draw_sphere(next_point, 0.1, Color.YELLOW) var direction: Vector3 = (next_point - global_position).normalized() #basis = Basis.looking_at(direction) #DebugDraw3D.draw_line(global_position, global_position + linear_velocity, Color.BLUE) target_velocity = direction * max_speed nav_agent_3d.velocity = target_velocity #DebugDraw3D.draw_line(global_position, global_positiaon + target_velocity, Color.MAGENTA) #DebugDraw3D.draw_text(global_position + Vector3(0,1,0), "%f" % nav_agent_3d.distance_to_target()) func unstuck() -> void: # teleport to next path point linear_velocity = Vector3() global_position = nav_agent_3d.get_next_path_position() stuck_timer = 0 func hurt(damage: float) -> void: hp -= damage #print("%s hit for %f damage, HP=%f" % [name, damage, hp]) if hp <= 0: die() func die() -> void: queue_free() func _physics_process(delta: float) -> void: if shapecast_3d.is_colliding(): var actual_target_velocity: Vector3 = target_velocity if avoidance_timeout > 0: avoidance_timeout -= delta actual_target_velocity = actual_target_velocity.slerp(avoidance_velocity, 0.25) #DebugDraw3D.draw_line(global_position, global_position + actual_target_velocity, Color.ORANGE) var force_direction: Vector3 = (actual_target_velocity-linear_velocity) var normal: Vector3 = shapecast_3d.get_collision_normal(0) #DebugDraw3D.draw_line(global_position, global_position + normal, Color.DODGER_BLUE) var force: Vector3 = (force_direction * movement_force).slide(normal) #DebugDraw3D.draw_line(global_position, global_position + force, Color.GREEN) apply_central_force(force) apply_torque(Vector3(0,force_direction.normalized().signed_angle_to(-global_basis.z, Vector3.DOWN) * rotation_torque,0)) #DebugDraw3D.draw_line(global_position, global_position - global_basis.z, Color.BLUE) #DebugDraw3D.draw_line(global_position, global_position + force_direction, Color.RED) func send_task_update(task_status: TaskStatus) -> void: task_updated.emit(task_status) func wait_for_task_update() -> TaskStatus: var status_received: TaskStatus = await task_updated; return status_received func go_to_destination(destination: Vector3) -> bool: move_target = destination moving = true return (await wait_for_task_update()) == TaskStatus.DONE func take_item_from_building(item: Item, building: Building) -> bool: if held_item != null: return false if building.producer != null: if !await building.producer.wait_for_item(item, 3.0): return false #if building.consumer == null: #return false #if await building.consumer.wait_for_item(item, 3.0): #if !building.consumer.take_item_from_storage(item): #return false held_item = item return true func drop_item() -> bool: return false