Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / common / src / entity / mod.rs

//! Entity physics shared by client and server: ports of the movement code of `Entity`,
//! `EntityLiving`, `EntityPlayer` and `EntityPlayerSP`.

pub mod mob;
pub mod painting;
pub mod player;
pub mod vehicle;

use std::f32::consts::PI;

use glam::DVec3;

use crate::JavaRandom;
use crate::item::ItemStack;
use crate::math::{self, floor_f64};
use crate::physics::Aabb;
use crate::world::shape::BlockView;
use crate::world::{Block, Material, fluid};

/// The world as entities see it.
pub trait EntityWorld: BlockView {
    /// `World.multiplayerWorld`: true on the client, where entities are only simulated.
    fn is_remote(&self) -> bool;
    fn chunks_exist(&self, min: [i32; 3], max: [i32; 3]) -> bool;
    /// `World.getCollidingBoundingBoxes`: blocks plus the solid boxes of other entities.
    fn colliding_boxes(&self, entity: &Entity, region: &Aabb) -> Vec<Aabb>;
    /// `World.canBlockBeRainedOn`.
    fn rained_on(&self, x: i32, y: i32, z: i32) -> bool;
    /// `Block.onEntityWalking`.
    fn on_walked(&mut self, _block: Block, _pos: [i32; 3], _entity: &mut Entity) {}
    /// `Block.onEntityCollidedWithBlock`.
    fn on_collided(&mut self, _block: Block, _pos: [i32; 3], _entity: &mut Entity) {}
    /// `Entity.attackEntityFrom` without an attacker, for fire, lava, drowning and falls.
    fn damage(&mut self, _entity: &mut Entity, _amount: i32) {}
    fn play_sound(&mut self, _entity: &Entity, _sound: &str, _volume: f32, _pitch: f32) {}
    /// The footstep and landing sounds of a moving entity.
    fn step_sound(&mut self, entity: &Entity, sound: &str, volume: f32, pitch: f32) {
        self.play_sound(entity, sound, volume, pitch);
    }
    /// `EntityLiving.playLivingSound`.
    fn living_sound(&mut self, _entity: &mut Entity) {}
    fn spawn_particle(&mut self, _name: &str, _pos: DVec3, _velocity: DVec3) {}
    fn award_achievement(&mut self, _player: i32, _index: usize) {}
    /// The mount of `rider` landed after falling `distance`.
    fn rider_fell(&mut self, _rider: i32, _distance: f32) {}
}

/// `EntityLiving` state.
#[derive(Clone, Debug, Default)]
pub struct Living {
    pub health: i32,
    pub prev_health: i32,
    pub hurt_time: i32,
    pub max_hurt_time: i32,
    pub death_time: i32,
    pub attack_time: i32,
    pub attacked_at_yaw: f32,
    pub hearts_halves_life: i32,
    pub natural_armor_rating: i32,
    /// The armor a player wears, which protects against and wears with each hit.
    pub armor: [Option<ItemStack>; 4],
    pub damage_remainder: i32,
    pub move_strafing: f32,
    pub move_forward: f32,
    pub random_yaw_velocity: f32,
    pub jumping: bool,
    pub move_speed: f32,
    pub render_yaw_offset: f32,
    pub prev_render_yaw_offset: f32,
    pub swing_progress: f32,
    pub prev_swing_progress: f32,
    pub walk_animation_speed: f32,
    pub prev_walk_animation_speed: f32,
    pub walk_animation_progress: f32,
    pub camera_pitch: f32,
    pub prev_camera_pitch: f32,
    pub entity_age: i32,
    pub living_sound_time: i32,
}

impl Living {
    pub fn new(health: i32) -> Self {
        Self {
            health,
            hearts_halves_life: 20,
            move_speed: 0.7,
            ..Default::default()
        }
    }
}

/// Entity flags of data watcher entry 0.
pub mod flags {
    pub const ON_FIRE: u8 = 1;
    pub const SNEAKING: u8 = 2;
    pub const RIDING: u8 = 4;
}

/// The fields of `Entity` that physics needs.
#[derive(Clone, Debug, Default)]
pub struct Entity {
    pub mob: Option<mob::MobKind>,
    pub vehicle: Option<vehicle::VehicleKind>,
    pub id: i32,
    pub pos: DVec3,
    pub prev_pos: DVec3,
    pub last_tick_pos: DVec3,
    pub motion: DVec3,
    pub yaw: f32,
    pub pitch: f32,
    pub prev_yaw: f32,
    pub prev_pitch: f32,
    pub bb: Aabb,
    pub on_ground: bool,
    pub collided_horizontally: bool,
    pub collided_vertically: bool,
    pub in_web: bool,
    pub dead: bool,
    pub y_offset: f32,
    pub width: f32,
    pub height: f32,
    pub prev_distance_walked: f32,
    pub distance_walked: f32,
    pub fall_distance: f32,
    next_step_distance: i32,
    pub y_size: f32,
    pub step_height: f32,
    pub no_clip: bool,
    pub collision_reduction: f32,
    pub rand: JavaRandom,
    pub ticks_existed: i32,
    pub fire_resistance: i32,
    pub fire: i32,
    pub air: i32,
    pub in_water: bool,
    pub hearts_life: i32,
    first_update: bool,
    pub immune_to_fire: bool,
    pub damage_received: i32,
    /// An eye height overriding the default of the entity type.
    pub eye: Option<f32>,
    /// `canTriggerWalking`: whether moving plays step sounds and tramples blocks.
    pub triggers_walking: bool,
    /// Data watcher entry 0, see [`flags`].
    pub flags: u8,
    pub living: Option<Living>,
    pub sleep: Sleep,
    pub portal: Portal,
    /// `ridingEntity`.
    pub riding: Option<i32>,
    /// `riddenByEntity`.
    pub rider: Option<i32>,
    /// `entityRiderYawDelta` and `entityRiderPitchDelta`: mount rotation still to follow.
    pub rider_turn: [f64; 2],
    /// `EntityPlayerMP` takes fall state from the client (`handleFalling`), not from moves.
    pub manual_falls: bool,
    /// `EntityPlayerMP.ticksOfInvuln`: no damage at all while it runs.
    pub spawn_immunity: i32,
    /// `EntityItem.handleWaterMovement` checks its whole box.
    pub full_water_box: bool,
}

