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