use super::{Bounds, Contact, Contacts, Velocity};
use crate::{BitFlags, Body, SpriteFlag};
pub(super) struct Collider {
position: (f32, f32),
size: (u16, u16),
solid: BitFlags<SpriteFlag>,
mask: BitFlags<SpriteFlag>,
worn: BitFlags<SpriteFlag>,
reads_map: bool,
}
pub(super) type Neighbour = (Bounds, BitFlags<SpriteFlag>, BitFlags<SpriteFlag>);
pub(super) trait Cast {
fn carried(&self) -> BitFlags<SpriteFlag>;
fn len(&self) -> usize;
fn at(&self, index: usize) -> Option<Neighbour>;
fn note(&self, _index: usize) {}
}
impl Collider {
#[inline(always)]
pub(super) fn new(
body: &Body,
bounds: Bounds,
solid: BitFlags<SpriteFlag>,
heeds: BitFlags<SpriteFlag>,
worn: BitFlags<SpriteFlag>,
reads_map: bool,
) -> Option<Self> {
if bounds.is_empty() {
return None;
}
let (x, y) = body.pos();
let (draw_x, draw_y) = body.draw_pos();
let position = (
x + (bounds.x() - draw_x) as f32,
y + (bounds.y() - draw_y) as f32,
);
Some(Self {
position,
size: (bounds.width(), bounds.height()),
solid,
mask: solid | heeds,
worn,
reads_map,
})
}
#[inline(always)]
pub(super) fn expel(&mut self, cast: &impl Cast) -> ((f32, f32), Contacts) {
let solid = self.solid;
let members = cast.len();
let mut placed = self.placed(self.position.0, self.position.1);
let (mut moved_x, mut moved_y) = (0, 0);
let mut sides = BitFlags::empty();
let mut touched = BitFlags::empty();
for _ in 0..PASSES {
let began = placed;
let began_edges = edges(began);
for index in 0..members {
let Some((bounds, flags, _)) = cast.at(index) else {
continue;
};
let neighbour = edges(bounds);
if !solid.intersects(flags)
|| !overlap(began_edges, neighbour)
|| !overlap(edges(placed), neighbour)
{
continue;
}
let (dx, dy, side) = shove(placed, bounds);
placed = nudged(placed, dx, dy);
moved_x += dx;
moved_y += dy;
sides = sides | side;
touched = touched | (flags & self.mask);
}
if placed == began {
break;
}
}
let push = (moved_x as f32, moved_y as f32);
self.position = (self.position.0 + push.0, self.position.1 + push.1);
(push, Contacts { sides, touched })
}
#[inline(always)]
pub(super) fn could_be_inside_something(&self, cast: &impl Cast) -> bool {
self.solid.intersects(cast.carried())
}
#[inline(always)]
pub(super) fn resolve(
&self,
velocity: Velocity,
tiles: impl Fn(i16, i16) -> BitFlags<SpriteFlag> + Copy,
cast: &impl Cast,
) -> (Velocity, Contacts) {
let (x, y) = self.position;
let (from_x, from_y) = (floor(x), floor(y));
let to_x = floor(x + velocity.dx);
let already = self.at(from_x, from_y);
let mut moved = velocity;
let mut sides = BitFlags::empty();
let (mut touched, stopped) = if moved.dx == 0.0 && moved.dy != 0.0 {
(BitFlags::empty(), false)
} else {
let attempted = self.at(to_x, from_y);
self.crossed(
span(already, attempted),
attempted,
already,
moved.dx != 0.0,
tiles,
cast,
)
};
if stopped {
if moved.dx > 0.0 {
sides = sides | Contact::Right;
} else {
sides = sides | Contact::Left;
}
moved.dx = 0.0;
}
if moved.dy != 0.0 {
let end_x = if moved.dx == 0.0 { from_x } else { to_x };
let from = if moved.dx == 0.0 {
already
} else {
self.at(end_x, from_y)
};
let attempted = self.at(end_x, floor(y + moved.dy));
let (met, stopped) =
self.crossed(span(from, attempted), attempted, already, true, tiles, cast);
touched = touched | met;
if stopped {
if moved.dy > 0.0 {
sides = sides | Contact::Below;
} else {
sides = sides | Contact::Above;
}
moved.dy = 0.0;
}
}
(moved, Contacts { sides, touched })
}
#[inline(always)]
fn crossed(
&self,
swept: Bounds,
reach: Bounds,
already: Bounds,
stopping: bool,
tiles: impl Fn(i16, i16) -> BitFlags<SpriteFlag>,
cast: &impl Cast,
) -> (BitFlags<SpriteFlag>, bool) {
let wall_hunting = stopping && !self.solid.is_empty();
let (mut flags, mut stopped) = self.tiles_under(swept, reach, wall_hunting, tiles);
let swept = edges(swept);
let reach = edges(reach);
let already = edges(already);
let solid = self.solid;
let mask = self.mask;
let worn = self.worn;
for index in 0..cast.len() {
let Some((bounds, carried, wants)) = cast.at(index) else {
continue;
};
let mine = mask.intersects(carried);
let theirs = wants.intersects(worn);
if !mine && !theirs {
continue;
}
let bounds = edges(bounds);
if overlap(swept, bounds) {
if mine {
flags = flags | (carried & mask);
}
if theirs {
cast.note(index);
}
}
if wall_hunting
&& !stopped
&& solid.intersects(carried)
&& overlap(reach, bounds)
&& !overlap(already, bounds)
{
stopped = true;
}
}
(flags, stopped)
}
#[inline(always)]
fn tiles_under(
&self,
swept: Bounds,
reach: Bounds,
wall_hunting: bool,
tiles: impl Fn(i16, i16) -> BitFlags<SpriteFlag>,
) -> (BitFlags<SpriteFlag>, bool) {
let mut flags = BitFlags::empty();
let mut stopped = false;
if !self.reads_map {
return (flags, stopped);
}
if self.mask.is_empty() {
return (flags, stopped);
}
let (Some((left, right)), Some((top, bottom))) = (
fenced(crossed(swept.x(), swept.width()), crate::MAP_WIDTH_TILES),
fenced(crossed(swept.y(), swept.height()), crate::MAP_HEIGHT_TILES),
) else {
return (flags, stopped);
};
let ((wall_left, wall_right), (wall_top, wall_bottom)) = if wall_hunting {
(
crossed(reach.x(), reach.width()),
crossed(reach.y(), reach.height()),
)
} else {
((0, 0), (0, 0))
};
let mut ty = top;
loop {
let row = wall_hunting && ty >= wall_top && ty <= wall_bottom;
let mut tx = left;
loop {
let carried = tiles(tx as i16, ty as i16);
flags = flags | (carried & self.mask);
if row && tx >= wall_left && tx <= wall_right && self.stops_at(carried) {
stopped = true;
}
if tx == right {
break;
}
tx += 1;
}
if ty == bottom {
break;
}
ty += 1;
}
(flags, stopped)
}
#[inline(always)]
pub(super) fn stops_at(&self, flags: BitFlags<SpriteFlag>) -> bool {
self.solid.intersects(flags)
}
#[inline(always)]
fn placed(&self, x: f32, y: f32) -> Bounds {
self.at(floor(x), floor(y))
}
#[inline(always)]
fn at(&self, x: i16, y: i16) -> Bounds {
let (width, height) = self.size;
Bounds::new(x, y, width, height)
}
}
#[inline(always)]
fn floor(value: f32) -> i16 {
crate::motion::floor_i16(value)
}
#[inline(always)]
fn edges(bounds: Bounds) -> (i32, i32, i32, i32) {
(
bounds.x() as i32,
bounds.y() as i32,
far(bounds.x(), bounds.width()),
far(bounds.y(), bounds.height()),
)
}
#[inline(always)]
fn overlap(one: (i32, i32, i32, i32), other: (i32, i32, i32, i32)) -> bool {
let (left, top, right, bottom) = one;
let (oleft, otop, oright, obottom) = other;
left < right
&& top < bottom
&& oleft < oright
&& otop < obottom
&& left < oright
&& right > oleft
&& top < obottom
&& bottom > otop
}
#[inline(always)]
fn shove(placed: Bounds, other: Bounds) -> (i32, i32, Contact) {
let (x, y) = (placed.x() as i32, placed.y() as i32);
let (width, height) = (placed.width() as i32, placed.height() as i32);
let (ox, oy) = (other.x() as i32, other.y() as i32);
let (owidth, oheight) = (other.width() as i32, other.height() as i32);
let across = (x + width).min(ox + owidth) - x.max(ox);
let down = (y + height).min(oy + oheight) - y.max(oy);
if across < down {
if x * 2 + width <= ox * 2 + owidth {
(-across, 0, Contact::Right)
} else {
(across, 0, Contact::Left)
}
} else if y * 2 + height <= oy * 2 + oheight {
(0, -down, Contact::Below)
} else {
(0, down, Contact::Above)
}
}
#[inline(always)]
fn nudged(placed: Bounds, dx: i32, dy: i32) -> Bounds {
let held = |value: i32| value.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
Bounds::new(
held(placed.x() as i32 + dx),
held(placed.y() as i32 + dy),
placed.width(),
placed.height(),
)
}
#[inline(always)]
fn span(from: Bounds, to: Bounds) -> Bounds {
let x = from.x().min(to.x());
let y = from.y().min(to.y());
let right = far(from.x(), from.width()).max(far(to.x(), to.width()));
let bottom = far(from.y(), from.height()).max(far(to.y(), to.height()));
Bounds::new(x, y, (right - x as i32) as u16, (bottom - y as i32) as u16)
}
#[inline(always)]
pub(super) fn far(near: i16, size: u16) -> i32 {
let edge = near as i32 + size as i32;
if edge > i16::MAX as i32 {
i16::MAX as i32
} else {
edge
}
}
fn fenced((near, far): (i32, i32), tiles: u16) -> Option<(i32, i32)> {
let near = near.max(0);
let far = far.min(tiles as i32 - 1);
(near <= far).then_some((near, far))
}
#[inline(always)]
fn crossed(start: i16, size: u16) -> (i32, i32) {
let last = (start as i32 + size as i32 - 1).min(i16::MAX as i32);
(start as i32 >> TILE_BITS, last >> TILE_BITS)
}
const TILE: i16 = 8;
const TILE_BITS: u32 = TILE.trailing_zeros();
const PASSES: usize = 4;
#[cfg(test)]
mod tests {
use core::iter;
use super::*;
fn map(rows: &'static [&'static str]) -> impl Fn(i16, i16) -> BitFlags<SpriteFlag> + Copy {
move |tx: i16, ty: i16| {
if tx < 0 || ty < 0 {
return BitFlags::empty();
}
match rows
.get(ty as usize)
.and_then(|row| row.as_bytes().get(tx as usize))
{
Some(b'#') => WALL.into(),
Some(b'~') => WATER.into(),
_ => BitFlags::empty(),
}
}
}
fn air(_: i16, _: i16) -> BitFlags<SpriteFlag> {
BitFlags::empty()
}
fn cast(list: &[Neighbour]) -> Written<'_> {
Written(list)
}
fn alone() -> Written<'static> {
Written(&[])
}
struct Written<'a>(&'a [Neighbour]);
impl Cast for Written<'_> {
fn carried(&self) -> BitFlags<SpriteFlag> {
self.0
.iter()
.fold(BitFlags::empty(), |all, &(_, f, _)| all | f)
}
fn len(&self) -> usize {
self.0.len()
}
fn at(&self, index: usize) -> Option<Neighbour> {
self.0.get(index).copied()
}
}
fn hitbox(x: f32, y: f32) -> Collider {
sized(x, y, 8, 8)
}
fn sized(x: f32, y: f32, width: u16, height: u16) -> Collider {
let body = Body::new(x, y);
let bounds = Bounds::of(&body, width, height);
Collider::new(
&body,
bounds,
WALL.into(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap()
}
fn heeding(
x: f32,
y: f32,
solid: BitFlags<SpriteFlag>,
heeds: BitFlags<SpriteFlag>,
) -> Collider {
let body = Body::new(x, y);
let bounds = Bounds::of(&body, 8, 8);
Collider::new(&body, bounds, solid, heeds, BitFlags::empty(), true).unwrap()
}
fn wall(x: i16, y: i16, width: u16, height: u16) -> Neighbour {
(
Bounds::new(x, y, width, height),
WALL.into(),
BitFlags::empty(),
)
}
fn carrying(x: i16, y: i16, flags: BitFlags<SpriteFlag>) -> Neighbour {
(Bounds::new(x, y, 8, 8), flags, BitFlags::empty())
}
#[test]
fn a_rectangle_with_no_pixels_in_it_has_no_collider() {
let body = Body::new(0.0, 0.0);
let solid = WALL.into();
for empty in [Bounds::new(0, 0, 0, 8), Bounds::new(0, 0, 8, 0)] {
assert!(
Collider::new(
&body,
empty,
solid,
BitFlags::all(),
BitFlags::empty(),
true
)
.is_none(),
"{empty:?}"
);
}
let bounds = Bounds::of(&body, 8, 8);
assert!(Collider::new(
&body,
bounds,
BitFlags::empty(),
BitFlags::all(),
BitFlags::empty(),
true
)
.is_some());
}
#[test]
fn what_a_box_heeds_is_what_it_is_told_about() {
let pond = [carrying(0, 0, WATER.into()), carrying(0, 0, SPIKES.into())];
let (_, contacts) = heeding(0.0, 0.0, BitFlags::empty(), WATER.into()).resolve(
Velocity::new(1.0, 0.0),
air,
&cast(&pond),
);
assert!(contacts.touches(WATER));
assert!(!contacts.touches(SPIKES));
let (_, contacts) = heeding(0.0, 0.0, BitFlags::empty(), BitFlags::empty()).resolve(
Velocity::new(1.0, 0.0),
air,
&cast(&pond),
);
assert_eq!(contacts, Contacts::empty());
}
#[test]
fn a_box_is_told_about_the_tiles_it_heeds_and_no_others() {
let pool = map(&["~~", "~~"]);
let (_, contacts) = heeding(0.0, 0.0, BitFlags::empty(), SPIKES.into()).resolve(
Velocity::new(1.0, 0.0),
pool,
&alone(),
);
assert_eq!(
contacts,
Contacts::empty(),
"the water was not its business"
);
let (_, contacts) = heeding(0.0, 0.0, BitFlags::empty(), WATER.into()).resolve(
Velocity::new(1.0, 0.0),
pool,
&alone(),
);
assert!(contacts.touches(WATER));
}
#[test]
fn a_box_the_size_of_the_coordinate_space_costs_the_map_s_worth_of_tiles() {
use core::cell::Cell;
let asked = Cell::new(0u32);
let counted = |_: i16, _: i16| {
asked.set(asked.get() + 1);
BitFlags::empty()
};
let body = Body::new(0.0, 0.0);
let everywhere = Bounds::new(i16::MIN, i16::MIN, u16::MAX, u16::MAX);
let collider = Collider::new(
&body,
everywhere,
WALL.into(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
collider.resolve(Velocity::new(1.0, 0.0), counted, &alone());
assert_eq!(
asked.get(),
128 * 64,
"a box over everything must be asked about the whole map — no less (a span cut wrong
loses real coverage) and no more (the coordinate space is a thousand times bigger)"
);
asked.set(0);
let body = Body::new(-300.0, -300.0);
let outside = Bounds::of(&body, 8, 8);
let collider = Collider::new(
&body,
outside,
WALL.into(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
collider.resolve(Velocity::new(1.0, 0.0), counted, &alone());
assert_eq!(asked.get(), 0);
}
#[test]
fn a_fall_stops_on_the_floor_and_reports_it() {
let floor = map(&["....", "....", "####"]);
let (moved, contacts) = hitbox(0.0, 7.0).resolve(Velocity::new(0.0, 4.0), floor, &alone());
assert_eq!(moved, Velocity::default(), "it fell through the floor");
assert!(contacts.below());
assert!(!contacts.above() && !contacts.left() && !contacts.right());
assert!(contacts.touches(WALL));
}
#[test]
fn a_fall_that_clears_the_floor_is_left_alone() {
let floor = map(&["....", "....", "####"]);
let velocity = Velocity::new(0.5, 1.0);
let (moved, contacts) = hitbox(0.0, 0.0).resolve(velocity, floor, &alone());
assert_eq!(moved, velocity);
assert_eq!(contacts, Contacts::empty());
}
#[test]
fn the_sub_pixel_position_is_kept_rather_than_the_drawn_one() {
let wall = map(&[".#"]);
let body = Body::new(0.5, 0.0);
let bounds = Bounds::of(&body, 8, 8);
let collider = Collider::new(
&body,
bounds,
WALL.into(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
assert_eq!(
bounds.x(),
0,
"the drawn pixel is the floor of the exact one"
);
let (moved, contacts) = collider.resolve(Velocity::new(0.5, 0.0), wall, &alone());
assert_eq!(moved, Velocity::default());
assert!(contacts.right());
}
#[test]
fn a_box_too_long_to_measure_still_samples_inside_itself() {
for size in [32768u16, 32769, 40000, u16::MAX] {
let (near, far) = crossed(0, size);
assert_eq!(near, 0, "{size} lost its near edge");
assert!(far >= near, "{size} crossed backwards: {near}..{far}");
}
let wall = map(&[".#"]);
let (moved, contacts) =
sized(0.0, 0.0, 40000, 8).resolve(Velocity::new(1.0, 0.0), wall, &alone());
assert_eq!(moved, Velocity::default(), "it walked through the wall");
assert!(contacts.right());
}
#[test]
fn a_box_over_the_origin_is_sampled_at_the_pixel_it_draws_on() {
let wall = map(&["..#"]);
let body = Body::new(-0.5, 0.0);
let collider = Collider::new(
&body,
Bounds::of(&body, 9, 8),
WALL.into(),
BitFlags::all(),
BitFlags::empty(),
true,
);
let (moved, contacts) = collider
.unwrap()
.resolve(Velocity::default(), wall, &alone());
assert_eq!(moved, Velocity::default());
assert_eq!(
contacts,
Contacts::empty(),
"stopped by a wall two tiles off"
);
}
#[test]
fn a_rectangle_is_stopped_where_the_entity_put_it() {
let wall = map(&[".#"]);
let body = Body::new(0.0, 0.0);
let inset = Bounds::new(body.draw_x() + 2, body.draw_y(), 4, 8);
let collider = Collider::new(
&body,
inset,
WALL.into(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
let (moved, contacts) = collider.resolve(Velocity::new(2.0, 0.0), wall, &alone());
assert_eq!(
moved,
Velocity::new(2.0, 0.0),
"the inset box was stopped early"
);
assert_eq!(contacts, Contacts::empty());
let collider = Collider::new(
&body,
inset,
WALL.into(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
let (moved, contacts) = collider.resolve(Velocity::new(4.0, 0.0), map(&[".#"]), &alone());
assert_eq!(moved, Velocity::default());
assert!(contacts.right());
let whole = Bounds::of(&body, 8, 8);
let collider = Collider::new(
&body,
whole,
WALL.into(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
let (moved, contacts) = collider.resolve(Velocity::new(2.0, 0.0), map(&[".#"]), &alone());
assert_eq!(moved, Velocity::default());
assert!(contacts.right());
}
#[test]
fn each_side_is_reported_from_the_way_it_was_moving() {
let room = map(&["###", "#.#", "###"]);
for (velocity, expected) in [
(Velocity::new(0.0, 2.0), Contact::Below),
(Velocity::new(0.0, -2.0), Contact::Above),
(Velocity::new(-2.0, 0.0), Contact::Left),
(Velocity::new(2.0, 0.0), Contact::Right),
] {
let (moved, contacts) =
hitbox(8.0, 8.0).resolve(velocity, map(&["###", "#.#", "###"]), &alone());
assert_eq!(moved, Velocity::default(), "it left the room");
assert_eq!(
contacts.sides,
expected.into(),
"{velocity:?} touched the wrong side"
);
assert!(contacts.touches(WALL));
}
let (moved, contacts) = hitbox(8.0, 8.0).resolve(Velocity::new(-2.0, 2.0), room, &alone());
assert_eq!(moved, Velocity::default());
assert!(contacts.left() && contacts.below());
}
#[test]
fn a_blocked_axis_leaves_the_other_one_moving() {
let wall = map(&["#..", "#..", "#.."]);
let (moved, contacts) = hitbox(8.0, 0.0).resolve(Velocity::new(-2.0, 1.0), wall, &alone());
assert_eq!(moved, Velocity::new(0.0, 1.0));
assert!(contacts.left() && !contacts.below());
}
#[test]
fn a_wall_hit_sideways_is_not_mistaken_for_a_floor() {
let wall = map(&["...", ".#.", "..."]);
let (moved, contacts) = hitbox(0.0, 8.0).resolve(Velocity::new(8.0, 1.0), wall, &alone());
assert_eq!(moved, Velocity::new(0.0, 1.0));
assert!(contacts.right());
assert!(!contacts.below(), "it landed on a wall");
}
#[test]
fn a_standstill_against_a_wall_reports_nothing() {
let room = map(&["###", "#.#", "###"]);
let (moved, contacts) = hitbox(8.0, 8.0).resolve(Velocity::default(), room, &alone());
assert_eq!(moved, Velocity::default());
assert_eq!(contacts, Contacts::empty());
}
#[test]
fn a_wide_box_cannot_straddle_a_tile() {
let spike = map(&["....", "....", ".#.."]);
let (moved, contacts) =
sized(0.0, 8.0, 24, 8).resolve(Velocity::new(0.0, 1.0), spike, &alone());
assert_eq!(moved, Velocity::default());
assert!(contacts.below());
let (moved, _) = sized(0.0, 8.0, 24, 8).resolve(
Velocity::new(0.0, 1.0),
map(&["....", "....", "...."]),
&alone(),
);
assert_eq!(moved, Velocity::new(0.0, 1.0));
}
#[test]
fn a_box_smaller_than_a_tile_samples_its_own_corners() {
let wall = map(&[".#"]);
let (moved, contacts) =
sized(7.0, 0.0, 1, 1).resolve(Velocity::new(1.0, 0.0), wall, &alone());
assert_eq!(moved, Velocity::default());
assert!(contacts.right());
let (moved, _) =
sized(0.0, 0.0, 1, 1).resolve(Velocity::new(1.0, 0.0), map(&[".#"]), &alone());
assert_eq!(moved, Velocity::new(1.0, 0.0));
}
#[test]
fn a_side_crosses_every_tile_it_covers_and_no_others() {
for start in [-100i16, -9, -8, -1, 0, 1, 7, 100] {
for size in [1u16, 2, 7, 8, 9, 16, 17, 24, 40] {
let (near, far) = crossed(start, size);
let last = start as i32 + size as i32 - 1;
assert_eq!(
near,
(start as i32).div_euclid(8),
"{start}+{size} missed its near tile"
);
assert_eq!(
far,
last.div_euclid(8),
"{start}+{size} missed its far tile"
);
for pixel in start as i32..=last {
let tile = pixel.div_euclid(8);
assert!((near..=far).contains(&tile), "{pixel} was left out");
}
for tile in near..=far {
assert!(
(start as i32..=last).any(|pixel| pixel.div_euclid(8) == tile),
"{start}+{size} asked about tile {tile}, which it is not on"
);
}
}
}
}
#[test]
fn a_side_crosses_the_tiles_a_pixel_walk_of_it_lands_in() {
for start in [i16::MIN, -300, -8, -1, 0, 1, 120, i16::MAX - 40] {
for size in [1u16, 2, 8, 9, 16, 24, 40, 128, 32768, u16::MAX] {
let (near, far) = crossed(start, size);
let last = (start as i32 + size as i32 - 1).min(i16::MAX as i32);
let sampled: Vec<i32> = (start as i32..=last)
.step_by(TILE as usize)
.chain(iter::once(last))
.map(|pixel| pixel.div_euclid(TILE as i32))
.collect();
assert_eq!(
(near, far),
(
*sampled.iter().min().unwrap(),
*sampled.iter().max().unwrap()
),
"{start}+{size}"
);
for tile in near..=far {
assert!(sampled.contains(&tile), "{start}+{size} grew tile {tile}");
}
}
}
}
#[test]
fn a_tile_stops_an_entity_by_any_flag_they_share() {
let body = Body::new(0.0, 0.0);
let bounds = Bounds::of(&body, 8, 8);
let solid = SpriteFlag::Flag0 | SpriteFlag::Flag1;
let walls = Collider::new(
&body,
bounds,
solid,
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
assert!(walls.stops_at(SpriteFlag::Flag1.into()));
assert!(walls.stops_at(SpriteFlag::Flag1 | SpriteFlag::Flag7));
assert!(!walls.stops_at(SpriteFlag::Flag7.into()));
assert!(!walls.stops_at(BitFlags::empty()));
}
#[test]
fn a_tile_that_is_no_wall_is_passed_through_and_still_reported() {
let pool = map(&["~~~~", "~~~~"]);
let velocity = Velocity::new(1.0, 1.0);
let (moved, contacts) = hitbox(0.0, 0.0).resolve(velocity, pool, &alone());
assert_eq!(moved, velocity);
assert_eq!(contacts.sides, BitFlags::empty());
assert!(contacts.touches(WATER));
assert!(!contacts.touches(WALL));
}
#[test]
fn an_entity_stopped_by_nothing_is_still_told_what_it_walked_through() {
let pool = map(&["~~~~", "~~~~"]);
let body = Body::new(0.0, 0.0);
let bounds = Bounds::of(&body, 8, 8);
let collider = Collider::new(
&body,
bounds,
BitFlags::empty(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
let hazard = [carrying(4, 0, SPIKES.into())];
let velocity = Velocity::new(1.0, 1.0);
let (moved, contacts) = collider.resolve(velocity, pool, &cast(&hazard));
assert_eq!(moved, velocity, "something stopped a sensor");
assert_eq!(contacts.sides, BitFlags::empty());
assert!(contacts.touches(WATER) && contacts.touches(SPIKES));
let collider = Collider::new(
&body,
bounds,
BitFlags::empty(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
let walls = [wall(4, 0, 8, 8)];
let (moved, contacts) = collider.resolve(velocity, air, &cast(&walls));
assert_eq!(moved, velocity, "a sensor was walled in");
assert_eq!(contacts.sides, BitFlags::empty());
assert!(contacts.touches(WALL));
}
#[test]
fn a_sensor_is_never_pushed_out_of_anything() {
let body = Body::new(0.0, 0.0);
let bounds = Bounds::of(&body, 8, 8);
let mut collider = Collider::new(
&body,
bounds,
BitFlags::empty(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
let walls = [wall(4, 0, 8, 8)];
let (push, pushed) = collider.expel(&cast(&walls));
assert_eq!(push, (0.0, 0.0));
assert_eq!(pushed, Contacts::empty());
let (_, contacts) = collider.resolve(Velocity::new(1.0, 0.0), air, &cast(&walls));
assert!(contacts.touches(WALL));
}
#[test]
fn a_neighbour_stops_a_fall_like_a_floor_tile() {
let floor = [wall(0, 16, 8, 8)];
let (moved, contacts) =
hitbox(0.0, 7.0).resolve(Velocity::new(0.0, 4.0), air, &cast(&floor));
assert_eq!(moved, Velocity::default(), "it fell through the neighbour");
assert!(contacts.below());
assert!(!contacts.above() && !contacts.left() && !contacts.right());
assert!(contacts.touches(WALL));
}
#[test]
fn each_side_is_reported_off_a_neighbour_as_off_a_tile() {
let room = [
wall(0, 0, 24, 8),
wall(0, 16, 24, 8),
wall(0, 8, 8, 8),
wall(16, 8, 8, 8),
];
for (velocity, expected) in [
(Velocity::new(0.0, 2.0), Contact::Below),
(Velocity::new(0.0, -2.0), Contact::Above),
(Velocity::new(-2.0, 0.0), Contact::Left),
(Velocity::new(2.0, 0.0), Contact::Right),
] {
let (moved, contacts) = hitbox(8.0, 8.0).resolve(velocity, air, &cast(&room));
assert_eq!(moved, Velocity::default(), "it left the room");
assert_eq!(
contacts.sides,
expected.into(),
"{velocity:?} touched the wrong side"
);
assert!(contacts.touches(WALL));
}
}
#[test]
fn a_blocked_axis_slides_along_a_neighbour() {
let side = [wall(0, 0, 8, 24)];
let (moved, contacts) =
hitbox(8.0, 0.0).resolve(Velocity::new(-2.0, 1.0), air, &cast(&side));
assert_eq!(moved, Velocity::new(0.0, 1.0));
assert!(contacts.left() && !contacts.below());
}
#[test]
fn a_standstill_against_a_neighbour_reports_nothing() {
let floor = [wall(0, 8, 8, 8)];
let (moved, contacts) = hitbox(0.0, 0.0).resolve(Velocity::default(), air, &cast(&floor));
assert_eq!(moved, Velocity::default());
assert_eq!(contacts, Contacts::empty());
}
#[test]
fn the_sub_pixel_position_is_kept_against_a_neighbour_too() {
let side = [wall(8, 0, 8, 8)];
let body = Body::new(0.5, 0.0);
let collider = Collider::new(
&body,
Bounds::of(&body, 8, 8),
WALL.into(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
let (moved, contacts) = collider.resolve(Velocity::new(0.5, 0.0), air, &cast(&side));
assert_eq!(moved, Velocity::default());
assert!(contacts.right());
}
#[test]
fn a_neighbour_sharing_no_flag_is_swum_through_and_still_reported() {
let pond = [(Bounds::new(0, 0, 24, 24), WATER.into(), BitFlags::empty())];
let mut collider = hitbox(8.0, 8.0);
let (push, pushed) = collider.expel(&cast(&pond));
assert_eq!(push, (0.0, 0.0), "the water shoved the swimmer out");
assert_eq!(pushed, Contacts::empty());
let velocity = Velocity::new(2.0, 0.0);
let (moved, contacts) = collider.resolve(velocity, air, &cast(&pond));
assert_eq!(moved, velocity);
assert_eq!(contacts.sides, BitFlags::empty());
assert!(contacts.touches(WATER));
}
#[test]
fn a_neighbour_standing_on_the_entity_pushes_it_back_out() {
let lift = [wall(0, 31, 24, 8)];
let mut collider = hitbox(0.0, 24.0);
let (push, pushed) = collider.expel(&cast(&lift));
assert_eq!(push, (0.0, -1.0));
assert!(pushed.below() && !pushed.above());
assert!(pushed.touches(WALL));
let crated = [wall(7, 24, 8, 8)];
let mut collider = hitbox(0.0, 24.0);
let (push, pushed) = collider.expel(&cast(&crated));
assert_eq!(push, (-1.0, 0.0));
assert!(pushed.right() && !pushed.left());
}
#[test]
fn an_entity_the_push_could_not_free_is_still_free_to_move() {
let jaws = [wall(0, 0, 12, 24), wall(14, 0, 12, 24)];
let mut collider = hitbox(8.0, 8.0);
let (push, pushed) = collider.expel(&cast(&jaws));
assert_ne!(push, (0.0, 0.0), "there was room to fit after all");
assert!(pushed.left() && pushed.right());
let velocity = Velocity::new(2.0, 0.0);
let (moved, contacts) = collider.resolve(velocity, air, &cast(&jaws));
assert_eq!(moved, velocity, "it was wedged in for good");
assert!(contacts.touches(WALL));
let (moved, contacts) = collider.resolve(Velocity::new(-2.0, 0.0), air, &cast(&jaws));
assert_eq!(moved, Velocity::default());
assert!(contacts.left());
}
#[test]
fn a_shove_into_something_it_only_shared_an_edge_with_is_answered_for() {
let adjacent = [wall(7, 0, 8, 8), wall(-8, 0, 8, 8)];
let mut collider = hitbox(0.0, 0.0);
let (push, pushed) = collider.expel(&cast(&adjacent));
assert_eq!(push, (0.0, 0.0), "there was room to fit after all");
assert!(pushed.left() && pushed.right());
assert!(pushed.touches(WALL));
let (moved, contacts) = collider.resolve(Velocity::new(-2.0, 0.0), air, &cast(&adjacent));
assert_eq!(
moved,
Velocity::default(),
"it walked out through the left-hand solid"
);
assert!(contacts.left());
let velocity = Velocity::new(2.0, 0.0);
let (moved, _) = collider.resolve(velocity, air, &cast(&adjacent));
assert_eq!(velocity, moved, "it was wedged in for good");
}
#[test]
fn the_shoving_ends_even_where_no_amount_of_it_would_free_the_box() {
let cage = [
wall(-8, -8, 8, 24),
wall(7, -8, 8, 24),
wall(-8, -8, 24, 8),
wall(-8, 7, 24, 8),
];
let mut collider = hitbox(0.0, 0.0);
let (push, pushed) = collider.expel(&cast(&cage));
assert!(
push.0.abs() <= 1.0 && push.1.abs() <= 1.0,
"the shoving ran on: {push:?}"
);
assert_eq!(
pushed.sides(),
Contact::Left | Contact::Right | Contact::Above | Contact::Below
);
let velocity = Velocity::new(2.0, 2.0);
let (moved, contacts) = collider.resolve(velocity, air, &cast(&cage));
assert_eq!(moved, velocity, "the cage kept it after all");
assert!(contacts.touches(WALL));
}
#[test]
fn the_map_and_the_cast_stop_the_same_step() {
let tiles = map(&["#..", "#..", "#.."]);
let body = Body::new(8.0, 0.0);
let floor = [wall(0, 8, 24, 8)];
let collider = Collider::new(
&body,
Bounds::of(&body, 8, 8),
WALL.into(),
BitFlags::all(),
BitFlags::empty(),
true,
)
.unwrap();
let (moved, contacts) = collider.resolve(Velocity::new(-2.0, 1.0), tiles, &cast(&floor));
assert_eq!(moved, Velocity::default());
assert!(contacts.left() && contacts.below());
assert!(contacts.touches(WALL));
}
#[test]
fn only_the_neighbours_over_the_box_reach_the_answer() {
let elsewhere = [wall(100, 100, 8, 8), carrying(4, 0, WATER.into())];
let (moved, contacts) =
hitbox(0.0, 0.0).resolve(Velocity::new(1.0, 0.0), air, &cast(&elsewhere));
assert_eq!(moved, Velocity::new(1.0, 0.0));
assert!(contacts.touches(WATER) && !contacts.touches(WALL));
}
#[test]
fn a_narrow_neighbour_crossed_between_the_endpoints_is_still_reported() {
let stream = [(Bounds::new(9, 0, 2, 8), WATER.into(), BitFlags::empty())];
let velocity = Velocity::new(12.0, 0.0);
let (moved, contacts) = hitbox(0.0, 0.0).resolve(velocity, air, &cast(&stream));
assert_eq!(moved, velocity, "the water stopped it");
assert!(contacts.touches(WATER));
let strip = [(Bounds::new(0, 9, 8, 2), WATER.into(), BitFlags::empty())];
let velocity = Velocity::new(0.0, 12.0);
let (moved, contacts) = hitbox(0.0, 0.0).resolve(velocity, air, &cast(&strip));
assert_eq!(moved, velocity);
assert!(contacts.touches(WATER));
}
#[test]
fn a_tile_column_stepped_clean_over_is_still_reported() {
let stream = map(&[".~.."]);
let velocity = Velocity::new(16.0, 0.0);
let (moved, contacts) = hitbox(0.0, 0.0).resolve(velocity, stream, &alone());
assert_eq!(moved, velocity, "the water stopped it");
assert!(contacts.touches(WATER));
}
#[test]
fn the_ground_a_step_began_on_is_reported_even_when_it_leaves() {
let pond = [carrying(0, 0, WATER.into())];
let velocity = Velocity::new(12.0, 0.0);
let (moved, contacts) = hitbox(0.0, 0.0).resolve(velocity, air, &cast(&pond));
assert_eq!(moved, velocity);
assert!(contacts.touches(WATER));
let pool = map(&["~..."]);
let (moved, contacts) = hitbox(0.0, 0.0).resolve(velocity, pool, &alone());
assert_eq!(moved, velocity);
assert!(contacts.touches(WATER));
}
#[test]
fn a_wall_thin_enough_to_be_stepped_over_is_reported_and_not_stopped_at() {
let paling = [(Bounds::new(9, 0, 2, 8), WALL.into(), BitFlags::empty())];
let velocity = Velocity::new(12.0, 0.0);
let (moved, contacts) = hitbox(0.0, 0.0).resolve(velocity, air, &cast(&paling));
assert_eq!(moved, velocity);
assert_eq!(
contacts.sides(),
BitFlags::empty(),
"it was stopped after all"
);
assert!(contacts.touches(WALL));
let velocity = Velocity::new(2.0, 0.0);
let (moved, contacts) = hitbox(0.0, 0.0).resolve(velocity, air, &cast(&paling));
assert_eq!(moved, Velocity::default());
assert!(contacts.right());
}
const WALL: SpriteFlag = SpriteFlag::Flag0;
const WATER: SpriteFlag = SpriteFlag::Flag1;
const SPIKES: SpriteFlag = SpriteFlag::Flag2;
}