/// The sleeping state of `EntityPlayer`.
#[derive(Clone, Copy, Debug, Default)]
pub struct Sleep {
    pub sleeping: bool,
    pub timer: i32,
    /// `playerLocation`: the bed of the current or last sleep.
    pub bed: Option<[i32; 3]>,
    /// Set by damage, which wakes the player up.
    pub disturbed: bool,
}

impl Sleep {
    /// `isPlayerFullyAsleep`.
    pub fn fully_asleep(&self) -> bool {
        self.sleeping && self.timer >= 100
    }

    /// The sleep timer of `EntityPlayer.onUpdate`, which also fades out after waking.
    pub fn tick(&mut self) {
        if self.sleeping {
            self.timer = (self.timer + 1).min(100);
        } else if self.timer > 0 {
            self.timer += 1;
            if self.timer >= 110 {
                self.timer = 0;
            }
        }
    }
}

/// The portal state of `EntityPlayer`.
#[derive(Clone, Copy, Debug)]
pub struct Portal {
    /// `inPortal`: touched a portal this tick.
    pub inside: bool,
    /// `timeInPortal`: grows to 1 inside a portal, which then teleports.
    pub time: f32,
    pub prev_time: f32,
    /// `timeUntilPortal`: no portal is entered while it runs.
    pub cooldown: i32,
}

impl Default for Portal {
    fn default() -> Self {
        Self {
            inside: false,
            time: 0.0,
            prev_time: 0.0,
            cooldown: 20,
        }
    }
}

impl Portal {
    /// `setInPortal`.
    pub fn enter(&mut self) {
        if self.cooldown > 0 {
            self.cooldown = 10;
        } else {
            self.inside = true;
        }
    }

    /// The portal timer of `onLivingUpdate`. Returns true once the time inside is full.
    pub fn tick(&mut self) -> bool {
        self.prev_time = self.time;
        let full = if self.inside {
            self.time = (self.time + 0.0125).min(1.0);
            self.time >= 1.0
        } else {
            self.time = (self.time - 0.05).max(0.0);
            false
        };
        self.inside = false;
        if self.cooldown > 0 {
            self.cooldown -= 1;
        }
        full
    }
}

impl Entity {
    pub fn new(id: i32, width: f32, height: f32, rand: JavaRandom) -> Self {
        let mut entity = Self {
            id,
            width,
            height,
            rand,
            next_step_distance: 1,
            fire_resistance: 1,
            air: 300,
            first_update: true,
            triggers_walking: true,
            ..Default::default()
        };
        entity.set_position(DVec3::ZERO);
        entity
    }

    /// A living entity with the defaults of the `EntityLiving` constructor.
    pub fn new_living(id: i32, width: f32, height: f32, health: i32, rand: JavaRandom) -> Self {
        let mut entity = Self::new(id, width, height, rand);
        entity.step_height = 0.5;
        entity.living = Some(Living::new(health));
        entity
    }

    /// The local player of `EntityPlayerSP`, positioned at eye height.
    pub fn new_player(id: i32, rand: JavaRandom) -> Self {
        let mut entity = Self::new_living(id, 0.6, 1.8, 20, rand);
        entity.y_offset = 1.62;
        entity.fire_resistance = 20;
        entity.eye = Some(0.12);
        entity
    }

    /// `EntityPlayerMP`: positioned at the feet and without step height.
    pub fn new_server_player(id: i32, rand: JavaRandom) -> Self {
        let mut entity = Self::new_player(id, rand);
        entity.y_offset = 0.0;
        entity.eye = Some(1.62);
        entity.step_height = 0.0;
        entity.manual_falls = true;
        entity
    }

    /// Only players have a separate eye height.
    pub fn is_player(&self) -> bool {
        self.eye.is_some() && self.mob.is_none()
    }

    /// `isEntityAlive`.
    pub fn is_alive(&self) -> bool {
        !self.dead && self.living.as_ref().is_some_and(|l| l.health > 0)
    }

    /// `BlockPortal.onEntityCollidedWithBlock`.
    pub fn touch_portal(&mut self) {
        if self.is_player() && self.riding.is_none() && self.rider.is_none() {
            self.portal.enter();
        }
    }

    pub fn flag(&self, flag: u8) -> bool {
        self.flags & flag != 0
    }

    pub fn set_flag(&mut self, flag: u8, value: bool) {
        if value {
            self.flags |= flag;
        } else {
            self.flags &= !flag;
        }
    }

    pub fn is_sneaking(&self) -> bool {
        self.flag(flags::SNEAKING)
    }

    /// The pitch of most entity sounds: around 1, randomly 0.2 up or down.
    pub fn sound_pitch(&mut self) -> f32 {
        (self.rand.next_float() - self.rand.next_float()) * 0.2 + 1.0
    }

    pub fn hurt(&mut self, amount: i32) -> bool {
        self.hurt_fresh(amount).is_some()
    }

    /// `EntityLiving.heal`: health up to 20, and the next hit is accepted at once.
    pub fn heal(&mut self, amount: i32) {
        if let Some(living) = &mut self.living
            && living.health > 0
        {
            living.health = (living.health + amount).min(20);
            self.hearts_life = living.hearts_halves_life / 2;
        }
    }

