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 / server / src / entity / projectiles.rs

use common::entity::{Entity, EntityWorld};
use common::math;
use common::physics::BlockHit;
use common::world::Block;
use common::world::shape::{self, BlockView};
use glam::DVec3;

use super::Kind;
use crate::level::Level;

/// The block a projectile sticks in, shared by arrows, fireballs and hooks.
#[derive(Clone, Debug)]
pub struct Stuck {
    pub tile: [i32; 3],
    pub block: Block,
    pub grounded: bool,
    pub ground_ticks: i32,
    pub air_ticks: i32,
}

impl Default for Stuck {
    fn default() -> Self {
        Self {
            tile: [-1; 3],
            block: Block::AIR,
            grounded: false,
            ground_ticks: 0,
            air_ticks: 0,
        }
    }
}

impl Stuck {
    /// A tick in the ground: counts down to despawning while the block is `still` there, or
    /// frees the projectile. Returns whether it stays stuck.
    fn update(&mut self, entity: &mut Entity, still: bool) -> bool {
        if still {
            self.ground_ticks += 1;
            entity.dead |= self.ground_ticks >= 1200;
            return true;
        }
        self.grounded = false;
        entity.motion *= DVec3::from_array(std::array::from_fn(|_| {
            f64::from(entity.rand.next_float() * 0.2)
        }));
        self.ground_ticks = 0;
        self.air_ticks = 0;
        false
    }
}

#[derive(Clone, Debug, Default)]
pub struct Arrow {
    pub owner: Option<i32>,
    pub player: bool,
    pub stuck: Stuck,
    pub metadata: u8,
    pub shake: i32,
}

/// A thrown snowball or egg (`EntitySnowball`, `EntityEgg`).
#[derive(Clone, Debug)]
pub struct Thrown {
    pub egg: bool,
    pub owner: Option<i32>,
    pub air_ticks: i32,
}

pub(super) fn heading(entity: &mut Entity, direction: DVec3, speed: f32, divergence: f32) {
    let direction = direction / f64::from(direction.length() as f32);
    let scatter = DVec3::from_array(std::array::from_fn(|_| entity.rand.next_gaussian()))
        * f64::from(0.0075_f32)
        * f64::from(divergence);
    entity.motion = (direction + scatter) * f64::from(speed);
    angles(entity);
    entity.prev_yaw = entity.yaw;
    entity.prev_pitch = entity.pitch;
}

/// Points the entity along its motion.
fn angles(entity: &mut Entity) {
    let horizontal = f64::from(
        (entity.motion.x * entity.motion.x + entity.motion.z * entity.motion.z).sqrt() as f32,
    );
    entity.yaw =
        (entity.motion.x.atan2(entity.motion.z) * 180.0 / f64::from(std::f32::consts::PI)) as f32;
    entity.pitch =
        (entity.motion.y.atan2(horizontal) * 180.0 / f64::from(std::f32::consts::PI)) as f32;
}

/// `angles`, eased in from the previous rotation.
pub(super) fn smooth_angles(entity: &mut Entity) {
    angles(entity);
    let interpolate = |previous: f32, current: f32| {
        previous + ((current - previous + 180.0).rem_euclid(360.0) - 180.0) * 0.2
    };
    entity.yaw = interpolate(entity.prev_yaw, entity.yaw);
    entity.pitch = interpolate(entity.prev_pitch, entity.pitch);
}

/// Where snowballs, eggs and arrows leave their thrower: below the eyes, a little to the side.
fn launch_point(source: &Entity) -> DVec3 {
    let yaw = source.yaw.to_radians();
    let mut position = source.pos + DVec3::Y * f64::from(source.eye_height());
    position.x -= f64::from(math::cos(yaw) * 0.16);
    position.y -= f64::from(0.1_f32);
    position.z -= f64::from(math::sin(yaw) * 0.16);
    position
}

