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};
26use bevy_ecs::prelude::*;
27use clip::box_traverse_blocks;
28use collision::{BLOCK_SHAPE, BlockWithShape, VoxelShape, move_colliding};
29
30use crate::collision::MoveCtx;
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<HasClientLoaded>),
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, &EntityDimensions, &InstanceName),
165 (With<LocalEntity>, With<HasClientLoaded>),
166 >,
167 instance_container: Res<InstanceContainer>,
168) {
169 for (mut physics, position, dimensions, 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, dimensions, &world, &movement_this_tick);
193 }
194}
195
196fn check_inside_blocks(
197 physics: &mut Physics,
198 dimensions: &EntityDimensions,
199 world: &Instance,
200 movements: &[EntityMovement],
201) -> Vec<BlockState> {
202 let mut blocks_inside = Vec::new();
203 let mut visited_blocks = HashSet::<BlockState>::new();
204
205 for movement in movements {
206 let bounding_box_at_target = 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 dimensions,
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 dimensions: &EntityDimensions,
266) -> bool {
267 let bounding_box_from = 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
370fn handle_relative_friction_and_calculate_movement(ctx: &mut MoveCtx, block_friction: f32) -> Vec3 {
371 move_relative(
372 ctx.physics,
373 ctx.direction,
374 get_friction_influenced_speed(ctx.physics, ctx.attributes, block_friction, ctx.sprinting),
375 Vec3::new(
376 ctx.physics.x_acceleration as f64,
377 ctx.physics.y_acceleration as f64,
378 ctx.physics.z_acceleration as f64,
379 ),
380 );
381
382 ctx.physics.velocity = handle_on_climbable(
383 ctx.physics.velocity,
384 ctx.on_climbable,
385 *ctx.position,
386 ctx.world,
387 ctx.pose,
388 );
389
390 move_colliding(ctx, ctx.physics.velocity).expect("Entity should exist");
391 if ctx.physics.horizontal_collision || *ctx.jumping {
399 let block_at_feet: Block = ctx
400 .world
401 .chunks
402 .get_block_state(BlockPos::from(*ctx.position))
403 .unwrap_or_default()
404 .into();
405
406 if *ctx.on_climbable || block_at_feet == Block::PowderSnow {
407 ctx.physics.velocity.y = 0.2;
408 }
409 }
410
411 ctx.physics.velocity
412}
413
414fn handle_on_climbable(
415 velocity: Vec3,
416 on_climbable: OnClimbable,
417 position: Position,
418 world: &Instance,
419 pose: Option<Pose>,
420) -> Vec3 {
421 if !*on_climbable {
422 return velocity;
423 }
424
425 const CLIMBING_SPEED: f64 = 0.15_f32 as f64;
428
429 let x = f64::clamp(velocity.x, -CLIMBING_SPEED, CLIMBING_SPEED);
430 let z = f64::clamp(velocity.z, -CLIMBING_SPEED, CLIMBING_SPEED);
431 let mut y = f64::max(velocity.y, -CLIMBING_SPEED);
432
433 if y < 0.0
435 && pose == Some(Pose::Crouching)
436 && azalea_registry::Block::from(
437 world
438 .chunks
439 .get_block_state(position.into())
440 .unwrap_or_default(),
441 ) != azalea_registry::Block::Scaffolding
442 {
443 y = 0.;
444 }
445
446 Vec3 { x, y, z }
447}
448
449fn get_friction_influenced_speed(
453 physics: &Physics,
454 attributes: &Attributes,
455 friction: f32,
456 sprinting: Sprinting,
457) -> f32 {
458 if physics.on_ground() {
460 let speed = attributes.movement_speed.calculate() as f32;
461 speed * (0.21600002f32 / (friction * friction * friction))
462 } else {
463 if *sprinting { 0.025999999f32 } else { 0.02 }
465 }
466}
467
468fn block_jump_factor(world: &Instance, position: Position) -> f32 {
471 let block_at_pos = world.chunks.get_block_state(position.into());
472 let block_below = world
473 .chunks
474 .get_block_state(get_block_pos_below_that_affects_movement(position));
475
476 let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
477 Box::<dyn BlockTrait>::from(block).behavior().jump_factor
478 } else {
479 1.
480 };
481 if block_at_pos_jump_factor != 1. {
482 return block_at_pos_jump_factor;
483 }
484
485 if let Some(block) = block_below {
486 Box::<dyn BlockTrait>::from(block).behavior().jump_factor
487 } else {
488 1.
489 }
490}
491
492fn jump_power(world: &Instance, position: Position) -> f32 {
499 0.42 * block_jump_factor(world, position)
500}
501
502fn jump_boost_power() -> f64 {
503 0.
514}