    /// The damage of `attackEntityFrom`: `None` when refused, otherwise whether it was a fresh
    /// hit rather than the extra damage of a stronger one while still immune.
    pub fn hurt_fresh(&mut self, amount: i32) -> Option<bool> {
        let Some(living) = &mut self.living else {
            self.damage_received += amount;
            return Some(true);
        };
        if living.health <= 0 || self.dead || amount < 0 {
            return None;
        }
        living.entity_age = 0;
        let fresh = self.hearts_life <= living.hearts_halves_life / 2;
        let damage = if !fresh {
            if amount <= living.natural_armor_rating {
                return None;
            }
            amount - living.natural_armor_rating
        } else {
            living.prev_health = living.health;
            self.hearts_life = living.hearts_halves_life;
            living.hurt_time = 10;
            living.max_hurt_time = 10;
            amount
        };
        living.natural_armor_rating = amount;
        let protection = crate::inventory::armor_value(&living.armor).clamp(0, 20);
        let scaled = damage * (25 - protection) + living.damage_remainder;
        crate::inventory::damage_armor(&mut living.armor, damage);
        living.health -= scaled / 25;
        living.damage_remainder = scaled % 25;
        self.damage_received += damage;
        Some(fresh)
    }

    pub fn look(&self) -> DVec3 {
        let yaw = -self.yaw.to_radians() - PI;
        let pitch = -self.pitch.to_radians();
        let cos = -math::cos(pitch);
        DVec3::new(
            f64::from(math::sin(yaw) * cos),
            f64::from(math::sin(pitch)),
            f64::from(math::cos(yaw) * cos),
        )
    }

    /// `setPosition`: moves the entity and rebuilds its bounding box.
    pub fn set_position(&mut self, pos: DVec3) {
        self.pos = pos;
        let half = f64::from(self.width / 2.0);
        let bottom = pos.y - f64::from(self.y_offset) + f64::from(self.y_size);
        self.bb = Aabb::from_coords(
            pos.x - half,
            bottom,
            pos.z - half,
            pos.x + half,
            bottom + f64::from(self.height),
            pos.z + half,
        );
    }

    /// The position `moveEntity` derives from the moved box.
    fn position_from_box(&mut self) {
        self.pos = DVec3::new(
            (self.bb.min.x + self.bb.max.x) / 2.0,
            self.bb.min.y + f64::from(self.y_offset) - f64::from(self.y_size),
            (self.bb.min.z + self.bb.max.z) / 2.0,
        );
    }

    /// `setSize`.
    pub fn set_size(&mut self, width: f32, height: f32) {
        self.width = width;
        self.height = height;
    }

    /// `setRotation`.
    pub fn set_rotation(&mut self, yaw: f32, pitch: f32) {
        self.yaw = yaw % 360.0;
        self.pitch = pitch % 360.0;
    }

    /// `setLocationAndAngles`: places the entity with its feet at `feet`.
    pub fn set_location_and_angles(&mut self, feet: DVec3, yaw: f32, pitch: f32) {
        let pos = feet + DVec3::new(0.0, f64::from(self.y_offset), 0.0);
        self.prev_pos = pos;
        self.last_tick_pos = pos;
        self.yaw = yaw;
        self.pitch = pitch;
        self.set_position(pos);
    }

    /// `setPositionAndRotation`.
    pub fn set_position_and_rotation(&mut self, pos: DVec3, yaw: f32, pitch: f32) {
        self.prev_pos = pos;
        self.prev_yaw = yaw;
        self.prev_pitch = pitch;
        self.y_size = 0.0;
        self.set_position(pos);
        self.set_rotation(yaw, pitch);
    }

    /// `setAngles`: mouse look, 0.15 degrees per unit.
    pub fn turn(&mut self, dx: f32, dy: f32) {
        let (old_pitch, old_yaw) = (self.pitch, self.yaw);
        self.yaw = (f64::from(self.yaw) + f64::from(dx) * 0.15) as f32;
        self.pitch = ((f64::from(self.pitch) - f64::from(dy) * 0.15) as f32).clamp(-90.0, 90.0);
        self.prev_pitch += self.pitch - old_pitch;
        self.prev_yaw += self.yaw - old_yaw;
    }

    /// `getEyeHeight`: above the entity position, which for players already is at eye height.
    pub fn eye_height(&self) -> f32 {
        match (self.eye, &self.living) {
            (Some(eye), _) => eye,
            (None, Some(_)) => self.height * 0.85,
            (None, None) => 0.0,
        }
    }

    /// `getYOffset`: how far above the mount's seat a rider's position is.
    pub fn riding_offset(&self) -> f64 {
        if self.living.is_some() && self.mob.is_none() {
            f64::from(self.y_offset - 0.5)
        } else {
            f64::from(self.y_offset)
        }
    }

    /// `updateRidden` once the mount moved: sits on `seat`, turning along with the mount.
    pub fn follow_mount(&mut self, seat: DVec3, mount_turn: [f32; 2]) {
        self.set_position(seat + DVec3::Y * self.riding_offset());
        let [yaw, pitch] = &mut self.rider_turn;
        *yaw += f64::from(mount_turn[0]);
        *pitch += f64::from(mount_turn[1]);
        for delta in [&mut *yaw, &mut *pitch] {
            while *delta >= 180.0 {
                *delta -= 360.0;
            }
            while *delta < -180.0 {
                *delta += 360.0;
            }
        }
        let step_yaw = (*yaw * 0.5).clamp(-10.0, 10.0);
        let step_pitch = (*pitch * 0.5).clamp(-10.0, 10.0);
        *yaw -= step_yaw;
        *pitch -= step_pitch;
        self.yaw = (f64::from(self.yaw) + step_yaw) as f32;
        self.pitch = (f64::from(self.pitch) + step_pitch) as f32;
    }

    /// The bottom of the entity, where its feet are.
    pub fn feet(&self) -> DVec3 {
        DVec3::new(self.pos.x, self.bb.min.y, self.pos.z)
    }