impl Level {
    /// The block and the nearest entity a projectile moving along its motion hits, skipping its
    /// owner for the first ticks.
    pub(super) fn projectile_hit(
        &self,
        entity: &Entity,
        solid_only: bool,
        owner: Option<i32>,
        air_ticks: i32,
    ) -> (Option<BlockHit>, Option<i32>) {
        let end = entity.pos + entity.motion;
        let hit = shape::ray_trace(self, entity.pos, end, false, solid_only);
        let end = hit.map_or(end, |hit| hit.point);
        let area = entity.bb.expand_towards(entity.motion).inflate(DVec3::ONE);
        let target = self
            .entities
            .iter()
            .filter(|other| {
                other.can_be_collided()
                    && (Some(other.entity.id) != owner || air_ticks >= 5)
                    && area.intersects(&other.entity.bb)
            })
            .filter_map(|other| {
                other
                    .entity
                    .bb
                    .inflate(DVec3::splat(f64::from(0.3_f32)))
                    .intercept(entity.pos, end)
                    .map(|(point, _)| (other.entity.id, point.distance_squared(entity.pos)))
            })
            .min_by(|a, b| a.1.total_cmp(&b.1))
            .map(|(id, _)| id);
        (hit, target)
    }

    /// A copy of the entity `id`, to attack others in its name.
    pub(super) fn entity_copy(&self, id: Option<i32>) -> Option<Entity> {
        id.and_then(|id| self.entities.get(id))
            .map(|e| e.entity.clone())
    }

    /// The ground state of fireballs and hooks; returns whether they stay stuck.
    pub(super) fn stays_stuck(&mut self, entity: &mut Entity, stuck: &mut Stuck) -> bool {
        if !stuck.grounded {
            stuck.air_ticks += 1;
            return false;
        }
        let [x, y, z] = stuck.tile;
        let still = self.block(x, y, z) == stuck.block;
        stuck.update(entity, still)
    }

    /// Bubbles in water and the drag factor there, or `air` outside it.
    pub(super) fn drag(&mut self, entity: &Entity, air: f32) -> f64 {
        if !entity.in_water {
            return f64::from(air);
        }
        for _ in 0..4 {
            self.spawn_particle("bubble", entity.pos - entity.motion * 0.25, entity.motion);
        }
        f64::from(0.8_f32)
    }

    /// A snowball or egg thrown from `position` towards `direction`.
    pub fn throw(
        &mut self,
        egg: bool,
        position: DVec3,
        direction: DVec3,
        speed: f32,
        divergence: f32,
        owner: Option<i32>,
    ) {
        let mut stored = self.entities.create(Kind::Thrown(Thrown {
            egg,
            owner,
            air_ticks: 0,
        }));
        stored.entity.set_position(position);
        heading(&mut stored.entity, direction, speed, divergence);
        self.spawn(stored);
    }

    /// `ItemSnowball` and `ItemEgg`: thrown from the eyes along the look.
    pub fn throw_from(&mut self, egg: bool, thrower: &Entity) {
        let yaw = thrower.yaw.to_radians();
        let pitch = thrower.pitch.to_radians();
        let direction = DVec3::new(
            f64::from(-math::sin(yaw) * math::cos(pitch) * 0.4),
            f64::from(-math::sin(pitch) * 0.4),
            f64::from(math::cos(yaw) * math::cos(pitch) * 0.4),
        );
        let position = launch_point(thrower);
        self.throw(egg, position, direction, 1.5, 1.0, Some(thrower.id));
    }

    pub(super) fn tick_thrown(&mut self, entity: &mut Entity, thrown: &mut Thrown) {
        entity.base_update(self);
        thrown.air_ticks += 1;
        let (hit, target) = self.projectile_hit(entity, false, thrown.owner, thrown.air_ticks);
        if hit.is_some() || target.is_some() {
            if let Some(id) = target {
                let owner = self.entity_copy(thrown.owner);
                self.attack_entity(id, owner.as_ref(), 0);
            }
            if thrown.egg && entity.rand.next_int(8) == 0 {
                let count = if entity.rand.next_int(32) == 0 { 4 } else { 1 };
                for _ in 0..count {
                    let chick = self.mob_entity(
                        common::entity::mob::MobKind::Chicken,
                        entity.pos,
                        entity.yaw,
                    );
                    self.spawn(chick);
                }
            }
            for _ in 0..8 {
                self.spawn_particle("snowballpoof", entity.pos, DVec3::ZERO);
            }
            entity.dead = true;
        }
        entity.pos += entity.motion;
        angles(entity);
        let interpolate = |previous: &mut f32, current: f32| {
            while current - *previous < -180.0 {
                *previous -= 360.0;
            }
            while current - *previous >= 180.0 {
                *previous += 360.0;
            }
            *previous + (current - *previous) * 0.2
        };
        entity.pitch = interpolate(&mut entity.prev_pitch, entity.pitch);
        entity.yaw = interpolate(&mut entity.prev_yaw, entity.yaw);
        entity.motion *= self.drag(entity, 0.99);
        entity.motion.y -= f64::from(0.03_f32);
        entity.set_position(entity.pos);
    }

