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 ActiveEffects, Attributes, EntityKindComponent, HasClientLoaded, Jumping, LocalEntity,
20 LookDirection, OnClimbable, Physics, Pose, Position, dimensions::EntityDimensions,
21 metadata::Sprinting, move_relative,
22};
23use azalea_registry::{Block, EntityKind, MobEffect};
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 &ActiveEffects,
74 &InstanceName,
75 &EntityKindComponent,
76 ),
77 (With<LocalEntity>, With<HasClientLoaded>),
78 >,
79 instance_container: Res<InstanceContainer>,
80) {
81 for (
82 mut physics,
83 jumping,
84 position,
85 look_direction,
86 sprinting,
87 active_effects,
88 instance_name,
89 entity_kind,
90 ) in &mut query
91 {
92 let is_player = **entity_kind == EntityKind::Player;
93
94 if physics.no_jump_delay > 0 {
98 physics.no_jump_delay -= 1;
99 }
100
101 if is_player {
102 if physics.velocity.horizontal_distance_squared() < 9.0e-6 {
103 physics.velocity.x = 0.;
104 physics.velocity.z = 0.;
105 }
106 } else {
107 if physics.velocity.x.abs() < 0.003 {
108 physics.velocity.x = 0.;
109 }
110 if physics.velocity.z.abs() < 0.003 {
111 physics.velocity.z = 0.;
112 }
113 }
114
115 if physics.velocity.y.abs() < 0.003 {
116 physics.velocity.y = 0.;
117 }
118
119 if is_player {
120 } else {
122 physics.x_acceleration *= 0.98;
123 physics.z_acceleration *= 0.98;
124 }
125
126 if jumping == Some(&Jumping(true)) {
127 let fluid_height = if physics.is_in_lava() {
128 physics.lava_fluid_height
129 } else if physics.is_in_water() {
130 physics.water_fluid_height
131 } else {
132 0.
133 };
134
135 let in_water = physics.is_in_water() && fluid_height > 0.;
136 let fluid_jump_threshold = travel::fluid_jump_threshold();
137
138 if !in_water || physics.on_ground() && fluid_height <= fluid_jump_threshold {
139 if !physics.is_in_lava()
140 || physics.on_ground() && fluid_height <= fluid_jump_threshold
141 {
142 if (physics.on_ground() || in_water && fluid_height <= fluid_jump_threshold)
143 && physics.no_jump_delay == 0
144 {
145 jump_from_ground(
146 &mut physics,
147 *position,
148 *look_direction,
149 *sprinting,
150 instance_name,
151 &instance_container,
152 active_effects,
153 );
154 physics.no_jump_delay = 10;
155 }
156 } else {
157 jump_in_liquid(&mut physics);
158 }
159 } else {
160 jump_in_liquid(&mut physics);
161 }
162 } else {
163 physics.no_jump_delay = 0;
164 }
165
166 }
169}
170
171fn jump_in_liquid(physics: &mut Physics) {
172 physics.velocity.y += 0.04;
173}
174
175#[allow(clippy::type_complexity)]
177pub fn apply_effects_from_blocks(
178 mut query: Query<
179 (&mut Physics, &Position, &EntityDimensions, &InstanceName),
180 (With<LocalEntity>, With<HasClientLoaded>),
181 >,
182 instance_container: Res<InstanceContainer>,
183) {
184 for (mut physics, position, dimensions, world_name) in &mut query {
185 let Some(world_lock) = instance_container.get(world_name) else {
186 continue;
187 };
188 let world = world_lock.read();
189
190 let movement_this_tick = [EntityMovement {
203 from: physics.old_position,
204 to: **position,
205 }];
206
207 check_inside_blocks(&mut physics, dimensions, &world, &movement_this_tick);
208 }
209}
210
211fn check_inside_blocks(
212 physics: &mut Physics,
213 dimensions: &EntityDimensions,
214 world: &Instance,
215 movements: &[EntityMovement],
216) -> Vec<BlockState> {
217 let mut blocks_inside = Vec::new();
218 let mut visited_blocks = HashSet::<BlockState>::new();
219
220 for movement in movements {
221 let bounding_box_at_target = dimensions
222 .make_bounding_box(movement.to)
223 .deflate_all(1.0E-5);
224
225 for traversed_block in
226 box_traverse_blocks(movement.from, movement.to, &bounding_box_at_target)
227 {
228 let traversed_block_state = world.get_block_state(traversed_block).unwrap_or_default();
233 if traversed_block_state.is_air() {
234 continue;
235 }
236 if !visited_blocks.insert(traversed_block_state) {
237 continue;
238 }
239
240 let entity_inside_collision_shape = &*BLOCK_SHAPE;
253
254 if entity_inside_collision_shape != &*BLOCK_SHAPE
255 && !collided_with_shape_moving_from(
256 movement.from,
257 movement.to,
258 traversed_block,
259 entity_inside_collision_shape,
260 dimensions,
261 )
262 {
263 continue;
264 }
265
266 handle_entity_inside_block(world, traversed_block_state, traversed_block, physics);
267
268 blocks_inside.push(traversed_block_state);
269 }
270 }
271
272 blocks_inside
273}
274
275fn collided_with_shape_moving_from(
276 from: Vec3,
277 to: Vec3,
278 traversed_block: BlockPos,
279 entity_inside_collision_shape: &VoxelShape,
280 dimensions: &EntityDimensions,
281) -> bool {
282 let bounding_box_from = dimensions.make_bounding_box(from);
283 let delta = to - from;
284 bounding_box_from.collided_along_vector(
285 delta,
286 &entity_inside_collision_shape
287 .move_relative(traversed_block.to_vec3_floored())
288 .to_aabbs(),
289 )
290}
291
292fn handle_entity_inside_block(
294 world: &Instance,
295 block: BlockState,
296 block_pos: BlockPos,
297 physics: &mut Physics,
298) {
299 let registry_block = azalea_registry::Block::from(block);
300 #[allow(clippy::single_match)]
301 match registry_block {
302 azalea_registry::Block::BubbleColumn => {
303 let block_above = world.get_block_state(block_pos.up(1)).unwrap_or_default();
304 let is_block_above_empty =
305 block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty();
306 let drag_down = block
307 .property::<properties::Drag>()
308 .expect("drag property should always be present on bubble columns");
309 let velocity = &mut physics.velocity;
310
311 if is_block_above_empty {
312 let new_y = if drag_down {
313 f64::max(-0.9, velocity.y - 0.03)
314 } else {
315 f64::min(1.8, velocity.y + 0.1)
316 };
317 velocity.y = new_y;
318 } else {
319 let new_y = if drag_down {
320 f64::max(-0.3, velocity.y - 0.03)
321 } else {
322 f64::min(0.7, velocity.y + 0.06)
323 };
324 velocity.y = new_y;
325 physics.reset_fall_distance();
326 }
327 }
328 _ => {}
329 }
330}
331
332pub struct EntityMovement {
333 pub from: Vec3,
334 pub to: Vec3,
335}
336
337pub fn jump_from_ground(
338 physics: &mut Physics,
339 position: Position,
340 look_direction: LookDirection,
341 sprinting: Sprinting,
342 instance_name: &InstanceName,
343 instance_container: &InstanceContainer,
344 active_effects: &ActiveEffects,
345) {
346 let world_lock = instance_container
347 .get(instance_name)
348 .expect("All entities should be in a valid world");
349 let world = world_lock.read();
350
351 let base_jump = jump_power(&world, position);
352 let jump_power = base_jump + jump_boost_power(active_effects);
353 if jump_power <= 1.0E-5 {
354 return;
355 }
356
357 let old_delta_movement = physics.velocity;
358 physics.velocity = Vec3 {
359 x: old_delta_movement.x,
360 y: f64::max(jump_power as f64, old_delta_movement.y),
361 z: old_delta_movement.z,
362 };
363 if *sprinting {
364 let y_rot = look_direction.y_rot() * 0.017453292;
366 physics.velocity += Vec3 {
367 x: (-math::sin(y_rot) * 0.2) as f64,
368 y: 0.,
369 z: (math::cos(y_rot) * 0.2) as f64,
370 };
371 }
372
373 physics.has_impulse = true;
374}
375
376pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) {
377 for (mut physics, position) in &mut query {
378 physics.set_old_pos(*position);
379 }
380}
381
382pub fn get_block_pos_below_that_affects_movement(position: Position) -> BlockPos {
383 BlockPos::new(
384 position.x.floor() as i32,
385 (position.y - 0.5f64).floor() as i32,
387 position.z.floor() as i32,
388 )
389}
390
391fn handle_relative_friction_and_calculate_movement(ctx: &mut MoveCtx, block_friction: f32) -> Vec3 {
392 move_relative(
393 ctx.physics,
394 ctx.direction,
395 get_friction_influenced_speed(ctx.physics, ctx.attributes, block_friction, ctx.sprinting),
396 Vec3::new(
397 ctx.physics.x_acceleration as f64,
398 ctx.physics.y_acceleration as f64,
399 ctx.physics.z_acceleration as f64,
400 ),
401 );
402
403 ctx.physics.velocity = handle_on_climbable(
404 ctx.physics.velocity,
405 ctx.on_climbable,
406 *ctx.position,
407 ctx.world,
408 ctx.pose,
409 );
410
411 move_colliding(ctx, ctx.physics.velocity);
412 if ctx.physics.horizontal_collision || *ctx.jumping {
420 let block_at_feet: Block = ctx
421 .world
422 .chunks
423 .get_block_state(BlockPos::from(*ctx.position))
424 .unwrap_or_default()
425 .into();
426
427 if *ctx.on_climbable || block_at_feet == Block::PowderSnow {
428 ctx.physics.velocity.y = 0.2;
429 }
430 }
431
432 ctx.physics.velocity
433}
434
435fn handle_on_climbable(
436 velocity: Vec3,
437 on_climbable: OnClimbable,
438 position: Position,
439 world: &Instance,
440 pose: Option<Pose>,
441) -> Vec3 {
442 if !*on_climbable {
443 return velocity;
444 }
445
446 const CLIMBING_SPEED: f64 = 0.15_f32 as f64;
449
450 let x = f64::clamp(velocity.x, -CLIMBING_SPEED, CLIMBING_SPEED);
451 let z = f64::clamp(velocity.z, -CLIMBING_SPEED, CLIMBING_SPEED);
452 let mut y = f64::max(velocity.y, -CLIMBING_SPEED);
453
454 if y < 0.0
456 && pose == Some(Pose::Crouching)
457 && azalea_registry::Block::from(
458 world
459 .chunks
460 .get_block_state(position.into())
461 .unwrap_or_default(),
462 ) != azalea_registry::Block::Scaffolding
463 {
464 y = 0.;
465 }
466
467 Vec3 { x, y, z }
468}
469
470fn get_friction_influenced_speed(
474 physics: &Physics,
475 attributes: &Attributes,
476 friction: f32,
477 sprinting: Sprinting,
478) -> f32 {
479 if physics.on_ground() {
481 let speed = attributes.movement_speed.calculate() as f32;
482 speed * (0.21600002f32 / (friction * friction * friction))
483 } else {
484 if *sprinting { 0.025999999f32 } else { 0.02 }
486 }
487}
488
489fn block_jump_factor(world: &Instance, position: Position) -> f32 {
492 let block_at_pos = world.chunks.get_block_state(position.into());
493 let block_below = world
494 .chunks
495 .get_block_state(get_block_pos_below_that_affects_movement(position));
496
497 let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
498 Box::<dyn BlockTrait>::from(block).behavior().jump_factor
499 } else {
500 1.
501 };
502 if block_at_pos_jump_factor != 1. {
503 return block_at_pos_jump_factor;
504 }
505
506 if let Some(block) = block_below {
507 Box::<dyn BlockTrait>::from(block).behavior().jump_factor
508 } else {
509 1.
510 }
511}
512
513fn jump_power(world: &Instance, position: Position) -> f32 {
520 0.42 * block_jump_factor(world, position)
521}
522
523fn jump_boost_power(active_effects: &ActiveEffects) -> f32 {
524 active_effects
525 .get_level(MobEffect::JumpBoost)
526 .map(|level| 0.1 * (level + 1) as f32)
527 .unwrap_or(0.)
528}