    /// The block the entity stands on, as its step sounds find it.
    fn block_below(&self) -> [i32; 3] {
        [
            floor_f64(self.pos.x),
            floor_f64(self.pos.y - f64::from(0.2_f32) - f64::from(self.y_offset)),
            floor_f64(self.pos.z),
        ]
    }

    /// `moveFlying`: accelerates along the look direction.
    pub fn move_flying(&mut self, strafe: f32, forward: f32, scale: f32) {
        let length = (strafe * strafe + forward * forward).sqrt();
        if length < 0.01 {
            return;
        }
        let factor = scale / if length < 1.0 { 1.0 } else { length };
        let (strafe, forward) = (strafe * factor, forward * factor);
        let sin = math::sin(self.yaw * PI / 180.0);
        let cos = math::cos(self.yaw * PI / 180.0);
        self.motion.x += f64::from(strafe * cos - forward * sin);
        self.motion.z += f64::from(forward * cos + strafe * sin);
    }

    /// `moveEntity`: moves by `d`, sliding along collisions, stepping up ledges, keeping
    /// sneaking players on edges, and triggering block walking and collision effects.
    pub fn move_entity(&mut self, world: &mut impl EntityWorld, mut d: DVec3) {
        if self.no_clip {
            self.bb = self.bb.offset(d);
            self.position_from_box();
            return;
        }
        self.y_size *= 0.4;
        let (start_x, start_z) = (self.pos.x, self.pos.z);
        if self.in_web {
            self.in_web = false;
            d *= DVec3::new(0.25, f64::from(0.05_f32), 0.25);
            self.motion = DVec3::ZERO;
        }
        let mut requested = d;
        let original_bb = self.bb;
        let sneak_edge = self.on_ground && self.is_sneaking();
        if sneak_edge {
            let step = 0.05;
            for (axis, unit) in [(0, DVec3::X), (2, DVec3::Z)] {
                while d[axis] != 0.0
                    && world
                        .colliding_boxes(self, &self.bb.offset(unit * d[axis] - DVec3::Y))
                        .is_empty()
                {
                    d[axis] = if d[axis] < step && d[axis] >= -step {
                        0.0
                    } else if d[axis] > 0.0 {
                        d[axis] - step
                    } else {
                        d[axis] + step
                    };
                    requested[axis] = d[axis];
                }
            }
        }

        let boxes = world.colliding_boxes(self, &self.bb.expand_towards(d));
        d = self.slide(&boxes, d);
        if self.step_height > 0.0
            && (self.on_ground || (requested.y != d.y && requested.y < 0.0))
            && (sneak_edge || self.y_size < 0.05)
            && (requested.x != d.x || requested.z != d.z)
        {
            let (flat, flat_bb) = (d, self.bb);
            self.bb = original_bb;
            let up = DVec3::new(requested.x, f64::from(self.step_height), requested.z);
            let boxes = world.colliding_boxes(self, &self.bb.expand_towards(up));
            let mut stepped = self.slide(&boxes, up);
            stepped.y = f64::from(-self.step_height);
            for b in &boxes {
                stepped.y = b.clip_y(&self.bb, stepped.y);
            }
            self.bb = self.bb.offset(DVec3::new(0.0, stepped.y, 0.0));
            if flat.x * flat.x + flat.z * flat.z >= stepped.x * stepped.x + stepped.z * stepped.z {
                d = flat;
                self.bb = flat_bb;
            } else {
                d = stepped;
                let fraction = self.bb.min.y - f64::from(self.bb.min.y as i32);
                if fraction > 0.0 {
                    self.y_size = (f64::from(self.y_size) + fraction + 0.01) as f32;
                }
            }
        }

        self.position_from_box();
        self.collided_horizontally = requested.x != d.x || requested.z != d.z;
        self.collided_vertically = requested.y != d.y;
        self.on_ground = requested.y != d.y && requested.y < 0.0;
        if !self.manual_falls {
            self.update_fall_state(world, d.y);
        }
        for axis in 0..3 {
            if requested[axis] != d[axis] {
                self.motion[axis] = 0.0;
            }
        }

        let (dx, dz) = (self.pos.x - start_x, self.pos.z - start_z);
        if self.triggers_walking && !sneak_edge && self.riding.is_none() {
            self.distance_walked = (f64::from(self.distance_walked)
                + f64::from((dx * dx + dz * dz).sqrt() as f32) * 0.6)
                as f32;
            let [x, y, z] = self.block_below();
            let mut block = world.block(x, y, z);
            if world.block(x, y - 1, z) == Block::FENCE {
                block = Block::FENCE;
            }
            if self.distance_walked > self.next_step_distance as f32 && !block.is_air() {
                self.next_step_distance += 1;
                let sound = block.sound();
                if world.block(x, y + 1, z) == Block::SNOW_LAYER {
                    let snow = Block::SNOW_LAYER.sound();
                    world.step_sound(self, snow.step(), snow.volume() * 0.15, snow.pitch());
                } else if !block.material().is_liquid() {
                    world.step_sound(self, sound.step(), sound.volume() * 0.15, sound.pitch());
                }
                world.on_walked(block, [x, y, z], self);
            }
        }

        self.collide_blocks(world);
    }

    /// The collision of `moveEntity`: y first, then x, then z.
    fn slide(&mut self, boxes: &[Aabb], mut d: DVec3) -> DVec3 {
        for b in boxes {
            d.y = b.clip_y(&self.bb, d.y);
        }
        self.bb = self.bb.offset(DVec3::new(0.0, d.y, 0.0));
        for b in boxes {
            d.x = b.clip_x(&self.bb, d.x);
        }
        self.bb = self.bb.offset(DVec3::new(d.x, 0.0, 0.0));
        for b in boxes {
            d.z = b.clip_z(&self.bb, d.z);
        }
        self.bb = self.bb.offset(DVec3::new(0.0, 0.0, d.z));
        d
    }

