use bevy_ecs::prelude::*;
use lunar_math::{Rect, Vec2, WorldTransform};
use crate::Camera;
#[derive(Resource)]
pub struct CameraFollow2d {
pub target: Entity,
pub lead: Vec2,
pub deadzone: Vec2,
pub bounds: Option<Rect>,
pub lerp_speed: f32,
}
pub(crate) fn camera_follow_system(
follow: Option<Res<CameraFollow2d>>,
mut camera: Option<ResMut<Camera>>,
transforms: Query<&WorldTransform>,
time: Res<lunar_core::Time>,
) {
let (Some(follow), Some(camera)) = (follow, camera.as_mut()) else {
return;
};
let Ok(target_transform) = transforms.get(follow.target) else {
return;
};
let desired = target_transform.translation + follow.lead;
let delta = desired - camera.position;
if delta.x.abs() <= follow.deadzone.x && delta.y.abs() <= follow.deadzone.y {
return;
}
let new_position = if follow.lerp_speed <= 0.0 {
desired
} else {
let t = (follow.lerp_speed * time.delta_seconds()).min(1.0);
camera.position + delta * t
};
camera.position = match follow.bounds {
None => new_position,
Some(bounds) => Vec2::new(
new_position.x.clamp(bounds.x, bounds.x + bounds.w),
new_position.y.clamp(bounds.y, bounds.y + bounds.h),
),
};
}
#[cfg(test)]
mod tests {
use super::*;
use lunar_core::Time;
fn make_world_with_target(position: Vec2) -> (World, Entity) {
let mut world = World::new();
let entity = world
.spawn(WorldTransform::from_xy(position.x, position.y))
.id();
world.insert_resource(Time::default());
(world, entity)
}
#[test]
fn snaps_to_target_with_zero_lerp() {
let (mut world, target) = make_world_with_target(Vec2::new(200.0, 100.0));
world.insert_resource(Camera::new());
world.insert_resource(CameraFollow2d {
target,
lead: Vec2::ZERO,
deadzone: Vec2::ZERO,
bounds: None,
lerp_speed: 0.0,
});
let mut system = IntoSystem::into_system(camera_follow_system);
system.initialize(&mut world);
let _ = system.run((), &mut world);
let camera = world.resource::<Camera>();
assert!((camera.position.x - 200.0).abs() < 0.001);
assert!((camera.position.y - 100.0).abs() < 0.001);
}
#[test]
fn deadzone_prevents_movement() {
let (mut world, target) = make_world_with_target(Vec2::new(10.0, 5.0));
world.insert_resource(Camera::new()); world.insert_resource(CameraFollow2d {
target,
lead: Vec2::ZERO,
deadzone: Vec2::new(50.0, 50.0),
bounds: None,
lerp_speed: 0.0,
});
let mut system = IntoSystem::into_system(camera_follow_system);
system.initialize(&mut world);
let _ = system.run((), &mut world);
let camera = world.resource::<Camera>();
assert!((camera.position.x - 0.0).abs() < 0.001);
assert!((camera.position.y - 0.0).abs() < 0.001);
}
#[test]
fn bounds_clamp_camera_position() {
let (mut world, target) = make_world_with_target(Vec2::new(9999.0, 9999.0));
world.insert_resource(Camera::new());
world.insert_resource(CameraFollow2d {
target,
lead: Vec2::ZERO,
deadzone: Vec2::ZERO,
bounds: Some(Rect::new(0.0, 0.0, 800.0, 600.0)),
lerp_speed: 0.0,
});
let mut system = IntoSystem::into_system(camera_follow_system);
system.initialize(&mut world);
let _ = system.run((), &mut world);
let camera = world.resource::<Camera>();
assert!((camera.position.x - 800.0).abs() < 0.001);
assert!((camera.position.y - 600.0).abs() < 0.001);
}
#[test]
fn lead_offset_applied() {
let (mut world, target) = make_world_with_target(Vec2::new(100.0, 100.0));
world.insert_resource(Camera::new());
world.insert_resource(CameraFollow2d {
target,
lead: Vec2::new(50.0, 0.0),
deadzone: Vec2::ZERO,
bounds: None,
lerp_speed: 0.0,
});
let mut system = IntoSystem::into_system(camera_follow_system);
system.initialize(&mut world);
let _ = system.run((), &mut world);
let camera = world.resource::<Camera>();
assert!((camera.position.x - 150.0).abs() < 0.001);
assert!((camera.position.y - 100.0).abs() < 0.001);
}
}