use crate::block::TickAction;
use crate::block::{
self, Block, BlockAttributes, Evoxel, Evoxels, MinEval, Modifier, Resolution::R16, AIR,
};
use crate::math::{Face6, GridAab, GridCoordinate, GridVector, Vol};
use crate::op::Operation;
use crate::universe;
#[non_exhaustive] #[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Move {
pub direction: Face6,
pub distance: u16,
pub velocity: i16,
}
impl Move {
pub fn new(direction: Face6, distance: u16, velocity: i16) -> Self {
Self {
direction,
distance,
velocity,
}
}
pub fn paired_move(direction: Face6, distance: u16, velocity: i16) -> [Modifier; 2] {
[
Modifier::Move(Move {
direction,
distance,
velocity,
}),
Modifier::Move(Move {
direction: direction.opposite(),
distance: 256 - distance,
velocity: -velocity,
}),
]
}
pub(super) fn evaluate(
&self,
block: &Block,
this_modifier_index: usize,
mut input: MinEval,
filter: &block::EvalFilter,
) -> Result<MinEval, block::InEvalError> {
let Move {
direction,
distance,
velocity,
} = *self;
input = block::Quote::default().evaluate(input, filter)?;
let (original_bounds, effective_resolution) = match input.voxels {
Evoxels::Many(resolution, ref array) => (array.bounds(), resolution),
Evoxels::One(_) => (GridAab::for_block(R16), R16),
};
let distance_in_res =
GridCoordinate::from(distance) * GridCoordinate::from(effective_resolution) / 256;
let translation_in_res = direction.normal_vector() * distance_in_res;
let displaced_bounds: Option<GridAab> = original_bounds
.translate(translation_in_res)
.intersection_cubes(GridAab::for_block(effective_resolution));
let animation_action: Option<TickAction> = if displaced_bounds.is_none() && velocity >= 0 {
Some(TickAction::from(Operation::Become(AIR)))
} else if translation_in_res == GridVector::zero() && velocity == 0
|| distance == 0 && velocity < 0
{
assert!(
matches!(&block.modifiers()[this_modifier_index], Modifier::Move(m) if m == self)
);
let mut new_block = block.clone();
new_block.modifiers_mut().remove(this_modifier_index); Some(TickAction::from(Operation::Become(new_block)))
} else if velocity != 0 {
assert!(
matches!(&block.modifiers()[this_modifier_index], Modifier::Move(m) if m == self)
);
let mut new_block = block.clone();
if let Modifier::Move(Move {
distance, velocity, ..
}) = &mut new_block.modifiers_mut()[this_modifier_index]
{
*distance = i32::from(*distance)
.saturating_add(i32::from(*velocity))
.clamp(0, i32::from(u16::MAX))
.try_into()
.unwrap();
}
Some(TickAction::from(Operation::Become(new_block)))
} else {
None
};
let animation_hint = if animation_action.is_some() {
input.attributes.animation_hint
| block::AnimationHint::replacement(block::AnimationChange::Shape)
} else {
input.attributes.animation_hint
};
let attributes = BlockAttributes {
animation_hint,
tick_action: animation_action,
..input.attributes
};
Ok(match displaced_bounds {
Some(displaced_bounds) => {
block::Budget::decrement_voxels(
&filter.budget,
displaced_bounds.volume().unwrap(),
)?;
let displaced_voxels = match &input.voxels {
Evoxels::Many(_, voxels) => Evoxels::Many(
effective_resolution,
Vol::from_fn(displaced_bounds, |cube| voxels[cube - translation_in_res]),
),
&Evoxels::One(voxel) => {
Evoxels::Many(
effective_resolution,
Vol::from_fn(displaced_bounds, |_| voxel),
)
}
};
MinEval {
attributes,
voxels: displaced_voxels,
}
}
None => MinEval {
attributes,
voxels: Evoxels::One(Evoxel::AIR),
},
})
}
}
impl From<Move> for Modifier {
fn from(value: Move) -> Self {
Modifier::Move(value)
}
}
impl universe::VisitHandles for Move {
fn visit_handles(&self, _visitor: &mut dyn universe::HandleVisitor) {
let Move {
direction: _,
distance: _,
velocity: _,
} = self;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::{Composite, EvaluatedBlock, Resolution::*};
use crate::content::make_some_blocks;
use crate::math::{notnan, rgba_const, FaceMap, GridPoint, OpacityCategory, Rgb, Rgba};
use crate::space::Space;
use crate::time;
use crate::universe::Universe;
use ordered_float::NotNan;
use pretty_assertions::assert_eq;
#[test]
fn move_atom_block_evaluation() {
let color = rgba_const!(1.0, 0.0, 0.0, 1.0);
let original = Block::from(color);
let moved = original.clone().with_modifier(Move {
direction: Face6::PY,
distance: 128, velocity: 0,
});
let expected_bounds = GridAab::from_lower_size([0, 8, 0], [16, 8, 16]);
let ev_original = original.evaluate().unwrap();
assert_eq!(
moved.evaluate().unwrap(),
EvaluatedBlock {
attributes: ev_original.attributes.clone(),
color: color.to_rgb().with_alpha(NotNan::new(2. / 3.).unwrap()),
face_colors: FaceMap {
nx: color.to_rgb().with_alpha(notnan!(0.5)),
ny: color.to_rgb().with_alpha(notnan!(1.0)),
nz: color.to_rgb().with_alpha(notnan!(0.5)),
px: color.to_rgb().with_alpha(notnan!(0.5)),
py: color.to_rgb().with_alpha(notnan!(1.0)),
pz: color.to_rgb().with_alpha(notnan!(0.5)),
},
light_emission: Rgb::ZERO,
voxels: Evoxels::Many(
R16,
Vol::repeat(expected_bounds, Evoxel::from_block(&ev_original))
),
opaque: FaceMap::repeat(false).with(Face6::PY, true),
visible: true,
uniform_collision: None,
voxel_opacity_mask: Some(Vol::repeat(expected_bounds, OpacityCategory::Opaque)),
cost: block::Cost {
components: ev_original.cost.components + 1,
voxels: expected_bounds.volume_f64() as u32,
recursion: 0
}
}
);
}
#[test]
fn move_voxel_block_evaluation() {
let mut universe = Universe::new();
let resolution = R2;
let color = rgba_const!(1.0, 0.0, 0.0, 1.0);
let original = Block::builder()
.voxels_fn(resolution, |_| Block::from(color))
.unwrap()
.build_into(&mut universe);
let moved = original.clone().with_modifier(Move {
direction: Face6::PY,
distance: 128, velocity: 0,
});
let expected_bounds = GridAab::from_lower_size([0, 1, 0], [2, 1, 2]);
let ev_original = original.evaluate().unwrap();
assert_eq!(
moved.evaluate().unwrap(),
EvaluatedBlock {
attributes: ev_original.attributes.clone(),
color: color.to_rgb().with_alpha(NotNan::new(2. / 3.).unwrap()),
face_colors: FaceMap {
nx: color.to_rgb().with_alpha(notnan!(0.5)),
ny: color.to_rgb().with_alpha(notnan!(1.0)),
nz: color.to_rgb().with_alpha(notnan!(0.5)),
px: color.to_rgb().with_alpha(notnan!(0.5)),
py: color.to_rgb().with_alpha(notnan!(1.0)),
pz: color.to_rgb().with_alpha(notnan!(0.5)),
},
light_emission: Rgb::ZERO,
voxels: Evoxels::Many(
resolution,
Vol::repeat(expected_bounds, Evoxel::from_block(&ev_original))
),
opaque: FaceMap::repeat(false).with(Face6::PY, true),
visible: true,
uniform_collision: None,
voxel_opacity_mask: Some(Vol::repeat(expected_bounds, OpacityCategory::Opaque)),
cost: block::Cost {
components: ev_original.cost.components + 1,
voxels: 2u32.pow(3) * 3 / 2, recursion: 0
}
}
);
}
#[test]
fn move_also_quotes() {
let original = Block::builder()
.color(Rgba::WHITE)
.tick_action(Some(TickAction::from(Operation::Become(AIR))))
.build();
let moved = original.with_modifier(Move {
direction: Face6::PY,
distance: 128,
velocity: 0,
});
assert_eq!(moved.evaluate().unwrap().attributes.tick_action, None);
}
fn move_block_test(direction: Face6, velocity: i16, checker: impl FnOnce(&Space, &Block)) {
let [block] = make_some_blocks();
let mut space = Space::empty(GridAab::from_lower_upper([-1, -1, -1], [2, 2, 2]));
let [move_out, move_in] = Move::paired_move(direction, 0, velocity);
space
.set([0, 0, 0], block.clone().with_modifier(move_out))
.unwrap();
space
.set(
GridPoint::origin() + direction.normal_vector(),
block.clone().with_modifier(move_in),
)
.unwrap();
let mut universe = Universe::new();
let space = universe.insert_anonymous(space);
for _ in 0..257 {
universe.step(false, time::DeadlineNt::Whenever);
}
checker(&space.read().unwrap(), &block);
}
#[test]
fn velocity_zero() {
move_block_test(Face6::PX, 0, |space, block| {
assert_eq!(&space[[0, 0, 0]], block);
assert_eq!(&space[[1, 0, 0]], &AIR);
});
}
#[test]
fn velocity_slow() {
move_block_test(Face6::PX, 1, |space, block| {
assert_eq!(&space[[0, 0, 0]], &AIR);
assert_eq!(&space[[1, 0, 0]], block);
});
}
#[test]
fn velocity_whole_cube_in_one_tick() {
move_block_test(Face6::PX, 256, |space, block| {
assert_eq!(&space[[0, 0, 0]], &AIR);
assert_eq!(&space[[1, 0, 0]], block);
});
}
#[test]
fn move_inside_composite_destination() {
let [base, extra] = make_some_blocks();
let composite = Composite::new(extra, block::CompositeOperator::Over);
let block = base
.clone()
.with_modifier(Move {
direction: Face6::PX,
distance: 10,
velocity: 10,
})
.with_modifier(composite.clone());
let expected_after_tick = base
.clone()
.with_modifier(Move {
direction: Face6::PX,
distance: 20,
velocity: 10,
})
.with_modifier(composite);
assert_eq!(
block.evaluate().unwrap().attributes.tick_action,
Some(TickAction::from(Operation::Become(expected_after_tick)))
);
}
#[test]
fn move_inside_composite_source() {
let [base, extra] = make_some_blocks();
let block = extra.clone().with_modifier(Composite::new(
base.clone().with_modifier(Move {
direction: Face6::PX,
distance: 10,
velocity: 10,
}),
block::CompositeOperator::Over,
));
let expected_after_tick = extra.clone().with_modifier(Composite::new(
base.clone().with_modifier(Move {
direction: Face6::PX,
distance: 10,
velocity: 10,
}),
block::CompositeOperator::Over,
));
if false {
assert_eq!(
block.evaluate().unwrap().attributes.tick_action,
Some(TickAction::from(Operation::Become(expected_after_tick)))
);
} else {
assert_eq!(block.evaluate().unwrap().attributes.tick_action, None);
}
}
}