    pub fn collide_blocks(&mut self, world: &mut impl EntityWorld) {
        let min = (self.bb.min + 0.001).to_array().map(floor_f64);
        let max = (self.bb.max - 0.001).to_array().map(floor_f64);
        if world.chunks_exist(min, max) {
            for x in min[0]..=max[0] {
                for y in min[1]..=max[1] {
                    for z in min[2]..=max[2] {
                        let block = world.block(x, y, z);
                        if !block.is_air() {
                            world.on_collided(block, [x, y, z], self);
                        }
                    }
                }
            }
        }

        let wet = self.is_wet(world);
        let (min, max) = cells(&self.bb.deflate(DVec3::splat(0.001)));
        let burning = world.chunks_exist(min, max)
            && any_cell(min, max, |x, y, z| {
                matches!(
                    world.block(x, y, z),
                    Block::FIRE | Block::FLOWING_LAVA | Block::LAVA
                )
            });
        if burning {
            if !self.immune_to_fire {
                world.damage(self, 1);
            }
            if !wet {
                self.fire += 1;
                if self.fire == 0 {
                    self.fire = 300;
                }
            }
        } else if self.fire <= 0 {
            self.fire = -self.fire_resistance;
        }
        if wet && self.fire > 0 {
            let pitch = 1.6 + (self.rand.next_float() - self.rand.next_float()) * 0.4;
            world.play_sound(self, "random.fizz", 0.7, pitch);
            self.fire = -self.fire_resistance;
        }
    }

    /// `updateFallState`.
    pub fn update_fall_state(&mut self, world: &mut impl EntityWorld, dy: f64) {
        if self.on_ground {
            if self.fall_distance > 0.0 {
                self.fall(world, self.fall_distance);
                self.fall_distance = 0.0;
            }
        } else if dy < 0.0 {
            self.fall_distance = (f64::from(self.fall_distance) - dy) as f32;
        }
    }

    /// `fall`: living entities take damage from falls over three blocks, and so do riders.
    pub fn fall(&mut self, world: &mut impl EntityWorld, distance: f32) {
        if matches!(self.mob, Some(mob::MobKind::Chicken | mob::MobKind::Ghast)) {
            return;
        }
        if let Some(rider) = self.rider {
            world.rider_fell(rider, distance);
            if self.mob == Some(mob::MobKind::Pig) && distance > 5.0 {
                world.award_achievement(rider, 15);
            }
        }
        let damage = (distance - 3.0).ceil() as i32;
        if self.living.is_some() && damage > 0 {
            world.damage(self, damage);
            let [x, y, z] = self.block_below();
            let block = world.block(x, y, z);
            if !block.is_air() {
                let sound = block.sound();
                world.step_sound(
                    self,
                    sound.step(),
                    sound.volume() * 0.5,
                    sound.pitch() * 0.75,
                );
            }
        }
    }

    /// `isWet`.
    pub fn is_wet(&self, world: &impl EntityWorld) -> bool {
        let [x, y, z] = self.pos.to_array().map(floor_f64);
        self.in_water || world.rained_on(x, y, z)
    }

    /// `World.handleMaterialAcceleration` for water: pushes the entity along the currents in
    /// `region` and reports whether it touches water.
    pub fn handle_water(&mut self, world: &impl EntityWorld, region: &Aabb) -> bool {
        let (min, max) = cells(region);
        if !world.chunks_exist(min, max) {
            return false;
        }
        let mut inside = false;
        let mut push = DVec3::ZERO;
        for x in min[0]..max[0] {
            for y in min[1]..max[1] {
                for z in min[2]..max[2] {
                    if world.block(x, y, z).material() != Material::Water {
                        continue;
                    }
                    let surface =
                        f64::from((y + 1) as f32 - fluid::percent_air(world.metadata(x, y, z)));
                    if f64::from(max[1]) >= surface {
                        inside = true;
                        push += fluid::flow_vector(world, Material::Water, x, y, z);
                    }
                }
            }
        }
        if push.length() > 0.0 {
            self.motion += fluid::normalize(push) * 0.014;
        }
        inside
    }

    /// `handleLavaMovement`.
    pub fn handle_lava_movement(&self, world: &impl EntityWorld) -> bool {
        let region = self.bb.inflate(DVec3::new(
            f64::from(-0.1_f32),
            f64::from(-0.4_f32),
            f64::from(-0.1_f32),
        ));
        material_in(world, &region, Material::Lava)
    }

    /// `isInsideOfMaterial`: whether the eyes are below the surface of a fluid.
    pub fn eyes_in(&self, world: &impl EntityWorld, material: Material) -> bool {
        let eye = self.pos.y + f64::from(self.eye_height());
        let (x, y, z) = (floor_f64(self.pos.x), floor_f64(eye), floor_f64(self.pos.z));
        if world.block(x, y, z).material() != material {
            return false;
        }
        let air = fluid::percent_air(world.metadata(x, y, z)) - 1.0 / 9.0;
        eye < f64::from((y + 1) as f32 - air)
    }

    /// `isEntityInsideOpaqueBlock`, never true for sleeping players.
    pub fn is_inside_opaque_block(&self, world: &impl EntityWorld) -> bool {
        !self.sleep.sleeping
            && (0..8).any(|i| {
                let dx = ((i & 1) as f32 - 0.5) * self.width * 0.9;
                let dy = ((i >> 1 & 1) as f32 - 0.5) * 0.1;
                let dz = ((i >> 2 & 1) as f32 - 0.5) * self.width * 0.9;
                let x = floor_f64(self.pos.x + f64::from(dx));
                let y = floor_f64(self.pos.y + f64::from(self.eye_height()) + f64::from(dy));
                let z = floor_f64(self.pos.z + f64::from(dz));
                world.block(x, y, z).is_normal_cube()
            })
    }

