diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2025-01-10 16:45:27 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-10 16:45:27 -0600 |
| commit | 0d16f01571ec8315f3979eae46981e559ade1cf9 (patch) | |
| tree | ea43c32a57b0e6a67579d75a134dfbc009d09781 /azalea-physics/src | |
| parent | 615d8f9d2ac56b3244d328587243301da253eafd (diff) | |
| download | azalea-drasl-0d16f01571ec8315f3979eae46981e559ade1cf9.tar.xz | |
Fluid physics (#199)
* start implementing fluid physics
* Initial implementation of fluid pushing
* different travel function in water
* bubble columns
* jumping in water
* cleanup
* change ultrawarm to be required
* fix for clippy
Diffstat (limited to 'azalea-physics/src')
| -rw-r--r-- | azalea-physics/src/clip.rs | 154 | ||||
| -rwxr-xr-x | azalea-physics/src/collision/discrete_voxel_shape.rs | 36 | ||||
| -rw-r--r-- | azalea-physics/src/collision/mod.rs | 44 | ||||
| -rwxr-xr-x | azalea-physics/src/collision/shape.rs | 67 | ||||
| -rw-r--r-- | azalea-physics/src/collision/world_collisions.rs | 29 | ||||
| -rw-r--r-- | azalea-physics/src/fluids.rs | 274 | ||||
| -rw-r--r-- | azalea-physics/src/lib.rs | 731 | ||||
| -rw-r--r-- | azalea-physics/src/travel.rs | 299 |
8 files changed, 1033 insertions, 601 deletions
diff --git a/azalea-physics/src/clip.rs b/azalea-physics/src/clip.rs index 4a374f58..b6e6a9f9 100644 --- a/azalea-physics/src/clip.rs +++ b/azalea-physics/src/clip.rs @@ -1,7 +1,13 @@ -use azalea_block::{BlockState, FluidState}; +use std::collections::HashSet; + +use azalea_block::{ + fluid_state::{FluidKind, FluidState}, + BlockState, +}; use azalea_core::{ + aabb::AABB, block_hit_result::BlockHitResult, - direction::Direction, + direction::{Axis, Direction}, math::{self, lerp, EPSILON}, position::{BlockPos, Vec3}, }; @@ -80,8 +86,8 @@ impl FluidPickType { match self { Self::None => false, Self::SourceOnly => fluid_state.amount == 8, - Self::Any => fluid_state.fluid != azalea_registry::Fluid::Empty, - Self::Water => fluid_state.fluid == azalea_registry::Fluid::Water, + Self::Any => fluid_state.kind != FluidKind::Empty, + Self::Water => fluid_state.kind == FluidKind::Water, } } } @@ -198,22 +204,10 @@ pub fn traverse_blocks<C, T>( let vec = right_after_end - right_before_start; - /// Returns either -1, 0, or 1, depending on whether the number is negative, - /// zero, or positive. - /// - /// This function exists because f64::signum doesn't check for 0. - fn get_number_sign(num: f64) -> f64 { - if num == 0. { - 0. - } else { - num.signum() - } - } - let vec_sign = Vec3 { - x: get_number_sign(vec.x), - y: get_number_sign(vec.y), - z: get_number_sign(vec.z), + x: math::sign(vec.x), + y: math::sign(vec.y), + z: math::sign(vec.z), }; #[rustfmt::skip] @@ -270,3 +264,125 @@ pub fn traverse_blocks<C, T>( } } } + +pub fn box_traverse_blocks(from: &Vec3, to: &Vec3, aabb: &AABB) -> HashSet<BlockPos> { + let delta = to - from; + let traversed_blocks = BlockPos::between_closed_aabb(aabb); + if delta.length_squared() < (0.99999_f32 * 0.99999) as f64 { + return traversed_blocks.into_iter().collect(); + } + + let mut traversed_and_collided_blocks = HashSet::new(); + let target_min_pos = aabb.min; + let from_min_pos = target_min_pos - delta; + add_collisions_along_travel( + &mut traversed_and_collided_blocks, + from_min_pos, + target_min_pos, + *aabb, + ); + traversed_and_collided_blocks.extend(traversed_blocks); + traversed_and_collided_blocks +} + +pub fn add_collisions_along_travel( + collisions: &mut HashSet<BlockPos>, + from: Vec3, + to: Vec3, + aabb: AABB, +) { + let delta = to - from; + let mut min_x = from.x.floor() as i32; + let mut min_y = from.y.floor() as i32; + let mut min_z = from.z.floor() as i32; + let direction_x = math::sign_as_int(delta.x); + let direction_y = math::sign_as_int(delta.y); + let direction_z = math::sign_as_int(delta.z); + let step_x = if direction_x == 0 { + f64::MAX + } else { + direction_x as f64 / delta.x + }; + let step_y = if direction_y == 0 { + f64::MAX + } else { + direction_y as f64 / delta.y + }; + let step_z = if direction_z == 0 { + f64::MAX + } else { + direction_z as f64 / delta.z + }; + let mut cur_x = step_x + * if direction_x > 0 { + 1. - math::fract(from.x) + } else { + math::fract(from.x) + }; + let mut cur_y = step_y + * if direction_y > 0 { + 1. - math::fract(from.y) + } else { + math::fract(from.y) + }; + let mut cur_z = step_z + * if direction_z > 0 { + 1. - math::fract(from.z) + } else { + math::fract(from.z) + }; + let mut step_count = 0; + + while cur_x <= 1. || cur_y <= 1. || cur_z <= 1. { + if cur_x < cur_y { + if cur_x < cur_z { + min_x += direction_x; + cur_x += step_x; + } else { + min_z += direction_z; + cur_z += step_z; + } + } else if cur_y < cur_z { + min_y += direction_y; + cur_y += step_y; + } else { + min_z += direction_z; + cur_z += step_z; + } + + if step_count > 16 { + break; + } + step_count += 1; + + let Some(clip_location) = AABB::clip_with_from_and_to( + &Vec3::new(min_x as f64, min_y as f64, min_z as f64), + &Vec3::new((min_x + 1) as f64, (min_y + 1) as f64, (min_z + 1) as f64), + &from, + &to, + ) else { + continue; + }; + + let initial_max_x = clip_location + .x + .clamp(min_x as f64 + 1.0E-5, min_x as f64 + 1.0 - 1.0E-5); + let initial_max_y = clip_location + .y + .clamp(min_y as f64 + 1.0E-5, min_y as f64 + 1.0 - 1.0E-5); + let initial_max_z = clip_location + .z + .clamp(min_z as f64 + 1.0E-5, min_z as f64 + 1.0 - 1.0E-5); + let max_x = (initial_max_x + aabb.get_size(Axis::X)).floor() as i32; + let max_y = (initial_max_y + aabb.get_size(Axis::Y)).floor() as i32; + let max_z = (initial_max_z + aabb.get_size(Axis::Z)).floor() as i32; + + for x in min_x..=max_x { + for y in min_y..=max_y { + for z in min_z..=max_z { + collisions.insert(BlockPos::new(x, y, z)); + } + } + } + } +} diff --git a/azalea-physics/src/collision/discrete_voxel_shape.rs b/azalea-physics/src/collision/discrete_voxel_shape.rs index 211e6303..cacbc987 100755 --- a/azalea-physics/src/collision/discrete_voxel_shape.rs +++ b/azalea-physics/src/collision/discrete_voxel_shape.rs @@ -238,39 +238,33 @@ impl BitSetDiscreteVoxelShape { var2: bool, ) { let mut var3 = BitSetDiscreteVoxelShape::from(var0); - for var4 in 0..var3.y_size { - for var5 in 0..var3.x_size { + for y in 0..var3.y_size { + for x in 0..var3.x_size { let mut var6 = None; - for var7 in 0..=var3.z_size { - if var3.is_full_wide(var5, var4, var7) { + for z in 0..=var3.z_size { + if var3.is_full_wide(x, y, z) { if var2 { if var6.is_none() { - var6 = Some(var7); + var6 = Some(z); } } else { - consumer(var5, var4, var7, var5 + 1, var4 + 1, var7 + 1); + consumer(x, y, z, x + 1, y + 1, z + 1); } } else if var6.is_some() { - let mut var8 = var5; - let mut var9 = var4; - var3.clear_z_strip(var6.unwrap(), var7, var5, var4); - while var3.is_z_strip_full(var6.unwrap(), var7, var8 + 1, var4) { - var3.clear_z_strip(var6.unwrap(), var7, var8 + 1, var4); + let mut var8 = x; + let mut var9 = y; + var3.clear_z_strip(var6.unwrap(), z, x, y); + while var3.is_z_strip_full(var6.unwrap(), z, var8 + 1, y) { + var3.clear_z_strip(var6.unwrap(), z, var8 + 1, y); var8 += 1; } - while var3.is_xz_rectangle_full( - var5, - var8 + 1, - var6.unwrap(), - var7, - var9 + 1, - ) { - for var10 in var5..=var8 { - var3.clear_z_strip(var6.unwrap(), var7, var10, var9 + 1); + while var3.is_xz_rectangle_full(x, var8 + 1, var6.unwrap(), z, var9 + 1) { + for var10 in x..=var8 { + var3.clear_z_strip(var6.unwrap(), z, var10, var9 + 1); } var9 += 1; } - consumer(var5, var4, var6.unwrap(), var8 + 1, var9 + 1, var7); + consumer(x, y, var6.unwrap(), var8 + 1, var9 + 1, z); var6 = None; } } diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index 39fc43f8..530aa47f 100644 --- a/azalea-physics/src/collision/mod.rs +++ b/azalea-physics/src/collision/mod.rs @@ -6,7 +6,7 @@ mod world_collisions; use std::{ops::Add, sync::LazyLock}; -use azalea_block::FluidState; +use azalea_block::{fluid_state::FluidState, BlockState}; use azalea_core::{ aabb::AABB, direction::Axis, @@ -22,6 +22,7 @@ use tracing::warn; use self::world_collisions::get_block_collisions; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MoverType { Own, Player, @@ -111,7 +112,7 @@ fn collide(movement: &Vec3, world: &Instance, physics: &azalea_entity::Physics) y: 0., z: movement.z, }, - &entity_bounding_box.move_relative(&directly_up_delta), + &entity_bounding_box.move_relative(directly_up_delta), world, entity_collisions.clone(), ) @@ -132,7 +133,7 @@ fn collide(movement: &Vec3, world: &Instance, physics: &azalea_entity::Physics) y: -step_to_delta.y + movement.y, z: 0., }, - &entity_bounding_box.move_relative(&step_to_delta), + &entity_bounding_box.move_relative(step_to_delta), world, entity_collisions.clone(), )); @@ -143,8 +144,10 @@ fn collide(movement: &Vec3, world: &Instance, physics: &azalea_entity::Physics) } /// Move an entity by a given delta, checking for collisions. +/// +/// In Mojmap, this is `Entity.move`. pub fn move_colliding( - _mover_type: &MoverType, + _mover_type: MoverType, movement: &Vec3, world: &Instance, position: &mut Mut<azalea_entity::Position>, @@ -296,7 +299,7 @@ fn collide_with_shapes( if y_movement != 0. { y_movement = Shapes::collide(&Axis::Y, &entity_box, collision_boxes, y_movement); if y_movement != 0. { - entity_box = entity_box.move_relative(&Vec3 { + entity_box = entity_box.move_relative(Vec3 { x: 0., y: y_movement, z: 0., @@ -311,7 +314,7 @@ fn collide_with_shapes( if more_z_movement && z_movement != 0. { z_movement = Shapes::collide(&Axis::Z, &entity_box, collision_boxes, z_movement); if z_movement != 0. { - entity_box = entity_box.move_relative(&Vec3 { + entity_box = entity_box.move_relative(Vec3 { x: 0., y: 0., z: z_movement, @@ -322,7 +325,7 @@ fn collide_with_shapes( if x_movement != 0. { x_movement = Shapes::collide(&Axis::X, &entity_box, collision_boxes, x_movement); if x_movement != 0. { - entity_box = entity_box.move_relative(&Vec3 { + entity_box = entity_box.move_relative(Vec3 { x: x_movement, y: 0., z: 0., @@ -352,7 +355,7 @@ pub fn fluid_shape( ) -> &'static VoxelShape { if fluid.amount == 9 { let fluid_state_above = world.get_fluid_state(&pos.up(1)).unwrap_or_default(); - if fluid_state_above.fluid == fluid.fluid { + if fluid_state_above.kind == fluid.kind { return &BLOCK_SHAPE; } } @@ -384,3 +387,28 @@ pub fn fluid_shape( fn calculate_shape_for_fluid(amount: u8) -> VoxelShape { box_shape(0.0, 0.0, 0.0, 1.0, (f32::from(amount) / 9.0) as f64, 1.0) } + +/// Whether the block is treated as "motion blocking". +/// +/// This is marked as deprecated in Minecraft. +pub fn legacy_blocks_motion(block: BlockState) -> bool { + let registry_block = azalea_registry::Block::from(block); + legacy_calculate_solid(block) + && registry_block != azalea_registry::Block::Cobweb + && registry_block != azalea_registry::Block::BambooSapling +} + +pub fn legacy_calculate_solid(block: BlockState) -> bool { + // force_solid has to be checked before anything else + let block_trait = Box::<dyn azalea_block::Block>::from(block); + if let Some(solid) = block_trait.behavior().force_solid { + return solid; + } + + let shape = block.collision_shape(); + if shape.is_empty() { + return false; + } + let bounds = shape.bounds(); + bounds.size() >= 0.7291666666666666 || bounds.get_size(Axis::Y) >= 1.0 +} diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs index fb733cae..9d870498 100755 --- a/azalea-physics/src/collision/shape.rs +++ b/azalea-physics/src/collision/shape.rs @@ -381,16 +381,25 @@ impl VoxelShape { } #[must_use] - pub fn move_relative(&self, x: f64, y: f64, z: f64) -> VoxelShape { + pub fn move_relative(&self, delta: Vec3) -> VoxelShape { if self.shape().is_empty() { return EMPTY_SHAPE.clone(); } VoxelShape::Array(ArrayVoxelShape::new( self.shape().to_owned(), - self.get_coords(Axis::X).iter().map(|c| c + x).collect(), - self.get_coords(Axis::Y).iter().map(|c| c + y).collect(), - self.get_coords(Axis::Z).iter().map(|c| c + z).collect(), + self.get_coords(Axis::X) + .iter() + .map(|c| c + delta.x) + .collect(), + self.get_coords(Axis::Y) + .iter() + .map(|c| c + delta.y) + .collect(), + self.get_coords(Axis::Z) + .iter() + .map(|c| c + delta.z) + .collect(), )) } @@ -526,13 +535,6 @@ impl VoxelShape { movement } - // public VoxelShape optimize() { - // VoxelShape[] var1 = new VoxelShape[]{Shapes.empty()}; - // this.forAllBoxes((var1x, var3, var5, var7, var9, var11) -> { - // var1[0] = Shapes.joinUnoptimized(var1[0], Shapes.box(var1x, var3, - // var5, var7, var9, var11), BooleanOp.OR); }); - // return var1[0]; - // } fn optimize(&self) -> VoxelShape { let mut shape = EMPTY_SHAPE.clone(); self.for_all_boxes(|var1x, var3, var5, var7, var9, var11| { @@ -545,35 +547,10 @@ impl VoxelShape { shape } - // public void forAllBoxes(Shapes.DoubleLineConsumer var1) { - // DoubleList var2 = this.getCoords(Direction.Axis.X); - // DoubleList var3 = this.getCoords(Direction.Axis.Y); - // DoubleList var4 = this.getCoords(Direction.Axis.Z); - // this.shape.forAllBoxes((var4x, var5, var6, var7, var8, var9) -> { - // var1.consume(var2.getDouble(var4x), var3.getDouble(var5), - // var4.getDouble(var6), var2.getDouble(var7), var3.getDouble(var8), - // var4.getDouble(var9)); }, true); - // } pub fn for_all_boxes(&self, mut consumer: impl FnMut(f64, f64, f64, f64, f64, f64)) where Self: Sized, { - // let x_coords = self.get_coords(Axis::X); - // let y_coords = self.get_coords(Axis::Y); - // let z_coords = self.get_coords(Axis::Z); - // self.shape().for_all_boxes( - // |var4x, var5, var6, var7, var8, var9| { - // consumer( - // x_coords[var4x as usize], - // y_coords[var5 as usize], - // z_coords[var6 as usize], - // x_coords[var7 as usize], - // y_coords[var8 as usize], - // z_coords[var9 as usize], - // ) - // }, - // true, - // ); let x_coords = self.get_coords(Axis::X); let y_coords = self.get_coords(Axis::Y); let z_coords = self.get_coords(Axis::Z); @@ -596,22 +573,26 @@ impl VoxelShape { let mut aabbs = Vec::new(); self.for_all_boxes(|min_x, min_y, min_z, max_x, max_y, max_z| { aabbs.push(AABB { - min_x, - min_y, - min_z, - max_x, - max_y, - max_z, + min: Vec3::new(min_x, min_y, min_z), + max: Vec3::new(max_x, max_y, max_z), }); }); aabbs } + + pub fn bounds(&self) -> AABB { + assert!(!self.is_empty(), "Can't get bounds for empty shape"); + AABB { + min: Vec3::new(self.min(Axis::X), self.min(Axis::Y), self.min(Axis::Z)), + max: Vec3::new(self.max(Axis::X), self.max(Axis::Y), self.max(Axis::Z)), + } + } } impl From<AABB> for VoxelShape { fn from(aabb: AABB) -> Self { box_shape_unchecked( - aabb.min_x, aabb.min_y, aabb.min_z, aabb.max_x, aabb.max_y, aabb.max_z, + aabb.min.x, aabb.min.y, aabb.min.z, aabb.max.x, aabb.max.y, aabb.max.z, ) } } diff --git a/azalea-physics/src/collision/world_collisions.rs b/azalea-physics/src/collision/world_collisions.rs index 36488777..f0b41986 100644 --- a/azalea-physics/src/collision/world_collisions.rs +++ b/azalea-physics/src/collision/world_collisions.rs @@ -49,28 +49,19 @@ pub fn get_block_collisions(world: &Instance, aabb: AABB) -> Vec<VoxelShape> { // if it's a full block do a faster collision check if block_state.is_collision_shape_full() { if !state.aabb.intersects_aabb(&AABB { - min_x: item.pos.x as f64, - min_y: item.pos.y as f64, - min_z: item.pos.z as f64, - max_x: (item.pos.x + 1) as f64, - max_y: (item.pos.y + 1) as f64, - max_z: (item.pos.z + 1) as f64, + min: item.pos.to_vec3_floored(), + max: (item.pos + 1).to_vec3_floored(), }) { continue; } - block_collisions.push(BLOCK_SHAPE.move_relative( - item.pos.x as f64, - item.pos.y as f64, - item.pos.z as f64, - )); + block_collisions.push(BLOCK_SHAPE.move_relative(item.pos.to_vec3_floored())); continue; } let block_shape = state.get_block_shape(block_state); - let block_shape = - block_shape.move_relative(item.pos.x as f64, item.pos.y as f64, item.pos.z as f64); + let block_shape = block_shape.move_relative(item.pos.to_vec3_floored()); // if the entity shape and block shape don't collide, continue if !Shapes::matches_anywhere(&block_shape, &state.entity_shape, |a, b| a && b) { continue; @@ -95,15 +86,15 @@ pub struct BlockCollisionsState<'a> { impl<'a> BlockCollisionsState<'a> { pub fn new(world: &'a Instance, aabb: AABB) -> Self { let origin = BlockPos { - x: (aabb.min_x - EPSILON).floor() as i32 - 1, - y: (aabb.min_y - EPSILON).floor() as i32 - 1, - z: (aabb.min_z - EPSILON).floor() as i32 - 1, + x: (aabb.min.x - EPSILON).floor() as i32 - 1, + y: (aabb.min.y - EPSILON).floor() as i32 - 1, + z: (aabb.min.z - EPSILON).floor() as i32 - 1, }; let end = BlockPos { - x: (aabb.max_x + EPSILON).floor() as i32 + 1, - y: (aabb.max_y + EPSILON).floor() as i32 + 1, - z: (aabb.max_z + EPSILON).floor() as i32 + 1, + x: (aabb.max.x + EPSILON).floor() as i32 + 1, + y: (aabb.max.y + EPSILON).floor() as i32 + 1, + z: (aabb.max.z + EPSILON).floor() as i32 + 1, }; let cursor = Cursor3d::new(origin, end); diff --git a/azalea-physics/src/fluids.rs b/azalea-physics/src/fluids.rs new file mode 100644 index 00000000..eca4266e --- /dev/null +++ b/azalea-physics/src/fluids.rs @@ -0,0 +1,274 @@ +use azalea_block::{ + fluid_state::{FluidKind, FluidState}, + BlockState, +}; +use azalea_core::{ + direction::Direction, + position::{BlockPos, Vec3}, +}; +use azalea_entity::{InLoadedChunk, LocalEntity, Physics, Position}; +use azalea_world::{Instance, InstanceContainer, InstanceName}; +use bevy_ecs::prelude::*; + +use crate::collision::legacy_blocks_motion; + +#[allow(clippy::type_complexity)] +pub fn update_in_water_state_and_do_fluid_pushing( + mut query: Query< + (&mut Physics, &Position, &InstanceName), + (With<LocalEntity>, With<InLoadedChunk>), + >, + instance_container: Res<InstanceContainer>, +) { + for (mut physics, position, instance_name) in &mut query { + let world_lock = instance_container + .get(instance_name) + .expect("All entities should be in a valid world"); + let world = world_lock.read(); + + physics.water_fluid_height = 0.; + physics.lava_fluid_height = 0.; + + update_in_water_state_and_do_water_current_pushing(&mut physics, &world, position); + + let is_ultrawarm = world + .registries + .dimension_type() + .and_then(|d| d.map.get(instance_name).map(|d| d.ultrawarm)) + == Some(true); + let lava_push_factor = if is_ultrawarm { + 0.007 + } else { + 0.0023333333333333335 + }; + + update_fluid_height_and_do_fluid_pushing( + &mut physics, + &world, + FluidKind::Lava, + lava_push_factor, + ); + } +} +fn update_in_water_state_and_do_water_current_pushing( + physics: &mut Physics, + world: &Instance, + _position: &Position, +) { + // TODO: implement vehicles and boats + // if vehicle == AbstractBoat { + // if !boat.is_underwater() { + // *was_touching_water = false; + // } + // } + + // updateFluidHeightAndDoFluidPushing + if update_fluid_height_and_do_fluid_pushing(physics, world, FluidKind::Water, 0.014) { + // if !was_touching_water && !first_tick { + // do_water_splash_effect(); + // } + + physics.reset_fall_distance(); + physics.was_touching_water = true; + physics.clear_fire(); + } else { + physics.was_touching_water = false; + } +} + +fn update_fluid_height_and_do_fluid_pushing( + physics: &mut Physics, + world: &Instance, + checking_fluid: FluidKind, + fluid_push_factor: f64, +) -> bool { + // if touching_unloaded_chunk() { + // return false; + // } + + let checking_liquids_aabb = physics.bounding_box.deflate_all(0.001); + + let min_x = checking_liquids_aabb.min.x.floor() as i32; + let min_y = checking_liquids_aabb.min.y.floor() as i32; + let min_z = checking_liquids_aabb.min.z.floor() as i32; + + let max_x = checking_liquids_aabb.max.x.ceil() as i32; + let max_y = checking_liquids_aabb.max.y.ceil() as i32; + let max_z = checking_liquids_aabb.max.z.ceil() as i32; + + let mut min_height_touching = 0.; + let is_entity_pushable_by_fluid = true; + let mut touching_fluid = false; + let mut additional_player_delta = Vec3::default(); + let mut num_fluids_being_touched = 0; + + for cur_x in min_x..=max_x { + for cur_y in min_y..=max_y { + for cur_z in min_z..=max_z { + let cur_pos = BlockPos::new(cur_x, cur_y, cur_z); + let Some(fluid_at_cur_pos) = world.get_fluid_state(&cur_pos) else { + continue; + }; + if fluid_at_cur_pos.kind != checking_fluid { + continue; + } + let fluid_max_y = (cur_y as f32 + fluid_at_cur_pos.height()) as f64; + if fluid_max_y < checking_liquids_aabb.min.y { + continue; + } + touching_fluid = true; + min_height_touching = f64::max( + fluid_max_y - checking_liquids_aabb.min.y, + min_height_touching, + ); + if !is_entity_pushable_by_fluid { + continue; + } + let mut additional_player_delta_for_fluid = + get_fluid_flow(&fluid_at_cur_pos, world, cur_pos); + if min_height_touching < 0.4 { + additional_player_delta_for_fluid *= min_height_touching; + }; + + additional_player_delta += additional_player_delta_for_fluid; + num_fluids_being_touched += 1; + } + } + } + + if additional_player_delta.length() > 0. { + additional_player_delta /= num_fluids_being_touched as f64; + + // if entity_kind != EntityKind::Player { + // additional_player_delta = additional_player_delta.normalize(); + // } + + let player_delta = physics.velocity; + additional_player_delta *= fluid_push_factor; + const MIN_PUSH: f64 = 0.003; + const MIN_PUSH_LENGTH: f64 = MIN_PUSH * 1.5; + + if player_delta.x.abs() < MIN_PUSH + && player_delta.z.abs() < MIN_PUSH + && additional_player_delta.length() < MIN_PUSH_LENGTH + { + additional_player_delta = additional_player_delta.normalize() * MIN_PUSH_LENGTH; + } + + physics.velocity += additional_player_delta; + } + + match checking_fluid { + FluidKind::Water => physics.water_fluid_height = min_height_touching, + FluidKind::Lava => physics.lava_fluid_height = min_height_touching, + FluidKind::Empty => panic!("FluidKind::Empty should not be passed to update_fluid_height"), + }; + + touching_fluid +} + +pub fn update_swimming() { + // TODO: swimming +} + +// FlowingFluid.getFlow +pub fn get_fluid_flow(fluid: &FluidState, world: &Instance, pos: BlockPos) -> Vec3 { + let mut z_flow: f64 = 0.; + let mut x_flow: f64 = 0.; + + for direction in Direction::HORIZONTAL { + let adjacent_block_pos = pos.offset_with_direction(direction); + let adjacent_fluid_state = world + .get_fluid_state(&adjacent_block_pos) + .unwrap_or_default(); + if fluid.affects_flow(&adjacent_fluid_state) { + let mut adjacent_fluid_height = adjacent_fluid_state.height(); + let mut adjacent_height_difference: f32 = 0.; + + if adjacent_fluid_height == 0. { + if !legacy_blocks_motion( + world + .get_block_state(&adjacent_block_pos) + .unwrap_or_default(), + ) { + let block_pos_below_adjacent = adjacent_block_pos.down(1); + let fluid_below_adjacent = world + .get_fluid_state(&block_pos_below_adjacent) + .unwrap_or_default(); + + if fluid.affects_flow(&fluid_below_adjacent) { + adjacent_fluid_height = fluid_below_adjacent.height(); + if adjacent_fluid_height > 0. { + adjacent_height_difference = + fluid.height() - (adjacent_fluid_height - 0.8888889); + } + } + } + } else if adjacent_fluid_height > 0. { + adjacent_height_difference = fluid.height() - adjacent_fluid_height; + } + + if adjacent_height_difference != 0. { + x_flow += (direction.x() as f32 * adjacent_height_difference) as f64; + z_flow += (direction.z() as f32 * adjacent_height_difference) as f64; + } + } + } + + let mut flow = Vec3::new(x_flow, 0., z_flow); + if fluid.falling { + for direction in Direction::HORIZONTAL { + let adjacent_block_pos = pos.offset_with_direction(direction); + if is_solid_face(fluid, world, adjacent_block_pos, direction) + || is_solid_face(fluid, world, adjacent_block_pos.up(1), direction) + { + flow = flow.normalize() + Vec3::new(0., -6., 0.); + break; + } + } + } + + flow.normalize() +} + +// i don't really get what this is for +fn is_solid_face( + fluid: &FluidState, + world: &Instance, + adjacent_pos: BlockPos, + direction: Direction, +) -> bool { + let block_state = world.get_block_state(&adjacent_pos).unwrap_or_default(); + let fluid_state = world.get_fluid_state(&adjacent_pos).unwrap_or_default(); + if fluid_state.is_same_kind(fluid) { + return false; + } + if direction == Direction::Up { + return true; + } + let registry_block = azalea_registry::Block::from(block_state); + if matches!( + registry_block, + // frosted ice is from frost walker + azalea_registry::Block::Ice | azalea_registry::Block::FrostedIce + ) { + return false; + } + is_face_sturdy(block_state, world, adjacent_pos, direction) +} + +fn is_face_sturdy( + _block_state: BlockState, + _world: &Instance, + _pos: BlockPos, + _direction: Direction, +) -> bool { + // TODO: this does a whole bunch of physics shape checks for waterlogged blocks + // that i honestly cannot be bothered to implement right now + + // see BlockBehavior.isFaceSturdy in the decompiled minecraft source + + // also, this probably should be in a module other than fluids.rs + + false +} diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs index 2ca64b1f..6ea4e946 100644 --- a/azalea-physics/src/lib.rs +++ b/azalea-physics/src/lib.rs @@ -3,8 +3,12 @@ pub mod clip; pub mod collision; +pub mod fluids; +pub mod travel; -use azalea_block::{Block, BlockState}; +use std::collections::HashSet; + +use azalea_block::{fluid_state::FluidState, properties, Block, BlockState}; use azalea_core::{ math, position::{BlockPos, Vec3}, @@ -22,7 +26,8 @@ use bevy_ecs::{ system::{Query, Res}, world::Mut, }; -use collision::{move_colliding, MoverType}; +use clip::box_traverse_blocks; +use collision::{move_colliding, BlockWithShape, MoverType, VoxelShape, BLOCK_SHAPE}; /// A Bevy [`SystemSet`] for running physics that makes entities do things. #[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] @@ -33,7 +38,15 @@ impl Plugin for PhysicsPlugin { fn build(&self, app: &mut App) { app.add_systems( GameTick, - (ai_step, travel) + ( + fluids::update_in_water_state_and_do_fluid_pushing + .before(azalea_entity::update_fluid_on_eyes), + update_old_position, + fluids::update_swimming.after(azalea_entity::update_fluid_on_eyes), + ai_step, + travel::travel, + apply_effects_from_blocks, + ) .chain() .in_set(PhysicsSet) .after(azalea_entity::update_in_loaded_chunk), @@ -41,110 +54,9 @@ impl Plugin for PhysicsPlugin { } } -/// Move the entity with the given acceleration while handling friction, -/// gravity, collisions, and some other stuff. -#[allow(clippy::type_complexity)] -fn travel( - mut query: Query< - ( - &mut Physics, - &mut LookDirection, - &mut Position, - Option<&Sprinting>, - Option<&Pose>, - &Attributes, - &InstanceName, - &OnClimbable, - &Jumping, - ), - (With<LocalEntity>, With<InLoadedChunk>), - >, - instance_container: Res<InstanceContainer>, -) { - for ( - mut physics, - direction, - position, - sprinting, - pose, - attributes, - world_name, - on_climbable, - jumping, - ) in &mut query - { - let Some(world_lock) = instance_container.get(world_name) else { - continue; - }; - let world = world_lock.read(); - // if !self.is_effective_ai() && !self.is_controlled_by_local_instance() { - // // this.calculateEntityAnimation(this, this instanceof FlyingAnimal); - // return; - // } - - let gravity: f64 = 0.08; - - // TODO: slow falling effect - // let is_falling = self.delta.y <= 0.; - - // TODO: fluids - - // TODO: elytra - - let block_pos_below = get_block_pos_below_that_affects_movement(&position); - - let block_state_below = world - .chunks - .get_block_state(&block_pos_below) - .unwrap_or(BlockState::AIR); - let block_below: Box<dyn Block> = block_state_below.into(); - let block_friction = block_below.behavior().friction; - - let inertia = if physics.on_ground() { - block_friction * 0.91 - } else { - 0.91 - }; - - // this applies the current delta - let mut movement = handle_relative_friction_and_calculate_movement( - HandleRelativeFrictionAndCalculateMovementOpts { - block_friction, - world: &world, - physics: &mut physics, - direction: &direction, - position, - attributes, - is_sprinting: sprinting.map(|s| **s).unwrap_or(false), - on_climbable, - pose, - jumping, - }, - ); - - movement.y -= gravity; - - // if (this.shouldDiscardFriction()) { - // this.setDeltaMovement(movement.x, yMovement, movement.z); - // } else { - // this.setDeltaMovement(movement.x * (double)inertia, yMovement * - // 0.9800000190734863D, movement.z * (double)inertia); } - - // if should_discard_friction(self) { - if false { - physics.velocity = movement; - } else { - physics.velocity = Vec3 { - x: movement.x * inertia as f64, - y: movement.y * 0.9800000190734863f64, - z: movement.z * inertia as f64, - }; - } - } -} - -/// applies air resistance, calls self.travel(), and some other random -/// stuff. +/// Applies air resistance and handles jumping. +/// +/// Happens before [`travel::travel`]. #[allow(clippy::type_complexity)] pub fn ai_step( mut query: Query< @@ -164,6 +76,10 @@ pub fn ai_step( // vanilla does movement interpolation here, doesn't really matter much for a // bot though + if physics.no_jump_delay > 0 { + physics.no_jump_delay -= 1; + } + if physics.velocity.x.abs() < 0.003 { physics.velocity.x = 0.; } @@ -178,27 +94,221 @@ pub fn ai_step( if **jumping { // TODO: jumping in liquids and jump delay - if physics.on_ground() { - jump_from_ground( - &mut physics, - position, - look_direction, - sprinting, - instance_name, - &instance_container, - ) + let fluid_height = if physics.is_in_lava() { + physics.lava_fluid_height + } else if physics.is_in_water() { + physics.water_fluid_height + } else { + 0. + }; + + let in_water = physics.is_in_water() && fluid_height > 0.; + let fluid_jump_threshold = travel::fluid_jump_threshold(); + + if !in_water || physics.on_ground() && fluid_height <= fluid_jump_threshold { + if !physics.is_in_lava() + || physics.on_ground() && fluid_height <= fluid_jump_threshold + { + if physics.on_ground() + || in_water + && fluid_height <= fluid_jump_threshold + && physics.no_jump_delay == 0 + { + jump_from_ground( + &mut physics, + position, + look_direction, + sprinting, + instance_name, + &instance_container, + ); + physics.no_jump_delay = 10; + } + } else { + jump_in_liquid(&mut physics); + } + } else { + jump_in_liquid(&mut physics); } } + } else { + physics.no_jump_delay = 0; } - physics.xxa *= 0.98; - physics.zza *= 0.98; + physics.x_acceleration *= 0.98; + physics.z_acceleration *= 0.98; // TODO: freezing, pushEntities, drowning damage (in their own systems, // after `travel`) } } +fn jump_in_liquid(physics: &mut Physics) { + physics.velocity.y += 0.04; +} + +// in minecraft, this is done as part of aiStep immediately after travel +#[allow(clippy::type_complexity)] +pub fn apply_effects_from_blocks( + mut query: Query< + (&mut Physics, &Position, &InstanceName), + (With<LocalEntity>, With<InLoadedChunk>), + >, + instance_container: Res<InstanceContainer>, +) { + for (mut physics, position, world_name) in &mut query { + let Some(world_lock) = instance_container.get(world_name) else { + continue; + }; + let world = world_lock.read(); + + // if !is_affected_by_blocks { + // continue + // } + + // if (this.onGround()) { + // BlockPos var3 = this.getOnPosLegacy(); + // BlockState var4 = this.level().getBlockState(var3); + // var4.getBlock().stepOn(this.level(), var3, var4, this); + // } + + // minecraft adds more entries to the list when the code is running on the + // server + let movement_this_tick = [EntityMovement { + from: physics.old_position, + to: **position, + }]; + + check_inside_blocks(&mut physics, &world, &movement_this_tick); + } +} + +fn check_inside_blocks( + physics: &mut Physics, + world: &Instance, + movements: &[EntityMovement], +) -> Vec<BlockState> { + let mut blocks_inside = Vec::new(); + let mut visited_blocks = HashSet::<BlockState>::new(); + + for movement in movements { + let bounding_box_at_target = physics + .dimensions + .make_bounding_box(&movement.to) + .deflate_all(1.0E-5); + + for traversed_block in + box_traverse_blocks(&movement.from, &movement.to, &bounding_box_at_target) + { + // if (!this.isAlive()) { + // return; + // } + + let traversed_block_state = world.get_block_state(&traversed_block).unwrap_or_default(); + if traversed_block_state.is_air() { + continue; + } + if !visited_blocks.insert(traversed_block_state) { + continue; + } + + /* + VoxelShape var12 = traversedBlockState.getEntityInsideCollisionShape(this.level(), traversedBlock); + if (var12 != Shapes.block() && !this.collidedWithShapeMovingFrom(from, to, traversedBlock, var12)) { + continue; + } + + traversedBlockState.entityInside(this.level(), traversedBlock, this); + this.onInsideBlock(traversedBlockState); + */ + + // this is different for end portal frames and tripwire hooks, i don't think it + // actually matters for a client though + let entity_inside_collision_shape = &*BLOCK_SHAPE; + + if entity_inside_collision_shape != &*BLOCK_SHAPE + && !collided_with_shape_moving_from( + &movement.from, + &movement.to, + traversed_block, + entity_inside_collision_shape, + physics, + ) + { + continue; + } + + handle_entity_inside_block(world, traversed_block_state, traversed_block, physics); + + blocks_inside.push(traversed_block_state); + } + } + + blocks_inside +} + +fn collided_with_shape_moving_from( + from: &Vec3, + to: &Vec3, + traversed_block: BlockPos, + entity_inside_collision_shape: &VoxelShape, + physics: &Physics, +) -> bool { + let bounding_box_from = physics.dimensions.make_bounding_box(from); + let delta = to - from; + bounding_box_from.collided_along_vector( + delta, + &entity_inside_collision_shape + .move_relative(traversed_block.to_vec3_floored()) + .to_aabbs(), + ) +} + +// BlockBehavior.entityInside +fn handle_entity_inside_block( + world: &Instance, + block: BlockState, + block_pos: BlockPos, + physics: &mut Physics, +) { + let registry_block = azalea_registry::Block::from(block); + #[allow(clippy::single_match)] + match registry_block { + azalea_registry::Block::BubbleColumn => { + let block_above = world.get_block_state(&block_pos.up(1)).unwrap_or_default(); + let is_block_above_empty = + block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty(); + let drag_down = block + .property::<properties::Drag>() + .expect("drag property should always be present on bubble columns"); + let velocity = &mut physics.velocity; + + if is_block_above_empty { + let new_y = if drag_down { + f64::max(-0.9, velocity.y - 0.03) + } else { + f64::min(1.8, velocity.y + 0.1) + }; + velocity.y = new_y; + } else { + let new_y = if drag_down { + f64::max(-0.3, velocity.y - 0.03) + } else { + f64::min(0.7, velocity.y + 0.06) + }; + velocity.y = new_y; + physics.reset_fall_distance(); + } + } + _ => {} + } +} + +pub struct EntityMovement { + pub from: Vec3, + pub to: Vec3, +} + pub fn jump_from_ground( physics: &mut Physics, position: &Position, @@ -232,6 +342,12 @@ pub fn jump_from_ground( physics.has_impulse = true; } +pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) { + for (mut physics, position) in &mut query { + physics.set_old_pos(position); + } +} + fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos { BlockPos::new( position.x.floor() as i32, @@ -241,7 +357,7 @@ fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos { ) } -// opts for handle_relative_friction_and_calculate_movement +/// Options for [`handle_relative_friction_and_calculate_movement`] struct HandleRelativeFrictionAndCalculateMovementOpts<'a> { block_friction: f32, world: &'a Instance, @@ -254,7 +370,6 @@ struct HandleRelativeFrictionAndCalculateMovementOpts<'a> { pose: Option<&'a Pose>, jumping: &'a Jumping, } - fn handle_relative_friction_and_calculate_movement( HandleRelativeFrictionAndCalculateMovementOpts { block_friction, @@ -274,22 +389,22 @@ fn handle_relative_friction_and_calculate_movement( direction, get_friction_influenced_speed(physics, attributes, block_friction, is_sprinting), &Vec3 { - x: physics.xxa as f64, - y: physics.yya as f64, - z: physics.zza as f64, + x: physics.x_acceleration as f64, + y: physics.y_acceleration as f64, + z: physics.z_acceleration as f64, }, ); physics.velocity = handle_on_climbable(physics.velocity, on_climbable, &position, world, pose); move_colliding( - &MoverType::Own, + MoverType::Own, &physics.velocity.clone(), world, &mut position, physics, ) - .expect("Entity should exist."); + .expect("Entity should exist"); // let delta_movement = entity.delta; // ladders // if ((entity.horizontalCollision || entity.jumping) && (entity.onClimbable() @@ -418,369 +533,3 @@ fn jump_boost_power() -> f64 { // } 0. } - -#[cfg(test)] -mod tests { - - use azalea_core::{position::ChunkPos, resource_location::ResourceLocation}; - use azalea_entity::{EntityBundle, EntityPlugin}; - use azalea_world::{Chunk, MinecraftEntityId, PartialInstance}; - use uuid::Uuid; - - use super::*; - - /// You need an app to spawn entities in the world and do updates. - fn make_test_app() -> App { - let mut app = App::new(); - app.add_plugins((PhysicsPlugin, EntityPlugin)) - .init_resource::<InstanceContainer>(); - app - } - - #[test] - fn test_gravity() { - let mut app = make_test_app(); - let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert( - ResourceLocation::new("minecraft:overworld"), - 384, - -64, - ); - let mut partial_world = PartialInstance::default(); - // the entity has to be in a loaded chunk for physics to work - partial_world.chunks.set( - &ChunkPos { x: 0, z: 0 }, - Some(Chunk::default()), - &mut world_lock.write().chunks, - ); - - let entity = app - .world_mut() - .spawn(( - EntityBundle::new( - Uuid::nil(), - Vec3 { - x: 0., - y: 70., - z: 0., - }, - azalea_registry::EntityKind::Zombie, - ResourceLocation::new("minecraft:overworld"), - ), - MinecraftEntityId(0), - LocalEntity, - )) - .id(); - { - let entity_pos = *app.world_mut().get::<Position>(entity).unwrap(); - // y should start at 70 - assert_eq!(entity_pos.y, 70.); - } - app.update(); - app.world_mut().run_schedule(GameTick); - app.update(); - { - let entity_pos = *app.world_mut().get::<Position>(entity).unwrap(); - // delta is applied before gravity, so the first tick only sets the delta - assert_eq!(entity_pos.y, 70.); - let entity_physics = app.world_mut().get::<Physics>(entity).unwrap(); - assert!(entity_physics.velocity.y < 0.); - } - app.world_mut().run_schedule(GameTick); - app.update(); - { - let entity_pos = *app.world_mut().get::<Position>(entity).unwrap(); - // the second tick applies the delta to the position, so now it should go down - assert!( - entity_pos.y < 70., - "Entity y ({}) didn't go down after physics steps", - entity_pos.y - ); - } - } - #[test] - fn test_collision() { - let mut app = make_test_app(); - let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert( - ResourceLocation::new("minecraft:overworld"), - 384, - -64, - ); - let mut partial_world = PartialInstance::default(); - - partial_world.chunks.set( - &ChunkPos { x: 0, z: 0 }, - Some(Chunk::default()), - &mut world_lock.write().chunks, - ); - let entity = app - .world_mut() - .spawn(( - EntityBundle::new( - Uuid::nil(), - Vec3 { - x: 0.5, - y: 70., - z: 0.5, - }, - azalea_registry::EntityKind::Player, - ResourceLocation::new("minecraft:overworld"), - ), - MinecraftEntityId(0), - LocalEntity, - )) - .id(); - let block_state = partial_world.chunks.set_block_state( - &BlockPos { x: 0, y: 69, z: 0 }, - azalea_registry::Block::Stone.into(), - &world_lock.write().chunks, - ); - assert!( - block_state.is_some(), - "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed" - ); - app.update(); - app.world_mut().run_schedule(GameTick); - app.update(); - { - let entity_pos = *app.world_mut().get::<Position>(entity).unwrap(); - // delta will change, but it won't move until next tick - assert_eq!(entity_pos.y, 70.); - let entity_physics = app.world_mut().get::<Physics>(entity).unwrap(); - assert!(entity_physics.velocity.y < 0.); - } - app.world_mut().run_schedule(GameTick); - app.update(); - { - let entity_pos = *app.world_mut().get::<Position>(entity).unwrap(); - // the second tick applies the delta to the position, but it also does collision - assert_eq!(entity_pos.y, 70.); - } - } - - #[test] - fn test_slab_collision() { - let mut app = make_test_app(); - let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert( - ResourceLocation::new("minecraft:overworld"), - 384, - -64, - ); - let mut partial_world = PartialInstance::default(); - - partial_world.chunks.set( - &ChunkPos { x: 0, z: 0 }, - Some(Chunk::default()), - &mut world_lock.write().chunks, - ); - let entity = app - .world_mut() - .spawn(( - EntityBundle::new( - Uuid::nil(), - Vec3 { - x: 0.5, - y: 71., - z: 0.5, - }, - azalea_registry::EntityKind::Player, - ResourceLocation::new("minecraft:overworld"), - ), - MinecraftEntityId(0), - LocalEntity, - )) - .id(); - let block_state = partial_world.chunks.set_block_state( - &BlockPos { x: 0, y: 69, z: 0 }, - azalea_block::blocks::StoneSlab { - kind: azalea_block::properties::Type::Bottom, - waterlogged: false, - } - .into(), - &world_lock.write().chunks, - ); - assert!( - block_state.is_some(), - "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed" - ); - // do a few steps so we fall on the slab - for _ in 0..20 { - app.world_mut().run_schedule(GameTick); - app.update(); - } - let entity_pos = app.world_mut().get::<Position>(entity).unwrap(); - assert_eq!(entity_pos.y, 69.5); - } - - #[test] - fn test_top_slab_collision() { - let mut app = make_test_app(); - let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert( - ResourceLocation::new("minecraft:overworld"), - 384, - -64, - ); - let mut partial_world = PartialInstance::default(); - - partial_world.chunks.set( - &ChunkPos { x: 0, z: 0 }, - Some(Chunk::default()), - &mut world_lock.write().chunks, - ); - let entity = app - .world_mut() - .spawn(( - EntityBundle::new( - Uuid::nil(), - Vec3 { - x: 0.5, - y: 71., - z: 0.5, - }, - azalea_registry::EntityKind::Player, - ResourceLocation::new("minecraft:overworld"), - ), - MinecraftEntityId(0), - LocalEntity, - )) - .id(); - let block_state = world_lock.write().chunks.set_block_state( - &BlockPos { x: 0, y: 69, z: 0 }, - azalea_block::blocks::StoneSlab { - kind: azalea_block::properties::Type::Top, - waterlogged: false, - } - .into(), - ); - assert!( - block_state.is_some(), - "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed" - ); - // do a few steps so we fall on the slab - for _ in 0..20 { - app.world_mut().run_schedule(GameTick); - app.update(); - } - let entity_pos = app.world_mut().get::<Position>(entity).unwrap(); - assert_eq!(entity_pos.y, 70.); - } - - #[test] - fn test_weird_wall_collision() { - let mut app = make_test_app(); - let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert( - ResourceLocation::new("minecraft:overworld"), - 384, - -64, - ); - let mut partial_world = PartialInstance::default(); - - partial_world.chunks.set( - &ChunkPos { x: 0, z: 0 }, - Some(Chunk::default()), - &mut world_lock.write().chunks, - ); - let entity = app - .world_mut() - .spawn(( - EntityBundle::new( - Uuid::nil(), - Vec3 { - x: 0.5, - y: 73., - z: 0.5, - }, - azalea_registry::EntityKind::Player, - ResourceLocation::new("minecraft:overworld"), - ), - MinecraftEntityId(0), - LocalEntity, - )) - .id(); - let block_state = world_lock.write().chunks.set_block_state( - &BlockPos { x: 0, y: 69, z: 0 }, - azalea_block::blocks::CobblestoneWall { - east: azalea_block::properties::WallEast::Low, - north: azalea_block::properties::WallNorth::Low, - south: azalea_block::properties::WallSouth::Low, - west: azalea_block::properties::WallWest::Low, - up: false, - waterlogged: false, - } - .into(), - ); - assert!( - block_state.is_some(), - "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed" - ); - // do a few steps so we fall on the wall - for _ in 0..20 { - app.world_mut().run_schedule(GameTick); - app.update(); - } - - let entity_pos = app.world_mut().get::<Position>(entity).unwrap(); - assert_eq!(entity_pos.y, 70.5); - } - - #[test] - fn test_negative_coordinates_weird_wall_collision() { - let mut app = make_test_app(); - let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert( - ResourceLocation::new("minecraft:overworld"), - 384, - -64, - ); - let mut partial_world = PartialInstance::default(); - - partial_world.chunks.set( - &ChunkPos { x: -1, z: -1 }, - Some(Chunk::default()), - &mut world_lock.write().chunks, - ); - let entity = app - .world_mut() - .spawn(( - EntityBundle::new( - Uuid::nil(), - Vec3 { - x: -7.5, - y: 73., - z: -7.5, - }, - azalea_registry::EntityKind::Player, - ResourceLocation::new("minecraft:overworld"), - ), - MinecraftEntityId(0), - LocalEntity, - )) - .id(); - let block_state = world_lock.write().chunks.set_block_state( - &BlockPos { - x: -8, - y: 69, - z: -8, - }, - azalea_block::blocks::CobblestoneWall { - east: azalea_block::properties::WallEast::Low, - north: azalea_block::properties::WallNorth::Low, - south: azalea_block::properties::WallSouth::Low, - west: azalea_block::properties::WallWest::Low, - up: false, - waterlogged: false, - } - .into(), - ); - assert!( - block_state.is_some(), - "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed" - ); - // do a few steps so we fall on the wall - for _ in 0..20 { - app.world_mut().run_schedule(GameTick); - app.update(); - } - - let entity_pos = app.world_mut().get::<Position>(entity).unwrap(); - assert_eq!(entity_pos.y, 70.5); - } -} diff --git a/azalea-physics/src/travel.rs b/azalea-physics/src/travel.rs new file mode 100644 index 00000000..08b59867 --- /dev/null +++ b/azalea-physics/src/travel.rs @@ -0,0 +1,299 @@ +use azalea_block::{Block, BlockState}; +use azalea_core::{aabb::AABB, position::Vec3}; +use azalea_entity::{ + metadata::Sprinting, move_relative, Attributes, InLoadedChunk, Jumping, LocalEntity, + LookDirection, OnClimbable, Physics, Pose, Position, +}; +use azalea_world::{Instance, InstanceContainer, InstanceName}; +use bevy_ecs::prelude::*; + +use crate::{ + collision::{move_colliding, MoverType}, + get_block_pos_below_that_affects_movement, handle_relative_friction_and_calculate_movement, + HandleRelativeFrictionAndCalculateMovementOpts, +}; + +/// Move the entity with the given acceleration while handling friction, +/// gravity, collisions, and some other stuff. +#[allow(clippy::type_complexity)] +pub fn travel( + mut query: Query< + ( + &mut Physics, + &mut LookDirection, + &mut Position, + Option<&Sprinting>, + Option<&Pose>, + &Attributes, + &InstanceName, + &OnClimbable, + &Jumping, + ), + (With<LocalEntity>, With<InLoadedChunk>), + >, + instance_container: Res<InstanceContainer>, +) { + for ( + mut physics, + direction, + position, + sprinting, + pose, + attributes, + world_name, + on_climbable, + jumping, + ) in &mut query + { + let Some(world_lock) = instance_container.get(world_name) else { + continue; + }; + let world = world_lock.read(); + + let sprinting = *sprinting.unwrap_or(&Sprinting(false)); + + // TODO: elytras + + if physics.is_in_water() || physics.is_in_lava() { + // minecraft also checks for `this.isAffectedByFluids() && + // !this.canStandOnFluid(fluidAtBlock)` here but it doesn't matter + // for players + travel_in_fluid( + &mut physics, + &direction, + position, + attributes, + sprinting, + on_climbable, + &world, + ); + } else { + travel_in_air( + &mut physics, + &direction, + position, + attributes, + sprinting, + on_climbable, + pose, + jumping, + &world, + ); + } + } +} + +/// The usual movement when we're not in water or using an elytra. +#[allow(clippy::too_many_arguments)] +fn travel_in_air( + physics: &mut Physics, + direction: &LookDirection, + position: Mut<Position>, + attributes: &Attributes, + sprinting: Sprinting, + on_climbable: &OnClimbable, + pose: Option<&Pose>, + jumping: &Jumping, + world: &Instance, +) { + let gravity = get_effective_gravity(); + + let block_pos_below = get_block_pos_below_that_affects_movement(&position); + + let block_state_below = world + .chunks + .get_block_state(&block_pos_below) + .unwrap_or(BlockState::AIR); + let block_below: Box<dyn Block> = block_state_below.into(); + let block_friction = block_below.behavior().friction; + + let inertia = if physics.on_ground() { + block_friction * 0.91 + } else { + 0.91 + }; + + // this applies the current delta + let mut movement = handle_relative_friction_and_calculate_movement( + HandleRelativeFrictionAndCalculateMovementOpts { + block_friction, + world, + physics, + direction, + position, + attributes, + is_sprinting: *sprinting, + on_climbable, + pose, + jumping, + }, + ); + + movement.y -= gravity; + + // if (this.shouldDiscardFriction()) { + // this.setDeltaMovement(movement.x, yMovement, movement.z); + // } else { + // this.setDeltaMovement(movement.x * (double)inertia, yMovement * + // 0.9800000190734863D, movement.z * (double)inertia); } + + // if should_discard_friction(self) { + if false { + physics.velocity = movement; + } else { + physics.velocity = Vec3 { + x: movement.x * inertia as f64, + y: movement.y * 0.9800000190734863f64, + z: movement.z * inertia as f64, + }; + } +} + +fn travel_in_fluid( + physics: &mut Physics, + direction: &LookDirection, + mut position: Mut<Position>, + attributes: &Attributes, + sprinting: Sprinting, + on_climbable: &OnClimbable, + world: &Instance, +) { + let moving_down = physics.velocity.y <= 0.; + let y = position.y; + let gravity = get_effective_gravity(); + + let acceleration = Vec3::new( + physics.x_acceleration as f64, + physics.y_acceleration as f64, + physics.z_acceleration as f64, + ); + + if physics.was_touching_water { + let mut water_movement_speed = if *sprinting { 0.9 } else { 0.8 }; + let mut speed = 0.02; + let mut water_efficiency_modifier = attributes.water_movement_efficiency.calculate() as f32; + if !physics.on_ground() { + water_efficiency_modifier *= 0.5; + } + + if water_efficiency_modifier > 0. { + water_movement_speed += (0.54600006 - water_movement_speed) * water_efficiency_modifier; + speed += (attributes.speed.calculate() as f32 - speed) * water_efficiency_modifier; + } + + // if (this.hasEffect(MobEffects.DOLPHINS_GRACE)) { + // waterMovementSpeed = 0.96F; + // } + + move_relative(physics, direction, speed, &acceleration); + move_colliding( + MoverType::Own, + &physics.velocity.clone(), + world, + &mut position, + physics, + ) + .expect("Entity should exist"); + + let mut new_velocity = physics.velocity; + if physics.horizontal_collision && **on_climbable { + // underwater ladders + new_velocity.y = 0.2; + } + new_velocity.x *= water_movement_speed as f64; + new_velocity.y *= 0.8; + new_velocity.z *= water_movement_speed as f64; + physics.velocity = + get_fluid_falling_adjusted_movement(gravity, moving_down, new_velocity, sprinting); + } else { + move_relative(physics, direction, 0.02, &acceleration); + move_colliding( + MoverType::Own, + &physics.velocity.clone(), + world, + &mut position, + physics, + ) + .expect("Entity should exist"); + + if physics.lava_fluid_height <= fluid_jump_threshold() { + physics.velocity.x *= 0.5; + physics.velocity.y *= 0.8; + physics.velocity.z *= 0.5; + let new_velocity = get_fluid_falling_adjusted_movement( + gravity, + moving_down, + physics.velocity, + sprinting, + ); + physics.velocity = new_velocity; + } else { + physics.velocity *= 0.5; + } + + if gravity != 0.0 { + physics.velocity.y -= gravity / 4.0; + } + } + + let velocity = physics.velocity; + if physics.horizontal_collision + && is_free( + physics.bounding_box, + world, + velocity.x, + velocity.y + 0.6 - position.y + y, + velocity.z, + ) + { + physics.velocity.y = 0.3; + } +} + +fn get_fluid_falling_adjusted_movement( + gravity: f64, + moving_down: bool, + new_velocity: Vec3, + sprinting: Sprinting, +) -> Vec3 { + if gravity != 0. && !*sprinting { + let new_y_velocity = if moving_down + && (new_velocity.y - 0.005).abs() >= 0.003 + && f64::abs(new_velocity.y - gravity / 16.0) < 0.003 + { + -0.003 + } else { + new_velocity.y - gravity / 16.0 + }; + + Vec3 { + x: new_velocity.x, + y: new_y_velocity, + z: new_velocity.z, + } + } else { + new_velocity + } +} + +fn is_free(bounding_box: AABB, world: &Instance, x: f64, y: f64, z: f64) -> bool { + // let bounding_box = bounding_box.move_relative(Vec3::new(x, y, z)); + + let _ = (bounding_box, world, x, y, z); + + // TODO: implement this, see Entity.isFree + + true +} + +fn get_effective_gravity() -> f64 { + // TODO: slow falling effect + 0.08 +} + +pub fn fluid_jump_threshold() -> f64 { + // this is 0.0 for entities with an eye height lower than 0.4, but that's not + // implemented since it's usually not relevant for players (unless the player + // was shrunk) + 0.4 +} |
