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::{BlockState, BlockTrait, fluid_state::FluidState, properties};
12use azalea_core::{
13 math,
14 position::{BlockPos, Vec3},
15 tick::GameTick,
16};
17use azalea_entity::{
18 Attributes, EntityKindComponent, InLoadedChunk, Jumping, LocalEntity, LookDirection,
19 OnClimbable, Physics, Pose, Position, metadata::Sprinting, move_relative,
20};
21use azalea_registry::{Block, EntityKind};
22use azalea_world::{Instance, InstanceContainer, InstanceName};
23use bevy_app::{App, Plugin};
24use bevy_ecs::prelude::*;
25use clip::box_traverse_blocks;
26use collision::{
27 BLOCK_SHAPE, BlockWithShape, MoverType, VoxelShape,
28 entity_collisions::{CollidableEntityQuery, PhysicsQuery},
29 move_colliding,
30};
31
32#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
34pub struct PhysicsSet;
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(PhysicsSet)
51 .after(azalea_entity::update_in_loaded_chunk),
52 );
53 }
54}
55
56#[allow(clippy::type_complexity)]
60pub fn ai_step(
61 mut query: Query<
62 (
63 &mut Physics,
64 Option<&Jumping>,
65 &Position,
66 &LookDirection,
67 &Sprinting,
68 &InstanceName,
69 &EntityKindComponent,
70 ),
71 (With<LocalEntity>, With<InLoadedChunk>),
72 >,
73 instance_container: Res<InstanceContainer>,
74) {
75 for (mut physics, jumping, position, look_direction, sprinting, instance_name, entity_kind) in
76 &mut query
77 {
78 let is_player = **entity_kind == EntityKind::Player;
79
80 if physics.no_jump_delay > 0 {
84 physics.no_jump_delay -= 1;
85 }
86
87 if is_player {
88 if physics.velocity.horizontal_distance_squared() < 9.0e-6 {
89 physics.velocity.x = 0.;
90 physics.velocity.z = 0.;
91 }
92 } else {
93 if physics.velocity.x.abs() < 0.003 {
94 physics.velocity.x = 0.;
95 }
96 if physics.velocity.z.abs() < 0.003 {
97 physics.velocity.z = 0.;
98 }
99 }
100
101 if physics.velocity.y.abs() < 0.003 {
102 physics.velocity.y = 0.;
103 }
104
105 if is_player {
106 } else {
108 physics.x_acceleration *= 0.98;
109 physics.z_acceleration *= 0.98;
110 }
111
112 if jumping == Some(&Jumping(true)) {
113 let fluid_height = if physics.is_in_lava() {
114 physics.lava_fluid_height
115 } else if physics.is_in_water() {
116 physics.water_fluid_height
117 } else {
118 0.
119 };
120
121 let in_water = physics.is_in_water() && fluid_height > 0.;
122 let fluid_jump_threshold = travel::fluid_jump_threshold();
123
124 if !in_water || physics.on_ground() && fluid_height <= fluid_jump_threshold {
125 if !physics.is_in_lava()
126 || physics.on_ground() && fluid_height <= fluid_jump_threshold
127 {
128 if (physics.on_ground() || in_water && fluid_height <= fluid_jump_threshold)
129 && physics.no_jump_delay == 0
130 {
131 jump_from_ground(
132 &mut physics,
133 *position,
134 *look_direction,
135 *sprinting,
136 instance_name,
137 &instance_container,
138 );
139 physics.no_jump_delay = 10;
140 }
141 } else {
142 jump_in_liquid(&mut physics);
143 }
144 } else {
145 jump_in_liquid(&mut physics);
146 }
147 } else {
148 physics.no_jump_delay = 0;
149 }
150
151 }
154}
155
156fn jump_in_liquid(physics: &mut Physics) {
157 physics.velocity.y += 0.04;
158}
159
160#[allow(clippy::type_complexity)]
162pub fn apply_effects_from_blocks(
163 mut query: Query<
164 (&mut Physics, &Position, &InstanceName),
165 (With<LocalEntity>, With<InLoadedChunk>),
166 >,
167 instance_container: Res<InstanceContainer>,
168) {
169 for (mut physics, position, world_name) in &mut query {
170 let Some(world_lock) = instance_container.get(world_name) else {
171 continue;
172 };
173 let world = world_lock.read();
174
175 let movement_this_tick = [EntityMovement {
188 from: physics.old_position,
189 to: **position,
190 }];
191
192 check_inside_blocks(&mut physics, &world, &movement_this_tick);
193 }
194}
195
196fn check_inside_blocks(
197 physics: &mut Physics,
198 world: &Instance,
199 movements: &[EntityMovement],
200) -> Vec<BlockState> {
201 let mut blocks_inside = Vec::new();
202 let mut visited_blocks = HashSet::<BlockState>::new();
203
204 for movement in movements {
205 let bounding_box_at_target = physics
206 .dimensions
207 .make_bounding_box(movement.to)
208 .deflate_all(1.0E-5);
209
210 for traversed_block in
211 box_traverse_blocks(movement.from, movement.to, &bounding_box_at_target)
212 {
213 let traversed_block_state = world.get_block_state(traversed_block).unwrap_or_default();
218 if traversed_block_state.is_air() {
219 continue;
220 }
221 if !visited_blocks.insert(traversed_block_state) {
222 continue;
223 }
224
225 let entity_inside_collision_shape = &*BLOCK_SHAPE;
238
239 if entity_inside_collision_shape != &*BLOCK_SHAPE
240 && !collided_with_shape_moving_from(
241 movement.from,
242 movement.to,
243 traversed_block,
244 entity_inside_collision_shape,
245 physics,
246 )
247 {
248 continue;
249 }
250
251 handle_entity_inside_block(world, traversed_block_state, traversed_block, physics);
252
253 blocks_inside.push(traversed_block_state);
254 }
255 }
256
257 blocks_inside
258}
259
260fn collided_with_shape_moving_from(
261 from: Vec3,
262 to: Vec3,
263 traversed_block: BlockPos,
264 entity_inside_collision_shape: &VoxelShape,
265 physics: &Physics,
266) -> bool {
267 let bounding_box_from = physics.dimensions.make_bounding_box(from);
268 let delta = to - from;
269 bounding_box_from.collided_along_vector(
270 delta,
271 &entity_inside_collision_shape
272 .move_relative(traversed_block.to_vec3_floored())
273 .to_aabbs(),
274 )
275}
276
277fn handle_entity_inside_block(
279 world: &Instance,
280 block: BlockState,
281 block_pos: BlockPos,
282 physics: &mut Physics,
283) {
284 let registry_block = azalea_registry::Block::from(block);
285 #[allow(clippy::single_match)]
286 match registry_block {
287 azalea_registry::Block::BubbleColumn => {
288 let block_above = world.get_block_state(block_pos.up(1)).unwrap_or_default();
289 let is_block_above_empty =
290 block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty();
291 let drag_down = block
292 .property::<properties::Drag>()
293 .expect("drag property should always be present on bubble columns");
294 let velocity = &mut physics.velocity;
295
296 if is_block_above_empty {
297 let new_y = if drag_down {
298 f64::max(-0.9, velocity.y - 0.03)
299 } else {
300 f64::min(1.8, velocity.y + 0.1)
301 };
302 velocity.y = new_y;
303 } else {
304 let new_y = if drag_down {
305 f64::max(-0.3, velocity.y - 0.03)
306 } else {
307 f64::min(0.7, velocity.y + 0.06)
308 };
309 velocity.y = new_y;
310 physics.reset_fall_distance();
311 }
312 }
313 _ => {}
314 }
315}
316
317pub struct EntityMovement {
318 pub from: Vec3,
319 pub to: Vec3,
320}
321
322pub fn jump_from_ground(
323 physics: &mut Physics,
324 position: Position,
325 look_direction: LookDirection,
326 sprinting: Sprinting,
327 instance_name: &InstanceName,
328 instance_container: &InstanceContainer,
329) {
330 let world_lock = instance_container
331 .get(instance_name)
332 .expect("All entities should be in a valid world");
333 let world = world_lock.read();
334
335 let jump_power: f64 = jump_power(&world, position) as f64 + jump_boost_power();
336 let old_delta_movement = physics.velocity;
337 physics.velocity = Vec3 {
338 x: old_delta_movement.x,
339 y: jump_power,
340 z: old_delta_movement.z,
341 };
342 if *sprinting {
343 let y_rot = look_direction.y_rot * 0.017453292;
345 physics.velocity += Vec3 {
346 x: (-math::sin(y_rot) * 0.2) as f64,
347 y: 0.,
348 z: (math::cos(y_rot) * 0.2) as f64,
349 };
350 }
351
352 physics.has_impulse = true;
353}
354
355pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) {
356 for (mut physics, position) in &mut query {
357 physics.set_old_pos(*position);
358 }
359}
360
361fn get_block_pos_below_that_affects_movement(position: Position) -> BlockPos {
362 BlockPos::new(
363 position.x.floor() as i32,
364 (position.y - 0.5f64).floor() as i32,
366 position.z.floor() as i32,
367 )
368}
369
370struct HandleRelativeFrictionAndCalculateMovementOpts<'a, 'b, 'world, 'state> {
372 block_friction: f32,
373 world: &'a Instance,
374 physics: &'a mut Physics,
375 direction: LookDirection,
376 position: Mut<'a, Position>,
377 attributes: &'a Attributes,
378 is_sprinting: bool,
379 on_climbable: OnClimbable,
380 pose: Option<Pose>,
381 jumping: Jumping,
382 entity: Entity,
383 physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
384 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
385}
386fn handle_relative_friction_and_calculate_movement(
387 HandleRelativeFrictionAndCalculateMovementOpts {
388 block_friction,
389 world,
390 physics,
391 direction,
392 mut position,
393 attributes,
394 is_sprinting,
395 on_climbable,
396 pose,
397 jumping,
398 entity,
399 physics_query,
400 collidable_entity_query,
401 }: HandleRelativeFrictionAndCalculateMovementOpts<'_, '_, '_, '_>,
402) -> Vec3 {
403 move_relative(
404 physics,
405 direction,
406 get_friction_influenced_speed(physics, attributes, block_friction, is_sprinting),
407 Vec3::new(
408 physics.x_acceleration as f64,
409 physics.y_acceleration as f64,
410 physics.z_acceleration as f64,
411 ),
412 );
413
414 physics.velocity = handle_on_climbable(physics.velocity, on_climbable, *position, world, pose);
415
416 move_colliding(
417 MoverType::Own,
418 physics.velocity,
419 world,
420 &mut position,
421 physics,
422 Some(entity),
423 physics_query,
424 collidable_entity_query,
425 )
426 .expect("Entity should exist");
427 if physics.horizontal_collision || *jumping {
435 let block_at_feet: Block = world
436 .chunks
437 .get_block_state((*position).into())
438 .unwrap_or_default()
439 .into();
440
441 if *on_climbable || block_at_feet == Block::PowderSnow {
442 physics.velocity.y = 0.2;
443 }
444 }
445
446 physics.velocity
447}
448
449fn handle_on_climbable(
450 velocity: Vec3,
451 on_climbable: OnClimbable,
452 position: Position,
453 world: &Instance,
454 pose: Option<Pose>,
455) -> Vec3 {
456 if !*on_climbable {
457 return velocity;
458 }
459
460 const CLIMBING_SPEED: f64 = 0.15_f32 as f64;
463
464 let x = f64::clamp(velocity.x, -CLIMBING_SPEED, CLIMBING_SPEED);
465 let z = f64::clamp(velocity.z, -CLIMBING_SPEED, CLIMBING_SPEED);
466 let mut y = f64::max(velocity.y, -CLIMBING_SPEED);
467
468 if y < 0.0
470 && pose == Some(Pose::Sneaking)
471 && azalea_registry::Block::from(
472 world
473 .chunks
474 .get_block_state(position.into())
475 .unwrap_or_default(),
476 ) != azalea_registry::Block::Scaffolding
477 {
478 y = 0.;
479 }
480
481 Vec3 { x, y, z }
482}
483
484fn get_friction_influenced_speed(
488 physics: &Physics,
489 attributes: &Attributes,
490 friction: f32,
491 is_sprinting: bool,
492) -> f32 {
493 if physics.on_ground() {
495 let speed: f32 = attributes.speed.calculate() as f32;
496 speed * (0.216f32 / (friction * friction * friction))
497 } else {
498 if is_sprinting { 0.025999999f32 } else { 0.02 }
500 }
501}
502
503fn block_jump_factor(world: &Instance, position: Position) -> f32 {
506 let block_at_pos = world.chunks.get_block_state(position.into());
507 let block_below = world
508 .chunks
509 .get_block_state(get_block_pos_below_that_affects_movement(position));
510
511 let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
512 Box::<dyn BlockTrait>::from(block).behavior().jump_factor
513 } else {
514 1.
515 };
516 if block_at_pos_jump_factor != 1. {
517 return block_at_pos_jump_factor;
518 }
519
520 if let Some(block) = block_below {
521 Box::<dyn BlockTrait>::from(block).behavior().jump_factor
522 } else {
523 1.
524 }
525}
526
527fn jump_power(world: &Instance, position: Position) -> f32 {
534 0.42 * block_jump_factor(world, position)
535}
536
537fn jump_boost_power() -> f64 {
538 0.
549}