    /// The common part of `Entity.onEntityUpdate`: tick counters, water and fire.
    pub fn base_update(&mut self, world: &mut impl EntityWorld) {
        self.ticks_existed += 1;
        self.prev_distance_walked = self.distance_walked;
        self.prev_pos = self.pos;
        self.prev_pitch = self.pitch;
        self.prev_yaw = self.yaw;
        let water = if self.full_water_box {
            self.bb
        } else {
            self.bb
                .inflate(DVec3::new(0.0, f64::from(-0.4_f32), 0.0))
                .deflate(DVec3::splat(0.001))
        };
        if self.handle_water(world, &water) {
            if !self.in_water && !self.first_update {
                self.splash(world);
            }
            self.fall_distance = 0.0;
            self.in_water = true;
            self.fire = 0;
        } else {
            self.in_water = false;
        }
        if world.is_remote() {
            self.fire = 0;
        } else if self.fire > 0 {
            if self.immune_to_fire {
                self.fire = (self.fire - 4).max(0);
            } else {
                if self.fire % 20 == 0 {
                    world.damage(self, 1);
                }
                self.fire -= 1;
            }
        }
        if self.handle_lava_movement(world) && !self.immune_to_fire {
            world.damage(self, 4);
            self.fire = 600;
        }
        // `kill`: living things take repeated damage in the void, the rest vanish
        if self.pos.y < -64.0 {
            if self.living.is_some() {
                world.damage(self, 4);
            } else {
                self.dead = true;
            }
        }
        if !world.is_remote() {
            self.set_flag(flags::ON_FIRE, self.fire > 0);
            self.set_flag(flags::RIDING, self.riding.is_some());
        }
        self.first_update = false;
    }

    /// The sound and particles of entering water.
    fn splash(&mut self, world: &mut impl EntityWorld) {
        let speed = (self.motion.x * self.motion.x * f64::from(0.2_f32)
            + self.motion.y * self.motion.y
            + self.motion.z * self.motion.z * f64::from(0.2_f32))
        .sqrt() as f32;
        let volume = (speed * 0.2).min(1.0);
        let pitch = 1.0 + (self.rand.next_float() - self.rand.next_float()) * 0.4;
        world.play_sound(self, "random.splash", volume, pitch);
        let surface = floor_f64(self.bb.min.y) as f32 + 1.0;
        let count = (1.0 + self.width * 20.0).ceil() as i32;
        for particle in ["bubble", "splash"] {
            for _ in 0..count {
                let dx = (self.rand.next_float() * 2.0 - 1.0) * self.width;
                let dz = (self.rand.next_float() * 2.0 - 1.0) * self.width;
                let pos = DVec3::new(
                    self.pos.x + f64::from(dx),
                    f64::from(surface),
                    self.pos.z + f64::from(dz),
                );
                let velocity = if particle == "bubble" {
                    self.motion - DVec3::new(0.0, f64::from(self.rand.next_float() * 0.2), 0.0)
                } else {
                    self.motion
                };
                world.spawn_particle(particle, pos, velocity);
            }
        }
    }

    // ---- Living -------------------------------------------------

    /// `EntityLiving.onEntityUpdate`: the base update plus living sounds, suffocation,
    /// drowning, hurt timers and the death animation. Returns true when the death animation
    /// ended and the entity should be removed.
    pub fn living_entity_update(&mut self, world: &mut impl EntityWorld) -> bool {
        self.living_update_with_sound(world, |entity, world| world.living_sound(entity))
    }

    pub fn living_update_with_sound<W: EntityWorld>(
        &mut self,
        world: &mut W,
        sound: impl FnOnce(&mut Self, &mut W),
    ) -> bool {
        if let Some(living) = &mut self.living {
            living.prev_swing_progress = living.swing_progress;
        }
        self.base_update(world);
        let living = self.living.get_or_insert_with(|| Living::new(0));
        let time = living.living_sound_time;
        living.living_sound_time += 1;
        if self.rand.next_int(1000) < time {
            let animal = self.mob.is_some_and(|kind| !kind.hostile());
            living.living_sound_time = if animal { -120 } else { -80 };
            sound(self, world);
        }
        if self.is_alive() && self.is_inside_opaque_block(world) {
            world.damage(self, 1);
        }
        if self.immune_to_fire || world.is_remote() {
            self.fire = 0;
        }
        if self.is_alive()
            && self.mob != Some(mob::MobKind::Squid)
            && self.eyes_in(world, Material::Water)
        {
            self.air -= 1;
            if self.air == -20 {
                self.air = 0;
                for _ in 0..8 {
                    let d = DVec3::new(
                        f64::from(self.rand.next_float() - self.rand.next_float()),
                        f64::from(self.rand.next_float() - self.rand.next_float()),
                        f64::from(self.rand.next_float() - self.rand.next_float()),
                    );
                    world.spawn_particle("bubble", self.pos + d, self.motion);
                }
                world.damage(self, 2);
            }
            self.fire = 0;
        } else {
            self.air = 300;
        }
        if self.hearts_life > 0 {
            self.hearts_life -= 1;
        }
        let mut finished = false;
        if let Some(living) = &mut self.living {
            living.prev_camera_pitch = living.camera_pitch;
            living.prev_render_yaw_offset = living.render_yaw_offset;
            if living.attack_time > 0 {
                living.attack_time -= 1;
            }
            if living.hurt_time > 0 {
                living.hurt_time -= 1;
            }
            if living.health <= 0 {
                living.death_time += 1;
                // Once: the original removes the dead entity, players keep ticking until respawn
                finished = living.death_time > 20 && !self.dead;
            }
        }
        if finished {
            self.dead = true;
            for _ in 0..20 {
                let v = DVec3::new(
                    self.rand.next_gaussian(),
                    self.rand.next_gaussian(),
                    self.rand.next_gaussian(),
                ) * 0.02;
                let p = DVec3::new(
                    self.pos.x + f64::from(self.rand.next_float() * self.width * 2.0)
                        - f64::from(self.width),
                    self.pos.y + f64::from(self.rand.next_float() * self.height),
                    self.pos.z + f64::from(self.rand.next_float() * self.width * 2.0)
                        - f64::from(self.width),
                );
                world.spawn_particle("explode", p, v);
            }
        }
        self.prev_yaw = self.yaw;
        self.prev_pitch = self.pitch;
        finished
    }

