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 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, ®ion, 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, ®ion).is_empty()
&& !any_liquid(world, ®ion)
{
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()
})
}