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