use super::{
collider::{far, Cast, Collider, Neighbour},
wire, Bounds, Contact, Contacts, Force, Kinetic, Member, MemberId, MemberMut, Subject,
Velocity,
};
use crate::{BitFlags, Context, Graphics, SpriteFlag, SpriteId};
pub struct World<const N: usize, F: Force = ()> {
reads_map: bool,
pub(super) solid: BitFlags<SpriteFlag>,
forces: F,
pub(super) records: [wire::Record; N],
pub(super) masses: [f32; N],
pub(super) offsets: [(i16, i16); N],
pub(super) generations: [u8; N],
seated: u64,
pub(super) own_solid: u64,
}
impl<const N: usize> World<N> {
pub const fn new() -> Self {
const {
assert!(
N <= wire::CAP,
"a World cannot have more seats than the sixty-four records the wire carries"
)
};
Self {
reads_map: true,
solid: BitFlags::empty(),
forces: (),
records: [wire::VACANT; N],
masses: [1.0; N],
offsets: [(0, 0); N],
generations: [0; N],
seated: 0,
own_solid: 0,
}
}
pub const fn mapless() -> Self {
Self {
reads_map: false,
..Self::new()
}
}
pub const fn with_forces<G: Force>(self, forces: G) -> World<N, G> {
World {
reads_map: self.reads_map,
solid: self.solid,
forces,
records: self.records,
masses: self.masses,
offsets: self.offsets,
generations: self.generations,
seated: self.seated,
own_solid: self.own_solid,
}
}
}
impl<const N: usize, F: Force> World<N, F> {
pub fn forces(&self) -> &F {
&self.forces
}
pub fn forces_mut(&mut self) -> &mut F {
&mut self.forces
}
pub fn with_solid(mut self, solid: impl Into<BitFlags<SpriteFlag>>) -> Self {
self.declare_solid(solid);
self
}
pub fn declare_solid(&mut self, solid: impl Into<BitFlags<SpriteFlag>>) {
self.solid = solid.into();
let word = self.solid.bits();
let mut theirs = self.seated & !self.own_solid;
while theirs != 0 {
let slot = theirs.trailing_zeros() as usize;
theirs &= theirs - 1;
self.records[slot].solid = word;
}
}
pub(super) fn claim(
&mut self,
mut record: wire::Record,
solid: Option<BitFlags<SpriteFlag>>,
mass: f32,
offset: (i16, i16),
) -> Option<MemberId> {
let slot = (!self.seated).trailing_zeros() as usize;
if slot >= N {
return None;
}
self.seated |= 1 << slot;
self.generations[slot] = self.generations[slot].wrapping_add(1);
let word = solid.unwrap_or(self.solid);
record.solid = word.bits();
self.records[slot] = record;
self.masses[slot] = mass;
self.offsets[slot] = offset;
if solid.is_some() {
self.own_solid |= 1 << slot;
} else {
self.own_solid &= !(1 << slot);
}
Some(MemberId {
slot: slot as u8,
generation: self.generations[slot],
})
}
pub fn step(&mut self, ctx: &Context) {
self.weather();
#[cfg(target_arch = "wasm32")]
{
let _ = ctx;
self.step_over_the_wire();
}
#[cfg(not(target_arch = "wasm32"))]
self.step_natively(ctx);
}
pub fn draw(&self, gfx: &mut Graphics, layers: impl Into<BitFlags<SpriteFlag>>) {
let layers = layers.into();
let taken = self.high_water();
#[cfg(target_arch = "wasm32")]
{
let _ = gfx;
unsafe {
crate::ffi::draw_cast(
self.records.as_ptr().cast(),
taken as u32,
u32::from(layers.bits()),
);
}
}
#[cfg(not(target_arch = "wasm32"))]
{
let carried = |sprite: SpriteId| {
let bits = unsafe { crate::ffi::sprite_flags(u32::from(sprite.0)) };
BitFlags::from_bits(bits as u8).expect(
"sprite_flags returned an unknown sprite-flag bit \
(pixel8 host/SDK ABI mismatch)",
)
};
for look in wire::looks(&self.records[..taken], layers, carried) {
gfx.sprite_ext(
look.sprite,
look.x,
look.y,
look.width,
look.height,
look.flip_x,
look.flip_y,
)
.expect("a look is at least one cell each way");
}
}
}
#[inline]
pub fn member(&self, id: MemberId) -> Member<'_> {
let slot = self.seat(id);
Member {
id,
record: &self.records[slot],
mass: self.masses[slot],
own_solid: self.own_solid & (1 << slot) != 0,
}
}
#[inline]
pub fn member_mut(&mut self, id: MemberId) -> MemberMut<'_> {
let slot = self.seat(id);
let scene_solid = self.solid;
MemberMut {
id,
record: &mut self.records[slot],
mass: &mut self.masses[slot],
offset: &mut self.offsets[slot],
seated: &mut self.seated,
own_solid: &mut self.own_solid,
generation: &mut self.generations[slot],
scene_solid,
}
}
#[inline]
pub fn get_member(&self, id: MemberId) -> Option<Member<'_>> {
self.holds(id).then(|| self.member(id))
}
#[inline]
pub fn get_member_mut(&mut self, id: MemberId) -> Option<MemberMut<'_>> {
self.holds(id).then(|| self.member_mut(id))
}
fn weather(&mut self) {
let mut seated = self.seated;
while seated != 0 {
let slot = seated.trailing_zeros() as usize;
seated &= seated - 1;
let record = &mut self.records[slot];
if record.meta & wire::PROP != 0 {
continue;
}
let mut subject = Subject::new(
Velocity::new(record.dx, record.dy),
self.masses[slot],
(record.x, record.y),
);
self.forces.apply(&mut subject);
let bent = subject.velocity();
(record.dx, record.dy) = (bent.dx, bent.dy);
}
}
#[cfg(target_arch = "wasm32")]
fn step_over_the_wire(&mut self) {
let taken = self.high_water();
unsafe {
crate::ffi::step_cast(
self.records.as_mut_ptr().cast(),
taken as u32,
self.reads_map as u32,
);
}
self.reseat();
}
#[cfg(not(target_arch = "wasm32"))]
fn step_natively(&mut self, ctx: &Context) {
let taken = self.high_water();
let mut cast: [wire::Recast; N] =
core::array::from_fn(|slot| wire::Recast::of(&self.records[slot]));
{
let mut members = cast.each_mut().map(|member| member as &mut dyn Kinetic);
self.step_cast(
&mut members[..taken],
|x, y| {
ctx.map_tile(x, y)
.map_or(BitFlags::empty(), |tile| ctx.sprite_flags(tile))
},
|sprite| ctx.sprite_flags(sprite),
);
}
for (member, record) in cast.iter().zip(self.records.iter_mut()).take(taken) {
member.report(record);
}
self.reseat();
}
#[doc(hidden)]
pub fn step_hosted(
&self,
cast: &mut [&mut dyn Kinetic],
tiles: impl Fn(i16, i16) -> BitFlags<SpriteFlag> + Copy,
carried: impl Fn(SpriteId) -> BitFlags<SpriteFlag>,
) {
self.step_cast(cast, tiles, carried);
}
fn reseat(&mut self) {
let mut seated = self.seated;
while seated != 0 {
let slot = seated.trailing_zeros() as usize;
seated &= seated - 1;
let offset = self.offsets[slot];
let record = &mut self.records[slot];
(record.bx, record.by) = corner((record.rx, record.ry), offset);
}
}
fn high_water(&self) -> usize {
(u64::BITS - self.seated.leading_zeros()) as usize
}
#[inline(always)]
pub(super) fn seat(&self, id: MemberId) -> usize {
if !self.holds(id) {
stale(id.slot, N);
}
id.slot as usize
}
#[inline(always)]
pub(super) fn holds(&self, id: MemberId) -> bool {
let slot = id.slot as usize;
slot < N && self.generations[slot] == id.generation
}
fn step_cast(
&self,
cast: &mut [&mut dyn Kinetic],
tiles: impl Fn(i16, i16) -> BitFlags<SpriteFlag> + Copy,
carried: impl Fn(SpriteId) -> BitFlags<SpriteFlag>,
) {
assert!(
cast.len() <= wire::CAP,
"a cast of {} was handed across a wire with a ceiling of {}",
cast.len(),
wire::CAP
);
let mut boxes = [EMPTY; SNAPSHOT];
let mut carries = [BitFlags::empty(); SNAPSHOT];
let mut wants = [BitFlags::empty(); SNAPSHOT];
let members = cast.len();
let fits = members <= SNAPSHOT;
let mut worn = BitFlags::empty();
if fits {
for (((rectangle, flagged), listening), member) in boxes
.iter_mut()
.zip(carries.iter_mut())
.zip(wants.iter_mut())
.zip(cast.iter())
{
if !member.prop() {
*listening = member.heeds() | member.solid().unwrap_or(self.solid);
}
let flags = member.sprite().map_or(BitFlags::empty(), &carried);
if !flags.is_empty() || !listening.is_empty() {
*rectangle = member.bounds();
*flagged = flags;
worn = worn | flags;
}
}
} else {
for member in cast.iter() {
if let Some(sprite) = member.sprite() {
worn = worn | carried(sprite);
}
}
}
let mut arrived = [BitFlags::<SpriteFlag>::empty(); wire::CAP];
for index in 0..cast.len() {
let (before, rest) = cast.split_at_mut(index);
let Some((entity, after)) = rest.split_first_mut() else {
break;
};
if entity.prop() {
continue;
}
let mine = if fits {
carries[index]
} else {
entity.sprite().map_or(BitFlags::empty(), &carried)
};
let neighbours = Neighbours {
taken: fits.then(|| (&boxes[..members], &carries[..members], &wants[..members])),
worn,
mine: index,
before,
after,
carried: &carried,
solid: self.solid,
met: core::cell::Cell::new(0),
};
step_entity(
&mut **entity,
tiles,
self.reads_map,
self.solid,
mine,
&neighbours,
);
let mut met = neighbours.met.get();
while met != 0 {
let slot = met.trailing_zeros() as usize;
met &= met - 1;
arrived[slot] = arrived[slot] | mine;
}
if fits && (!carries[index].is_empty() || !wants[index].is_empty()) {
boxes[index] = entity.bounds();
}
}
for (index, entity) in cast.iter_mut().enumerate() {
let news = arrived[index];
if news.is_empty() {
continue;
}
let listening = if fits {
wants[index]
} else if entity.prop() {
BitFlags::empty()
} else {
entity.heeds() | entity.solid().unwrap_or(self.solid)
};
let contacts = entity.contacts_mut();
contacts.touched = contacts.touched | (news & listening);
}
}
}
const SNAPSHOT: usize = 32;
const EMPTY: Bounds = Bounds::new(0, 0, 0, 0);
impl<const N: usize> Default for World<N> {
fn default() -> Self {
Self::new()
}
}
#[inline(always)]
fn step_entity(
entity: &mut dyn Kinetic,
tiles: impl Fn(i16, i16) -> BitFlags<SpriteFlag> + Copy,
reads_map: bool,
solid: BitFlags<SpriteFlag>,
worn: BitFlags<SpriteFlag>,
neighbours: &impl Cast,
) {
let limits = entity.confines();
let mut velocity = *entity.velocity_mut();
let mut contacts = Contacts::empty();
let solid = entity.solid().unwrap_or(solid);
let heeds = entity.heeds();
if let Some(mut collider) = Collider::new(
entity.body(),
entity.bounds(),
solid,
heeds,
worn,
reads_map,
) {
if collider.could_be_inside_something(neighbours) {
let ((dx, dy), pushed) = collider.expel(neighbours);
if (dx, dy) != (0.0, 0.0) {
let (x, y) = entity.body().pos();
entity.body_mut().set_pos(x + dx, y + dy);
}
contacts = pushed;
}
let (survived, resolved) = collider.resolve(velocity, tiles, neighbours);
velocity = survived;
contacts |= resolved;
}
entity.body_mut().move_by(velocity.dx, velocity.dy);
if let Some(limits) = limits {
contacts |= hold(entity, limits, &mut velocity).into();
}
*entity.velocity_mut() = velocity;
*entity.contacts_mut() = contacts;
}
type Snapshot<'a> = (
&'a [Bounds],
&'a [BitFlags<SpriteFlag>],
&'a [BitFlags<SpriteFlag>],
);
struct Neighbours<'a, 'cast, F> {
taken: Option<Snapshot<'a>>,
worn: BitFlags<SpriteFlag>,
mine: usize,
before: &'a [&'cast mut dyn Kinetic],
after: &'a [&'cast mut dyn Kinetic],
carried: F,
solid: BitFlags<SpriteFlag>,
met: core::cell::Cell<u64>,
}
impl<F: Fn(SpriteId) -> BitFlags<SpriteFlag>> Cast for Neighbours<'_, '_, F> {
#[inline(always)]
fn carried(&self) -> BitFlags<SpriteFlag> {
self.worn
}
fn note(&self, index: usize) {
let slot = match self.taken {
Some(_) => index,
None if index < self.mine => index,
None => index + 1,
};
self.met.set(self.met.get() | 1 << slot);
}
#[inline(always)]
fn len(&self) -> usize {
if self.worn.is_empty() {
return 0;
}
match self.taken {
Some((boxes, ..)) => boxes.len(),
None => self.before.len() + self.after.len(),
}
}
#[inline(always)]
fn at(&self, index: usize) -> Option<Neighbour> {
let Some((boxes, carries, wants)) = self.taken else {
return self.asked(index);
};
if index == self.mine {
return None;
}
let flags = *carries.get(index)?;
let listening = *wants.get(index)?;
if flags.is_empty() && listening.is_empty() {
return None;
}
Some((*boxes.get(index)?, flags, listening))
}
}
impl<F: Fn(SpriteId) -> BitFlags<SpriteFlag>> Neighbours<'_, '_, F> {
#[inline(never)]
fn asked(&self, index: usize) -> Option<Neighbour> {
let other = match self.before.get(index) {
Some(other) => other,
None => self.after.get(index - self.before.len())?,
};
let flags = other
.sprite()
.map_or(BitFlags::empty(), |sprite| (self.carried)(sprite));
let listening = if other.prop() {
BitFlags::empty()
} else {
other.heeds() | other.solid().unwrap_or(self.solid)
};
if flags.is_empty() && listening.is_empty() {
return None;
}
Some((other.bounds(), flags, listening))
}
}
#[inline(always)]
fn hold(entity: &mut dyn Kinetic, limits: Bounds, velocity: &mut Velocity) -> BitFlags<Contact> {
let bounds = entity.bounds();
let body = entity.body();
let (x, y) = body.pos();
let (draw_x, draw_y) = body.draw_pos();
let left = x + (bounds.x() - draw_x) as f32;
let top = y + (bounds.y() - draw_y) as f32;
let rightmost =
(far(limits.x(), limits.width()) - bounds.width() as i32).max(limits.x() as i32) as f32;
let lowest =
(far(limits.y(), limits.height()) - bounds.height() as i32).max(limits.y() as i32) as f32;
let (mut dx, mut dy) = (0.0, 0.0);
let mut held = BitFlags::empty();
if left < limits.x() as f32 {
dx = limits.x() as f32 - left;
held = held | Contact::Left;
} else if left > rightmost {
dx = rightmost - left;
held = held | Contact::Right;
}
if top < limits.y() as f32 {
dy = limits.y() as f32 - top;
held = held | Contact::Above;
} else if top > lowest {
dy = lowest - top;
held = held | Contact::Below;
}
if !held.is_empty() {
if (held.contains(Contact::Left) && velocity.dx < 0.0)
|| (held.contains(Contact::Right) && velocity.dx > 0.0)
{
velocity.dx = 0.0;
}
if (held.contains(Contact::Above) && velocity.dy < 0.0)
|| (held.contains(Contact::Below) && velocity.dy > 0.0)
{
velocity.dy = 0.0;
}
}
if (dx, dy) != (0.0, 0.0) {
entity.body_mut().set_pos(x + dx, y + dy);
}
held
}
pub(super) fn corner((rx, ry): (i16, i16), (dx, dy): (i16, i16)) -> (i16, i16) {
fn along(pixel: i16, offset: i16) -> i16 {
(pixel as i32 + offset as i32).clamp(i16::MIN as i32, i16::MAX as i32) as i16
}
(along(rx, dx), along(ry, dy))
}
pub(super) fn mirrored(meta: u8, flip_x: bool, flip_y: bool) -> u8 {
let mut mirrored = meta & !(wire::FLIP_X | wire::FLIP_Y);
if flip_x {
mirrored |= wire::FLIP_X;
}
if flip_y {
mirrored |= wire::FLIP_Y;
}
mirrored
}
pub(super) fn span_byte(width: u8, height: u8) -> u8 {
assert!(
(1..=16).contains(&width) && (1..=16).contains(&height),
"a member is drawn from one to sixteen cells each way, not {width}x{height}"
);
(width - 1) | ((height - 1) << 4)
}
#[cold]
#[inline(never)]
fn stale(slot: u8, seats: usize) -> ! {
assert!(
(slot as usize) < seats,
"seat {slot} is no seat of this world: a member that was never enlisted is being asked about"
);
panic!("seat {slot} was retired: a member that has left the cast is still being asked about")
}
#[cfg(test)]
mod tests {
use super::{
super::{force::Mob, Atmosphere, Gravity, Velocity, Wind},
*,
};
use crate::Body;
const GRAVITY: Gravity = Gravity::new();
const WORLD: World<0> = World::new();
const CTX: Context = Context { _private: () };
fn walled() -> World<0> {
World::new().with_solid(WALL)
}
#[test]
#[should_panic(expected = "ceiling")]
fn a_cast_past_the_wire_s_ceiling_is_refused_loudly() {
let mut things: Vec<Thing> = (0..wire::CAP + 1)
.map(|i| Thing::at(i as f32 * 100.0, 0.0))
.collect();
let mut cast: Vec<&mut dyn Kinetic> = things.iter_mut().map(Thing::as_kinetic).collect();
WORLD.step_cast(&mut cast, air, unflagged);
}
#[test]
fn a_meeting_is_told_to_both_of_its_parties_whoever_made_it() {
let mut stander = Thing::at(0.0, 0.0).wearing(CRATE_SPRITE);
let mut mover = Thing::at(12.0, 0.0).wearing(WALL_SPRITE).moving(-6.0, 0.0);
WORLD.step_cast(
&mut [stander.as_kinetic(), mover.as_kinetic()],
air,
flagged,
);
assert!(
mover.contacts.touches(CRATE),
"the mover's own sweep missed the meeting"
);
assert!(
stander.contacts.touches(WALL),
"the one arrived upon was never told"
);
}
#[test]
fn an_arrival_is_heard_only_by_a_listener_and_only_for_what_it_heeds() {
let mut deaf = Thing::at(0.0, 0.0)
.wearing(CRATE_SPRITE)
.heeding(BitFlags::empty());
let mut mover = Thing::at(12.0, 0.0).wearing(WALL_SPRITE).moving(-6.0, 0.0);
WORLD.step_cast(&mut [deaf.as_kinetic(), mover.as_kinetic()], air, flagged);
assert!(mover.contacts.touches(CRATE));
assert_eq!(deaf.contacts, Contacts::empty(), "the deaf one was told");
let mut stander = Thing::at(0.0, 0.0).wearing(CRATE_SPRITE);
let mut oblivious = Thing::at(12.0, 0.0)
.wearing(WALL_SPRITE)
.heeding(BitFlags::empty())
.moving(-6.0, 0.0);
WORLD.step_cast(
&mut [stander.as_kinetic(), oblivious.as_kinetic()],
air,
flagged,
);
assert_eq!(oblivious.contacts, Contacts::empty());
assert!(
stander.contacts.touches(WALL),
"the mover's own disinterest cost the stander its news"
);
}
#[test]
fn an_arrival_reaches_an_entity_wearing_nothing_at_all() {
let mut bare = Thing::at(0.0, 0.0).heeding(WALL.into());
let mut mover = Thing::at(12.0, 0.0).wearing(WALL_SPRITE).moving(-6.0, 0.0);
WORLD.step_cast(&mut [bare.as_kinetic(), mover.as_kinetic()], air, flagged);
assert!(bare.contacts.touches(WALL));
assert_eq!(
mover.contacts,
Contacts::empty(),
"something wearing nothing was met"
);
}
#[test]
fn an_arrival_on_a_prop_is_nobody_s_news() {
let mut door = Thing::at(0.0, 0.0).wearing(CRATE_SPRITE).parked();
let mut mover = Thing::at(12.0, 0.0)
.wearing(WALL_SPRITE)
.stopped_by(BitFlags::empty())
.moving(-6.0, 0.0);
WORLD.step_cast(&mut [door.as_kinetic(), mover.as_kinetic()], air, flagged);
assert!(mover.contacts.touches(CRATE));
assert_eq!(door.contacts, Contacts::empty());
}
#[test]
fn a_meeting_reaches_both_parties_on_a_cast_too_long_to_snapshot() {
let mut cast: Vec<Thing> = (0..SNAPSHOT + 2)
.map(|i| Thing::at(3000.0 + i as f32 * 100.0, 0.0))
.collect();
cast[0] = Thing::at(12.0, 0.0).wearing(WALL_SPRITE).moving(-6.0, 0.0);
let last = cast.len() - 1;
cast[last] = Thing::at(0.0, 0.0).wearing(CRATE_SPRITE);
let mut handed: Vec<&mut dyn Kinetic> = cast.iter_mut().map(Thing::as_kinetic).collect();
WORLD.step_cast(&mut handed, air, flagged);
assert!(cast[0].contacts.touches(CRATE));
assert!(
cast[last].contacts.touches(WALL),
"the stander after `mine`"
);
let mut cast: Vec<Thing> = (0..SNAPSHOT + 2)
.map(|i| Thing::at(3000.0 + i as f32 * 100.0, 0.0))
.collect();
cast[0] = Thing::at(0.0, 0.0).wearing(CRATE_SPRITE);
let last = cast.len() - 1;
cast[last] = Thing::at(12.0, 0.0).wearing(WALL_SPRITE).moving(-6.0, 0.0);
let mut handed: Vec<&mut dyn Kinetic> = cast.iter_mut().map(Thing::as_kinetic).collect();
WORLD.step_cast(&mut handed, air, flagged);
assert!(cast[last].contacts.touches(CRATE));
assert!(cast[0].contacts.touches(WALL), "the stander before `mine`");
}
#[test]
fn a_cast_at_the_ceiling_of_boxes_over_everything_costs_two_map_s_worths_apiece() {
use core::cell::Cell;
let asked = Cell::new(0u32);
let counted = |_: i16, _: i16| {
asked.set(asked.get() + 1);
BitFlags::empty()
};
let mut things: Vec<Thing> = (0..64)
.map(|_| {
Thing::at(i16::MIN as f32, i16::MIN as f32)
.sized(u16::MAX, u16::MAX)
.moving(1.0, 1.0)
})
.collect();
let mut cast: Vec<&mut dyn Kinetic> = things.iter_mut().map(Thing::as_kinetic).collect();
walled().step_cast(&mut cast, counted, unflagged);
let map = u32::from(crate::MAP_WIDTH_TILES) * u32::from(crate::MAP_HEIGHT_TILES);
assert_eq!(asked.get(), 64 * 2 * map);
}
#[test]
fn a_step_moves_the_body_along_the_velocity() {
let mut mob = Mob::moving(1.5, -2.0);
step(&mut [mob.as_kinetic()]);
assert_eq!(mob.body.pos(), (1.5, -2.0));
assert_eq!(mob.contacts, Contacts::empty());
step(&mut [mob.as_kinetic()]);
assert_eq!(mob.body.pos(), (3.0, -4.0));
}
#[test]
fn the_world_s_forces_are_applied_to_every_member_of_the_cast() {
let mut world: World<2, Gravity> = World::new().with_forces(GRAVITY);
let one = pebble(&mut world, 0.0, 0.0);
let two = pebble(&mut world, 40.0, 0.0);
world.step(&CTX);
for (member, x) in [(one, 0.0), (two, 40.0)] {
assert_eq!(
world.member(member).velocity(),
Velocity::new(0.0, Gravity::DEFAULT_STRENGTH)
);
assert_eq!(world.member(member).pos(), (x, Gravity::DEFAULT_STRENGTH));
}
for _ in 0..1_000 {
world.step(&CTX);
}
assert_eq!(
world.member(one).velocity().dy,
Gravity::DEFAULT_TERMINAL_VELOCITY,
"the pull never settled at its terminal velocity"
);
}
#[test]
fn a_world_owning_no_forces_applies_none() {
let mut world: World<1> = World::new();
let drifter = Member::builder(0.0, 0.0, 8, 8)
.moving(0.0, 1.0)
.enlist(&mut world)
.unwrap();
world.step(&CTX);
assert_eq!(world.member(drifter).velocity(), Velocity::new(0.0, 1.0));
assert_eq!(world.member(drifter).pos(), (0.0, 1.0));
}
#[test]
fn forces_run_in_the_order_the_tuple_composes_them() {
const PULL: Gravity = Gravity::new().with_terminal_velocity(f32::MAX);
const AIR: Atmosphere = Atmosphere::new();
let (pulled, pulled_first) = one_update_under((PULL, AIR));
let (aired, aired_first) = one_update_under((AIR, PULL));
let pulled_dy = pulled.member(pulled_first).velocity().dy;
let aired_dy = aired.member(aired_first).velocity().dy;
assert_eq!(aired_dy, Gravity::DEFAULT_STRENGTH);
assert!(
pulled_dy < aired_dy,
"the order made no difference: {pulled_dy} against {aired_dy}"
);
}
#[test]
fn mass_is_read_off_the_seat_by_the_forces_the_world_runs() {
let mut world: World<2, Wind> = World::new().with_forces(Wind::new(2.0));
let light = Member::builder(0.0, 0.0, 8, 8)
.weighing(0.5)
.enlist(&mut world)
.unwrap();
let heavy = Member::builder(0.0, 40.0, 8, 8)
.weighing(4.0)
.enlist(&mut world)
.unwrap();
world.step(&CTX);
let (light_dx, heavy_dx) = (
world.member(light).velocity().dx,
world.member(heavy).velocity().dx,
);
assert!(
light_dx > heavy_dx,
"the mass was not read: {light_dx} against {heavy_dx}"
);
world.member_mut(heavy).set_mass(0.5);
assert_eq!(world.member(heavy).mass(), 0.5);
}
#[test]
fn forces_of_different_types_compose_into_one_weather() {
struct Updraft;
impl Force for Updraft {
fn apply(&self, subject: &mut Subject) {
subject.velocity_mut().dy -= 0.5;
}
}
let (world, member) = one_update_under((GRAVITY, Wind::new(1.0), Updraft));
assert_eq!(world.member(member).velocity().dx, 0.05);
assert_eq!(
world.member(member).velocity().dy,
Gravity::DEFAULT_STRENGTH - 0.5
);
}
#[test]
fn no_force_reaches_a_prop_the_cart_drives_itself() {
let mut world: World<2, Gravity> = World::new().with_forces(GRAVITY);
let faller = pebble(&mut world, 0.0, 0.0);
let lift = Member::builder(0.0, 40.0, 8, 8)
.prop()
.enlist(&mut world)
.unwrap();
for _ in 0..8 {
world.step(&CTX);
}
assert!(
world.member(faller).pos().1 > 0.0,
"the pull missed the faller"
);
assert_eq!(world.member(lift).pos(), (0.0, 40.0), "the prop was moved");
assert_eq!(world.member(lift).velocity(), Velocity::default());
}
fn one_update_under<F: Force>(forces: F) -> (World<1, F>, MemberId) {
let mut world: World<1, F> = World::new().with_forces(forces);
let member = pebble(&mut world, 0.0, 0.0);
world.step(&CTX);
(world, member)
}
fn pebble<const N: usize, F: Force>(world: &mut World<N, F>, x: f32, y: f32) -> MemberId {
Member::builder(x, y, 8, 8).enlist(world).unwrap()
}
#[test]
fn a_member_takes_the_lowest_empty_seat_and_a_full_house_turns_one_away() {
let mut world: World<3> = World::new();
let seats: Vec<MemberId> = (0..3)
.map(|i| pebble(&mut world, i as f32 * 16.0, 0.0))
.collect();
assert_eq!(
seats.iter().map(|m| m.seat()).collect::<Vec<_>>(),
[0, 1, 2],
"the seats were not filled in order"
);
assert!(
Member::builder(0.0, 0.0, 8, 8).enlist(&mut world).is_none(),
"a fourth member was seated in a world of three"
);
world.member_mut(seats[1]).retire();
let understudy = pebble(&mut world, 64.0, 0.0);
assert_eq!(understudy.seat(), 1);
assert_eq!(world.member(understudy).pos(), (64.0, 0.0));
}
#[test]
fn a_member_described_and_never_enlisted_takes_no_seat() {
let mut world: World<1> = World::new();
let _ = Member::builder(10.0, 10.0, 8, 8)
.wearing(CRATE_SPRITE)
.flipped(true, true)
.spanning(2, 2)
.hidden();
let member = Member::builder(0.0, 0.0, 8, 8)
.enlist(&mut world)
.expect("the seat was leaked");
assert_eq!(member.seat(), 0);
assert!(world.get_member(member).is_some());
assert_eq!(world.member(member).pos(), (0.0, 0.0));
assert_eq!(world.member(member).sprite(), None);
assert_eq!(world.member(member).flip(), (false, false));
assert_eq!(world.member(member).span(), (1, 1));
assert!(!world.member(member).hidden());
}
#[test]
fn one_description_seats_as_many_members_as_it_is_enlisted() {
let mut world: World<2> = World::new();
let description = Member::builder(4.0, 8.0, 8, 8)
.moving(1.0, -2.0)
.wearing(CRATE_SPRITE)
.flipped(true, false);
let first = description
.enlist(&mut world)
.expect("a seat for the first");
let second = description
.enlist(&mut world)
.expect("a seat for the second");
assert_eq!((first.seat(), second.seat()), (0, 1));
for member in [first, second] {
assert_eq!(world.member(member).pos(), (4.0, 8.0));
assert_eq!(world.member(member).velocity(), Velocity::new(1.0, -2.0));
assert_eq!(world.member(member).sprite(), Some(CRATE_SPRITE));
assert_eq!(world.member(member).flip(), (true, false));
}
}
#[test]
fn a_bare_description_is_a_whole_member_already() {
let mut world: World<1> = World::new();
let bare = Member::builder(4.5, -2.25, 8, 8)
.enlist(&mut world)
.unwrap();
let record = &world.records[bare.seat()];
assert_eq!((record.x, record.y), (4.5, -2.25));
assert_eq!((record.rx, record.ry), (4, -3));
assert_eq!((record.bx, record.by, record.bw, record.bh), (4, -3, 8, 8));
assert_eq!((record.dx, record.dy), (0.0, 0.0));
assert_eq!(record.sprite, wire::UNWORN);
assert_eq!(record.heeds, BitFlags::<SpriteFlag>::all().bits());
assert_eq!(record.meta, 0);
assert_eq!(world.member(bare).mass(), 1.0);
assert_eq!(world.offsets[bare.seat()], (0, 0));
}
#[test]
fn the_scene_s_word_for_wall_is_a_member_s_unless_it_has_one_of_its_own() {
let mut world: World<3> = World::new().with_solid(WALL);
let plain = pebble(&mut world, 0.0, 0.0);
assert_eq!(
world.records[plain.seat()].solid,
BitFlags::from(WALL).bits()
);
assert_eq!(world.member(plain).solid(), None);
let own = Member::builder(0.0, 40.0, 8, 8)
.stopped_by(CRATE)
.enlist(&mut world)
.unwrap();
assert_eq!(
world.records[own.seat()].solid,
BitFlags::from(CRATE).bits()
);
let ghost = Member::builder(0.0, 80.0, 8, 8)
.stopped_by(BitFlags::empty())
.enlist(&mut world)
.unwrap();
assert_eq!(world.records[ghost.seat()].solid, 0);
assert_eq!(world.member(ghost).solid(), Some(BitFlags::empty()));
}
#[test]
fn everything_a_description_says_reaches_the_seat_it_fills() {
const ROOM: Bounds = Bounds::new(-8, 4, 100, 64);
let mut world: World<1> = World::new();
let member = Member::builder(10.0, 20.0, 6, 4)
.moving(1.5, -0.5)
.wearing(SpriteId(9))
.heeding(CRATE)
.confined_to(ROOM)
.offset(1, 2)
.prop()
.weighing(3.0)
.enlist(&mut world)
.unwrap();
let record = &world.records[member.seat()];
assert_eq!((record.dx, record.dy), (1.5, -0.5));
assert_eq!(record.sprite, 9);
assert_eq!(record.heeds, BitFlags::from(CRATE).bits());
assert_eq!(
(record.cx, record.cy, record.cw, record.ch),
(-8, 4, 100, 64)
);
assert_eq!(record.meta, wire::PROP | wire::CONFINED);
assert_eq!((record.bx, record.by, record.bw, record.bh), (11, 22, 6, 4));
assert_eq!(world.member(member).mass(), 3.0);
}
#[test]
fn a_rectangle_at_the_end_of_the_coordinate_space_is_held_rather_than_wrapped() {
assert_eq!(corner((i16::MAX, i16::MIN), (8, -8)), (i16::MAX, i16::MIN));
assert_eq!(corner((0, 0), (-4, 6)), (-4, 6));
}
#[test]
#[should_panic(expected = "seat 0 was retired")]
fn a_retired_member_s_handle_is_nobody() {
let mut world: World<2> = World::new();
let member = pebble(&mut world, 0.0, 0.0);
assert!(world.get_member(member).is_some());
world.member_mut(member).retire();
assert!(world.get_member(member).is_none());
world.member(member);
}
#[test]
#[should_panic(expected = "seat 1 was retired")]
fn a_seat_let_again_does_not_answer_the_last_member_s_handle() {
let mut world: World<2> = World::new();
let _ = pebble(&mut world, 0.0, 0.0);
let gone = pebble(&mut world, 16.0, 0.0);
world.member_mut(gone).retire();
let successor = pebble(&mut world, 32.0, 0.0);
assert_eq!(successor.seat(), gone.seat());
assert_ne!(successor, gone);
assert_eq!(world.member(successor).pos(), (32.0, 0.0));
world.member_mut(gone).set_velocity(Velocity::new(1.0, 0.0));
}
#[test]
fn an_empty_seat_is_nothing_to_anybody() {
let mut hole = wire::Recast::of(&wire::VACANT);
let mut left = Thing::at(0.0, 20.0).wearing(CRATE_SPRITE).stopped_by(CRATE);
let mut right = Thing::at(16.0, 20.0)
.wearing(CRATE_SPRITE)
.stopped_by(CRATE);
for _ in 0..3 {
left.velocity = Velocity::new(2.0, 0.0);
right.velocity = Velocity::new(-2.0, 0.0);
WORLD.step_cast(
&mut [left.as_kinetic(), &mut hole, right.as_kinetic()],
air,
flagged,
);
}
assert_eq!(left.body.pos(), (4.0, 20.0));
assert_eq!(right.body.pos(), (12.0, 20.0));
assert!(left.contacts.right() && left.contacts.touches(CRATE));
assert!(right.contacts.left() && right.contacts.touches(CRATE));
assert_eq!(hole.body().pos(), (0.0, 0.0));
assert_eq!(*hole.contacts(), Contacts::empty());
}
#[test]
fn a_rectangle_keeps_its_seat_on_the_body_the_step_moved() {
let mut world: World<1> = World::new();
let inset = Member::builder(0.0, 0.0, 4, 8)
.offset(2, 0)
.moving(3.0, 1.0)
.enlist(&mut world)
.unwrap();
assert_eq!(world.member(inset).bounds(), Bounds::new(2, 0, 4, 8));
world.step(&CTX);
assert_eq!(world.member(inset).pos(), (3.0, 1.0));
assert_eq!(world.member(inset).bounds(), Bounds::new(5, 1, 4, 8));
world.member_mut(inset).resize(8, 4);
world.member_mut(inset).set_offset(0, 4);
assert_eq!(world.member(inset).bounds(), Bounds::new(3, 5, 8, 4));
}
#[test]
fn a_teleport_re_snaps_the_drawn_pixel_and_takes_the_rectangle_with_it() {
let mut world: World<1> = World::new();
let prop = Member::builder(0.0, 0.0, 8, 8)
.offset(1, -1)
.prop()
.enlist(&mut world)
.unwrap();
world.member_mut(prop).set_pos(40.9, 12.2);
assert_eq!(world.member(prop).pos(), (40.9, 12.2));
assert_eq!(world.member(prop).draw_pos(), (40, 12));
assert_eq!(world.member(prop).bounds(), Bounds::new(41, 11, 8, 8));
}
#[test]
fn what_a_member_is_told_after_it_is_seated_lands_in_its_seat() {
let mut world: World<1> = World::new().with_solid(WALL);
let walker = pebble(&mut world, 0.0, 0.0);
let seat = walker.seat();
world.member_mut(walker).set_sprite(Some(CRATE_SPRITE));
assert_eq!(world.records[seat].sprite, CRATE_SPRITE.0 as u16);
world.member_mut(walker).set_sprite(None);
assert_eq!(world.records[seat].sprite, wire::UNWORN);
world.member_mut(walker).set_heeds(CRATE);
assert_eq!(world.records[seat].heeds, BitFlags::from(CRATE).bits());
world.member_mut(walker).set_solid(Some(CRATE.into()));
assert_eq!(world.records[seat].solid, BitFlags::from(CRATE).bits());
world.member_mut(walker).set_solid(None);
assert_eq!(world.records[seat].solid, BitFlags::from(WALL).bits());
world
.member_mut(walker)
.set_confines(Some(Bounds::new(-8, 4, 100, 64)));
let record = &world.records[seat];
assert_eq!(record.meta & wire::CONFINED, wire::CONFINED);
assert_eq!(
(record.cx, record.cy, record.cw, record.ch),
(-8, 4, 100, 64)
);
world.member_mut(walker).set_confines(None);
assert_eq!(world.records[seat].meta & wire::CONFINED, 0);
}
#[test]
fn what_a_member_is_described_as_reads_back_through_its_handle() {
let mut world: World<1> = World::new().with_solid(WALL);
let walker = pebble(&mut world, 0.0, 0.0);
assert_eq!(world.member(walker).sprite(), None);
assert_eq!(world.member(walker).solid(), None, "no rule of its own yet");
assert_eq!(world.member(walker).heeds(), BitFlags::all());
assert_eq!(world.member(walker).confines(), None);
world.member_mut(walker).set_sprite(Some(CRATE_SPRITE));
world.member_mut(walker).set_solid(Some(WALL | CRATE));
world.member_mut(walker).set_heeds(CRATE);
world
.member_mut(walker)
.set_confines(Some(Bounds::new(-8, 4, 100, 64)));
assert_eq!(world.member(walker).sprite(), Some(CRATE_SPRITE));
assert_eq!(world.member(walker).solid(), Some(WALL | CRATE));
assert_eq!(world.member(walker).heeds(), CRATE.into());
assert_eq!(
world.member(walker).confines(),
Some(Bounds::new(-8, 4, 100, 64))
);
world.member_mut(walker).set_solid(None);
assert_eq!(world.member(walker).solid(), None);
}
#[test]
fn a_member_borrowed_to_change_answers_what_it_was_just_told() {
let mut world: World<1> = World::new();
let walker = pebble(&mut world, 0.0, 0.0);
let mut member = world.member_mut(walker);
member.set_velocity(Velocity::new(1.5, -0.5));
member.set_flip(true, false);
member.set_span(2, 3);
member.set_hidden(true);
assert_eq!(member.velocity(), Velocity::new(1.5, -0.5));
assert_eq!(member.flip(), (true, false));
assert_eq!(member.span(), (2, 3));
assert!(member.hidden());
let fresh = world.member(walker);
assert_eq!(fresh.velocity(), Velocity::new(1.5, -0.5));
assert_eq!(fresh.flip(), (true, false));
assert_eq!(fresh.span(), (2, 3));
assert!(fresh.hidden());
}
#[test]
fn two_members_can_be_read_side_by_side() {
let mut world: World<2> = World::new();
let a = Member::builder(0.0, 20.0, 8, 8).enlist(&mut world).unwrap();
let b = Member::builder(0.0, 20.0, 8, 8).enlist(&mut world).unwrap();
let (first, second) = (world.member(a), world.member(b));
assert_eq!(first.bounds(), second.bounds());
}
#[test]
fn either_view_prints_the_member_it_borrows() {
let mut world: World<1> = World::new();
let member = Member::builder(4.0, 8.0, 8, 8)
.moving(1.0, -2.0)
.enlist(&mut world)
.unwrap();
let read = format!("{:?}", world.member(member));
assert!(
read.starts_with(
"Member { id: MemberId { slot: 0, generation: 1 }, pos: (4.0, 8.0), \
draw_pos: (4, 8), velocity: Velocity { dx: 1.0, dy: -2.0 },"
),
"{read}"
);
assert!(read.ends_with("hidden: false, prop: false }"), "{read}");
assert_eq!(
format!("{:?}", world.member_mut(member)),
format!("MemberMut({read})")
);
}
#[test]
fn the_scene_s_word_for_wall_reaches_everybody_who_goes_by_it() {
let mut world: World<2> = World::new();
let walker = pebble(&mut world, 0.0, 0.0);
let ghost = Member::builder(0.0, 40.0, 8, 8)
.stopped_by(BitFlags::empty())
.enlist(&mut world)
.unwrap();
let mut world = world.with_solid(WALL);
assert_eq!(
world.records[walker.seat()].solid,
BitFlags::from(WALL).bits()
);
assert_eq!(world.records[ghost.seat()].solid, 0);
world.declare_solid(CRATE);
assert_eq!(
world.records[walker.seat()].solid,
BitFlags::from(CRATE).bits()
);
assert_eq!(world.records[ghost.seat()].solid, 0);
}
#[test]
#[should_panic(expected = "seat 255 is no seat of this world")]
fn a_handle_to_nobody_is_nobody_in_any_world() {
let mut world: World<2> = World::new();
assert!(world.get_member(MemberId::NOBODY).is_none());
let _ = pebble(&mut world, 0.0, 0.0);
assert!(world.get_member(MemberId::NOBODY).is_none());
world.member(MemberId::NOBODY);
}
#[test]
fn a_stale_id_is_no_member_to_get() {
let mut world: World<1> = World::new();
let member = pebble(&mut world, 0.0, 0.0);
assert!(world.get_member(member).is_some());
assert!(world.get_member_mut(member).is_some());
world.member_mut(member).retire();
assert!(world.get_member(member).is_none());
assert!(world.get_member_mut(member).is_none());
assert!(world.get_member(MemberId::NOBODY).is_none());
assert!(world.get_member_mut(MemberId::NOBODY).is_none());
}
#[test]
fn an_id_is_nobody_in_a_world_built_afresh() {
let mut first: World<2> = World::new();
let member = pebble(&mut first, 0.0, 0.0);
let fresh: World<2> = World::new();
assert!(fresh.get_member(member).is_none());
}
#[test]
fn a_seat_s_ids_come_round_on_its_hundred_and_twenty_ninth_letting() {
let mut world: World<1> = World::new();
let first = pebble(&mut world, 0.0, 0.0);
world.member_mut(first).retire();
for _ in 1..128 {
let id = pebble(&mut world, 0.0, 0.0);
assert_ne!(id, first);
world.member_mut(id).retire();
}
assert_eq!(pebble(&mut world, 0.0, 0.0), first);
}
#[test]
fn a_whole_world_can_be_spelled_out_as_a_constant() {
const LEVEL: World<2, Gravity> = World::new().with_forces(Gravity::new());
static SCENE: World<2, Gravity> = LEVEL;
assert_eq!(SCENE.high_water(), 0);
let mut world = LEVEL;
world.declare_solid(WALL);
let member = pebble(&mut world, 0.0, 0.0);
assert_eq!(
world.records[member.seat()].solid,
BitFlags::from(WALL).bits()
);
}
#[test]
fn a_member_is_held_inside_the_limits_it_is_confined_to() {
let mut world: World<1> = World::new();
let walker = Member::builder(-4.0, 8.0, 8, 8)
.moving(-2.0, 0.5)
.confined_to(Bounds::screen())
.enlist(&mut world)
.unwrap();
world.step(&CTX);
assert!(world.member(walker).contacts().left());
assert_eq!(world.member(walker).pos(), (0.0, 8.5));
world.member_mut(walker).set_confines(None);
world
.member_mut(walker)
.set_velocity(Velocity::new(-2.0, 0.0));
world.step(&CTX);
assert_eq!(world.member(walker).pos(), (-2.0, 8.5));
assert_eq!(world.member(walker).contacts(), Contacts::empty());
}
#[test]
fn a_bare_member_is_drawn_from_one_cell_unflipped_and_shown() {
let mut world: World<1> = World::new();
let bare = pebble(&mut world, 4.5, -2.25);
assert_eq!(world.member(bare).flip(), (false, false));
assert_eq!(world.member(bare).span(), (1, 1));
assert!(!world.member(bare).hidden());
let record = &world.records[bare.seat()];
assert_eq!(
record.meta & (wire::FLIP_X | wire::FLIP_Y | wire::HIDDEN),
0
);
assert_eq!(record.span, 0);
let taken = &world.records[..world.high_water()];
assert_eq!(wire::looks(taken, BitFlags::empty(), unflagged).count(), 0);
}
#[test]
fn everything_a_description_says_about_the_look_reaches_the_seat() {
let mut world: World<1> = World::new();
let member = Member::builder(0.0, 0.0, 8, 8)
.wearing(CRATE_SPRITE)
.flipped(true, false)
.spanning(2, 3)
.hidden()
.enlist(&mut world)
.unwrap();
let record = &world.records[member.seat()];
assert_eq!(record.meta, wire::FLIP_X | wire::HIDDEN);
assert_eq!(record.span, 0x21);
assert_eq!(world.member(member).flip(), (true, false));
assert_eq!(world.member(member).span(), (2, 3));
assert!(world.member(member).hidden());
}
#[test]
fn what_a_member_looks_like_can_be_changed_after_it_is_seated() {
let mut world: World<1> = World::new();
let door = Member::builder(0.0, 0.0, 8, 8)
.wearing(CRATE_SPRITE)
.confined_to(Bounds::screen())
.prop()
.enlist(&mut world)
.unwrap();
let seat = door.seat();
let steps_by = wire::PROP | wire::CONFINED;
world.member_mut(door).set_flip(true, false);
assert_eq!(world.records[seat].meta, steps_by | wire::FLIP_X);
assert_eq!(world.member(door).flip(), (true, false));
world.member_mut(door).set_flip(false, true);
assert_eq!(world.records[seat].meta, steps_by | wire::FLIP_Y);
assert_eq!(world.member(door).flip(), (false, true));
world.member_mut(door).set_span(16, 16);
assert_eq!(world.records[seat].span, 0xff);
assert_eq!(world.member(door).span(), (16, 16));
world.member_mut(door).set_hidden(true);
assert_eq!(
world.records[seat].meta,
steps_by | wire::FLIP_Y | wire::HIDDEN
);
assert!(world.member(door).hidden());
assert_eq!(world.records[seat].look(), None);
world.member_mut(door).set_hidden(false);
assert_eq!(world.records[seat].meta, steps_by | wire::FLIP_Y);
assert!(!world.member(door).hidden());
assert!(
world.records[seat].look().is_some(),
"it was never shown again"
);
}
#[test]
fn the_cast_is_drawn_where_it_stands_in_seat_order() {
let mut world: World<4> = World::new();
let plain = Member::builder(3.5, 7.25, 8, 8)
.wearing(SpriteId(5))
.enlist(&mut world)
.unwrap();
let gone = Member::builder(20.0, 0.0, 8, 8)
.wearing(SpriteId(6))
.enlist(&mut world)
.unwrap();
let _ghost = Member::builder(40.0, 0.0, 8, 8)
.wearing(WALL_SPRITE)
.hidden()
.enlist(&mut world)
.unwrap();
let turned = Member::builder(60.5, 30.0, 8, 8)
.wearing(CRATE_SPRITE)
.flipped(true, false)
.spanning(2, 3)
.enlist(&mut world)
.unwrap();
world.member_mut(gone).retire();
let taken = &world.records[..world.high_water()];
let (plain_x, plain_y) = world.member(plain).draw_pos();
let (turned_x, turned_y) = world.member(turned).draw_pos();
let shown: Vec<wire::Look> = wire::looks(taken, BitFlags::empty(), unflagged).collect();
assert_eq!(
shown,
[
wire::Look {
sprite: SpriteId(5),
x: plain_x,
y: plain_y,
width: 8,
height: 8,
flip_x: false,
flip_y: false,
},
wire::Look {
sprite: CRATE_SPRITE,
x: turned_x,
y: turned_y,
width: 16,
height: 24,
flip_x: true,
flip_y: false,
},
]
);
let layered: Vec<SpriteId> = wire::looks(taken, WALL | CRATE, flagged)
.map(|look| look.sprite)
.collect();
assert_eq!(layered, [CRATE_SPRITE]);
}
#[test]
fn a_step_moves_the_look_with_the_body_and_leaves_the_rest_of_it_alone() {
let mut world: World<1> = World::new();
let runner = Member::builder(0.0, 0.0, 8, 8)
.moving(3.0, 2.0)
.wearing(CRATE_SPRITE)
.flipped(true, true)
.spanning(2, 1)
.hidden()
.enlist(&mut world)
.unwrap();
world.step(&CTX);
assert_eq!(
world.member(runner).draw_pos(),
(3, 2),
"the step never moved it"
);
assert_eq!(world.member(runner).flip(), (true, true));
assert_eq!(world.member(runner).span(), (2, 1));
assert!(world.member(runner).hidden());
world.member_mut(runner).set_hidden(false);
let look = world.records[runner.seat()]
.look()
.expect("a member wearing a cell and shown has a look");
assert_eq!((look.x, look.y), world.member(runner).draw_pos());
}
#[test]
fn a_retired_seat_shows_nothing_and_the_next_member_in_it_starts_plain() {
let mut world: World<1> = World::new();
let showy = Member::builder(0.0, 0.0, 8, 8)
.wearing(CRATE_SPRITE)
.flipped(true, true)
.spanning(3, 2)
.enlist(&mut world)
.unwrap();
let seat = showy.seat();
assert!(world.records[seat].look().is_some());
world.member_mut(showy).retire();
assert_eq!(world.records[seat].look(), None, "a retired seat was drawn");
let successor = Member::builder(8.0, 16.0, 8, 8)
.wearing(CRATE_SPRITE)
.enlist(&mut world)
.unwrap();
assert_eq!(world.member(successor).flip(), (false, false));
assert_eq!(world.member(successor).span(), (1, 1));
assert!(!world.member(successor).hidden());
assert_eq!(
world.records[seat].look(),
Some(wire::Look {
sprite: CRATE_SPRITE,
x: 8,
y: 16,
width: 8,
height: 8,
flip_x: false,
flip_y: false,
})
);
}
#[test]
#[should_panic(expected = "one to sixteen cells")]
fn a_member_drawn_from_no_cells_at_all_is_refused_loudly() {
let mut world: World<1> = World::new();
let _ = Member::builder(0.0, 0.0, 8, 8)
.spanning(0, 1)
.enlist(&mut world)
.unwrap();
}
#[test]
#[should_panic(expected = "one to sixteen cells")]
fn a_member_drawn_from_past_the_edge_of_the_sheet_is_refused_loudly() {
let mut world: World<1> = World::new();
let member = pebble(&mut world, 0.0, 0.0);
world.member_mut(member).set_span(1, 17);
}
#[test]
fn a_draw_walks_the_seats_natively_whatever_the_layers() {
let mut world: World<3> = World::new();
let _shown = Member::builder(0.0, 0.0, 8, 8)
.wearing(CRATE_SPRITE)
.enlist(&mut world)
.unwrap();
let gone = pebble(&mut world, 16.0, 0.0);
let _sheet = Member::builder(32.0, 0.0, 8, 8)
.wearing(WALL_SPRITE)
.flipped(true, true)
.spanning(16, 16)
.enlist(&mut world)
.unwrap();
world.member_mut(gone).retire();
let mut gfx = Graphics { _private: () };
world.draw(&mut gfx, BitFlags::empty());
world.draw(&mut gfx, WALL);
}
#[test]
fn an_entity_stopped_by_nothing_steps_straight_through_everything() {
let mut sensor = Thing::at(0.0, 0.0).moving(4.0, 0.0);
let mut wall = Thing::at(8.0, 0.0).wearing(WALL_SPRITE);
assert!(sensor.solid().is_none());
stepped(&mut [sensor.as_kinetic(), wall.as_kinetic()]);
assert_eq!(sensor.body.pos(), (4.0, 0.0));
assert!(sensor.contacts.touches(WALL) && sensor.contacts.sides().is_empty());
}
#[test]
fn an_entity_alone_in_an_empty_world_meets_nothing() {
let mut walker = Thing::at(8.0, 8.0).stopped_by(WALL).moving(1.0, 1.0);
stepped(&mut [walker.as_kinetic()]);
assert_eq!(walker.contacts, Contacts::empty());
assert_eq!(walker.body.pos(), (9.0, 9.0));
assert_eq!(walker.velocity, Velocity::new(1.0, 1.0));
}
#[test]
fn the_map_stops_an_entity_that_answers_to_walls() {
let mut faller = Thing::at(0.0, 7.0).stopped_by(WALL).moving(0.0, 4.0);
WORLD.step_cast(
&mut [faller.as_kinetic()],
map(&["....", "....", "####"]),
unflagged,
);
assert!(faller.contacts.below() && faller.contacts.touches(WALL));
assert_eq!(faller.velocity.dy, 0.0);
}
#[test]
fn the_world_s_solid_stops_every_entity_that_has_no_rule_of_its_own() {
let mut walker = Thing::at(0.0, 0.0).moving(4.0, 0.0);
assert_eq!(walker.solid(), None);
walled().step_cast(&mut [walker.as_kinetic()], map(&[".#"]), flagged);
assert_eq!(walker.body.pos(), (0.0, 0.0));
assert!(walker.contacts.right() && walker.contacts.touches(WALL));
let mut lift = Thing::at(30.0, 0.0).wearing(WALL_SPRITE).parked();
let mut other = Thing::at(20.0, 0.0).moving(4.0, 0.0);
walled().step_cast(&mut [lift.as_kinetic(), other.as_kinetic()], air, flagged);
assert_eq!(other.body.pos(), (20.0, 0.0), "it walked into the lift");
assert!(other.contacts.right() && other.contacts.touches(WALL));
}
#[test]
fn an_entity_with_rules_of_its_own_replaces_the_world_s_rather_than_adds_to_them() {
let mut walker = Thing::at(0.0, 0.0).stopped_by(CRATE).moving(4.0, 0.0);
walled().step_cast(&mut [walker.as_kinetic()], map(&[".#"]), flagged);
assert_eq!(walker.body.pos(), (4.0, 0.0), "the world's wall stopped it");
assert!(walker.contacts.touches(WALL) && walker.contacts.sides().is_empty());
let mut ghost = Thing::at(0.0, 0.0)
.stopped_by(BitFlags::empty())
.moving(4.0, 0.0);
walled().step_cast(&mut [ghost.as_kinetic()], map(&[".#"]), flagged);
assert_eq!(ghost.body.pos(), (4.0, 0.0));
assert!(ghost.contacts.touches(WALL) && ghost.contacts.sides().is_empty());
}
#[test]
fn an_entity_is_stopped_by_the_cast_member_in_its_way() {
let mut walker = Thing::at(0.0, 0.0).stopped_by(CRATE).moving(4.0, 0.0);
let mut boxed = Thing::at(10.0, 0.0).wearing(CRATE_SPRITE);
stepped(&mut [walker.as_kinetic(), boxed.as_kinetic()]);
assert_eq!(walker.body.pos(), (0.0, 0.0), "it walked through the crate");
assert!(walker.contacts.right() && walker.contacts.touches(CRATE));
assert_eq!(boxed.body.pos(), (10.0, 0.0));
}
#[test]
fn two_entities_of_one_kind_stop_each_other_and_neither_is_its_own_wall() {
let mut left = Thing::at(0.0, 20.0)
.wearing(CRATE_SPRITE)
.stopped_by(CRATE)
.moving(2.0, 0.0);
let mut right = Thing::at(16.0, 20.0)
.wearing(CRATE_SPRITE)
.stopped_by(CRATE)
.moving(-2.0, 0.0);
for _ in 0..2 {
left.velocity = Velocity::new(2.0, 0.0);
right.velocity = Velocity::new(-2.0, 0.0);
stepped(&mut [left.as_kinetic(), right.as_kinetic()]);
}
assert_eq!(left.body.pos(), (4.0, 20.0));
assert_eq!(right.body.pos(), (12.0, 20.0));
assert_eq!(left.contacts, Contacts::empty());
assert_eq!(right.contacts, Contacts::empty());
for _ in 0..4 {
left.velocity = Velocity::new(2.0, 0.0);
right.velocity = Velocity::new(-2.0, 0.0);
stepped(&mut [left.as_kinetic(), right.as_kinetic()]);
}
assert_eq!(left.body.pos(), (4.0, 20.0));
assert_eq!(right.body.pos(), (12.0, 20.0));
assert!(left.contacts.right() && left.contacts.touches(CRATE));
assert!(right.contacts.left() && right.contacts.touches(CRATE));
}
#[test]
fn a_cast_too_long_to_snapshot_is_answered_exactly_as_a_short_one() {
let snapshotted = crates_meeting(0);
let brimming = crates_meeting(SNAPSHOT - 2);
let walked = crates_meeting(SNAPSHOT);
assert_eq!(snapshotted, brimming, "the full snapshot disagreed");
assert_eq!(snapshotted, walked, "the fallback disagreed");
assert_eq!(snapshotted.0, (4.0, 20.0));
assert_eq!(snapshotted.1, (12.0, 20.0));
assert!(snapshotted.2.right() && snapshotted.2.touches(CRATE));
assert!(snapshotted.3.left() && snapshotted.3.touches(CRATE));
}
fn crates_meeting(padding: usize) -> ((f32, f32), (f32, f32), Contacts, Contacts) {
let mut left = Thing::at(0.0, 20.0).wearing(CRATE_SPRITE).stopped_by(CRATE);
let mut right = Thing::at(16.0, 20.0)
.wearing(CRATE_SPRITE)
.stopped_by(CRATE);
let mut crowd: Vec<Thing> = (0..padding).map(|_| Thing::at(100.0, 100.0)).collect();
for _ in 0..3 {
left.velocity = Velocity::new(2.0, 0.0);
right.velocity = Velocity::new(-2.0, 0.0);
let mut cast: Vec<&mut dyn Kinetic> = Vec::with_capacity(padding + 2);
cast.push(left.as_kinetic());
cast.push(right.as_kinetic());
cast.extend(crowd.iter_mut().map(Thing::as_kinetic));
WORLD.step_cast(&mut cast, air, flagged);
}
(
left.body.pos(),
right.body.pos(),
left.contacts,
right.contacts,
)
}
#[test]
fn an_entity_of_a_kind_all_on_its_own_is_never_stopped_by_itself() {
for velocity in [
Velocity::new(2.0, 0.0),
Velocity::new(0.0, 2.0),
Velocity::new(-2.0, -2.0),
] {
let mut boxed = Thing::at(8.0, 8.0)
.wearing(CRATE_SPRITE)
.stopped_by(CRATE)
.moving(velocity.dx, velocity.dy);
stepped(&mut [boxed.as_kinetic()]);
assert_eq!(
boxed.body.pos(),
(8.0 + velocity.dx, 8.0 + velocity.dy),
"{velocity:?} ran into itself"
);
assert_eq!(boxed.contacts, Contacts::empty(), "{velocity:?} met itself");
}
}
#[test]
fn an_entity_that_wears_nothing_is_there_for_nobody() {
let mut ghost = Thing::at(0.0, 0.0).stopped_by(CRATE).moving(4.0, 0.0);
let mut boxed = Thing::at(10.0, 0.0)
.wearing(CRATE_SPRITE)
.stopped_by(CRATE)
.moving(-4.0, 0.0);
assert_eq!(ghost.sprite(), None);
stepped(&mut [ghost.as_kinetic(), boxed.as_kinetic()]);
assert_eq!(ghost.body.pos(), (0.0, 0.0));
assert!(ghost.contacts.right() && ghost.contacts.touches(CRATE));
assert_eq!(boxed.body.pos(), (6.0, 0.0));
assert_eq!(boxed.contacts, Contacts::empty());
}
#[test]
fn a_lift_stepped_before_its_rider_carries_it_the_same_update() {
let mut lift = Thing::at(0.0, 32.0)
.sized(24, 8)
.wearing(WALL_SPRITE)
.moving(0.0, -1.0);
let mut rider = Thing::at(0.0, 24.0).stopped_by(WALL);
for update in 1..=20 {
lift.velocity = Velocity::new(0.0, -1.0);
rider.velocity = Velocity::new(0.0, 1.0);
stepped(&mut [lift.as_kinetic(), rider.as_kinetic()]);
assert!(
rider.contacts.below(),
"the rider let go on update {update}"
);
assert!(rider.contacts.touches(WALL));
assert_eq!(
rider.body.draw_y() + 8,
lift.body.draw_y(),
"the rider is not standing on the lift on update {update}"
);
}
}
#[test]
fn a_lift_stepped_after_its_rider_is_a_frame_behind() {
let mut lift = Thing::at(0.0, 32.0)
.sized(24, 8)
.wearing(WALL_SPRITE)
.moving(0.0, -1.0);
let mut rider = Thing::at(0.0, 24.0).stopped_by(WALL);
for _ in 0..4 {
lift.velocity = Velocity::new(0.0, -1.0);
rider.velocity = Velocity::new(0.0, 1.0);
stepped(&mut [rider.as_kinetic(), lift.as_kinetic()]);
}
assert!(
rider.body.draw_y() + 8 > lift.body.draw_y(),
"the rider kept up with a lift stepped after it"
);
}
#[test]
fn an_entity_with_no_rectangle_at_all_is_only_moved() {
let mut nothing = Thing::at(0.0, 0.0)
.sized(0, 8)
.stopped_by(WALL)
.moving(4.0, 0.0);
let mut wall = Thing::at(2.0, 0.0).wearing(WALL_SPRITE);
stepped(&mut [nothing.as_kinetic(), wall.as_kinetic()]);
assert_eq!(nothing.body.pos(), (4.0, 0.0));
assert_eq!(nothing.contacts, Contacts::empty());
}
#[test]
fn an_entity_that_confines_nothing_is_free_to_walk_off_the_map() {
let mut walker = Thing::at(120.0, 120.0).stopped_by(WALL).moving(4.0, 4.0);
assert_eq!(walker.confines(), None);
for _ in 0..4 {
walker.velocity = Velocity::new(4.0, 4.0);
stepped(&mut [walker.as_kinetic()]);
assert_eq!(walker.contacts, Contacts::empty());
}
assert_eq!(walker.body.pos(), (136.0, 136.0));
assert!(!walker.bounds().on_screen(), "something held it back");
}
#[test]
fn an_entity_is_held_inside_the_limits_it_confines_itself_to() {
let mut walker = confined(-4.0, 8.0).moving(-2.0, 0.5);
stepped(&mut [walker.as_kinetic()]);
let held = walker.contacts;
assert!(held.left() && !held.right() && !held.above() && !held.below());
assert_eq!(walker.body.pos(), (0.0, 8.5));
assert_eq!(walker.velocity, Velocity::new(0.0, 0.5));
let mut walker = confined(200.0, 200.0).moving(1.0, 4.0);
stepped(&mut [walker.as_kinetic()]);
assert!(walker.contacts.right() && walker.contacts.below());
assert_eq!(walker.body.pos(), (120.0, 120.0));
assert_eq!(walker.velocity, Velocity::default());
}
#[test]
fn an_entity_is_held_at_the_top_edge_and_reports_it() {
let mut walker = confined(8.0, -4.0).moving(0.5, -2.0);
stepped(&mut [walker.as_kinetic()]);
let held = walker.contacts;
assert!(held.above() && !held.below() && !held.left() && !held.right());
assert_eq!(walker.body.pos(), (8.5, 0.0));
assert_eq!(walker.velocity, Velocity::new(0.5, 0.0));
}
#[test]
fn an_entity_within_the_limits_is_left_alone() {
let mut walker = confined(64.0, 64.0).moving(0.5, 0.5);
stepped(&mut [walker.as_kinetic()]);
assert_eq!(walker.contacts, Contacts::empty());
assert_eq!(walker.body.pos(), (64.5, 64.5));
assert_eq!(walker.velocity, Velocity::new(0.5, 0.5));
}
#[test]
fn an_entity_flush_against_an_edge_is_not_held() {
for (x, y) in [(0.0, 0.0), (120.0, 120.0)] {
let mut walker = confined(x, y);
stepped(&mut [walker.as_kinetic()]);
assert_eq!(
walker.contacts,
Contacts::empty(),
"held at ({x}, {y}), which is inside the screen"
);
assert_eq!(walker.body.pos(), (x, y));
}
}
#[test]
fn an_entity_inside_the_limits_keeps_the_pixel_it_draws_at() {
let mut walker = confined(64.0, 64.0);
for _ in 0..3 {
walker.velocity = Velocity::new(0.5, 0.4);
stepped(&mut [walker.as_kinetic()]);
}
walker.velocity = Velocity::default();
let drawn = walker.body.draw_pos();
assert_ne!(drawn.1, walker.body.y() as i16, "expected a held-back row");
stepped(&mut [walker.as_kinetic()]);
assert_eq!(walker.contacts, Contacts::empty());
assert_eq!(walker.body.draw_pos(), drawn);
}
#[test]
fn speed_carrying_an_entity_back_inside_is_not_spent() {
let mut walker = confined(140.0, 8.0).moving(-1.0, 0.0);
stepped(&mut [walker.as_kinetic()]);
assert!(walker.contacts.right());
assert_eq!(walker.body.pos(), (120.0, 8.0));
assert_eq!(walker.velocity, Velocity::new(-1.0, 0.0));
}
#[test]
fn limits_that_do_not_start_at_the_origin_hold_at_their_own_edges() {
let room = Bounds::new(-64, 32, 32, 48);
for (start, expected, side) in [
((-100.0, 40.0), (-64.0, 40.0), Contact::Left),
((0.0, 40.0), (-40.0, 40.0), Contact::Right),
((-50.0, 0.0), (-50.0, 32.0), Contact::Above),
((-50.0, 100.0), (-50.0, 72.0), Contact::Below),
] {
let mut walker = Thing::at(start.0, start.1).within(room);
stepped(&mut [walker.as_kinetic()]);
assert_eq!(walker.contacts, side.into(), "from {start:?}");
assert_eq!(walker.body.pos(), expected, "from {start:?}");
}
}
#[test]
fn limits_too_small_to_hold_the_entity_do_not_throw_it_out() {
let cramped = Bounds::new(0, 0, 4, 4);
let mut walker = Thing::at(8.0, 8.0).within(cramped);
stepped(&mut [walker.as_kinetic()]);
assert!(walker.contacts.right());
assert_eq!(walker.body.pos(), (0.0, 0.0));
stepped(&mut [walker.as_kinetic()]);
assert_eq!(walker.contacts, Contacts::empty());
assert_eq!(walker.body.pos(), (0.0, 0.0));
}
#[test]
fn no_limits_anywhere_can_be_made_to_hold_an_entity_badly() {
const CORNERS: [i16; 6] = [i16::MIN, -129, -1, 0, 127, i16::MAX - 1];
const SIDES: [u16; 6] = [0, 1, 8, 128, 32768, u16::MAX];
for &x in &CORNERS {
for &y in &CORNERS {
for &width in &SIDES {
for &height in &SIDES {
let limits = Bounds::new(x, y, width, height);
for start in [(0.0, 0.0), (-300.0, 60.0), (300.0, -60.0)] {
let mut walker =
Thing::at(start.0, start.1).within(limits).moving(1.0, 1.0);
stepped(&mut [walker.as_kinetic()]);
walker.velocity = Velocity::default();
stepped(&mut [walker.as_kinetic()]);
let settled = walker.body.pos();
stepped(&mut [walker.as_kinetic()]);
assert_eq!(
walker.body.pos(),
settled,
"{limits:?} could not settle an entity from {start:?}"
);
}
}
}
}
}
}
#[test]
fn limits_at_the_end_of_the_coordinate_space_do_not_overflow() {
let mut near = Thing::at(0.0, 0.0).within(Bounds::new(i16::MIN, i16::MIN, 4, 4));
stepped(&mut [near.as_kinetic()]);
let mut far = Thing::at(0.0, 0.0).within(Bounds::new(i16::MAX - 4, i16::MAX - 4, 4, 4));
stepped(&mut [far.as_kinetic()]);
let mut walker = Thing::at(0.0, 0.0).within(Bounds::new(0, 0, 40000, 8));
stepped(&mut [walker.as_kinetic()]);
assert_eq!(walker.contacts, Contacts::empty());
}
#[test]
fn limits_of_a_cart_s_own_hold_as_the_screen_does() {
let level = Bounds::new(0, 0, 256, 128);
let mut walker = Thing::at(300.0, 8.0).within(level);
stepped(&mut [walker.as_kinetic()]);
assert!(walker.contacts.right());
assert_eq!(walker.body.pos(), (248.0, 8.0));
}
#[test]
fn an_entity_is_held_by_the_rectangle_it_covers_wherever_it_put_it() {
let mut inset = Thing::at(200.0, 8.0).inset(2, 4, 8).moving(2.0, 0.0);
stepped(&mut [inset.as_kinetic()]);
assert!(inset.contacts.right());
assert_eq!(inset.bounds().right(), Bounds::screen().right());
assert_eq!(inset.body.pos(), (122.0, 8.0));
let mut inset = Thing::at(-10.0, 8.0).inset(2, 4, 8).moving(-2.0, 0.0);
stepped(&mut [inset.as_kinetic()]);
assert!(inset.contacts.left());
assert_eq!(inset.bounds().x(), 0);
assert_eq!(inset.body.pos(), (-2.0, 8.0));
let mut plank = Thing::at(200.0, 200.0)
.sized(16, 4)
.within(Bounds::screen());
stepped(&mut [plank.as_kinetic()]);
assert!(plank.contacts.right() && plank.contacts.below());
assert_eq!(plank.body.pos(), (112.0, 124.0));
}
#[test]
fn a_wall_and_an_edge_in_one_update_are_both_reported() {
let mut walker = Thing::at(100.0, 200.0)
.within(Bounds::screen())
.stopped_by(CRATE)
.moving(4.0, 4.0);
let mut boxed = Thing::at(110.0, 200.0).wearing(CRATE_SPRITE);
stepped(&mut [walker.as_kinetic(), boxed.as_kinetic()]);
assert!(walker.contacts.right() && walker.contacts.below());
assert!(walker.contacts.touches(CRATE));
}
#[test]
fn a_prop_is_met_where_the_cart_parked_it_and_never_moved() {
let mut hazard = Thing::at(16.0, 20.0).wearing(CRATE_SPRITE).parked();
let mut walker = Thing::at(0.0, 20.0).stopped_by(CRATE);
for _ in 0..8 {
walker.velocity = Velocity::new(2.0, 0.0);
WORLD.step_cast(
&mut [hazard.as_kinetic(), walker.as_kinetic()],
air,
flagged,
);
}
assert_eq!(hazard.body.pos(), (16.0, 20.0));
assert_eq!(*hazard.velocity_mut(), Velocity::default());
assert_eq!(hazard.contacts, Contacts::empty());
assert_eq!(walker.body.pos().0, 8.0);
assert!(walker.contacts.right() && walker.contacts.touches(CRATE));
}
#[test]
fn a_prop_the_cart_moves_is_met_where_it_now_is() {
let mut hazard = Thing::at(40.0, 20.0).wearing(CRATE_SPRITE).parked();
let mut sensor = Thing::at(20.0, 20.0);
stepped(&mut [hazard.as_kinetic(), sensor.as_kinetic()]);
assert!(!sensor.contacts.touches(CRATE));
hazard.body.set_pos(21.0, 20.0);
stepped(&mut [hazard.as_kinetic(), sensor.as_kinetic()]);
assert!(sensor.contacts.touches(CRATE));
}
#[test]
fn an_entity_hears_about_everything_it_meets_unless_it_says_otherwise() {
let mut crate_ = Thing::at(8.0, 0.0).wearing(CRATE_SPRITE).parked();
let mut walker = Thing::at(0.0, 0.0);
walker.velocity = Velocity::new(4.0, 0.0);
WORLD.step_cast(
&mut [crate_.as_kinetic(), walker.as_kinetic()],
map(&["##"]),
flagged,
);
assert!(walker.contacts.touches(CRATE), "the crate went unreported");
assert!(walker.contacts.touches(WALL), "the tile went unreported");
}
#[test]
fn a_neighbour_carrying_nothing_heeded_is_never_met() {
let mut crate_ = Thing::at(8.0, 0.0).wearing(CRATE_SPRITE).parked();
let mut walker = Thing::at(0.0, 0.0).heeding(WALL.into());
walker.velocity = Velocity::new(4.0, 0.0);
WORLD.step_cast(
&mut [crate_.as_kinetic(), walker.as_kinetic()],
air,
flagged,
);
assert!(!walker.contacts.touches(CRATE));
assert_eq!(walker.contacts, Contacts::empty());
assert_eq!(walker.body.pos().0, 4.0);
}
#[test]
fn a_tile_carrying_nothing_heeded_is_never_reported() {
let mut walker = Thing::at(0.0, 0.0).heeding(CRATE.into());
walker.velocity = Velocity::new(4.0, 0.0);
step_over(&mut [walker.as_kinetic()], map(&["##"]));
assert!(!walker.contacts.touches(WALL));
assert_eq!(walker.contacts, Contacts::empty());
}
#[test]
fn a_wall_it_never_asked_to_hear_about_still_stops_it_and_is_still_reported() {
let mut walker = Thing::at(0.0, 0.0).stopped_by(WALL).heeding(CRATE.into());
walker.velocity = Velocity::new(4.0, 0.0);
step_over(&mut [walker.as_kinetic()], map(&[".#"]));
assert_eq!(walker.body.pos().0, 0.0, "the wall let it through");
assert!(walker.contacts.right());
assert!(walker.contacts.touches(WALL), "the wall went unreported");
}
#[test]
fn a_neighbour_that_stops_it_is_reported_though_it_heeds_nothing_at_all() {
let mut crate_ = Thing::at(8.0, 0.0).wearing(CRATE_SPRITE).parked();
let mut walker = Thing::at(0.0, 0.0)
.stopped_by(CRATE)
.heeding(BitFlags::empty());
walker.velocity = Velocity::new(4.0, 0.0);
WORLD.step_cast(
&mut [crate_.as_kinetic(), walker.as_kinetic()],
air,
flagged,
);
assert_eq!(walker.body.pos().0, 0.0);
assert!(walker.contacts.right() && walker.contacts.touches(CRATE));
}
#[test]
fn a_mapless_world_leaves_the_tiles_to_the_picture() {
let mut walker = Thing::at(0.0, 0.0).stopped_by(WALL);
walker.velocity = Velocity::new(4.0, 0.0);
let mapful: World<0> = World::new();
mapful.step_cast(&mut [walker.as_kinetic()], map(&[".#"]), unflagged);
assert_eq!(walker.body.pos().0, 0.0);
assert!(walker.contacts.right() && walker.contacts.touches(WALL));
let mut drifter = Thing::at(0.0, 0.0).stopped_by(WALL);
drifter.velocity = Velocity::new(4.0, 0.0);
let scenery: World<0> = World::mapless();
scenery.step_cast(&mut [drifter.as_kinetic()], map(&[".#"]), unflagged);
assert_eq!(drifter.body.pos().0, 4.0);
assert_eq!(drifter.contacts, Contacts::empty());
}
#[test]
fn a_mapless_world_still_has_a_cast_and_still_has_edges() {
let mut crate_ = Thing::at(8.0, 0.0).wearing(CRATE_SPRITE).parked();
let mut walker = Thing::at(0.0, 0.0)
.stopped_by(CRATE)
.within(Bounds::new(0, 0, 64, 64));
walker.velocity = Velocity::new(4.0, 0.0);
let scenery: World<0> = World::mapless();
scenery.step_cast(
&mut [crate_.as_kinetic(), walker.as_kinetic()],
map(&["##"]),
flagged,
);
assert_eq!(walker.body.pos().0, 0.0, "the crate let it through");
assert!(walker.contacts.touches(CRATE));
let mut leaver = Thing::at(60.0, 0.0).within(Bounds::new(0, 0, 64, 64));
leaver.velocity = Velocity::new(8.0, 0.0);
scenery.step_cast(&mut [leaver.as_kinetic()], air, unflagged);
assert_eq!(leaver.body.pos().0, 56.0);
assert!(leaver.contacts.right());
}
fn step_over(
cast: &mut [&mut dyn Kinetic],
tiles: impl Fn(i16, i16) -> BitFlags<SpriteFlag> + Copy,
) {
WORLD.step_cast(cast, tiles, flagged);
}
fn stepped(cast: &mut [&mut dyn Kinetic]) {
WORLD.step_cast(cast, air, flagged);
}
fn step(cast: &mut [&mut dyn Kinetic]) {
WORLD.step_cast(cast, air, unflagged);
}
fn confined(x: f32, y: f32) -> Thing {
Thing::at(x, y).within(Bounds::screen())
}
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(),
_ => BitFlags::empty(),
}
}
}
fn air(_: i16, _: i16) -> BitFlags<SpriteFlag> {
BitFlags::empty()
}
fn flagged(sprite: SpriteId) -> BitFlags<SpriteFlag> {
match sprite {
WALL_SPRITE => WALL.into(),
CRATE_SPRITE => CRATE.into(),
_ => BitFlags::empty(),
}
}
fn unflagged(_: SpriteId) -> BitFlags<SpriteFlag> {
BitFlags::empty()
}
struct Thing {
body: Body,
velocity: Velocity,
contacts: Contacts,
size: (u16, u16),
offset: i16,
sprite: Option<SpriteId>,
solid: Option<BitFlags<SpriteFlag>>,
heeds: BitFlags<SpriteFlag>,
limits: Option<Bounds>,
prop: bool,
}
impl Thing {
fn at(x: f32, y: f32) -> Self {
Self {
body: Body::new(x, y),
velocity: Velocity::default(),
contacts: Contacts::default(),
size: (8, 8),
offset: 0,
sprite: None,
solid: None,
heeds: BitFlags::all(),
limits: None,
prop: false,
}
}
fn sized(mut self, width: u16, height: u16) -> Self {
self.size = (width, height);
self
}
fn inset(mut self, offset: i16, width: u16, height: u16) -> Self {
self.offset = offset;
self.sized(width, height).within(Bounds::screen())
}
fn moving(mut self, dx: f32, dy: f32) -> Self {
self.velocity = Velocity::new(dx, dy);
self
}
fn wearing(mut self, sprite: SpriteId) -> Self {
self.sprite = Some(sprite);
self
}
fn heeding(mut self, heeds: BitFlags<SpriteFlag>) -> Self {
self.heeds = heeds;
self
}
fn stopped_by(mut self, flags: impl Into<BitFlags<SpriteFlag>>) -> Self {
self.solid = Some(flags.into());
self
}
fn within(mut self, limits: Bounds) -> Self {
self.limits = Some(limits);
self
}
fn parked(mut self) -> Self {
self.prop = true;
self
}
fn as_kinetic(&mut self) -> &mut dyn Kinetic {
self
}
}
impl Kinetic for Thing {
fn body(&self) -> &Body {
&self.body
}
fn body_mut(&mut self) -> &mut Body {
&mut self.body
}
fn velocity_mut(&mut self) -> &mut Velocity {
&mut self.velocity
}
fn contacts(&self) -> &Contacts {
&self.contacts
}
fn contacts_mut(&mut self) -> &mut Contacts {
&mut self.contacts
}
fn bounds(&self) -> Bounds {
let (x, y) = self.body.draw_pos();
Bounds::new(x + self.offset, y, self.size.0, self.size.1)
}
fn prop(&self) -> bool {
self.prop
}
fn solid(&self) -> Option<BitFlags<SpriteFlag>> {
self.solid
}
fn heeds(&self) -> BitFlags<SpriteFlag> {
self.heeds
}
fn sprite(&self) -> Option<SpriteId> {
self.sprite
}
fn confines(&self) -> Option<Bounds> {
self.limits
}
}
const WALL: SpriteFlag = SpriteFlag::Flag0;
const CRATE: SpriteFlag = SpriteFlag::Flag1;
const WALL_SPRITE: SpriteId = SpriteId(1);
const CRATE_SPRITE: SpriteId = SpriteId(9);
}