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::prelude::*;
24use clip::box_traverse_blocks;
25use collision::{
26 BLOCK_SHAPE, BlockWithShape, MoverType, VoxelShape,
27 entity_collisions::{CollidableEntityQuery, PhysicsQuery},
28 move_colliding,
29};
30
31#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
33pub struct PhysicsSet;
34
35pub struct PhysicsPlugin;
36impl Plugin for PhysicsPlugin {
37 fn build(&self, app: &mut App) {
38 app.add_systems(
39 GameTick,
40 (
41 fluids::update_in_water_state_and_do_fluid_pushing
42 .before(azalea_entity::update_fluid_on_eyes),
43 update_old_position,
44 fluids::update_swimming.after(azalea_entity::update_fluid_on_eyes),
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 ),
70 (With<LocalEntity>, With<InLoadedChunk>),
71 >,
72 instance_container: Res<InstanceContainer>,
73) {
74 for (mut physics, jumping, position, look_direction, sprinting, instance_name) in &mut query {
75 if physics.no_jump_delay > 0 {
79 physics.no_jump_delay -= 1;
80 }
81
82 if physics.velocity.x.abs() < 0.003 {
83 physics.velocity.x = 0.;
84 }
85 if physics.velocity.y.abs() < 0.003 {
86 physics.velocity.y = 0.;
87 }
88 if physics.velocity.z.abs() < 0.003 {
89 physics.velocity.z = 0.;
90 }
91
92 if jumping == Some(&Jumping(true)) {
93 let fluid_height = if physics.is_in_lava() {
94 physics.lava_fluid_height
95 } else if physics.is_in_water() {
96 physics.water_fluid_height
97 } else {
98 0.
99 };
100
101 let in_water = physics.is_in_water() && fluid_height > 0.;
102 let fluid_jump_threshold = travel::fluid_jump_threshold();
103
104 if !in_water || physics.on_ground() && fluid_height <= fluid_jump_threshold {
105 if !physics.is_in_lava()
106 || physics.on_ground() && fluid_height <= fluid_jump_threshold
107 {
108 if (physics.on_ground() || in_water && fluid_height <= fluid_jump_threshold)
109 && physics.no_jump_delay == 0
110 {
111 jump_from_ground(
112 &mut physics,
113 position,
114 look_direction,
115 sprinting,
116 instance_name,
117 &instance_container,
118 );
119 physics.no_jump_delay = 10;
120 }
121 } else {
122 jump_in_liquid(&mut physics);
123 }
124 } else {
125 jump_in_liquid(&mut physics);
126 }
127 } else {
128 physics.no_jump_delay = 0;
129 }
130
131 physics.x_acceleration *= 0.98;
132 physics.z_acceleration *= 0.98;
133
134 }
137}
138
139fn jump_in_liquid(physics: &mut Physics) {
140 physics.velocity.y += 0.04;
141}
142
143#[allow(clippy::type_complexity)]
145pub fn apply_effects_from_blocks(
146 mut query: Query<
147 (&mut Physics, &Position, &InstanceName),
148 (With<LocalEntity>, With<InLoadedChunk>),
149 >,
150 instance_container: Res<InstanceContainer>,
151) {
152 for (mut physics, position, world_name) in &mut query {
153 let Some(world_lock) = instance_container.get(world_name) else {
154 continue;
155 };
156 let world = world_lock.read();
157
158 let movement_this_tick = [EntityMovement {
171 from: physics.old_position,
172 to: **position,
173 }];
174
175 check_inside_blocks(&mut physics, &world, &movement_this_tick);
176 }
177}
178
179fn check_inside_blocks(
180 physics: &mut Physics,
181 world: &Instance,
182 movements: &[EntityMovement],
183) -> Vec<BlockState> {
184 let mut blocks_inside = Vec::new();
185 let mut visited_blocks = HashSet::<BlockState>::new();
186
187 for movement in movements {
188 let bounding_box_at_target = physics
189 .dimensions
190 .make_bounding_box(&movement.to)
191 .deflate_all(1.0E-5);
192
193 for traversed_block in
194 box_traverse_blocks(&movement.from, &movement.to, &bounding_box_at_target)
195 {
196 let traversed_block_state = world.get_block_state(&traversed_block).unwrap_or_default();
201 if traversed_block_state.is_air() {
202 continue;
203 }
204 if !visited_blocks.insert(traversed_block_state) {
205 continue;
206 }
207
208 let entity_inside_collision_shape = &*BLOCK_SHAPE;
221
222 if entity_inside_collision_shape != &*BLOCK_SHAPE
223 && !collided_with_shape_moving_from(
224 &movement.from,
225 &movement.to,
226 traversed_block,
227 entity_inside_collision_shape,
228 physics,
229 )
230 {
231 continue;
232 }
233
234 handle_entity_inside_block(world, traversed_block_state, traversed_block, physics);
235
236 blocks_inside.push(traversed_block_state);
237 }
238 }
239
240 blocks_inside
241}
242
243fn collided_with_shape_moving_from(
244 from: &Vec3,
245 to: &Vec3,
246 traversed_block: BlockPos,
247 entity_inside_collision_shape: &VoxelShape,
248 physics: &Physics,
249) -> bool {
250 let bounding_box_from = physics.dimensions.make_bounding_box(from);
251 let delta = to - from;
252 bounding_box_from.collided_along_vector(
253 delta,
254 &entity_inside_collision_shape
255 .move_relative(traversed_block.to_vec3_floored())
256 .to_aabbs(),
257 )
258}
259
260fn handle_entity_inside_block(
262 world: &Instance,
263 block: BlockState,
264 block_pos: BlockPos,
265 physics: &mut Physics,
266) {
267 let registry_block = azalea_registry::Block::from(block);
268 #[allow(clippy::single_match)]
269 match registry_block {
270 azalea_registry::Block::BubbleColumn => {
271 let block_above = world.get_block_state(&block_pos.up(1)).unwrap_or_default();
272 let is_block_above_empty =
273 block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty();
274 let drag_down = block
275 .property::<properties::Drag>()
276 .expect("drag property should always be present on bubble columns");
277 let velocity = &mut physics.velocity;
278
279 if is_block_above_empty {
280 let new_y = if drag_down {
281 f64::max(-0.9, velocity.y - 0.03)
282 } else {
283 f64::min(1.8, velocity.y + 0.1)
284 };
285 velocity.y = new_y;
286 } else {
287 let new_y = if drag_down {
288 f64::max(-0.3, velocity.y - 0.03)
289 } else {
290 f64::min(0.7, velocity.y + 0.06)
291 };
292 velocity.y = new_y;
293 physics.reset_fall_distance();
294 }
295 }
296 _ => {}
297 }
298}
299
300pub struct EntityMovement {
301 pub from: Vec3,
302 pub to: Vec3,
303}
304
305pub fn jump_from_ground(
306 physics: &mut Physics,
307 position: &Position,
308 look_direction: &LookDirection,
309 sprinting: &Sprinting,
310 instance_name: &InstanceName,
311 instance_container: &InstanceContainer,
312) {
313 let world_lock = instance_container
314 .get(instance_name)
315 .expect("All entities should be in a valid world");
316 let world = world_lock.read();
317
318 let jump_power: f64 = jump_power(&world, position) as f64 + jump_boost_power();
319 let old_delta_movement = physics.velocity;
320 physics.velocity = Vec3 {
321 x: old_delta_movement.x,
322 y: jump_power,
323 z: old_delta_movement.z,
324 };
325 if **sprinting {
326 let y_rot = look_direction.y_rot * 0.017453292;
328 physics.velocity += Vec3 {
329 x: (-math::sin(y_rot) * 0.2) as f64,
330 y: 0.,
331 z: (math::cos(y_rot) * 0.2) as f64,
332 };
333 }
334
335 physics.has_impulse = true;
336}
337
338pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) {
339 for (mut physics, position) in &mut query {
340 physics.set_old_pos(position);
341 }
342}
343
344fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos {
345 BlockPos::new(
346 position.x.floor() as i32,
347 (position.y - 0.5f64).floor() as i32,
349 position.z.floor() as i32,
350 )
351}
352
353struct HandleRelativeFrictionAndCalculateMovementOpts<'a, 'b, 'world, 'state> {
355 block_friction: f32,
356 world: &'a Instance,
357 physics: &'a mut Physics,
358 direction: &'a LookDirection,
359 position: Mut<'a, Position>,
360 attributes: &'a Attributes,
361 is_sprinting: bool,
362 on_climbable: &'a OnClimbable,
363 pose: Option<&'a Pose>,
364 jumping: &'a Jumping,
365 entity: Entity,
366 physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
367 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
368}
369fn handle_relative_friction_and_calculate_movement(
370 HandleRelativeFrictionAndCalculateMovementOpts {
371 block_friction,
372 world,
373 physics,
374 direction,
375 mut position,
376 attributes,
377 is_sprinting,
378 on_climbable,
379 pose,
380 jumping,
381 entity,
382 physics_query,
383 collidable_entity_query,
384 }: HandleRelativeFrictionAndCalculateMovementOpts<'_, '_, '_, '_>,
385) -> Vec3 {
386 move_relative(
387 physics,
388 direction,
389 get_friction_influenced_speed(physics, attributes, block_friction, is_sprinting),
390 &Vec3 {
391 x: physics.x_acceleration as f64,
392 y: physics.y_acceleration as f64,
393 z: physics.z_acceleration as f64,
394 },
395 );
396
397 physics.velocity = handle_on_climbable(physics.velocity, on_climbable, &position, world, pose);
398
399 move_colliding(
400 MoverType::Own,
401 &physics.velocity.clone(),
402 world,
403 &mut position,
404 physics,
405 Some(entity),
406 physics_query,
407 collidable_entity_query,
408 )
409 .expect("Entity should exist");
410 if physics.horizontal_collision || **jumping {
418 let block_at_feet: azalea_registry::Block = world
419 .chunks
420 .get_block_state(&(*position).into())
421 .unwrap_or_default()
422 .into();
423
424 if **on_climbable || block_at_feet == azalea_registry::Block::PowderSnow {
425 physics.velocity.y = 0.2;
426 }
427 }
428
429 physics.velocity
430}
431
432fn handle_on_climbable(
433 velocity: Vec3,
434 on_climbable: &OnClimbable,
435 position: &Position,
436 world: &Instance,
437 pose: Option<&Pose>,
438) -> Vec3 {
439 if !**on_climbable {
440 return velocity;
441 }
442
443 const CLIMBING_SPEED: f64 = 0.15_f32 as f64;
446
447 let x = f64::clamp(velocity.x, -CLIMBING_SPEED, CLIMBING_SPEED);
448 let z = f64::clamp(velocity.z, -CLIMBING_SPEED, CLIMBING_SPEED);
449 let mut y = f64::max(velocity.y, -CLIMBING_SPEED);
450
451 if y < 0.0
453 && pose.copied() == Some(Pose::Sneaking)
454 && azalea_registry::Block::from(
455 world
456 .chunks
457 .get_block_state(&position.into())
458 .unwrap_or_default(),
459 ) != azalea_registry::Block::Scaffolding
460 {
461 y = 0.;
462 }
463
464 Vec3 { x, y, z }
465}
466
467fn get_friction_influenced_speed(
471 physics: &Physics,
472 attributes: &Attributes,
473 friction: f32,
474 is_sprinting: bool,
475) -> f32 {
476 if physics.on_ground() {
478 let speed: f32 = attributes.speed.calculate() as f32;
479 speed * (0.216f32 / (friction * friction * friction))
480 } else {
481 if is_sprinting { 0.025999999f32 } else { 0.02 }
483 }
484}
485
486fn block_jump_factor(world: &Instance, position: &Position) -> f32 {
489 let block_at_pos = world.chunks.get_block_state(&position.into());
490 let block_below = world
491 .chunks
492 .get_block_state(&get_block_pos_below_that_affects_movement(position));
493
494 let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
495 Box::<dyn Block>::from(block).behavior().jump_factor
496 } else {
497 1.
498 };
499 if block_at_pos_jump_factor != 1. {
500 return block_at_pos_jump_factor;
501 }
502
503 if let Some(block) = block_below {
504 Box::<dyn Block>::from(block).behavior().jump_factor
505 } else {
506 1.
507 }
508}
509
510fn jump_power(world: &Instance, position: &Position) -> f32 {
517 0.42 * block_jump_factor(world, position)
518}
519
520fn jump_boost_power() -> f64 {
521 0.
532}