1#![doc = include_str!("../README.md")]
2#![feature(trait_alias)]
3
4pub mod clip;
5pub mod collision;
6pub mod fluids;
7pub mod travel;
8
9use std::collections::HashSet;
10
11use azalea_block::{Block, BlockState, fluid_state::FluidState, properties};
12use azalea_core::{
13 math,
14 position::{BlockPos, Vec3},
15 tick::GameTick,
16};
17use azalea_entity::{
18 Attributes, InLoadedChunk, Jumping, LocalEntity, LookDirection, OnClimbable, Physics, Pose,
19 Position, metadata::Sprinting, move_relative,
20};
21use azalea_world::{Instance, InstanceContainer, InstanceName};
22use bevy_app::{App, Plugin};
23use bevy_ecs::{
24 entity::Entity,
25 query::With,
26 schedule::{IntoSystemConfigs, SystemSet},
27 system::{Query, Res},
28 world::Mut,
29};
30use clip::box_traverse_blocks;
31use collision::{
32 BLOCK_SHAPE, BlockWithShape, MoverType, VoxelShape,
33 entity_collisions::{CollidableEntityQuery, PhysicsQuery},
34 move_colliding,
35};
36
37#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
39pub struct PhysicsSet;
40
41pub struct PhysicsPlugin;
42impl Plugin for PhysicsPlugin {
43 fn build(&self, app: &mut App) {
44 app.add_systems(
45 GameTick,
46 (
47 fluids::update_in_water_state_and_do_fluid_pushing
48 .before(azalea_entity::update_fluid_on_eyes),
49 update_old_position,
50 fluids::update_swimming.after(azalea_entity::update_fluid_on_eyes),
51 ai_step,
52 travel::travel,
53 apply_effects_from_blocks,
54 )
55 .chain()
56 .in_set(PhysicsSet)
57 .after(azalea_entity::update_in_loaded_chunk),
58 );
59 }
60}
61
62#[allow(clippy::type_complexity)]
66pub fn ai_step(
67 mut query: Query<
68 (
69 &mut Physics,
70 Option<&Jumping>,
71 &Position,
72 &LookDirection,
73 &Sprinting,
74 &InstanceName,
75 ),
76 (With<LocalEntity>, With<InLoadedChunk>),
77 >,
78 instance_container: Res<InstanceContainer>,
79) {
80 for (mut physics, jumping, position, look_direction, sprinting, instance_name) in &mut query {
81 if physics.no_jump_delay > 0 {
85 physics.no_jump_delay -= 1;
86 }
87
88 if physics.velocity.x.abs() < 0.003 {
89 physics.velocity.x = 0.;
90 }
91 if physics.velocity.y.abs() < 0.003 {
92 physics.velocity.y = 0.;
93 }
94 if physics.velocity.z.abs() < 0.003 {
95 physics.velocity.z = 0.;
96 }
97
98 if jumping == Some(&Jumping(true)) {
99 let fluid_height = if physics.is_in_lava() {
100 physics.lava_fluid_height
101 } else if physics.is_in_water() {
102 physics.water_fluid_height
103 } else {
104 0.
105 };
106
107 let in_water = physics.is_in_water() && fluid_height > 0.;
108 let fluid_jump_threshold = travel::fluid_jump_threshold();
109
110 if !in_water || physics.on_ground() && fluid_height <= fluid_jump_threshold {
111 if !physics.is_in_lava()
112 || physics.on_ground() && fluid_height <= fluid_jump_threshold
113 {
114 if (physics.on_ground() || in_water && fluid_height <= fluid_jump_threshold)
115 && physics.no_jump_delay == 0
116 {
117 jump_from_ground(
118 &mut physics,
119 position,
120 look_direction,
121 sprinting,
122 instance_name,
123 &instance_container,
124 );
125 physics.no_jump_delay = 10;
126 }
127 } else {
128 jump_in_liquid(&mut physics);
129 }
130 } else {
131 jump_in_liquid(&mut physics);
132 }
133 } else {
134 physics.no_jump_delay = 0;
135 }
136
137 physics.x_acceleration *= 0.98;
138 physics.z_acceleration *= 0.98;
139
140 }
143}
144
145fn jump_in_liquid(physics: &mut Physics) {
146 physics.velocity.y += 0.04;
147}
148
149#[allow(clippy::type_complexity)]
151pub fn apply_effects_from_blocks(
152 mut query: Query<
153 (&mut Physics, &Position, &InstanceName),
154 (With<LocalEntity>, With<InLoadedChunk>),
155 >,
156 instance_container: Res<InstanceContainer>,
157) {
158 for (mut physics, position, world_name) in &mut query {
159 let Some(world_lock) = instance_container.get(world_name) else {
160 continue;
161 };
162 let world = world_lock.read();
163
164 let movement_this_tick = [EntityMovement {
177 from: physics.old_position,
178 to: **position,
179 }];
180
181 check_inside_blocks(&mut physics, &world, &movement_this_tick);
182 }
183}
184
185fn check_inside_blocks(
186 physics: &mut Physics,
187 world: &Instance,
188 movements: &[EntityMovement],
189) -> Vec<BlockState> {
190 let mut blocks_inside = Vec::new();
191 let mut visited_blocks = HashSet::<BlockState>::new();
192
193 for movement in movements {
194 let bounding_box_at_target = physics
195 .dimensions
196 .make_bounding_box(&movement.to)
197 .deflate_all(1.0E-5);
198
199 for traversed_block in
200 box_traverse_blocks(&movement.from, &movement.to, &bounding_box_at_target)
201 {
202 let traversed_block_state = world.get_block_state(&traversed_block).unwrap_or_default();
207 if traversed_block_state.is_air() {
208 continue;
209 }
210 if !visited_blocks.insert(traversed_block_state) {
211 continue;
212 }
213
214 let entity_inside_collision_shape = &*BLOCK_SHAPE;
227
228 if entity_inside_collision_shape != &*BLOCK_SHAPE
229 && !collided_with_shape_moving_from(
230 &movement.from,
231 &movement.to,
232 traversed_block,
233 entity_inside_collision_shape,
234 physics,
235 )
236 {
237 continue;
238 }
239
240 handle_entity_inside_block(world, traversed_block_state, traversed_block, physics);
241
242 blocks_inside.push(traversed_block_state);
243 }
244 }
245
246 blocks_inside
247}
248
249fn collided_with_shape_moving_from(
250 from: &Vec3,
251 to: &Vec3,
252 traversed_block: BlockPos,
253 entity_inside_collision_shape: &VoxelShape,
254 physics: &Physics,
255) -> bool {
256 let bounding_box_from = physics.dimensions.make_bounding_box(from);
257 let delta = to - from;
258 bounding_box_from.collided_along_vector(
259 delta,
260 &entity_inside_collision_shape
261 .move_relative(traversed_block.to_vec3_floored())
262 .to_aabbs(),
263 )
264}
265
266fn handle_entity_inside_block(
268 world: &Instance,
269 block: BlockState,
270 block_pos: BlockPos,
271 physics: &mut Physics,
272) {
273 let registry_block = azalea_registry::Block::from(block);
274 #[allow(clippy::single_match)]
275 match registry_block {
276 azalea_registry::Block::BubbleColumn => {
277 let block_above = world.get_block_state(&block_pos.up(1)).unwrap_or_default();
278 let is_block_above_empty =
279 block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty();
280 let drag_down = block
281 .property::<properties::Drag>()
282 .expect("drag property should always be present on bubble columns");
283 let velocity = &mut physics.velocity;
284
285 if is_block_above_empty {
286 let new_y = if drag_down {
287 f64::max(-0.9, velocity.y - 0.03)
288 } else {
289 f64::min(1.8, velocity.y + 0.1)
290 };
291 velocity.y = new_y;
292 } else {
293 let new_y = if drag_down {
294 f64::max(-0.3, velocity.y - 0.03)
295 } else {
296 f64::min(0.7, velocity.y + 0.06)
297 };
298 velocity.y = new_y;
299 physics.reset_fall_distance();
300 }
301 }
302 _ => {}
303 }
304}
305
306pub struct EntityMovement {
307 pub from: Vec3,
308 pub to: Vec3,
309}
310
311pub fn jump_from_ground(
312 physics: &mut Physics,
313 position: &Position,
314 look_direction: &LookDirection,
315 sprinting: &Sprinting,
316 instance_name: &InstanceName,
317 instance_container: &InstanceContainer,
318) {
319 let world_lock = instance_container
320 .get(instance_name)
321 .expect("All entities should be in a valid world");
322 let world = world_lock.read();
323
324 let jump_power: f64 = jump_power(&world, position) as f64 + jump_boost_power();
325 let old_delta_movement = physics.velocity;
326 physics.velocity = Vec3 {
327 x: old_delta_movement.x,
328 y: jump_power,
329 z: old_delta_movement.z,
330 };
331 if **sprinting {
332 let y_rot = look_direction.y_rot * 0.017453292;
334 physics.velocity += Vec3 {
335 x: (-math::sin(y_rot) * 0.2) as f64,
336 y: 0.,
337 z: (math::cos(y_rot) * 0.2) as f64,
338 };
339 }
340
341 physics.has_impulse = true;
342}
343
344pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) {
345 for (mut physics, position) in &mut query {
346 physics.set_old_pos(position);
347 }
348}
349
350fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos {
351 BlockPos::new(
352 position.x.floor() as i32,
353 (position.y - 0.5f64).floor() as i32,
355 position.z.floor() as i32,
356 )
357}
358
359struct HandleRelativeFrictionAndCalculateMovementOpts<'a, 'b, 'world, 'state> {
361 block_friction: f32,
362 world: &'a Instance,
363 physics: &'a mut Physics,
364 direction: &'a LookDirection,
365 position: Mut<'a, Position>,
366 attributes: &'a Attributes,
367 is_sprinting: bool,
368 on_climbable: &'a OnClimbable,
369 pose: Option<&'a Pose>,
370 jumping: &'a Jumping,
371 entity: Entity,
372 physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
373 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
374}
375fn handle_relative_friction_and_calculate_movement(
376 HandleRelativeFrictionAndCalculateMovementOpts {
377 block_friction,
378 world,
379 physics,
380 direction,
381 mut position,
382 attributes,
383 is_sprinting,
384 on_climbable,
385 pose,
386 jumping,
387 entity,
388 physics_query,
389 collidable_entity_query,
390 }: HandleRelativeFrictionAndCalculateMovementOpts<'_, '_, '_, '_>,
391) -> Vec3 {
392 move_relative(
393 physics,
394 direction,
395 get_friction_influenced_speed(physics, attributes, block_friction, is_sprinting),
396 &Vec3 {
397 x: physics.x_acceleration as f64,
398 y: physics.y_acceleration as f64,
399 z: physics.z_acceleration as f64,
400 },
401 );
402
403 physics.velocity = handle_on_climbable(physics.velocity, on_climbable, &position, world, pose);
404
405 move_colliding(
406 MoverType::Own,
407 &physics.velocity.clone(),
408 world,
409 &mut position,
410 physics,
411 Some(entity),
412 physics_query,
413 collidable_entity_query,
414 )
415 .expect("Entity should exist");
416 if physics.horizontal_collision || **jumping {
424 let block_at_feet: azalea_registry::Block = world
425 .chunks
426 .get_block_state(&(*position).into())
427 .unwrap_or_default()
428 .into();
429
430 if **on_climbable || block_at_feet == azalea_registry::Block::PowderSnow {
431 physics.velocity.y = 0.2;
432 }
433 }
434
435 physics.velocity
436}
437
438fn handle_on_climbable(
439 velocity: Vec3,
440 on_climbable: &OnClimbable,
441 position: &Position,
442 world: &Instance,
443 pose: Option<&Pose>,
444) -> Vec3 {
445 if !**on_climbable {
446 return velocity;
447 }
448
449 const CLIMBING_SPEED: f64 = 0.15_f32 as f64;
452
453 let x = f64::clamp(velocity.x, -CLIMBING_SPEED, CLIMBING_SPEED);
454 let z = f64::clamp(velocity.z, -CLIMBING_SPEED, CLIMBING_SPEED);
455 let mut y = f64::max(velocity.y, -CLIMBING_SPEED);
456
457 if y < 0.0
459 && pose.copied() == Some(Pose::Sneaking)
460 && azalea_registry::Block::from(
461 world
462 .chunks
463 .get_block_state(&position.into())
464 .unwrap_or_default(),
465 ) != azalea_registry::Block::Scaffolding
466 {
467 y = 0.;
468 }
469
470 Vec3 { x, y, z }
471}
472
473fn get_friction_influenced_speed(
477 physics: &Physics,
478 attributes: &Attributes,
479 friction: f32,
480 is_sprinting: bool,
481) -> f32 {
482 if physics.on_ground() {
484 let speed: f32 = attributes.speed.calculate() as f32;
485 speed * (0.216f32 / (friction * friction * friction))
486 } else {
487 if is_sprinting { 0.025999999f32 } else { 0.02 }
489 }
490}
491
492fn block_jump_factor(world: &Instance, position: &Position) -> f32 {
495 let block_at_pos = world.chunks.get_block_state(&position.into());
496 let block_below = world
497 .chunks
498 .get_block_state(&get_block_pos_below_that_affects_movement(position));
499
500 let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
501 Box::<dyn Block>::from(block).behavior().jump_factor
502 } else {
503 1.
504 };
505 if block_at_pos_jump_factor != 1. {
506 return block_at_pos_jump_factor;
507 }
508
509 if let Some(block) = block_below {
510 Box::<dyn Block>::from(block).behavior().jump_factor
511 } else {
512 1.
513 }
514}
515
516fn jump_power(world: &Instance, position: &Position) -> f32 {
523 0.42 * block_jump_factor(world, position)
524}
525
526fn jump_boost_power() -> f64 {
527 0.
538}