extends CharacterBody3D const CHASE_SPEED := 3.5 const RETURN_SPEED := 2.5 const GRAVITY := -15.0 const ATTACK_COOLDOWN := 1.5 const ATTACK_DIST := 3.0 const ATTACK_WINDUP := 0.75 var target: Node3D = null var spawn_position := Vector3() var chasing := true var _attack_cooldown := 0.0 var _windup_timer := 0.0 var _is_flashing := false func set_target(node: Node3D) -> void: target = node func _physics_process(delta: float) -> void: if not target: return var goal_pos: Vector3 if chasing: goal_pos = target.global_position elif not chasing: goal_pos = spawn_position else: goal_pos = global_position var to_goal := goal_pos - global_position to_goal.y = 0.0 var distance := to_goal.length() if distance < 0.5: velocity.x = 0.0 velocity.z = 0.0 else: var direction := to_goal.normalized() var speed := CHASE_SPEED if chasing else RETURN_SPEED velocity.x = direction.x * speed velocity.z = direction.z * speed var target_angle := atan2(direction.x, direction.z) var current_angle := rotation.y var diff := target_angle - current_angle while diff > PI: diff -= TAU while diff < -PI: diff += TAU rotation.y += diff * clampf(10.0 * delta, 0.0, 1.0) if not is_on_floor(): velocity.y += GRAVITY * delta _attack_cooldown = maxf(0.0, _attack_cooldown - delta) if chasing and distance < ATTACK_DIST and _attack_cooldown <= 0.0: velocity.x = 0.0 velocity.z = 0.0 _face_player(delta) _windup_timer += delta if _windup_timer >= ATTACK_WINDUP: _windup_timer = 0.0 var to_player := target.global_position - global_position to_player.y = 0.0 if to_player.length() < ATTACK_DIST: target.take_hit() _attack_cooldown = ATTACK_COOLDOWN else: _windup_timer = 0.0 move_and_slide() func _face_player(delta: float) -> void: var direction := (target.global_position - global_position) direction.y = 0.0 direction = direction.normalized() var target_angle := atan2(direction.x, direction.z) var current_angle := rotation.y var diff := target_angle - current_angle while diff > PI: diff -= TAU while diff < -PI: diff += TAU rotation.y += diff * clampf(10.0 * delta, 0.0, 1.0) func take_hit() -> void: if _is_flashing: return _is_flashing = true _flash_meshes(Color.WHITE, Color.WHITE, 0.15) func _flash_meshes(flash_color: Color, emissive_color: Color, duration: float) -> void: var flash_mat := StandardMaterial3D.new() flash_mat.albedo_color = flash_color flash_mat.emission = emissive_color flash_mat.emission_energy = 2.0 for child in get_children(): if child is MeshInstance3D: var old: Material = child.material_override child.material_override = flash_mat await get_tree().create_timer(duration).timeout child.material_override = old _is_flashing = false