📄 src/dog.rs
use bevy::{
    app::{App, Startup, Update},
    asset::{AssetServer, Assets},
    camera::Camera,
    ecs::{
        component::Component,
        query::With,
        system::{Commands, Res, ResMut, Single},
    },
    gltf::GltfAssetLabel,
    math::{
        Dir3, Vec3,
        primitives::{Cuboid, InfinitePlane3d},
    },
    mesh::{Mesh, Mesh3d, MeshBuilder, Meshable},
    scene::SceneRoot,
    time::Time,
    transform::components::{GlobalTransform, Transform},
    window::Window,
};

pub trait DogSystems {
    fn add_dog_systems(&mut self) -> &mut Self;
}

impl DogSystems for App {
    fn add_dog_systems(&mut self) -> &mut Self {
        self.add_systems(Startup, setup)
            .add_systems(Update, control)
    }
}

#[derive(Component)]
pub struct Dog;

const SPEED: f32 = 10.0;

fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut meshes: ResMut<Assets<Mesh>>) {
    let mesh = meshes.add(Cuboid::from_length(2.0).mesh().build());

    commands.spawn((
        SceneRoot(
            asset_server
                .load(GltfAssetLabel::Scene(0).from_asset("models/cube-pets/animal-dog.glb")),
        ),
        Transform::from_xyz(0.0, 0.0, 0.0),
        Mesh3d(mesh),
        Dog,
    ));
}

fn control(
    window: Single<&Window>,
    camera: Single<(&Camera, &GlobalTransform)>,
    time: Res<Time>,
    mut dog: Single<&mut Transform, With<Dog>>,
) {
    let (camera, camera_transform) = *camera;

    let Some(cursor_position) = window
        .cursor_position()
        .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok())
    else {
        return;
    };

    let Some(cursor_position) =
        cursor_position.plane_intersection_point(Vec3::ZERO, InfinitePlane3d { normal: Dir3::Y })
    else {
        return;
    };

    let direction = cursor_position - dog.translation;
    let delta = SPEED * time.delta_secs();
    if direction.length_squared() > delta * delta {
        dog.look_to(-direction, Vec3::Y);
        dog.translation += direction.normalize() * delta;
    }
}