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::{BlockState, BlockTrait, fluid_state::FluidState, properties};
12use azalea_core::{
13    math,
14    position::{BlockPos, Vec3},
15    tick::GameTick,
16};
17use azalea_entity::{
18    Attributes, EntityKindComponent, InLoadedChunk, Jumping, LocalEntity, LookDirection,
19    OnClimbable, Physics, Pose, Position, metadata::Sprinting, move_relative,
20};
21use azalea_registry::{Block, EntityKind};
22use azalea_world::{Instance, InstanceContainer, InstanceName};
23use bevy_app::{App, Plugin};
24use bevy_ecs::prelude::*;
25use clip::box_traverse_blocks;
26use collision::{
27    BLOCK_SHAPE, BlockWithShape, MoverType, VoxelShape,
28    entity_collisions::{CollidableEntityQuery, PhysicsQuery},
29    move_colliding,
30};
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<InLoadedChunk>),
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, &InstanceName),
165        (With<LocalEntity>, With<InLoadedChunk>),
166    >,
167    instance_container: Res<InstanceContainer>,
168) {
169    for (mut physics, position, 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, &world, &movement_this_tick);
193    }
194}
195
196fn check_inside_blocks(
197    physics: &mut Physics,
198    world: &Instance,
199    movements: &[EntityMovement],
200) -> Vec<BlockState> {
201    let mut blocks_inside = Vec::new();
202    let mut visited_blocks = HashSet::<BlockState>::new();
203
204    for movement in movements {
205        let bounding_box_at_target = physics
206            .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                    physics,
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    physics: &Physics,
266) -> bool {
267    let bounding_box_from = physics.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
370/// Options for [`handle_relative_friction_and_calculate_movement`]
371struct HandleRelativeFrictionAndCalculateMovementOpts<'a, 'b, 'world, 'state> {
372    block_friction: f32,
373    world: &'a Instance,
374    physics: &'a mut Physics,
375    direction: LookDirection,
376    position: Mut<'a, Position>,
377    attributes: &'a Attributes,
378    is_sprinting: bool,
379    on_climbable: OnClimbable,
380    pose: Option<Pose>,
381    jumping: Jumping,
382    entity: Entity,
383    physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
384    collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
385}
386fn handle_relative_friction_and_calculate_movement(
387    HandleRelativeFrictionAndCalculateMovementOpts {
388        block_friction,
389        world,
390        physics,
391        direction,
392        mut position,
393        attributes,
394        is_sprinting,
395        on_climbable,
396        pose,
397        jumping,
398        entity,
399        physics_query,
400        collidable_entity_query,
401    }: HandleRelativeFrictionAndCalculateMovementOpts<'_, '_, '_, '_>,
402) -> Vec3 {
403    move_relative(
404        physics,
405        direction,
406        get_friction_influenced_speed(physics, attributes, block_friction, is_sprinting),
407        Vec3::new(
408            physics.x_acceleration as f64,
409            physics.y_acceleration as f64,
410            physics.z_acceleration as f64,
411        ),
412    );
413
414    physics.velocity = handle_on_climbable(physics.velocity, on_climbable, *position, world, pose);
415
416    move_colliding(
417        MoverType::Own,
418        physics.velocity,
419        world,
420        &mut position,
421        physics,
422        Some(entity),
423        physics_query,
424        collidable_entity_query,
425    )
426    .expect("Entity should exist");
427    // let delta_movement = entity.delta;
428    // ladders
429    //   if ((entity.horizontalCollision || entity.jumping) && (entity.onClimbable()
430    // || entity.getFeetBlockState().is(Blocks.POWDER_SNOW) &&
431    // PowderSnowBlock.canEntityWalkOnPowderSnow(entity))) {      var3 = new
432    // Vec3(var3.x, 0.2D, var3.z);   }
433
434    if physics.horizontal_collision || *jumping {
435        let block_at_feet: Block = world
436            .chunks
437            .get_block_state((*position).into())
438            .unwrap_or_default()
439            .into();
440
441        if *on_climbable || block_at_feet == Block::PowderSnow {
442            physics.velocity.y = 0.2;
443        }
444    }
445
446    physics.velocity
447}
448
449fn handle_on_climbable(
450    velocity: Vec3,
451    on_climbable: OnClimbable,
452    position: Position,
453    world: &Instance,
454    pose: Option<Pose>,
455) -> Vec3 {
456    if !*on_climbable {
457        return velocity;
458    }
459
460    // minecraft does resetFallDistance here
461
462    const CLIMBING_SPEED: f64 = 0.15_f32 as f64;
463
464    let x = f64::clamp(velocity.x, -CLIMBING_SPEED, CLIMBING_SPEED);
465    let z = f64::clamp(velocity.z, -CLIMBING_SPEED, CLIMBING_SPEED);
466    let mut y = f64::max(velocity.y, -CLIMBING_SPEED);
467
468    // sneaking on ladders/vines
469    if y < 0.0
470        && pose == Some(Pose::Sneaking)
471        && azalea_registry::Block::from(
472            world
473                .chunks
474                .get_block_state(position.into())
475                .unwrap_or_default(),
476        ) != azalea_registry::Block::Scaffolding
477    {
478        y = 0.;
479    }
480
481    Vec3 { x, y, z }
482}
483
484// private float getFrictionInfluencedSpeed(float friction) {
485//     return this.onGround ? this.getSpeed() * (0.21600002F / (friction *
486// friction * friction)) : this.flyingSpeed; }
487fn get_friction_influenced_speed(
488    physics: &Physics,
489    attributes: &Attributes,
490    friction: f32,
491    is_sprinting: bool,
492) -> f32 {
493    // TODO: have speed & flying_speed fields in entity
494    if physics.on_ground() {
495        let speed: f32 = attributes.speed.calculate() as f32;
496        speed * (0.216f32 / (friction * friction * friction))
497    } else {
498        // entity.flying_speed
499        if is_sprinting { 0.025999999f32 } else { 0.02 }
500    }
501}
502
503/// Returns the what the entity's jump should be multiplied by based on the
504/// block they're standing on.
505fn block_jump_factor(world: &Instance, position: Position) -> f32 {
506    let block_at_pos = world.chunks.get_block_state(position.into());
507    let block_below = world
508        .chunks
509        .get_block_state(get_block_pos_below_that_affects_movement(position));
510
511    let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
512        Box::<dyn BlockTrait>::from(block).behavior().jump_factor
513    } else {
514        1.
515    };
516    if block_at_pos_jump_factor != 1. {
517        return block_at_pos_jump_factor;
518    }
519
520    if let Some(block) = block_below {
521        Box::<dyn BlockTrait>::from(block).behavior().jump_factor
522    } else {
523        1.
524    }
525}
526
527// protected float getJumpPower() {
528//     return 0.42F * this.getBlockJumpFactor();
529// }
530// public double getJumpBoostPower() {
531//     return this.hasEffect(MobEffects.JUMP) ? (double)(0.1F *
532// (float)(this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D; }
533fn jump_power(world: &Instance, position: Position) -> f32 {
534    0.42 * block_jump_factor(world, position)
535}
536
537fn jump_boost_power() -> f64 {
538    // TODO: potion effects
539    // if let Some(effects) = entity.effects() {
540    //     if let Some(jump_effect) = effects.get(&Effect::Jump) {
541    //         0.1 * (jump_effect.amplifier + 1) as f32
542    //     } else {
543    //         0.
544    //     }
545    // } else {
546    //     0.
547    // }
548    0.
549}