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