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