Skip to main content

azalea_physics/
travel.rs

1use azalea_block::{BlockState, fluid_state::FluidState};
2use azalea_core::{
3    aabb::Aabb,
4    position::{BlockPos, Vec3},
5};
6use azalea_entity::{
7    Attributes, HasClientLoaded, Jumping, LocalEntity, LookDirection, OnClimbable, Physics,
8    PlayerAbilities, Pose, Position,
9    metadata::{FallFlying, Sprinting},
10    move_relative, view_vector,
11};
12use azalea_world::{World, WorldName, Worlds};
13use bevy_ecs::prelude::*;
14
15use crate::{
16    client_movement::ClientMovementState,
17    collision::{
18        MoveCtx, MoverType, Shapes,
19        entity_collisions::{AabbQuery, CollidableEntityQuery, get_entity_collisions},
20        move_colliding,
21        world_collisions::{get_block_and_liquid_collisions, get_block_collisions},
22    },
23    get_block_pos_below_that_affects_movement, handle_relative_friction_and_calculate_movement,
24};
25
26/// Move the entity with the given acceleration while handling friction,
27/// gravity, collisions, and some other stuff.
28#[allow(clippy::type_complexity)]
29pub fn travel(
30    mut query: Query<
31        (
32            Entity,
33            &Attributes,
34            &WorldName,
35            &OnClimbable,
36            &Jumping,
37            Option<&ClientMovementState>,
38            Option<&Sprinting>,
39            Option<&Pose>,
40            Option<&PlayerAbilities>,
41            &mut Physics,
42            &mut LookDirection,
43            &mut Position,
44            Option<&mut FallFlying>,
45        ),
46        (With<LocalEntity>, With<HasClientLoaded>),
47    >,
48    worlds: Res<Worlds>,
49    aabb_query: AabbQuery,
50    collidable_entity_query: CollidableEntityQuery,
51) {
52    for (
53        entity,
54        attributes,
55        world_name,
56        on_climbable,
57        jumping,
58        physics_state,
59        sprinting,
60        pose,
61        abilities,
62        mut physics,
63        direction,
64        position,
65        fall_flying,
66    ) in &mut query
67    {
68        let Some(world_lock) = worlds.get(world_name) else {
69            continue;
70        };
71        let world = world_lock.read();
72
73        let sprinting = *sprinting.unwrap_or(&Sprinting(false));
74
75        let mut ctx = MoveCtx {
76            mover_type: MoverType::Own,
77            world: &world,
78            position,
79            physics: &mut physics,
80            source_entity: entity,
81            aabb_query: &aabb_query,
82            collidable_entity_query: &collidable_entity_query,
83            physics_state,
84            attributes,
85            abilities,
86            direction: *direction,
87            sprinting,
88            on_climbable: *on_climbable,
89            pose: pose.copied(),
90            jumping: *jumping,
91        };
92
93        if ctx.physics.is_in_water() || ctx.physics.is_in_lava() {
94            // minecraft also checks for `this.isAffectedByFluids() &&
95            // !this.canStandOnFluid(fluidAtBlock)` here but it doesn't matter
96            // for players
97            travel_in_fluid(&mut ctx);
98        } else if fall_flying
99            .as_deref()
100            .is_some_and(|fall_flying| **fall_flying)
101        {
102            travel_fall_flying(&mut ctx, &mut fall_flying.unwrap());
103        } else {
104            travel_in_air(&mut ctx);
105        }
106    }
107}
108
109/// The usual movement when we're not in water or using an elytra.
110fn travel_in_air(ctx: &mut MoveCtx) {
111    let gravity = get_effective_gravity();
112
113    let block_pos_below = get_block_pos_below_that_affects_movement(*ctx.position);
114
115    let block_below = ctx
116        .world
117        .chunks
118        .get_block_state(block_pos_below)
119        .unwrap_or(BlockState::AIR);
120
121    let block_friction = block_below.behavior().friction;
122    let inertia = if ctx.physics.on_ground() {
123        block_friction * 0.91
124    } else {
125        0.91
126    };
127
128    // this applies the current delta
129    let mut movement = handle_relative_friction_and_calculate_movement(ctx, block_friction);
130
131    movement.y -= gravity;
132
133    // if (this.shouldDiscardFriction()) {
134    //     this.setDeltaMovement(movement.x, yMovement, movement.z);
135    // } else {
136    //     this.setDeltaMovement(movement.x * (double)inertia, yMovement *
137    // 0.9800000190734863D, movement.z * (double)inertia); }
138
139    // if should_discard_friction(self) {
140    if false {
141        ctx.physics.velocity = movement;
142    } else {
143        ctx.physics.velocity = Vec3 {
144            x: movement.x * inertia as f64,
145            y: movement.y * 0.9800000190734863f64,
146            z: movement.z * inertia as f64,
147        };
148    }
149}
150
151fn travel_in_fluid(ctx: &mut MoveCtx) {
152    let moving_down = ctx.physics.velocity.y <= 0.;
153    let y = ctx.position.y;
154    let gravity = get_effective_gravity();
155
156    let acceleration = Vec3::new(
157        ctx.physics.x_acceleration as f64,
158        ctx.physics.y_acceleration as f64,
159        ctx.physics.z_acceleration as f64,
160    );
161
162    if ctx.physics.was_touching_water {
163        let mut water_movement_speed = if *ctx.sprinting { 0.9 } else { 0.8 };
164        let mut speed = 0.02;
165        let mut water_efficiency_modifier =
166            ctx.attributes.water_movement_efficiency.calculate() as f32;
167        if !ctx.physics.on_ground() {
168            water_efficiency_modifier *= 0.5;
169        }
170
171        if water_efficiency_modifier > 0. {
172            water_movement_speed += (0.54600006 - water_movement_speed) * water_efficiency_modifier;
173            speed += (ctx.attributes.movement_speed.calculate() as f32 - speed)
174                * water_efficiency_modifier;
175        }
176
177        // if (this.hasEffect(MobEffects.DOLPHINS_GRACE)) {
178        //     waterMovementSpeed = 0.96F;
179        // }
180
181        move_relative(ctx.physics, ctx.direction, speed, acceleration);
182        move_colliding(ctx, ctx.physics.velocity);
183
184        let mut new_velocity = ctx.physics.velocity;
185        if ctx.physics.horizontal_collision && *ctx.on_climbable {
186            // underwater ladders
187            new_velocity.y = 0.2;
188        }
189        new_velocity.x *= water_movement_speed as f64;
190        new_velocity.y *= 0.8;
191        new_velocity.z *= water_movement_speed as f64;
192        ctx.physics.velocity =
193            get_fluid_falling_adjusted_movement(gravity, moving_down, new_velocity, ctx.sprinting);
194    } else {
195        move_relative(ctx.physics, ctx.direction, 0.02, acceleration);
196        move_colliding(ctx, ctx.physics.velocity);
197
198        if ctx.physics.lava_fluid_height <= fluid_jump_threshold() {
199            ctx.physics.velocity.x *= 0.5;
200            ctx.physics.velocity.y *= 0.8;
201            ctx.physics.velocity.z *= 0.5;
202            let new_velocity = get_fluid_falling_adjusted_movement(
203                gravity,
204                moving_down,
205                ctx.physics.velocity,
206                ctx.sprinting,
207            );
208            ctx.physics.velocity = new_velocity;
209        } else {
210            ctx.physics.velocity *= 0.5;
211        }
212
213        if gravity != 0.0 {
214            ctx.physics.velocity.y -= gravity / 4.0;
215        }
216    }
217
218    let velocity = ctx.physics.velocity;
219    if ctx.physics.horizontal_collision
220        && is_free(
221            ctx.world,
222            ctx.source_entity,
223            ctx.aabb_query,
224            ctx.collidable_entity_query,
225            ctx.physics,
226            ctx.physics.bounding_box,
227            velocity.up(0.6).down(ctx.position.y).up(y),
228        )
229    {
230        ctx.physics.velocity.y = 0.3;
231    }
232}
233
234fn travel_fall_flying(ctx: &mut MoveCtx, fall_flying: &mut FallFlying) {
235    if *ctx.on_climbable {
236        travel_in_air(ctx);
237        **fall_flying = false; // vanilla first set to true then set to false again, quite confusing
238    } else {
239        let look = ctx.direction;
240        let look_angle = view_vector(look);
241
242        let lean_angle = look.x_rot().to_radians();
243
244        let look_horizontal_length =
245            (look_angle.x * look_angle.x + look_angle.z * look_angle.z).sqrt();
246        let move_horizontal_length = ctx.physics.velocity.horizontal_distance();
247        let gravity = get_effective_gravity();
248
249        // vanilla convert to double first, we match vanilla here
250        let lift_force = f64::from(lean_angle).cos().powi(2);
251
252        let mut movement = ctx.physics.velocity;
253
254        movement.y += gravity * (-1.0 + lift_force * 0.75);
255        if movement.y < 0.0 && look_horizontal_length > 0.0 {
256            let convert = movement.y * -0.1 * lift_force;
257            movement += Vec3::new(
258                look_angle.x * convert / look_horizontal_length,
259                convert,
260                look_angle.z * convert / look_horizontal_length,
261            );
262        }
263
264        if lean_angle < 0.0 && look_horizontal_length > 0.0 {
265            let convert =
266                move_horizontal_length * -azalea_core::math::sin(lean_angle) as f64 * 0.04;
267            movement += Vec3::new(
268                -look_angle.x * convert / look_horizontal_length,
269                convert * 3.2,
270                -look_angle.z * convert / look_horizontal_length,
271            );
272        }
273
274        if look_horizontal_length > 0.0 {
275            movement += Vec3::new(
276                (look_angle.x / look_horizontal_length * move_horizontal_length - movement.x) * 0.1,
277                0.0,
278                (look_angle.z / look_horizontal_length * move_horizontal_length - movement.z) * 0.1,
279            );
280        }
281
282        ctx.physics.velocity = movement.multiply(0.99f32 as f64, 0.98f32 as f64, 0.99f32 as f64);
283
284        move_colliding(ctx, ctx.physics.velocity);
285    }
286}
287
288fn get_fluid_falling_adjusted_movement(
289    gravity: f64,
290    moving_down: bool,
291    new_velocity: Vec3,
292    sprinting: Sprinting,
293) -> Vec3 {
294    if gravity != 0. && !*sprinting {
295        let new_y_velocity = if moving_down
296            && (new_velocity.y - 0.005).abs() >= 0.003
297            && f64::abs(new_velocity.y - gravity / 16.0) < 0.003
298        {
299            -0.003
300        } else {
301            new_velocity.y - gravity / 16.0
302        };
303
304        Vec3 {
305            x: new_velocity.x,
306            y: new_y_velocity,
307            z: new_velocity.z,
308        }
309    } else {
310        new_velocity
311    }
312}
313
314fn is_free(
315    world: &World,
316    source_entity: Entity,
317    aabb_query: &AabbQuery,
318    collidable_entity_query: &CollidableEntityQuery,
319    entity_physics: &Physics,
320    bounding_box: Aabb,
321    delta: Vec3,
322) -> bool {
323    let bounding_box = bounding_box.move_relative(delta);
324
325    no_collision(
326        world,
327        Some(source_entity),
328        aabb_query,
329        collidable_entity_query,
330        entity_physics,
331        &bounding_box,
332        false,
333    ) && !contains_any_liquid(world, bounding_box)
334}
335
336pub fn no_collision(
337    world: &World,
338    source_entity: Option<Entity>,
339    aabb_query: &AabbQuery,
340    collidable_entity_query: &CollidableEntityQuery,
341    entity_physics: &Physics,
342    aabb: &Aabb,
343    include_liquid_collisions: bool,
344) -> bool {
345    let collisions = if include_liquid_collisions {
346        get_block_and_liquid_collisions(world, aabb)
347    } else {
348        get_block_collisions(world, aabb)
349    };
350
351    for collision in collisions {
352        if !collision.is_empty() {
353            return false;
354        }
355    }
356
357    if !get_entity_collisions(
358        world,
359        aabb,
360        source_entity,
361        aabb_query,
362        collidable_entity_query,
363    )
364    .is_empty()
365    {
366        false
367    } else if source_entity.is_none() {
368        true
369    } else {
370        let collision = border_collision(entity_physics, aabb);
371        if let Some(collision) = collision {
372            // !Shapes.joinIsNotEmpty(collision, Shapes.create(aabb), BooleanOp.AND);
373            !Shapes::matches_anywhere(&collision.into(), &aabb.into(), |a, b| a && b)
374        } else {
375            true
376        }
377    }
378}
379
380fn border_collision(_entity_physics: &Physics, _aabb: &Aabb) -> Option<Aabb> {
381    // TODO: implement world border, see CollisionGetter.borderCollision
382
383    None
384}
385
386fn contains_any_liquid(world: &World, bounding_box: Aabb) -> bool {
387    let min = bounding_box.min.to_block_pos_floor();
388    let max = bounding_box.max.to_block_pos_ceil();
389
390    for x in min.x..max.x {
391        for y in min.y..max.y {
392            for z in min.z..max.z {
393                let block_state = world
394                    .chunks
395                    .get_block_state(BlockPos::new(x, y, z))
396                    .unwrap_or_default();
397                if !FluidState::from(block_state).is_empty() {
398                    return true;
399                }
400            }
401        }
402    }
403
404    false
405}
406
407fn get_effective_gravity() -> f64 {
408    // TODO: slow falling effect
409    0.08
410}
411
412pub fn fluid_jump_threshold() -> f64 {
413    // this is 0.0 for entities with an eye height lower than 0.4, but that's not
414    // implemented since it's usually not relevant for players (unless the player
415    // was shrunk)
416    0.4
417}