    /// A dispenser's arrow, which players can pick up.
    pub fn dispense_arrow(&mut self, position: DVec3, direction: DVec3) {
        let mut stored = self.entities.create(Kind::Arrow(Arrow {
            player: true,
            ..Default::default()
        }));
        stored.entity.set_position(position);
        heading(&mut stored.entity, direction, 1.1, 6.0);
        self.spawn(stored);
    }

    pub fn shoot_arrow(&mut self, source: &mut Entity, target: Option<&Entity>) {
        let mut stored = self.entities.create(Kind::Arrow(Arrow {
            owner: Some(source.id),
            player: source.mob.is_none(),
            ..Default::default()
        }));
        let entity = &mut stored.entity;
        entity.set_position(launch_point(source));
        let look = common::interaction::look(source.yaw, source.pitch);
        heading(entity, look, 1.5, 1.0);
        if let Some(target) = target {
            entity.set_position(entity.pos + DVec3::Y * f64::from(1.4_f32));
            let delta = target.pos - source.pos;
            let y =
                target.pos.y + f64::from(target.eye_height()) - f64::from(0.2_f32) - entity.pos.y;
            let lift = (delta.x * delta.x + delta.z * delta.z).sqrt() as f32 * 0.2;
            heading(
                entity,
                DVec3::new(delta.x, y + f64::from(lift), delta.z),
                0.6,
                12.0,
            );
        }
        let pitch = 1.0 / (source.rand.next_float() * 0.4 + 0.8);
        self.play_sound(source, "random.bow", 1.0, pitch);
        self.spawn(stored);
    }

    pub(super) fn tick_arrow(&mut self, entity: &mut Entity, arrow: &mut Arrow) {
        entity.base_update(self);
        arrow.shake = (arrow.shake - 1).max(0);
        let stuck = &mut arrow.stuck;
        let [x, y, z] = stuck.tile;
        let block = BlockView::block(self, x, y, z);
        if !block.is_air()
            && shape::collision_box(self, block, x, y, z).is_some_and(|b| b.contains(entity.pos))
        {
            stuck.grounded = true;
        }
        if stuck.grounded {
            let still =
                block == stuck.block && BlockView::metadata(self, x, y, z) == arrow.metadata;
            stuck.update(entity, still);
            return;
        }
        stuck.air_ticks += 1;
        let (hit, target) = self.projectile_hit(entity, true, arrow.owner, stuck.air_ticks);
        let struck = if let Some(id) = target {
            let owner = self.entity_copy(arrow.owner);
            let struck = self.attack_living(id, owner.as_ref(), 4, true);
            if struck {
                entity.dead = true;
            } else {
                entity.motion *= f64::from(-0.1_f32);
                entity.yaw += 180.0;
                entity.prev_yaw += 180.0;
                stuck.air_ticks = 0;
            }
            struck
        } else if let Some(hit) = hit {
            stuck.tile = [hit.x, hit.y, hit.z];
            stuck.block = BlockView::block(self, hit.x, hit.y, hit.z);
            arrow.metadata = BlockView::metadata(self, hit.x, hit.y, hit.z);
            entity.motion = (hit.point - entity.pos).map(|v| f64::from(v as f32));
            let length = f64::from(entity.motion.length() as f32);
            if length > 0.0 {
                entity.pos -= entity.motion / length * f64::from(0.05_f32);
            }
            stuck.grounded = true;
            arrow.shake = 7;
            true
        } else {
            false
        };
        if struck {
            let pitch = 1.2 / (entity.rand.next_float() * 0.2 + 0.9);
            self.play_sound(entity, "random.drr", 1.0, pitch);
        }
        entity.set_position(entity.pos + entity.motion);
        smooth_angles(entity);
        entity.motion *= self.drag(entity, 0.99);
        entity.motion.y -= f64::from(0.03_f32);
    }
}