    /// `isOnLadder`.
    pub fn is_on_ladder(&self, world: &impl EntityWorld) -> bool {
        if self.mob == Some(mob::MobKind::Spider) {
            return self.collided_horizontally;
        }
        let [x, _, z] = self.pos.to_array().map(floor_f64);
        world.block(x, floor_f64(self.bb.min.y), z) == Block::LADDER
    }

    fn ground_friction(&self, world: &impl EntityWorld) -> f32 {
        if !self.on_ground {
            return 0.91;
        }
        let [x, _, z] = self.pos.to_array().map(floor_f64);
        let below = world.block(x, floor_f64(self.bb.min.y) - 1, z);
        if below.is_air() {
            546.0 * 0.1 * 0.1 * 0.1
        } else {
            below.slipperiness() * 0.91
        }
    }

    /// `EntityLiving.moveEntityWithHeading`: swimming, lava, or walking with friction and ladders.
    pub fn move_with_heading(&mut self, world: &mut impl EntityWorld, strafe: f32, forward: f32) {
        if self.in_water || self.handle_lava_movement(world) {
            let drag = if self.in_water {
                f64::from(0.8_f32)
            } else {
                0.5
            };
            let start_y = self.pos.y;
            self.move_flying(strafe, forward, 0.02);
            self.move_entity(world, self.motion);
            self.motion *= drag;
            self.motion.y -= 0.02;
            let offset = DVec3::new(
                self.motion.x,
                self.motion.y + f64::from(0.6_f32) - self.pos.y + start_y,
                self.motion.z,
            );
            // `isOffsetPositionInLiquid`
            let region = self.bb.offset(offset);
            if self.collided_horizontally
                && world.colliding_boxes(self, &region).is_empty()
                && !any_liquid(world, &region)
            {
                self.motion.y = f64::from(0.3_f32);
            }
        } else {
            let friction = self.ground_friction(world);
            let acceleration = 0.162_771_36 / (friction * friction * friction);
            let scale = if self.on_ground {
                0.1 * acceleration
            } else {
                0.02
            };
            self.move_flying(strafe, forward, scale);
            let friction = self.ground_friction(world);
            if self.is_on_ladder(world) {
                let limit = f64::from(0.15_f32);
                self.motion.x = self.motion.x.clamp(-limit, limit);
                self.motion.z = self.motion.z.clamp(-limit, limit);
                self.fall_distance = 0.0;
                if self.motion.y < -0.15 {
                    self.motion.y = -0.15;
                }
                if self.is_sneaking() && self.motion.y < 0.0 {
                    self.motion.y = 0.0;
                }
            }
            self.move_entity(world, self.motion);
            if self.collided_horizontally && self.is_on_ladder(world) {
                self.motion.y = 0.2;
            }
            self.motion.y -= 0.08;
            self.motion.y *= f64::from(0.98_f32);
            self.motion.x *= f64::from(friction);
            self.motion.z *= f64::from(friction);
        }
        self.animate_walk();
    }

    /// The limb swing at the end of `moveEntityWithHeading`, from the distance moved.
    pub fn animate_walk(&mut self) {
        let (dx, dz) = (self.pos.x - self.prev_pos.x, self.pos.z - self.prev_pos.z);
        if let Some(living) = &mut self.living {
            living.prev_walk_animation_speed = living.walk_animation_speed;
            let speed = ((dx * dx + dz * dz).sqrt() as f32 * 4.0).min(1.0);
            living.walk_animation_speed += (speed - living.walk_animation_speed) * 0.4;
            living.walk_animation_progress += living.walk_animation_speed;
        }
    }

    /// The movement part of `EntityLiving.onLivingUpdate`: jumping, swimming up and the move
    /// itself.
    pub fn living_movement(&mut self, world: &mut impl EntityWorld) {
        let in_lava = self.handle_lava_movement(world);
        let Some(living) = &mut self.living else {
            return;
        };
        if living.health <= 0 || self.sleep.sleeping {
            living.jumping = false;
            living.move_strafing = 0.0;
            living.move_forward = 0.0;
            living.random_yaw_velocity = 0.0;
        }
        if living.jumping {
            if self.in_water || in_lava {
                self.motion.y += f64::from(0.04_f32);
            } else if self.on_ground {
                self.motion.y = f64::from(0.42_f32);
            }
        }
        living.move_strafing *= 0.98;
        living.move_forward *= 0.98;
        living.random_yaw_velocity *= 0.9;
        let (strafe, forward) = (living.move_strafing, living.move_forward);
        self.move_with_heading(world, strafe, forward);
    }

    /// `other.applyEntityCollision(self)`: the impulse that pushes `self` away from an entity at
    /// `other`, which receives the opposite one. Riders and mounts do not push each other.
    pub fn collision_push(&self, other: i32, position: DVec3, reduction: f32) -> Option<DVec3> {
        if self.rider == Some(other) || self.riding == Some(other) {
            return None;
        }
        let delta = self.pos - position;
        let largest = delta.x.abs().max(delta.z.abs());
        if largest < 0.01 {
            return None;
        }
        let scale = largest.sqrt();
        Some(
            DVec3::new(delta.x / scale, 0.0, delta.z / scale)
                * (1.0 / scale).min(1.0)
                * 0.05
                * f64::from(1.0 - reduction),
        )
    }

