1#![doc = include_str!("../README.md")]
2#![feature(trait_alias)]
3
4pub mod clip;
5pub mod collision;
6pub mod fluids;
7pub mod local_player;
8pub mod travel;
9
10use std::collections::HashSet;
11
12use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState, properties};
13use azalea_core::{
14 math,
15 position::{BlockPos, Vec3},
16 tick::GameTick,
17};
18use azalea_entity::{
19 Attributes, EntityKindComponent, HasClientLoaded, Jumping, LocalEntity, LookDirection,
20 OnClimbable, Physics, Pose, Position, dimensions::EntityDimensions, metadata::Sprinting,
21 move_relative,
22};
23use azalea_registry::{Block, EntityKind};
24use azalea_world::{Instance, InstanceContainer, InstanceName};
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::collision::{MoveCtx, entity_collisions::update_last_bounding_box};
31
32#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
34pub struct PhysicsSystems;
35
36pub struct PhysicsPlugin;
37impl Plugin for PhysicsPlugin {
38 fn build(&self, app: &mut App) {
39 app.add_systems(
40 GameTick,
41 (
42 fluids::update_in_water_state_and_do_fluid_pushing,
43 update_old_position,
44 fluids::update_swimming,
45 ai_step,
46 travel::travel,
47 apply_effects_from_blocks,
48 )
49 .chain()
50 .in_set(PhysicsSystems)
51 .after(azalea_entity::update_in_loaded_chunk),
52 )
53 .add_systems(
55 Update,
56 update_last_bounding_box.after(azalea_entity::update_bounding_box),
57 );
58 }
59}
60
61#[allow(clippy::type_complexity)]
65pub fn ai_step(
66 mut query: Query<
67 (
68 &mut Physics,
69 Option<&Jumping>,
70 &Position,
71 &LookDirection,
72 &Sprinting,
73 &InstanceName,
74 &EntityKindComponent,
75 ),
76 (With<LocalEntity>, With<HasClientLoaded>),
77 >,
78 instance_container: Res<InstanceContainer>,
79) {
80 for (mut physics, jumping, position, look_direction, sprinting, instance_name, entity_kind) in
81 &mut query
82 {
83 let is_player = **entity_kind == EntityKind::Player;
84
85 if physics.no_jump_delay > 0 {
89 physics.no_jump_delay -= 1;
90 }
91
92 if is_player {
93 if physics.velocity.horizontal_distance_squared() < 9.0e-6 {
94 physics.velocity.x = 0.;
95 physics.velocity.z = 0.;
96 }
97 } else {
98 if physics.velocity.x.abs() < 0.003 {
99 physics.velocity.x = 0.;
100 }
101 if physics.velocity.z.abs() < 0.003 {
102 physics.velocity.z = 0.;
103 }
104 }
105
106 if physics.velocity.y.abs() < 0.003 {
107 physics.velocity.y = 0.;
108 }
109
110 if is_player {
111 } else {
113 physics.x_acceleration *= 0.98;
114 physics.z_acceleration *= 0.98;
115 }
116
117 if jumping == Some(&Jumping(true)) {
118 let fluid_height = if physics.is_in_lava() {
119 physics.lava_fluid_height
120 } else if physics.is_in_water() {
121 physics.water_fluid_height
122 } else {
123 0.
124 };
125
126 let in_water = physics.is_in_water() && fluid_height > 0.;
127 let fluid_jump_threshold = travel::fluid_jump_threshold();
128
129 if !in_water || physics.on_ground() && fluid_height <= fluid_jump_threshold {
130 if !physics.is_in_lava()
131 || physics.on_ground() && fluid_height <= fluid_jump_threshold
132 {
133 if (physics.on_ground() || in_water && fluid_height <= fluid_jump_threshold)
134 && physics.no_jump_delay == 0
135 {
136 jump_from_ground(
137 &mut physics,
138 *position,
139 *look_direction,
140 *sprinting,
141 instance_name,
142 &instance_container,
143 );
144 physics.no_jump_delay = 10;
145 }
146 } else {
147 jump_in_liquid(&mut physics);
148 }
149 } else {
150 jump_in_liquid(&mut physics);
151 }
152 } else {
153 physics.no_jump_delay = 0;
154 }
155
156 }
159}
160
161fn jump_in_liquid(physics: &mut Physics) {
162 physics.velocity.y += 0.04;
163}
164
165#[allow(clippy::type_complexity)]
167pub fn apply_effects_from_blocks(
168 mut query: Query<
169 (&mut Physics, &Position, &EntityDimensions, &InstanceName),
170 (With<LocalEntity>, With<HasClientLoaded>),
171 >,
172 instance_container: Res<InstanceContainer>,
173) {
174 for (mut physics, position, dimensions, world_name) in &mut query {
175 let Some(world_lock) = instance_container.get(world_name) else {
176 continue;
177 };
178 let world = world_lock.read();
179
180 let movement_this_tick = [EntityMovement {
193 from: physics.old_position,
194 to: **position,
195 }];
196
197 check_inside_blocks(&mut physics, dimensions, &world, &movement_this_tick);
198 }
199}
200
201fn check_inside_blocks(
202 physics: &mut Physics,
203 dimensions: &EntityDimensions,
204 world: &Instance,
205 movements: &[EntityMovement],
206) -> Vec<BlockState> {
207 let mut blocks_inside = Vec::new();
208 let mut visited_blocks = HashSet::<BlockState>::new();
209
210 for movement in movements {
211 let bounding_box_at_target = dimensions
212 .make_bounding_box(movement.to)
213 .deflate_all(1.0E-5);
214
215 for traversed_block in
216 box_traverse_blocks(movement.from, movement.to, &bounding_box_at_target)
217 {
218 let traversed_block_state = world.get_block_state(traversed_block).unwrap_or_default();
223 if traversed_block_state.is_air() {
224 continue;
225 }
226 if !visited_blocks.insert(traversed_block_state) {
227 continue;
228 }
229
230 let entity_inside_collision_shape = &*BLOCK_SHAPE;
243
244 if entity_inside_collision_shape != &*BLOCK_SHAPE
245 && !collided_with_shape_moving_from(
246 movement.from,
247 movement.to,
248 traversed_block,
249 entity_inside_collision_shape,
250 dimensions,
251 )
252 {
253 continue;
254 }
255
256 handle_entity_inside_block(world, traversed_block_state, traversed_block, physics);
257
258 blocks_inside.push(traversed_block_state);
259 }
260 }
261
262 blocks_inside
263}
264
265fn collided_with_shape_moving_from(
266 from: Vec3,
267 to: Vec3,
268 traversed_block: BlockPos,
269 entity_inside_collision_shape: &VoxelShape,
270 dimensions: &EntityDimensions,
271) -> bool {
272 let bounding_box_from = dimensions.make_bounding_box(from);
273 let delta = to - from;
274 bounding_box_from.collided_along_vector(
275 delta,
276 &entity_inside_collision_shape
277 .move_relative(traversed_block.to_vec3_floored())
278 .to_aabbs(),
279 )
280}
281
282fn handle_entity_inside_block(
284 world: &Instance,
285 block: BlockState,
286 block_pos: BlockPos,
287 physics: &mut Physics,
288) {
289 let registry_block = azalea_registry::Block::from(block);
290 #[allow(clippy::single_match)]
291 match registry_block {
292 azalea_registry::Block::BubbleColumn => {
293 let block_above = world.get_block_state(block_pos.up(1)).unwrap_or_default();
294 let is_block_above_empty =
295 block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty();
296 let drag_down = block
297 .property::<properties::Drag>()
298 .expect("drag property should always be present on bubble columns");
299 let velocity = &mut physics.velocity;
300
301 if is_block_above_empty {
302 let new_y = if drag_down {
303 f64::max(-0.9, velocity.y - 0.03)
304 } else {
305 f64::min(1.8, velocity.y + 0.1)
306 };
307 velocity.y = new_y;
308 } else {
309 let new_y = if drag_down {
310 f64::max(-0.3, velocity.y - 0.03)
311 } else {
312 f64::min(0.7, velocity.y + 0.06)
313 };
314 velocity.y = new_y;
315 physics.reset_fall_distance();
316 }
317 }
318 _ => {}
319 }
320}
321
322pub struct EntityMovement {
323 pub from: Vec3,
324 pub to: Vec3,
325}
326
327pub fn jump_from_ground(
328 physics: &mut Physics,
329 position: Position,
330 look_direction: LookDirection,
331 sprinting: Sprinting,
332 instance_name: &InstanceName,
333 instance_container: &InstanceContainer,
334) {
335 let world_lock = instance_container
336 .get(instance_name)
337 .expect("All entities should be in a valid world");
338 let world = world_lock.read();
339
340 let jump_power: f64 = jump_power(&world, position) as f64 + jump_boost_power();
341 let old_delta_movement = physics.velocity;
342 physics.velocity = Vec3 {
343 x: old_delta_movement.x,
344 y: jump_power,
345 z: old_delta_movement.z,
346 };
347 if *sprinting {
348 let y_rot = look_direction.y_rot() * 0.017453292;
350 physics.velocity += Vec3 {
351 x: (-math::sin(y_rot) * 0.2) as f64,
352 y: 0.,
353 z: (math::cos(y_rot) * 0.2) as f64,
354 };
355 }
356
357 physics.has_impulse = true;
358}
359
360pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) {
361 for (mut physics, position) in &mut query {
362 physics.set_old_pos(*position);
363 }
364}
365
366pub fn get_block_pos_below_that_affects_movement(position: Position) -> BlockPos {
367 BlockPos::new(
368 position.x.floor() as i32,
369 (position.y - 0.5f64).floor() as i32,
371 position.z.floor() as i32,
372 )
373}
374
375fn handle_relative_friction_and_calculate_movement(ctx: &mut MoveCtx, block_friction: f32) -> Vec3 {
376 move_relative(
377 ctx.physics,
378 ctx.direction,
379 get_friction_influenced_speed(ctx.physics, ctx.attributes, block_friction, ctx.sprinting),
380 Vec3::new(
381 ctx.physics.x_acceleration as f64,
382 ctx.physics.y_acceleration as f64,
383 ctx.physics.z_acceleration as f64,
384 ),
385 );
386
387 ctx.physics.velocity = handle_on_climbable(
388 ctx.physics.velocity,
389 ctx.on_climbable,
390 *ctx.position,
391 ctx.world,
392 ctx.pose,
393 );
394
395 move_colliding(ctx, ctx.physics.velocity).expect("Entity should exist");
396 if ctx.physics.horizontal_collision || *ctx.jumping {
404 let block_at_feet: Block = ctx
405 .world
406 .chunks
407 .get_block_state(BlockPos::from(*ctx.position))
408 .unwrap_or_default()
409 .into();
410
411 if *ctx.on_climbable || block_at_feet == Block::PowderSnow {
412 ctx.physics.velocity.y = 0.2;
413 }
414 }
415
416 ctx.physics.velocity
417}
418
419fn handle_on_climbable(
420 velocity: Vec3,
421 on_climbable: OnClimbable,
422 position: Position,
423 world: &Instance,
424 pose: Option<Pose>,
425) -> Vec3 {
426 if !*on_climbable {
427 return velocity;
428 }
429
430 const CLIMBING_SPEED: f64 = 0.15_f32 as f64;
433
434 let x = f64::clamp(velocity.x, -CLIMBING_SPEED, CLIMBING_SPEED);
435 let z = f64::clamp(velocity.z, -CLIMBING_SPEED, CLIMBING_SPEED);
436 let mut y = f64::max(velocity.y, -CLIMBING_SPEED);
437
438 if y < 0.0
440 && pose == Some(Pose::Crouching)
441 && azalea_registry::Block::from(
442 world
443 .chunks
444 .get_block_state(position.into())
445 .unwrap_or_default(),
446 ) != azalea_registry::Block::Scaffolding
447 {
448 y = 0.;
449 }
450
451 Vec3 { x, y, z }
452}
453
454fn get_friction_influenced_speed(
458 physics: &Physics,
459 attributes: &Attributes,
460 friction: f32,
461 sprinting: Sprinting,
462) -> f32 {
463 if physics.on_ground() {
465 let speed = attributes.movement_speed.calculate() as f32;
466 speed * (0.21600002f32 / (friction * friction * friction))
467 } else {
468 if *sprinting { 0.025999999f32 } else { 0.02 }
470 }
471}
472
473fn block_jump_factor(world: &Instance, position: Position) -> f32 {
476 let block_at_pos = world.chunks.get_block_state(position.into());
477 let block_below = world
478 .chunks
479 .get_block_state(get_block_pos_below_that_affects_movement(position));
480
481 let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
482 Box::<dyn BlockTrait>::from(block).behavior().jump_factor
483 } else {
484 1.
485 };
486 if block_at_pos_jump_factor != 1. {
487 return block_at_pos_jump_factor;
488 }
489
490 if let Some(block) = block_below {
491 Box::<dyn BlockTrait>::from(block).behavior().jump_factor
492 } else {
493 1.
494 }
495}
496
497fn jump_power(world: &Instance, position: Position) -> f32 {
504 0.42 * block_jump_factor(world, position)
505}
506
507fn jump_boost_power() -> f64 {
508 0.
519}