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