Skip to main content

azalea_physics/
lib.rs

1#![doc = include_str!("../README.md")]
2#![feature(trait_alias)]
3
4pub mod client_movement;
5pub mod clip;
6pub mod collision;
7pub mod fluids;
8pub mod travel;
9
10use std::collections::HashSet;
11
12use azalea_block::{BlockState, 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::builtin::{BlockKind, EntityKind, MobEffect};
24use azalea_world::{World, WorldName, Worlds};
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::{
31    client_movement::ClientMovementState,
32    collision::{MoveCtx, entity_collisions::update_last_bounding_box},
33};
34
35/// A Bevy [`SystemSet`] for running physics that makes entities do things.
36#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
37pub struct PhysicsSystems;
38
39pub struct PhysicsPlugin;
40impl Plugin for PhysicsPlugin {
41    fn build(&self, app: &mut App) {
42        app.add_systems(
43            GameTick,
44            (
45                fluids::update_in_water_state_and_do_fluid_pushing,
46                update_old_position,
47                fluids::update_swimming,
48                ai_step,
49                travel::travel,
50                apply_effects_from_blocks,
51            )
52                .chain()
53                .in_set(PhysicsSystems)
54                .after(azalea_entity::update_in_loaded_chunk),
55        )
56        // we want this to happen after packets are handled but before physics
57        .add_systems(
58            Update,
59            update_last_bounding_box.after(azalea_entity::update_bounding_box),
60        );
61    }
62}
63
64/// Applies air resistance and handles jumping.
65///
66/// Happens before [`travel::travel`].
67#[allow(clippy::type_complexity)]
68pub fn ai_step(
69    mut query: Query<
70        (
71            &mut Physics,
72            Option<&Jumping>,
73            &Position,
74            &LookDirection,
75            &Sprinting,
76            &ActiveEffects,
77            &WorldName,
78            &EntityKindComponent,
79            &ClientMovementState,
80        ),
81        (With<LocalEntity>, With<HasClientLoaded>),
82    >,
83    worlds: Res<Worlds>,
84) {
85    for (
86        mut physics,
87        jumping,
88        position,
89        look_direction,
90        sprinting,
91        active_effects,
92        world_name,
93        entity_kind,
94        client_movement,
95    ) in &mut query
96    {
97        let is_player = **entity_kind == EntityKind::Player;
98
99        // vanilla does movement interpolation here, doesn't really matter much for a
100        // bot though
101
102        if physics.no_jump_delay > 0 {
103            physics.no_jump_delay -= 1;
104        }
105
106        if is_player {
107            if physics.velocity.horizontal_distance_squared() < 9.0e-6 {
108                physics.velocity.x = 0.;
109                physics.velocity.z = 0.;
110            }
111        } else {
112            if physics.velocity.x.abs() < 0.003 {
113                physics.velocity.x = 0.;
114            }
115            if physics.velocity.z.abs() < 0.003 {
116                physics.velocity.z = 0.;
117            }
118        }
119
120        if physics.velocity.y.abs() < 0.003 {
121            physics.velocity.y = 0.;
122        }
123
124        if is_player {
125            // handled in local_player_ai_step
126        } else {
127            physics.x_acceleration *= 0.98;
128            physics.z_acceleration *= 0.98;
129        }
130
131        if client_movement.trying_to_crouch && physics.is_in_water() {
132            go_down_in_water(&mut physics);
133        }
134
135        if jumping == Some(&Jumping(true)) {
136            let fluid_height = if physics.is_in_lava() {
137                physics.lava_fluid_height
138            } else if physics.is_in_water() {
139                physics.water_fluid_height
140            } else {
141                0.
142            };
143
144            let in_water = physics.is_in_water() && fluid_height > 0.;
145            let fluid_jump_threshold = travel::fluid_jump_threshold();
146
147            if !in_water || physics.on_ground() && fluid_height <= fluid_jump_threshold {
148                if !physics.is_in_lava()
149                    || physics.on_ground() && fluid_height <= fluid_jump_threshold
150                {
151                    if (physics.on_ground() || in_water && fluid_height <= fluid_jump_threshold)
152                        && physics.no_jump_delay == 0
153                    {
154                        jump_from_ground(
155                            &mut physics,
156                            *position,
157                            *look_direction,
158                            *sprinting,
159                            world_name,
160                            &worlds,
161                            active_effects,
162                        );
163                        physics.no_jump_delay = 10;
164                    }
165                } else {
166                    jump_in_liquid(&mut physics);
167                }
168            } else {
169                jump_in_liquid(&mut physics);
170            }
171        } else {
172            physics.no_jump_delay = 0;
173        }
174
175        // TODO: freezing, pushEntities, drowning damage (in their own systems,
176        // after `travel`)
177    }
178}
179
180fn jump_in_liquid(physics: &mut Physics) {
181    physics.velocity.y += 0.04f32 as f64;
182}
183
184fn go_down_in_water(physics: &mut Physics) {
185    physics.velocity.y -= 0.04f32 as f64;
186}
187
188// in minecraft, this is done as part of aiStep immediately after travel
189#[allow(clippy::type_complexity)]
190pub fn apply_effects_from_blocks(
191    mut query: Query<
192        (&mut Physics, &Position, &EntityDimensions, &WorldName),
193        (With<LocalEntity>, With<HasClientLoaded>),
194    >,
195    worlds: Res<Worlds>,
196) {
197    for (mut physics, position, dimensions, world_name) in &mut query {
198        let Some(world_lock) = worlds.get(world_name) else {
199            continue;
200        };
201        let world = world_lock.read();
202
203        // if !is_affected_by_blocks {
204        //     continue
205        // }
206
207        // if (this.onGround()) {
208        //     BlockPos var3 = this.getOnPosLegacy();
209        //     BlockState var4 = this.level().getBlockState(var3);
210        //     var4.getBlock().stepOn(this.level(), var3, var4, this);
211        //  }
212
213        // minecraft adds more entries to the list when the code is running on the
214        // server
215        let movement_this_tick = [EntityMovement {
216            from: physics.old_position,
217            to: **position,
218        }];
219
220        check_inside_blocks(&mut physics, dimensions, &world, &movement_this_tick);
221    }
222}
223
224fn check_inside_blocks(
225    physics: &mut Physics,
226    dimensions: &EntityDimensions,
227    world: &World,
228    movements: &[EntityMovement],
229) -> Vec<BlockState> {
230    let mut blocks_inside = Vec::new();
231    let mut visited_blocks = HashSet::<BlockState>::new();
232
233    for movement in movements {
234        let bounding_box_at_target = dimensions
235            .make_bounding_box(movement.to)
236            .deflate_all(1.0E-5);
237
238        for traversed_block in
239            box_traverse_blocks(movement.from, movement.to, &bounding_box_at_target)
240        {
241            // if (!this.isAlive()) {
242            //     return;
243            // }
244
245            let traversed_block_state = world.get_block_state(traversed_block).unwrap_or_default();
246            if traversed_block_state.is_air() {
247                continue;
248            }
249            if !visited_blocks.insert(traversed_block_state) {
250                continue;
251            }
252
253            /*
254            VoxelShape var12 = traversedBlockState.getEntityInsideCollisionShape(this.level(), traversedBlock);
255            if (var12 != Shapes.block() && !this.collidedWithShapeMovingFrom(from, to, traversedBlock, var12)) {
256               continue;
257            }
258
259            traversedBlockState.entityInside(this.level(), traversedBlock, this);
260            this.onInsideBlock(traversedBlockState);
261            */
262
263            // this is different for end portal frames and tripwire hooks, i don't think it
264            // actually matters for a client though
265            let entity_inside_collision_shape = &*BLOCK_SHAPE;
266
267            if entity_inside_collision_shape != &*BLOCK_SHAPE
268                && !collided_with_shape_moving_from(
269                    movement.from,
270                    movement.to,
271                    traversed_block,
272                    entity_inside_collision_shape,
273                    dimensions,
274                )
275            {
276                continue;
277            }
278
279            handle_entity_inside_block(world, traversed_block_state, traversed_block, physics);
280
281            blocks_inside.push(traversed_block_state);
282        }
283    }
284
285    blocks_inside
286}
287
288fn collided_with_shape_moving_from(
289    from: Vec3,
290    to: Vec3,
291    traversed_block: BlockPos,
292    entity_inside_collision_shape: &VoxelShape,
293    dimensions: &EntityDimensions,
294) -> bool {
295    let bounding_box_from = dimensions.make_bounding_box(from);
296    let delta = to - from;
297    bounding_box_from.collided_along_vector(
298        delta,
299        &entity_inside_collision_shape
300            .move_relative(traversed_block.to_vec3_floored())
301            .to_aabbs(),
302    )
303}
304
305// BlockBehavior.entityInside
306fn handle_entity_inside_block(
307    world: &World,
308    block: BlockState,
309    block_pos: BlockPos,
310    physics: &mut Physics,
311) {
312    let registry_block = BlockKind::from(block);
313    #[allow(clippy::single_match)]
314    match registry_block {
315        BlockKind::BubbleColumn => {
316            let block_above = world.get_block_state(block_pos.up(1)).unwrap_or_default();
317            let is_block_above_empty =
318                block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty();
319            let drag_down = block
320                .property::<properties::Drag>()
321                .expect("drag property should always be present on bubble columns");
322            let velocity = &mut physics.velocity;
323
324            if is_block_above_empty {
325                let new_y = if drag_down {
326                    f64::max(-0.9, velocity.y - 0.03)
327                } else {
328                    f64::min(1.8, velocity.y + 0.1)
329                };
330                velocity.y = new_y;
331            } else {
332                let new_y = if drag_down {
333                    f64::max(-0.3, velocity.y - 0.03)
334                } else {
335                    f64::min(0.7, velocity.y + 0.06)
336                };
337                velocity.y = new_y;
338                physics.reset_fall_distance();
339            }
340        }
341        _ => {}
342    }
343}
344
345pub struct EntityMovement {
346    pub from: Vec3,
347    pub to: Vec3,
348}
349
350pub fn jump_from_ground(
351    physics: &mut Physics,
352    position: Position,
353    look_direction: LookDirection,
354    sprinting: Sprinting,
355    world_name: &WorldName,
356    worlds: &Worlds,
357    active_effects: &ActiveEffects,
358) {
359    let world_lock = worlds
360        .get(world_name)
361        .expect("All entities should be in a valid world");
362    let world = world_lock.read();
363
364    let base_jump = jump_power(&world, position);
365    let jump_power = base_jump + jump_boost_power(active_effects);
366    if jump_power <= 1.0E-5 {
367        return;
368    }
369
370    let old_delta_movement = physics.velocity;
371    physics.velocity = Vec3 {
372        x: old_delta_movement.x,
373        y: f64::max(jump_power as f64, old_delta_movement.y),
374        z: old_delta_movement.z,
375    };
376    if *sprinting {
377        // sprint jumping gives some extra velocity
378        let y_rot = look_direction.y_rot() * 0.017453292;
379        physics.velocity += Vec3 {
380            x: (-math::sin(y_rot) * 0.2) as f64,
381            y: 0.,
382            z: (math::cos(y_rot) * 0.2) as f64,
383        };
384    }
385
386    physics.has_impulse = true;
387}
388
389pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) {
390    for (mut physics, position) in &mut query {
391        physics.set_old_pos(*position);
392    }
393}
394
395pub fn get_block_pos_below_that_affects_movement(position: Position) -> BlockPos {
396    BlockPos::new(
397        position.x.floor() as i32,
398        // TODO: this uses bounding_box.min_y instead of position.y
399        (position.y - 0.5f64).floor() as i32,
400        position.z.floor() as i32,
401    )
402}
403
404fn handle_relative_friction_and_calculate_movement(ctx: &mut MoveCtx, block_friction: f32) -> Vec3 {
405    move_relative(
406        ctx.physics,
407        ctx.direction,
408        get_friction_influenced_speed(ctx.physics, ctx.attributes, block_friction, ctx.sprinting),
409        Vec3::new(
410            ctx.physics.x_acceleration as f64,
411            ctx.physics.y_acceleration as f64,
412            ctx.physics.z_acceleration as f64,
413        ),
414    );
415
416    ctx.physics.velocity = handle_on_climbable(
417        ctx.physics.velocity,
418        ctx.on_climbable,
419        *ctx.position,
420        ctx.world,
421        ctx.pose,
422    );
423
424    move_colliding(ctx, ctx.physics.velocity);
425    // let delta_movement = entity.delta;
426    // ladders
427    //   if ((entity.horizontalCollision || entity.jumping) && (entity.onClimbable()
428    // || entity.getFeetBlockState().is(Blocks.POWDER_SNOW) &&
429    // PowderSnowBlock.canEntityWalkOnPowderSnow(entity))) {      var3 = new
430    // Vec3(var3.x, 0.2D, var3.z);   }
431
432    if ctx.physics.horizontal_collision || *ctx.jumping {
433        let block_at_feet: BlockKind = ctx
434            .world
435            .chunks
436            .get_block_state(BlockPos::from(*ctx.position))
437            .unwrap_or_default()
438            .into();
439
440        if *ctx.on_climbable || block_at_feet == BlockKind::PowderSnow {
441            ctx.physics.velocity.y = 0.2;
442        }
443    }
444
445    ctx.physics.velocity
446}
447
448fn handle_on_climbable(
449    velocity: Vec3,
450    on_climbable: OnClimbable,
451    position: Position,
452    world: &World,
453    pose: Option<Pose>,
454) -> Vec3 {
455    if !*on_climbable {
456        return velocity;
457    }
458
459    // minecraft does resetFallDistance here
460
461    const CLIMBING_SPEED: f64 = 0.15_f32 as f64;
462
463    let x = f64::clamp(velocity.x, -CLIMBING_SPEED, CLIMBING_SPEED);
464    let z = f64::clamp(velocity.z, -CLIMBING_SPEED, CLIMBING_SPEED);
465    let mut y = f64::max(velocity.y, -CLIMBING_SPEED);
466
467    // sneaking on ladders/vines
468    if y < 0.0
469        && pose == Some(Pose::Crouching)
470        && BlockKind::from(
471            world
472                .chunks
473                .get_block_state(position.into())
474                .unwrap_or_default(),
475        ) != BlockKind::Scaffolding
476    {
477        y = 0.;
478    }
479
480    Vec3 { x, y, z }
481}
482
483// private float getFrictionInfluencedSpeed(float friction) {
484//     return this.onGround ? this.getSpeed() * (0.21600002F / (friction *
485// friction * friction)) : this.flyingSpeed; }
486fn get_friction_influenced_speed(
487    physics: &Physics,
488    attributes: &Attributes,
489    friction: f32,
490    sprinting: Sprinting,
491) -> f32 {
492    // TODO: have speed & flying_speed fields in entity
493    if physics.on_ground() {
494        let speed = attributes.movement_speed.calculate() as f32;
495        speed * (0.21600002f32 / (friction * friction * friction))
496    } else {
497        // entity.flying_speed
498        if *sprinting { 0.025999999f32 } else { 0.02 }
499    }
500}
501
502/// Returns the what the entity's jump should be multiplied by based on the
503/// block they're standing on.
504fn block_jump_factor(world: &World, position: Position) -> f32 {
505    let block_at_pos = world.chunks.get_block_state(position.into());
506    let block_below = world
507        .chunks
508        .get_block_state(get_block_pos_below_that_affects_movement(position));
509
510    let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
511        block.behavior().jump_factor
512    } else {
513        1.
514    };
515    if block_at_pos_jump_factor != 1. {
516        return block_at_pos_jump_factor;
517    }
518
519    if let Some(block) = block_below {
520        block.behavior().jump_factor
521    } else {
522        1.
523    }
524}
525
526// protected float getJumpPower() {
527//     return 0.42F * this.getBlockJumpFactor();
528// }
529// public double getJumpBoostPower() {
530//     return this.hasEffect(MobEffects.JUMP) ? (double)(0.1F *
531// (float)(this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D; }
532fn jump_power(world: &World, position: Position) -> f32 {
533    0.42 * block_jump_factor(world, position)
534}
535
536fn jump_boost_power(active_effects: &ActiveEffects) -> f32 {
537    active_effects
538        .get_level(MobEffect::JumpBoost)
539        .map(|level| 0.1 * (level + 1) as f32)
540        .unwrap_or(0.)
541}