feat: ender pearl

This commit is contained in:
Alexander Medvedev 2026-04-16 21:46:18 +02:00
parent 9b2bd09dcc
commit d8a124cf92
7 changed files with 245 additions and 0 deletions

View File

@ -0,0 +1,137 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use crate::{
entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage, projectile::ThrownItemEntity},
server::Server,
};
use pumpkin_data::entity::EntityStatus;
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_util::math::vector3::Vector3;
pub struct EnderPearlEntity {
pub thrown: ThrownItemEntity,
}
impl EnderPearlEntity {
pub async fn new(entity: Entity) -> Self {
entity.set_velocity(Vector3::new(0.0, 0.1, 0.0)).await;
let thrown = ThrownItemEntity {
entity,
owner_id: None,
collides_with_projectiles: false,
has_hit: AtomicBool::new(false),
};
Self { thrown }
}
pub async fn new_shot(entity: Entity, shooter: &Entity) -> Self {
let thrown = ThrownItemEntity::new(entity, shooter);
thrown
.entity
.set_velocity(Vector3::new(0.0, 0.1, 0.0))
.await;
Self { thrown }
}
}
impl NBTStorage for EnderPearlEntity {}
impl EntityBase for EnderPearlEntity {
fn tick<'a>(
&'a self,
caller: Arc<dyn EntityBase>,
server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move { self.thrown.process_tick(caller, server).await })
}
fn get_entity(&self) -> &Entity {
self.thrown.get_entity()
}
fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> {
None
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
fn on_hit(&self, hit: crate::entity::projectile::ProjectileHit) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();
let world = entity.world.load();
// Spawn portal particles at hit position
let hit_pos = hit.hit_pos();
for _ in 0..32 {
let offset = Vector3::new(
rand::random::<f32>() as f64 - 0.5,
rand::random::<f32>() as f64 * 2.0,
rand::random::<f32>() as f64 - 0.5,
);
let speed = Vector3::new(
rand::random::<f64>() - 0.5,
0.0,
rand::random::<f64>() - 0.5,
);
world
.spawn_particle(
hit_pos.add(&offset),
Vector3::new(speed.x as f32, speed.y as f32, speed.z as f32),
1.0,
1,
Particle::Portal,
)
.await;
}
if let Some(owner_id) = self.thrown.owner_id
&& let Some(owner) = world.get_entity_by_id(owner_id)
{
// Teleport owner to pearl's position before hit (approx)
let teleport_pos = entity.pos.load();
// In vanilla, teleport handles everything including sound
owner
.clone()
.teleport(
teleport_pos,
Some(owner.get_entity().yaw.load()),
Some(owner.get_entity().pitch.load()),
world.clone(),
)
.await;
// Play teleport sound at new position
world
.play_sound(
Sound::EntityPlayerTeleport,
SoundCategory::Players,
&teleport_pos,
)
.await;
// Deal 5 damage to owner
owner
.damage(
owner.as_ref(),
5.0,
pumpkin_data::damage::DamageType::ENDER_PEARL,
)
.await;
}
world.send_entity_status(entity, EntityStatus::Death).await;
})
}
}

View File

