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