    /// The body rotation of `EntityLiving.onUpdate`, after the update.
    pub fn update_body_rotation(&mut self) {
        let (dx, dz) = (self.pos.x - self.prev_pos.x, self.pos.z - self.prev_pos.z);
        let Some(living) = &mut self.living else {
            return;
        };
        let mut body = living.render_yaw_offset;
        if (dx * dx + dz * dz).sqrt() as f32 > 0.05 {
            body = (dz.atan2(dx) as f32) * 180.0 / PI - 90.0;
        }
        if living.swing_progress > 0.0 {
            body = self.yaw;
        }
        living.render_yaw_offset += wrap_degrees(body - living.render_yaw_offset) * 0.3;
        let head = wrap_degrees(self.yaw - living.render_yaw_offset).clamp(-75.0, 75.0);
        living.render_yaw_offset = self.yaw - head;
        if head * head > 2500.0 {
            living.render_yaw_offset += head * 0.2;
        }
        follow_angle(&mut self.prev_yaw, self.yaw);
        follow_angle(&mut living.prev_render_yaw_offset, living.render_yaw_offset);
        follow_angle(&mut self.prev_pitch, self.pitch);
    }

    /// `Entity.pushOutOfBlocks`: nudges an entity inside a cube towards the nearest open side.
    pub fn push_out_of_cube(&mut self, world: &impl EntityWorld, x: f64, y: f64, z: f64) {
        let point = [x, y, z];
        let cell = point.map(floor_f64);
        let solid = |[x, y, z]: [i32; 3]| world.block(x, y, z).is_normal_cube();
        if !solid(cell) {
            return;
        }
        let fraction: [f64; 3] = std::array::from_fn(|i| point[i] - f64::from(cell[i]));
        let mut best = 9999.0;
        let mut direction = None;
        for axis in 0..3 {
            for positive in [false, true] {
                let mut neighbor = cell;
                neighbor[axis] += if positive { 1 } else { -1 };
                let distance = if positive {
                    1.0 - fraction[axis]
                } else {
                    fraction[axis]
                };
                if !solid(neighbor) && distance < best {
                    best = distance;
                    direction = Some((axis, positive));
                }
            }
        }
        let push = self.rand.next_float() * 0.2 + 0.1;
        if let Some((axis, positive)) = direction {
            self.motion[axis] = f64::from(if positive { push } else { -push });
        }
    }

    // ---- Player -------------------------------------------------

    /// `EntityPlayerSP.pushOutOfBlocks`: nudges a player stuck in a block towards open space.
    pub fn push_out_of_blocks(&mut self, world: &impl EntityWorld, x: f64, y: f64, z: f64) {
        let (bx, by, bz) = (floor_f64(x), floor_f64(y), floor_f64(z));
        let (fx, fz) = (x - f64::from(bx), z - f64::from(bz));
        let solid = |x: i32, y: i32, z: i32| world.block(x, y, z).is_normal_cube();
        if !solid(bx, by, bz) && !solid(bx, by + 1, bz) {
            return;
        }
        let open =
            |dx: i32, dz: i32| !solid(bx + dx, by, bz + dz) && !solid(bx + dx, by + 1, bz + dz);
        let mut best = 9999.0;
        let mut direction = None;
        for (is_open, distance, axis, sign) in [
            (open(-1, 0), fx, 0, -1.0),
            (open(1, 0), 1.0 - fx, 0, 1.0),
            (open(0, -1), fz, 2, -1.0),
            (open(0, 1), 1.0 - fz, 2, 1.0),
        ] {
            if is_open && distance < best {
                best = distance;
                direction = Some((axis, sign));
            }
        }
        if let Some((axis, sign)) = direction {
            self.motion[axis] = sign * f64::from(0.1_f32);
        }
    }
}

/// Wraps an angle in degrees into `-180..180`.
fn wrap_degrees(mut angle: f32) -> f32 {
    while angle < -180.0 {
        angle += 360.0;
    }
    while angle >= 180.0 {
        angle -= 360.0;
    }
    angle
}

/// Moves the previous value of an angle by whole turns to within half a turn of `current`.
fn follow_angle(previous: &mut f32, current: f32) {
    while current - *previous < -180.0 {
        *previous -= 360.0;
    }
    while current - *previous >= 180.0 {
        *previous += 360.0;
    }
}

/// Block cells of a box as in the original loops: `floor(min)` to `floor(max + 1)` exclusive.
fn cells(region: &Aabb) -> ([i32; 3], [i32; 3]) {
    (
        region.min.to_array().map(floor_f64),
        (region.max + 1.0).to_array().map(floor_f64),
    )
}

/// Whether `test` holds for any cell from `min` to `max` exclusive.
fn any_cell(min: [i32; 3], max: [i32; 3], test: impl Fn(i32, i32, i32) -> bool) -> bool {
    (min[0]..max[0]).any(|x| (min[1]..max[1]).any(|y| (min[2]..max[2]).any(|z| test(x, y, z))))
}

/// `World.isMaterialInBB`.
pub fn material_in(world: &impl BlockView, region: &Aabb, material: Material) -> bool {
    let (min, max) = cells(region);
    any_cell(min, max, |x, y, z| {
        world.block(x, y, z).material() == material
    })
}

/// `World.getIsAnyLiquid`, which widens the box by one on negative sides.
fn any_liquid(world: &impl BlockView, region: &Aabb) -> bool {
    let (mut min, max) = cells(region);
    for (axis, cell) in min.iter_mut().enumerate() {
        if region.min[axis] < 0.0 {
            *cell -= 1;
        }
    }
    any_cell(min, max, |x, y, z| {
        world.block(x, y, z).material().is_liquid()
    })
}