1use crate::block::TickAction;
2use crate::block::{
3 self, AIR, Block, BlockAttributes, Evoxel, Evoxels, MinEval, Modifier, Resolution::R16,
4};
5use crate::math::{Face6, GridAab, GridCoordinate, GridRotation, GridVector, Vol};
6use crate::op::Operation;
7use crate::time;
8use crate::universe;
9
10#[derive(Clone, Debug, Eq, Hash, PartialEq)]
22#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
23#[non_exhaustive]
24pub struct Move {
25 pub direction: Face6,
27
28 pub distance: u16,
30
31 pub velocity: i16,
34
35 pub schedule: time::Schedule,
39}
40
41impl Move {
42 pub fn new(direction: Face6, distance: u16, velocity: i16) -> Self {
44 Self {
45 direction,
46 distance,
47 velocity,
48 schedule: time::Schedule::EVERY_TICK,
49 }
50 }
51
52 #[must_use]
61 pub fn into_paired(self) -> [Self; 2] {
62 let complement = self.complement();
63 [self, complement]
64 }
65
66 #[must_use]
70 pub fn complement(&self) -> Self {
71 Move {
72 direction: self.direction.opposite(),
73 distance: 256 - self.distance,
74 velocity: -self.velocity,
75 schedule: self.schedule,
76 }
77 }
78
79 #[must_use]
81 pub fn rotate(mut self, rotation: GridRotation) -> Self {
82 self.direction = rotation.transform(self.direction);
83 self
84 }
85
86 pub(super) fn evaluate(
88 &self,
89 block: &Block,
90 this_modifier_index: usize,
91 mut input: MinEval,
92 filter: &block::EvalFilter<'_>,
93 ) -> Result<MinEval, block::InEvalError> {
94 let Move {
95 direction,
96 distance,
97 velocity,
98 schedule,
99 } = *self;
100
101 input = block::Quote::default().evaluate(input, filter)?;
106
107 let (input_attributes, input_voxels) = input.into_parts();
110 let (original_bounds, effective_resolution) = match input_voxels.single_voxel() {
111 None => (input_voxels.bounds(), input_voxels.resolution()),
112 Some(_) => (GridAab::for_block(R16), R16),
114 };
115
116 let distance_in_res =
119 GridCoordinate::from(distance) * GridCoordinate::from(effective_resolution) / 256;
120 let translation_in_res = direction.vector(distance_in_res);
121
122 let displaced_bounds: Option<GridAab> = original_bounds
124 .translate(translation_in_res)
125 .intersection_cubes(GridAab::for_block(effective_resolution));
126
127 let animation_op: Option<Operation> = if displaced_bounds.is_none() && velocity >= 0 {
128 Some(Operation::Become(AIR))
130 } else if translation_in_res == GridVector::zero() && velocity == 0
131 || distance == 0 && velocity < 0
132 {
133 assert!(
135 matches!(&block.modifiers()[this_modifier_index], Modifier::Move(m) if m == self)
136 );
137 let mut new_block = block.clone();
138 new_block.modifiers_mut().remove(this_modifier_index); Some(Operation::Become(new_block))
140 } else if velocity != 0 {
141 assert!(
143 matches!(&block.modifiers()[this_modifier_index], Modifier::Move(m) if m == self)
144 );
145 let mut new_block = block.clone();
146 {
147 let modifiers = new_block.modifiers_mut();
148
149 #[expect(clippy::shadow_unrelated)]
151 if let Modifier::Move(Move {
152 distance, velocity, ..
153 }) = &mut modifiers[this_modifier_index]
154 {
155 *distance = i32::from(*distance)
156 .saturating_add(i32::from(*velocity))
157 .clamp(0, i32::from(u16::MAX))
158 .try_into()
159 .unwrap();
160 }
161
162 modifiers.truncate(this_modifier_index + 1);
165 }
166 Some(Operation::Become(new_block))
167 } else {
168 None
170 };
171
172 let animation_hint = if animation_op.is_some() {
173 input_attributes.animation_hint
174 | block::AnimationHint::replacement(block::AnimationChange::Shape)
175 } else {
176 input_attributes.animation_hint
177 };
178
179 let attributes = BlockAttributes {
180 animation_hint,
181 tick_action: animation_op.map(|operation| TickAction {
182 operation,
183 schedule,
184 }),
185 ..input_attributes
186 };
187
188 Ok(match displaced_bounds {
189 Some(displaced_bounds) => {
190 block::Budget::decrement_voxels(
191 &filter.budget,
192 displaced_bounds.volume().unwrap(),
193 )?;
194
195 let displaced_voxels = match input_voxels.single_voxel() {
196 None => {
197 let voxels = input_voxels.as_vol_ref();
198 Evoxels::from_many(
199 effective_resolution,
200 Vol::from_fn(displaced_bounds, |cube| {
201 voxels[cube - translation_in_res]
202 }),
203 )
204 }
205 Some(voxel) => {
206 Evoxels::from_many(
210 effective_resolution,
211 Vol::from_fn(displaced_bounds, |_| voxel),
212 )
213 }
214 };
215 MinEval::new(attributes, displaced_voxels)
216 }
217 None => MinEval::new(attributes, Evoxels::from_one(Evoxel::AIR)),
218 })
219 }
220}
221
222impl From<Move> for Modifier {
223 fn from(value: Move) -> Self {
224 Modifier::Move(value)
225 }
226}
227
228impl universe::VisitHandles for Move {
229 fn visit_handles(&self, _visitor: &mut dyn universe::HandleVisitor) {
230 let Move {
231 direction: _,
232 distance: _,
233 velocity: _,
234 schedule: _,
235 } = self;
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::block::{Composite, EvaluatedBlock, Resolution::*, VoxelOpacityMask};
243 use crate::content::make_some_blocks;
244 use crate::math::{FaceMap, GridPoint, OpacityCategory, Rgb, Rgba, rgba_const, zo32};
245 use crate::space::{self, Space};
246 use crate::universe::{ReadTicket, Universe};
247 use pretty_assertions::assert_eq;
248
249 #[test]
250 fn move_atom_block_evaluation() {
251 let color = rgba_const!(1.0, 0.0, 0.0, 1.0);
252 let original = Block::from(color);
253 let moved = original.clone().with_modifier(Move {
254 direction: Face6::PY,
255 distance: 128, velocity: 0,
257 schedule: time::Schedule::EVERY_TICK,
258 });
259
260 let expected_bounds = GridAab::from_lower_size([0, 8, 0], [16, 8, 16]);
261
262 let ev_original = original.evaluate(ReadTicket::stub()).unwrap();
263 assert_eq!(
264 moved.evaluate(ReadTicket::stub()).unwrap(),
265 EvaluatedBlock {
266 block: moved,
267 attributes: ev_original.attributes.clone(),
268 voxels: Evoxels::from_many(
269 R16,
270 Vol::repeat(expected_bounds, Evoxel::from_block(&ev_original))
271 ),
272 cost: block::Cost {
273 components: ev_original.cost.components + 1,
274 voxels: expected_bounds.volume_f64() as u32,
275 recursion: 0
276 },
277 derived: block::Derived {
278 color: color.to_rgb().with_alpha(zo32(2. / 3.)),
279 face_colors: FaceMap {
280 nx: color.to_rgb().with_alpha(zo32(0.5)),
281 ny: color.to_rgb().with_alpha(zo32(1.0)),
282 nz: color.to_rgb().with_alpha(zo32(0.5)),
283 px: color.to_rgb().with_alpha(zo32(0.5)),
284 py: color.to_rgb().with_alpha(zo32(1.0)),
285 pz: color.to_rgb().with_alpha(zo32(0.5)),
286 },
287 light_emission: Rgb::ZERO,
288 opaque: FaceMap::splat(false).with(Face6::PY, true),
289 visible: true,
290 uniform_collision: None,
291 voxel_opacity_mask: VoxelOpacityMask::new_raw(
292 R16,
293 Vol::repeat(expected_bounds, OpacityCategory::Opaque)
294 ),
295 }
296 }
297 );
298 }
299
300 #[test]
301 fn move_voxel_block_evaluation() {
302 let mut universe = Universe::new();
303 let resolution = R2;
304 let color = rgba_const!(1.0, 0.0, 0.0, 1.0);
305 let original = Block::builder()
306 .voxels_fn(resolution, |_| Block::from(color))
307 .unwrap()
308 .build_into(&mut universe);
309
310 let moved = original.clone().with_modifier(Move {
311 direction: Face6::PY,
312 distance: 128, velocity: 0,
314 schedule: time::Schedule::EVERY_TICK,
315 });
316
317 let expected_bounds = GridAab::from_lower_size([0, 1, 0], [2, 1, 2]);
318
319 let ev_original = original.evaluate(universe.read_ticket()).unwrap();
320 assert_eq!(
321 moved.evaluate(universe.read_ticket()).unwrap(),
322 EvaluatedBlock {
323 block: moved,
324 attributes: ev_original.attributes.clone(),
325 cost: block::Cost {
326 components: ev_original.cost.components + 1,
327 voxels: 2u32.pow(3) * 3 / 2, recursion: 0
329 },
330 voxels: Evoxels::from_many(
331 resolution,
332 Vol::repeat(expected_bounds, Evoxel::from_block(&ev_original))
333 ),
334 derived: block::Derived {
335 color: color.to_rgb().with_alpha(zo32(2. / 3.)),
336 face_colors: FaceMap {
337 nx: color.to_rgb().with_alpha(zo32(0.5)),
338 ny: color.to_rgb().with_alpha(zo32(1.0)),
339 nz: color.to_rgb().with_alpha(zo32(0.5)),
340 px: color.to_rgb().with_alpha(zo32(0.5)),
341 py: color.to_rgb().with_alpha(zo32(1.0)),
342 pz: color.to_rgb().with_alpha(zo32(0.5)),
343 },
344 light_emission: Rgb::ZERO,
345 opaque: FaceMap::splat(false).with(Face6::PY, true),
346 visible: true,
347 uniform_collision: None,
348 voxel_opacity_mask: VoxelOpacityMask::new_raw(
349 resolution,
350 Vol::repeat(expected_bounds, OpacityCategory::Opaque)
351 ),
352 }
353 }
354 );
355 }
356
357 #[test]
360 fn move_also_quotes() {
361 let universe = Universe::new();
362 let original = Block::builder()
363 .color(Rgba::WHITE)
364 .tick_action(Some(TickAction::from(Operation::Become(AIR))))
365 .build();
366 let moved = original.with_modifier(Move {
367 direction: Face6::PY,
368 distance: 128,
369 velocity: 0,
370 schedule: time::Schedule::EVERY_TICK,
371 });
372
373 assert_eq!(
374 moved.evaluate(universe.read_ticket()).unwrap().attributes.tick_action,
375 None
376 );
377 }
378
379 fn move_block_test(
381 direction: Face6,
382 velocity: i16,
383 checker: impl FnOnce(space::Read<'_>, &Block),
384 ) {
385 let [block] = make_some_blocks();
386 let [move_out, move_in] = Move::new(direction, 0, velocity).into_paired();
387 let space = Space::builder(GridAab::from_lower_upper([-1, -1, -1], [2, 2, 2]))
388 .build_and_mutate(|m| {
389 m.set([0, 0, 0], block.clone().with_modifier(move_out))?;
390 m.set(
391 GridPoint::origin() + direction.normal_vector(),
392 block.clone().with_modifier(move_in),
393 )?;
394 Ok(())
395 })
396 .unwrap();
397 let mut universe = Universe::new();
398 let space = universe.insert("space".into(), space).unwrap();
399 for _ in 0..257 {
402 universe.step(false, time::Deadline::Whenever);
403 }
404 checker(space.read(universe.read_ticket()).unwrap(), &block);
405 }
406
407 #[test]
408 fn velocity_zero() {
409 move_block_test(Face6::PX, 0, |space, block| {
410 assert_eq!(&space[[0, 0, 0]], block);
411 assert_eq!(&space[[1, 0, 0]], &AIR);
412 });
413 }
414
415 #[test]
416 fn velocity_slow() {
417 move_block_test(Face6::PX, 1, |space, block| {
418 assert_eq!(&space[[0, 0, 0]], &AIR);
419 assert_eq!(&space[[1, 0, 0]], block);
420 });
421 }
422
423 #[test]
424 fn velocity_whole_cube_in_one_tick() {
425 move_block_test(Face6::PX, 256, |space, block| {
426 assert_eq!(&space[[0, 0, 0]], &AIR);
427 assert_eq!(&space[[1, 0, 0]], block);
428 });
429 }
430
431 #[test]
434 fn move_inside_rotation() {
435 let universe = Universe::new();
436 let [base] = make_some_blocks();
437 const R: Modifier = Modifier::Rotate(Face6::PY.clockwise());
438
439 let block = base
440 .clone()
441 .with_modifier(Move {
442 direction: Face6::PX,
443 distance: 10,
444 velocity: 10,
445 schedule: time::Schedule::EVERY_TICK,
446 })
447 .with_modifier(R);
448
449 let expected_after_tick = base
450 .with_modifier(Move {
451 direction: Face6::PX,
452 distance: 20,
453 velocity: 10,
454 schedule: time::Schedule::EVERY_TICK,
455 })
456 .with_modifier(R);
457
458 assert_eq!(
459 block.evaluate(universe.read_ticket()).unwrap().attributes.tick_action,
460 Some(TickAction::from(Operation::Become(expected_after_tick)))
461 );
462 }
463
464 #[test]
466 fn move_inside_composite_destination() {
467 let universe = Universe::new();
468 let [base, extra] = make_some_blocks();
469 let composite = Composite::new(extra, block::CompositeOperator::Over);
470
471 let block = base
472 .clone()
473 .with_modifier(Move {
474 direction: Face6::PX,
475 distance: 10,
476 velocity: 10,
477 schedule: time::Schedule::EVERY_TICK,
478 })
479 .with_modifier(composite.clone());
480
481 let expected_after_tick = base
482 .with_modifier(Move {
483 direction: Face6::PX,
484 distance: 20,
485 velocity: 10,
486 schedule: time::Schedule::EVERY_TICK,
487 })
488 .with_modifier(composite);
489
490 assert_eq!(
491 block.evaluate(universe.read_ticket()).unwrap().attributes.tick_action,
492 Some(TickAction::from(Operation::Become(expected_after_tick)))
493 );
494 }
495
496 #[test]
498 fn move_inside_composite_source() {
499 let universe = Universe::new();
500 let [base, extra] = make_some_blocks();
501
502 let block = extra.clone().with_modifier(Composite::new(
503 base.clone().with_modifier(Move {
504 direction: Face6::PX,
505 distance: 10,
506 velocity: 10,
507 schedule: time::Schedule::EVERY_TICK,
508 }),
509 block::CompositeOperator::Over,
510 ));
511
512 let expected_after_tick = extra.with_modifier(Composite::new(
513 base.with_modifier(Move {
514 direction: Face6::PX,
515 distance: 20,
516 velocity: 10,
517 schedule: time::Schedule::EVERY_TICK,
518 }),
519 block::CompositeOperator::Over,
520 ));
521
522 assert_eq!(
523 block.evaluate(universe.read_ticket()).unwrap().attributes.tick_action,
524 Some(TickAction::from(Operation::Become(expected_after_tick)))
525 );
526 }
527}