class_name PoliceCar extends CharacterBody3D const FRAME_IDXS := 10 @export var dot_curve: Curve @export var max_speed := 8. @export var health := 10. @export var knockback_mul := 1. @export var invulnerable := false @export var shatter_scene: PackedScene @export var mesh: Node3D var frame_idx := 0 var frame_count := 0 var knockback := Vector3.ZERO var default_movement := true var stunned := false func set_rotation_to_velocity() -> void: rotation.y = Vector2(velocity.x, -velocity.z).angle() func exp_lerp(a: Variant, b: Variant, decay: float, dt: float) -> Variant: return lerp(a, b, 1 - exp(-decay * dt)) func _ready() -> void: frame_idx = randi() % FRAME_IDXS func _update_nav_agent() -> void: %NavAgent.target_position = Player.instance.global_position func _process_movement(delta: float) -> void: var dir := global_position.direction_to(%NavAgent.get_next_path_position()) dir.y = 0 var dot := dir.dot(velocity.normalized()) var dot_power := dot_curve.sample(dot) velocity += dir * 8. * delta * dot_power velocity = velocity.limit_length(max_speed) func _process(delta: float) -> void: if not default_movement: return if stunned: return frame_count = (frame_count + 1) % FRAME_IDXS if frame_count == frame_idx: _update_nav_agent() if knockback.length() < 0.1: _process_movement(delta) set_rotation_to_velocity() position.y = 0.01 else: velocity = knockback knockback = exp_lerp(knockback, Vector3.ZERO, 15., delta) move_and_slide() func kill(damager_pos = Vector3.ZERO) -> void: queue_free() var siren: AudioStreamPlayer3D = %Siren siren.reparent(get_tree().current_scene) var t := siren.create_tween() if randf() < .25: t.tween_property(siren, "pitch_scale", .25, .5) t.tween_property(siren, "pitch_scale", .35, .25) t.tween_property(siren, "pitch_scale", .0, .25) else: t.tween_property(siren, "pitch_scale", .0, .5) t.finished.connect(siren.queue_free) var meshes: Array[MeshInstance3D] if shatter_scene: var shatter: Node3D = shatter_scene.instantiate() get_tree().current_scene.add_child(shatter) shatter.global_transform = mesh.global_transform var rbs := shatter.get_child(1).get_children() for rb: RigidBody3D in rbs: var dir = damager_pos.direction_to(rb.global_position) rb.apply_impulse(dir * 17) rb.collision_layer = 0 meshes.push_back(rb.get_child(1)) var t2 := shatter.create_tween() var first := true t2.tween_interval(1) for m in meshes: var t3 = t2 if not first: t3 = t2.parallel() t3.tween_property(m, "transparency", 1., 0.5) first = false t2.finished.connect(shatter.queue_free) SignalBus.enemy_destroyed.emit() func hit(proj: Node3D, damage: float) -> bool: if invulnerable: return true health -= damage if health <= 0: kill(proj.global_position) else: if proj is PlayerProjectile: knockback = proj.velocity * 2. * knockback_mul * proj.knockback_mul return true func _on_hurtbox_body_entered(body: Node3D) -> void: if body is Player: body.damage(self)