aboutsummaryrefslogtreecommitdiff
path: root/azalea-physics/src
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-01-10 16:45:27 -0600
committerGitHub <noreply@github.com>2025-01-10 16:45:27 -0600
commit0d16f01571ec8315f3979eae46981e559ade1cf9 (patch)
treeea43c32a57b0e6a67579d75a134dfbc009d09781 /azalea-physics/src
parent615d8f9d2ac56b3244d328587243301da253eafd (diff)
downloadazalea-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.rs154
-rwxr-xr-xazalea-physics/src/collision/discrete_voxel_shape.rs36
-rw-r--r--azalea-physics/src/collision/mod.rs44
-rwxr-xr-xazalea-physics/src/collision/shape.rs67
-rw-r--r--azalea-physics/src/collision/world_collisions.rs29
-rw-r--r--azalea-physics/src/fluids.rs274
-rw-r--r--azalea-physics/src/lib.rs731
-rw-r--r--azalea-physics/src/travel.rs299
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
+}