Pumpkin/pumpkin/src/entity/projectile/eye_of_ender.rs
Alexander Medvedev 2c338f468c feat: add dragon ai
not perfect, bit buggy, but it works overall
2026-04-12 19:10:14 +02:00

227 lines
7.2 KiB
Rust

use crate::entity::item::ItemEntity;
use crate::entity::living::LivingEntity;
use crate::entity::player::Player;
use crate::{entity::EntityBaseFuture, server::Server};
use core::f64;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::sound::Sound;
use pumpkin_data::world::WorldEvent;
use pumpkin_data::{damage::DamageType, meta_data_type::MetaDataType, tracked_data::TrackedData};
use pumpkin_protocol::{
codec::item_stack_seralizer::ItemStackSerializer, java::client::play::Metadata,
};
use pumpkin_util::math::vector3::Vector3;
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU32, Ordering},
};
use tokio::sync::Mutex;
use super::{Entity, EntityBase, NBTStorage};
/// Maximum horizontal distance the eye will travel before signalling at an elevated height.
const TOO_FAR_DISTANCE: f64 = 12.0;
/// Height offset used when the target is beyond [`TOO_FAR_DISTANCE`].
const TOO_FAR_SIGNAL_HEIGHT: f64 = 8.0;
/// The eye despawns after this many ticks.
const MAX_LIFE: u32 = 80;
/// Chance (1-in-5) the eye drops its item instead of playing the break effect.
const SURVIVE_CHANCE: u32 = 5;
pub struct EyeOfEnder {
entity: Entity,
item_stack: Mutex<ItemStack>,
target: Mutex<Option<Vector3<f64>>>,
life: AtomicU32,
survive_after_death: AtomicBool,
}
impl EyeOfEnder {
pub fn new(entity: Entity) -> Self {
Self {
entity,
item_stack: Mutex::new(ItemStack::new(1, &Item::ENDER_EYE)),
target: Mutex::new(None),
life: AtomicU32::new(0),
survive_after_death: AtomicBool::new(false),
}
}
/// Aim the eye at `target`, clamping to [`TOO_FAR_DISTANCE`] if necessary,
/// and randomly decide whether it should drop its item on expiry.
pub async fn signal_to(&self, target: Vector3<f64>) {
let pos = self.entity.pos.load();
let delta = target.sub(&pos);
let horizontal_dist = delta.x.hypot(delta.z);
let clamped_target = if horizontal_dist > TOO_FAR_DISTANCE {
Vector3::new(
pos.x + delta.x / horizontal_dist * TOO_FAR_DISTANCE,
pos.y + TOO_FAR_SIGNAL_HEIGHT,
pos.z + delta.z / horizontal_dist * TOO_FAR_DISTANCE,
)
} else {
target
};
*self.target.lock().await = Some(clamped_target);
self.life.store(0, Ordering::Relaxed);
// 4-in-5 chance to survive (drop item); 1-in-5 plays the break effect.
let survive = !rand::random::<u32>().is_multiple_of(SURVIVE_CHANCE);
self.survive_after_death.store(survive, Ordering::Relaxed);
}
/// Compute the next delta-movement so the eye smoothly homes toward `target`.
///
/// Mirrors the vanilla `updateDeltaMovement` logic.
fn compute_new_velocity(
old_velo: Vector3<f64>,
position: Vector3<f64>,
target: Vector3<f64>,
) -> Vector3<f64> {
let horizontal_delta = Vector3::new(target.x - position.x, 0.0, target.z - position.z);
let horizontal_len = horizontal_delta.x.hypot(horizontal_delta.z);
let old_h_speed = old_velo.x.hypot(old_velo.z);
let mut wanted_speed = lerp(0.0025, old_h_speed, horizontal_len);
let mut move_y = old_velo.y;
if horizontal_len < 1.0 {
wanted_speed *= 0.8;
move_y *= 0.8;
}
let wanted_y_dir = if position.y - old_velo.y < target.y {
1.0f64
} else {
-1.0f64
};
let scale = wanted_speed / horizontal_len;
Vector3::new(
horizontal_delta.x * scale,
move_y + (wanted_y_dir - move_y) * 0.015,
horizontal_delta.z * scale,
)
}
}
/// Linear interpolation helper (`start + t * (end - start)`).
#[inline]
fn lerp(t: f64, start: f64, end: f64) -> f64 {
start + t * (end - start)
}
impl NBTStorage for EyeOfEnder {}
impl EntityBase for EyeOfEnder {
fn tick<'a>(
&'a self,
caller: Arc<dyn EntityBase>,
server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let entity = &self.entity;
entity.tick(caller, server).await;
// Advance position by current velocity.
let velocity = entity.velocity.load();
let new_pos = entity.pos.load().add(&velocity);
// Server-side: steer toward target if one is set.
{
let target_guard = self.target.lock().await;
if let Some(target) = *target_guard {
let new_velo = Self::compute_new_velocity(velocity, new_pos, target);
entity.velocity.store(new_velo);
}
}
entity.set_pos(new_pos);
entity.send_pos_rot().await;
// Tick lifetime and handle expiry.
let life = self.life.fetch_add(1, Ordering::Relaxed) + 1;
if life > MAX_LIFE {
entity.play_sound(Sound::EntityEnderEyeDeath).await;
entity.remove().await;
if self.survive_after_death.load(Ordering::Relaxed) {
// Drop the item at the current position.
let item_stack = self.item_stack.lock().await.clone();
let world = entity.world.load();
let entity = Entity::new(world.clone(), new_pos, &EntityType::ITEM);
let item_entity = Arc::new(ItemEntity::new(entity, item_stack).await);
world.spawn_entity(item_entity).await;
} else {
entity
.world
.load()
.sync_world_event(
WorldEvent::ParticlesEyeOfEnderDeath,
new_pos.to_block_pos(),
0,
)
.await;
}
}
})
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async {
self.entity
.send_meta_data(&[Metadata::new(
TrackedData::ITEM,
MetaDataType::ITEM_STACK,
&ItemStackSerializer::from(self.item_stack.lock().await.clone()),
)])
.await;
})
}
fn damage_with_context<'a>(
&'a self,
_caller: &'a dyn EntityBase,
_amount: f32,
_damage_type: DamageType,
_position: Option<Vector3<f64>>,
_source: Option<&'a dyn EntityBase>,
_cause: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
// Eye of Ender is not attackable.
Box::pin(async { false })
}
fn on_player_collision<'a>(&'a self, _player: &'a Arc<Player>) -> EntityBaseFuture<'a, ()> {
// Eye of Ender cannot be picked up.
Box::pin(async {})
}
fn get_entity(&self) -> &Entity {
&self.entity
}
fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
fn get_item_entity(self: Arc<Self>) -> Option<Arc<ItemEntity>> {
None
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
}