@ -11,6 +11,7 @@ use std::{
};
pub mod arrow;
pub mod egg;
pub mod ender_pearl;
pub mod eye_of_ender;
pub mod firework_rocket;
pub mod lingering_potion;
@ -27,6 +28,7 @@ pub fn is_projectile(entity_type: &EntityType) -> bool {
|| *entity_type == EntityType::WIND_CHARGE
|| *entity_type == EntityType::SPLASH_POTION
|| *entity_type == EntityType::LINGERING_POTION
|| *entity_type == EntityType::ENDER_PEARL
}
pub struct ThrownItemEntity {

View File

@ -7,6 +7,9 @@ use uuid::Uuid;
use crate::entity::boss::ender_dragon::EnderDragonEntity;
use crate::entity::mob::blaze::BlazeEntity;
use crate::entity::mob::zombie::zombie_villager::ZombieVillagerEntity;
use crate::entity::projectile::egg::EggEntity;
use crate::entity::projectile::ender_pearl::EnderPearlEntity;
use crate::entity::projectile::snowball::SnowballEntity;
use crate::{
entity::{
Entity, EntityBase,
@ -80,6 +83,9 @@ pub async fn from_type(
id if id == EntityType::ARMOR_STAND.id => Arc::new(ArmorStandEntity::new(entity)),
id if id == EntityType::PAINTING.id => Arc::new(PaintingEntity::new(entity)),
id if id == EntityType::END_CRYSTAL.id => Arc::new(EndCrystalEntity::new(entity)),
id if id == EntityType::ENDER_PEARL.id => Arc::new(EnderPearlEntity::new(entity).await),
id if id == EntityType::SNOWBALL.id => Arc::new(SnowballEntity::new(entity).await),
id if id == EntityType::EGG.id => Arc::new(EggEntity::new(entity).await),
id if id == EntityType::SILVERFISH.id => SilverfishEntity::new(entity).await,
id if id == EntityType::SLIME.id => SlimeEntity::new(entity),
// Fallback Entity

View File

@ -51,6 +51,19 @@ impl ItemBehaviour for EggItem {
egg.thrown
.set_velocity_from(&player.living_entity.entity, pitch, yaw, 0.0, POWER, 1.0);
world.spawn_entity(Arc::new(egg)).await;
// Consume item
let held_item = player.inventory.held_item();
let mut main_hand = held_item.lock().await;
if !main_hand.is_empty() && Self::ids().contains(&main_hand.item.id) {
main_hand.decrement_unless_creative(player.gamemode.load(), 1);
} else {
let off_hand_item = player.inventory.off_hand_item().await;
let mut off_hand = off_hand_item.lock().await;
if !off_hand.is_empty() && Self::ids().contains(&off_hand.item.id) {
off_hand.decrement_unless_creative(player.gamemode.load(), 1);
}
}
})
}

View File

@ -0,0 +1,71 @@
use std::pin::Pin;
use std::sync::Arc;
use crate::entity::Entity;
use crate::entity::player::Player;
use crate::entity::projectile::ender_pearl::EnderPearlEntity;
use crate::item::{ItemBehaviour, ItemMetadata};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::sound::Sound;
pub struct EnderPearlItem;
impl ItemMetadata for EnderPearlItem {
fn ids() -> Box<[u16]> {
[Item::ENDER_PEARL.id].into()
}
}
const POWER: f32 = 1.5;
impl ItemBehaviour for EnderPearlItem {
fn normal_use<'a>(
&'a self,
_item: &'a Item,
player: &'a Player,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async move {
let position = player.position();
let world = player.world();
world
.play_sound(
Sound::EntityEnderPearlThrow,
pumpkin_data::sound::SoundCategory::Neutral,
&position,
)
.await;
let entity = Entity::new(world.clone(), position, &EntityType::ENDER_PEARL);
let pearl = EnderPearlEntity::new_shot(entity, &player.living_entity.entity).await;
let yaw = player.living_entity.entity.yaw.load();
let pitch = player.living_entity.entity.pitch.load();
pearl.thrown.set_velocity_from(
&player.living_entity.entity,
pitch,
yaw,
0.0,
POWER,
1.0,
);
world.spawn_entity(Arc::new(pearl)).await;
// Consume item
let held_item = player.inventory.held_item();
let mut main_hand = held_item.lock().await;
if !main_hand.is_empty() && main_hand.item.id == Item::ENDER_PEARL.id {
main_hand.decrement_unless_creative(player.gamemode.load(), 1);
} else {
let off_hand_item = player.inventory.off_hand_item().await;
let mut off_hand = off_hand_item.lock().await;
if !off_hand.is_empty() && off_hand.item.id == Item::ENDER_PEARL.id {
off_hand.decrement_unless_creative(player.gamemode.load(), 1);
}
}
})
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}

View File

@ -8,6 +8,7 @@ pub mod dye;
pub mod egg;
pub mod end_crystal;
pub mod ender_eye;
pub mod ender_pearl;
pub mod firework_rocket;
pub mod glowing_ink_sac;
pub mod hoe;
@ -43,6 +44,7 @@ use bucket::{EmptyBucketItem, FilledBucketItem};
use dye::DyeItem;
use egg::EggItem;
use ender_eye::EnderEyeItem;
use ender_pearl::EnderPearlItem;
use glowing_ink_sac::GlowingInkSacItem;
use hoe::HoeItem;
use honeycomb::HoneyCombItem;
@ -82,6 +84,7 @@ pub fn default_registry() -> Arc<ItemRegistry> {
manager.register(HoneyCombItem);
manager.register(NameTagItem);
manager.register(EnderEyeItem);
manager.register(EnderPearlItem);
manager.register(FireChargeItem);
manager.register(DyeItem);
manager.register(FireworkRocketItem);

View File

@ -48,6 +48,19 @@ impl ItemBehaviour for SnowBallItem {
1.0,
);
world.spawn_entity(Arc::new(snowball)).await;
// Consume item
let held_item = player.inventory.held_item();
let mut main_hand = held_item.lock().await;
if !main_hand.is_empty() && main_hand.item.id == Item::SNOWBALL.id {
main_hand.decrement_unless_creative(player.gamemode.load(), 1);
} else {
let off_hand_item = player.inventory.off_hand_item().await;
let mut off_hand = off_hand_item.lock().await;
if !off_hand.is_empty() && off_hand.item.id == Item::SNOWBALL.id {
off_hand.decrement_unless_creative(player.gamemode.load(), 1);
}
}
})
}