azalea_physics/
lib.rs

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