1#![doc = include_str!("../README.md")]
2#![feature(trait_alias)]
3
4pub mod client_movement;
5pub mod clip;
6pub mod collision;
7pub mod fluids;
8pub mod travel;
9
10use std::collections::HashSet;
11
12use azalea_block::{BlockState, fluid_state::FluidState, properties};
13use azalea_core::{
14 math,
15 position::{BlockPos, Vec3},
16 tick::GameTick,
17};
18use azalea_entity::{
19 ActiveEffects, Attributes, EntityKindComponent, HasClientLoaded, Jumping, LocalEntity,
20 LookDirection, OnClimbable, Physics, Pose, Position, dimensions::EntityDimensions,
21 metadata::Sprinting, move_relative,
22};
23use azalea_registry::builtin::{BlockKind, EntityKind, MobEffect};
24use azalea_world::{World, WorldName, Worlds};
25use bevy_app::{App, Plugin, Update};
26use bevy_ecs::prelude::*;
27use clip::box_traverse_blocks;
28use collision::{BLOCK_SHAPE, BlockWithShape, VoxelShape, move_colliding};
29
30use crate::{
31 client_movement::ClientMovementState,
32 collision::{MoveCtx, entity_collisions::update_last_bounding_box},
33};
34
35#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
37pub struct PhysicsSystems;
38
39pub struct PhysicsPlugin;
40impl Plugin for PhysicsPlugin {
41 fn build(&self, app: &mut App) {
42 app.add_systems(
43 GameTick,
44 (
45 fluids::update_in_water_state_and_do_fluid_pushing,
46 update_old_position,
47 fluids::update_swimming,
48 ai_step,
49 travel::travel,
50 apply_effects_from_blocks,
51 )
52 .chain()
53 .in_set(PhysicsSystems)
54 .after(azalea_entity::update_in_loaded_chunk),
55 )
56 .add_systems(
58 Update,
59 update_last_bounding_box.after(azalea_entity::update_bounding_box),
60 );
61 }
62}
63
64#[allow(clippy::type_complexity)]
68pub fn ai_step(
69 mut query: Query<
70 (
71 &mut Physics,
72 Option<&Jumping>,
73 &Position,
74 &LookDirection,
75 &Sprinting,
76 &ActiveEffects,
77 &WorldName,
78 &EntityKindComponent,
79 &ClientMovementState,
80 ),
81 (With<LocalEntity>, With<HasClientLoaded>),
82 >,
83 worlds: Res<Worlds>,
84) {
85 for (
86 mut physics,
87 jumping,
88 position,
89 look_direction,
90 sprinting,
91 active_effects,
92 world_name,
93 entity_kind,
94 client_movement,
95 ) in &mut query
96 {
97 let is_player = **entity_kind == EntityKind::Player;
98
99 if physics.no_jump_delay > 0 {
103 physics.no_jump_delay -= 1;
104 }
105
106 if is_player {
107 if physics.velocity.horizontal_distance_squared() < 9.0e-6 {
108 physics.velocity.x = 0.;
109 physics.velocity.z = 0.;
110 }
111 } else {
112 if physics.velocity.x.abs() < 0.003 {
113 physics.velocity.x = 0.;
114 }
115 if physics.velocity.z.abs() < 0.003 {
116 physics.velocity.z = 0.;
117 }
118 }
119
120 if physics.velocity.y.abs() < 0.003 {
121 physics.velocity.y = 0.;
122 }
123
124 if is_player {
125 } else {
127 physics.x_acceleration *= 0.98;
128 physics.z_acceleration *= 0.98;
129 }
130
131 if client_movement.trying_to_crouch && physics.is_in_water() {
132 go_down_in_water(&mut physics);
133 }
134
135 if jumping == Some(&Jumping(true)) {
136 let fluid_height = if physics.is_in_lava() {
137 physics.lava_fluid_height
138 } else if physics.is_in_water() {
139 physics.water_fluid_height
140 } else {
141 0.
142 };
143
144 let in_water = physics.is_in_water() && fluid_height > 0.;
145 let fluid_jump_threshold = travel::fluid_jump_threshold();
146
147 if !in_water || physics.on_ground() && fluid_height <= fluid_jump_threshold {
148 if !physics.is_in_lava()
149 || physics.on_ground() && fluid_height <= fluid_jump_threshold
150 {
151 if (physics.on_ground() || in_water && fluid_height <= fluid_jump_threshold)
152 && physics.no_jump_delay == 0
153 {
154 jump_from_ground(
155 &mut physics,
156 *position,
157 *look_direction,
158 *sprinting,
159 world_name,
160 &worlds,
161 active_effects,
162 );
163 physics.no_jump_delay = 10;
164 }
165 } else {
166 jump_in_liquid(&mut physics);
167 }
168 } else {
169 jump_in_liquid(&mut physics);
170 }
171 } else {
172 physics.no_jump_delay = 0;
173 }
174
175 }
178}
179
180fn jump_in_liquid(physics: &mut Physics) {
181 physics.velocity.y += 0.04f32 as f64;
182}
183
184fn go_down_in_water(physics: &mut Physics) {
185 physics.velocity.y -= 0.04f32 as f64;
186}
187
188#[allow(clippy::type_complexity)]
190pub fn apply_effects_from_blocks(
191 mut query: Query<
192 (&mut Physics, &Position, &EntityDimensions, &WorldName),
193 (With<LocalEntity>, With<HasClientLoaded>),
194 >,
195 worlds: Res<Worlds>,
196) {
197 for (mut physics, position, dimensions, world_name) in &mut query {
198 let Some(world_lock) = worlds.get(world_name) else {
199 continue;
200 };
201 let world = world_lock.read();
202
203 let movement_this_tick = [EntityMovement {
216 from: physics.old_position,
217 to: **position,
218 }];
219
220 check_inside_blocks(&mut physics, dimensions, &world, &movement_this_tick);
221 }
222}
223
224fn check_inside_blocks(
225 physics: &mut Physics,
226 dimensions: &EntityDimensions,
227 world: &World,
228 movements: &[EntityMovement],
229) -> Vec<BlockState> {
230 let mut blocks_inside = Vec::new();
231 let mut visited_blocks = HashSet::<BlockState>::new();
232
233 for movement in movements {
234 let bounding_box_at_target = dimensions
235 .make_bounding_box(movement.to)
236 .deflate_all(1.0E-5);
237
238 for traversed_block in
239 box_traverse_blocks(movement.from, movement.to, &bounding_box_at_target)
240 {
241 let traversed_block_state = world.get_block_state(traversed_block).unwrap_or_default();
246 if traversed_block_state.is_air() {
247 continue;
248 }
249 if !visited_blocks.insert(traversed_block_state) {
250 continue;
251 }
252
253 let entity_inside_collision_shape = &*BLOCK_SHAPE;
266
267 if entity_inside_collision_shape != &*BLOCK_SHAPE
268 && !collided_with_shape_moving_from(
269 movement.from,
270 movement.to,
271 traversed_block,
272 entity_inside_collision_shape,
273 dimensions,
274 )
275 {
276 continue;
277 }
278
279 handle_entity_inside_block(world, traversed_block_state, traversed_block, physics);
280
281 blocks_inside.push(traversed_block_state);
282 }
283 }
284
285 blocks_inside
286}
287
288fn collided_with_shape_moving_from(
289 from: Vec3,
290 to: Vec3,
291 traversed_block: BlockPos,
292 entity_inside_collision_shape: &VoxelShape,
293 dimensions: &EntityDimensions,
294) -> bool {
295 let bounding_box_from = dimensions.make_bounding_box(from);
296 let delta = to - from;
297 bounding_box_from.collided_along_vector(
298 delta,
299 &entity_inside_collision_shape
300 .move_relative(traversed_block.to_vec3_floored())
301 .to_aabbs(),
302 )
303}
304
305fn handle_entity_inside_block(
307 world: &World,
308 block: BlockState,
309 block_pos: BlockPos,
310 physics: &mut Physics,
311) {
312 let registry_block = BlockKind::from(block);
313 #[allow(clippy::single_match)]
314 match registry_block {
315 BlockKind::BubbleColumn => {
316 let block_above = world.get_block_state(block_pos.up(1)).unwrap_or_default();
317 let is_block_above_empty =
318 block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty();
319 let drag_down = block
320 .property::<properties::Drag>()
321 .expect("drag property should always be present on bubble columns");
322 let velocity = &mut physics.velocity;
323
324 if is_block_above_empty {
325 let new_y = if drag_down {
326 f64::max(-0.9, velocity.y - 0.03)
327 } else {
328 f64::min(1.8, velocity.y + 0.1)
329 };
330 velocity.y = new_y;
331 } else {
332 let new_y = if drag_down {
333 f64::max(-0.3, velocity.y - 0.03)
334 } else {
335 f64::min(0.7, velocity.y + 0.06)
336 };
337 velocity.y = new_y;
338 physics.reset_fall_distance();
339 }
340 }
341 _ => {}
342 }
343}
344
345pub struct EntityMovement {
346 pub from: Vec3,
347 pub to: Vec3,
348}
349
350pub fn jump_from_ground(
351 physics: &mut Physics,
352 position: Position,
353 look_direction: LookDirection,
354 sprinting: Sprinting,
355 world_name: &WorldName,
356 worlds: &Worlds,
357 active_effects: &ActiveEffects,
358) {
359 let world_lock = worlds
360 .get(world_name)
361 .expect("All entities should be in a valid world");
362 let world = world_lock.read();
363
364 let base_jump = jump_power(&world, position);
365 let jump_power = base_jump + jump_boost_power(active_effects);
366 if jump_power <= 1.0E-5 {
367 return;
368 }
369
370 let old_delta_movement = physics.velocity;
371 physics.velocity = Vec3 {
372 x: old_delta_movement.x,
373 y: f64::max(jump_power as f64, old_delta_movement.y),
374 z: old_delta_movement.z,
375 };
376 if *sprinting {
377 let y_rot = look_direction.y_rot() * 0.017453292;
379 physics.velocity += Vec3 {
380 x: (-math::sin(y_rot) * 0.2) as f64,
381 y: 0.,
382 z: (math::cos(y_rot) * 0.2) as f64,
383 };
384 }
385
386 physics.has_impulse = true;
387}
388
389pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) {
390 for (mut physics, position) in &mut query {
391 physics.set_old_pos(*position);
392 }
393}
394
395pub fn get_block_pos_below_that_affects_movement(position: Position) -> BlockPos {
396 BlockPos::new(
397 position.x.floor() as i32,
398 (position.y - 0.5f64).floor() as i32,
400 position.z.floor() as i32,
401 )
402}
403
404fn handle_relative_friction_and_calculate_movement(ctx: &mut MoveCtx, block_friction: f32) -> Vec3 {
405 move_relative(
406 ctx.physics,
407 ctx.direction,
408 get_friction_influenced_speed(ctx.physics, ctx.attributes, block_friction, ctx.sprinting),
409 Vec3::new(
410 ctx.physics.x_acceleration as f64,
411 ctx.physics.y_acceleration as f64,
412 ctx.physics.z_acceleration as f64,
413 ),
414 );
415
416 ctx.physics.velocity = handle_on_climbable(
417 ctx.physics.velocity,
418 ctx.on_climbable,
419 *ctx.position,
420 ctx.world,
421 ctx.pose,
422 );
423
424 move_colliding(ctx, ctx.physics.velocity);
425 if ctx.physics.horizontal_collision || *ctx.jumping {
433 let block_at_feet: BlockKind = ctx
434 .world
435 .chunks
436 .get_block_state(BlockPos::from(*ctx.position))
437 .unwrap_or_default()
438 .into();
439
440 if *ctx.on_climbable || block_at_feet == BlockKind::PowderSnow {
441 ctx.physics.velocity.y = 0.2;
442 }
443 }
444
445 ctx.physics.velocity
446}
447
448fn handle_on_climbable(
449 velocity: Vec3,
450 on_climbable: OnClimbable,
451 position: Position,
452 world: &World,
453 pose: Option<Pose>,
454) -> Vec3 {
455 if !*on_climbable {
456 return velocity;
457 }
458
459 const CLIMBING_SPEED: f64 = 0.15_f32 as f64;
462
463 let x = f64::clamp(velocity.x, -CLIMBING_SPEED, CLIMBING_SPEED);
464 let z = f64::clamp(velocity.z, -CLIMBING_SPEED, CLIMBING_SPEED);
465 let mut y = f64::max(velocity.y, -CLIMBING_SPEED);
466
467 if y < 0.0
469 && pose == Some(Pose::Crouching)
470 && BlockKind::from(
471 world
472 .chunks
473 .get_block_state(position.into())
474 .unwrap_or_default(),
475 ) != BlockKind::Scaffolding
476 {
477 y = 0.;
478 }
479
480 Vec3 { x, y, z }
481}
482
483fn get_friction_influenced_speed(
487 physics: &Physics,
488 attributes: &Attributes,
489 friction: f32,
490 sprinting: Sprinting,
491) -> f32 {
492 if physics.on_ground() {
494 let speed = attributes.movement_speed.calculate() as f32;
495 speed * (0.21600002f32 / (friction * friction * friction))
496 } else {
497 if *sprinting { 0.025999999f32 } else { 0.02 }
499 }
500}
501
502fn block_jump_factor(world: &World, position: Position) -> f32 {
505 let block_at_pos = world.chunks.get_block_state(position.into());
506 let block_below = world
507 .chunks
508 .get_block_state(get_block_pos_below_that_affects_movement(position));
509
510 let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
511 block.behavior().jump_factor
512 } else {
513 1.
514 };
515 if block_at_pos_jump_factor != 1. {
516 return block_at_pos_jump_factor;
517 }
518
519 if let Some(block) = block_below {
520 block.behavior().jump_factor
521 } else {
522 1.
523 }
524}
525
526fn jump_power(world: &World, position: Position) -> f32 {
533 0.42 * block_jump_factor(world, position)
534}
535
536fn jump_boost_power(active_effects: &ActiveEffects) -> f32 {
537 active_effects
538 .get_level(MobEffect::JumpBoost)
539 .map(|level| 0.1 * (level + 1) as f32)
540 .unwrap_or(0.)
541}