alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH 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
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);
}
}